Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort items by urgency #57

Closed
wants to merge 23 commits into from
Closed

Sort items by urgency #57

wants to merge 23 commits into from

Conversation

kweeuhree
Copy link
Collaborator

@kweeuhree kweeuhree commented Sep 28, 2024

Description

This code proposes a way to sort items using sortByUrgency function and urgency state. The urgency status is passed to each item as a className.
I am using colors and borders to visualize the urgency, but it is a temporary solution:
overdue - dotted white border,
soon - green solid;
kindOfSoon - yellow thick border;
notSoon - solid blue thin border;
inactive - dashed red border;

The code does not pass tests, I do not know why. I faked return mock values, as it seems that I was not able to read dates.js module.

What isn't good:

  • I am relying on a global variable urgencyObject that holds the details temporarily before updating state with the details. Probably should be wrapped in a hook.
  • Inactivity of items is being hardcoded.
  • Sorting alphabetically doesn't work as expected.

Type of Changes

enhancement

Updates

After

Screenshot 2024-09-29 090307

Testing Steps / QA Criteria

Copious console logs show that items do get sorted

Screenshot 2024-09-29 090748

@@ -15,7 +15,7 @@
"settings": { "react": { "version": "detect" } },
"rules": {
"no-duplicate-imports": "warn",
"no-unused-vars": "warn",
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"react/jsx-filename-extension": [1, { "allow": "as-needed" }],
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was my solution as I had to loop over sets and didn't know how to avoid this here:

	const getClassName = (name) => {
		const statusArray = Object.entries(urgency).find(([_, items]) => {
			return Array.from(items).some((item) => item.name === name);
		});
		if (!statusArray) {
			throw new Error(`Failed to get class name of ${name}`);
		}
		return statusArray[0];
	};

Copy link

Visit the preview URL for this PR (updated for commit c92e940):

https://tcl-75-smart-shopping-list--pr57-sort-by-urgency-jwz2o8i8.web.app

(expires Sat, 05 Oct 2024 14:08:38 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 1f1fd53c369e1fa31e15c310aa075b4e8f4f8dde

@@ -221,3 +225,74 @@ export async function deleteItem(listPath, itemId) {
throw new Error(`Failed updating item: ${error.message}`);
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global urgency object will be populated with details and later set urgency state within List component

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like leveraging context would be a great approach if the urgencyObject needs to be accessible across different components. Could we create a context for this and remove it from this file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was in firebase.js as per acceptance criteria, but the logic for useUrgency is inside hooks.js!

- Updated `getItemUrgency` to calculate item urgency based on the time until the next purchase date in addition to the last purchase or creation date.
- If more than 60 days have passed since the last purchase or creation, the item is considered inactive and assigned an urgency of 100.
- If fewer than 60 days have passed, urgency is determined by the number of days until the next estimated purchase date.
Comment on lines +247 to +269
const sortByUrgency = (item, daysUntilNextPurchase) => {
if (item.name.includes('sun')) {
console.log(`${item.name} urgencyStatus ${daysUntilNextPurchase}`);
}
if (daysUntilNextPurchase < 0) {
urgencyObject.overdue.add(item);
return;
} else if (daysUntilNextPurchase === 1000) {
urgencyObject.inactive.add(item);
return;
} else if (daysUntilNextPurchase < 7) {
urgencyObject.soon.add(item);
return;
} else if (daysUntilNextPurchase >= 7 && daysUntilNextPurchase < 30) {
urgencyObject.kindOfSoon.add(item);
return;
} else if (daysUntilNextPurchase >= 30) {
urgencyObject.notSoon.add(item);
return;
} else {
throw new Error(`Failed to place [${item.name}]`);
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears to be purely client-side and doesn't interact with Firebase. Could we move it to a utility file and add a test to ensure its logic is covered?

Comment on lines +282 to +311
const getItemUrgency = (item) => {
// get date the item was purchased or created
const itemDate = getDateLastPurchasedOrDateCreated(
item.dateLastPurchased,
item.dateCreated,
);
// check how many days have passed since that date
const daysToToday = getDaysFromDate(itemDate);

// if more than 60 days have passed
if (daysToToday >= 60) {
// sort as inactive
sortByUrgency(item, 1000);
return 1000;
} else {
// sort by the amount of days until next purchase date
const daysUntilNextPurchase = getDaysBetweenDates(
new Date(),
item.dateNextPurchased.toDate(),
);
if (item.name.includes('sun')) {
console.log(
`${item.name} days until next purchase is ${daysUntilNextPurchase}`,
);
}
sortByUrgency(item, daysUntilNextPurchase);

return daysUntilNextPurchase;
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears to be purely client-side and doesn't interact with Firebase. Could we move it to a utility file and add a test to ensure its logic is covered?

Comment on lines +322 to +332
export function comparePurchaseUrgency(item1, item2) {
const item1UrgencyStatus = getItemUrgency(item1);
const item2UrgencyStatus = getItemUrgency(item2);

if (item1UrgencyStatus === item2UrgencyStatus) {
console.log(`Sorting alphabetically: ${item1.name} vs ${item2.name}`);
return item1.name.localeCompare(item2.name);
}
// otherwise sort in descending order
return item1UrgencyStatus - item2UrgencyStatus;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears to be purely client-side and doesn't interact with Firebase. Could we move it to a utility file and add a test to ensure its logic is covered?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add tests!

@@ -17,7 +17,7 @@ const calculateIsPurchased = (dateLastPurchased) => {
return currentDate < oneDayLater;
};

export function ListItem({ item, listPath }) {
export function ListItem({ item, listPath, className }) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're trying to do here! Could I suggest a cleaner approach? Instead of passing a className, we should pass an urgencyStatus prop. This way, the className can be determined internally based on the urgencyStatus, keeping the styling logic encapsulated within the ListItem component. This approach is more semantic, makes the component more reusable, and keeps the parent component simpler.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I am adding this as a change to useUrgency and use-urgency branch

Comment on lines +59 to +60
<li className={`ListItem`}>
<div className={`urgency-status ${className}`} />
Copy link
Collaborator

@ahsanatzapier ahsanatzapier Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you've passed the urgencyStatus prop, you can use a switch statement like the one below to determine the appropriate class for rendering:

const getUrgencyClass = (urgencyStatus) => {
  switch (urgencyStatus) {
    case 'overdue':
      return 'urgency-overdue';
    case 'soon':
      return 'urgency-soon';
    case 'kindOfSoon':
      return 'urgency-kind-of-soon';
    case 'notSoon':
      return 'urgency-not-soon';
    case 'inactive':
      return 'urgency-inactive';
    default:
      return '';
  }
};

Then, in the JSX:

<li className="ListItem">
  <div className={`urgency-status ${getUrgencyClass(urgencyStatus)}`} />
</li>

As a bonus, you could even make getUrgencyClass into its own util to keep the code in this file cleaner!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand, do we just add 'urgency-' to the class with this function? Is is about camel case? Or would it return a ui piece in the future?

Comment on lines +62 to +66
export const getDaysFromDate = (pastDate) => {
const today = new Date();
const daysToToday = getDaysBetweenDates(pastDate, today);
return daysToToday;
};
Copy link
Collaborator

@ahsanatzapier ahsanatzapier Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can further optimize this by simplifying the function to:

export const getDaysFromDate = (pastDate) => getDaysBetweenDates(pastDate, new Date());

Comment on lines +69 to +75
export const getDateLastPurchasedOrDateCreated = (
dateLastPurchased,
dateCreated,
) => {
const lastPurchaseDate = dateLastPurchased?.toDate();
return lastPurchaseDate ?? dateCreated.toDate();
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can further optimize this by simplifying the function to:

export const getDateLastPurchasedOrDateCreated = (dateLastPurchased, dateCreated) => dateLastPurchased?.toDate() ?? dateCreated.toDate();

Comment on lines +9 to +24
const [urgency, setUrgency] = useState(urgencyObject);
const listName = listPath.slice(listPath.indexOf('/') + 1);

useEffect(() => {
setUrgency(urgencyObject);
}, [urgencyObject]);

const getClassName = (name) => {
const statusArray = Object.entries(urgency).find(([_, items]) => {
return Array.from(items).some((item) => item.name === name);
});
if (!statusArray) {
throw new Error(`Failed to get class name of ${name}`);
}
return statusArray[0];
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic will likely be replaced when we start passing the urgencyStatus prop directly to the component, removing the need for managing urgency locally.

Comment on lines +29 to +40
console.log('----');
console.log(`before filter: ${data.map((d) => d.name)}`);
const filteredItems = data
.filter((item) =>
item.name.toLowerCase().includes(searchItem.toLowerCase()),
)
.sort(comparePurchaseUrgency);

const filteredItems = data.filter((item) =>
item.name.toLowerCase().includes(searchItem.toLowerCase()),
);

const listName = listPath.slice(listPath.indexOf('/') + 1);
console.log(`filter: ${filteredItems.map((d) => d.name)}`);
console.log('----');
console.log(`Urgency state object:`);
console.log(urgency);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need to keep these console logs in production, can we put them behind a feature flag controlled by a query parameter in the URL? Perhaps something like ?debug=on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console logs are just to make testing more visual, I will remove before merging!

key={item.id}
item={item}
listPath={listPath}
className={itemClassName}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be replaced by passing the urgencyStatus prop!

@@ -1,15 +1,29 @@
import { render, screen } from '@testing-library/react';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the test!

Comment on lines +3 to +7
describe('Compare Purchase Urgency', () => {
test('it works', () => {
expect(true).toBe(true);
});
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a placeholder. Could we remove it for now and add a meaningful test for the comparePurchaseUrgency function once it's ready?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it was very much a prototype, I will work on creating proper tests

@@ -15,7 +15,7 @@
"settings": { "react": { "version": "detect" } },
"rules": {
"no-duplicate-imports": "warn",
"no-unused-vars": "warn",
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the reasoning behind this change! Are there specific cases where we're intentionally using unused variables or function parameters prefixed with an underscore

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this for the urgency hook! but this was my original getClassName function where I didnt know how to avoid an unused variable when looping over items within a set. In the urgency hook I managed though!
original reason:

	const getClassName = (name) => {
		const statusArray = Object.entries(urgency).find(([_, items]) => {
			return Array.from(items).some((item) => item.name === name);
		});
		if (!statusArray) {
			throw new Error(`Failed to get class name of ${name}`);
		}
		return statusArray[0];
	};

@kweeuhree kweeuhree closed this Oct 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants