diff options
Diffstat (limited to 'frontend/src/components/product-page.tsx')
-rw-r--r-- | frontend/src/components/product-page.tsx | 567 |
1 files changed, 567 insertions, 0 deletions
diff --git a/frontend/src/components/product-page.tsx b/frontend/src/components/product-page.tsx new file mode 100644 index 0000000..aff9d53 --- /dev/null +++ b/frontend/src/components/product-page.tsx @@ -0,0 +1,567 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { + Heart, + Share2, + ShoppingBag, + Star, + Truck, + RefreshCw, + Shield, + Ruler, + ChevronLeft, + ChevronRight, + Minus, + Plus, + Check, +} from "lucide-react"; + +interface ProductPageProps { + productId?: string; +} + +export function ProductPage({ productId = "1" }: ProductPageProps) { + const [selectedImage, setSelectedImage] = useState(0); + const [selectedSize, setSelectedSize] = useState(""); + const [selectedColor, setSelectedColor] = useState(""); + const [quantity, setQuantity] = useState(1); + const [isWishlisted, setIsWishlisted] = useState(false); + + // Mock product data - in real app, this would come from an API + const product = { + id: productId, + name: "Oversized Cotton Hoodie", + brand: "blcklst", + price: 89, + originalPrice: 129, + rating: 4.5, + reviewCount: 234, + description: "A premium oversized cotton hoodie crafted for ultimate comfort and style. Made from 100% organic cotton with a soft fleece lining, this hoodie features a relaxed fit perfect for layering or wearing solo.", + features: [ + "100% organic cotton construction", + "Soft fleece interior lining", + "Kangaroo pocket with hidden phone compartment", + "Reinforced double-stitched seams", + "Pre-shrunk for lasting fit", + "Unisex design" + ], + images: [ + "/api/placeholder/600/800", + "/api/placeholder/600/800", + "/api/placeholder/600/800", + "/api/placeholder/600/800", + ], + colors: [ + { name: "Black", value: "#000000", available: true }, + { name: "White", value: "#FFFFFF", available: true }, + { name: "Gray", value: "#6B7280", available: true }, + { name: "Navy", value: "#1E3A8A", available: false }, + ], + sizes: [ + { name: "XS", available: true }, + { name: "S", available: true }, + { name: "M", available: true }, + { name: "L", available: true }, + { name: "XL", available: true }, + { name: "XXL", available: false }, + ], + inStock: true, + stockCount: 12, + }; + + const relatedProducts = [ + { + id: "2", + name: "Minimal T-Shirt", + price: 45, + originalPrice: 65, + image: "/api/placeholder/300/400", + rating: 4.3, + }, + { + id: "3", + name: "Denim Jacket", + price: 129, + originalPrice: 179, + image: "/api/placeholder/300/400", + rating: 4.7, + }, + { + id: "4", + name: "Cargo Pants", + price: 89, + originalPrice: 119, + image: "/api/placeholder/300/400", + rating: 4.2, + }, + { + id: "5", + name: "Sneakers", + price: 149, + originalPrice: 199, + image: "/api/placeholder/300/400", + rating: 4.6, + }, + ]; + + const reviews = [ + { + id: 1, + name: "Sarah M.", + rating: 5, + date: "2024-01-15", + comment: "Amazing quality! The fit is perfect and the material feels premium. Definitely worth the price.", + verified: true, + }, + { + id: 2, + name: "Alex K.", + rating: 4, + date: "2024-01-10", + comment: "Great hoodie, very comfortable. Only wish it came in more colors.", + verified: true, + }, + { + id: 3, + name: "Jordan L.", + rating: 5, + date: "2024-01-05", + comment: "Best hoodie I've ever owned. The oversized fit is exactly what I wanted.", + verified: true, + }, + ]; + + const nextImage = () => { + setSelectedImage((prev) => (prev + 1) % product.images.length); + }; + + const prevImage = () => { + setSelectedImage((prev) => (prev - 1 + product.images.length) % product.images.length); + }; + + const addToCart = () => { + if (!selectedSize || !selectedColor) { + alert("Please select size and color"); + return; + } + // Add to cart logic here + console.log("Added to cart:", { product, selectedSize, selectedColor, quantity }); + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + <Star + key={i} + className={`h-4 w-4 ${ + i < Math.floor(rating) + ? "fill-yellow-400 text-yellow-400" + : i < rating + ? "fill-yellow-400/50 text-yellow-400" + : "text-gray-300" + }`} + /> + )); + }; + + return ( + <div className="min-h-screen bg-background"> + {/* Breadcrumb */} + <div className="border-b"> + <div className="container mx-auto px-4 py-4"> + <nav className="flex items-center space-x-2 text-sm text-muted-foreground"> + <Link href="/" className="hover:text-foreground">Home</Link> + <span>/</span> + <Link href="/men" className="hover:text-foreground">Men</Link> + <span>/</span> + <Link href="/men/hoodies" className="hover:text-foreground">Hoodies</Link> + <span>/</span> + <span className="text-foreground">{product.name}</span> + </nav> + </div> + </div> + + <div className="container mx-auto px-4 py-8"> + <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12"> + {/* Product Images */} + <div className="space-y-4"> + {/* Main Image */} + <div className="relative aspect-[3/4] overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-800"> + <Image + src={product.images[selectedImage]} + alt={product.name} + fill + className="object-cover" + priority + /> + {product.originalPrice && ( + <Badge className="absolute top-4 left-4 bg-red-500 hover:bg-red-600"> + {Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF + </Badge> + )} + + {/* Navigation Arrows */} + <button + onClick={prevImage} + className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/80 dark:bg-black/80 p-2 rounded-full hover:bg-white dark:hover:bg-black transition-colors" + > + <ChevronLeft className="h-5 w-5" /> + </button> + <button + onClick={nextImage} + className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/80 dark:bg-black/80 p-2 rounded-full hover:bg-white dark:hover:bg-black transition-colors" + > + <ChevronRight className="h-5 w-5" /> + </button> + </div> + + {/* Thumbnail Images */} + <div className="grid grid-cols-4 gap-3"> + {product.images.map((image, index) => ( + <button + key={index} + onClick={() => setSelectedImage(index)} + className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${ + selectedImage === index + ? "border-primary" + : "border-transparent hover:border-neutral-300" + }`} + > + <Image + src={image} + alt={`${product.name} ${index + 1}`} + fill + className="object-cover" + /> + </button> + ))} + </div> + </div> + + {/* Product Details */} + <div className="space-y-6"> + {/* Header */} + <div> + <p className="text-sm text-muted-foreground font-medium mb-2">{product.brand}</p> + <h1 className="text-3xl font-bold mb-4">{product.name}</h1> + + {/* Rating */} + <div className="flex items-center space-x-2 mb-4"> + <div className="flex items-center space-x-1"> + {renderStars(product.rating)} + </div> + <span className="text-sm text-muted-foreground"> + {product.rating} ({product.reviewCount} reviews) + </span> + </div> + + {/* Price */} + <div className="flex items-center space-x-3 mb-6"> + <span className="text-3xl font-bold">${product.price}</span> + {product.originalPrice && ( + <span className="text-xl text-muted-foreground line-through"> + ${product.originalPrice} + </span> + )} + </div> + </div> + + {/* Color Selection */} + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <h3 className="font-medium">Color</h3> + {selectedColor && ( + <span className="text-sm text-muted-foreground capitalize">{selectedColor}</span> + )} + </div> + <div className="flex space-x-3"> + {product.colors.map((color) => ( + <button + key={color.name} + onClick={() => color.available && setSelectedColor(color.name)} + disabled={!color.available} + className={`relative w-10 h-10 rounded-full border-2 transition-all ${ + selectedColor === color.name + ? "border-primary scale-110" + : "border-neutral-300 hover:border-neutral-400" + } ${!color.available && "opacity-50 cursor-not-allowed"}`} + style={{ backgroundColor: color.value }} + > + {selectedColor === color.name && ( + <Check className="absolute inset-0 m-auto h-4 w-4 text-white" /> + )} + {!color.available && ( + <div className="absolute inset-0 bg-neutral-500/50 rounded-full" /> + )} + </button> + ))} + </div> + </div> + + {/* Size Selection */} + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <h3 className="font-medium">Size</h3> + <Button variant="link" className="h-auto p-0 text-sm"> + <Ruler className="h-4 w-4 mr-1" /> + Size Guide + </Button> + </div> + <div className="grid grid-cols-3 gap-3"> + {product.sizes.map((size) => ( + <button + key={size.name} + onClick={() => size.available && setSelectedSize(size.name)} + disabled={!size.available} + className={`py-3 px-4 border rounded-lg text-sm font-medium transition-colors ${ + selectedSize === size.name + ? "border-primary bg-primary text-primary-foreground" + : size.available + ? "border-neutral-300 hover:border-neutral-400 hover:bg-white dark:hover:bg-white hover:text-black" + : "border-neutral-200 text-muted-foreground opacity-50 cursor-not-allowed" + }`} + > + {size.name} + </button> + ))} + </div> + </div> + + {/* Quantity */} + <div className="space-y-3"> + <h3 className="font-medium">Quantity</h3> + <div className="flex items-center space-x-3"> + <div className="flex items-center border rounded-lg"> + <button + onClick={() => setQuantity(Math.max(1, quantity - 1))} + className="p-3 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors focus:outline-none focus:ring-0 m-1" + > + <Minus className="h-4 w-4" /> + </button> + <span className="px-4 py-3 min-w-[60px] text-center">{quantity}</span> + <button + onClick={() => setQuantity(Math.min(product.stockCount, quantity + 1))} + className="p-3 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors focus:outline-none focus:ring-0 m-1" + > + <Plus className="h-4 w-4" /> + </button> + </div> + <span className="text-sm text-muted-foreground"> + {product.stockCount} items left + </span> + </div> + </div> + + {/* Add to Cart */} + <div className="space-y-3"> + <div className="flex space-x-3"> + <Button + onClick={addToCart} + className="flex-1 h-12" + disabled={!product.inStock} + > + <ShoppingBag className="h-5 w-5 mr-2" /> + {product.inStock ? "Add to Cart" : "Out of Stock"} + </Button> + <Button + variant="outline" + size="icon" + className="h-12 w-12" + onClick={() => setIsWishlisted(!isWishlisted)} + > + <Heart className={`h-5 w-5 ${isWishlisted ? "fill-red-500 text-red-500" : ""}`} /> + </Button> + <Button variant="outline" size="icon" className="h-12 w-12"> + <Share2 className="h-5 w-5" /> + </Button> + </div> + + {/* Features */} + <div className="grid grid-cols-3 gap-4 pt-4"> + <div className="flex items-center space-x-2 text-sm"> + <Truck className="h-4 w-4 text-muted-foreground" /> + <span>Free Shipping</span> + </div> + <div className="flex items-center space-x-2 text-sm"> + <RefreshCw className="h-4 w-4 text-muted-foreground" /> + <span>Free Returns</span> + </div> + <div className="flex items-center space-x-2 text-sm"> + <Shield className="h-4 w-4 text-muted-foreground" /> + <span>2 Year Warranty</span> + </div> + </div> + </div> + </div> + </div> + + {/* Product Details Tabs */} + <div className="mt-16"> + <Tabs defaultValue="description" className="w-full"> + <TabsList className="grid w-full grid-cols-3"> + <TabsTrigger value="description">Description</TabsTrigger> + <TabsTrigger value="specifications">Specifications</TabsTrigger> + <TabsTrigger value="reviews">Reviews ({product.reviewCount})</TabsTrigger> + </TabsList> + + <TabsContent value="description" className="mt-8"> + <div className="prose dark:prose-invert max-w-none"> + <p className="text-lg text-muted-foreground mb-6">{product.description}</p> + <h3 className="text-xl font-semibold mb-4">Features</h3> + <ul className="space-y-2"> + {product.features.map((feature, index) => ( + <li key={index} className="flex items-start space-x-2"> + <Check className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" /> + <span>{feature}</span> + </li> + ))} + </ul> + </div> + </TabsContent> + + <TabsContent value="specifications" className="mt-8"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <div className="space-y-4"> + <h3 className="text-lg font-semibold">Material & Care</h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-muted-foreground">Material:</span> + <span>100% Organic Cotton</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Weight:</span> + <span>400 GSM</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Care:</span> + <span>Machine Wash Cold</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Origin:</span> + <span>Made in Portugal</span> + </div> + </div> + </div> + <div className="space-y-4"> + <h3 className="text-lg font-semibold">Sizing</h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-muted-foreground">Fit:</span> + <span>Oversized</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Model Height:</span> + <span>6'0" / 183cm</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Model Size:</span> + <span>Size M</span> + </div> + </div> + </div> + </div> + </TabsContent> + + <TabsContent value="reviews" className="mt-8"> + <div className="space-y-6"> + <div className="flex items-center justify-between"> + <div> + <div className="flex items-center space-x-2 mb-2"> + <div className="flex items-center space-x-1"> + {renderStars(product.rating)} + </div> + <span className="text-2xl font-bold">{product.rating}</span> + <span className="text-muted-foreground">({product.reviewCount} reviews)</span> + </div> + <p className="text-sm text-muted-foreground">Based on verified purchases</p> + </div> + <Button variant="outline">Write a Review</Button> + </div> + + <Separator /> + + <div className="space-y-6"> + {reviews.map((review) => ( + <div key={review.id} className="space-y-3"> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-3"> + <div> + <div className="flex items-center space-x-2"> + <span className="font-medium">{review.name}</span> + {review.verified && ( + <Badge variant="secondary" className="text-xs"> + Verified Purchase + </Badge> + )} + </div> + <div className="flex items-center space-x-2 mt-1"> + <div className="flex items-center space-x-1"> + {renderStars(review.rating)} + </div> + <span className="text-sm text-muted-foreground">{review.date}</span> + </div> + </div> + </div> + </div> + <p className="text-muted-foreground">{review.comment}</p> + <Separator /> + </div> + ))} + </div> + </div> + </TabsContent> + </Tabs> + </div> + + {/* Related Products */} + <div className="mt-16"> + <h2 className="text-2xl font-bold mb-8">You might also like</h2> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> + {relatedProducts.map((item) => ( + <Link key={item.id} href={`/products/${item.id}`} className="group"> + <div className="space-y-3"> + <div className="relative aspect-[3/4] overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-800"> + <Image + src={item.image} + alt={item.name} + fill + className="object-cover group-hover:scale-105 transition-transform duration-300" + /> + {item.originalPrice && ( + <Badge className="absolute top-3 left-3 bg-red-500 hover:bg-red-600"> + {Math.round(((item.originalPrice - item.price) / item.originalPrice) * 100)}% OFF + </Badge> + )} + </div> + <div> + <h3 className="font-medium group-hover:text-primary transition-colors"> + {item.name} + </h3> + <div className="flex items-center space-x-1 mt-1"> + {renderStars(item.rating)} + <span className="text-sm text-muted-foreground">({item.rating})</span> + </div> + <div className="flex items-center space-x-2 mt-2"> + <span className="font-semibold">${item.price}</span> + {item.originalPrice && ( + <span className="text-sm text-muted-foreground line-through"> + ${item.originalPrice} + </span> + )} + </div> + </div> + </div> + </Link> + ))} + </div> + </div> + </div> + </div> + ); +}
\ No newline at end of file |