Initial: ecommerce template via tAI
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"Glob",
|
||||
"Grep"
|
||||
]
|
||||
},
|
||||
"model": "claude-sonnet-4-20250514"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# .tai-manifest.tn — ecommerce (vite-react)
|
||||
# Auto-generated by tAI manifest generator
|
||||
# tN (tAI Notation) — compact template encoding
|
||||
|
||||
σ:template|ecommerce|vite-react|17 files|117KB
|
||||
σ:docs|CLAUDE.md|1124B|imports:1
|
||||
σ:docs|README.md|41B
|
||||
σ:entry|index.html|278B|title="Ecommerce"
|
||||
σ:app-root|src/App.tsx|1170B|routes:8|imports:10
|
||||
σ:component|src/components/Footer.tsx|3349B|imports:2|uses:[Link,Separator]
|
||||
σ:component|src/components/Header.tsx|6954B|imports:10
|
||||
σ:style|src/index.css|59B
|
||||
σ:utility|src/lib/cart-context.tsx|2532B|imports:1|uses:[CartContextType,CartItem,CartContext]
|
||||
σ:bootstrap|src/main.tsx|236B|imports:3|uses:[React,App]
|
||||
σ:page|src/pages/Account.tsx|10374B|h1="Welcome back, {profile.firstName}"|imports:11
|
||||
σ:page|src/pages/Cart.tsx|8631B|h1="Your cart is empty"|imports:12
|
||||
σ:page|src/pages/Checkout.tsx|19311B|imports:12
|
||||
σ:page|src/pages/Index.tsx|20029B|imports:10
|
||||
σ:page|src/pages/OrderConfirmation.tsx|3351B|h1="Order Confirmed!"|imports:7
|
||||
σ:page|src/pages/ProductDetail.tsx|16562B|h1="Classic Leather Jacket"|imports:13
|
||||
σ:page|src/pages/Products.tsx|18616B|h1="All Products"|imports:10
|
||||
σ:page|src/pages/Search.tsx|7498B|imports:10
|
||||
@@ -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
|
||||
+12
@@ -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
@@ -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;
|
||||
@@ -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">
|
||||
© 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user