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 Wishlist from './components/Wishlist';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import { WishlistProvider } from './context/WishlistContext';
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="/wishlist" element={<Wishlist />} />
<Route path="/login" element={<Login />} />
<Route path="/admin/products" element={<AdminProducts />} />
</Routes>
Expand All @@ -37,7 +40,9 @@ function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedApp />
<WishlistProvider>
<ThemedApp />
</WishlistProvider>
</ThemeProvider>
</AuthProvider>
);
Expand Down
10 changes: 10 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 { useWishlist } from '../context/WishlistContext';
import { useState } from 'react';

export default function Navigation() {
const { isLoggedIn, isAdmin, logout } = useAuth();
const { darkMode, toggleTheme } = useTheme();
const { wishlistItems } = useWishlist();
const [adminMenuOpen, setAdminMenuOpen] = useState(false);

return (
Expand All @@ -29,6 +31,14 @@ export default function Navigation() {
<div className="ml-10 flex items-baseline space-x-4">
<Link to="/" className={`${darkMode ? 'text-light hover:text-primary' : 'text-gray-700 hover:text-primary'} px-3 py-2 rounded-md text-sm font-medium transition-colors`}>Home</Link>
<Link to="/products" className={`${darkMode ? 'text-light hover:text-primary' : 'text-gray-700 hover:text-primary'} px-3 py-2 rounded-md text-sm font-medium transition-colors`}>Products</Link>
<Link to="/wishlist" className={`${darkMode ? 'text-light hover:text-primary' : 'text-gray-700 hover:text-primary'} px-3 py-2 rounded-md text-sm font-medium flex items-center transition-colors relative`}>
Wishlist
{wishlistItems.length > 0 && (
<span className="ml-1 bg-primary text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{wishlistItems.length}
</span>
)}
</Link>
<Link to="/about" className={`${darkMode ? 'text-light hover:text-primary' : 'text-gray-700 hover:text-primary'} px-3 py-2 rounded-md text-sm font-medium transition-colors`}>About us</Link>
{isAdmin && (
<div className="relative">
Expand Down
126 changes: 126 additions & 0 deletions frontend/src/components/Wishlist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useQuery } from 'react-query';
import axios from 'axios';
import { useWishlist } from '../context/WishlistContext';
import { useTheme } from '../context/ThemeContext';
import { api } from '../api/config';

interface Product {
productId: number;
name: string;
description: string;
price: number;
imgName: string;
sku: string;
unit: string;
supplierId: number;
discount?: number;
}

const fetchProducts = async (): Promise<Product[]> => {
const { data } = await axios.get(`${api.baseURL}${api.endpoints.products}`);
return data;
};

export default function Wishlist() {
const { wishlistItems, removeFromWishlist } = useWishlist();
const { darkMode } = useTheme();
const { data: allProducts, isLoading } = useQuery('products', fetchProducts);

const wishlistProducts = allProducts?.filter(product =>
wishlistItems.includes(product.productId)
) || [];

if (isLoading) {
return (
<div className={`min-h-screen ${darkMode ? 'bg-dark' : 'bg-gray-100'} pt-20 px-4 transition-colors duration-300`}>
<div className="max-w-7xl mx-auto">
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-primary"></div>
</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-7xl mx-auto">
<div className="flex flex-col space-y-6">
<h1 className={`text-3xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'} transition-colors duration-300`}>
My Wishlist
</h1>

{wishlistProducts.length === 0 ? (
<div className={`flex flex-col items-center justify-center py-16 ${darkMode ? 'text-gray-400' : 'text-gray-600'}`}>
<svg
className="w-24 h-24 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<p className="text-xl font-semibold mb-2">Your wishlist is empty</p>
<p className="text-sm">Add products to your wishlist to save them for later</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{wishlistProducts.map(product => (
<div key={product.productId} className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg overflow-hidden shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-[0_0_25px_rgba(118,184,82,0.3)] flex flex-col`}>
<div className={`relative h-56 ${darkMode ? 'bg-gradient-to-t from-gray-700 to-gray-800' : 'bg-gradient-to-t from-gray-100 to-white'} transition-colors duration-300`}>
<img
src={`/${product.imgName}`}
alt={product.name}
className="w-full h-full object-contain p-2"
/>
{product.discount && (
<div className="absolute top-8 left-0 bg-primary text-white px-3 py-1 -rotate-90 transform -translate-x-5 shadow-md">
{Math.round(product.discount * 100)}% OFF
</div>
)}
<button
onClick={() => removeFromWishlist(product.productId)}
className="absolute top-2 right-2 p-2 rounded-full bg-white/90 hover:bg-white transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label={`Remove ${product.name} from wishlist`}
>
<svg
className="w-6 h-6 text-primary fill-current"
viewBox="0 0 24 24"
>
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</button>
</div>

<div className="p-4 flex flex-col flex-grow">
<h3 className={`text-xl font-semibold ${darkMode ? 'text-light' : 'text-gray-800'} mb-2 transition-colors duration-300`}>
{product.name}
</h3>
<p className={`${darkMode ? 'text-gray-400' : 'text-gray-600'} mb-4 flex-grow transition-colors duration-300`}>
{product.description}
</p>
<div className="flex justify-between items-center mt-auto">
{product.discount ? (
<div>
<span className="text-gray-500 line-through text-sm mr-2">${product.price.toFixed(2)}</span>
<span className="text-primary text-xl font-bold">${(product.price * (1 - product.discount)).toFixed(2)}</span>
</div>
) : (
<span className="text-primary text-xl font-bold">${product.price.toFixed(2)}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}
29 changes: 29 additions & 0 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 { useWishlist } from '../../../context/WishlistContext';

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 { addToWishlist, removeFromWishlist, isInWishlist } = useWishlist();

const filteredProducts = products?.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
Expand All @@ -53,6 +55,15 @@ export default function Products() {
}
};

const handleWishlistToggle = (productId: number, event: React.MouseEvent) => {
event.stopPropagation();
if (isInWishlist(productId)) {
removeFromWishlist(productId);
} else {
addToWishlist(productId);
}
};

const handleProductClick = (product: Product) => {
setSelectedProduct(product);
setShowModal(true);
Expand Down Expand Up @@ -125,6 +136,24 @@ export default function Products() {
{Math.round(product.discount * 100)}% OFF
</div>
)}
<button
onClick={(e) => handleWishlistToggle(product.productId, e)}
className="absolute top-2 right-2 p-2 rounded-full bg-white/90 hover:bg-white transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label={isInWishlist(product.productId) ? `Remove ${product.name} from wishlist` : `Add ${product.name} to wishlist`}
>
<svg
className={`w-6 h-6 transition-colors duration-300 ${isInWishlist(product.productId) ? 'text-primary fill-current' : 'text-gray-400'}`}
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</button>
</div>

<div className="p-4 flex flex-col flex-grow">
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/context/WishlistContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface WishlistContextType {
wishlistItems: number[];
addToWishlist: (productId: number) => void;
removeFromWishlist: (productId: number) => void;
isInWishlist: (productId: number) => boolean;
}

const WishlistContext = createContext<WishlistContextType | undefined>(undefined);

const WISHLIST_KEY = 'octocat-wishlist';

export function WishlistProvider({ children }: { children: ReactNode }) {
const [wishlistItems, setWishlistItems] = useState<number[]>(() => {
try {
const saved = localStorage.getItem(WISHLIST_KEY);
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.error('Error loading wishlist from localStorage:', error);
return [];
}
});

useEffect(() => {
try {
localStorage.setItem(WISHLIST_KEY, JSON.stringify(wishlistItems));
} catch (error) {
console.error('Error saving wishlist to localStorage:', error);
}
}, [wishlistItems]);

const addToWishlist = (productId: number) => {
setWishlistItems(prev => {
if (!prev.includes(productId)) {
return [...prev, productId];
}
return prev;
});
};

const removeFromWishlist = (productId: number) => {
setWishlistItems(prev => prev.filter(id => id !== productId));
};

const isInWishlist = (productId: number) => {
return wishlistItems.includes(productId);
};

return (
<WishlistContext.Provider value={{ wishlistItems, addToWishlist, removeFromWishlist, isInWishlist }}>
{children}
</WishlistContext.Provider>
);
}

export function useWishlist() {
const context = useContext(WishlistContext);
if (!context) {
throw new Error('useWishlist must be used within a WishlistProvider');
}
return context;
}