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) {
diff --git a/src/components/AddItems.jsx b/src/components/AddItems.jsx
index 7b0d01a..89f73cf 100644
--- a/src/components/AddItems.jsx
+++ b/src/components/AddItems.jsx
@@ -69,13 +69,13 @@ export function AddItems({ items }) {
required={true}
/>
- {Object.entries(daysUntilPurchaseOptions).map(([key, value]) => (
+ {Object.entries(daysUntilPurchaseOptions).map(([status, days]) => (
))}
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..125057d
--- /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 function IconWithTooltip({
+ icon,
+ onClick,
+ ariaLabel,
+ title,
+ placement,
+}) {
+ return (
+ {title}}
+ placement={placement}
+ arrow
+ >
+
+ {icon}
+
+
+ );
+}
diff --git a/src/components/InfoCard.jsx b/src/components/InfoCard.jsx
new file mode 100644
index 0000000..e0cd921
--- /dev/null
+++ b/src/components/InfoCard.jsx
@@ -0,0 +1,63 @@
+import { describeAveragePurchaseInterval } from '../utils';
+import {
+ Box,
+ Card,
+ CardContent,
+ CardHeader,
+ Typography,
+ IconButton,
+ Collapse,
+} from '@mui/material';
+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: 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'}`,
+ };
+
+ return (
+
+
+
+ {item?.name}}
+ action={
+ ({
+ position: 'absolute',
+ right: 20,
+ top: 15,
+ color: theme.palette.grey[700],
+ })}
+ >
+
+
+ }
+ />
+
+ {Object.entries(typographyOptions).map(([key, option]) => (
+
+ {option}
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/src/components/ListItem.jsx b/src/components/ListItem.jsx
index c1220a0..01ff1a8 100644
--- a/src/components/ListItem.jsx
+++ b/src/components/ListItem.jsx
@@ -1,14 +1,17 @@
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 { DeleteIconWithTooltip, tooltipStyle } from './DeleteIconWithTooltip';
+import {
+ IconWithTooltip,
+ tooltipStyle,
+ InfoCard,
+ ConfirmDialog,
+} from './index';
import {
ListItem as MaterialListItem,
Tooltip,
- IconButton,
ListItemButton,
ListItemIcon,
ListItemText,
@@ -20,55 +23,43 @@ import {
RadioButtonUnchecked as KindOfSoonIcon,
RemoveCircle as NotSoonIcon,
RadioButtonChecked as InactiveIcon,
+ MoreHoriz,
+ DeleteOutlineOutlined,
} from '@mui/icons-material';
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,
};
-const urgencyStatusStyle = {
+const largeWhiteFontStyle = {
fontSize: '2.5rem',
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;
const updateItemOnPurchase = () => {
+ const { nextPurchaseEstimate, averagePurchaseInterval } =
+ calculateDateNextPurchased(currentDate, item);
return {
dateLastPurchased: currentDate,
- dateNextPurchased: calculateDateNextPurchased(currentDate, item),
+ dateNextPurchased: nextPurchaseEstimate,
totalPurchases: item.totalPurchases + 1,
+ averagePurchaseInterval,
};
};
@@ -86,7 +77,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 +86,33 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) {
return;
};
+ const toggleMoreInformation = () => {
+ setShowCard((prev) => !prev);
+ };
+
const UrgencyStatusIcon = urgencyStatusIcons[itemUrgencyStatus];
- const props = {
+ 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 +121,61 @@ export function ListItem({ item, listPath, itemUrgencyStatus }) {
return (
<>
- {open && }
+ {open && }
- {UrgencyStatusIcon && (
- {itemUrgencyStatus}}
- placement="left"
- arrow
- >
-
-
-
-
- )}
-
-
- {tooltipTitle}}
- placement="left"
- arrow
- >
-
-
-
-
-
+ ) : (
+ <>
+ {UrgencyStatusIcon && (
+ {itemUrgencyStatus}}
+ placement="left"
+ arrow
+ >
+
+
+ )}
+
+
+
+ {tooltipTitle}}
+ placement="left"
+ arrow
+ >
+
+
+
+
+
+
-
+ {/* delete icon */}
+
+
+ {/* more information icon */}
+
+ >
+ )}
>
);
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"
/>
>
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';
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(),
};
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/dates.js b/src/utils/dates.js
index 6f78a6f..daaaa56 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.
+ */
+export function getAveragePurchaseInterval(
+ purchaseIntervals,
+ estimatedDaysUntilPurchase,
+) {
+ const { lastEstimatedInterval, daysSinceLastPurchase } = purchaseIntervals;
+ const knownIntervals = [
+ lastEstimatedInterval,
+ daysSinceLastPurchase,
+ estimatedDaysUntilPurchase,
+ ];
+
+ return (
+ knownIntervals.reduce((sum, interval) => sum + interval, 0) /
+ knownIntervals.length
+ );
+}
diff --git a/src/utils/index.js b/src/utils/index.js
index bf31387..9ebdc1e 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -2,3 +2,5 @@ export * from './dates';
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';
+ }
+};
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/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"
>
-
+
-
+