diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 29b9f53..f634d34 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,9 +4,11 @@ import Welcome from './components/Welcome'; import About from './components/About'; import Footer from './components/Footer'; import Products from './components/entity/product/Products'; +import Cart from './components/entity/cart/Cart'; import Login from './components/Login'; import { AuthProvider } from './context/AuthContext'; import { ThemeProvider } from './context/ThemeContext'; +import { CartProvider } from './context/CartContext'; import AdminProducts from './components/admin/AdminProducts'; import { useTheme } from './context/ThemeContext'; @@ -25,6 +27,7 @@ function ThemedApp() { } /> } /> } /> + } /> } /> } /> @@ -39,7 +42,9 @@ function App() { return ( - + + + ); diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index 5f35e12..57bd2c9 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 { useCart } from '../context/CartContext'; import { useState } from 'react'; export default function Navigation() { const { isLoggedIn, isAdmin, logout } = useAuth(); const { darkMode, toggleTheme } = useTheme(); + const { cart } = useCart(); const [adminMenuOpen, setAdminMenuOpen] = useState(false); return ( @@ -85,6 +87,30 @@ export default function Navigation() {
+ + + + + {cart.totalItems > 0 && ( + + {cart.totalItems} + + )} + +
+ + + ); + } + + return ( +
+
+

+ Shopping Cart +

+ + {checkoutError && ( +
+ {checkoutError} +
+ )} + +
+ {cart.items.map((item) => { + const itemPrice = item.discount ? item.price * (1 - item.discount) : item.price; + const itemTotal = itemPrice * item.quantity; + + return ( +
+ {item.name} +
+

+ {item.name} +

+
+ {item.discount ? ( + <> + + ${item.price.toFixed(2)} + + + ${itemPrice.toFixed(2)} + + + ) : ( + ${itemPrice.toFixed(2)} + )} +
+
+
+
+ + + {item.quantity} + + +
+
+ ${itemTotal.toFixed(2)} +
+ +
+
+ ); + })} +
+ +
+
+ + Total Items: + + + {cart.totalItems} + +
+
+ + Total: + + + ${cart.totalPrice.toFixed(2)} + +
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/entity/product/Products.tsx b/frontend/src/components/entity/product/Products.tsx index bb7790e..6161383 100644 --- a/frontend/src/components/entity/product/Products.tsx +++ b/frontend/src/components/entity/product/Products.tsx @@ -3,6 +3,7 @@ import axios from 'axios'; import { useQuery } from 'react-query'; import { api } from '../../../api/config'; import { useTheme } from '../../../context/ThemeContext'; +import { useCart } from '../../../context/CartContext'; interface Product { productId: number; @@ -28,6 +29,7 @@ export default function Products() { const [showModal, setShowModal] = useState(false); const { data: products, isLoading, error } = useQuery('products', fetchProducts); const { darkMode } = useTheme(); + const { addToCart } = useCart(); const filteredProducts = products?.filter( (product) => @@ -45,12 +47,23 @@ export default function Products() { const handleAddToCart = (productId: number) => { const quantity = quantities[productId] || 0; if (quantity > 0) { - // TODO: Implement cart functionality - alert(`Added ${quantity} items to cart`); - setQuantities((prev) => ({ - ...prev, - [productId]: 0, - })); + const product = products?.find((p) => p.productId === productId); + if (product) { + addToCart( + { + productId: product.productId, + name: product.name, + price: product.price, + imgName: product.imgName, + discount: product.discount, + }, + quantity, + ); + setQuantities((prev) => ({ + ...prev, + [productId]: 0, + })); + } } }; diff --git a/frontend/src/context/CartContext.tsx b/frontend/src/context/CartContext.tsx new file mode 100644 index 0000000..45d1ce9 --- /dev/null +++ b/frontend/src/context/CartContext.tsx @@ -0,0 +1,91 @@ +import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; +import { CartItem, Cart } from '../types/cart'; + +interface CartContextType { + cart: Cart; + addToCart: (item: Omit, quantity: number) => void; + removeFromCart: (productId: number) => void; + updateQuantity: (productId: number, quantity: number) => void; + clearCart: () => void; +} + +const CartContext = createContext(undefined); + +const CART_STORAGE_KEY = 'octocat_supply_cart'; + +const calculateTotals = (items: CartItem[]): { totalItems: number; totalPrice: number } => { + const totalItems = items.reduce((sum, item) => sum + item.quantity, 0); + const totalPrice = items.reduce((sum, item) => { + const itemPrice = item.discount ? item.price * (1 - item.discount) : item.price; + return sum + itemPrice * item.quantity; + }, 0); + return { totalItems, totalPrice }; +}; + +export function CartProvider({ children }: { children: ReactNode }) { + const [items, setItems] = useState(() => { + try { + const stored = localStorage.getItem(CART_STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error('Failed to parse cart data from localStorage:', error); + return []; + } + }); + + useEffect(() => { + localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(items)); + }, [items]); + + const { totalItems, totalPrice } = calculateTotals(items); + + const cart: Cart = { + items, + totalItems, + totalPrice, + }; + + const addToCart = (item: Omit, quantity: number) => { + setItems((prevItems) => { + const existingItem = prevItems.find((i) => i.productId === item.productId); + if (existingItem) { + return prevItems.map((i) => + i.productId === item.productId ? { ...i, quantity: i.quantity + quantity } : i, + ); + } + return [...prevItems, { ...item, quantity }]; + }); + }; + + const removeFromCart = (productId: number) => { + setItems((prevItems) => prevItems.filter((item) => item.productId !== productId)); + }; + + const updateQuantity = (productId: number, quantity: number) => { + if (quantity <= 0) { + removeFromCart(productId); + return; + } + setItems((prevItems) => + prevItems.map((item) => (item.productId === productId ? { ...item, quantity } : item)), + ); + }; + + const clearCart = () => { + setItems([]); + }; + + return ( + + {children} + + ); +} + +export function useCart() { + const context = useContext(CartContext); + if (!context) { + throw new Error('useCart must be used within a CartProvider'); + } + return context; +} diff --git a/frontend/src/types/cart.ts b/frontend/src/types/cart.ts new file mode 100644 index 0000000..cc8cb34 --- /dev/null +++ b/frontend/src/types/cart.ts @@ -0,0 +1,14 @@ +export interface CartItem { + productId: number; + name: string; + price: number; + quantity: number; + imgName: string; + discount?: number; +} + +export interface Cart { + items: CartItem[]; + totalItems: number; + totalPrice: number; +}