From 02c6400650cf399f8197a1b9842a8c54f3276d01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:29:18 +0000 Subject: [PATCH 1/6] Initial plan From b2ddae2daf6a2553b83fb9b62e3e21aceca942cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:38:52 +0000 Subject: [PATCH 2/6] Add Cart and Checkout components with CartContext integration Co-authored-by: yortch <4576246+yortch@users.noreply.github.com> --- frontend/src/App.tsx | 46 ++- frontend/src/components/Cart.tsx | 176 +++++++++++ frontend/src/components/Checkout.tsx | 288 ++++++++++++++++++ frontend/src/components/Navigation.tsx | 13 + .../components/entity/product/Products.tsx | 17 +- frontend/src/context/CartContext.tsx | 104 +++++++ 6 files changed, 625 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/Cart.tsx create mode 100644 frontend/src/components/Checkout.tsx create mode 100644 frontend/src/context/CartContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d0b02da..aed09d8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,26 +5,42 @@ import About from './components/About'; import Footer from './components/Footer'; import Products from './components/entity/product/Products'; import Login from './components/Login'; +import Cart from './components/Cart'; +import Checkout from './components/Checkout'; 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'; -// Wrapper component to apply theme classes +const ROUTE_PATHS = { + HOME: "/", + ABOUT: "/about", + PRODUCTS: "/products", + CART: "/cart", + CHECKOUT: "/checkout", + LOGIN: "/login", + ADMIN_PRODUCTS: "/admin/products" +}; + function ThemedApp() { - const { darkMode } = useTheme(); + const themeState = useTheme(); + const isDarkTheme = themeState.darkMode; + const backgroundClass = isDarkTheme ? 'bg-dark' : 'bg-gray-100'; return ( -
+
- } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } />
@@ -34,12 +50,16 @@ function ThemedApp() { } function App() { - return ( - - + const authWrapper = (children: React.ReactNode) => {children}; + const themeWrapper = (children: React.ReactNode) => {children}; + const cartWrapper = (children: React.ReactNode) => {children}; + + return authWrapper( + themeWrapper( + cartWrapper( - - + ) + ) ); } diff --git a/frontend/src/components/Cart.tsx b/frontend/src/components/Cart.tsx new file mode 100644 index 0000000..83c6d14 --- /dev/null +++ b/frontend/src/components/Cart.tsx @@ -0,0 +1,176 @@ +import { useNavigate } from "react-router-dom"; +import { useCart } from "../context/CartContext"; +import { useTheme } from "../context/ThemeContext"; +import { useState } from "react"; + +interface CartItemDisplayProps { + productId: number; + name: string; + description: string; + price: number; + imgName: string; + quantity: number; + discount?: number; + onQuantityChange: (id: number, newQty: number) => void; + onRemove: (id: number) => void; + isDark: boolean; +} + +function CartItemDisplay(props: CartItemDisplayProps) { + const effectivePrice = props.discount ? props.price * (1 - props.discount) : props.price; + const totalPrice = effectivePrice * props.quantity; + + return ( +
+
+
+ {props.name} +
+
+
+
+

{props.name}

+

{props.description}

+
+ +
+
+
+ Price: + {props.discount ? ( +
${props.price.toFixed(2)}${effectivePrice.toFixed(2)}
+ ) : ( + ${effectivePrice.toFixed(2)} + )} +
+
+
+ + {props.quantity} + +
+
+ Total: + ${totalPrice.toFixed(2)} +
+
+
+
+
+
+ ); +} + +export default function Cart() { + const navigate = useNavigate(); + const { cartItems, updateQuantity, removeFromCart, getTotalPrice } = useCart(); + const { darkMode } = useTheme(); + const [isProcessing] = useState(false); + + const handleItemQuantityUpdate = (productId: number, newQuantity: number) => { + if (newQuantity <= 0) { + removeFromCart(productId); + } else { + updateQuantity(productId, newQuantity); + } + }; + + const handleItemRemoval = (productId: number) => { + removeFromCart(productId); + }; + + const goToCheckout = () => { + navigate("/checkout"); + }; + + const continueBrowsing = () => { + navigate("/products"); + }; + + const pricingDetails = { + subtotalValue: getTotalPrice(), + taxRate: 0.08, + shippingCostValue: 0 + }; + + const taxValue = pricingDetails.subtotalValue * pricingDetails.taxRate; + const grandTotalValue = pricingDetails.subtotalValue + taxValue + pricingDetails.shippingCostValue; + + const hasItems = cartItems && cartItems.length > 0; + + if (!hasItems) { + return ( +
+
+

Shopping Cart

+
+ + + +

Your cart is empty

+

Add some products to get started!

+ +
+
+
+ ); + } + + return ( +
+
+

Shopping Cart

+
+
+ {cartItems.map((item) => ( + + ))} +
+
+
+

Order Summary

+
+
+ Subtotal + ${pricingDetails.subtotalValue.toFixed(2)} +
+
+ Shipping + FREE +
+
+ Tax (8%) + ${taxValue.toFixed(2)} +
+
+
+ Total + ${grandTotalValue.toFixed(2)} +
+
+
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/Checkout.tsx b/frontend/src/components/Checkout.tsx new file mode 100644 index 0000000..1d7c680 --- /dev/null +++ b/frontend/src/components/Checkout.tsx @@ -0,0 +1,288 @@ +import { useState, FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import { useCart } from "../context/CartContext"; +import { useTheme } from "../context/ThemeContext"; + +interface CheckoutFormData { + fullName: string; + email: string; + phone: string; + address: string; + city: string; + state: string; + zipCode: string; + cardNumber: string; + expiryDate: string; + cvv: string; +} + +export default function Checkout() { + const navigate = useNavigate(); + const { cartItems, getTotalPrice, clearCart } = useCart(); + const { darkMode } = useTheme(); + + const [formData, setFormData] = useState({ + fullName: "", + email: "", + phone: "", + address: "", + city: "", + state: "", + zipCode: "", + cardNumber: "", + expiryDate: "", + cvv: "" + }); + + const [showSuccessModal, setShowSuccessModal] = useState(false); + + const handleInputChange = (fieldName: keyof CheckoutFormData, value: string) => { + setFormData(prevData => ({ + ...prevData, + [fieldName]: value + })); + }; + + const handleFormSubmit = (e: FormEvent) => { + e.preventDefault(); + setShowSuccessModal(true); + clearCart(); + + setTimeout(() => { + setShowSuccessModal(false); + navigate("/products"); + }, 3000); + }; + + const calculatePricing = () => { + const subtotalAmount = getTotalPrice(); + const taxAmount = subtotalAmount * 0.08; + const shippingAmount = 0; + const totalAmount = subtotalAmount + taxAmount + shippingAmount; + + return { subtotalAmount, taxAmount, shippingAmount, totalAmount }; + }; + + const pricing = calculatePricing(); + + const inputBaseClasses = `w-full px-4 py-2 ${ + darkMode ? "bg-gray-800 text-light border-gray-700" : "bg-white text-gray-800 border-gray-300" + } rounded-lg border focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none transition-colors duration-300`; + + const labelClasses = `block ${darkMode ? "text-light" : "text-gray-800"} font-medium mb-2 transition-colors duration-300`; + + return ( +
+
+

Checkout

+ +
+
+
+

Billing Information

+ +
+
+
+ + handleInputChange("fullName", e.target.value)} + className={inputBaseClasses} + required + /> +
+
+ + handleInputChange("email", e.target.value)} + className={inputBaseClasses} + required + /> +
+
+ +
+ + handleInputChange("phone", e.target.value)} + className={inputBaseClasses} + required + /> +
+ +
+ + handleInputChange("address", e.target.value)} + className={inputBaseClasses} + required + /> +
+ +
+
+ + handleInputChange("city", e.target.value)} + className={inputBaseClasses} + required + /> +
+
+ + handleInputChange("state", e.target.value)} + className={inputBaseClasses} + required + /> +
+
+ + handleInputChange("zipCode", e.target.value)} + className={inputBaseClasses} + required + /> +
+
+ +
+ +

Payment Information

+ +
+ + handleInputChange("cardNumber", e.target.value)} + className={inputBaseClasses} + placeholder="1234 5678 9012 3456" + required + /> +
+ +
+
+ + handleInputChange("expiryDate", e.target.value)} + className={inputBaseClasses} + placeholder="MM/YY" + required + /> +
+
+ + handleInputChange("cvv", e.target.value)} + className={inputBaseClasses} + placeholder="123" + required + /> +
+
+ + +
+
+
+ +
+
+

Order Summary

+ +
+
+ {cartItems.map((item) => { + const effectivePrice = item.discount ? item.price * (1 - item.discount) : item.price; + return ( +
+
+ {item.name} +
+
+

{item.name}

+

Qty: {item.quantity}

+
+ ${(effectivePrice * item.quantity).toFixed(2)} +
+ ); + })} +
+ +
+ +
+ Subtotal + ${pricing.subtotalAmount.toFixed(2)} +
+
+ Shipping + FREE +
+
+ Tax (8%) + ${pricing.taxAmount.toFixed(2)} +
+
+
+ Total + ${pricing.totalAmount.toFixed(2)} +
+
+
+
+
+
+
+ + {showSuccessModal && ( +
+
+
+ + + +
+

Order Placed Successfully!

+

Thank you for your purchase. Redirecting to products...

+
+
+ )} +
+ ); +} diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index d7b393b..feee8da 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -1,12 +1,15 @@ 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 { getTotalItems } = useCart(); const [adminMenuOpen, setAdminMenuOpen] = useState(false); + const cartItemCount = getTotalItems(); return (
+ + + + + {cartItemCount > 0 && ( + + {cartItemCount} + + )} +
- +
diff --git a/frontend/src/components/Checkout.tsx b/frontend/src/components/Checkout.tsx index 1d7c680..f7812ad 100644 --- a/frontend/src/components/Checkout.tsx +++ b/frontend/src/components/Checkout.tsx @@ -3,6 +3,8 @@ import { useNavigate } from "react-router-dom"; import { useCart } from "../context/CartContext"; import { useTheme } from "../context/ThemeContext"; +// NOTE: This is a demo checkout form. In production, NEVER handle raw payment card data on the frontend. +// Use a PCI-DSS compliant payment gateway (Stripe, PayPal, etc.) with proper tokenization. interface CheckoutFormData { fullName: string; email: string; diff --git a/frontend/src/context/CartContext.tsx b/frontend/src/context/CartContext.tsx index 4b2ceba..cdfbf57 100644 --- a/frontend/src/context/CartContext.tsx +++ b/frontend/src/context/CartContext.tsx @@ -9,7 +9,7 @@ interface Product { sku: string; unit: string; supplierId: number; - discount?: number; + discount?: number; // Expected as decimal (0.1 = 10% off) } interface CartItem extends Product { From 3452888cad933bda4cf46ebf79427650b057a171 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:17 +0000 Subject: [PATCH 4/6] Address code review feedback: fix cart clearing timing and simplify App.tsx Co-authored-by: yortch <4576246+yortch@users.noreply.github.com> --- frontend/src/App.tsx | 6 ++---- frontend/src/components/Checkout.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aee1c33..03206ad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,13 +24,11 @@ const ROUTE_PATHS = { }; function ThemedApp() { - const themeState = useTheme(); - const isDarkTheme = themeState.darkMode; - const backgroundClass = isDarkTheme ? 'bg-dark' : 'bg-gray-100'; + const { darkMode } = useTheme(); return ( -
+
diff --git a/frontend/src/components/Checkout.tsx b/frontend/src/components/Checkout.tsx index f7812ad..2a48892 100644 --- a/frontend/src/components/Checkout.tsx +++ b/frontend/src/components/Checkout.tsx @@ -48,9 +48,9 @@ export default function Checkout() { const handleFormSubmit = (e: FormEvent) => { e.preventDefault(); setShowSuccessModal(true); - clearCart(); setTimeout(() => { + clearCart(); setShowSuccessModal(false); navigate("/products"); }, 3000); From 9a5a594a9722c65e4f21beb17c0b34c9acc5efb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:48:09 +0000 Subject: [PATCH 5/6] Extract magic numbers to constants and add cleanup for timeout Co-authored-by: yortch <4576246+yortch@users.noreply.github.com> --- frontend/src/components/Cart.tsx | 5 +++-- frontend/src/components/Checkout.tsx | 22 ++++++++++++++++------ frontend/src/constants.ts | 6 ++++++ 3 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 frontend/src/constants.ts diff --git a/frontend/src/components/Cart.tsx b/frontend/src/components/Cart.tsx index 47d390d..5561a15 100644 --- a/frontend/src/components/Cart.tsx +++ b/frontend/src/components/Cart.tsx @@ -1,6 +1,7 @@ import { useNavigate } from "react-router-dom"; import { useCart } from "../context/CartContext"; import { useTheme } from "../context/ThemeContext"; +import { TAX_RATE, SHIPPING_COST } from "../constants"; interface CartItemDisplayProps { productId: number; @@ -89,8 +90,8 @@ export default function Cart() { const pricingDetails = { subtotalValue: getTotalPrice(), - taxRate: 0.08, - shippingCostValue: 0 + taxRate: TAX_RATE, + shippingCostValue: SHIPPING_COST }; const taxValue = pricingDetails.subtotalValue * pricingDetails.taxRate; diff --git a/frontend/src/components/Checkout.tsx b/frontend/src/components/Checkout.tsx index 2a48892..b5e7c6c 100644 --- a/frontend/src/components/Checkout.tsx +++ b/frontend/src/components/Checkout.tsx @@ -1,7 +1,8 @@ -import { useState, FormEvent } from "react"; +import { useState, FormEvent, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useCart } from "../context/CartContext"; import { useTheme } from "../context/ThemeContext"; +import { TAX_RATE, SHIPPING_COST, ORDER_SUCCESS_REDIRECT_DELAY } from "../constants"; // NOTE: This is a demo checkout form. In production, NEVER handle raw payment card data on the frontend. // Use a PCI-DSS compliant payment gateway (Stripe, PayPal, etc.) with proper tokenization. @@ -22,6 +23,7 @@ export default function Checkout() { const navigate = useNavigate(); const { cartItems, getTotalPrice, clearCart } = useCart(); const { darkMode } = useTheme(); + const timeoutRef = useRef(null); const [formData, setFormData] = useState({ fullName: "", @@ -38,6 +40,14 @@ export default function Checkout() { const [showSuccessModal, setShowSuccessModal] = useState(false); + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + const handleInputChange = (fieldName: keyof CheckoutFormData, value: string) => { setFormData(prevData => ({ ...prevData, @@ -47,19 +57,19 @@ export default function Checkout() { const handleFormSubmit = (e: FormEvent) => { e.preventDefault(); + clearCart(); setShowSuccessModal(true); - setTimeout(() => { - clearCart(); + timeoutRef.current = setTimeout(() => { setShowSuccessModal(false); navigate("/products"); - }, 3000); + }, ORDER_SUCCESS_REDIRECT_DELAY); }; const calculatePricing = () => { const subtotalAmount = getTotalPrice(); - const taxAmount = subtotalAmount * 0.08; - const shippingAmount = 0; + const taxAmount = subtotalAmount * TAX_RATE; + const shippingAmount = SHIPPING_COST; const totalAmount = subtotalAmount + taxAmount + shippingAmount; return { subtotalAmount, taxAmount, shippingAmount, totalAmount }; diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts new file mode 100644 index 0000000..6c2b954 --- /dev/null +++ b/frontend/src/constants.ts @@ -0,0 +1,6 @@ +// Application-wide constants + +export const TAX_RATE = 0.08; // 8% tax rate +export const SHIPPING_COST = 0; // Free shipping + +export const ORDER_SUCCESS_REDIRECT_DELAY = 3000; // 3 seconds From 74318838fa58aeb50f790c86050d86d7891e6e82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:49:43 +0000 Subject: [PATCH 6/6] Use TAX_RATE constant for display strings to ensure consistency Co-authored-by: yortch <4576246+yortch@users.noreply.github.com> --- frontend/src/components/Cart.tsx | 2 +- frontend/src/components/Checkout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Cart.tsx b/frontend/src/components/Cart.tsx index 5561a15..4ecf8a7 100644 --- a/frontend/src/components/Cart.tsx +++ b/frontend/src/components/Cart.tsx @@ -152,7 +152,7 @@ export default function Cart() { FREE
- Tax (8%) + Tax ({(TAX_RATE * 100).toFixed(0)}%) ${taxValue.toFixed(2)}
diff --git a/frontend/src/components/Checkout.tsx b/frontend/src/components/Checkout.tsx index b5e7c6c..0682288 100644 --- a/frontend/src/components/Checkout.tsx +++ b/frontend/src/components/Checkout.tsx @@ -267,7 +267,7 @@ export default function Checkout() { FREE
- Tax (8%) + Tax ({(TAX_RATE * 100).toFixed(0)}%) ${pricing.taxAmount.toFixed(2)}