From 6950a978ff223641437861ca4dbc26d627b12dc1 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:00:04 -0400 Subject: [PATCH 01/21] feat: rename DeleteIconWithTooltip into IconWithTooltip to make the component even more reusable; implement IconWithTooltip and InfoCard in ListItem component; update the way props are being passed into IconWithTooltip --- src/components/DeleteIconWithTooltip.jsx | 18 ---- src/components/IconWithTooltip.jsx | 27 ++++++ src/components/ListItem.jsx | 118 ++++++++++++++--------- 3 files changed, 102 insertions(+), 61 deletions(-) delete mode 100644 src/components/DeleteIconWithTooltip.jsx create mode 100644 src/components/IconWithTooltip.jsx diff --git a/src/components/DeleteIconWithTooltip.jsx b/src/components/DeleteIconWithTooltip.jsx deleted file mode 100644 index 1d46c02..0000000 --- a/src/components/DeleteIconWithTooltip.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { DeleteOutlineOutlined } from '@mui/icons-material'; -import { Tooltip, IconButton } from '@mui/material'; - -export const tooltipStyle = { - fontSize: '1.5rem', - marginBlockStart: '0', - marginBlockEnd: '0', -}; - -export const DeleteIconWithTooltip = ({ ariaLabel, toggleDialog }) => { - return ( - Delete

} placement="right" arrow> - - - -
- ); -}; diff --git a/src/components/IconWithTooltip.jsx b/src/components/IconWithTooltip.jsx new file mode 100644 index 0000000..6c6af26 --- /dev/null +++ b/src/components/IconWithTooltip.jsx @@ -0,0 +1,27 @@ +import { Tooltip, IconButton, Box } from '@mui/material'; + +export const tooltipStyle = { + fontSize: '1.5rem', + marginBlockStart: '0', + marginBlockEnd: '0', +}; + +export const IconWithTooltip = ({ + icon, + onClick, + ariaLabel, + title, + placement, +}) => { + return ( + {title}} + placement={placement} + arrow + > + + {icon} + + + ); +}; diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx index c1220a0..284578d 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.jsx @@ -4,11 +4,11 @@ import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils'; import { toast } from 'react-toastify'; import { useConfirmDialog } from '../hooks/useConfirmDialog'; import { ConfirmDialog } from './ConfirmDialog'; -import { DeleteIconWithTooltip, tooltipStyle } from './DeleteIconWithTooltip'; +import { InfoCard } from './InfoCard'; +import { IconWithTooltip, tooltipStyle } from './IconWithTooltip'; import { ListItem as MaterialListItem, Tooltip, - IconButton, ListItemButton, ListItemIcon, ListItemText, @@ -20,6 +20,8 @@ import { RadioButtonUnchecked as KindOfSoonIcon, RemoveCircle as NotSoonIcon, RadioButtonChecked as InactiveIcon, + MoreHoriz, + DeleteOutlineOutlined, } from '@mui/icons-material'; import './ListItem.css'; @@ -34,7 +36,7 @@ const urgencyStatusIcons = { inactive: InactiveIcon, }; -const urgencyStatusStyle = { +const largeWhiteFontStyle = { fontSize: '2.5rem', color: 'white', }; @@ -59,6 +61,7 @@ const calculateIsPurchased = (dateLastPurchased) => { export function ListItem({ item, listPath, itemUrgencyStatus }) { const { open, isOpen, toggleDialog } = useConfirmDialog(); + const [showCard, setShowCard] = useState(false); const [isPurchased, setIsPurchased] = useState(() => calculateIsPurchased(item.dateLastPurchased), ); @@ -86,7 +89,6 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { }; const handleDeleteItem = async () => { - console.log('attempting item deletion'); try { await deleteItem(listPath, id); toast.success('Item deleted'); @@ -96,13 +98,40 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { return; }; + const toggleMoreInformation = () => { + setShowCard((prev) => !prev); + }; + const UrgencyStatusIcon = urgencyStatusIcons[itemUrgencyStatus]; - const props = { + const urgencyIconProps = { + icon: , + ariaLabel: { itemUrgencyStatus }, + title:

{itemUrgencyStatus}

, + placement: 'left', + }; + + const deleteIconProps = { + icon: , + onClick: toggleDialog, + ariaLabel: 'Delete item', + title: 'Delete', + placement: 'left', + }; + + const moreInformationProps = { + icon: , + onClick: toggleMoreInformation, + ariaLabel: 'More information', + title: 'More information', + placement: 'right', + }; + + const confirmDialogProps = { handleDelete: handleDeleteItem, title: `Are you sure you want to delete ${name}?`, setOpen: isOpen, - open: open, + open, }; const tooltipTitle = isPurchased @@ -111,46 +140,49 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { return ( <> - {open && } + {open && } - {UrgencyStatusIcon && ( - {itemUrgencyStatus}

} - placement="left" - arrow - > - - - -
- )} - - - {tooltipTitle}

} - placement="left" - arrow - > - -
-
- -
+ ) : ( + <> + {UrgencyStatusIcon && } + + + + {tooltipTitle}

} + placement="left" + arrow + > + +
+
+ + +
+ + {/* delete icon */} + - + {/* more information icon */} + + + )}
); From 65f72d0ad297946b0da643550a9dfd16cc3bb041 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:00:54 -0400 Subject: [PATCH 02/21] feat: InfoCard component displays additional information about a selected list item --- src/components/InfoCard.jsx | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/components/InfoCard.jsx diff --git a/src/components/InfoCard.jsx b/src/components/InfoCard.jsx new file mode 100644 index 0000000..96201f2 --- /dev/null +++ b/src/components/InfoCard.jsx @@ -0,0 +1,58 @@ +import { + Card, + CardContent, + CardHeader, + Typography, + IconButton, + Grow, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; + +export const InfoCard = ({ item, toggleCard, show }) => { + const typographyOptions = { + totalPurchases: `Total purchases: ${item.totalPurchases}`, + dateCreated: `Date created: ${item.dateCreated?.toDate().toLocaleString()}`, + dateLastPurchased: `Date last purchased: ${item.dateLastPurchased?.toDate().toLocaleString() ?? 'none yet'}`, + dateNextPurchased: `Date next purchased: ${item.dateNextPurchased?.toDate().toLocaleString() ?? 'none yet'}`, + }; + + return ( + + + + ({ + position: 'absolute', + right: 10, + top: 10, + color: theme.palette.grey[700], + })} + > + + + + {Object.entries(typographyOptions).map(([key, option]) => ( + + {option} + + ))} + + + + ); +}; From 22324f875c42dec0352fd25d480e7515a94080a6 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:02:09 -0400 Subject: [PATCH 03/21] fix: update the mapping over daysUntilPurchaseOptions to be more readable --- src/components/AddItems.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/AddItems.jsx b/src/components/AddItems.jsx index 7b0d01a..93607c6 100644 --- a/src/components/AddItems.jsx +++ b/src/components/AddItems.jsx @@ -20,7 +20,7 @@ export function AddItems({ items }) { const daysUntilNextPurchase = event.target.elements['purchase-date'].value; - + console.log(daysUntilNextPurchase, 'days until next purchase'); const itemName = event.target.elements['item-name'].value; try { @@ -69,13 +69,13 @@ export function AddItems({ items }) { required={true} /> - {Object.entries(daysUntilPurchaseOptions).map(([key, value]) => ( + {Object.entries(daysUntilPurchaseOptions).map(([status, days]) => ( ))} From 8b6642c96bb06bd4f9bff89bf503c4cb5d31bb38 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:02:56 -0400 Subject: [PATCH 04/21] feat: implement IconWithTooltip in SingleList component --- src/components/SingleList.jsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/SingleList.jsx b/src/components/SingleList.jsx index f8e8ae7..dc3771d 100644 --- a/src/components/SingleList.jsx +++ b/src/components/SingleList.jsx @@ -1,13 +1,17 @@ import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; import { toast } from 'react-toastify'; -import { PushPin, PushPinOutlined } from '@mui/icons-material'; +import { + PushPin, + PushPinOutlined, + DeleteOutlineOutlined, +} from '@mui/icons-material'; import { Tooltip, IconButton, Button } from '@mui/material'; import { deleteList } from '../api'; import { useAuth } from '../hooks'; import { useConfirmDialog } from '../hooks/useConfirmDialog'; import { ConfirmDialog } from './ConfirmDialog'; -import { tooltipStyle, DeleteIconWithTooltip } from './DeleteIconWithTooltip'; +import { tooltipStyle, IconWithTooltip } from './IconWithTooltip'; import './SingleList.css'; const deletionResponse = { @@ -69,7 +73,7 @@ export function SingleList({ handleDelete, title: `Are you sure you want to delete ${name}?`, setOpen: isOpen, - open: open, + open, }; const importantStatusLabel = isImportant ? 'Unpin list' : 'Pin list'; @@ -104,9 +108,14 @@ export function SingleList({ {name} - + } ariaLabel="Delete list" - toggleDialog={toggleDialog} + onClick={toggleDialog} + title="Delete" + placement="right" /> From 928994969cd22b67dc3c2af59a46c2ae1af78325 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:03:40 -0400 Subject: [PATCH 05/21] fix: update exports from components folder --- src/components/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/index.js b/src/components/index.js index b1b83c6..5b48ccf 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -4,4 +4,5 @@ export * from './AddItems'; export * from './TextInputElement'; export * from './RadioInputElement'; export * from './ConfirmDialog'; -export * from './DeleteIconWithTooltip'; +export * from './IconWithTooltip'; +export * from './InfoCard'; From 134ba4fa2029cbfc82b455b1663ddb07be2bf91a Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Wed, 16 Oct 2024 21:06:39 -0400 Subject: [PATCH 06/21] test: update List test --- tests/List.test.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index 663f774..42d0e95 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -12,6 +12,7 @@ import { vi.mock('../src/hooks', () => ({ useEnsureListPath: vi.fn(), useStateWithStorage: vi.fn(), + useEnsureListPath: vi.fn(), useUrgency: vi.fn(() => ({ getUrgency: vi.fn((name) => { if (name === 'nutella') return 'soon'; From 927d0cdff582a7c7d67e546c62a7ca4bec37d8cc Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:37:34 -0400 Subject: [PATCH 07/21] feat: update ListItem - move calculateIsPurchased into a utils folder; - rename urgency statses kindOfSoon to "kind of soon" and notSoon to "not soon"; - update UrgencyStatusIcon to be a component of its own, without using IconWithTooltip, as it was causing errors; - use IconWithTooltip to display Delete and More Information icons. --- src/components/ListItem.jsx | 55 +++++++++++-------------------- src/utils/calculateIsPurchased.js | 13 ++++++++ src/utils/urgencyUtils.js | 4 +-- tests/sortByUrgency.test.js | 8 ++--- 4 files changed, 39 insertions(+), 41 deletions(-) create mode 100644 src/utils/calculateIsPurchased.js diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx index 284578d..537aa07 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.jsx @@ -1,11 +1,14 @@ import { useState } from 'react'; import { updateItem, deleteItem } from '../api'; -import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils'; +import { calculateDateNextPurchased, calculateIsPurchased } from '../utils'; import { toast } from 'react-toastify'; import { useConfirmDialog } from '../hooks/useConfirmDialog'; -import { ConfirmDialog } from './ConfirmDialog'; -import { InfoCard } from './InfoCard'; -import { IconWithTooltip, tooltipStyle } from './IconWithTooltip'; +import { + IconWithTooltip, + tooltipStyle, + InfoCard, + ConfirmDialog, +} from './index'; import { ListItem as MaterialListItem, Tooltip, @@ -26,13 +29,11 @@ import { import './ListItem.css'; -const currentDate = new Date(); - const urgencyStatusIcons = { overdue: OverdueIcon, soon: SoonIcon, - kindOfSoon: KindOfSoonIcon, - notSoon: NotSoonIcon, + 'kind of soon': KindOfSoonIcon, + 'not soon': NotSoonIcon, inactive: InactiveIcon, }; @@ -41,29 +42,13 @@ const largeWhiteFontStyle = { color: 'white', }; -const toolTipStyle = { - fontSize: '1.5rem', - marginBlockStart: '0', - marginBlockEnd: '0', -}; - -const calculateIsPurchased = (dateLastPurchased) => { - if (!dateLastPurchased) { - return false; - } - const purchaseDate = dateLastPurchased.toDate(); - const oneDayLater = new Date( - purchaseDate.getTime() + ONE_DAY_IN_MILLISECONDS, - ); - - return currentDate < oneDayLater; -}; - export function ListItem({ item, listPath, itemUrgencyStatus }) { const { open, isOpen, toggleDialog } = useConfirmDialog(); const [showCard, setShowCard] = useState(false); + + const currentDate = new Date(); const [isPurchased, setIsPurchased] = useState(() => - calculateIsPurchased(item.dateLastPurchased), + calculateIsPurchased(item.dateLastPurchased, currentDate), ); const { name, id } = item; @@ -104,13 +89,6 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { const UrgencyStatusIcon = urgencyStatusIcons[itemUrgencyStatus]; - const urgencyIconProps = { - icon: , - ariaLabel: { itemUrgencyStatus }, - title:

{itemUrgencyStatus}

, - placement: 'left', - }; - const deleteIconProps = { icon: , onClick: toggleDialog, @@ -150,7 +128,14 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { /> ) : ( <> - {UrgencyStatusIcon && } + {UrgencyStatusIcon && ( + {itemUrgencyStatus}

} + /> + )} diff --git a/src/utils/calculateIsPurchased.js b/src/utils/calculateIsPurchased.js new file mode 100644 index 0000000..22ca23d --- /dev/null +++ b/src/utils/calculateIsPurchased.js @@ -0,0 +1,13 @@ +import { ONE_DAY_IN_MILLISECONDS } from './dates'; + +export const calculateIsPurchased = (dateLastPurchased, currentDate) => { + if (!dateLastPurchased) { + return false; + } + const purchaseDate = dateLastPurchased.toDate(); + const oneDayLater = new Date( + purchaseDate.getTime() + ONE_DAY_IN_MILLISECONDS, + ); + + return currentDate < oneDayLater; +}; diff --git a/src/utils/urgencyUtils.js b/src/utils/urgencyUtils.js index d1aede2..a198fb9 100644 --- a/src/utils/urgencyUtils.js +++ b/src/utils/urgencyUtils.js @@ -10,9 +10,9 @@ export const sortByUrgency = (item, daysUntilNextPurchase) => { } else if (daysUntilNextPurchase < 7) { return 'soon'; } else if (daysUntilNextPurchase >= 7 && daysUntilNextPurchase < 30) { - return 'kindOfSoon'; + return 'kind of soon'; } else if (daysUntilNextPurchase >= 30) { - return 'notSoon'; + return 'not soon'; } else { throw new Error(`Failed to place [${item.name}]`); } diff --git a/tests/sortByUrgency.test.js b/tests/sortByUrgency.test.js index ef18092..d064857 100644 --- a/tests/sortByUrgency.test.js +++ b/tests/sortByUrgency.test.js @@ -16,18 +16,18 @@ describe('sortByUrgency', () => { expect(result).toBe('soon'); }); - it('should return "kindOfSoon" if daysUntilNextPurchase is between 7 and 30', () => { + it('should return "kind of soon" if daysUntilNextPurchase is between 7 and 30', () => { const item = { name: 'Jam' }; const daysUntilNextPurchase = 15; const result = sortByUrgency(item, daysUntilNextPurchase); - expect(result).toBe('kindOfSoon'); + expect(result).toBe('kind of soon'); }); - it('should return "notSoon" if daysUntilNextPurchase is 30 or more', () => { + it('should return "not soon" if daysUntilNextPurchase is 30 or more', () => { const item = { name: 'Nutella' }; const daysUntilNextPurchase = 30; const result = sortByUrgency(item, daysUntilNextPurchase); - expect(result).toBe('notSoon'); + expect(result).toBe('not soon'); }); it('should throw an error if daysUntilNextPurchase cannot be classified', () => { From 5ca6136685f9f9945a1d0011b3a673f42d3ff4f1 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:39:37 -0400 Subject: [PATCH 08/21] fix: remove redundant console.log --- src/components/AddItems.jsx | 2 +- src/components/IconWithTooltip.jsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AddItems.jsx b/src/components/AddItems.jsx index 93607c6..89f73cf 100644 --- a/src/components/AddItems.jsx +++ b/src/components/AddItems.jsx @@ -20,7 +20,7 @@ export function AddItems({ items }) { const daysUntilNextPurchase = event.target.elements['purchase-date'].value; - console.log(daysUntilNextPurchase, 'days until next purchase'); + const itemName = event.target.elements['item-name'].value; try { diff --git a/src/components/IconWithTooltip.jsx b/src/components/IconWithTooltip.jsx index 6c6af26..125057d 100644 --- a/src/components/IconWithTooltip.jsx +++ b/src/components/IconWithTooltip.jsx @@ -6,13 +6,13 @@ export const tooltipStyle = { marginBlockEnd: '0', }; -export const IconWithTooltip = ({ +export function IconWithTooltip({ icon, onClick, ariaLabel, title, placement, -}) => { +}) { return ( {title}} @@ -24,4 +24,4 @@ export const IconWithTooltip = ({ ); -}; +} From 4b83bd5841f4d04db9c8773ac990824822f11c5e Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:40:16 -0400 Subject: [PATCH 09/21] update utils folder exports --- src/utils/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/index.js b/src/utils/index.js index bf31387..3a035ec 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -2,3 +2,4 @@ export * from './dates'; export * from './normalize'; export * from './urgencyUtils'; export * from './importanceUtils'; +export * from './calculateIsPurchased'; From 497db6db60935583d980e9d1450ba824b848b1f8 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:41:09 -0400 Subject: [PATCH 10/21] fix: correct kindOfSoon and notSoon statuses inside useUrgency hook --- src/hooks/useUrgency.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useUrgency.js b/src/hooks/useUrgency.js index aedfaea..7fa1942 100644 --- a/src/hooks/useUrgency.js +++ b/src/hooks/useUrgency.js @@ -5,8 +5,8 @@ export function useUrgency(items) { const [urgencyObject, setUrgencyObject] = useState({ overdue: new Set(), soon: new Set(), - kindOfSoon: new Set(), - notSoon: new Set(), + 'kind of soon': new Set(), + 'not soon': new Set(), inactive: new Set(), }); @@ -16,8 +16,8 @@ export function useUrgency(items) { let initialUrgencyState = { overdue: new Set(), soon: new Set(), - kindOfSoon: new Set(), - notSoon: new Set(), + 'kind of soon': new Set(), + 'not soon': new Set(), inactive: new Set(), }; From fd0f513652dd82bb8ab065ea609c5d62a64652d8 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:42:26 -0400 Subject: [PATCH 11/21] feat: update the way Typography component is displayed; change Grow transition to Collapse --- src/components/InfoCard.jsx | 83 ++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/components/InfoCard.jsx b/src/components/InfoCard.jsx index 96201f2..46e2349 100644 --- a/src/components/InfoCard.jsx +++ b/src/components/InfoCard.jsx @@ -1,58 +1,57 @@ import { + Box, Card, CardContent, CardHeader, Typography, IconButton, - Grow, + Collapse, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; export const InfoCard = ({ item, toggleCard, show }) => { const typographyOptions = { - totalPurchases: `Total purchases: ${item.totalPurchases}`, - dateCreated: `Date created: ${item.dateCreated?.toDate().toLocaleString()}`, - dateLastPurchased: `Date last purchased: ${item.dateLastPurchased?.toDate().toLocaleString() ?? 'none yet'}`, - dateNextPurchased: `Date next purchased: ${item.dateNextPurchased?.toDate().toLocaleString() ?? 'none yet'}`, + totalPurchases: `You've purchased this item ${item.totalPurchases} times`, + dateCreated: `Item added on: ${item.dateCreated?.toDate().toLocaleString()}`, + dateLastPurchased: `Last bought on: ${item.dateLastPurchased?.toDate().toLocaleString() ?? 'Not purchased yet'}`, + dateNextPurchased: `Expected to buy again by: ${item.dateNextPurchased?.toDate().toLocaleString() ?? 'No estimate yet'}`, }; return ( - - - - ({ - position: 'absolute', - right: 10, - top: 10, - color: theme.palette.grey[700], - })} - > - - - - {Object.entries(typographyOptions).map(([key, option]) => ( - - {option} - - ))} - - - + + + + {item?.name}} + action={ + ({ + position: 'absolute', + right: 20, + top: 15, + color: theme.palette.grey[700], + })} + > + + + } + /> + + {Object.entries(typographyOptions).map(([key, option]) => ( + + {option} + + ))} + + + + ); }; From 27664fca38c376d0f267a8c302a40d72ec5e62eb Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:43:04 -0400 Subject: [PATCH 12/21] test: update List test to mock calculateIsPurchased --- tests/List.test.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index 42d0e95..494031d 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -34,6 +34,7 @@ vi.mock('../src/utils', () => ({ getDateLastPurchasedOrDateCreated: vi.fn(), getDaysFromDate: vi.fn(), getDaysBetweenDates: vi.fn(), + calculateIsPurchased: vi.fn(), })); beforeEach(() => { From 9726593b2485e777806560432d714994654d65b6 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:43:49 -0400 Subject: [PATCH 13/21] test: introduce ListItem test that ensures that additional information is being displayed upon clicking More Information button --- tests/ListItem.test.jsx | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/ListItem.test.jsx diff --git a/tests/ListItem.test.jsx b/tests/ListItem.test.jsx new file mode 100644 index 0000000..7b6ff62 --- /dev/null +++ b/tests/ListItem.test.jsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { userEvent } from '@testing-library/user-event'; +import { ListItem } from '../src/components/ListItem'; +import { mockShoppingListData } from '../src/mocks/__fixtures__/shoppingListData'; + +describe('ListItem Component', () => { + test('displays additional item information if More Information button is clicked', async () => { + render( + + + , + ); + + const moreInformationIcon = screen.getByTestId('MoreHorizIcon'); + await userEvent.click(moreInformationIcon); + + expect(screen.getByText(mockShoppingListData[1].name)).toBeInTheDocument(); + expect( + screen.getByText( + `Item added on: ${mockShoppingListData[1].dateCreated.toDate().toLocaleString()}`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `Last bought on: ${mockShoppingListData[1].dateLastPurchased.toDate().toLocaleString()}`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `Expected to buy again by: ${mockShoppingListData[1].dateNextPurchased.toDate().toLocaleString()}`, + ), + ).toBeInTheDocument(); + }); +}); From 3b59117c4757d3d8593bd225b3871c453b176154 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 11:49:45 -0400 Subject: [PATCH 14/21] fix: return Tooltip component to the UrgencyStatusIcon --- src/components/ListItem.jsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx index 537aa07..265ec69 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.jsx @@ -129,12 +129,18 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { ) : ( <> {UrgencyStatusIcon && ( - {itemUrgencyStatus}

} - /> + placement="left" + arrow + > + {itemUrgencyStatus}

} + /> + )} From 8fc10e1a75bea498630f4ebbda49d081611d5460 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 12:35:52 -0400 Subject: [PATCH 15/21] fix: remove redundant title from UrgencyStatusIcon --- src/components/ListItem.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx index 265ec69..4f2f763 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.jsx @@ -138,7 +138,6 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { sx={largeWhiteFontStyle} fontSize="large" aria-label={itemUrgencyStatus} - title={

{itemUrgencyStatus}

} /> )} From b8d80951749a546db80790143c7840ef9c57d9d3 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 14:22:08 -0400 Subject: [PATCH 16/21] fix: update List test --- tests/List.test.jsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index 184ddab..d36c2a3 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -75,6 +75,20 @@ describe('List Component', () => { }); }); + test('shows AddItems component with existing items', () => { + render( + + + , + ); + + expect(screen.getByLabelText('Item Name:')).toBeInTheDocument(); + expect(screen.getByLabelText('Soon')).toBeInTheDocument(); + expect(screen.getByLabelText('Kind of soon')).toBeInTheDocument(); + expect(screen.getByLabelText('Not soon')).toBeInTheDocument(); + expect(screen.getByText('Submit')).toBeInTheDocument(); + }); + test('shows welcome message and AddItems component when no items are present', () => { render( @@ -110,18 +124,4 @@ describe('List Component', () => { 'It seems like you landed here without first creating a list or selecting an existing one. Please select or create a new list first. Redirecting to Home.', ); }); - - test('shows AddItems component with existing items', () => { - render( - - - , - ); - - expect(screen.getByLabelText('Item Name:')).toBeInTheDocument(); - expect(screen.getByLabelText('Soon')).toBeInTheDocument(); - expect(screen.getByLabelText('Kind of soon')).toBeInTheDocument(); - expect(screen.getByLabelText('Not soon')).toBeInTheDocument(); - expect(screen.getByText('Submit')).toBeInTheDocument(); - }); }); From 5a2b6f75a36b3866e7ef0bee138e51596b3704fa Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 17:56:26 -0400 Subject: [PATCH 17/21] feat: add averagePurchaseInterval to each item doc in Firebase --- src/api/firebase.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/api/firebase.js b/src/api/firebase.js index f0584dd..7c5bfaf 100644 --- a/src/api/firebase.js +++ b/src/api/firebase.js @@ -167,6 +167,7 @@ export async function addItem(listPath, { itemName, daysUntilNextPurchase }) { dateNextPurchased: addDaysFromToday(daysUntilNextPurchase), name: itemName, totalPurchases: 0, + averagePurchaseInterval: 0, }); } @@ -178,13 +179,19 @@ export async function addItem(listPath, { itemName, daysUntilNextPurchase }) { * @param {Date} updatedData.dateLastPurchased The date the item was last purchased. * @param {Date} updatedData.dateNextPurchased The estimated date for the next purchase. * @param {number} updatedData.totalPurchases The total number of times the item has been purchased. + * @param {number} updatedData.averagePurchaseInterval The average purchase interval of the item that has been purchased. * @returns {Promise} A message confirming the item was successfully updated. * @throws {Error} If the item update fails. */ export async function updateItem( listPath, itemId, - { dateLastPurchased, dateNextPurchased, totalPurchases }, + { + dateLastPurchased, + dateNextPurchased, + totalPurchases, + averagePurchaseInterval, + }, ) { // reference the item path const itemDocRef = doc(db, listPath, 'items', itemId); @@ -194,6 +201,7 @@ export async function updateItem( dateLastPurchased, dateNextPurchased, totalPurchases, + averagePurchaseInterval, }); return 'item purchased'; } catch (error) { From bca8dd6be28727469c95c16418845ac22d60f5d3 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 17:58:37 -0400 Subject: [PATCH 18/21] feat: update propmts inside InfoCard; abstract describeAveragePurchaseInterval into a utils file; update utils exports --- src/components/InfoCard.jsx | 8 +++++++- src/utils/index.js | 1 + src/utils/infoCardUtils.js | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/utils/infoCardUtils.js diff --git a/src/components/InfoCard.jsx b/src/components/InfoCard.jsx index 46e2349..e0cd921 100644 --- a/src/components/InfoCard.jsx +++ b/src/components/InfoCard.jsx @@ -1,3 +1,4 @@ +import { describeAveragePurchaseInterval } from '../utils'; import { Box, Card, @@ -12,8 +13,13 @@ import CloseIcon from '@mui/icons-material/Close'; export const InfoCard = ({ item, toggleCard, show }) => { const typographyOptions = { totalPurchases: `You've purchased this item ${item.totalPurchases} times`, + averagePurchaseInterval: describeAveragePurchaseInterval( + item.averagePurchaseInterval, + ), dateCreated: `Item added on: ${item.dateCreated?.toDate().toLocaleString()}`, - dateLastPurchased: `Last bought on: ${item.dateLastPurchased?.toDate().toLocaleString() ?? 'Not purchased yet'}`, + dateLastPurchased: item.dateLastPurchased + ? `Last bought on: ${item.dateLastPurchased?.toDate().toLocaleString()}` + : 'Not purchased yet', dateNextPurchased: `Expected to buy again by: ${item.dateNextPurchased?.toDate().toLocaleString() ?? 'No estimate yet'}`, }; diff --git a/src/utils/index.js b/src/utils/index.js index 3a035ec..9ebdc1e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,3 +3,4 @@ export * from './normalize'; export * from './urgencyUtils'; export * from './importanceUtils'; export * from './calculateIsPurchased'; +export * from './infoCardUtils'; diff --git a/src/utils/infoCardUtils.js b/src/utils/infoCardUtils.js new file mode 100644 index 0000000..38143da --- /dev/null +++ b/src/utils/infoCardUtils.js @@ -0,0 +1,9 @@ +export const describeAveragePurchaseInterval = (averageInterval) => { + if (averageInterval > 1) { + return `On average, this item is purchased every ${averageInterval} days`; + } else if (1 >= averageInterval) { + return 'On average, this item is purchased every day'; + } else { + return 'No average purchase interval available yet'; + } +}; From 77c1b192747e3c35017c57e424e57e0b47f259d9 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 18:00:56 -0400 Subject: [PATCH 19/21] feat: add logic that calculates average purchase interval of a selected item; pass averagePurchaseInterval to ListItem --- src/components/ListItem.jsx | 5 ++++- src/utils/dates.js | 43 ++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx index 4f2f763..01ff1a8 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.jsx @@ -53,10 +53,13 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) { const { name, id } = item; const updateItemOnPurchase = () => { + const { nextPurchaseEstimate, averagePurchaseInterval } = + calculateDateNextPurchased(currentDate, item); return { dateLastPurchased: currentDate, - dateNextPurchased: calculateDateNextPurchased(currentDate, item), + dateNextPurchased: nextPurchaseEstimate, totalPurchases: item.totalPurchases + 1, + averagePurchaseInterval, }; }; diff --git a/src/utils/dates.js b/src/utils/dates.js index 6f78a6f..ec33940 100644 --- a/src/utils/dates.js +++ b/src/utils/dates.js @@ -34,12 +34,15 @@ export const calculateDateNextPurchased = (currentDate, item) => { item.dateNextPurchased, item.dateLastPurchased, ); - const estimatedNextPurchaseDate = getNextPurchaseEstimate( + const { estimatedDaysUntilPurchase, nextPurchaseEstimate } = + getNextPurchaseEstimate(purchaseIntervals, item.totalPurchases + 1); + + const averagePurchaseInterval = getAveragePurchaseInterval( purchaseIntervals, - item.totalPurchases + 1, - ); + estimatedDaysUntilPurchase, + ).toFixed(1); - return estimatedNextPurchaseDate; + return { nextPurchaseEstimate, averagePurchaseInterval }; } catch (error) { throw new Error(`Failed getting next purchase date: ${error}`); } @@ -120,8 +123,38 @@ function getNextPurchaseEstimate(purchaseIntervals, totalPurchases) { const nextPurchaseEstimate = addDaysFromToday(estimatedDaysUntilPurchase); - return nextPurchaseEstimate; + return { estimatedDaysUntilPurchase, nextPurchaseEstimate }; } catch (error) { throw new Error(`Failed updaing date next purchased: ${error}`); } } + +/** + * Calculates the average purchase interval based on known purchase intervals. + * + * This function takes into account the last estimated purchase interval, + * the number of days since the last purchase, and the estimated days until the next purchase. + * It computes the average of these intervals by summing them up and dividing by the total count. + * + * @param {Object} purchaseIntervals - An object containing the purchase interval data. + * @param {number} purchaseIntervals.lastEstimatedInterval - The last estimated interval in days. + * @param {number} purchaseIntervals.daysSinceLastPurchase - The number of days since the last purchase. + * @param {number} estimatedDaysUntilPurchase - The estimated number of days until the next purchase. + * @returns {number} The average purchase interval calculated from the provided intervals. + */ +function getAveragePurchaseInterval( + purchaseIntervals, + estimatedDaysUntilPurchase, +) { + const { lastEstimatedInterval, daysSinceLastPurchase } = purchaseIntervals; + const knownIntervals = [ + lastEstimatedInterval, + daysSinceLastPurchase, + estimatedDaysUntilPurchase, + ]; + + return ( + knownIntervals.reduce((sum, interval) => sum + interval, 0) / + knownIntervals.length + ); +} From a7ddba0bef3d9d5cf8ac7bd91096966bda8d4dde Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 17 Oct 2024 18:23:49 -0400 Subject: [PATCH 20/21] feat: export getAveragePurchaseInterval function and create a unit test for it --- src/utils/dates.js | 2 +- tests/getAveragePurchaseInterval.test.js | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/getAveragePurchaseInterval.test.js diff --git a/src/utils/dates.js b/src/utils/dates.js index ec33940..daaaa56 100644 --- a/src/utils/dates.js +++ b/src/utils/dates.js @@ -142,7 +142,7 @@ function getNextPurchaseEstimate(purchaseIntervals, totalPurchases) { * @param {number} estimatedDaysUntilPurchase - The estimated number of days until the next purchase. * @returns {number} The average purchase interval calculated from the provided intervals. */ -function getAveragePurchaseInterval( +export function getAveragePurchaseInterval( purchaseIntervals, estimatedDaysUntilPurchase, ) { diff --git a/tests/getAveragePurchaseInterval.test.js b/tests/getAveragePurchaseInterval.test.js new file mode 100644 index 0000000..5c91b03 --- /dev/null +++ b/tests/getAveragePurchaseInterval.test.js @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { getAveragePurchaseInterval } from '../src/utils/dates'; + +describe('getAveragePurchaseInterval function', () => { + it('correctly calculates average purchase intervals', () => { + const purchaseIntervals = { + lastEstimatedInterval: 4, + daysSinceLastPurchase: 6, + }; + const estimatedDaysUntilPurchase = 5; + const result = getAveragePurchaseInterval( + purchaseIntervals, + estimatedDaysUntilPurchase, + ); + expect(result).toBe(5); + }); + + it('handles zero values in the intervals', () => { + const purchaseIntervals = { + lastEstimatedInterval: 0, + daysSinceLastPurchase: 6, + }; + const estimatedDaysUntilPurchase = 5; + + const result = getAveragePurchaseInterval( + purchaseIntervals, + estimatedDaysUntilPurchase, + ); + + expect(result).toBeCloseTo(3.67, 2); + }); + + it('returns 0 when all intervals are zero', () => { + const purchaseIntervals = { + lastEstimatedInterval: 0, + daysSinceLastPurchase: 0, + }; + const estimatedDaysUntilPurchase = 0; + + const result = getAveragePurchaseInterval( + purchaseIntervals, + estimatedDaysUntilPurchase, + ); + + expect(result).toBe(0); + }); +}); From a82d8fad2c96ead64634d6397a48f089122317d4 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 18 Oct 2024 12:31:41 -0400 Subject: [PATCH 21/21] fix: remove deprecated Grid component and replace with Grid2 component --- src/views/List.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/views/List.jsx b/src/views/List.jsx index 4f5727a..ac9c8fa 100644 --- a/src/views/List.jsx +++ b/src/views/List.jsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { useEnsureListPath, useUrgency } from '../hooks'; import { getUrgency } from '../utils/urgencyUtils'; -import { List as UnorderedList, Box, Grid } from '@mui/material'; +import { List as UnorderedList, Box } from '@mui/material'; +import Grid from '@mui/material/Grid2'; import { ListItem, AddItems, TextInputElement } from '../components'; // React.memo is needed to prevent unnecessary re-renders of the List component @@ -47,10 +48,10 @@ export const List = React.memo(function List({ data, listPath }) { columns={16} justifyContent="space-between" > - + - +
event.preventDefault()}>