Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,6 +25,7 @@ function ThemedApp() {
<Route path="/" element={<Welcome />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/cart" element={<Cart />} />
<Route path="/login" element={<Login />} />
<Route path="/admin/products" element={<AdminProducts />} />
</Routes>
Expand All @@ -37,7 +40,9 @@ function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedApp />
<CartProvider>
<ThemedApp />
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -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 { totalItems } = useCart();
const [adminMenuOpen, setAdminMenuOpen] = useState(false);

return (
Expand Down Expand Up @@ -68,6 +70,20 @@ export default function Navigation() {
</div>
</div>
<div className="flex items-center space-x-4">
<Link
to="/cart"
className="relative p-2 rounded-full focus:outline-none transition-colors"
aria-label={`Cart with ${totalItems} items`}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`h-6 w-6 ${darkMode ? 'text-light' : 'text-gray-700'} hover:text-primary transition-colors`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
{totalItems > 0 && (
<span className="absolute -top-1 -right-1 bg-primary text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{totalItems > 99 ? '99+' : totalItems}
</span>
)}
</Link>
<button
onClick={toggleTheme}
className="p-2 rounded-full focus:outline-none transition-colors"
Expand Down
141 changes: 141 additions & 0 deletions frontend/src/components/entity/cart/Cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Link } from 'react-router-dom';
import { useTheme } from '../../../context/ThemeContext';
import { useCart, CartItem } from '../../../context/CartContext';

export default function Cart() {
const { darkMode } = useTheme();
const { items, removeFromCart, updateQuantity, totalPrice, clearCart } = useCart();

if (items.length === 0) {
return (
<div className={`min-h-screen ${darkMode ? 'bg-dark' : 'bg-gray-100'} pt-20 px-4 transition-colors duration-300`}>
<div className="max-w-3xl mx-auto">
<h1 className={`text-3xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'} mb-8 transition-colors duration-300`}>
Your Cart
</h1>
<div className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow p-12 flex flex-col items-center space-y-4 transition-colors duration-300`}>
<svg className="w-20 h-20 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<p className={`text-xl ${darkMode ? 'text-gray-300' : 'text-gray-600'} transition-colors duration-300`}>
Your cart is empty
</p>
<Link
to="/products"
className="bg-primary hover:bg-accent text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
Browse Products
</Link>
</div>
</div>
</div>
);
}

return (
<div className={`min-h-screen ${darkMode ? 'bg-dark' : 'bg-gray-100'} pt-20 pb-16 px-4 transition-colors duration-300`}>
<div className="max-w-3xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className={`text-3xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'} transition-colors duration-300`}>
Your Cart
</h1>
<button
onClick={clearCart}
className={`text-sm ${darkMode ? 'text-gray-400 hover:text-red-400' : 'text-gray-500 hover:text-red-500'} transition-colors`}
>
Clear cart
</button>
</div>

<div className="space-y-4">
{items.map((item: CartItem) => {
const effectivePrice = item.discount ? item.price * (1 - item.discount) : item.price;
return (
<div
key={item.productId}
className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow p-4 flex items-center space-x-4 transition-colors duration-300`}
>
<img
src={`/${item.imgName}`}
alt={item.name}
className="w-20 h-20 object-contain flex-shrink-0"
/>
<div className="flex-grow min-w-0">
<h3 className={`font-semibold text-lg ${darkMode ? 'text-light' : 'text-gray-800'} truncate transition-colors duration-300`}>
{item.name}
</h3>
<p className="text-primary font-bold">
${effectivePrice.toFixed(2)}
{item.discount && (
<span className={`ml-2 text-sm line-through ${darkMode ? 'text-gray-400' : 'text-gray-500'}`}>
${item.price.toFixed(2)}
</span>
)}
</p>
</div>
<div className="flex items-center space-x-2 flex-shrink-0">
<div className={`flex items-center space-x-2 ${darkMode ? 'bg-gray-700' : 'bg-gray-100'} rounded-lg p-1 transition-colors duration-300`}>
<button
onClick={() => updateQuantity(item.productId, item.quantity - 1)}
className={`w-8 h-8 flex items-center justify-center ${darkMode ? 'text-light' : 'text-gray-700'} hover:text-primary transition-colors`}
aria-label={`Decrease quantity of ${item.name}`}
>
<span aria-hidden="true">-</span>
</button>
<span
className={`${darkMode ? 'text-light' : 'text-gray-800'} min-w-[2rem] text-center`}
aria-label={`Quantity of ${item.name}`}
>
{item.quantity}
</span>
<button
onClick={() => updateQuantity(item.productId, item.quantity + 1)}
className={`w-8 h-8 flex items-center justify-center ${darkMode ? 'text-light' : 'text-gray-700'} hover:text-primary transition-colors`}
aria-label={`Increase quantity of ${item.name}`}
>
<span aria-hidden="true">+</span>
</button>
</div>
<p className={`w-20 text-right font-semibold ${darkMode ? 'text-light' : 'text-gray-800'} transition-colors duration-300`}>
${(effectivePrice * item.quantity).toFixed(2)}
</p>
<button
onClick={() => removeFromCart(item.productId)}
className={`ml-2 p-1 ${darkMode ? 'text-gray-400 hover:text-red-400' : 'text-gray-400 hover:text-red-500'} transition-colors`}
aria-label={`Remove ${item.name} from cart`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
);
})}
</div>

{/* Order Summary */}
<div className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow p-6 mt-6 transition-colors duration-300`}>
<h2 className={`text-xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'} mb-4 transition-colors duration-300`}>
Order Summary
</h2>
<div className={`flex justify-between text-lg font-semibold ${darkMode ? 'text-light' : 'text-gray-800'} border-t ${darkMode ? 'border-gray-600' : 'border-gray-200'} pt-4 transition-colors duration-300`}>
<span>Total</span>
<span className="text-primary">${totalPrice.toFixed(2)}</span>
</div>
<button
className="mt-6 w-full bg-primary hover:bg-accent text-white py-3 rounded-lg font-semibold transition-colors"
>
Proceed to Checkout
</button>
<Link
to="/products"
className={`mt-3 block text-center text-sm ${darkMode ? 'text-gray-400 hover:text-primary' : 'text-gray-500 hover:text-primary'} transition-colors`}
>
Continue Shopping
</Link>
</div>
</div>
</div>
);
}
14 changes: 12 additions & 2 deletions frontend/src/components/entity/product/Products.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
Expand All @@ -44,8 +46,16 @@ 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`);
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
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/context/CartContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react';

export interface CartItem {
productId: number;
name: string;
price: number;
imgName: string;
quantity: number;
discount?: number;
}

interface CartContextType {
items: CartItem[];
addToCart: (item: Omit<CartItem, 'quantity'>, quantity: number) => void;
removeFromCart: (productId: number) => void;
updateQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
totalItems: number;
totalPrice: number;
}

const CartContext = createContext<CartContextType | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);

const addToCart = useCallback((product: Omit<CartItem, 'quantity'>, quantity: number) => {
setItems(prev => {
const existing = prev.find(i => i.productId === product.productId);
if (existing) {
return prev.map(i =>
i.productId === product.productId
? { ...i, quantity: i.quantity + quantity }
: i
);
}
return [...prev, { ...product, quantity }];
});
}, []);

const removeFromCart = useCallback((productId: number) => {
setItems(prev => prev.filter(i => i.productId !== productId));
}, []);

const updateQuantity = useCallback((productId: number, quantity: number) => {
if (quantity <= 0) {
setItems(prev => prev.filter(i => i.productId !== productId));
return;
}
setItems(prev =>
prev.map(i => (i.productId === productId ? { ...i, quantity } : i))
);
}, []);

const clearCart = useCallback(() => setItems([]), []);

const totalItems = useMemo(() => items.reduce((sum, i) => sum + i.quantity, 0), [items]);
const totalPrice = useMemo(() => items.reduce((sum, i) => {
const effectivePrice = i.discount ? i.price * (1 - i.discount) : i.price;
return sum + effectivePrice * i.quantity;
}, 0), [items]);

const value = useMemo(
() => ({ items, addToCart, removeFromCart, updateQuantity, clearCart, totalItems, totalPrice }),
[items, addToCart, removeFromCart, updateQuantity, clearCart, totalItems, totalPrice]
);

return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}

export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}