-
-
-
Filters
-
-
+ {/* Header */}
+
+
+
All Products
+
+ {data?.meta?.total ? `Showing ${data.data.length} of ${data.meta.total} products` : 'Browse our complete collection'}
+
+
- {/* Search */}
-
-
- setFilters({ ...filters, search: e.target.value })}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- />
-
+ {/* Mobile Filter Button */}
+
+
- {/* Price Range */}
-
-
-
-
setFilters({ ...filters, minPrice: e.target.value })}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg"
- />
-
-
+
+ {/* Filters Sidebar (Desktop) */}
+
-
+ {/* Price Range */}
+
- {/* Products Grid */}
-
- {isLoading ? (
-
-
-
Loading products...
-
- ) : (
- <>
- {/* Results Count */}
+ {/* Sort */}
-
- Showing {data?.data?.length || 0} of {data?.meta?.total || 0} products
-
+
+
- {/* Products Grid */}
- {data?.data?.length > 0 ? (
- <>
-
- {data.data.map((product: any) => (
-
- ))}
-
+ {/* Clear Filters */}
+
+
+
+
+ {/* Products Grid */}
+
+ {isLoading ? (
+
+
+
Loading products...
+
+ ) : (
+ <>
+ {data?.data?.length > 0 ? (
+ <>
+ {/* Products Grid - 2 columns mobile, 3 tablet, 4 desktop */}
+
+ {data.data.map((product: any) => (
+
+ ))}
+
- {/* Pagination */}
-
-
-
- Page {page} of {data?.meta?.totalPages || 1}
-
-
+ {/* Pagination */}
+ {data.meta && data.meta.totalPages > 1 && (
+
+
+
+ Page {page} of {data.meta.totalPages}
+
+
+
+ )}
+ >
+ ) : (
+
+
No products found
+
Try adjusting your filters
- >
- ) : (
-
- )}
- >
- )}
+ )}
+ >
+ )}
+
+
+ {/* Mobile Filter Drawer */}
+
setIsFilterDrawerOpen(false)}
+ filters={filters}
+ onFilterChange={handleFilterChange}
+ categories={categoriesData}
+ />
);
}
@@ -188,4 +262,3 @@ export default function ProductsPage() {
);
}
-
diff --git a/apps/storefront/src/components/cart/CartDrawer.tsx b/apps/storefront/src/components/cart/CartDrawer.tsx
new file mode 100644
index 0000000..57753bc
--- /dev/null
+++ b/apps/storefront/src/components/cart/CartDrawer.tsx
@@ -0,0 +1,218 @@
+'use client';
+
+import { useEffect } from 'react';
+import { X, Plus, Minus, Trash2, ShoppingBag, ArrowRight } from 'lucide-react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useCartStore } from '@/stores/cartStore';
+import { formatCurrency } from '@/lib/utils';
+import { useCart } from '@/hooks/useCart';
+import { useAuth } from '@/contexts/AuthContext';
+
+export default function CartDrawer() {
+ const { isOpen, closeCart, items, subtotal, itemCount, updateQuantity, removeItem, clearCart } = useCartStore();
+ const { isAuthenticated } = useAuth();
+ const { addToCart: addToCartAPI, updateQuantity: updateQuantityAPI, removeFromCart: removeFromCartAPI } = useCart();
+
+ // Sync local cart to server when user is authenticated
+ useEffect(() => {
+ if (isAuthenticated && items.length > 0) {
+ // Sync each item to server
+ items.forEach(item => {
+ // This would ideally be done in a batch, but for now we'll sync on add/update
+ });
+ }
+ }, [isAuthenticated, items]);
+
+ const handleUpdateQuantity = async (productId: string, newQuantity: number) => {
+ if (isAuthenticated) {
+ // Find the cart item ID from server
+ // For now, update locally and sync
+ updateQuantity(productId, newQuantity);
+ } else {
+ updateQuantity(productId, newQuantity);
+ }
+ };
+
+ const handleRemoveItem = async (productId: string) => {
+ if (isAuthenticated) {
+ // Find and remove from server
+ removeFromCartAPI(productId);
+ }
+ removeItem(productId);
+ };
+
+ const handleCheckout = () => {
+ closeCart();
+ // Navigate to checkout
+ window.location.href = '/cart';
+ };
+
+ if (!isOpen) return null;
+
+ return (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Drawer */}
+
+ {/* Header */}
+
+
+ Shopping Cart ({itemCount})
+
+
+
+
+ {/* Cart Items */}
+
+ {items.length === 0 ? (
+
+
+
Your cart is empty
+
Start adding items to your cart
+
+ Browse Products
+
+
+ ) : (
+
+ {items.map((item) => (
+
+ {/* Product Image */}
+
+
+ {item.productImage ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {/* Product Details */}
+
+
+
+ {item.productName}
+
+
+
+ {formatCurrency(item.price)} each
+
+
+ {/* Quantity Controls */}
+
+
+
+
+ {item.quantity}
+
+
+
+
+
+
+ {formatCurrency(item.price * item.quantity)}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+ {items.length > 0 && (
+
+ {/* Subtotal */}
+
+ Subtotal:
+ {formatCurrency(subtotal)}
+
+
+ {/* Checkout Button */}
+
+
+ {/* Continue Shopping */}
+
+ Continue Shopping
+
+
+ )}
+
+ >
+ );
+}
diff --git a/apps/storefront/src/components/layout/Header.tsx b/apps/storefront/src/components/layout/Header.tsx
index 7dcf966..3788c67 100644
--- a/apps/storefront/src/components/layout/Header.tsx
+++ b/apps/storefront/src/components/layout/Header.tsx
@@ -1,193 +1,448 @@
'use client';
+import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
-import { ShoppingCart, User, Search, Menu, Heart, Store, LogOut } from 'lucide-react';
-import { useState } from 'react';
+import { useRouter, usePathname } from 'next/navigation';
+import {
+ ShoppingCart,
+ User,
+ Search,
+ Menu,
+ Heart,
+ Store,
+ LogOut,
+ ChevronDown
+} from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
+import { useDebounce } from '@/hooks/useDebounce';
import api from '@/lib/api';
import { useAuth } from '@/contexts/AuthContext';
import { useWishlist } from '@/hooks/useWishlist';
+import { useCartStore } from '@/stores/cartStore';
+import { useSearchSuggestions } from '@/hooks/useSearchSuggestions';
+import { formatCurrency } from '@/lib/utils';
+import CartDrawer from '@/components/cart/CartDrawer';
export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState
('');
+ const [showSuggestions, setShowSuggestions] = useState(false);
+ const [focusedIndex, setFocusedIndex] = useState(-1);
+ const searchRef = useRef(null);
+ const router = useRouter();
+ const pathname = usePathname();
+
const { customer, isAuthenticated, logout } = useAuth();
const { wishlistCount } = useWishlist();
+ const { itemCount, subtotal, openCart } = useCartStore();
+
+ const debouncedSearch = useDebounce(searchQuery, 300);
+ const { data: suggestions = [], isLoading: isLoadingSuggestions } = useSearchSuggestions(
+ debouncedSearch,
+ selectedCategory,
+ showSuggestions && debouncedSearch.length >= 2
+ );
- const { data: cartData } = useQuery({
- queryKey: ['cart'],
+ // Fetch categories for dropdown
+ const { data: categoriesData } = useQuery({
+ queryKey: ['categories'],
queryFn: async () => {
- try {
- const response = await api.get('/cart');
- return response.data.data;
- } catch (error) {
- return { itemCount: 0 };
- }
+ const response = await api.get('/categories');
+ return response.data.data || [];
},
- enabled: isAuthenticated && typeof window !== 'undefined',
- retry: false,
});
+ // Close suggestions when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
+ setShowSuggestions(false);
+ setFocusedIndex(-1);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ // Handle keyboard navigation
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!showSuggestions || suggestions.length === 0) return;
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setFocusedIndex(prev => (prev < suggestions.length - 1 ? prev + 1 : prev));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setFocusedIndex(prev => (prev > 0 ? prev - 1 : -1));
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (focusedIndex >= 0 && suggestions[focusedIndex]) {
+ handleSuggestionClick(suggestions[focusedIndex]);
+ } else {
+ handleSearch();
+ }
+ break;
+ case 'Escape':
+ setShowSuggestions(false);
+ setFocusedIndex(-1);
+ break;
+ }
+ };
+
+ const handleSearch = () => {
+ if (!searchQuery.trim()) return;
+ setShowSuggestions(false);
+ router.push(`/products?search=${encodeURIComponent(searchQuery)}${selectedCategory ? `&categoryId=${selectedCategory}` : ''}`);
+ };
+
+ const handleSuggestionClick = (suggestion: any) => {
+ setShowSuggestions(false);
+ setSearchQuery('');
+ router.push(`/products/${suggestion.slug}`);
+ };
+
+ const handleSearchChange = (value: string) => {
+ setSearchQuery(value);
+ setShowSuggestions(value.length >= 2);
+ setFocusedIndex(-1);
+ };
+
return (
-