mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-05 18:46:45 -05:00
more stuff
This commit is contained in:
@@ -1,7 +1,16 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'placehold.co',
|
||||||
|
port: '',
|
||||||
|
pathname: '/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
684
src/app/page.tsx
684
src/app/page.tsx
@@ -8,640 +8,23 @@ import SearchInput from '@/components/SearchInput';
|
|||||||
import ProductCard from '@/components/ProductCard';
|
import ProductCard from '@/components/ProductCard';
|
||||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import Link from 'next/link';
|
||||||
// Sample firearm parts data
|
import { mockProducts } from '@/mock/product';
|
||||||
const parts = [
|
import type { Product } from '@/mock/product';
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extract unique values for dropdowns
|
// Extract unique values for dropdowns
|
||||||
const categories = ['All', ...Array.from(new Set(parts.map(part => part.category.name)))];
|
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
||||||
const brands = ['All', ...Array.from(new Set(parts.map(part => part.brand.name)))];
|
const brands = ['All', ...Array.from(new Set(mockProducts.map(part => part.brand.name)))];
|
||||||
const vendors = ['All', ...Array.from(new Set(parts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
|
const vendors = ['All', ...Array.from(new Set(mockProducts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
|
||||||
const restrictions = [...new Set(parts.flatMap(part => part.restrictions))];
|
|
||||||
|
// Restrictions for filter dropdown
|
||||||
|
const restrictionOptions = [
|
||||||
|
'',
|
||||||
|
'NFA',
|
||||||
|
'SBR',
|
||||||
|
'SUPPRESSOR',
|
||||||
|
'STATE_RESTRICTIONS',
|
||||||
|
];
|
||||||
|
|
||||||
type SortField = 'name' | 'category' | 'price';
|
type SortField = 'name' | 'category' | 'price';
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
@@ -800,7 +183,7 @@ export default function Home() {
|
|||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
// Filter parts based on selected criteria
|
// 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 matchesCategory = selectedCategory === 'All' || part.category.name === selectedCategory;
|
||||||
const matchesBrand = selectedBrand === 'All' || part.brand.name === selectedBrand;
|
const matchesBrand = selectedBrand === 'All' || part.brand.name === selectedBrand;
|
||||||
const matchesVendor = selectedVendor === 'All' || part.offers.some(offer => offer.vendor.name === selectedVendor);
|
const matchesVendor = selectedVendor === 'All' || part.offers.some(offer => offer.vendor.name === selectedVendor);
|
||||||
@@ -808,7 +191,16 @@ export default function Home() {
|
|||||||
part.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
part.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
part.brand.name.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
|
// Price range filtering
|
||||||
let matchesPrice = true;
|
let matchesPrice = true;
|
||||||
@@ -882,6 +274,16 @@ export default function Home() {
|
|||||||
|
|
||||||
const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || priceRange || selectedRestriction;
|
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 (
|
return (
|
||||||
<main className="min-h-screen bg-neutral-50 dark:bg-neutral-900">
|
<main className="min-h-screen bg-neutral-50 dark:bg-neutral-900">
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
@@ -1019,7 +421,7 @@ export default function Home() {
|
|||||||
label="Restriction"
|
label="Restriction"
|
||||||
value={selectedRestriction}
|
value={selectedRestriction}
|
||||||
onChange={setSelectedRestriction}
|
onChange={setSelectedRestriction}
|
||||||
options={['', ...restrictions]}
|
options={restrictionOptions}
|
||||||
placeholder="All restrictions"
|
placeholder="All restrictions"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1047,7 +449,7 @@ export default function Home() {
|
|||||||
{/* View Toggle and Results Count */}
|
{/* View Toggle and Results Count */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
<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 && (
|
{hasActiveFilters && (
|
||||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||||
(filtered)
|
(filtered)
|
||||||
@@ -1076,7 +478,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Restriction Alert Example */}
|
{/* Restriction Alert Example */}
|
||||||
{sortedParts.some(part => part.restrictions.includes('NFA')) && (
|
{sortedParts.some(part => part.restrictions?.nfa) && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<RestrictionAlert
|
<RestrictionAlert
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -1158,9 +560,11 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<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
|
View Details
|
||||||
</button>
|
</a>
|
||||||
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -1172,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="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="flex items-center justify-between">
|
||||||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
<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 && (
|
{hasActiveFilters && (
|
||||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||||
(filtered)
|
(filtered)
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { mockProducts } from '@/mock/products';
|
import { mockProducts } from '@/mock/product';
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Product } from '@/mock/product';
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
product: {
|
product: Product;
|
||||||
id: string;
|
}
|
||||||
name: string;
|
|
||||||
description: string;
|
function getRestrictionFlags(restrictions?: Product['restrictions']): string[] {
|
||||||
image_url: string;
|
const flags: string[] = [];
|
||||||
brand: { name: string };
|
if (restrictions?.nfa) flags.push('NFA');
|
||||||
category: { name: string };
|
if (restrictions?.sbr) flags.push('SBR');
|
||||||
restrictions: string[];
|
if (restrictions?.suppressor) flags.push('SUPPRESSOR');
|
||||||
offers: Array<{ price: number; vendor: { name: string } }>;
|
if (restrictions?.stateRestrictions && restrictions.stateRestrictions.length > 0) flags.push('STATE_RESTRICTIONS');
|
||||||
};
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductCard({ product }: ProductCardProps) {
|
export default function ProductCard({ product }: ProductCardProps) {
|
||||||
@@ -89,9 +91,9 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
/>
|
/>
|
||||||
{product.restrictions && product.restrictions.length > 0 && (
|
{getRestrictionFlags(product.restrictions).length > 0 && (
|
||||||
<div className="absolute top-2 left-2 flex flex-wrap gap-1">
|
<div className="absolute top-2 left-2 flex flex-wrap gap-1">
|
||||||
{product.restrictions.map((restriction) => (
|
{getRestrictionFlags(product.restrictions).map((restriction) => (
|
||||||
<RestrictionBadge key={restriction} restriction={restriction} />
|
<RestrictionBadge key={restriction} restriction={restriction} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -109,9 +111,11 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-base-content/50">{product.category.name}</span>
|
<span className="text-xs text-base-content/50">{product.category.name}</span>
|
||||||
<button className="btn btn-primary btn-sm">
|
<Link href={`/products/${product.id}`} legacyBehavior>
|
||||||
|
<a className="btn btn-primary btn-sm">
|
||||||
View Details
|
View Details
|
||||||
</button>
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1169
src/mock/product.ts
1169
src/mock/product.ts
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Reference in New Issue
Block a user