ecom / client /src /pages /product-detail.tsx
shashwatIDR's picture
Upload 147 files
b89a86e verified
import { useState } from "react";
import { useParams, useLocation } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useCart } from "@/hooks/use-cart";
import { productsApi } from "@/lib/api";
import Header from "@/components/layout/header";
import CartSidebar from "@/components/cart/cart-sidebar";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "@/components/ui/card";
import { Star, Store, ShoppingCart, Plus, Minus, ArrowLeft } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
export default function ProductDetail() {
const { id } = useParams();
const [, setLocation] = useLocation();
const { user } = useAuth();
const { addItem } = useCart();
const { toast } = useToast();
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const [quantity, setQuantity] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const { data: product, isLoading, error } = useQuery({
queryKey: ['/api/products', id],
queryFn: () => productsApi.getById(id!),
enabled: !!id,
});
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<Skeleton className="aspect-square w-full mb-4" />
<div className="flex space-x-2">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="w-16 h-16" />
))}
</div>
</div>
<div className="space-y-4">
<Skeleton className="h-8 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-6 w-1/4" />
<Skeleton className="h-32 w-full" />
</div>
</div>
</div>
</div>
);
}
if (error || !product) {
return (
<div className="min-h-screen bg-gray-50">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Product Not Found</h1>
<Button onClick={() => setLocation('/')}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Button>
</div>
</div>
</div>
);
}
const handleAddToCart = async () => {
if (!user) {
toast({
variant: "destructive",
title: "Sign in required",
description: "Please sign in to add items to your cart.",
});
setLocation('/auth');
return;
}
try {
await addItem(product.id, quantity);
toast({
title: "Added to cart",
description: `${quantity} ${quantity === 1 ? 'item' : 'items'} added to your cart.`,
});
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to add item to cart.",
});
}
};
const handleBuyNow = async () => {
if (!user) {
toast({
variant: "destructive",
title: "Sign in required",
description: "Please sign in to make a purchase.",
});
setLocation('/auth');
return;
}
await handleAddToCart();
setLocation('/checkout');
};
const discountPercentage = product.originalPrice
? Math.round((1 - parseFloat(product.price) / parseFloat(product.originalPrice)) * 100)
: 0;
const images = product.images && product.images.length > 0 ? product.images : ['/api/placeholder/400/400'];
return (
<div className="min-h-screen bg-gray-50">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<CartSidebar />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-testid="product-detail">
<Button
variant="ghost"
onClick={() => setLocation('/')}
className="mb-6"
data-testid="back-button"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Products
</Button>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Product Images */}
<div>
{/* Main Image */}
<div className="aspect-square bg-gray-100 rounded-lg mb-4 overflow-hidden">
<img
src={images[selectedImageIndex]}
alt={product.title}
className="w-full h-full object-cover"
data-testid="main-product-image"
/>
</div>
{/* Thumbnail Images */}
<div className="flex space-x-2" data-testid="thumbnail-images">
{images.map((image: string, index: number) => (
<button
key={index}
onClick={() => setSelectedImageIndex(index)}
className={`w-16 h-16 rounded cursor-pointer border-2 overflow-hidden ${
selectedImageIndex === index ? 'border-blue-500' : 'border-gray-300'
}`}
data-testid={`thumbnail-${index}`}
>
<img
src={image}
alt={`${product.title} ${index + 1}`}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
</div>
{/* Product Info */}
<div>
{/* Seller Info */}
{product.store && (
<Card className="mb-4 p-3 bg-gray-50">
<CardContent className="p-0">
<div className="flex items-center">
<Store className="text-blue-600 mr-2 h-4 w-4" />
<div>
<span className="font-medium text-gray-900" data-testid="store-name">
{product.store.name}
</span>
<div className="flex items-center mt-1">
<div className="flex text-yellow-400 text-sm">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-3 w-3 fill-current" />
))}
</div>
<span className="text-xs text-gray-500 ml-2">(4.9/5)</span>
</div>
</div>
</div>
</CardContent>
</Card>
)}
<h1 className="text-2xl font-bold text-gray-900 mb-4" data-testid="product-title">
{product.title}
</h1>
{/* Rating and Reviews */}
<div className="flex items-center mb-4">
<div className="flex text-yellow-400">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-4 w-4 fill-current" />
))}
</div>
<span className="text-gray-600 ml-2" data-testid="product-rating">4.9</span>
<span className="text-gray-500 ml-2" data-testid="review-count">(124 reviews)</span>
</div>
{/* Price */}
<div className="mb-6">
<div className="flex items-center space-x-3">
<span className="text-3xl font-bold text-gray-900" data-testid="product-price">
₹{parseFloat(product.price).toFixed(2)}
</span>
{product.originalPrice && (
<span className="text-xl text-gray-500 line-through" data-testid="original-price">
₹{parseFloat(product.originalPrice).toFixed(2)}
</span>
)}
{discountPercentage > 0 && (
<Badge variant="destructive" data-testid="discount-badge">
{discountPercentage}% OFF
</Badge>
)}
</div>
</div>
{/* Quantity Selector */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Quantity</label>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => setQuantity(Math.max(1, quantity - 1))}
disabled={quantity <= 1}
data-testid="decrease-quantity"
>
<Minus className="h-4 w-4" />
</Button>
<span className="w-16 text-center border border-gray-300 rounded py-2" data-testid="quantity-value">
{quantity}
</span>
<Button
variant="outline"
size="icon"
onClick={() => setQuantity(quantity + 1)}
disabled={quantity >= product.stock}
data-testid="increase-quantity"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
{/* Stock Info */}
<div className="mb-6">
{product.stock > 0 ? (
<p className="text-green-600" data-testid="stock-info">
{product.stock} in stock
</p>
) : (
<p className="text-red-600" data-testid="out-of-stock">
Out of stock
</p>
)}
</div>
{/* Action Buttons */}
<div className="flex space-x-4 mb-6">
<Button
className="flex-1 bg-blue-600 hover:bg-blue-700"
onClick={handleAddToCart}
disabled={product.stock === 0}
data-testid="add-to-cart-button"
>
<ShoppingCart className="mr-2 h-4 w-4" />
Add to Cart
</Button>
<Button
variant="outline"
className="flex-1"
onClick={handleBuyNow}
disabled={product.stock === 0}
data-testid="buy-now-button"
>
Buy Now
</Button>
</div>
{/* Product Description */}
<div className="border-t pt-6">
<h3 className="font-semibold text-gray-900 mb-3">Product Description</h3>
<p className="text-gray-600 leading-relaxed" data-testid="product-description">
{product.description}
</p>
</div>
{/* Category */}
{product.category && (
<div className="border-t pt-6 mt-6">
<h3 className="font-semibold text-gray-900 mb-3">Category</h3>
<Badge variant="secondary" data-testid="product-category">
{product.category.name}
</Badge>
</div>
)}
</div>
</div>
</div>
</div>
);
}