Skip to content

Commit

Permalink
Merge pull request #45 from the-collab-lab/nk-estimate-next-purchase-…
Browse files Browse the repository at this point in the history
…date

[ISSUE 11] Nk estimate next purchase date
  • Loading branch information
kweeuhree authored Sep 15, 2024
2 parents dd8b855 + 136efe2 commit e73e0e6
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 56 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"npm": ">=8.19.0"
},
"dependencies": {
"@the-collab-lab/shopping-list-utils": "^2.2.0",
"firebase": "^10.12.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function App() {
path="/list"
element={<List data={data} listPath={listPath} />}
/>
<Route path="/manage-list" element={<ManageList />} />
<Route path="/manage-list" element={<ManageList items={data} />} />
</Route>
</Routes>
</Router>
Expand Down
2 changes: 1 addition & 1 deletion src/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ This function takes user-provided data and uses it to create a new item in the F

#### Note

**`daysUntilNextPurchase` is not added to the item directly**. It is used alomngside the `getFutureDate` utility function to create a new _JavaScript Date_ that represents when we think the user will buy the item again.
**`daysUntilNextPurchase` is not added to the item directly**. It is used alomngside the `addDaysFromToday` utility function to create a new _JavaScript Date_ that represents when we think the user will buy the item again.

### `updateItem`

Expand Down
22 changes: 17 additions & 5 deletions src/api/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'firebase/firestore';
import { useEffect, useState } from 'react';
import { db } from './config';
import { getFutureDate } from '../utils';
import { addDaysFromToday } from '../utils';

/**
* A custom hook that subscribes to the user's shopping lists in our Firestore
Expand Down Expand Up @@ -173,28 +173,40 @@ export async function addItem(listPath, { itemName, daysUntilNextPurchase }) {
return addDoc(listCollectionRef, {
dateCreated: new Date(),
dateLastPurchased: null,
dateNextPurchased: getFutureDate(daysUntilNextPurchase),
dateNextPurchased: addDaysFromToday(daysUntilNextPurchase),
name: itemName,
totalPurchases: 0,
});
}

/**
* Update an item in the user's list in Firestore with new purchase information.
* @param {string} listPath The path of the list the item belongs to.
* @param {string} itemId The ID of the item being updated.
* @param {Object} updatedData Object containing the updated item data.
* @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.
* @returns {Promise<string>} A message confirming the item was successfully updated.
* @throws {Error} If the item update fails.
*/
export async function updateItem(
listPath,
itemId,
{ dateLastPurchased, totalPurchases },
{ dateLastPurchased, dateNextPurchased, totalPurchases },
) {
// reference the item path
const itemDocRef = doc(db, listPath, 'items', itemId);
// update the item with the purchase date and increment the total purchases made
try {
await updateDoc(itemDocRef, {
dateLastPurchased,
dateNextPurchased,
totalPurchases,
});
return 'item purchased';
} catch {
return;
} catch (error) {
throw new Error(`Failed updating item: ${error.message}`);
}
}

Expand Down
20 changes: 19 additions & 1 deletion src/components/AddItems.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,32 @@ const daysUntilPurchaseOptions = {
'Not soon': 30,
};

export function AddItems() {
export function AddItems({ items }) {
const [listPath] = useStateWithStorage('tcl-shopping-list-path', null);

const handleSubmit = useCallback(
async (event) => {
event.preventDefault();

const itemName = event.target.elements['item-name'].value;
const normalizedItemName = itemName
.trim()
.toLowerCase()
.replace(/[&\/\\#, +$!,~%.'":*?<>{}]/g, '');
if (items) {
const currentItems = items.map((item) =>
item.name
.trim()
.toLowerCase()
.replace(/[&\/\\#, +$!,~%.'":*?<>{}]/g, ''),
);
if (currentItems.includes(normalizedItemName)) {
alert('This item already exists in the list');
event.target.reset();
return;
}
}

const daysUntilNextPurchase =
event.target.elements['purchase-date'].value;

Expand Down
65 changes: 34 additions & 31 deletions src/components/ListItem.jsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import './ListItem.css';
import { updateItem } from '../api';
import { useStateWithStorage } from '../utils';
import { increment } from 'firebase/firestore';
import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils';

export function ListItem({ name, itemId, purchaseTimestamp }) {
const [isPurchased, setIsPurchased] = useState(false);
const [listPath] = useStateWithStorage('tcl-shopping-list-path', null);
const currentDate = new Date();

useEffect(() => {
if (!purchaseTimestamp) {
setIsPurchased(false);
return;
}
const purchaseDate = purchaseTimestamp.toDate();
const oneDayLater = new Date(purchaseDate.getTime() + 24 * 60 * 60 * 1000);
const currentDate = new Date();
if (purchaseTimestamp) {
if (currentDate < oneDayLater) {
setIsPurchased(true);
} else {
setIsPurchased(false);
}
} else {
return;
}
}, []);
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 }) {
const [isPurchased, setIsPurchased] = useState(() =>
calculateIsPurchased(item.dateLastPurchased),
);
const { name, id } = item;

const updateItemOnPurchase = () => {
return {
dateLastPurchased: currentDate,
dateNextPurchased: calculateDateNextPurchased(currentDate, item),
totalPurchases: item.totalPurchases + 1,
};
};

const handleChange = async () => {
setIsPurchased(!isPurchased);
if (!isPurchased) {
try {
await updateItem(listPath, itemId, {
dateLastPurchased: new Date(),
totalPurchases: increment(1),
});
const updatedItem = updateItemOnPurchase();

await updateItem(listPath, id, { ...updatedItem });
} catch (error) {
alert(`Item was not marked as purchased`, error);
alert(`Item was not marked as purchased`, error.message);
}
}
};
Expand All @@ -45,11 +48,11 @@ export function ListItem({ name, itemId, purchaseTimestamp }) {
<li className="ListItem">
<input
type="checkbox"
id={`checkbox-${itemId}`}
id={`checkbox-${id}`}
checked={isPurchased}
onChange={handleChange}
/>
<label htmlFor={`checkbox-${itemId}`}>{name}</label>
<label htmlFor={`checkbox-${id}`}>{name}</label>
</li>
);
}
114 changes: 109 additions & 5 deletions src/utils/dates.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,116 @@
const ONE_DAY_IN_MILLISECONDS = 86400000;
import { calculateEstimate } from '@the-collab-lab/shopping-list-utils';

export const ONE_DAY_IN_MILLISECONDS = 86400000;

/**
* Get a new JavaScript Date that is `offset` days in the future.
* @example
* // Returns a Date 3 days in the future
* getFutureDate(3)
* @param {number} offset
* addDaysFromToday(3)
* @param {number} daysOffset
*/
export function addDaysFromToday(daysOffset) {
return new Date(Date.now() + daysOffset * ONE_DAY_IN_MILLISECONDS);
}

/**
* Calculates the estimated date for the next purchase based on current date, purchase history,
* and total purchases.
* @param {Date} currentDate - The current date to calculate against.
* @param {Object} item - The item object containing purchase data.
* @param {Date} item.dateCreated - The date the item was created.
* @param {Date} item.dateNextPurchased - The previously estimated next purchase date.
* @param {Date|null} item.dateLastPurchased - The last date the item was actually purchased, or null if not purchased yet.
* @param {number} item.totalPurchases - The total number of purchases made for the item.
* @returns {Date} - The estimated date of the next purchase.
* @throws {Error} - Throws an error if the next purchase date cannot be calculated.
*/
export const calculateDateNextPurchased = (currentDate, item) => {
try {
// get purchase intervals and get new estimation for next purchase date
const purchaseIntervals = calculatePurchaseIntervals(
currentDate,
item.dateCreated,
item.dateNextPurchased,
item.dateLastPurchased,
);
const estimatedNextPurchaseDate = getNextPurchaseEstimate(
purchaseIntervals,
item.totalPurchases + 1,
);

return estimatedNextPurchaseDate;
} catch (error) {
throw new Error(`Failed getting next purchase date: ${error}`);
}
};

/**
* Calculate the number of days between two dates.
* @param {Date} earlierDate The starting date.
* @param {Date} laterDate The ending date.
* @returns {number} The number of days between the two dates.
*/
function getDaysBetweenDates(earlierDate, laterDate) {
return Math.floor(
(laterDate.getTime() - earlierDate.getTime()) / ONE_DAY_IN_MILLISECONDS,
);
}

/**
* Calculate the purchase intervals between current, next, and last purchase dates.
* @param {Date} currentDate The current date.
* @param {Date} dateNextPurchased The previously estimated next purchase date.
* @param {Date|null} dateLastPurchased The date the item was last purchased (can be null).
* @returns {Object} An object containing the last estimated interval and days since last purchase.
*/
function calculatePurchaseIntervals(
currentDate,
dateCreated,
dateNextPurchased,
dateLastPurchased,
) {
const lastPurchaseDate = dateLastPurchased?.toDate();

const lastEstimatedIntervalStartDate =
lastPurchaseDate ?? dateCreated.toDate();

const lastEstimatedInterval = getDaysBetweenDates(
lastEstimatedIntervalStartDate,
dateNextPurchased.toDate(),
);

const daysSinceLastPurchase =
lastPurchaseDate === undefined
? 0
: getDaysBetweenDates(lastPurchaseDate, currentDate);

return { lastEstimatedInterval, daysSinceLastPurchase };
}

/**
* Calculate the next purchase estimate based on purchase intervals and total purchases.
* @param {Object} purchaseIntervals The intervals between the previous and current purchases.
* @param {number} purchaseIntervals.lastEstimatedInterval The previously estimated number of days between purchases.
* @param {number} purchaseIntervals.daysSinceLastPurchase The number of days since the last purchase.
* @param {number} totalPurchases The total number of purchases made.
* @returns {Date} The estimated next purchase date.
* @throws {Error} If an error occurs during the next purchase estimation process.
*/
export function getFutureDate(offset) {
return new Date(Date.now() + offset * ONE_DAY_IN_MILLISECONDS);
function getNextPurchaseEstimate(purchaseIntervals, totalPurchases) {
const { lastEstimatedInterval, daysSinceLastPurchase } = purchaseIntervals;

try {
const estimatedDaysUntilPurchase = calculateEstimate(
lastEstimatedInterval,
daysSinceLastPurchase,
totalPurchases,
);

const nextPurchaseEstimate = addDaysFromToday(estimatedDaysUntilPurchase);

return nextPurchaseEstimate;
} catch (error) {
throw new Error(`Failed updaing date next purchased: ${error}`);
}
}
9 changes: 1 addition & 8 deletions src/views/List.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,7 @@ export function List({ data }) {
</form>
<ul>
{filteredItems.map((item) => {
return (
<ListItem
key={item.id}
name={item.name}
itemId={item.id}
purchaseTimestamp={item.dateLastPurchased}
/>
);
return <ListItem key={item.id} item={item} listPath={listPath} />;
})}
</ul>
</>
Expand Down
4 changes: 2 additions & 2 deletions src/views/ManageList.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AddItems } from '../components/AddItems';
import { ShareList } from '../components/ShareList';

export function ManageList() {
export function ManageList({ items }) {
return (
<div>
<AddItems />
<AddItems items={items} />
<ShareList />
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion tests/List.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useStateWithStorage } from '../src/utils';

vi.mock('../src/utils', () => ({
useStateWithStorage: vi.fn(),
ONE_DAY_IN_MILLISECONDS: 86400000,
}));

beforeEach(() => {
Expand All @@ -30,7 +31,7 @@ describe('List Component', () => {
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.getByLabelText('Not soon')).toBeInTheDocument();
expect(screen.getByText('Submit')).toBeInTheDocument();
});
});
Loading

0 comments on commit e73e0e6

Please sign in to comment.