Compare commits
3 Commits
ec6a0861f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aa87ea11d | |||
| 5cd7db4339 | |||
| 14590afa40 |
+10
-1
@@ -1,7 +1,16 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'placehold.co',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+45
-4
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"daisyui": "^4.7.3",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
@@ -2346,7 +2347,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -2519,11 +2519,20 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-selector-tokenizer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
|
||||
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"fastparse": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
@@ -2539,6 +2548,34 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/culori": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
|
||||
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "4.7.3",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.7.3.tgz",
|
||||
"integrity": "sha512-R8jUpBMAUm4rSyxzGa9QqFdJTkzREtb1QahXdDoOfElGiF4VbSuu5bfqQoOro1kkSagPy+aTKu5WtSSXmH3u3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-selector-tokenizer": "^0.8",
|
||||
"culori": "^3",
|
||||
"picocolors": "^1",
|
||||
"postcss-js": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/daisyui"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@@ -3411,6 +3448,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastparse": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
||||
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
@@ -5133,7 +5176,6 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5180,7 +5222,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"daisyui": "^4.7.3",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
|
||||
export default function DaisyUIDemo() {
|
||||
return (
|
||||
<main className="min-h-screen bg-base-200 py-8">
|
||||
<div className="max-w-2xl mx-auto space-y-8">
|
||||
<h1 className="text-3xl font-bold text-base-content mb-2">DaisyUI Component Demo</h1>
|
||||
<p className="text-base-content/70 mb-6">Test and validate DaisyUI components and theme integration.</p>
|
||||
|
||||
{/* Alerts */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-2">Alerts</h2>
|
||||
<div className="space-y-2">
|
||||
<div className="alert alert-info">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-info text-info-content w-8 h-8 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span>This is a DaisyUI info alert! 🎉</span>
|
||||
</div>
|
||||
<div className="alert alert-success">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-success text-success-content w-8 h-8 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>DaisyUI is working perfectly with your theme!</span>
|
||||
</div>
|
||||
<div className="alert alert-warning">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-warning text-warning-content w-8 h-8 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>This is a warning alert example</span>
|
||||
</div>
|
||||
<div className="alert alert-error">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-error text-error-content w-8 h-8 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>This is an error alert example</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Buttons */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-2">Buttons</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className="btn btn-primary">Primary</button>
|
||||
<button className="btn btn-secondary">Secondary</button>
|
||||
<button className="btn btn-accent">Accent</button>
|
||||
<button className="btn btn-info">Info</button>
|
||||
<button className="btn btn-success">Success</button>
|
||||
<button className="btn btn-warning">Warning</button>
|
||||
<button className="btn btn-error">Error</button>
|
||||
<button className="btn btn-outline">Outline</button>
|
||||
<button className="btn btn-disabled" disabled>Disabled</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Cards */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-2">Cards</h2>
|
||||
<div className="card w-full bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">DaisyUI Card</h2>
|
||||
<p>This is a sample card using DaisyUI's card component.</p>
|
||||
<div className="card-actions justify-end">
|
||||
<button className="btn btn-primary">Action</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Badges */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-2">Badges</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="badge badge-primary">Primary</span>
|
||||
<span className="badge badge-secondary">Secondary</span>
|
||||
<span className="badge badge-accent">Accent</span>
|
||||
<span className="badge badge-info">Info</span>
|
||||
<span className="badge badge-success">Success</span>
|
||||
<span className="badge badge-warning">Warning</span>
|
||||
<span className="badge badge-error">Error</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tooltip */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-2">Tooltip</h2>
|
||||
<Tooltip content="This is a DaisyUI tooltip!">
|
||||
<button className="btn btn-outline">Hover me</button>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+197
-774
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { mockProducts } from '@/mock/products';
|
||||
import { mockProducts } from '@/mock/product';
|
||||
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Product } from '@/mock/product';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
function getRestrictionFlags(restrictions?: Product['restrictions']): string[] {
|
||||
const flags: string[] = [];
|
||||
if (restrictions?.nfa) flags.push('NFA');
|
||||
if (restrictions?.sbr) flags.push('SBR');
|
||||
if (restrictions?.suppressor) flags.push('SUPPRESSOR');
|
||||
if (restrictions?.stateRestrictions && restrictions.stateRestrictions.length > 0) flags.push('STATE_RESTRICTIONS');
|
||||
return flags;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product }: ProductCardProps) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const lowestPrice = Math.min(...product.offers.map(offer => offer.price));
|
||||
|
||||
// Restriction badge component
|
||||
const RestrictionBadge = ({ restriction }: { restriction: string }) => {
|
||||
const restrictionConfig = {
|
||||
NFA: {
|
||||
label: 'NFA',
|
||||
color: 'badge-error',
|
||||
icon: '🔒',
|
||||
tooltip: 'National Firearms Act - Requires special registration'
|
||||
},
|
||||
SBR: {
|
||||
label: 'SBR',
|
||||
color: 'badge-warning',
|
||||
icon: '📏',
|
||||
tooltip: 'Short Barrel Rifle - Requires NFA registration'
|
||||
},
|
||||
SUPPRESSOR: {
|
||||
label: 'Suppressor',
|
||||
color: 'badge-secondary',
|
||||
icon: '🔇',
|
||||
tooltip: 'Sound Suppressor - Requires NFA registration'
|
||||
},
|
||||
FFL_REQUIRED: {
|
||||
label: 'FFL',
|
||||
color: 'badge-info',
|
||||
icon: '🏪',
|
||||
tooltip: 'Federal Firearms License required for purchase'
|
||||
},
|
||||
STATE_RESTRICTIONS: {
|
||||
label: 'State',
|
||||
color: 'badge-warning',
|
||||
icon: '🗺️',
|
||||
tooltip: 'State-specific restrictions may apply'
|
||||
},
|
||||
HIGH_CAPACITY: {
|
||||
label: 'High Cap',
|
||||
color: 'badge-accent',
|
||||
icon: '🥁',
|
||||
tooltip: 'High capacity magazine - check local laws'
|
||||
},
|
||||
SILENCERSHOP_PARTNER: {
|
||||
label: 'SilencerShop',
|
||||
color: 'badge-success',
|
||||
icon: '🤝',
|
||||
tooltip: 'Available through SilencerShop partnership'
|
||||
}
|
||||
};
|
||||
|
||||
const config = restrictionConfig[restriction as keyof typeof restrictionConfig];
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`badge ${config.color} gap-1 cursor-help`}
|
||||
title={config.tooltip}
|
||||
>
|
||||
<span>{config.icon}</span>
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow duration-300 border border-base-300">
|
||||
<figure className="relative">
|
||||
<img
|
||||
src={imageError ? 'https://placehold.co/300x200/6b7280/ffffff?text=No+Image' : product.image_url}
|
||||
alt={product.name}
|
||||
className="w-full h-48 object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
{getRestrictionFlags(product.restrictions).length > 0 && (
|
||||
<div className="absolute top-2 left-2 flex flex-wrap gap-1">
|
||||
{getRestrictionFlags(product.restrictions).map((restriction) => (
|
||||
<RestrictionBadge key={restriction} restriction={restriction} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</figure>
|
||||
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content line-clamp-2">{product.name}</h3>
|
||||
<p className="text-base-content/70 text-sm line-clamp-2">{product.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-base-content/60">{product.brand.name}</span>
|
||||
<span className="text-lg font-bold text-primary">${lowestPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-base-content/50">{product.category.name}</span>
|
||||
<Link href={`/products/${product.id}`} legacyBehavior>
|
||||
<a className="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
interface RestrictionAlertProps {
|
||||
type: 'warning' | 'error' | 'info' | 'success';
|
||||
title: string;
|
||||
message: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export default function RestrictionAlert({ type, title, message, icon }: RestrictionAlertProps) {
|
||||
const alertConfig = {
|
||||
warning: {
|
||||
className: 'alert alert-warning',
|
||||
icon: icon || '⚠️'
|
||||
},
|
||||
error: {
|
||||
className: 'alert alert-error',
|
||||
icon: icon || '🚫'
|
||||
},
|
||||
info: {
|
||||
className: 'alert alert-info',
|
||||
icon: icon || 'ℹ️'
|
||||
},
|
||||
success: {
|
||||
className: 'alert alert-success',
|
||||
icon: icon || '✅'
|
||||
}
|
||||
};
|
||||
|
||||
const config = alertConfig[type];
|
||||
|
||||
return (
|
||||
<div className={config.className}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{config.icon}</span>
|
||||
<div>
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<div className="text-sm">{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
interface TooltipProps {
|
||||
content: string;
|
||||
children: React.ReactNode;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Tooltip({ content, children, position = 'top', className = '' }: TooltipProps) {
|
||||
return (
|
||||
<div className={`tooltip tooltip-${position} ${className}`} data-tip={content}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1205
-46
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
export const mockProducts = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Faxon 16" Gunner Barrel - 5.56 NATO',
|
||||
description: 'Lightweight, high-performance AR-15 barrel.',
|
||||
image_url: 'https://via.placeholder.com/300x200?text=Barrel',
|
||||
brand: {
|
||||
id: 'b1',
|
||||
name: 'Faxon Firearms',
|
||||
},
|
||||
category: {
|
||||
id: 'c1',
|
||||
name: 'Barrel',
|
||||
},
|
||||
offers: [
|
||||
{
|
||||
price: 189.99,
|
||||
url: 'https://primaryarms.com/faxon-16-gunner-barrel',
|
||||
vendor: {
|
||||
name: 'Primary Arms',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'BCM M4 Upper Receiver',
|
||||
description: 'Forged upper with M4 feed ramps.',
|
||||
image_url: 'https://via.placeholder.com/300x200?text=Upper',
|
||||
brand: {
|
||||
id: 'b2',
|
||||
name: 'BCM',
|
||||
},
|
||||
category: {
|
||||
id: 'c2',
|
||||
name: 'Upper Receiver',
|
||||
},
|
||||
offers: [
|
||||
{
|
||||
price: 129.99,
|
||||
url: 'https://rainierarms.com/bcm-m4-upper',
|
||||
vendor: {
|
||||
name: 'Rainier Arms',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
+51
-1
@@ -1,3 +1,4 @@
|
||||
console.log('DaisyUI plugin loaded');
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
@@ -100,5 +101,54 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
"primary": "#0ea5e9",
|
||||
"primary-content": "#ffffff",
|
||||
"secondary": "#ef4444",
|
||||
"secondary-content": "#ffffff",
|
||||
"accent": "#f59e0b",
|
||||
"accent-content": "#ffffff",
|
||||
"neutral": "#737373",
|
||||
"neutral-content": "#ffffff",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f5f5f5",
|
||||
"base-300": "#e5e5e5",
|
||||
"base-content": "#171717",
|
||||
"info": "#0ea5e9",
|
||||
"success": "#22c55e",
|
||||
"warning": "#f59e0b",
|
||||
"error": "#ef4444",
|
||||
},
|
||||
dark: {
|
||||
"primary": "#38bdf8",
|
||||
"primary-content": "#ffffff",
|
||||
"secondary": "#f87171",
|
||||
"secondary-content": "#ffffff",
|
||||
"accent": "#fbbf24",
|
||||
"accent-content": "#ffffff",
|
||||
"neutral": "#a3a3a3",
|
||||
"neutral-content": "#ffffff",
|
||||
"base-100": "#171717",
|
||||
"base-200": "#262626",
|
||||
"base-300": "#404040",
|
||||
"base-content": "#ffffff",
|
||||
"info": "#38bdf8",
|
||||
"success": "#4ade80",
|
||||
"warning": "#fbbf24",
|
||||
"error": "#f87171",
|
||||
},
|
||||
},
|
||||
],
|
||||
darkTheme: "dark",
|
||||
base: true,
|
||||
styled: true,
|
||||
utils: true,
|
||||
prefix: "",
|
||||
logs: true,
|
||||
themeRoot: ":root",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user