3 Commits

Author SHA1 Message Date
sean 6aa87ea11d more stuff 2025-06-29 09:20:25 -04:00
sean 5cd7db4339 Merge pull request 'fuck yeah. daisyui integrated' (#1) from daisyui-integrate into main
Reviewed-on: #1
2025-06-29 08:40:55 -04:00
sean 14590afa40 fuck yeah. daisyui integrated 2025-06-29 08:39:26 -04:00
14 changed files with 6360 additions and 876 deletions
+10 -1
View File
@@ -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;
+4228
View File
File diff suppressed because it is too large Load Diff
+45 -4
View File
@@ -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
View File
@@ -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"
+105
View File
@@ -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>
);
}
+109 -686
View File
@@ -5,640 +5,26 @@ import { useSearchParams } from 'next/navigation';
import { Listbox, Transition } from '@headlessui/react';
import { ChevronUpDownIcon, CheckIcon, XMarkIcon } from '@heroicons/react/20/solid';
import SearchInput from '@/components/SearchInput';
// Sample firearm parts data
const parts = [
{
id: '1',
name: 'Faxon 16" Gunner Barrel - 5.56 NATO',
description: 'Lightweight, high-performance AR-15 barrel with 1:8 twist rate',
image_url: 'https://placehold.co/300x200/1f2937/ffffff?text=Barrel',
brand: {
id: 'b1',
name: 'Faxon Firearms',
},
category: {
id: 'c1',
name: 'Barrel',
},
restrictions: [],
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 receiver with M4 feed ramps and T-markings',
image_url: 'https://placehold.co/300x200/374151/ffffff?text=Upper+Receiver',
brand: {
id: 'b2',
name: 'BCM',
},
category: {
id: 'c2',
name: 'Upper Receiver',
},
restrictions: [],
offers: [
{
price: 129.99,
url: 'https://rainierarms.com/bcm-m4-upper',
vendor: {
name: 'Rainier Arms',
},
},
],
},
{
id: '3',
name: 'Aero Precision Lower Receiver',
description: 'Mil-spec forged lower receiver with trigger guard and threaded bolt catch',
image_url: 'https://placehold.co/300x200/4b5563/ffffff?text=Lower+Receiver',
brand: {
id: 'b3',
name: 'Aero Precision',
},
category: {
id: 'c3',
name: 'Lower Receiver',
},
restrictions: ['FFL_REQUIRED'],
offers: [
{
price: 89.99,
url: 'https://aeroprecisionusa.com/lower',
vendor: {
name: 'Aero Precision',
},
},
],
},
{
id: '4',
name: 'Toolcraft BCG - Nitride',
description: 'Mil-spec bolt carrier group with Carpenter 158 bolt and nitride finish',
image_url: 'https://placehold.co/300x200/6b7280/ffffff?text=BCG',
brand: {
id: 'b4',
name: 'Toolcraft',
},
category: {
id: 'c4',
name: 'BCG',
},
restrictions: [],
offers: [
{
price: 149.99,
url: 'https://wctoolcraft.com/bcg-nitride',
vendor: {
name: 'WC Toolcraft',
},
},
],
},
{
id: '5',
name: 'Geissele SSA-E Trigger',
description: 'Two-stage trigger with 3.5lb total pull weight and enhanced reliability',
image_url: 'https://placehold.co/300x200/1e40af/ffffff?text=Trigger',
brand: {
id: 'b5',
name: 'Geissele',
},
category: {
id: 'c5',
name: 'Trigger',
},
restrictions: [],
offers: [
{
price: 249.99,
url: 'https://geissele.com/ssa-e',
vendor: {
name: 'Geissele',
},
},
],
},
{
id: '6',
name: 'Magpul CTR Stock',
description: 'Collapsible stock with friction lock system and QD sling mount',
image_url: 'https://placehold.co/300x200/059669/ffffff?text=Stock',
brand: {
id: 'b6',
name: 'Magpul',
},
category: {
id: 'c6',
name: 'Stock',
},
restrictions: [],
offers: [
{
price: 69.99,
url: 'https://magpul.com/ctr-stock',
vendor: {
name: 'Magpul',
},
},
],
},
{
id: '7',
name: 'Radian Raptor Charging Handle',
description: 'Ambidextrous charging handle with oversized latches and smooth operation',
image_url: 'https://placehold.co/300x200/7c3aed/ffffff?text=Charging+Handle',
brand: {
id: 'b7',
name: 'Radian Weapons',
},
category: {
id: 'c7',
name: 'Charging Handle',
},
restrictions: [],
offers: [
{
price: 89.99,
url: 'https://radianweapons.com/raptor',
vendor: {
name: 'Radian Weapons',
},
},
],
},
{
id: '8',
name: 'BCM Gunfighter Handguard - 15" M-LOK',
description: 'Free-float handguard with M-LOK slots and integrated QD mounts',
image_url: 'https://placehold.co/300x200/dc2626/ffffff?text=Handguard',
brand: {
id: 'b2',
name: 'BCM',
},
category: {
id: 'c8',
name: 'Handguard',
},
restrictions: [],
offers: [
{
price: 199.99,
url: 'https://bravocompanyusa.com/handguard',
vendor: {
name: 'BCM',
},
},
],
},
{
id: '9',
name: 'SureFire WarComp Flash Hider',
description: 'Compensator/flash hider hybrid with suppressor mount capability',
image_url: 'https://placehold.co/300x200/ea580c/ffffff?text=Muzzle+Device',
brand: {
id: 'b8',
name: 'SureFire',
},
category: {
id: 'c9',
name: 'Muzzle Device',
},
restrictions: [],
offers: [
{
price: 159.99,
url: 'https://surefire.com/warcomp',
vendor: {
name: 'SureFire',
},
},
],
},
{
id: '10',
name: 'Aero Precision Gas Block - Low Profile',
description: 'Low-profile adjustable gas block for free-float handguards',
image_url: 'https://placehold.co/300x200/be185d/ffffff?text=Gas+Block',
brand: {
id: 'b3',
name: 'Aero Precision',
},
category: {
id: 'c10',
name: 'Gas Block',
},
restrictions: [],
offers: [
{
price: 49.99,
url: 'https://aeroprecisionusa.com/gas-block',
vendor: {
name: 'Aero Precision',
},
},
],
},
{
id: '11',
name: 'BCM Gas Tube - Mid Length',
description: 'Stainless steel gas tube for mid-length gas systems',
image_url: 'https://placehold.co/300x200/0891b2/ffffff?text=Gas+Tube',
brand: {
id: 'b2',
name: 'BCM',
},
category: {
id: 'c11',
name: 'Gas Tube',
},
restrictions: [],
offers: [
{
price: 19.99,
url: 'https://bravocompanyusa.com/gas-tube',
vendor: {
name: 'BCM',
},
},
],
},
{
id: '12',
name: 'Magpul MOE Pistol Grip',
description: 'Ergonomic pistol grip with storage compartment and enhanced texture',
image_url: 'https://placehold.co/300x200/166534/ffffff?text=Pistol+Grip',
brand: {
id: 'b6',
name: 'Magpul',
},
category: {
id: 'c12',
name: 'Pistol Grip',
},
restrictions: [],
offers: [
{
price: 24.99,
url: 'https://magpul.com/moe-grip',
vendor: {
name: 'Magpul',
},
},
],
},
{
id: '13',
name: 'BCM Buffer Tube - Mil-Spec',
description: 'Mil-spec buffer tube with proper castle nut and end plate',
image_url: 'https://placehold.co/300x200/92400e/ffffff?text=Buffer+Tube',
brand: {
id: 'b2',
name: 'BCM',
},
category: {
id: 'c13',
name: 'Buffer Tube',
},
restrictions: [],
offers: [
{
price: 44.99,
url: 'https://bravocompanyusa.com/buffer-tube',
vendor: {
name: 'BCM',
},
},
],
},
{
id: '14',
name: 'H2 Buffer Weight',
description: 'Heavy buffer weight for improved recoil management',
image_url: 'https://placehold.co/300x200/7c2d12/ffffff?text=Buffer',
brand: {
id: 'b9',
name: 'Spikes Tactical',
},
category: {
id: 'c14',
name: 'Buffer',
},
restrictions: [],
offers: [
{
price: 29.99,
url: 'https://spikestactical.com/h2-buffer',
vendor: {
name: 'Spikes Tactical',
},
},
],
},
{
id: '15',
name: 'Sprinco Blue Buffer Spring',
description: 'Enhanced buffer spring for improved reliability and reduced wear',
image_url: 'https://placehold.co/300x200/1e3a8a/ffffff?text=Buffer+Spring',
brand: {
id: 'b10',
name: 'Sprinco',
},
category: {
id: 'c15',
name: 'Buffer Spring',
},
restrictions: [],
offers: [
{
price: 19.99,
url: 'https://sprinco.com/blue-spring',
vendor: {
name: 'Sprinco',
},
},
],
},
{
id: '16',
name: 'Magpul PMAG 30-Round Magazine',
description: '30-round polymer magazine with anti-tilt follower and dust cover',
image_url: 'https://placehold.co/300x200/065f46/ffffff?text=Magazine',
brand: {
id: 'b6',
name: 'Magpul',
},
category: {
id: 'c16',
name: 'Magazine',
},
restrictions: ['STATE_RESTRICTIONS'],
offers: [
{
price: 14.99,
url: 'https://magpul.com/pmag',
vendor: {
name: 'Magpul',
},
},
],
},
{
id: '17',
name: 'Troy Industries BUIS - Folding',
description: 'Folding backup iron sights with micro-adjustable elevation',
image_url: 'https://placehold.co/300x200/581c87/ffffff?text=Sights',
brand: {
id: 'b11',
name: 'Troy Industries',
},
category: {
id: 'c17',
name: 'Sights',
},
restrictions: [],
offers: [
{
price: 129.99,
url: 'https://troyind.com/buis',
vendor: {
name: 'Troy Industries',
},
},
],
},
{
id: '18',
name: 'Aero Precision Trigger Guard',
description: 'Enhanced trigger guard with oversized opening for gloved hands',
image_url: 'https://placehold.co/300x200/991b1b/ffffff?text=Trigger+Guard',
brand: {
id: 'b3',
name: 'Aero Precision',
},
category: {
id: 'c18',
name: 'Trigger Guard',
},
restrictions: [],
offers: [
{
price: 12.99,
url: 'https://aeroprecisionusa.com/trigger-guard',
vendor: {
name: 'Aero Precision',
},
},
],
},
{
id: '19',
name: 'Larue MBT-2S Trigger',
description: 'Two-stage trigger with 4.5lb total pull weight and excellent value',
image_url: 'https://placehold.co/300x200/0c4a6e/ffffff?text=Trigger',
brand: {
id: 'b12',
name: 'LaRue Tactical',
},
category: {
id: 'c5',
name: 'Trigger',
},
restrictions: [],
offers: [
{
price: 89.99,
url: 'https://laruetactical.com/mbt-2s',
vendor: {
name: 'LaRue Tactical',
},
},
],
},
{
id: '20',
name: 'Daniel Defense 18" Barrel - 5.56 NATO',
description: 'Cold hammer forged barrel with 1:7 twist rate for precision shooting',
image_url: 'https://placehold.co/300x200/1f2937/ffffff?text=Barrel',
brand: {
id: 'b13',
name: 'Daniel Defense',
},
category: {
id: 'c1',
name: 'Barrel',
},
restrictions: [],
offers: [
{
price: 399.99,
url: 'https://danieldefense.com/18-barrel',
vendor: {
name: 'Daniel Defense',
},
},
],
},
{
id: '21',
name: 'BCM Complete Lower Receiver',
description: 'Complete lower receiver with BCM trigger, grip, and stock',
image_url: 'https://placehold.co/300x200/374151/ffffff?text=Lower+Receiver',
brand: {
id: 'b2',
name: 'BCM',
},
category: {
id: 'c3',
name: 'Lower Receiver',
},
restrictions: ['FFL_REQUIRED'],
offers: [
{
price: 449.99,
url: 'https://bravocompanyusa.com/complete-lower',
vendor: {
name: 'BCM',
},
},
],
},
{
id: '22',
name: 'Aimpoint PRO Red Dot Sight',
description: '2 MOA red dot sight with 2x magnification and 40mm objective',
image_url: 'https://placehold.co/300x200/4b5563/ffffff?text=Red+Dot+Sight',
brand: {
id: 'b14',
name: 'Aimpoint',
},
category: {
id: 'c17',
name: 'Sights',
},
restrictions: [],
offers: [
{
price: 449.99,
url: 'https://aimpoint.com/pro',
vendor: {
name: 'Aimpoint',
},
},
],
},
{
id: '23',
name: 'SilencerCo Omega 300 Suppressor',
description: 'Multi-caliber suppressor with quick-detach mount system',
image_url: 'https://placehold.co/300x200/7f1d1d/ffffff?text=Suppressor',
brand: {
id: 'b15',
name: 'SilencerCo',
},
category: {
id: 'c19',
name: 'Suppressor',
},
restrictions: ['NFA', 'SUPPRESSOR', 'SILENCERSHOP_PARTNER'],
offers: [
{
price: 899.99,
url: 'https://silencershop.com/omega-300',
vendor: {
name: 'SilencerShop',
},
},
],
},
{
id: '24',
name: 'Daniel Defense 10.3" SBR Barrel',
description: 'Short barrel rifle barrel with 1:7 twist rate for SBR builds',
image_url: 'https://placehold.co/300x200/1e293b/ffffff?text=SBR+Barrel',
brand: {
id: 'b13',
name: 'Daniel Defense',
},
category: {
id: 'c1',
name: 'Barrel',
},
restrictions: ['NFA', 'SBR', 'FFL_REQUIRED'],
offers: [
{
price: 299.99,
url: 'https://danieldefense.com/10-3-sbr-barrel',
vendor: {
name: 'Daniel Defense',
},
},
],
},
{
id: '25',
name: 'Dead Air Sandman-S Suppressor',
description: 'Rugged 30-caliber suppressor with E-Brake technology',
image_url: 'https://placehold.co/300x200/374151/ffffff?text=Suppressor',
brand: {
id: 'b16',
name: 'Dead Air',
},
category: {
id: 'c19',
name: 'Suppressor',
},
restrictions: ['NFA', 'SUPPRESSOR', 'SILENCERSHOP_PARTNER'],
offers: [
{
price: 799.99,
url: 'https://silencershop.com/sandman-s',
vendor: {
name: 'SilencerShop',
},
},
],
},
{
id: '26',
name: 'Magpul 60-Round Drum Magazine',
description: 'High-capacity drum magazine for AR-15 platform',
image_url: 'https://placehold.co/300x200/065f46/ffffff?text=Drum+Mag',
brand: {
id: 'b6',
name: 'Magpul',
},
category: {
id: 'c16',
name: 'Magazine',
},
restrictions: ['STATE_RESTRICTIONS', 'HIGH_CAPACITY'],
offers: [
{
price: 129.99,
url: 'https://magpul.com/drum-mag',
vendor: {
name: 'Magpul',
},
},
],
}
];
import ProductCard from '@/components/ProductCard';
import RestrictionAlert from '@/components/RestrictionAlert';
import Tooltip from '@/components/Tooltip';
import Link from 'next/link';
import { mockProducts } from '@/mock/product';
import type { Product } from '@/mock/product';
// Extract unique values for dropdowns
const categories = ['All', ...Array.from(new Set(parts.map(part => part.category.name)))];
const brands = ['All', ...Array.from(new Set(parts.map(part => part.brand.name)))];
const vendors = ['All', ...Array.from(new Set(parts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
const restrictions = [...new Set(parts.flatMap(part => part.restrictions))];
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
const brands = ['All', ...Array.from(new Set(mockProducts.map(part => part.brand.name)))];
const vendors = ['All', ...Array.from(new Set(mockProducts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
// Restrictions for filter dropdown
const restrictionOptions = [
'',
'NFA',
'SBR',
'SUPPRESSOR',
'STATE_RESTRICTIONS',
];
type SortField = 'name' | 'category' | 'price';
type SortDirection = 'asc' | 'desc';
@@ -704,46 +90,6 @@ const RestrictionBadge = ({ restriction }: { restriction: string }) => {
);
};
// Product card component
const ProductCard = ({ product }: { product: any }) => {
const [imageError, setImageError] = useState(false);
const lowestPrice = Math.min(...product.offers.map((offer: any) => offer.price));
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 border border-gray-200">
<div 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)}
/>
{product.restrictions && product.restrictions.length > 0 && (
<div className="absolute top-2 left-2 flex flex-wrap gap-1">
{product.restrictions.map((restriction: string) => (
<RestrictionBadge key={restriction} restriction={restriction} />
))}
</div>
)}
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">{product.name}</h3>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{product.description}</p>
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-gray-500">{product.brand.name}</span>
<span className="text-lg font-bold text-gray-900">${lowestPrice.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">{product.category.name}</span>
<button className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
View Details
</button>
</div>
</div>
</div>
);
};
// Tailwind UI Dropdown Component
const Dropdown = ({
label,
@@ -762,16 +108,16 @@ const Dropdown = ({
<div className="relative">
<Listbox value={value} onChange={onChange}>
<div className="relative">
<Listbox.Label className="block text-sm font-medium text-gray-700 mb-1">
<Listbox.Label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{label}
</Listbox.Label>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm">
<span className="block truncate">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-neutral-800 py-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
<span className="block truncate text-neutral-900 dark:text-white">
{value || placeholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
className="h-5 w-5 text-neutral-400"
aria-hidden="true"
/>
</span>
@@ -781,7 +127,7 @@ const Dropdown = ({
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<Listbox.Options>
{options.map((option, optionIdx) => (
@@ -789,7 +135,7 @@ const Dropdown = ({
key={optionIdx}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-blue-100 text-blue-900' : 'text-gray-900'
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-neutral-900 dark:text-white'
}`
}
value={option}
@@ -800,7 +146,7 @@ const Dropdown = ({
{option}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
@@ -826,6 +172,7 @@ export default function Home() {
const [selectedRestriction, setSelectedRestriction] = useState('');
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
// Read category from URL parameter on page load
useEffect(() => {
@@ -836,7 +183,7 @@ export default function Home() {
}, [searchParams]);
// Filter parts based on selected criteria
const filteredParts = parts.filter(part => {
const filteredParts = mockProducts.filter(part => {
const matchesCategory = selectedCategory === 'All' || part.category.name === selectedCategory;
const matchesBrand = selectedBrand === 'All' || part.brand.name === selectedBrand;
const matchesVendor = selectedVendor === 'All' || part.offers.some(offer => offer.vendor.name === selectedVendor);
@@ -844,7 +191,16 @@ export default function Home() {
part.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
part.brand.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesRestriction = !selectedRestriction || part.restrictions.includes(selectedRestriction);
// Restriction filter logic
let matchesRestriction = true;
if (selectedRestriction) {
if (selectedRestriction === 'NFA') matchesRestriction = !!part.restrictions?.nfa;
else if (selectedRestriction === 'SBR') matchesRestriction = !!part.restrictions?.sbr;
else if (selectedRestriction === 'SUPPRESSOR') matchesRestriction = !!part.restrictions?.suppressor;
else if (selectedRestriction === 'STATE_RESTRICTIONS') matchesRestriction = !!(part.restrictions?.stateRestrictions && part.restrictions.stateRestrictions.length > 0);
else matchesRestriction = false;
}
// Price range filtering
let matchesPrice = true;
@@ -918,6 +274,16 @@ export default function Home() {
const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || priceRange || selectedRestriction;
// RestrictionBadge for table view (show NFA/SBR/Suppressor/State)
const getRestrictionFlags = (restrictions?: Product['restrictions']) => {
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;
};
return (
<main className="min-h-screen bg-neutral-50 dark:bg-neutral-900">
{/* Page Title */}
@@ -1055,7 +421,7 @@ export default function Home() {
label="Restriction"
value={selectedRestriction}
onChange={setSelectedRestriction}
options={['', ...restrictions]}
options={restrictionOptions}
placeholder="All restrictions"
/>
@@ -1078,8 +444,53 @@ export default function Home() {
</div>
</div>
{/* Parts Table */}
{/* Parts Display */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* View Toggle and Results Count */}
<div className="flex justify-between items-center mb-6">
<div className="text-sm text-neutral-700 dark:text-neutral-300">
Showing {sortedParts.length} of {mockProducts.length} parts
{hasActiveFilters && (
<span className="ml-2 text-primary-600 dark:text-primary-400">
(filtered)
</span>
)}
</div>
{/* View Toggle */}
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-600 dark:text-neutral-400">View:</span>
<div className="btn-group">
<button
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
onClick={() => setViewMode('table')}
>
Table
</button>
<button
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
onClick={() => setViewMode('cards')}
>
Cards
</button>
</div>
</div>
</div>
{/* Restriction Alert Example */}
{sortedParts.some(part => part.restrictions?.nfa) && (
<div className="mb-6">
<RestrictionAlert
type="warning"
title="NFA Items Detected"
message="Some items in your search require National Firearms Act registration. Please ensure compliance with all federal and state regulations."
icon="🔒"
/>
</div>
)}
{/* Table View */}
{viewMode === 'table' && (
<div className="bg-white dark:bg-neutral-800 shadow-sm rounded-lg overflow-hidden border border-neutral-200 dark:border-neutral-700">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
@@ -1149,9 +560,11 @@ export default function Home() {
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="btn-primary text-xs">
<Link href={`/products/${part.id}`} legacyBehavior>
<a className="btn btn-primary btn-sm">
View Details
</button>
</a>
</Link>
</td>
</tr>
))}
@@ -1163,7 +576,7 @@ export default function Home() {
<div className="bg-neutral-50 dark:bg-neutral-700 px-6 py-3 border-t border-neutral-200 dark:border-neutral-600">
<div className="flex items-center justify-between">
<div className="text-sm text-neutral-700 dark:text-neutral-300">
Showing {sortedParts.length} of {parts.length} parts
Showing {sortedParts.length} of {mockProducts.length} parts
{hasActiveFilters && (
<span className="ml-2 text-primary-600 dark:text-primary-400">
(filtered)
@@ -1176,6 +589,16 @@ export default function Home() {
</div>
</div>
</div>
)}
{/* Card View */}
{viewMode === 'cards' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{sortedParts.map((part) => (
<ProductCard key={part.id} product={part} />
))}
</div>
)}
</div>
{/* Compact Restriction Legend */}
+334
View 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>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
'use client';
import { mockProducts } from '@/mock/products';
import { mockProducts } from '@/mock/product';
export default function ProductsPage() {
return (
+123
View File
@@ -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>
);
}
+43
View File
@@ -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>
);
}
+16
View File
@@ -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>
);
}
+1163 -4
View File
File diff suppressed because it is too large Load Diff
-48
View File
@@ -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
View File
@@ -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",
},
};