Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions src/app/store/[slug]/components/product-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
"use client";

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Star, User } from "lucide-react";
import { cn } from "@/lib/utils";

// Mock reviews data for demonstration (can be replaced with actual data from API)
const MOCK_REVIEWS = [
{
id: "1",
author: "John Doe",
rating: 5,
date: "2024-03-15",
comment: "Excellent product! Highly recommended.",
verified: true,
},
{
id: "2",
author: "Jane Smith",
rating: 4,
date: "2024-03-10",
comment: "Good quality, fast delivery.",
verified: true,
},
];

interface Review {
id: string;
author: string;
rating: number;
date: string;
comment: string;
verified: boolean;
}

interface ProductTabsProps {
description?: string | null;
specifications?: Record<string, string>;
sku?: string;
barcode?: string | null;
weight?: number | null;
dimensions?: {
length?: number | null;
width?: number | null;
height?: number | null;
};
reviews?: Review[];
averageRating?: number;
className?: string;
}

/**
* Product tabs component for displaying Description, Specifications, and Reviews
* Used on product detail pages to organize product information
*/
export function ProductTabs({
description,
specifications = {},
sku,
barcode,
weight,
dimensions,
reviews = MOCK_REVIEWS,
averageRating,
className,
}: ProductTabsProps) {
// Calculate average rating from reviews if not provided
const calculatedRating = averageRating ?? (
reviews.length > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
: 0
);

const hasSpecs = Object.keys(specifications).length > 0 || sku || barcode || weight ||
(dimensions?.length && dimensions?.width && dimensions?.height);

return (
<Tabs defaultValue="description" className={cn("w-full", className)}>
<TabsList className="w-full justify-start border-b rounded-none h-auto p-0 bg-transparent">
<TabsTrigger
value="description"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
>
Description
</TabsTrigger>
{hasSpecs && (
<TabsTrigger
value="specifications"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
>
Specifications
</TabsTrigger>
)}
<TabsTrigger
value="reviews"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
>
Reviews ({reviews.length})
</TabsTrigger>
</TabsList>

{/* Description Tab */}
<TabsContent value="description" className="mt-6">
<div className="prose prose-sm max-w-none dark:prose-invert">
{description ? (
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
{description}
</p>
) : (
<p className="text-muted-foreground">
No description available for this product.
</p>
)}
</div>
</TabsContent>

{/* Specifications Tab */}
{hasSpecs && (
<TabsContent value="specifications" className="mt-6">
<div className="grid gap-3">
{sku && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">SKU</span>
<span className="col-span-2 text-muted-foreground">{sku}</span>
</div>
)}
{barcode && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Barcode</span>
<span className="col-span-2 text-muted-foreground">{barcode}</span>
</div>
)}
{weight && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Weight</span>
<span className="col-span-2 text-muted-foreground">{weight} kg</span>
</div>
)}
{dimensions?.length && dimensions?.width && dimensions?.height && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Dimensions</span>
<span className="col-span-2 text-muted-foreground">
{dimensions.length} × {dimensions.width} × {dimensions.height} cm
</span>
</div>
)}
{Object.entries(specifications).map(([key, value]) => (
<div key={key} className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">{key}</span>
<span className="col-span-2 text-muted-foreground">{value}</span>
</div>
))}
</div>
</TabsContent>
)}

{/* Reviews Tab */}
<TabsContent value="reviews" className="mt-6">
<div className="space-y-6">
{/* Reviews Summary */}
<div className="flex items-start justify-between pb-6 border-b">
<div>
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={cn(
"h-5 w-5",
star <= calculatedRating
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
)}
/>
))}
</div>
<span className="text-2xl font-bold">
{calculatedRating > 0 ? calculatedRating.toFixed(1) : "N/A"}
</span>
</div>
<p className="text-sm text-muted-foreground">
Based on {reviews.length} {reviews.length === 1 ? "review" : "reviews"}
</p>
</div>
<Button variant="outline">Write a Review</Button>
</div>

{/* Reviews List */}
<div className="space-y-4">
{reviews.length > 0 ? (
reviews.map((review) => (
<Card key={review.id}>
<CardContent className="pt-6">
<div className="space-y-3">
{/* Reviewer Info */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center">
<User className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<p className="font-medium flex items-center gap-2">
{review.author}
{review.verified && (
<span className="text-xs text-green-600 bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded">
Verified Purchase
</span>
)}
</p>
<p className="text-xs text-muted-foreground">
{new Date(review.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</div>
</div>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={cn(
"h-4 w-4",
star <= review.rating
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
)}
/>
))}
</div>
</div>

{/* Review Comment */}
<p className="text-sm text-muted-foreground">
{review.comment}
</p>
</div>
</CardContent>
</Card>
))
) : (
<div className="text-center py-12 text-muted-foreground">
<p>No reviews yet. Be the first to review this product!</p>
</div>
)}
</div>

{/* Load More */}
<div className="flex justify-center pt-4">
<Button variant="outline">Load More Reviews</Button>
</div>
</div>
</TabsContent>
</Tabs>
);
}
72 changes: 12 additions & 60 deletions src/app/store/[slug]/products/[productSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { StockBadge } from "../../components/stock-badge";
import { VariantSelector } from "../../components/variant-selector";
import { AddToCartButton } from "../../components/add-to-cart-button";
import { ProductGrid } from "../../components/product-grid";
import { ProductTabs } from "../../components/product-tabs";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Package, TruckIcon, RefreshCw, ShieldCheck } from "lucide-react";

interface StoreProductPageProps {
Expand Down Expand Up @@ -297,65 +297,17 @@ export default async function StoreProductPage({ params }: StoreProductPageProps

{/* Product Details Tabs */}
<div className="mb-16">
<Tabs defaultValue="description" className="w-full">
<TabsList className="w-full justify-start border-b rounded-none h-auto p-0 bg-transparent">
<TabsTrigger
value="description"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
>
Description
</TabsTrigger>
<TabsTrigger
value="specs"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
>
Specifications
</TabsTrigger>
</TabsList>

<TabsContent value="description" className="mt-6">
<div className="prose prose-sm max-w-none dark:prose-invert">
{product.description ? (
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
{product.description}
</p>
) : (
<p className="text-muted-foreground">
No description available for this product.
</p>
)}
</div>
</TabsContent>

<TabsContent value="specs" className="mt-6">
<div className="grid gap-3">
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">SKU</span>
<span className="col-span-2 text-muted-foreground">{product.sku}</span>
</div>
{product.barcode && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Barcode</span>
<span className="col-span-2 text-muted-foreground">{product.barcode}</span>
</div>
)}
{product.weight && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Weight</span>
<span className="col-span-2 text-muted-foreground">{product.weight} kg</span>
</div>
)}
{(product.length && product.width && product.height) && (
<div className="grid grid-cols-3 gap-4 py-3 border-b">
<span className="font-medium">Dimensions</span>
<span className="col-span-2 text-muted-foreground">
{product.length} × {product.width} × {product.height} cm
</span>
</div>
)}
</div>
</TabsContent>
</Tabs>
<ProductTabs
description={product.description}
sku={product.sku}
barcode={product.barcode}
weight={product.weight}
dimensions={{
length: product.length,
width: product.width,
height: product.height,
}}
/>
</div>

{/* Related Products */}
Expand Down
Loading