Files
template-ecommerce/src/pages/Products.tsx
T

304 lines
18 KiB
TypeScript

import { useState } from "react";
import { toast } from "sonner";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Dialog, DialogContent, DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Star, Heart, SlidersHorizontal, ChevronLeft, ChevronRight, Grid3X3, LayoutList, X, ShoppingBag
} from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const allProducts = [
{ id: 1, name: "Classic Leather Jacket", price: 189, originalPrice: 249, rating: 5, badge: "Sale", gradient: "from-rose-100 to-stone-200", brand: "StyleHaus", size: ["S","M","L","XL"], color: "black" },
{ id: 2, name: "Silk Evening Dress", price: 245, originalPrice: null, rating: 5, badge: "New", gradient: "from-amber-100 to-orange-200", brand: "Luxe", size: ["XS","S","M"], color: "rose" },
{ id: 3, name: "Cashmere Knit Sweater", price: 128, originalPrice: null, rating: 4, badge: null, gradient: "from-stone-100 to-stone-300", brand: "StyleHaus", size: ["S","M","L"], color: "stone" },
{ id: 4, name: "Linen Wide-Leg Trousers", price: 95, originalPrice: 135, rating: 4, badge: "Sale", gradient: "from-rose-50 to-amber-100", brand: "Atelier", size: ["S","M","L","XL"], color: "cream" },
{ id: 5, name: "Italian Leather Handbag", price: 320, originalPrice: null, rating: 5, badge: "New", gradient: "from-stone-200 to-rose-100", brand: "Luxe", size: ["One Size"], color: "brown" },
{ id: 6, name: "Merino Wool Blazer", price: 165, originalPrice: null, rating: 4, badge: null, gradient: "from-amber-50 to-stone-200", brand: "StyleHaus", size: ["S","M","L","XL","XXL"], color: "navy" },
{ id: 7, name: "Organic Cotton Tee", price: 45, originalPrice: null, rating: 4, badge: null, gradient: "from-rose-100 to-rose-200", brand: "Atelier", size: ["XS","S","M","L","XL"], color: "white" },
{ id: 8, name: "Velvet Midi Skirt", price: 115, originalPrice: 155, rating: 5, badge: "Sale", gradient: "from-stone-300 to-stone-100", brand: "Luxe", size: ["XS","S","M","L"], color: "rose" },
{ id: 9, name: "Suede Ankle Boots", price: 198, originalPrice: null, rating: 5, badge: "New", gradient: "from-amber-200 to-stone-200", brand: "StyleHaus", size: ["36","37","38","39","40","41"], color: "brown" },
{ id: 10, name: "Satin Blouse", price: 89, originalPrice: null, rating: 4, badge: null, gradient: "from-rose-50 to-rose-100", brand: "Atelier", size: ["XS","S","M","L"], color: "cream" },
{ id: 11, name: "Tailored Wool Coat", price: 385, originalPrice: null, rating: 5, badge: null, gradient: "from-stone-200 to-stone-400", brand: "Luxe", size: ["S","M","L","XL"], color: "stone" },
{ id: 12, name: "Pearl Drop Earrings", price: 68, originalPrice: null, rating: 4, badge: "New", gradient: "from-amber-100 to-rose-100", brand: "Luxe", size: ["One Size"], color: "gold" },
];
const colorOptions = [
{ name: "Black", value: "black", class: "bg-stone-900" },
{ name: "Rose", value: "rose", class: "bg-rose-400" },
{ name: "Stone", value: "stone", class: "bg-stone-400" },
{ name: "Cream", value: "cream", class: "bg-amber-100 border border-stone-200" },
{ name: "Brown", value: "brown", class: "bg-amber-700" },
{ name: "Navy", value: "navy", class: "bg-blue-900" },
{ name: "White", value: "white", class: "bg-white border border-stone-200" },
{ name: "Gold", value: "gold", class: "bg-amber-400" },
];
export default function ProductsPage() {
const [sort, setSort] = useState("featured");
const [page, setPage] = useState(1);
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
const [selectedColors, setSelectedColors] = useState<string[]>([]);
const [priceRange, setPriceRange] = useState<[number, number]>([0, 500]);
const [showFilters, setShowFilters] = useState(false);
const [wishlist, setWishlist] = useState<number[]>([]);
const [quickView, setQuickView] = useState<number | null>(null);
const [selectedSize, setSelectedSize] = useState<string>("");
const toggleBrand = (b: string) =>
setSelectedBrands((prev) => prev.includes(b) ? prev.filter((x) => x !== b) : [...prev, b]);
const toggleColor = (c: string) =>
setSelectedColors((prev) => prev.includes(c) ? prev.filter((x) => x !== c) : [...prev, c]);
const toggleWishlist = (id: number) => {
const isAdding = !wishlist.includes(id);
setWishlist((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
if (isAdding) toast.success("Added to wishlist!");
};
const filtered = allProducts.filter((p) => {
if (selectedBrands.length && !selectedBrands.includes(p.brand)) return false;
if (selectedColors.length && !selectedColors.includes(p.color)) return false;
if (p.price < priceRange[0] || p.price > priceRange[1]) return false;
return true;
});
const renderStars = (count: number) =>
Array.from({ length: 5 }, (_, i) => (
<Star key={i} className={`h-3 w-3 \${i < count ? "fill-amber-400 text-amber-400" : "text-stone-300"}`} />
));
const Sidebar = () => (
<div className="space-y-6">
<div>
<h3 className="font-semibold text-sm mb-3">Price Range</h3>
<div className="flex items-center gap-2 text-sm">
<Input type="number" value={priceRange[0]} onChange={(e) => setPriceRange([+e.target.value, priceRange[1]])} className="w-20 h-8 text-xs" />
<span className="text-stone-400">to</span>
<Input type="number" value={priceRange[1]} onChange={(e) => setPriceRange([priceRange[0], +e.target.value])} className="w-20 h-8 text-xs" />
</div>
</div>
<Separator />
<div>
<h3 className="font-semibold text-sm mb-3">Brand</h3>
<div className="space-y-2">
{["StyleHaus", "Luxe", "Atelier"].map((b) => (
<label key={b} className="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" checked={selectedBrands.includes(b)} onChange={() => toggleBrand(b)} className="rounded border-stone-300 text-rose-500 focus:ring-rose-500" />
{b}
</label>
))}
</div>
</div>
<Separator />
<div>
<h3 className="font-semibold text-sm mb-3">Color</h3>
<div className="flex flex-wrap gap-2">
{colorOptions.map((c) => (
<button
key={c.value}
onClick={() => toggleColor(c.value)}
className={`h-7 w-7 rounded-full \${c.class} \${selectedColors.includes(c.value) ? "ring-2 ring-offset-2 ring-rose-500" : ""} transition-all`}
title={c.name}
/>
))}
</div>
</div>
<Separator />
<Button variant="outline" size="sm" className="w-full" onClick={() => { setSelectedBrands([]); setSelectedColors([]); setPriceRange([0, 500]); }}>
Clear All Filters
</Button>
</div>
);
return (
<div className="min-h-screen bg-white text-stone-900 antialiased">
<Header />
<div className="mx-auto max-w-7xl px-6 pt-28 pb-20">
{/* Top Bar */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight animate-fade-up">All Products</h1>
<p className="mt-1 text-sm text-stone-500">{filtered.length} products found</p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" className="lg:hidden" onClick={() => setShowFilters(!showFilters)}>
<SlidersHorizontal className="h-4 w-4 mr-1.5" /> Filters
</Button>
<Select value={sort} onValueChange={setSort}>
<SelectTrigger className="w-[160px] h-9 text-sm">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="featured">Featured</SelectItem>
<SelectItem value="price-asc">Price: Low to High</SelectItem>
<SelectItem value="price-desc">Price: High to Low</SelectItem>
<SelectItem value="newest">Newest</SelectItem>
<SelectItem value="rating">Top Rated</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Active Filters */}
{(selectedBrands.length > 0 || selectedColors.length > 0) && (
<div className="flex flex-wrap gap-2 mb-6">
{selectedBrands.map((b) => (
<Badge key={b} variant="secondary" className="cursor-pointer gap-1" onClick={() => toggleBrand(b)}>
{b} <X className="h-3 w-3" />
</Badge>
))}
{selectedColors.map((c) => (
<Badge key={c} variant="secondary" className="cursor-pointer gap-1" onClick={() => toggleColor(c)}>
{c} <X className="h-3 w-3" />
</Badge>
))}
</div>
)}
<div className="flex gap-8">
{/* Sidebar */}
<aside className={`w-56 shrink-0 \${showFilters ? "block" : "hidden"} lg:block`}>
<Sidebar />
</aside>
{/* Product Grid */}
<div className="flex-1">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 stagger">
{filtered.map((product, i) => (
<Dialog key={product.id} open={quickView === product.id} onOpenChange={(open) => { setQuickView(open ? product.id : null); if (!open) setSelectedSize(""); }}>
<DialogTrigger asChild>
<button className="group text-left w-full">
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
<div className={`relative aspect-[3/4] bg-gradient-to-br \${product.gradient} overflow-hidden`}>
<div className="absolute inset-0 transition-transform duration-300 group-hover:scale-110 group-hover:brightness-110" style={{ backgroundImage: "radial-gradient(circle, rgba(0,0,0,0.06) 1px, transparent 1px)", backgroundSize: "16px 16px" }} />
<div className="absolute top-1/4 -left-8 h-24 w-24 rounded-full bg-white/30 blur-2xl" />
<div className="absolute bottom-1/4 -right-8 h-20 w-20 rounded-full bg-white/20 blur-2xl" />
<div className="absolute inset-0 flex items-center justify-center text-stone-400 text-xs transition-transform duration-300 group-hover:scale-110 group-hover:brightness-110">Image</div>
{product.badge && (
<Badge className={`absolute top-2 left-2 border-0 text-white text-xs \${product.badge === "Sale" ? "bg-rose-500" : "bg-stone-900"}`}>
{product.badge}
</Badge>
)}
<span
onClick={(e) => { e.stopPropagation(); toggleWishlist(product.id); }}
className={`absolute top-2 right-2 h-7 w-7 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all hover:scale-125 active:scale-90 \${wishlist.includes(product.id) ? "!opacity-100" : ""}`}
>
<Heart className={`h-3.5 w-3.5 \${wishlist.includes(product.id) ? "fill-rose-500 text-rose-500" : "text-stone-600"}`} />
</span>
<div className="absolute bottom-0 left-0 right-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 p-3"><Button className="w-full" size="sm">Quick Add</Button></div>
</div>
<CardContent className="p-3">
<h3 className="font-medium text-xs text-stone-900 truncate">{product.name}</h3>
<div className="flex gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity"><span className="w-3 h-3 rounded-full bg-stone-900" /><span className="w-3 h-3 rounded-full bg-rose-400" /><span className="w-3 h-3 rounded-full bg-amber-200" /></div>
<div className="flex items-center gap-0.5 mt-1">{renderStars(product.rating)}</div>
<div className="mt-1.5 flex items-center gap-2">
<span className="font-semibold text-sm">\${product.price}</span>
{product.originalPrice && (
<span className="text-xs text-stone-400 line-through">\${product.originalPrice}</span>
)}
</div>
<div className="flex items-center gap-1 mt-1"><div className="flex">{[...Array(5)].map((_, j) => <Star key={j} className="h-3 w-3 fill-amber-400 text-amber-400" />)}</div><span className="text-xs text-muted-foreground">(128)</span></div>
{i < 2 && <span className="text-xs text-amber-600 font-medium">Only 3 left</span>}
</CardContent>
</Card>
</button>
</DialogTrigger>
<DialogContent className="max-w-2xl p-0 overflow-hidden backdrop-blur-xl bg-white/80 dark:bg-slate-900/80 border-white/20">
<div className="grid md:grid-cols-2 gap-0">
{/* Product Image */}
<div className={`aspect-square bg-gradient-to-br \${product.gradient} relative`}>
<div className="absolute inset-0" style={{ backgroundImage: "radial-gradient(circle, rgba(0,0,0,0.06) 1px, transparent 1px)", backgroundSize: "16px 16px" }} />
<div className="absolute top-1/4 -left-8 h-32 w-32 rounded-full bg-white/30 blur-2xl" />
<div className="absolute bottom-1/4 -right-8 h-28 w-28 rounded-full bg-white/20 blur-2xl" />
<div className="absolute inset-0 flex items-center justify-center text-stone-400 text-sm">Product Image</div>
{product.badge && (
<Badge className={`absolute top-4 left-4 border-0 text-white \${product.badge === "Sale" ? "bg-rose-500" : "bg-stone-900"}`}>
{product.badge}
</Badge>
)}
</div>
{/* Product Details */}
<div className="p-6 flex flex-col">
<p className="text-xs text-stone-400 uppercase tracking-wider mb-1">{product.brand}</p>
<h2 className="text-xl font-bold text-stone-900">{product.name}</h2>
<div className="flex items-center gap-0.5 mt-2">{renderStars(product.rating)}</div>
<div className="mt-3 flex items-center gap-3">
<span className="text-2xl font-bold">\${product.price}</span>
{product.originalPrice && (
<span className="text-base text-stone-400 line-through">\${product.originalPrice}</span>
)}
</div>
<p className="mt-3 text-sm text-stone-500 leading-relaxed">Premium quality crafted with the finest materials. Designed for comfort and style that lasts.</p>
<Separator className="my-4" />
<div>
<p className="text-sm font-medium mb-2">Size</p>
<div className="flex flex-wrap gap-2">
{product.size.map((s) => (
<button
key={s}
onClick={() => setSelectedSize(s)}
className={`h-9 min-w-[2.5rem] px-3 rounded-md border text-sm font-medium transition-all \${selectedSize === s ? "border-stone-900 bg-stone-900 text-white" : "border-stone-200 hover:border-stone-400"}`}
>
{s}
</button>
))}
</div>
</div>
<div className="mt-auto pt-4 space-y-2">
<Button
className="w-full bg-stone-900 hover:bg-stone-800 text-white rounded-full active:scale-[0.98] transition-all"
onClick={() => { if (!selectedSize) { toast.error("Please select a size"); return; } toast.success(`Added \${product.name} (size \${selectedSize}) to cart`); setQuickView(null); setSelectedSize(""); }}
>
<ShoppingBag className="mr-2 h-4 w-4" /> Add to Cart
</Button>
<Button asChild variant="outline" className="w-full rounded-full" onClick={() => setQuickView(null)}>
<Link to="/product">View Full Details</Link>
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
))}
</div>
{/* Pagination */}
<div className="flex items-center justify-center gap-2 mt-12">
<Button variant="outline" size="icon" className="h-9 w-9" disabled={page === 1} onClick={() => setPage(page - 1)}>
<ChevronLeft className="h-4 w-4" />
</Button>
{[1, 2, 3].map((p) => (
<Button key={p} variant={page === p ? "default" : "outline"} size="icon" className={`h-9 w-9 \${page === p ? "bg-stone-900" : ""}`} onClick={() => setPage(p)}>
{p}
</Button>
))}
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setPage(page + 1)}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}