From fca2c25e12f5891e61f92e08815ddaa554f077d1 Mon Sep 17 00:00:00 2001 From: Biswa Kalyan Bhuyan Date: Wed, 28 May 2025 16:30:40 +0530 Subject: feat: added frontpage layout --- frontend/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/src/app/globals.css | 122 +++++++++++ frontend/src/app/layout.tsx | 29 +++ frontend/src/app/page.tsx | 154 +++++++++++++ frontend/src/components/footer.tsx | 267 +++++++++++++++++++++++ frontend/src/components/header.tsx | 287 +++++++++++++++++++++++++ frontend/src/components/hero-section.tsx | 163 ++++++++++++++ frontend/src/components/product-card.tsx | 251 +++++++++++++++++++++ frontend/src/components/ui/avatar.tsx | 53 +++++ frontend/src/components/ui/badge.tsx | 46 ++++ frontend/src/components/ui/button.tsx | 59 +++++ frontend/src/components/ui/card.tsx | 92 ++++++++ frontend/src/components/ui/checkbox.tsx | 32 +++ frontend/src/components/ui/dialog.tsx | 135 ++++++++++++ frontend/src/components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++++++ frontend/src/components/ui/input.tsx | 21 ++ frontend/src/components/ui/navigation-menu.tsx | 168 +++++++++++++++ frontend/src/components/ui/radio-group.tsx | 45 ++++ frontend/src/components/ui/select.tsx | 185 ++++++++++++++++ frontend/src/components/ui/separator.tsx | 28 +++ frontend/src/components/ui/sheet.tsx | 139 ++++++++++++ frontend/src/components/ui/skeleton.tsx | 13 ++ frontend/src/components/ui/slider.tsx | 63 ++++++ frontend/src/components/ui/sonner.tsx | 25 +++ frontend/src/lib/utils.ts | 6 + 25 files changed, 2640 insertions(+) create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/components/footer.tsx create mode 100644 frontend/src/components/header.tsx create mode 100644 frontend/src/components/hero-section.tsx create mode 100644 frontend/src/components/product-card.tsx create mode 100644 frontend/src/components/ui/avatar.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/navigation-menu.tsx create mode 100644 frontend/src/components/ui/radio-group.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/separator.tsx create mode 100644 frontend/src/components/ui/sheet.tsx create mode 100644 frontend/src/components/ui/skeleton.tsx create mode 100644 frontend/src/components/ui/slider.tsx create mode 100644 frontend/src/components/ui/sonner.tsx create mode 100644 frontend/src/lib/utils.ts (limited to 'frontend/src') diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..92fb855 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-mono: ui-monospace, "SFMono-Regular", "Consolas", monospace; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..50bb899 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,29 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Toaster } from "@/components/ui/sonner"; + +export const metadata: Metadata = { + title: "blcklst - Modern Fashion Brand", + description: "not everyone gets blcklsted - discover carefully curated fashion pieces that define modern elegance.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..7a9bea6 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,154 @@ +import { Header } from "@/components/header"; +import { HeroSection } from "@/components/hero-section"; +import { ProductCard } from "@/components/product-card"; +import { Footer } from "@/components/footer"; + +export default function Home() { + // Sample product data for demonstration + const featuredProducts = [ + { + id: "1", + name: "Premium Cotton T-Shirt", + price: 29.99, + originalPrice: 39.99, + image: "/api/placeholder/400/500", + rating: 4.8, + reviewCount: 124, + isNew: true, + isSale: true, + category: "Men's Clothing", + colors: ["#000000", "#FFFFFF", "#808080", "#000080"], + sizes: ["S", "M", "L", "XL"], + }, + { + id: "2", + name: "Elegant Summer Dress", + price: 89.99, + originalPrice: 119.99, + image: "/api/placeholder/400/500", + rating: 4.9, + reviewCount: 89, + isSale: true, + category: "Women's Clothing", + colors: ["#FF69B4", "#000000", "#800080"], + sizes: ["XS", "S", "M", "L"], + }, + { + id: "3", + name: "Classic Denim Jacket", + price: 79.99, + image: "/api/placeholder/400/500", + rating: 4.7, + reviewCount: 156, + isNew: true, + category: "Outerwear", + colors: ["#4169E1", "#000000", "#708090"], + sizes: ["S", "M", "L", "XL", "XXL"], + }, + { + id: "4", + name: "Comfortable Sneakers", + price: 99.99, + originalPrice: 129.99, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 203, + isSale: true, + category: "Footwear", + colors: ["#FFFFFF", "#000000", "#FF0000"], + sizes: ["7", "8", "9", "10", "11", "12"], + }, + { + id: "5", + name: "Cozy Winter Sweater", + price: 69.99, + image: "/api/placeholder/400/500", + rating: 4.8, + reviewCount: 78, + isNew: true, + category: "Knitwear", + colors: ["#8B4513", "#000000", "#008000", "#800080"], + sizes: ["S", "M", "L", "XL"], + }, + { + id: "6", + name: "Stylish Crossbody Bag", + price: 45.99, + originalPrice: 59.99, + image: "/api/placeholder/400/500", + rating: 4.5, + reviewCount: 92, + isSale: true, + category: "Accessories", + colors: ["#000000", "#8B4513", "#800080"], + sizes: ["One Size"], + }, + ]; + + return ( +
+
+ +
+ + + {/* Featured Products Section */} +
+
+
+

Featured Products

+

+ Discover our handpicked selection of trending items that our customers love most. +

+
+ +
+ {featuredProducts.map((product) => ( + + ))} +
+ +
+ +
+
+
+ + {/* Categories Section */} +
+
+
+

Shop by Category

+

+ Explore our diverse range of fashion categories +

+
+ +
+ {[ + { name: "Women", image: "/api/placeholder/600/400", count: "2,345+ items" }, + { name: "Men", image: "/api/placeholder/600/400", count: "1,892+ items" }, + { name: "Kids", image: "/api/placeholder/600/400", count: "756+ items" }, + ].map((category) => ( +
+
+
+

{category.name}

+

{category.count}

+
+
+ ))} +
+
+
+
+ +
+
+ ); +} diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx new file mode 100644 index 0000000..d7c8ae7 --- /dev/null +++ b/frontend/src/components/footer.tsx @@ -0,0 +1,267 @@ +import Link from "next/link"; +import Image from "next/image"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + Facebook, + Instagram, + Twitter, + Youtube, + Mail, + Phone, + MapPin, + CreditCard, + Shield, + Truck, + RefreshCw, +} from "lucide-react"; + +export function Footer() { + const currentYear = new Date().getFullYear(); + + const footerLinks = { + shop: [ + { name: "Women", href: "/women" }, + { name: "Men", href: "/men" }, + { name: "Kids", href: "/kids" }, + { name: "Sale", href: "/sale" }, + { name: "New Arrivals", href: "/new-arrivals" }, + { name: "Best Sellers", href: "/best-sellers" }, + ], + company: [ + { name: "About Us", href: "/about" }, + { name: "Careers", href: "/careers" }, + { name: "Press", href: "/press" }, + { name: "Sustainability", href: "/sustainability" }, + { name: "Store Locator", href: "/stores" }, + ], + help: [ + { name: "Customer Service", href: "/customer-service" }, + { name: "Size Guide", href: "/size-guide" }, + { name: "Shipping & Returns", href: "/shipping-returns" }, + { name: "Track Your Order", href: "/track-order" }, + { name: "FAQ", href: "/faq" }, + { name: "Contact Us", href: "/contact" }, + ], + legal: [ + { name: "Privacy Policy", href: "/privacy" }, + { name: "Terms of Service", href: "/terms" }, + { name: "Cookie Policy", href: "/cookies" }, + { name: "Accessibility", href: "/accessibility" }, + ], + }; + + return ( +
+ {/* Newsletter Section */} +
+
+
+

Stay in the loop

+

+ Subscribe to our newsletter and be the first to know about new arrivals, sales, and exclusive offers. +

+
+ + +
+

+ By subscribing, you agree to our Privacy Policy and Terms of Service. +

+
+
+
+ + {/* Main Footer Content */} +
+
+ {/* Brand Info */} +
+ + blcklst + +

+ not everyone gets blcklsted - discover carefully curated fashion pieces that define modern elegance and exclusivity. +

+ + {/* Contact Info */} +
+
+ + +1 (555) 123-4567 +
+
+ + hello@blcklst.com +
+
+ + 123 Fashion St, New York, NY 10001 +
+
+ + {/* Social Media */} +
+ + + + + + + + + + + + +
+
+ + {/* Shop Links */} +
+

Shop

+
    + {footerLinks.shop.map((link) => ( +
  • + + {link.name} + +
  • + ))} +
+
+ + {/* Company Links */} +
+

Company

+
    + {footerLinks.company.map((link) => ( +
  • + + {link.name} + +
  • + ))} +
+
+ + {/* Help Links */} +
+

Help

+
    + {footerLinks.help.map((link) => ( +
  • + + {link.name} + +
  • + ))} +
+
+
+ + {/* Features Section */} +
+
+
+
+ +
+
+

Free Shipping

+

On orders over $100

+
+
+
+
+ +
+
+

Easy Returns

+

30-day return policy

+
+
+
+
+ +
+
+

Secure Payment

+

SSL encrypted checkout

+
+
+
+
+ +
+
+

Multiple Payment

+

Cards, PayPal & more

+
+
+
+
+
+ + {/* Bottom Footer */} +
+
+
+
+

+ Β© {currentYear} blcklst. All rights reserved. +

+
+ {footerLinks.legal.map((link) => ( + + {link.name} + + ))} +
+
+ + {/* Payment Methods */} +
+ We accept: +
+ {["Visa", "Mastercard", "AmEx", "PayPal", "Apple Pay"].map((method) => ( +
+ {method} +
+ ))} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx new file mode 100644 index 0000000..d64061a --- /dev/null +++ b/frontend/src/components/header.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, +} from "@/components/ui/navigation-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Search, + ShoppingBag, + Heart, + User, + Menu, + Phone, + Mail, + Truck, + RefreshCw, + Shield, + Globe, +} from "lucide-react"; + +export function Header() { + const [cartItems] = useState(3); + const [wishlistItems] = useState(5); + + const categories = [ + { + title: "Women", + items: [ + { name: "Dresses", href: "/women/dresses" }, + { name: "Tops & Blouses", href: "/women/tops" }, + { name: "Pants & Jeans", href: "/women/pants" }, + { name: "Outerwear", href: "/women/outerwear" }, + { name: "Accessories", href: "/women/accessories" }, + ], + }, + { + title: "Men", + items: [ + { name: "T-Shirts", href: "/men/tshirts" }, + { name: "Shirts", href: "/men/shirts" }, + { name: "Pants & Jeans", href: "/men/pants" }, + { name: "Jackets", href: "/men/jackets" }, + { name: "Accessories", href: "/men/accessories" }, + ], + }, + { + title: "Kids", + items: [ + { name: "Boys", href: "/kids/boys" }, + { name: "Girls", href: "/kids/girls" }, + { name: "Baby", href: "/kids/baby" }, + { name: "Shoes", href: "/kids/shoes" }, + ], + }, + ]; + + return ( +
+ {/* Top banner */} +
+

+ FREE SHIPPING ON ORDERS OVER $100 β€’ NEW ARRIVALS EVERY WEEK +

+
+ + {/* Announcement bar */} +
+

+ πŸ”₯ WINTER SALE - Up to 50% off on selected items +

+
+ + {/* Main header */} +
+
+
+ {/* Mobile menu */} + + + + + + + Menu + + + + + + {/* Logo */} +
+ + blcklst + +
+ + {/* Desktop Navigation */} + + + {categories.map((category) => ( + + + {category.title} + + +
+ {category.items.map((item) => ( + + +
{item.name}
+ +
+ ))} +
+
+
+ ))} + + + + Sale + + + +
+
+ + {/* Search Bar */} +
+
+ + +
+
+ + {/* Action buttons */} +
+ {/* Search icon for mobile */} + + + {/* User menu */} + + + + + + + + My Account + + + + Orders + + + + Wishlist + + + + Sign In + + + Create Account + + + + + {/* Wishlist */} + + + {/* Cart */} + + + {/* Language/Currency */} + + + + + + πŸ‡ΊπŸ‡Έ USD + πŸ‡ͺπŸ‡Ί EUR + πŸ‡¬πŸ‡§ GBP + + +
+
+
+
+ + {/* Service features */} +
+
+
+
+ + Free Shipping +
+
+ + 30-Day Returns +
+
+ + Secure Payment +
+
+ + 24/7 Support +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/hero-section.tsx b/frontend/src/components/hero-section.tsx new file mode 100644 index 0000000..2450365 --- /dev/null +++ b/frontend/src/components/hero-section.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { ArrowRight, Star, Sparkles } from "lucide-react"; +import Link from "next/link"; + +export function HeroSection() { + return ( +
+ {/* Background decoration */} +
+
+
+ +
+
+ {/* Left side - Content */} +
+ {/* Badge */} +
+ + + New Winter Collection + + + Limited Time + +
+ + {/* Main heading */} +
+

+ Curated + Exclusives +

+

+ not everyone gets blcklsted. Discover carefully curated fashion pieces that define modern elegance and exclusivity. +

+
+ + {/* Features */} +
+
+
+ Free shipping worldwide +
+
+
+ 30-day easy returns +
+
+
+ Sustainable materials +
+
+ + {/* CTA Buttons */} +
+ + +
+ + {/* Social proof */} +
+
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ 4.9/5 +
+
+ Trusted by 50,000+ customers +
+
+
+ + {/* Right side - Product showcase */} +
+ {/* Main product image */} +
+
+
+
+ +
+

Featured Product

+

Premium Winter Coat

+
+
+ + {/* Price tag */} +
+ 50% OFF +
+
+ + {/* Floating product cards */} +
+
+
+
+

Summer Dress

+

$89.99

+
+ + 4.8 +
+
+
+
+ +
+
+
+
+

Casual Sneakers

+

$129.99

+
+ + 4.9 +
+
+
+
+ + {/* Stats bubble */} +
+
+

This Week

+

2.5K+

+

Orders

+
+
+
+
+
+ + {/* Bottom wave */} +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/product-card.tsx b/frontend/src/components/product-card.tsx new file mode 100644 index 0000000..f790c98 --- /dev/null +++ b/frontend/src/components/product-card.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Heart, Eye, ShoppingBag, Star } from "lucide-react"; + +interface ProductCardProps { + id: string; + name: string; + price: number; + originalPrice?: number; + image: string; + images?: string[]; + rating: number; + reviewCount: number; + isNew?: boolean; + isSale?: boolean; + isWishlisted?: boolean; + category: string; + colors?: string[]; + sizes?: string[]; +} + +export function ProductCard({ + id, + name, + price, + originalPrice, + image, + images = [], + rating, + reviewCount, + isNew = false, + isSale = false, + isWishlisted = false, + category, + colors = [], + sizes = [], +}: ProductCardProps) { + const [currentImage, setCurrentImage] = useState(0); + const [isHovered, setIsHovered] = useState(false); + const [wishlisted, setWishlisted] = useState(isWishlisted); + + const allImages = [image, ...images]; + const discountPercentage = originalPrice + ? Math.round(((originalPrice - price) / originalPrice) * 100) + : 0; + + const handleWishlistToggle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setWishlisted(!wishlisted); + }; + + const handleQuickView = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // TODO: Implement quick view modal + console.log("Quick view for product:", id); + }; + + const handleAddToCart = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // TODO: Implement add to cart + console.log("Add to cart:", id); + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+ {/* Product Image */} +
+
+ {/* Placeholder for actual images */} +
+
+ +
+

{name}

+
+
+ + {/* Badges */} +
+ {isNew && ( + + New + + )} + {isSale && ( + + -{discountPercentage}% + + )} +
+ + {/* Wishlist Button */} + + + {/* Image dots for multiple images */} + {allImages.length > 1 && ( +
+ {allImages.map((_, index) => ( +
+ )} + + {/* Hover overlay with actions */} +
+
+ + +
+
+
+ + {/* Product Info */} + + {/* Category */} +

+ {category} +

+ + {/* Product Name */} +

+ {name} +

+ + {/* Rating */} +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ + ({reviewCount}) + +
+ + {/* Colors */} + {colors.length > 0 && ( +
+ {colors.slice(0, 4).map((color, index) => ( +
+ ))} + {colors.length > 4 && ( + + +{colors.length - 4} + + )} +
+ )} + + {/* Price */} +
+ + ${price.toFixed(2)} + + {originalPrice && ( + + ${originalPrice.toFixed(2)} + + )} +
+ + {/* Sizes */} + {sizes.length > 0 && ( +
+ {sizes.slice(0, 4).map((size) => ( + + {size} + + ))} + {sizes.length > 4 && ( + + +{sizes.length - 4} + + )} +
+ )} + +
+ + + ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..ec51e9c --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/frontend/src/components/ui/navigation-menu.tsx b/frontend/src/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..1199945 --- /dev/null +++ b/frontend/src/components/ui/navigation-menu.tsx @@ -0,0 +1,168 @@ +import * as React from "react" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function NavigationMenu({ + className, + children, + viewport = true, + ...props +}: React.ComponentProps & { + viewport?: boolean +}) { + return ( + + {children} + {viewport && } + + ) +} + +function NavigationMenuList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1" +) + +function NavigationMenuTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + {children}{" "} + + ) +} + +function NavigationMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuViewport({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ +
+ ) +} + +function NavigationMenuLink({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuIndicator({ + className, + ...props +}: React.ComponentProps) { + return ( + +
+ + ) +} + +export { + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, + navigationMenuTriggerStyle, +} diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..5e6778c --- /dev/null +++ b/frontend/src/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000..dcbbc0c --- /dev/null +++ b/frontend/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..67c73e5 --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx new file mode 100644 index 0000000..84649ad --- /dev/null +++ b/frontend/src/components/ui/sheet.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/frontend/src/components/ui/slider.tsx b/frontend/src/components/ui/slider.tsx new file mode 100644 index 0000000..09391e8 --- /dev/null +++ b/frontend/src/components/ui/slider.tsx @@ -0,0 +1,63 @@ +"use client" + +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: React.ComponentProps) { + const _values = React.useMemo( + () => + Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max], + [value, defaultValue, min, max] + ) + + return ( + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + ) +} + +export { Slider } diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx new file mode 100644 index 0000000..957524e --- /dev/null +++ b/frontend/src/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} -- cgit v1.2.3-59-g8ed1b