mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 11:06:46 -05:00
more stuff
This commit is contained in:
334
src/app/products/[id]/page.tsx
Normal file
334
src/app/products/[id]/page.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { mockProducts } from '@/mock/product';
|
||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||
import { StarIcon } from '@heroicons/react/20/solid';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function ProductDetailsPage() {
|
||||
const params = useParams();
|
||||
const productId = params.id as string;
|
||||
|
||||
const product = mockProducts.find(p => p.id === productId);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
const [selectedOffer, setSelectedOffer] = useState(0);
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="alert alert-error">
|
||||
<span>Product not found</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Use images array if present, otherwise fallback to image_url
|
||||
const allImages = product.images && product.images.length > 0
|
||||
? product.images
|
||||
: [product.image_url];
|
||||
const lowestPrice = Math.min(...product.offers.map(o => o.price));
|
||||
const highestPrice = Math.max(...product.offers.map(o => o.price));
|
||||
const averageRating = product.reviews
|
||||
? product.reviews.reduce((acc, review) => acc + review.rating, 0) / product.reviews.length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Breadcrumb */}
|
||||
<div className="text-sm breadcrumbs mb-6">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href={`/products?category=${product.category.id}`}>{product.category.name}</a></li>
|
||||
<li>{product.name}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Restriction Alert */}
|
||||
{(product.restrictions?.nfa || product.restrictions?.sbr || product.restrictions?.suppressor) && (
|
||||
<RestrictionAlert
|
||||
type="warning"
|
||||
title="Restricted Item"
|
||||
message="This item may have federal or state restrictions. Please ensure compliance with all applicable laws and regulations."
|
||||
icon="🔒"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Product Images */}
|
||||
<div className="space-y-4">
|
||||
<div className="aspect-square bg-neutral-100 dark:bg-neutral-800 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={allImages[selectedImageIndex]}
|
||||
alt={product.name}
|
||||
width={600}
|
||||
height={600}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{allImages.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{allImages.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImageIndex(index)}
|
||||
className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 ${
|
||||
selectedImageIndex === index
|
||||
? 'border-primary-500'
|
||||
: 'border-neutral-200 dark:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={image}
|
||||
alt={`${product.name} view ${index + 1}`}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="space-y-6">
|
||||
{/* Brand & Category */}
|
||||
<div className="flex items-center gap-4">
|
||||
{product.brand.logo && (
|
||||
<Image
|
||||
src={product.brand.logo}
|
||||
alt={product.brand.name}
|
||||
width={100}
|
||||
height={50}
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{product.brand.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{product.category.icon}</span>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{product.category.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Name */}
|
||||
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
{/* Price Range */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-3xl font-bold text-primary-600">
|
||||
${lowestPrice.toFixed(2)}
|
||||
</div>
|
||||
{lowestPrice !== highestPrice && (
|
||||
<div className="text-lg text-neutral-600 dark:text-neutral-400">
|
||||
- ${highestPrice.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-neutral-500">
|
||||
from {product.offers.length} vendor{product.offers.length > 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews */}
|
||||
{product.reviews && product.reviews.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<StarIcon
|
||||
key={star}
|
||||
className={`h-5 w-5 ${
|
||||
star <= averageRating
|
||||
? 'text-yellow-400'
|
||||
: 'text-neutral-300 dark:text-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{averageRating.toFixed(1)} ({product.reviews.length} reviews)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Description</h3>
|
||||
<p className="text-neutral-700 dark:text-neutral-300">
|
||||
{product.longDescription || product.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Add to Build Button */}
|
||||
<div className="flex gap-4">
|
||||
<button className="btn btn-primary flex-1">
|
||||
Add to Current Build
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
Save for Later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Specifications */}
|
||||
{product.specifications && (
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold mb-6">Specifications</h2>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{product.specifications.weight && (
|
||||
<div>
|
||||
<span className="font-semibold">Weight:</span> {product.specifications.weight}
|
||||
</div>
|
||||
)}
|
||||
{product.specifications.length && (
|
||||
<div>
|
||||
<span className="font-semibold">Length:</span> {product.specifications.length}
|
||||
</div>
|
||||
)}
|
||||
{product.specifications.material && (
|
||||
<div>
|
||||
<span className="font-semibold">Material:</span> {product.specifications.material}
|
||||
</div>
|
||||
)}
|
||||
{product.specifications.finish && (
|
||||
<div>
|
||||
<span className="font-semibold">Finish:</span> {product.specifications.finish}
|
||||
</div>
|
||||
)}
|
||||
{product.specifications.caliber && (
|
||||
<div>
|
||||
<span className="font-semibold">Caliber:</span> {product.specifications.caliber}
|
||||
</div>
|
||||
)}
|
||||
{product.specifications.compatibility && (
|
||||
<div className="md:col-span-2">
|
||||
<span className="font-semibold">Compatibility:</span>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{product.specifications.compatibility.map((comp, index) => (
|
||||
<span key={index} className="badge badge-outline">{comp}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vendor Offers */}
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold mb-6">Where to Buy</h2>
|
||||
<div className="space-y-4">
|
||||
{product.offers.map((offer, index) => (
|
||||
<div key={index} className="card">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{offer.vendor.logo && (
|
||||
<Image
|
||||
src={offer.vendor.logo}
|
||||
alt={offer.vendor.name}
|
||||
width={80}
|
||||
height={40}
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-semibold">{offer.vendor.name}</div>
|
||||
{offer.shipping && (
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{offer.shipping}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-primary-600">
|
||||
${offer.price.toFixed(2)}
|
||||
</div>
|
||||
{offer.inStock !== undefined && (
|
||||
<div className={`text-sm ${offer.inStock ? 'text-success' : 'text-error'}`}>
|
||||
{offer.inStock ? 'In Stock' : 'Out of Stock'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={offer.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
View Deal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews */}
|
||||
{product.reviews && product.reviews.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
|
||||
<div className="space-y-4">
|
||||
{product.reviews.map((review) => (
|
||||
<div key={review.id} className="card">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<StarIcon
|
||||
key={star}
|
||||
className={`h-4 w-4 ${
|
||||
star <= review.rating
|
||||
? 'text-yellow-400'
|
||||
: 'text-neutral-300 dark:text-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{new Date(review.date).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-semibold mb-1">{review.user}</div>
|
||||
<p className="text-neutral-700 dark:text-neutral-300">{review.comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Compatibility */}
|
||||
{product.compatibility && product.compatibility.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold mb-6">Compatible Parts</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.compatibility.map((part, index) => (
|
||||
<span key={index} className="badge badge-primary">{part}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user