From dc3ac1ef622a6e7446721401c818cdc63a7046fe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 14:47:13 +0000
Subject: [PATCH 1/2] Initial plan
From 0fb78c0f7193c8661079800d7c16d97c6b01d240 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 14:51:46 +0000
Subject: [PATCH 2/2] feat: add wishlist with price-drop alerts (Variation 3)
Agent-Logs-Url: https://github.com/thomasiverson/GitHubCopilot_Customized/sessions/cb969598-68cd-4ed3-8b23-0236eeb0cbf4
Co-authored-by: thomasiverson <12767513+thomasiverson@users.noreply.github.com>
---
frontend/src/App.tsx | 7 +-
frontend/src/components/Navigation.tsx | 12 ++
.../components/entity/product/Products.tsx | 29 ++++
frontend/src/components/wishlist/Wishlist.tsx | 156 ++++++++++++++++++
frontend/src/context/WishlistContext.tsx | 81 +++++++++
5 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/components/wishlist/Wishlist.tsx
create mode 100644 frontend/src/context/WishlistContext.tsx
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d0b02da..47fd94e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,6 +9,8 @@ import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import AdminProducts from './components/admin/AdminProducts';
import { useTheme } from './context/ThemeContext';
+import { WishlistProvider } from './context/WishlistContext';
+import Wishlist from './components/wishlist/Wishlist';
// Wrapper component to apply theme classes
function ThemedApp() {
@@ -25,6 +27,7 @@ function ThemedApp() {
} />
} />
} />
+ } />
@@ -37,7 +40,9 @@ function App() {
return (
-
+
+
+
);
diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx
index d7b393b..023dc7a 100644
--- a/frontend/src/components/Navigation.tsx
+++ b/frontend/src/components/Navigation.tsx
@@ -1,11 +1,13 @@
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';
+import { useWishlist } from '../context/WishlistContext';
import { useState } from 'react';
export default function Navigation() {
const { isLoggedIn, isAdmin, logout } = useAuth();
const { darkMode, toggleTheme } = useTheme();
+ const { wishlistCount } = useWishlist();
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
return (
@@ -30,6 +32,16 @@ export default function Navigation() {
Home
Products
About us
+ {isLoggedIn && (
+
+ Wishlist
+ {wishlistCount > 0 && (
+
+ {wishlistCount}
+
+ )}
+
+ )}
{isAdmin && (
)}
+
+ {(() => {
+ const drop = getPriceDrop(product);
+ if (drop !== null && drop < 0) {
+ return (
+
+ Price Drop! Save ${Math.abs(drop).toFixed(2)}
+
+ );
+ }
+ return null;
+ })()}
diff --git a/frontend/src/components/wishlist/Wishlist.tsx b/frontend/src/components/wishlist/Wishlist.tsx
new file mode 100644
index 0000000..0676579
--- /dev/null
+++ b/frontend/src/components/wishlist/Wishlist.tsx
@@ -0,0 +1,156 @@
+import axios from 'axios';
+import { useQuery } from 'react-query';
+import { api } from '../../api/config';
+import { useTheme } from '../../context/ThemeContext';
+import { useWishlist } from '../../context/WishlistContext';
+
+interface Product {
+ productId: number;
+ name: string;
+ description: string;
+ price: number;
+ imgName: string;
+ sku: string;
+ unit: string;
+ supplierId: number;
+ discount?: number;
+ stockLevel?: number;
+}
+
+const fetchProducts = async (): Promise
=> {
+ const { data } = await axios.get(`${api.baseURL}${api.endpoints.products}`);
+ return data;
+};
+
+const getEffectivePrice = (price: number, discount?: number): number =>
+ discount ? price * (1 - discount) : price;
+
+const Wishlist = () => {
+ const { darkMode } = useTheme();
+ const { wishlistEntries, toggleWishlist, isWishlisted, getPriceDrop } = useWishlist();
+ const { data: products, isLoading, error } = useQuery('products', fetchProducts);
+
+ const wishlistedProducts = products?.filter(p =>
+ wishlistEntries.some(e => e.productId === p.productId)
+ ) ?? [];
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
Failed to fetch products
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ My Wishlist
+
+
+ {wishlistedProducts.length === 0 ? (
+
+ Your wishlist is empty — browse products to save items for later
+
+ ) : (
+
+ {wishlistedProducts.map(product => {
+ const entry = wishlistEntries.find(e => e.productId === product.productId);
+ if (!entry) return null;
+ const currentPrice = getEffectivePrice(product.price, product.discount);
+ const drop = getPriceDrop(product);
+ const priceDropped = drop !== null && drop < 0;
+
+ return (
+
+
+

+
+
+
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+
+
+ {product.discount ? (
+ <>
+ ${product.price.toFixed(2)}
+ ${currentPrice.toFixed(2)}
+ >
+ ) : (
+ ${currentPrice.toFixed(2)}
+ )}
+
+
+
+ You saved it at ${entry.savedPrice.toFixed(2)}
+
+
+ {priceDropped && (
+
+ Price dropped! Now ${currentPrice.toFixed(2)}
+
+ )}
+
+ {product.stockLevel !== undefined && product.stockLevel <= 5 && (
+
+ Low stock: only {product.stockLevel} left!
+
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ );
+};
+
+export default Wishlist;
diff --git a/frontend/src/context/WishlistContext.tsx b/frontend/src/context/WishlistContext.tsx
new file mode 100644
index 0000000..dbd0820
--- /dev/null
+++ b/frontend/src/context/WishlistContext.tsx
@@ -0,0 +1,81 @@
+/* eslint-disable react-refresh/only-export-components */
+import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+export interface WishlistEntry {
+ productId: number;
+ savedPrice: number;
+ addedAt: string;
+}
+
+interface WishlistContextType {
+ wishlistEntries: WishlistEntry[];
+ toggleWishlist: (product: { productId: number; price: number; discount?: number }) => void;
+ isWishlisted: (productId: number) => boolean;
+ wishlistCount: number;
+ getPriceDrop: (product: { productId: number; price: number; discount?: number }) => number | null;
+}
+
+const WishlistContext = createContext(undefined);
+
+const STORAGE_KEY = 'wishlist_v2';
+
+const getEffectivePrice = (price: number, discount?: number): number =>
+ discount ? price * (1 - discount) : price;
+
+export function WishlistProvider({ children }: { children: ReactNode }) {
+ const [wishlistEntries, setWishlistEntries] = useState(() => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ return stored ? (JSON.parse(stored) as WishlistEntry[]) : [];
+ } catch {
+ return [];
+ }
+ });
+
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(wishlistEntries));
+ }, [wishlistEntries]);
+
+ const toggleWishlist = (product: { productId: number; price: number; discount?: number }) => {
+ setWishlistEntries(prev => {
+ const exists = prev.find(e => e.productId === product.productId);
+ if (exists) {
+ return prev.filter(e => e.productId !== product.productId);
+ }
+ return [
+ ...prev,
+ {
+ productId: product.productId,
+ savedPrice: getEffectivePrice(product.price, product.discount),
+ addedAt: new Date().toISOString(),
+ },
+ ];
+ });
+ };
+
+ const isWishlisted = (productId: number): boolean =>
+ wishlistEntries.some(e => e.productId === productId);
+
+ const wishlistCount = wishlistEntries.length;
+
+ const getPriceDrop = (product: { productId: number; price: number; discount?: number }): number | null => {
+ const entry = wishlistEntries.find(e => e.productId === product.productId);
+ if (!entry) return null;
+ const currentPrice = getEffectivePrice(product.price, product.discount);
+ return currentPrice - entry.savedPrice;
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useWishlist() {
+ const context = useContext(WishlistContext);
+ if (context === undefined) {
+ throw new Error('useWishlist must be used within a WishlistProvider');
+ }
+ return context;
+}