Seed: ecommerce template (24 files)
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user