Initial: ecommerce template via tAI

This commit is contained in:
Joe Wee
2026-05-21 11:32:38 +01:00
commit 7a4f8b75f9
18 changed files with 2284 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Read",
"Write",
"Edit",
"Bash",
"Glob",
"Grep"
]
},
"model": "claude-sonnet-4-20250514"
}
+32
View File
@@ -0,0 +1,32 @@
# StyleHaus
Multi-page e-commerce store with product catalog, cart, checkout, and account pages
Stack: React 18 / TypeScript / Vite / Tailwind CSS / shadcn/ui
Pages: Index, Products, ProductDetail, Cart, Checkout, Account, Search, OrderConfirmation
Palette: rose/stone/amber warm tones
## Files
- src/pages/Index.tsx (19KB)
- src/pages/Products.tsx (18KB)
- src/pages/ProductDetail.tsx (16KB)
- src/pages/Cart.tsx (8KB)
- src/pages/Checkout.tsx (19KB)
- src/pages/Account.tsx (10KB)
- src/pages/Search.tsx (7KB)
- src/pages/OrderConfirmation.tsx (3KB)
- src/components/Header.tsx (7KB)
- src/components/Footer.tsx (3KB)
- src/App.tsx (1KB)
- src/lib/cart-context.tsx (2KB)
## Imports
UI: @/components/ui/{button,card,badge,input,textarea,tabs,accordion,dialog,sheet}
Icons: lucide-react
Toast: import { toast } from "sonner"
Theme: ThemeToggle in headers (next-themes)
Router: react-router-dom BrowserRouter
## Rules
- MODIFY existing files — never rebuild from scratch
- Pages import Header + Footer from @/components/
- Tailwind only — no inline styles
- Use existing shadcn components — don't create custom ones
+3
View File
@@ -0,0 +1,3 @@
# template-ecommerce
Template: ecommerce
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ecommerce</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { CartProvider } from "@/lib/cart-context";
import IndexPage from "@/pages/Index";
import ProductsPage from "@/pages/Products";
import ProductDetailPage from "@/pages/ProductDetail";
import CartPage from "@/pages/Cart";
import CheckoutPage from "@/pages/Checkout";
import AccountPage from "@/pages/Account";
import SearchPage from "@/pages/Search";
import OrderConfirmationPage from "@/pages/OrderConfirmation";
function App() {
return (
<BrowserRouter>
<CartProvider>
<Routes>
<Route path="/" element={<IndexPage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/product" element={<ProductDetailPage />} />
<Route path="/cart" element={<CartPage />} />
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/order-confirmation" element={<OrderConfirmationPage />} />
</Routes>
</CartProvider>
</BrowserRouter>
);
}
export default App;
+93
View File
@@ -0,0 +1,93 @@
import { Link } from "react-router-dom";
import { Separator } from "@/components/ui/separator";
const footerLinks = {
shop: [
{ label: "New Arrivals", to: "/products" },
{ label: "Best Sellers", to: "/products" },
{ label: "Sale", to: "/products" },
{ label: "Collections", to: "/products" },
],
help: [
{ label: "Customer Service", to: "/" },
{ label: "Size Guide", to: "/" },
{ label: "Shipping & Returns", to: "/" },
{ label: "FAQ", to: "/" },
],
about: [
{ label: "Our Story", to: "/" },
{ label: "Sustainability", to: "/" },
{ label: "Careers", to: "/" },
{ label: "Press", to: "/" },
],
};
export default function Footer() {
return (
<footer className="bg-stone-900 text-white">
<div className="mx-auto max-w-7xl px-6 py-16">
<div className="grid grid-cols-2 md:grid-cols-4 gap-10">
{/* Brand */}
<div className="col-span-2 md:col-span-1">
<h3 className="text-lg font-bold mb-4">StyleHaus</h3>
<p className="text-sm text-stone-400 leading-relaxed">
Discover curated collections of premium fashion, accessories, and lifestyle essentials crafted for the modern individual.
</p>
</div>
{/* Shop */}
<div>
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">Shop</h4>
<ul className="space-y-3">
{footerLinks.shop.map((link) => (
<li key={link.label}>
<Link to={link.to} className="text-sm text-stone-400 hover:text-white transition-colors">{link.label}</Link>
</li>
))}
</ul>
</div>
{/* Help */}
<div>
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">Help</h4>
<ul className="space-y-3">
{footerLinks.help.map((link) => (
<li key={link.label}>
<Link to={link.to} className="text-sm text-stone-400 hover:text-white transition-colors">{link.label}</Link>
</li>
))}
</ul>
</div>
{/* About */}
<div>
<h4 className="font-semibold text-sm mb-4 text-stone-300 uppercase tracking-wider">About</h4>
<ul className="space-y-3">
{footerLinks.about.map((link) => (
<li key={link.label}>
<Link to={link.to} className="text-sm text-stone-400 hover:text-white transition-colors">{link.label}</Link>
</li>
))}
</ul>
</div>
</div>
<Separator className="my-10 bg-stone-800" />
{/* Payment & Copyright */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-stone-500">
&copy; 2024 StyleHaus. All rights reserved.
</p>
<div className="flex items-center gap-3">
{["Visa", "Mastercard", "Amex", "PayPal", "Apple Pay"].map((method) => (
<span key={method} className="text-xs text-stone-500 bg-stone-800 px-2 py-1 rounded">
{method}
</span>
))}
</div>
</div>
</div>
</footer>
);
}
+148
View File
@@ -0,0 +1,148 @@
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Menu, Search, ShoppingBag, User, Heart, Separator as SeparatorIcon } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { ThemeToggle } from "@/components/ThemeToggle";
import { useCart } from "@/lib/cart-context";
const cartPreviewItems = [
{ name: "Classic Leather Jacket", price: 189, qty: 1 },
{ name: "Silk Scarf", price: 65, qty: 1 },
{ name: "Organic Cotton Tee", price: 45, qty: 2 },
];
const navLinks = [
{ label: "Home", to: "/" },
{ label: "Shop", to: "/products" },
{ label: "New Arrivals", to: "/products" },
{ label: "Sale", to: "/products" },
];
export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { itemCount } = useCart();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<header className={\`fixed top-0 inset-x-0 z-50 transition-all duration-300 \${
scrolled ? "backdrop-blur-xl bg-white/80 dark:bg-slate-900/80 border-white/20 border-b border-stone-100 shadow-sm" : "bg-white/80 backdrop-blur-sm"
}\`}>
{/* Top Banner */}
<div className="bg-stone-900 text-white text-center text-xs py-1.5">
Free shipping on orders over $100 | Use code STYLE20 for 20% off
</div>
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 py-3">
{/* Logo */}
<Link to="/" className="text-xl font-bold tracking-tight text-stone-900">
StyleHaus
</Link>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-8">
{navLinks.map((l) => (
<Link key={l.to + l.label} to={l.to} className="text-sm text-stone-600 hover:text-stone-900 transition-colors font-medium">
{l.label}
</Link>
))}
</nav>
{/* Right Icons */}
<div className="flex items-center gap-1">
<Button asChild variant="ghost" size="icon" className="hidden sm:flex h-9 w-9">
<Link to="/search"><Search className="h-4 w-4" /></Link>
</Button>
<Button asChild variant="ghost" size="icon" className="hidden sm:flex h-9 w-9">
<Link to="/account?tab=wishlist"><Heart className="h-4 w-4" /></Link>
</Button>
<Sheet>
<SheetTrigger asChild>
<Button variant={itemCount > 0 ? "default" : "ghost"} size="icon" className={\`relative h-9 w-9 transition-all \${itemCount > 0 ? "bg-stone-900 hover:bg-stone-800 text-white" : ""}\`}>
<ShoppingBag className="h-4 w-4" />
{itemCount > 0 && (
<span className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-rose-500 text-[10px] font-bold text-white flex items-center justify-center ring-2 ring-white animate-fade-in">
{itemCount}
</span>
)}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-80 bg-white flex flex-col">
<h2 className="text-lg font-bold mb-4">Your Cart</h2>
<div className="flex-1 overflow-y-auto space-y-4">
{cartPreviewItems.map((item, i) => (
<div key={i} className="flex items-center justify-between text-sm">
<div>
<p className="font-medium">{item.name}</p>
<p className="text-stone-500 text-xs">Qty: {item.qty}</p>
</div>
<span className="font-semibold">\${(item.price * item.qty).toFixed(2)}</span>
</div>
))}
</div>
<Separator className="my-4" />
<div className="flex justify-between text-sm font-bold mb-4">
<span>Subtotal</span>
<span>\${cartPreviewItems.reduce((s, i) => s + i.price * i.qty, 0).toFixed(2)}</span>
</div>
<div className="space-y-2">
<Button asChild variant="outline" className="w-full rounded-full">
<Link to="/cart">View Cart</Link>
</Button>
<Button asChild className="w-full rounded-full bg-stone-900 hover:bg-stone-800">
<Link to="/checkout">Checkout</Link>
</Button>
</div>
</SheetContent>
</Sheet>
<Button asChild variant="ghost" size="icon" className="hidden sm:flex h-9 w-9">
<Link to="/account"><User className="h-4 w-4" /></Link>
</Button>
<ThemeToggle />
{/* Mobile Menu */}
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger asChild className="md:hidden">
<Button variant="ghost" size="icon" className="h-9 w-9"><Menu className="h-5 w-5" /></Button>
</SheetTrigger>
<SheetContent side="right" className="w-72 bg-white">
<nav className="mt-8 flex flex-col gap-1">
{navLinks.map((l) => (
<Link
key={l.to + l.label}
to={l.to}
className="px-4 py-3 text-base font-medium hover:bg-stone-50 rounded-lg transition-colors"
onClick={() => setMobileOpen(false)}
>
{l.label}
</Link>
))}
<div className="border-t border-stone-100 mt-4 pt-4 space-y-1">
<Link to="/search" className="flex items-center gap-3 px-4 py-3 text-sm text-stone-600 hover:bg-stone-50 rounded-lg" onClick={() => setMobileOpen(false)}>
<Search className="h-4 w-4" /> Search
</Link>
<Link to="/account" className="flex items-center gap-3 px-4 py-3 text-sm text-stone-600 hover:bg-stone-50 rounded-lg" onClick={() => setMobileOpen(false)}>
<User className="h-4 w-4" /> Account
</Link>
<Link to="/account?tab=wishlist" className="flex items-center gap-3 px-4 py-3 text-sm text-stone-600 hover:bg-stone-50 rounded-lg" onClick={() => setMobileOpen(false)}>
<Heart className="h-4 w-4" /> Wishlist
</Link>
<div className="px-4 pt-2"><ThemeToggle /></div>
</div>
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+102
View File
@@ -0,0 +1,102 @@
import { createContext, useContext, useState, ReactNode } from "react";
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
size?: string;
color?: string;
image?: string;
}
interface CartContextType {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: number;
itemCount: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<CartItem[]>([
{
id: "leather-jacket-1",
name: "Classic Leather Jacket",
price: 189,
quantity: 1,
size: "M",
color: "Black",
image: "",
},
{
id: "silk-dress-1",
name: "Silk Evening Dress",
price: 245,
quantity: 1,
size: "S",
color: "Rose",
image: "",
},
{
id: "cashmere-sweater-1",
name: "Cashmere Knit Sweater",
price: 128,
quantity: 2,
size: "M",
color: "Stone",
image: "",
},
]);
const addItem = (newItem: CartItem) => {
setItems((prev) => {
const existing = prev.find(
(i) => i.id === newItem.id && i.size === newItem.size && i.color === newItem.color
);
if (existing) {
return prev.map((i) =>
i.id === existing.id && i.size === existing.size && i.color === existing.color
? { ...i, quantity: i.quantity + newItem.quantity }
: i
);
}
return [...prev, newItem];
});
};
const removeItem = (id: string) => {
setItems((prev) => prev.filter((i) => i.id !== id));
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity <= 0) {
removeItem(id);
return;
}
setItems((prev) => prev.map((i) => (i.id === id ? { ...i, quantity } : i)));
};
const clearCart = () => setItems([]);
const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const itemCount = items.reduce((sum, i) => sum + i.quantity, 0);
return (
<CartContext.Provider value={{ items, addItem, removeItem, updateQuantity, clearCart, total, itemCount }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error("useCart must be used within a CartProvider");
}
return context;
}
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+201
View File
@@ -0,0 +1,201 @@
import { useState } from "react";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from "@/components/ui/table";
import {
Package, User, MapPin, Heart, Star, Eye, Trash2, ShoppingBag, Edit
} from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const orders = [
{ id: "ORD-2024-1847", date: "Mar 15, 2024", status: "Delivered", statusColor: "bg-green-100 text-green-700", total: 423.00, items: 3 },
{ id: "ORD-2024-1692", date: "Feb 28, 2024", status: "Shipped", statusColor: "bg-blue-100 text-blue-700", total: 189.00, items: 1 },
{ id: "ORD-2024-1534", date: "Feb 10, 2024", status: "Processing", statusColor: "bg-amber-100 text-amber-700", total: 567.00, items: 4 },
];
const wishlistItems = [
{ id: 1, name: "Silk Evening Dress", price: 245, gradient: "from-amber-100 to-orange-200", rating: 5 },
{ id: 2, name: "Italian Leather Handbag", price: 320, gradient: "from-stone-200 to-rose-100", rating: 5 },
{ id: 3, name: "Tailored Wool Coat", price: 385, gradient: "from-stone-200 to-stone-400", rating: 5 },
{ id: 4, name: "Pearl Drop Earrings", price: 68, gradient: "from-amber-100 to-rose-100", rating: 4 },
];
export default function AccountPage() {
const [profile, setProfile] = useState({
firstName: "Alexandra",
lastName: "Chen",
email: "alex.chen@email.com",
phone: "+1 (555) 234-5678",
});
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"}\`} />
));
return (
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
<Header />
<div className="mx-auto max-w-5xl px-6 pt-28 pb-20">
{/* Profile Header */}
<div className="flex items-center gap-4 mb-10">
<Avatar className="h-16 w-16">
<AvatarFallback className="bg-rose-100 text-rose-700 text-xl font-bold">AC</AvatarFallback>
</Avatar>
<div>
<h1 className="text-2xl font-bold animate-fade-up">Welcome back, {profile.firstName}</h1>
<p className="text-stone-500 text-sm">{profile.email}</p>
</div>
</div>
<Tabs defaultValue="orders" className="space-y-8">
<TabsList className="bg-white border shadow-sm rounded-full p-1">
<TabsTrigger value="orders" className="rounded-full gap-1.5 text-sm"><Package className="h-4 w-4" /> Orders</TabsTrigger>
<TabsTrigger value="profile" className="rounded-full gap-1.5 text-sm"><User className="h-4 w-4" /> Profile</TabsTrigger>
<TabsTrigger value="addresses" className="rounded-full gap-1.5 text-sm"><MapPin className="h-4 w-4" /> Addresses</TabsTrigger>
<TabsTrigger value="wishlist" className="rounded-full gap-1.5 text-sm"><Heart className="h-4 w-4" /> Wishlist</TabsTrigger>
</TabsList>
{/* Orders Tab */}
<TabsContent value="orders">
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="bg-stone-50">
<TableHead className="font-semibold">Order</TableHead>
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold text-right">Total</TableHead>
<TableHead className="font-semibold text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{order.id}</TableCell>
<TableCell className="text-stone-500">{order.date}</TableCell>
<TableCell>
<Badge className={\`\${order.statusColor} border-0 font-medium\`}>{order.status}</Badge>
</TableCell>
<TableCell className="text-right font-medium">\${order.total.toFixed(2)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" className="text-rose-600 hover:text-rose-700">
<Eye className="h-4 w-4 mr-1" /> View
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Profile Tab */}
<TabsContent value="profile">
<Card>
<CardContent className="p-6 sm:p-8">
<h2 className="text-xl font-bold mb-6">Personal Information</h2>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">First Name</label>
<Input value={profile.firstName} onChange={(e) => setProfile({ ...profile, firstName: e.target.value })} />
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Last Name</label>
<Input value={profile.lastName} onChange={(e) => setProfile({ ...profile, lastName: e.target.value })} />
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Email</label>
<Input type="email" value={profile.email} onChange={(e) => setProfile({ ...profile, email: e.target.value })} />
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Phone</label>
<Input value={profile.phone} onChange={(e) => setProfile({ ...profile, phone: e.target.value })} />
</div>
</div>
<Button className="mt-6 bg-stone-900 hover:bg-stone-800 rounded-full active:scale-[0.98] transition-all">Save Changes</Button>
</CardContent>
</Card>
</TabsContent>
{/* Addresses Tab */}
<TabsContent value="addresses">
<div className="grid sm:grid-cols-2 gap-6">
<Card className="relative">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<Badge className="bg-stone-900 text-white border-0">Default</Badge>
<Button variant="ghost" size="sm"><Edit className="h-4 w-4" /></Button>
</div>
<h3 className="font-semibold mb-1">Home</h3>
<p className="text-sm text-stone-600 leading-relaxed">
Alexandra Chen<br />
456 Park Avenue, Apt 12B<br />
New York, NY 10022
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<Badge variant="secondary">Office</Badge>
<Button variant="ghost" size="sm"><Edit className="h-4 w-4" /></Button>
</div>
<h3 className="font-semibold mb-1">Office</h3>
<p className="text-sm text-stone-600 leading-relaxed">
Alexandra Chen<br />
789 Madison Ave, Floor 15<br />
New York, NY 10065
</p>
</CardContent>
</Card>
</div>
<Button variant="outline" className="mt-6 rounded-full">+ Add New Address</Button>
</TabsContent>
{/* Wishlist Tab */}
<TabsContent value="wishlist">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 stagger">
{wishlistItems.map((item) => (
<Card key={item.id} className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
<div className={\`aspect-[3/4] bg-gradient-to-br \${item.gradient} flex items-center justify-center text-stone-400 text-xs relative overflow-hidden\`}>
<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-24 w-24 rounded-full bg-white/30 blur-2xl" />
<span className="relative">Image</span>
</div>
<CardContent className="p-4 space-y-2">
<h3 className="font-medium text-sm">{item.name}</h3>
<div className="flex items-center gap-0.5">{renderStars(item.rating)}</div>
<p className="font-semibold">\${item.price}</p>
<div className="flex gap-2 pt-1">
<Button size="sm" className="flex-1 bg-stone-900 hover:bg-stone-800 text-xs rounded-full">
<ShoppingBag className="h-3 w-3 mr-1" /> Add to Cart
</Button>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-full text-stone-400 hover:text-rose-500">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</div>
<Footer />
</div>
);
}
+177
View File
@@ -0,0 +1,177 @@
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 { Minus, Plus, X, ShoppingBag, ArrowRight, ArrowLeft, Tag } from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useCart } from "@/lib/cart-context";
export default function CartPage() {
const { items, removeItem, updateQuantity, total, itemCount, clearCart } = useCart();
const [coupon, setCoupon] = useState("");
const [couponApplied, setCouponApplied] = useState(false);
const shipping = total >= 100 ? 0 : 12.99;
const tax = total * 0.08;
const discount = couponApplied ? total * 0.1 : 0;
const grandTotal = total + shipping + tax - discount;
const applyCoupon = () => {
if (coupon.trim().length > 0) {
setCouponApplied(true);
toast.success("Coupon applied!");
}
};
if (items.length === 0) {
return (
<div className="min-h-screen bg-white text-stone-900 antialiased">
<Header />
<div className="flex flex-col items-center justify-center py-40 px-6">
<div className="h-20 w-20 rounded-full bg-stone-100 flex items-center justify-center mb-6">
<ShoppingBag className="h-10 w-10 text-stone-400" />
</div>
<h1 className="text-2xl font-bold mb-2">Your cart is empty</h1>
<p className="text-stone-500 mb-8">Looks like you haven't added anything to your cart yet.</p>
<Button asChild className="bg-stone-900 hover:bg-stone-800 rounded-full px-8">
<Link to="/products">Continue Shopping</Link>
</Button>
</div>
<Footer />
</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">
<h1 className="text-3xl font-bold tracking-tight mb-2 animate-fade-up">Shopping Cart</h1>
<p className="text-stone-500 mb-10">{itemCount} {itemCount === 1 ? "item" : "items"} in your cart</p>
<div className="grid lg:grid-cols-3 gap-10">
{/* Cart Items */}
<div className="lg:col-span-2 space-y-4">
{items.map((item) => (
<Card key={item.id + item.size + item.color} className="border-stone-100">
<CardContent className="p-4 sm:p-6">
<div className="flex gap-4">
<div className="h-24 w-20 sm:h-28 sm:w-24 rounded-lg bg-gradient-to-br from-rose-100 to-stone-200 flex items-center justify-center text-stone-400 text-xs shrink-0 relative overflow-hidden">
<div className="absolute inset-0" style={{ backgroundImage: "radial-gradient(circle, rgba(0,0,0,0.06) 1px, transparent 1px)", backgroundSize: "12px 12px" }} />
<div className="absolute top-1/4 -left-4 h-12 w-12 rounded-full bg-white/30 blur-xl" />
<span className="relative">Image</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold text-sm sm:text-base">{item.name}</h3>
<div className="flex items-center gap-2 mt-1">
{item.color && <Badge variant="secondary" className="text-xs">{item.color}</Badge>}
{item.size && <Badge variant="secondary" className="text-xs">Size: {item.size}</Badge>}
</div>
</div>
<button onClick={() => removeItem(item.id)} className="text-stone-400 hover:text-stone-600 transition-colors">
<X className="h-4 w-4" />
</button>
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center border rounded-full">
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full active:scale-90 transition-transform" onClick={() => updateQuantity(item.id, Math.max(1, item.quantity - 1))}>
<Minus className="h-3 w-3" />
</Button>
<span className="w-8 text-center text-sm font-medium">{item.quantity}</span>
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full active:scale-90 transition-transform" onClick={() => updateQuantity(item.id, item.quantity + 1)}>
<Plus className="h-3 w-3" />
</Button>
</div>
<span className="font-semibold">\${(item.price * item.quantity).toFixed(2)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Order Summary */}
<div>
<Card className="border-stone-200 sticky top-28 backdrop-blur-xl bg-white/80 dark:bg-slate-900/80 border-white/20">
<CardContent className="p-6">
<h2 className="text-lg font-bold mb-6">Order Summary</h2>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-stone-500">Subtotal</span>
<span className="font-medium">\${total.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Shipping</span>
<span className={\`font-medium \${shipping === 0 ? "text-green-600" : ""}\`}>
{shipping === 0 ? "Free" : \`\$\${shipping.toFixed(2)}\`}
</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Tax</span>
<span className="font-medium">\${tax.toFixed(2)}</span>
</div>
{couponApplied && (
<div className="flex justify-between text-green-600">
<span>Discount (10%)</span>
<span>-\${discount.toFixed(2)}</span>
</div>
)}
</div>
<Separator className="my-4" />
<div className="flex justify-between text-lg font-bold">
<span>Total</span>
<span>\${grandTotal.toFixed(2)}</span>
</div>
{/* Coupon */}
<div className="mt-6 flex gap-2">
<div className="relative flex-1">
<Tag className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-400" />
<Input
placeholder="Coupon code"
value={coupon}
onChange={(e) => setCoupon(e.target.value)}
className="pl-9 rounded-full text-sm"
/>
</div>
<Button variant="outline" size="sm" className="rounded-full" onClick={applyCoupon}>
Apply
</Button>
</div>
{shipping > 0 && (
<p className="text-xs text-stone-400 mt-4 text-center">
Add \${(100 - total).toFixed(2)} more for free shipping
</p>
)}
<div className="mt-6 space-y-3">
<Button asChild size="lg" className="w-full bg-stone-900 hover:bg-stone-800 rounded-full active:scale-[0.98] transition-all">
<Link to="/checkout">Checkout <ArrowRight className="ml-2 h-4 w-4" /></Link>
</Button>
<Button asChild variant="outline" size="lg" className="w-full rounded-full active:scale-[0.98] transition-all">
<Link to="/products"><ArrowLeft className="mr-2 h-4 w-4" /> Continue Shopping</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<Footer />
</div>
);
}
+334
View File
@@ -0,0 +1,334 @@
import { useState } from "react";
import { toast } from "sonner";
import { Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Check, CreditCard, MapPin, ClipboardList, ArrowLeft, ArrowRight, Lock } from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useCart } from "@/lib/cart-context";
const steps = [
{ num: 1, label: "Shipping", icon: MapPin },
{ num: 2, label: "Payment", icon: CreditCard },
{ num: 3, label: "Review", icon: ClipboardList },
];
export default function CheckoutPage() {
const [step, setStep] = useState(1);
const [errors, setErrors] = useState<Record<string, string>>({});
const { items, total, clearCart } = useCart();
const navigate = useNavigate();
const [shipping, setShipping] = useState({
firstName: "", lastName: "", email: "", address: "", city: "", state: "", zip: "",
});
const [payment, setPayment] = useState({
cardNumber: "", expiry: "", cvv: "", cardholder: "",
});
const shippingCost = total >= 100 ? 0 : 12.99;
const tax = total * 0.08;
const grandTotal = total + shippingCost + tax;
const validateShipping = () => {
const e: Record<string, string> = {};
if (!shipping.firstName.trim()) e.firstName = "First name is required";
if (!shipping.lastName.trim()) e.lastName = "Last name is required";
if (!shipping.email.includes("@")) e.email = "Valid email is required";
if (!shipping.address.trim()) e.address = "Address is required";
if (!shipping.city.trim()) e.city = "City is required";
if (!shipping.zip.trim()) e.zip = "ZIP code is required";
setErrors(e);
return Object.keys(e).length === 0;
};
const validatePayment = () => {
const e: Record<string, string> = {};
if (payment.cardNumber.replace(/\s/g, "").length < 16) e.cardNumber = "Enter a valid 16-digit card number";
if (!payment.expiry.trim()) e.expiry = "Expiry date is required";
if (payment.cvv.length < 3) e.cvv = "CVV must be at least 3 digits";
setErrors(e);
return Object.keys(e).length === 0;
};
const handlePlaceOrder = () => {
toast.success("Order placed!");
clearCart();
navigate("/order-confirmation");
};
const updateShipping = (field: string, value: string) => {
setShipping((prev) => ({ ...prev, [field]: value }));
if (errors[field]) setErrors((prev) => { const n = { ...prev }; delete n[field]; return n; });
};
const updatePayment = (field: string, value: string) => {
setPayment((prev) => ({ ...prev, [field]: value }));
if (errors[field]) setErrors((prev) => { const n = { ...prev }; delete n[field]; return n; });
};
return (
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
<Header />
<div className="mx-auto max-w-6xl px-6 pt-28 pb-20">
{/* Step Indicator */}
<div className="flex items-center justify-center gap-4 mb-12">
{steps.map((s, i) => (
<div key={s.num} className="flex items-center gap-2">
<div className={\`flex items-center gap-2 cursor-pointer \${step >= s.num ? "text-stone-900" : "text-stone-400"}\`} onClick={() => s.num < step && setStep(s.num)}>
<div className={\`h-9 w-9 rounded-full flex items-center justify-center text-sm font-semibold backdrop-blur-xl border-white/20 \${
step > s.num ? "bg-green-500/80 text-white" : step === s.num ? "bg-stone-900/80 text-white" : "bg-white/80 dark:bg-slate-900/80 text-stone-500"
}\`}>
{step > s.num ? <Check className="h-4 w-4" /> : s.num}
</div>
<span className="hidden sm:inline text-sm font-medium">{s.label}</span>
</div>
{i < steps.length - 1 && (
<div className={\`w-12 sm:w-20 h-px \${step > s.num ? "bg-green-500" : "bg-stone-200"}\`} />
)}
</div>
))}
</div>
<div className="grid lg:grid-cols-3 gap-10">
{/* Main Form */}
<div className="lg:col-span-2">
{/* Step 1: Shipping */}
{step === 1 && (
<Card>
<CardContent className="p-6 sm:p-8">
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 animate-fade-up">
<MapPin className="h-5 w-5 text-rose-500" /> Shipping Information
</h2>
<form onSubmit={(e) => { e.preventDefault(); if (validateShipping()) setStep(2); }}>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">First Name</label>
<div className="relative">
<Input value={shipping.firstName} onChange={(e) => updateShipping("firstName", e.target.value)} placeholder="John" className={\`\${errors.firstName ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.firstName && shipping.firstName.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.firstName && <p className="text-xs text-red-500 mt-1">{errors.firstName}</p>}
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Last Name</label>
<div className="relative">
<Input value={shipping.lastName} onChange={(e) => updateShipping("lastName", e.target.value)} placeholder="Doe" className={\`\${errors.lastName ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.lastName && shipping.lastName.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.lastName && <p className="text-xs text-red-500 mt-1">{errors.lastName}</p>}
</div>
<div className="sm:col-span-2">
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Email</label>
<div className="relative">
<Input type="email" value={shipping.email} onChange={(e) => updateShipping("email", e.target.value)} placeholder="john@example.com" className={\`\${errors.email ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.email && shipping.email?.includes("@") && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.email && <p className="text-xs text-red-500 mt-1">{errors.email}</p>}
</div>
<div className="sm:col-span-2">
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Address</label>
<div className="relative">
<Input value={shipping.address} onChange={(e) => updateShipping("address", e.target.value)} placeholder="123 Main Street" className={\`\${errors.address ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.address && shipping.address.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address}</p>}
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">City</label>
<div className="relative">
<Input value={shipping.city} onChange={(e) => updateShipping("city", e.target.value)} placeholder="New York" className={\`\${errors.city ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.city && shipping.city.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.city && <p className="text-xs text-red-500 mt-1">{errors.city}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">State</label>
<Input value={shipping.state} onChange={(e) => updateShipping("state", e.target.value)} placeholder="NY" />
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">ZIP</label>
<div className="relative">
<Input value={shipping.zip} onChange={(e) => updateShipping("zip", e.target.value)} placeholder="10001" className={\`\${errors.zip ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.zip && shipping.zip.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.zip && <p className="text-xs text-red-500 mt-1">{errors.zip}</p>}
</div>
</div>
</div>
<div className="flex justify-between mt-8">
<Button asChild variant="outline" className="rounded-full active:scale-[0.98] transition-all">
<Link to="/cart"><ArrowLeft className="mr-2 h-4 w-4" /> Back to Cart</Link>
</Button>
<Button type="submit" className="bg-stone-900 hover:bg-stone-800 rounded-full active:scale-[0.98] transition-all">
Continue to Payment <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* Step 2: Payment */}
{step === 2 && (
<Card>
<CardContent className="p-6 sm:p-8">
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 animate-fade-up">
<CreditCard className="h-5 w-5 text-rose-500" /> Payment Details
</h2>
<form onSubmit={(e) => { e.preventDefault(); if (validatePayment()) setStep(3); }}>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Cardholder Name</label>
<Input value={payment.cardholder} onChange={(e) => updatePayment("cardholder", e.target.value)} placeholder="John Doe" />
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Card Number</label>
<div className="relative">
<Input value={payment.cardNumber} onChange={(e) => updatePayment("cardNumber", e.target.value)} placeholder="4242 4242 4242 4242" className={\`\${errors.cardNumber ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.cardNumber && payment.cardNumber.replace(/\s/g, "").length >= 16 && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.cardNumber && <p className="text-xs text-red-500 mt-1">{errors.cardNumber}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">Expiry Date</label>
<div className="relative">
<Input value={payment.expiry} onChange={(e) => updatePayment("expiry", e.target.value)} placeholder="MM/YY" className={\`\${errors.expiry ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.expiry && payment.expiry.trim() && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.expiry && <p className="text-xs text-red-500 mt-1">{errors.expiry}</p>}
</div>
<div>
<label className="text-sm font-medium text-stone-700 mb-1.5 block">CVV</label>
<div className="relative">
<Input value={payment.cvv} onChange={(e) => updatePayment("cvv", e.target.value)} placeholder="123" className={\`\${errors.cvv ? "border-red-500 focus-visible:ring-red-500" : ""}\`} />
{!errors.cvv && payment.cvv.length >= 3 && <Check className="h-4 w-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2" />}
</div>
{errors.cvv && <p className="text-xs text-red-500 mt-1">{errors.cvv}</p>}
</div>
</div>
</div>
<div className="flex items-center gap-2 mt-4 text-xs text-stone-500">
<Lock className="h-3 w-3" /> Your payment information is encrypted and secure.
</div>
<div className="flex justify-between mt-8">
<Button type="button" variant="outline" className="rounded-full active:scale-[0.98] transition-all" onClick={() => setStep(1)}>
<ArrowLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button type="submit" className="bg-stone-900 hover:bg-stone-800 rounded-full active:scale-[0.98] transition-all">
Review Order <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* Step 3: Review */}
{step === 3 && (
<Card>
<CardContent className="p-6 sm:p-8">
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 animate-fade-up">
<ClipboardList className="h-5 w-5 text-rose-500" /> Review Your Order
</h2>
{/* Items */}
<div className="space-y-4 mb-6">
{items.map((item) => (
<div key={item.id} className="flex items-center gap-4">
<div className="h-14 w-12 rounded-lg bg-gradient-to-br from-rose-100 to-stone-200 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{item.name}</p>
<p className="text-xs text-stone-500">Qty: {item.quantity} | {item.size} | {item.color}</p>
</div>
<span className="text-sm font-semibold">\${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<Separator className="my-6" />
{/* Shipping Address */}
<div className="mb-6">
<h3 className="text-sm font-semibold mb-2">Shipping Address</h3>
<p className="text-sm text-stone-600">
{shipping.firstName} {shipping.lastName}<br />
{shipping.address}<br />
{shipping.city}, {shipping.state} {shipping.zip}
</p>
</div>
{/* Payment */}
<div className="mb-6">
<h3 className="text-sm font-semibold mb-2">Payment Method</h3>
<div className="flex items-center gap-2 text-sm text-stone-600">
<CreditCard className="h-4 w-4" />
<span>Card ending in {payment.cardNumber.slice(-4) || "****"}</span>
</div>
</div>
<div className="flex justify-between mt-8">
<Button variant="outline" className="rounded-full active:scale-[0.98] transition-all" onClick={() => setStep(2)}>
<ArrowLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button size="lg" className="bg-rose-600 hover:bg-rose-700 rounded-full px-8 active:scale-[0.98] transition-all" onClick={handlePlaceOrder}>
Place Order <Check className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
)}
</div>
{/* Right Sidebar — Order Summary */}
<div>
<Card className="border-stone-200 sticky top-28">
<CardContent className="p-6">
<h2 className="text-lg font-bold mb-4">Order Summary</h2>
<div className="space-y-3 mb-4">
{items.map((item) => (
<div key={item.id} className="flex justify-between text-sm">
<span className="text-stone-600 truncate mr-2">{item.name} x{item.quantity}</span>
<span className="font-medium shrink-0">\${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<Separator className="my-4" />
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-stone-500">Subtotal</span>
<span>\${total.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Shipping</span>
<span className={shippingCost === 0 ? "text-green-600" : ""}>
{shippingCost === 0 ? "Free" : \`\$\${shippingCost.toFixed(2)}\`}
</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Tax</span>
<span>\${tax.toFixed(2)}</span>
</div>
</div>
<Separator className="my-4" />
<div className="flex justify-between text-lg font-bold">
<span>Total</span>
<span>\${grandTotal.toFixed(2)}</span>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<Footer />
</div>
);
}
+304
View File
@@ -0,0 +1,304 @@
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 {
Truck, RotateCcw, ShieldCheck, Star, ArrowRight, Heart,
ChevronRight, Mail, Sparkles, Check
} from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const featuredProducts = [
{ id: 1, name: "Classic Leather Jacket", price: 189, originalPrice: 249, rating: 5, sale: true, gradient: "from-rose-100 to-stone-200", image: "" },
{ id: 2, name: "Silk Evening Dress", price: 245, originalPrice: null, rating: 5, sale: false, gradient: "from-amber-100 to-orange-200", image: "" },
{ id: 3, name: "Cashmere Knit Sweater", price: 128, originalPrice: null, rating: 4, sale: false, gradient: "from-stone-100 to-stone-300", image: "" },
{ id: 4, name: "Linen Wide-Leg Trousers", price: 95, originalPrice: 135, rating: 4, sale: true, gradient: "from-rose-50 to-amber-100", image: "" },
{ id: 5, name: "Italian Leather Handbag", price: 320, originalPrice: null, rating: 5, sale: false, gradient: "from-stone-200 to-rose-100", image: "" },
{ id: 6, name: "Merino Wool Blazer", price: 165, originalPrice: null, rating: 4, sale: false, gradient: "from-amber-50 to-stone-200", image: "" },
];
const categories = [
{ name: "Women", gradient: "from-stone-700 to-stone-900", count: 124 },
{ name: "Men", gradient: "from-rose-700 to-rose-900", count: 86 },
{ name: "Accessories", gradient: "from-amber-700 to-amber-900", count: 53 },
{ name: "Footwear", gradient: "from-stone-600 to-rose-800", count: 97 },
];
export default function IndexPage() {
const [email, setEmail] = useState("");
const [subscribed, setSubscribed] = useState(false);
const renderStars = (count: number) =>
Array.from({ length: 5 }, (_, i) => (
<Star key={i} className={\`h-3.5 w-3.5 \${i < count ? "fill-amber-400 text-amber-400" : "text-stone-300"}\`} />
));
return (
<div className="min-h-screen bg-white text-stone-900 antialiased">
<Header />
{/* ── HERO ── */}
<section className="relative overflow-hidden bg-gradient-to-br from-rose-50 via-white to-amber-50 pt-28 pb-20 sm:pt-36 sm:pb-28">
{/* Floating gradient blurs */}
<div className="absolute top-10 left-1/4 h-72 w-72 rounded-full bg-rose-300 opacity-20 blur-[100px]" />
<div className="absolute bottom-10 right-1/4 h-64 w-64 rounded-full bg-amber-300 opacity-20 blur-[100px]" />
<div className="absolute top-1/2 left-10 h-56 w-56 rounded-full bg-violet-300 opacity-20 blur-[100px]" />
<div className="absolute bottom-1/3 right-10 h-48 w-48 rounded-full bg-sky-300 opacity-20 blur-[100px]" />
{/* Dot grid pattern overlay */}
<div className="absolute inset-0" style={{ backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.5) 1px, transparent 1px)", backgroundSize: "32px 32px" }} />
<div className="mx-auto max-w-7xl px-6 grid lg:grid-cols-2 gap-12 items-center relative">
<div className="text-center lg:text-left">
<Badge className="mb-6 bg-rose-100 text-rose-700 hover:bg-rose-100 border-0 px-4 py-1.5 text-sm font-medium animate-fade-in">
<Sparkles className="mr-1.5 h-3.5 w-3.5" /> Summer Sale — Up to 40% Off
</Badge>
<h1 className="mx-auto lg:mx-0 max-w-4xl text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
<span className="block animate-fade-up">Elevate Your Everyday Style</span>
</h1>
<p className="mx-auto lg:mx-0 mt-6 max-w-2xl text-lg text-stone-500 animate-fade-up [animation-delay:100ms]">
Discover curated collections of premium fashion, accessories, and lifestyle essentials crafted for the modern individual.
</p>
<div className="mt-10 flex flex-col sm:flex-row items-center lg:justify-start justify-center gap-4 animate-fade-up [animation-delay:200ms]">
<Button asChild size="lg" className="bg-stone-900 hover:bg-stone-800 text-white px-8 rounded-full active:scale-[0.98] transition-all">
<Link to="/products">Shop Collection <ArrowRight className="ml-2 h-4 w-4" /></Link>
</Button>
<Button asChild variant="outline" size="lg" className="rounded-full border-stone-300 active:scale-[0.98] transition-all">
<Link to="/products">New Arrivals</Link>
</Button>
</div>
<div className="mt-12 flex flex-wrap lg:justify-start justify-center gap-8 text-sm text-stone-500 animate-fade-in [animation-delay:400ms]">
<span className="flex items-center gap-2"><Truck className="h-4 w-4 text-rose-500" /> Free Shipping Over $100</span>
<span className="flex items-center gap-2"><RotateCcw className="h-4 w-4 text-rose-500" /> 30-Day Returns</span>
<span className="flex items-center gap-2"><ShieldCheck className="h-4 w-4 text-rose-500" /> Secure Checkout</span>
</div>
</div>
{/* Featured product card floating on right */}
<div className="hidden lg:flex justify-center animate-fade-up [animation-delay:300ms]">
<Card className="w-72 overflow-hidden border-0 shadow-2xl hover:shadow-3xl transition-shadow duration-500 rotate-2 hover:rotate-0">
<div className="aspect-[3/4] bg-gradient-to-br from-rose-100 to-amber-100 relative overflow-hidden">
<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-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-sm">Featured</div>
<Badge className="absolute top-3 left-3 bg-rose-500 text-white border-0">Trending</Badge>
</div>
<CardContent className="p-4">
<h3 className="font-semibold text-stone-900">{featuredProducts[0].name}</h3>
<p className="text-lg font-bold mt-1 text-stone-900">\${featuredProducts[0].price}</p>
</CardContent>
</Card>
</div>
</div>
</section>
{/* ── FEATURED PRODUCTS ── */}
<section className="py-24 bg-white">
<div className="mx-auto max-w-7xl px-6">
<div className="flex items-end justify-between mb-10">
<div>
<h2 className="text-3xl font-bold tracking-tight animate-fade-up">Featured Collection</h2>
<p className="mt-2 text-stone-500 animate-fade-up [animation-delay:100ms]">Handpicked pieces for the season</p>
</div>
<Link to="/products" className="hidden sm:flex items-center text-sm font-medium text-rose-600 hover:text-rose-700">
View All <ChevronRight className="ml-1 h-4 w-4" />
</Link>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 stagger">
{featuredProducts.map((product, i) => (
<Link to="/product" key={product.id} className="group">
<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} animate-scale-in 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-sm transition-transform duration-300 group-hover:scale-110 group-hover:brightness-110">
Product Image
</div>
{product.sale && (
<Badge className="absolute top-3 left-3 bg-rose-500 text-white border-0">Sale</Badge>
)}
<button className="absolute top-3 right-3 h-8 w-8 rounded-full bg-white/80 backdrop-blur-sm flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white hover:scale-125 active:scale-90 transition-transform">
<Heart className="h-4 w-4 text-stone-600" />
</button>
<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-4">
<h3 className="font-medium text-sm text-stone-900 group-hover:text-rose-600 transition-colors">{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-1 mt-1">{renderStars(product.rating)}</div>
<div className="mt-2 flex items-center gap-2">
<span className="font-semibold text-stone-900">\${product.price}</span>
{product.originalPrice && (
<span className="text-sm 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>
</Link>
))}
</div>
<div className="mt-8 text-center sm:hidden">
<Button asChild variant="outline" className="rounded-full">
<Link to="/products">View All Products</Link>
</Button>
</div>
</div>
</section>
{/* ── CATEGORIES ── */}
<section className="py-24 bg-stone-50">
<div className="mx-auto max-w-7xl px-6">
<h2 className="text-3xl font-bold tracking-tight text-center mb-12 animate-fade-up">Shop by Category</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 stagger">
{categories.map((cat) => (
<Link to="/products" key={cat.name} className="group relative overflow-hidden rounded-2xl aspect-[4/5] hover:-translate-y-1 hover:shadow-xl transition-all duration-300">
<div className={\`absolute inset-0 bg-gradient-to-br \${cat.gradient}\`} />
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors" />
<div className="relative h-full flex flex-col justify-end p-6 text-white">
<h3 className="text-xl font-bold">{cat.name}</h3>
<p className="text-sm text-white/70 mt-1">{cat.count} items</p>
<span className="mt-3 text-sm font-medium flex items-center gap-1 group-hover:gap-2 transition-all">
Shop Now <ArrowRight className="h-4 w-4" />
</span>
</div>
</Link>
))}
</div>
</div>
</section>
{/* ── TRUST BAR ── */}
<section className="py-24 bg-white border-y border-stone-100">
<div className="mx-auto max-w-7xl px-6 grid grid-cols-2 md:grid-cols-4 gap-8 text-center stagger">
{[
{ icon: Truck, title: "Free Shipping", desc: "On orders over $100" },
{ icon: RotateCcw, title: "Easy Returns", desc: "30-day return policy" },
{ icon: ShieldCheck, title: "Secure Payment", desc: "SSL encrypted checkout" },
{ icon: Sparkles, title: "Premium Quality", desc: "Handpicked materials" },
].map((item) => (
<div key={item.title} className="flex flex-col items-center gap-3 animate-fade-in">
<div className="h-12 w-12 rounded-full bg-rose-50 flex items-center justify-center">
<item.icon className="h-5 w-5 text-rose-600" />
</div>
<h3 className="font-semibold text-sm">{item.title}</h3>
<p className="text-xs text-stone-500">{item.desc}</p>
</div>
))}
</div>
</section>
{/* ── THE COLLECTION LOOKBOOK ── */}
<section className="py-24">
<div className="mx-auto max-w-7xl px-6">
<h2 className="text-3xl font-bold tracking-tight mb-12 animate-fade-up">The Collection</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="col-span-2 row-span-2 relative group overflow-hidden rounded-2xl">
<div className="aspect-square bg-gradient-to-br from-stone-800 to-rose-900 transition-transform duration-700 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors flex items-end p-8">
<div>
<Badge className="bg-white/20 backdrop-blur text-white border-0 mb-2">New Season</Badge>
<h3 className="text-2xl font-bold text-white">Spring Essentials</h3>
</div>
</div>
</div>
<div className="relative group overflow-hidden rounded-2xl">
<div className="aspect-square bg-gradient-to-br from-amber-600 to-amber-800 transition-transform duration-700 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors flex items-end p-6">
<div>
<Badge className="bg-white/20 backdrop-blur text-white border-0 mb-2">Trending</Badge>
<h3 className="text-lg font-bold text-white">Accessories</h3>
</div>
</div>
</div>
<div className="relative group overflow-hidden rounded-2xl">
<div className="aspect-square bg-gradient-to-br from-rose-600 to-stone-700 transition-transform duration-700 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors flex items-end p-6">
<div>
<Badge className="bg-white/20 backdrop-blur text-white border-0 mb-2">Popular</Badge>
<h3 className="text-lg font-bold text-white">Best Sellers</h3>
</div>
</div>
</div>
<div className="relative group overflow-hidden rounded-2xl">
<div className="aspect-square bg-gradient-to-br from-violet-700 to-stone-900 transition-transform duration-700 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors flex items-end p-6">
<div>
<Badge className="bg-white/20 backdrop-blur text-white border-0 mb-2">Exclusive</Badge>
<h3 className="text-lg font-bold text-white">Limited Edition</h3>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ── BRAND STORY ── */}
<section className="py-24 bg-muted/30">
<div className="mx-auto max-w-4xl px-6 text-center">
<p className="text-sm uppercase tracking-widest text-muted-foreground mb-4 animate-fade-up">Our Story</p>
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight mb-6 animate-fade-up">Crafted with Care Since 2020</h2>
<p className="text-lg text-muted-foreground leading-relaxed mb-8">Every piece in our collection is thoughtfully designed and ethically sourced. We believe in quality over quantity, creating timeless pieces that tell a story.</p>
<Button variant="outline" className="rounded-full">Learn More <ArrowRight className="ml-2 h-4 w-4" /></Button>
</div>
</section>
{/* ── NEWSLETTER ── */}
<section className="py-24 bg-gradient-to-br from-stone-900 to-stone-800 text-white">
<div className="mx-auto max-w-2xl px-6 text-center">
<Mail className="mx-auto h-10 w-10 text-rose-400 mb-6" />
<h2 className="text-3xl font-bold animate-fade-up">Stay in the Loop</h2>
<p className="mt-3 text-stone-300 animate-fade-up [animation-delay:100ms]">Get early access to new arrivals, exclusive deals, and styling tips.</p>
{subscribed ? (
<div className="mt-8 animate-fade-in">
<div className="inline-flex items-center gap-2 bg-green-500/20 text-green-300 rounded-full px-6 py-3 text-sm font-medium">
<Check className="h-4 w-4" /> You&apos;re subscribed! Check your inbox.
</div>
</div>
) : (
<form onSubmit={(e) => { e.preventDefault(); if (email) { setSubscribed(true); toast.success("Subscribed!"); } }} className="mt-8 flex flex-col sm:flex-row gap-3 max-w-md mx-auto">
<Input
type="email"
required
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-white/10 border-white/20 text-white placeholder:text-stone-400 rounded-full"
/>
<Button type="submit" className="bg-rose-500 hover:bg-rose-600 text-white rounded-full px-6 whitespace-nowrap active:scale-[0.98] transition-all">
Subscribe
</Button>
</form>
)}
<p className="mt-4 text-xs text-stone-400">Join 15,000+ subscribers. No spam, unsubscribe anytime.</p>
</div>
</section>
{/* ── INSTAGRAM GALLERY ── */}
<section className="py-24 bg-white">
<div className="mx-auto max-w-7xl px-6 text-center">
<h2 className="text-3xl font-bold tracking-tight mb-2 animate-fade-up">Follow @StyleHaus</h2>
<p className="text-stone-500 mb-10">Tag us in your looks for a chance to be featured</p>
<div className="grid grid-cols-3 md:grid-cols-6 gap-2">
{["from-rose-100 to-rose-200", "from-stone-100 to-stone-200", "from-amber-100 to-amber-200",
"from-rose-200 to-stone-100", "from-stone-200 to-amber-100", "from-amber-200 to-rose-100"
].map((grad, i) => (
<div key={i} className={\`aspect-square rounded-lg bg-gradient-to-br \${grad} group cursor-pointer relative overflow-hidden\`}>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-center justify-center">
<Heart className="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
))}
</div>
</div>
</section>
<Footer />
</div>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { CheckCircle, Package, ArrowRight } from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function OrderConfirmationPage() {
const orderNumber = "ORD-2024-" + Math.floor(1000 + Math.random() * 9000);
return (
<div className="min-h-screen bg-stone-50 text-stone-900 antialiased">
<Header />
<div className="mx-auto max-w-xl px-6 pt-28 pb-20 text-center">
<div className="h-20 w-20 mx-auto rounded-full bg-green-100 flex items-center justify-center mb-6 animate-scale-in">
<CheckCircle className="h-10 w-10 text-green-600" />
</div>
<h1 className="text-3xl font-bold mb-2 animate-fade-up">Order Confirmed!</h1>
<p className="text-stone-500 mb-2">Order number: <span className="font-mono font-semibold text-stone-900">{orderNumber}</span></p>
<p className="text-stone-500 mb-10">Thank you for your purchase. You will receive a confirmation email shortly.</p>
<Card className="text-left">
<CardContent className="p-6">
<h2 className="font-bold mb-4">Order Summary</h2>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-stone-600">Classic Leather Jacket x1</span>
<span className="font-medium">$189.00</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-stone-600">Silk Evening Dress x1</span>
<span className="font-medium">$245.00</span>
</div>
</div>
<Separator className="my-4" />
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-stone-500">Subtotal</span>
<span>$434.00</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Shipping</span>
<span className="text-green-600">Free</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">Tax</span>
<span>$34.72</span>
</div>
</div>
<Separator className="my-4" />
<div className="flex justify-between font-bold text-lg">
<span>Total</span>
<span>$468.72</span>
</div>
</CardContent>
</Card>
<div className="flex flex-col sm:flex-row gap-3 mt-8 justify-center">
<Button asChild className="bg-stone-900 hover:bg-stone-800 rounded-full px-8 active:scale-[0.98] transition-all">
<Link to="/products">Continue Shopping <ArrowRight className="ml-2 h-4 w-4" /></Link>
</Button>
<Button asChild variant="outline" className="rounded-full px-8 active:scale-[0.98] transition-all">
<Link to="/account"><Package className="mr-2 h-4 w-4" /> Track Order</Link>
</Button>
</div>
</div>
<Footer />
</div>
);
}
+307
View File
@@ -0,0 +1,307 @@
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Star, Heart, Minus, Plus, ChevronRight, Truck, RotateCcw, ShieldCheck, ShoppingBag, Flame, ZoomIn
} from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useCart } from "@/lib/cart-context";
const thumbnails = [
"from-rose-100 to-stone-200",
"from-stone-100 to-rose-100",
"from-amber-100 to-stone-100",
"from-stone-200 to-amber-100",
];
const colors = [
{ name: "Black", class: "bg-stone-900" },
{ name: "Cognac", class: "bg-amber-700" },
{ name: "Rose", class: "bg-rose-400" },
{ name: "Stone", class: "bg-stone-400" },
];
const sizes = ["XS", "S", "M", "L", "XL", "XXL"];
const reviews = [
{ name: "Sarah M.", date: "2 weeks ago", rating: 5, text: "Absolutely stunning quality. The leather is soft yet durable and the fit is perfect. Worth every penny!", avatar: "SM" },
{ name: "James K.", date: "1 month ago", rating: 4, text: "Great jacket overall. Runs slightly large so consider sizing down. The color is rich and beautiful.", avatar: "JK" },
{ name: "Emily R.", date: "1 month ago", rating: 5, text: "This is my third purchase from StyleHaus and they never disappoint. The craftsmanship is exceptional.", avatar: "ER" },
];
const relatedProducts = [
{ id: 10, name: "Satin Blouse", price: 89, gradient: "from-rose-50 to-rose-100", rating: 4 },
{ id: 11, name: "Tailored Wool Coat", price: 385, gradient: "from-stone-200 to-stone-400", rating: 5 },
{ id: 7, name: "Organic Cotton Tee", price: 45, gradient: "from-rose-100 to-rose-200", rating: 4 },
{ id: 8, name: "Velvet Midi Skirt", price: 115, gradient: "from-stone-300 to-stone-100", rating: 5 },
];
export default function ProductDetailPage() {
const [selectedImage, setSelectedImage] = useState(0);
const [selectedColor, setSelectedColor] = useState("Black");
const [selectedSize, setSelectedSize] = useState("M");
const [quantity, setQuantity] = useState(1);
const [wishlisted, setWishlisted] = useState(false);
const { addItem } = useCart();
const renderStars = (count: number) =>
Array.from({ length: 5 }, (_, i) => (
<Star key={i} className={\`h-4 w-4 \${i < count ? "fill-amber-400 text-amber-400" : "text-stone-300"}\`} />
));
const handleAddToCart = () => {
addItem({
id: "leather-jacket-1",
name: "Classic Leather Jacket",
price: 189,
quantity,
size: selectedSize,
color: selectedColor,
image: "",
});
toast.success("Added to cart!");
};
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">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm text-stone-500 mb-8">
<Link to="/" className="hover:text-stone-900">Home</Link>
<ChevronRight className="h-3 w-3" />
<Link to="/products" className="hover:text-stone-900">Products</Link>
<ChevronRight className="h-3 w-3" />
<span className="text-stone-900">Classic Leather Jacket</span>
</nav>
{/* Product Layout */}
<div className="grid lg:grid-cols-2 gap-12">
{/* Left — Images */}
<div className="space-y-4">
<Dialog>
<DialogTrigger asChild>
<div className={\`aspect-square rounded-2xl bg-gradient-to-br \${thumbnails[selectedImage]} flex items-center justify-center text-stone-400 relative overflow-hidden animate-scale-in cursor-zoom-in group/zoom\`}>
<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-12 h-32 w-32 rounded-full bg-white/30 blur-2xl" />
<div className="absolute bottom-1/4 -right-12 h-28 w-28 rounded-full bg-white/20 blur-2xl" />
<span className="relative">Product Image</span>
<div className="absolute bottom-3 right-3 h-8 w-8 rounded-full bg-white/80 backdrop-blur flex items-center justify-center opacity-0 group-hover/zoom:opacity-100 transition-opacity shadow-sm">
<ZoomIn className="h-4 w-4 text-stone-700" />
</div>
</div>
</DialogTrigger>
<DialogContent className="max-w-4xl p-0 border-0 overflow-hidden">
<div className={\`w-full aspect-square bg-gradient-to-br \${thumbnails[selectedImage]} flex items-center justify-center text-stone-400 relative overflow-hidden\`}>
<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-12 h-32 w-32 rounded-full bg-white/30 blur-2xl" />
<div className="absolute bottom-1/4 -right-12 h-28 w-28 rounded-full bg-white/20 blur-2xl" />
<span className="relative text-lg">Product Image</span>
</div>
</DialogContent>
</Dialog>
<div className="grid grid-cols-4 gap-3">
{thumbnails.map((grad, i) => (
<button
key={i}
onClick={() => setSelectedImage(i)}
className={\`aspect-square rounded-lg bg-gradient-to-br \${grad} \${selectedImage === i ? "ring-2 ring-stone-900 ring-offset-2" : "opacity-60 hover:opacity-100"} transition-all\`}
/>
))}
</div>
</div>
{/* Right — Details */}
<div className="space-y-6">
<div>
<Badge className="bg-rose-100 text-rose-700 border-0 mb-3 animate-fade-in">Best Seller</Badge>
<h1 className="text-3xl font-bold tracking-tight animate-fade-up">Classic Leather Jacket</h1>
<div className="flex items-center gap-3 mt-3">
<div className="flex items-center gap-0.5">{renderStars(5)}</div>
<span className="text-sm text-stone-500">(128 reviews)</span>
</div>
</div>
<div className="flex items-baseline gap-3">
<span className="text-3xl font-bold">$189</span>
<span className="text-xl text-stone-400 line-through">$249</span>
<Badge className="bg-rose-500 text-white border-0">-24%</Badge>
</div>
<p className="text-stone-600 leading-relaxed">Timeless silhouette crafted from premium full-grain leather. Features a tailored fit with minimal hardware for a clean, modern aesthetic.</p>
<Separator />
{/* Color Selector */}
<div>
<h3 className="text-sm font-semibold mb-3">Color: <span className="font-normal text-stone-500">{selectedColor}</span></h3>
<div className="flex gap-3">
{colors.map((c) => (
<button
key={c.name}
onClick={() => setSelectedColor(c.name)}
className={\`h-9 w-9 rounded-full \${c.class} \${selectedColor === c.name ? "ring-2 ring-offset-2 ring-stone-900" : ""} transition-all\`}
title={c.name}
/>
))}
</div>
</div>
{/* Size Selector */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold">Size</h3>
<button className="text-xs text-rose-600 hover:underline">Size Guide</button>
</div>
<div className="flex flex-wrap gap-2">
{sizes.map((s) => (
<Button
key={s}
variant={selectedSize === s ? "default" : "outline"}
size="sm"
className={\`min-w-[3rem] \${selectedSize === s ? "bg-stone-900" : ""}\`}
onClick={() => setSelectedSize(s)}
>
{s}
</Button>
))}
</div>
</div>
{/* Scarcity Signal */}
<div className="flex items-center gap-2 text-sm text-amber-600 mb-3"><Flame className="h-4 w-4" /> Selling Fast — Only 5 left in stock</div>
{/* Quantity + Add to Cart */}
<div className="flex items-center gap-4">
<div className="flex items-center border rounded-full">
<Button variant="ghost" size="icon" className="h-10 w-10 rounded-full active:scale-90 transition-transform" onClick={() => setQuantity(Math.max(1, quantity - 1))}>
<Minus className="h-4 w-4" />
</Button>
<span className="w-10 text-center font-medium">{quantity}</span>
<Button variant="ghost" size="icon" className="h-10 w-10 rounded-full active:scale-90 transition-transform" onClick={() => setQuantity(quantity + 1)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<Button size="lg" className="flex-1 bg-stone-900 hover:bg-stone-800 rounded-full active:scale-[0.98] transition-all" onClick={handleAddToCart}>
<ShoppingBag className="mr-2 h-4 w-4" /> Add to Cart
</Button>
<Button variant="outline" size="icon" className={\`h-11 w-11 rounded-full hover:scale-125 active:scale-90 transition-transform \${wishlisted ? "border-rose-300 bg-rose-50" : ""}\`} onClick={() => { if (!wishlisted) toast.success("Added to wishlist!"); setWishlisted(!wishlisted); }}>
<Heart className={\`h-4 w-4 \${wishlisted ? "fill-rose-500 text-rose-500" : ""}\`} />
</Button>
</div>
{/* Trust */}
<div className="grid grid-cols-3 gap-4 pt-4 animate-fade-in">
{[
{ icon: Truck, text: "Free Shipping" },
{ icon: RotateCcw, text: "Easy Returns" },
{ icon: ShieldCheck, text: "Secure Payment" },
].map((item) => (
<div key={item.text} className="flex items-center gap-2 text-xs text-stone-500">
<item.icon className="h-4 w-4 text-rose-500 shrink-0" />
{item.text}
</div>
))}
</div>
</div>
</div>
{/* Tabs */}
<Tabs defaultValue="description" className="mt-16">
<TabsList className="bg-stone-100 rounded-full p-1">
<TabsTrigger value="description" className="rounded-full text-sm">Description</TabsTrigger>
<TabsTrigger value="reviews" className="rounded-full text-sm">Reviews (128)</TabsTrigger>
<TabsTrigger value="shipping" className="rounded-full text-sm">Shipping Info</TabsTrigger>
</TabsList>
<TabsContent value="description" className="mt-6 prose prose-stone max-w-none">
<p className="text-stone-600 leading-relaxed">Our Classic Leather Jacket is the cornerstone of any wardrobe. Made from hand-selected full-grain leather that develops a beautiful patina over time, this jacket is designed to be your companion for years to come. The tailored fit flatters every body type while allowing comfortable layering.</p>
<ul className="mt-4 space-y-2 text-sm text-stone-600">
<li>Premium full-grain leather construction</li>
<li>YKK zippers with custom pulls</li>
<li>Interior and exterior pockets</li>
<li>Fully lined with satin finish</li>
<li>Available in 4 colors</li>
</ul>
</TabsContent>
<TabsContent value="reviews" className="mt-6 space-y-6">
<div className="flex items-center gap-4 mb-8">
<div className="text-4xl font-bold">4.8</div>
<div>
<div className="flex gap-0.5">{[...Array(5)].map((_, i) => <Star key={i} className="h-5 w-5 fill-amber-400 text-amber-400" />)}</div>
<p className="text-sm text-muted-foreground mt-1">Based on 128 reviews</p>
</div>
</div>
{reviews.map((review, i) => (
<div key={i} className="flex gap-4 pb-6 border-b border-stone-100 last:border-0">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-rose-400 to-amber-300 flex items-center justify-center text-white text-sm font-bold shrink-0">{review.avatar}</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-medium text-sm">{review.name}</h4>
<Badge className="bg-green-50 text-green-700 border-0 text-[10px] px-1.5 py-0">Verified Purchase</Badge>
<span className="text-xs text-stone-400 ml-auto">{review.date}</span>
</div>
<div className="flex items-center gap-0.5 mt-1">{renderStars(review.rating)}</div>
<p className="mt-2 text-sm text-stone-600">{review.text}</p>
</div>
</div>
))}
<Button variant="outline" className="rounded-full mt-4">Write a Review</Button>
</TabsContent>
<TabsContent value="shipping" className="mt-6">
<div className="space-y-4 text-sm text-stone-600">
<div className="flex items-start gap-3">
<Truck className="h-5 w-5 text-rose-500 mt-0.5" />
<div>
<h4 className="font-medium text-stone-900">Free Standard Shipping</h4>
<p>On all orders over $100. Delivery in 5-7 business days.</p>
</div>
</div>
<div className="flex items-start gap-3">
<RotateCcw className="h-5 w-5 text-rose-500 mt-0.5" />
<div>
<h4 className="font-medium text-stone-900">30-Day Returns</h4>
<p>Not satisfied? Return within 30 days for a full refund.</p>
</div>
</div>
</div>
</TabsContent>
</Tabs>
{/* You Might Also Like */}
<section className="py-24">
<h2 className="text-2xl font-bold tracking-tight mb-8 animate-fade-up">You Might Also Like</h2>
<div className="flex gap-6 overflow-x-auto pb-4 snap-x">
{relatedProducts.map((product) => (
<Link to="/product" key={product.id} className="group snap-start min-w-[250px]">
<Card className="overflow-hidden border-0 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
<div className={\`aspect-[3/4] bg-gradient-to-br \${product.gradient} flex items-center justify-center text-stone-400 text-xs relative overflow-hidden animate-scale-in\`}>
<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-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" />
<span className="relative">Image</span>
</div>
<CardContent className="p-4">
<h3 className="font-medium text-sm group-hover:text-rose-600 transition-colors">{product.name}</h3>
<div className="flex items-center gap-0.5 mt-1">{renderStars(product.rating)}</div>
<p className="mt-1.5 font-semibold">\${product.price}</p>
</CardContent>
</Card>
</Link>
))}
</div>
</section>
</div>
<Footer />
</div>
);
}
+303
View File
@@ -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>
);
}
+137
View File
@@ -0,0 +1,137 @@
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 { Search as SearchIcon, Star, Heart, X, Sparkles } from "lucide-react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const allProducts = [
{ id: 1, name: "Classic Leather Jacket", price: 189, gradient: "from-rose-100 to-stone-200", rating: 5, category: "Outerwear" },
{ id: 2, name: "Silk Evening Dress", price: 245, gradient: "from-amber-100 to-orange-200", rating: 5, category: "Dresses" },
{ id: 3, name: "Cashmere Knit Sweater", price: 128, gradient: "from-stone-100 to-stone-300", rating: 4, category: "Knitwear" },
{ id: 4, name: "Linen Wide-Leg Trousers", price: 95, gradient: "from-rose-50 to-amber-100", rating: 4, category: "Bottoms" },
{ id: 5, name: "Italian Leather Handbag", price: 320, gradient: "from-stone-200 to-rose-100", rating: 5, category: "Accessories" },
{ id: 6, name: "Merino Wool Blazer", price: 165, gradient: "from-amber-50 to-stone-200", rating: 4, category: "Outerwear" },
{ id: 7, name: "Organic Cotton Tee", price: 45, gradient: "from-rose-100 to-rose-200", rating: 4, category: "Tops" },
{ id: 8, name: "Velvet Midi Skirt", price: 115, gradient: "from-stone-300 to-stone-100", rating: 5, category: "Bottoms" },
{ id: 9, name: "Suede Ankle Boots", price: 198, gradient: "from-amber-200 to-stone-200", rating: 5, category: "Footwear" },
{ id: 10, name: "Satin Blouse", price: 89, gradient: "from-rose-50 to-rose-100", rating: 4, category: "Tops" },
{ id: 11, name: "Tailored Wool Coat", price: 385, gradient: "from-stone-200 to-stone-400", rating: 5, category: "Outerwear" },
{ id: 12, name: "Pearl Drop Earrings", price: 68, gradient: "from-amber-100 to-rose-100", rating: 4, category: "Accessories" },
];
const filterChips = ["All", "Outerwear", "Dresses", "Tops", "Bottoms", "Accessories", "Footwear", "Knitwear"];
export default function SearchPage() {
const [query, setQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("All");
const filtered = allProducts.filter((p) => {
const matchesQuery = query.length === 0 || p.name.toLowerCase().includes(query.toLowerCase());
const matchesFilter = activeFilter === "All" || p.category === activeFilter;
return matchesQuery && matchesFilter;
});
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"}\`} />
));
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">
{/* Search Input */}
<div className="max-w-2xl mx-auto mb-10">
<div className="relative">
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-stone-400" />
<Input
type="text"
placeholder="Search for products..."
value={query}
onChange={(e) => setQuery(e.target.value)}
autoFocus
className="pl-12 pr-10 h-14 text-lg rounded-full border-stone-200 focus-visible:ring-rose-500 focus-visible:ring-2 focus-visible:border-rose-400 transition-shadow"
/>
{query && (
<button onClick={() => setQuery("")} className="absolute right-4 top-1/2 -translate-y-1/2 text-stone-400 hover:text-stone-600">
<X className="h-5 w-5" />
</button>
)}
</div>
</div>
{/* Results heading */}
{query && (
<h2 className="text-xl font-bold mb-6">
{filtered.length} result{filtered.length !== 1 ? "s" : ""} for "<span className="text-rose-600">{query}</span>"
</h2>
)}
{/* Filter Chips */}
<div className="flex flex-wrap gap-2 mb-8">
{filterChips.map((chip) => (
<Button
key={chip}
variant={activeFilter === chip ? "default" : "outline"}
size="sm"
className={\`rounded-full text-sm \${activeFilter === chip ? "bg-stone-900" : ""}\`}
onClick={() => setActiveFilter(chip)}
>
{chip}
</Button>
))}
</div>
{/* Results */}
{filtered.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 stagger">
{filtered.map((product) => (
<Link to="/product" key={product.id} className="group">
<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" 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">Image</div>
<button 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-110 active:scale-95">
<Heart className="h-3.5 w-3.5 text-stone-600" />
</button>
</div>
<CardContent className="p-3">
<h3 className="font-medium text-xs truncate">{product.name}</h3>
<div className="flex items-center gap-0.5 mt-1">{renderStars(product.rating)}</div>
<p className="mt-1.5 font-semibold text-sm">\${product.price}</p>
</CardContent>
</Card>
</Link>
))}
</div>
) : (
<div className="text-center py-20">
<div className="h-16 w-16 mx-auto rounded-full bg-stone-100 flex items-center justify-center mb-4">
<SearchIcon className="h-8 w-8 text-stone-400" />
</div>
<h3 className="text-xl font-bold mb-2">No results found</h3>
<p className="text-stone-500 mb-6 max-w-md mx-auto">
We couldn't find anything matching your search. Try different keywords or browse our categories.
</p>
<div className="flex flex-wrap justify-center gap-2">
<Badge variant="secondary" className="cursor-pointer hover:bg-stone-200">Summer Dresses</Badge>
<Badge variant="secondary" className="cursor-pointer hover:bg-stone-200">Leather Jackets</Badge>
<Badge variant="secondary" className="cursor-pointer hover:bg-stone-200">Cashmere</Badge>
<Badge variant="secondary" className="cursor-pointer hover:bg-stone-200">Accessories</Badge>
</div>
</div>
)}
</div>
<Footer />
</div>
);
}