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
31 changes: 19 additions & 12 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navigation from './components/Navigation';
import Welcome from './components/Welcome';
import About from './components/About';
import Footer from './components/Footer';
import Products from './components/entity/product/Products';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import AdminProducts from './components/admin/AdminProducts';
import { useTheme } from './context/ThemeContext';
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Navigation from "./components/Navigation";
import Welcome from "./components/Welcome";
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
function ThemedApp() {
Expand All @@ -24,6 +27,8 @@ function ThemedApp() {
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/admin/products" element={<AdminProducts />} />
</Routes>
</main>
Expand All @@ -37,7 +42,9 @@ function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedApp />
<CartProvider>
<ThemedApp />
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
Expand Down
113 changes: 113 additions & 0 deletions frontend/src/components/Cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useCart } from "../context/CartContext";
import { useTheme } from "../context/ThemeContext";
import { useNavigate } from "react-router-dom";

export default function Cart() {
const cart = useCart();
const { darkMode } = useTheme();
const navigate = useNavigate();

const cartItems = cart.items;
const total = cart.totalPrice;

const handleRemove = (id: number) => {
cart.removeFromCart(id);
};

const handleUpdateQty = (id: number, qty: number) => {
cart.updateQuantity(id, qty);
};

const handleClearAll = () => {
cart.clearCart();
};

if (cartItems.length === 0) {
return (
<div className={`min-h-screen pt-20 px-4 ${darkMode ? "bg-dark" : "bg-gray-100"}`}>
<div className="max-w-4xl mx-auto">
<h1 className={`text-3xl font-bold mb-8 ${darkMode ? "text-light" : "text-gray-800"}`}>Shopping Cart</h1>
<div className={`rounded-lg p-8 text-center ${darkMode ? "bg-gray-800 text-light" : "bg-white text-gray-800"}`}>
<p className="text-xl mb-4">Your cart is empty</p>
<button onClick={() => navigate("/products")} className="bg-primary text-white px-6 py-2 rounded hover:bg-accent">
Browse Products
</button>
</div>
</div>
</div>
);
}

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

<div className="space-y-4 mb-8">
{cartItems.map(product => {
const finalPrice = product.discount ? product.price * (1 - product.discount) : product.price;
const lineTotal = finalPrice * product.quantity;

return (
<div key={product.productId} className={`rounded-lg p-4 shadow ${darkMode ? "bg-gray-800" : "bg-white"}`}>
<div className="flex items-center gap-4">
<img src={`/${product.imgName}`} alt={product.name} className={`w-20 h-20 object-contain rounded ${darkMode ? "bg-gray-700" : "bg-gray-100"}`} />

<div className="flex-grow">
<h3 className={`font-semibold ${darkMode ? "text-light" : "text-gray-800"}`}>{product.name}</h3>
<div className="mt-1">
{product.discount ? (
<div>
<span className="text-gray-500 line-through text-sm mr-2">${product.price.toFixed(2)}</span>
<span className="text-primary font-bold">${finalPrice.toFixed(2)}</span>
</div>
) : (
<span className="text-primary font-bold">${product.price.toFixed(2)}</span>
)}
</div>
</div>

<div className="flex items-center gap-4">
<div className={`flex items-center gap-2 rounded px-2 py-1 ${darkMode ? "bg-gray-700" : "bg-gray-200"}`}>
<button onClick={() => handleUpdateQty(product.productId, product.quantity - 1)} className="w-7 h-7 hover:text-primary">-</button>
<span className="min-w-[30px] text-center">{product.quantity}</span>
<button onClick={() => handleUpdateQty(product.productId, product.quantity + 1)} className="w-7 h-7 hover:text-primary">+</button>
</div>

<div className="min-w-[70px] text-right">
<span className={`font-semibold ${darkMode ? "text-light" : "text-gray-800"}`}>${lineTotal.toFixed(2)}</span>
</div>

<button onClick={() => handleRemove(product.productId)} className={`${darkMode ? "text-gray-400 hover:text-red-400" : "text-gray-600 hover:text-red-600"}`}>
</button>
</div>
</div>
</div>
);
})}
</div>

<div className={`rounded-lg p-6 shadow ${darkMode ? "bg-gray-800" : "bg-white"}`}>
<div className="flex justify-between mb-4">
<span className={`text-xl font-semibold ${darkMode ? "text-light" : "text-gray-800"}`}>Total</span>
<span className="text-2xl font-bold text-primary">${total.toFixed(2)}</span>
</div>
<div className="flex gap-4">
<button onClick={() => navigate("/products")} className={`flex-1 px-6 py-3 rounded ${darkMode ? "bg-gray-700 hover:bg-gray-600 text-light" : "bg-gray-200 hover:bg-gray-300 text-gray-800"}`}>
Continue Shopping
</button>
<button onClick={() => navigate("/checkout")} className="flex-1 bg-primary hover:bg-accent text-white px-6 py-3 rounded">
Checkout
</button>
</div>
</div>
</div>
</div>
);
}
101 changes: 101 additions & 0 deletions frontend/src/components/Checkout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useCart } from "../context/CartContext";
import { useTheme } from "../context/ThemeContext";
import { useNavigate } from "react-router-dom";
import { useState } from "react";

export default function Checkout() {
const cart = useCart();
const { darkMode } = useTheme();
const navigate = useNavigate();
const [orderPlaced, setOrderPlaced] = useState(false);

const cartItems = cart.items;
const total = cart.totalPrice;

const handlePlaceOrder = () => {
setOrderPlaced(true);
setTimeout(() => {
cart.clearCart();
navigate("/");
}, 2000);
};

if (cartItems.length === 0 && !orderPlaced) {
return (
<div className={`min-h-screen pt-20 px-4 ${darkMode ? "bg-dark" : "bg-gray-100"}`}>
<div className="max-w-4xl mx-auto">
<h1 className={`text-3xl font-bold mb-8 ${darkMode ? "text-light" : "text-gray-800"}`}>Checkout</h1>
<div className={`rounded-lg p-8 text-center ${darkMode ? "bg-gray-800 text-light" : "bg-white text-gray-800"}`}>
<p className="text-xl mb-4">Your cart is empty</p>
<button onClick={() => navigate("/products")} className="bg-primary text-white px-6 py-2 rounded hover:bg-accent">
Browse Products
</button>
</div>
</div>
</div>
);
}

if (orderPlaced) {
return (
<div className={`min-h-screen pt-20 px-4 ${darkMode ? "bg-dark" : "bg-gray-100"}`}>
<div className="max-w-4xl mx-auto">
<div className={`rounded-lg p-8 text-center ${darkMode ? "bg-gray-800 text-light" : "bg-white text-gray-800"}`}>
<div className="text-primary text-6xl mb-4">✓</div>
<h2 className="text-2xl font-bold mb-2">Order Placed Successfully!</h2>
<p className={`${darkMode ? "text-gray-400" : "text-gray-600"}`}>Thank you for your order. Redirecting to home...</p>
</div>
</div>
</div>
);
}

return (
<div className={`min-h-screen pt-20 pb-16 px-4 ${darkMode ? "bg-dark" : "bg-gray-100"}`}>
<div className="max-w-4xl mx-auto">
<h1 className={`text-3xl font-bold mb-8 ${darkMode ? "text-light" : "text-gray-800"}`}>Checkout</h1>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h2 className={`text-xl font-semibold mb-4 ${darkMode ? "text-light" : "text-gray-800"}`}>Order Summary</h2>
<div className={`rounded-lg p-4 ${darkMode ? "bg-gray-800" : "bg-white"}`}>
{cartItems.map(item => {
const finalPrice = item.discount ? item.price * (1 - item.discount) : item.price;
const lineTotal = finalPrice * item.quantity;

return (
<div key={item.productId} className={`flex justify-between py-2 border-b ${darkMode ? "border-gray-700" : "border-gray-200"}`}>
<div>
<div className={`${darkMode ? "text-light" : "text-gray-800"}`}>{item.name}</div>
<div className={`text-sm ${darkMode ? "text-gray-400" : "text-gray-600"}`}>Qty: {item.quantity}</div>
</div>
<div className={`font-semibold ${darkMode ? "text-light" : "text-gray-800"}`}>${lineTotal.toFixed(2)}</div>
</div>
);
})}
<div className={`flex justify-between pt-4 mt-4 border-t ${darkMode ? "border-gray-700" : "border-gray-200"}`}>
<span className={`text-lg font-bold ${darkMode ? "text-light" : "text-gray-800"}`}>Total</span>
<span className="text-lg font-bold text-primary">${total.toFixed(2)}</span>
</div>
</div>
</div>

<div>
<h2 className={`text-xl font-semibold mb-4 ${darkMode ? "text-light" : "text-gray-800"}`}>Shipping Information</h2>
<div className={`rounded-lg p-4 ${darkMode ? "bg-gray-800" : "bg-white"}`}>
<p className={`mb-4 ${darkMode ? "text-gray-400" : "text-gray-600"}`}>
This is a demo checkout page. No actual payment will be processed.
</p>
<button onClick={handlePlaceOrder} className="w-full bg-primary hover:bg-accent text-white px-6 py-3 rounded font-semibold">
Place Order
</button>
<button onClick={() => navigate("/cart")} className={`w-full mt-3 px-6 py-3 rounded ${darkMode ? "bg-gray-700 hover:bg-gray-600 text-light" : "bg-gray-200 hover:bg-gray-300 text-gray-800"}`}>
Back to Cart
</button>
</div>
</div>
</div>
</div>
</div>
);
}
16 changes: 12 additions & 4 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 { useState } from 'react';
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 @@ -83,6 +85,12 @@ export default function Navigation() {
</svg>
)}
</button>
<Link to="/cart" className="relative p-2" aria-label="View cart">
<svg className={`h-6 w-6 ${darkMode ? "text-light" : "text-gray-700"}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
{totalItems > 0 && <span className="absolute -top-1 -right-1 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-bold">{totalItems}</span>}
</Link>
{isLoggedIn ? (
<>
<span className={`${darkMode ? 'text-light' : 'text-gray-700'} text-sm transition-colors`}>
Expand Down
37 changes: 24 additions & 13 deletions frontend/src/components/entity/product/Products.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';
import { api } from '../../../api/config';
import { useTheme } from '../../../context/ThemeContext';
import { useState } from "react";
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 @@ -23,11 +24,12 @@ const fetchProducts = async (): Promise<Product[]> => {

export default function Products() {
const [quantities, setQuantities] = useState<Record<number, number>>({});
const [searchTerm, setSearchTerm] = useState('');
const [searchTerm, setSearchTerm] = useState("");
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [showModal, setShowModal] = useState(false);
const { data: products, isLoading, error } = useQuery('products', fetchProducts);
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,12 +46,21 @@ 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
}));
}
}
};

Expand Down
Loading