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"
>
-
+
-
+