272 lines
9.1 KiB
TypeScript
272 lines
9.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Search, Filter, X } from 'lucide-react';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import { useCartStore } from '@/lib/cart/store';
|
|
import { Button } from '@/components/ui/button';
|
|
import Container from '@/components/Layout/Container';
|
|
import { ProductCard } from '@/components/ProductCard';
|
|
import { toast } from 'sonner';
|
|
import { useTheme, useLayout } from '@/contexts/ThemeContext';
|
|
import { useShopSettings } from '@/hooks/useAppearanceSettings';
|
|
import SEOHead from '@/components/SEOHead';
|
|
import type { ProductsResponse, ProductCategory, Product } from '@/types/product';
|
|
|
|
export default function Shop() {
|
|
const navigate = useNavigate();
|
|
const { config } = useTheme();
|
|
const { layout } = useLayout();
|
|
const { layout: shopLayout, elements } = useShopSettings();
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState('');
|
|
const [category, setCategory] = useState('');
|
|
const [sortBy, setSortBy] = useState('');
|
|
const { addItem } = useCartStore();
|
|
|
|
// Map grid columns setting to Tailwind classes (responsive)
|
|
const gridCols = typeof shopLayout.grid_columns === 'object'
|
|
? shopLayout.grid_columns
|
|
: { mobile: '2', tablet: '3', desktop: '4' };
|
|
|
|
// Map to actual Tailwind classes (can't use template literals due to purging)
|
|
const mobileClass = {
|
|
'1': 'grid-cols-1',
|
|
'2': 'grid-cols-2',
|
|
'3': 'grid-cols-3',
|
|
}[gridCols.mobile] || 'grid-cols-2';
|
|
|
|
const tabletClass = {
|
|
'2': 'md:grid-cols-2',
|
|
'3': 'md:grid-cols-3',
|
|
'4': 'md:grid-cols-4',
|
|
}[gridCols.tablet] || 'md:grid-cols-3';
|
|
|
|
const desktopClass = {
|
|
'2': 'lg:grid-cols-2',
|
|
'3': 'lg:grid-cols-3',
|
|
'4': 'lg:grid-cols-4',
|
|
'5': 'lg:grid-cols-5',
|
|
'6': 'lg:grid-cols-6',
|
|
}[gridCols.desktop] || 'lg:grid-cols-4';
|
|
|
|
const gridColsClass = `${mobileClass} ${tabletClass} ${desktopClass}`;
|
|
|
|
// Masonry column classes
|
|
const masonryMobileClass = {
|
|
'1': 'columns-1',
|
|
'2': 'columns-2',
|
|
'3': 'columns-3',
|
|
}[gridCols.mobile] || 'columns-2';
|
|
|
|
const masonryTabletClass = {
|
|
'2': 'md:columns-2',
|
|
'3': 'md:columns-3',
|
|
'4': 'md:columns-4',
|
|
}[gridCols.tablet] || 'md:columns-3';
|
|
|
|
const masonryDesktopClass = {
|
|
'2': 'lg:columns-2',
|
|
'3': 'lg:columns-3',
|
|
'4': 'lg:columns-4',
|
|
'5': 'lg:columns-5',
|
|
'6': 'lg:columns-6',
|
|
}[gridCols.desktop] || 'lg:columns-4';
|
|
|
|
const masonryColsClass = `${masonryMobileClass} ${masonryTabletClass} ${masonryDesktopClass}`;
|
|
|
|
const isMasonry = shopLayout.grid_style === 'masonry';
|
|
|
|
// Fetch products
|
|
const { data: productsData, isLoading: productsLoading } = useQuery<ProductsResponse>({
|
|
queryKey: ['products', page, search, category],
|
|
queryFn: () => apiClient.get(apiClient.endpoints.shop.products, {
|
|
page,
|
|
per_page: 12,
|
|
search,
|
|
category,
|
|
}),
|
|
});
|
|
|
|
// Fetch categories
|
|
const { data: categories } = useQuery<ProductCategory[]>({
|
|
queryKey: ['categories'],
|
|
queryFn: () => apiClient.get(apiClient.endpoints.shop.categories),
|
|
});
|
|
|
|
const handleAddToCart = async (product: any) => {
|
|
try {
|
|
const response = await apiClient.post(apiClient.endpoints.cart.add, {
|
|
product_id: product.id,
|
|
quantity: 1,
|
|
});
|
|
|
|
// Add to local cart store
|
|
addItem({
|
|
key: `${product.id}`,
|
|
product_id: product.id,
|
|
name: product.name,
|
|
price: parseFloat(product.price),
|
|
quantity: 1,
|
|
image: product.image,
|
|
virtual: product.virtual,
|
|
downloadable: product.downloadable,
|
|
});
|
|
|
|
toast.success(`${product.name} added to cart!`, {
|
|
action: {
|
|
label: 'View Cart',
|
|
onClick: () => navigate('/cart'),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
toast.error('Failed to add to cart');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Container>
|
|
{/* SEO Meta Tags for Social Sharing */}
|
|
<SEOHead
|
|
title="Shop"
|
|
description="Browse our collection of products"
|
|
/>
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl font-bold mb-2">Shop</h1>
|
|
<p className="text-muted-foreground">Browse our collection of products</p>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
{(elements.search_bar || elements.category_filter) && (
|
|
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
|
{/* Search */}
|
|
{elements.search_bar && (
|
|
<div className="flex-1 relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search products..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full !pl-10 pr-10 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
{search && (
|
|
<button
|
|
onClick={() => setSearch('')}
|
|
className="font-[inherit] absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-100 rounded-full transition-colors"
|
|
>
|
|
<X className="h-4 w-4 text-muted-foreground" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Category Filter */}
|
|
{elements.category_filter && categories && categories.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
|
<select
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
className="px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
<option value="">All Categories</option>
|
|
{categories.map((cat: any) => (
|
|
<option key={cat.id} value={cat.slug}>
|
|
{cat.name} ({cat.count})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sort Dropdown */}
|
|
{elements.sort_dropdown && (
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
className="px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
>
|
|
<option value="">Default sorting</option>
|
|
<option value="popularity">Sort by popularity</option>
|
|
<option value="rating">Sort by average rating</option>
|
|
<option value="date">Sort by latest</option>
|
|
<option value="price">Sort by price: low to high</option>
|
|
<option value="price-desc">Sort by price: high to low</option>
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Products Grid */}
|
|
{productsLoading ? (
|
|
<div className={`grid ${gridColsClass} gap-6`}>
|
|
{[...Array(8)].map((_, i) => (
|
|
<div key={i} className="animate-pulse">
|
|
<div className="bg-gray-200 aspect-square rounded-lg mb-4" />
|
|
<div className="h-4 bg-gray-200 rounded mb-2" />
|
|
<div className="h-4 bg-gray-200 rounded w-2/3" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : productsData?.products && productsData.products.length > 0 ? (
|
|
<>
|
|
<div className={isMasonry ? `${masonryColsClass} gap-6` : `grid ${gridColsClass} gap-6`}>
|
|
{productsData.products.map((product: any) => (
|
|
<div key={product.id} className={isMasonry ? 'mb-6 break-inside-avoid' : ''}>
|
|
<ProductCard
|
|
product={product}
|
|
onAddToCart={handleAddToCart}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
{productsData.total_pages > 1 && (
|
|
<div className="flex justify-center gap-2 mt-8">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<span className="flex items-center px-4">
|
|
Page {page} of {productsData.total_pages}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setPage(p => Math.min(productsData.total_pages, p + 1))}
|
|
disabled={page === productsData.total_pages}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<p className="text-muted-foreground text-lg">No products found</p>
|
|
{(search || category) && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setSearch('');
|
|
setCategory('');
|
|
}}
|
|
className="mt-4"
|
|
>
|
|
Clear Filters
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Container>
|
|
);
|
|
}
|