Files
gunbuilder-next-tailwind/src/app/parts/page.tsx
2025-06-30 06:36:03 -04:00

766 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { Listbox, Transition } from '@headlessui/react';
import { ChevronUpDownIcon, CheckIcon, XMarkIcon, TableCellsIcon, Squares2X2Icon, MagnifyingGlassIcon } from '@heroicons/react/20/solid';
import SearchInput from '@/components/SearchInput';
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';
import Image from 'next/image';
import { useBuildStore } from '@/store/useBuildStore';
import { buildGroups } from '../build/page';
// Extract unique values for dropdowns
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 = [
'All',
'NFA',
'SBR',
'Suppressor',
'State Restrictions',
];
type SortField = 'name' | 'category' | 'price';
type SortDirection = 'asc' | 'desc';
// Restriction indicator component
const RestrictionBadge = ({ restriction }: { restriction: string }) => {
const restrictionConfig = {
NFA: {
label: 'NFA',
color: 'bg-red-600 text-white',
icon: '🔒',
tooltip: 'National Firearms Act - Requires special registration'
},
SBR: {
label: 'SBR',
color: 'bg-orange-600 text-white',
icon: '📏',
tooltip: 'Short Barrel Rifle - Requires NFA registration'
},
SUPPRESSOR: {
label: 'Suppressor',
color: 'bg-purple-600 text-white',
icon: '🔇',
tooltip: 'Sound Suppressor - Requires NFA registration'
},
FFL_REQUIRED: {
label: 'FFL',
color: 'bg-blue-600 text-white',
icon: '🏪',
tooltip: 'Federal Firearms License required for purchase'
},
STATE_RESTRICTIONS: {
label: 'State',
color: 'bg-yellow-600 text-black',
icon: '🗺️',
tooltip: 'State-specific restrictions may apply'
},
HIGH_CAPACITY: {
label: 'High Cap',
color: 'bg-pink-600 text-white',
icon: '🥁',
tooltip: 'High capacity magazine - check local laws'
},
SILENCERSHOP_PARTNER: {
label: 'SilencerShop',
color: 'bg-green-600 text-white',
icon: '🤝',
tooltip: 'Available through SilencerShop partnership'
}
};
const config = restrictionConfig[restriction as keyof typeof restrictionConfig];
if (!config) return null;
return (
<div
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${config.color} cursor-help`}
title={config.tooltip}
>
<span>{config.icon}</span>
<span>{config.label}</span>
</div>
);
};
// Tailwind UI Dropdown Component
const Dropdown = ({
label,
value,
onChange,
options,
placeholder = "Select option"
}: {
label: string;
value: string;
onChange: (value: string) => void;
options: string[];
placeholder?: string;
}) => {
return (
<div className="relative">
<Listbox value={value} onChange={onChange}>
<div className="relative">
<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 dark:bg-neutral-800 py-1.5 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-4 w-4 text-neutral-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as="div"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="absolute z-20 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) => (
<Listbox.Option
key={optionIdx}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-neutral-900 dark:text-white'
}`
}
value={option}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400">
<CheckIcon className="h-4 w-4" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
};
// Map product categories to checklist component categories
const getComponentCategory = (productCategory: string): string => {
const categoryMap: Record<string, string> = {
// Upper components
'Upper Receiver': 'Upper',
'Barrel': 'Upper',
'BCG': 'Upper',
'Bolt Carrier Group': 'Upper',
'Charging Handle': 'Upper',
'Gas Block': 'Upper',
'Gas Tube': 'Upper',
'Handguard': 'Upper',
'Muzzle Device': 'Upper',
'Suppressor': 'Upper',
// Lower components
'Lower Receiver': 'Lower',
'Trigger': 'Lower',
'Trigger Guard': 'Lower',
'Pistol Grip': 'Lower',
'Buffer Tube': 'Lower',
'Buffer': 'Lower',
'Buffer Spring': 'Lower',
'Stock': 'Lower',
// Accessories
'Magazine': 'Accessory',
'Sights': 'Accessory',
'Optic': 'Accessory',
'Scope': 'Accessory',
'Red Dot': 'Accessory',
};
return categoryMap[productCategory] || 'Accessory'; // Default to Accessory if no match
};
// Map product categories to specific checklist component names
const getMatchingComponentName = (productCategory: string): string => {
const componentMap: Record<string, string> = {
'Upper Receiver': 'Upper Receiver',
'Barrel': 'Barrel',
'BCG': 'Bolt Carrier Group (BCG)',
'Bolt Carrier Group': 'Bolt Carrier Group (BCG)',
'Charging Handle': 'Charging Handle',
'Gas Block': 'Gas Block',
'Gas Tube': 'Gas Tube',
'Handguard': 'Handguard',
'Muzzle Device': 'Muzzle Device',
'Suppressor': 'Muzzle Device', // Suppressors go to Muzzle Device component
'Lower Receiver': 'Lower Receiver',
'Trigger': 'Trigger',
'Trigger Guard': 'Trigger Guard',
'Pistol Grip': 'Pistol Grip',
'Buffer Tube': 'Buffer Tube',
'Buffer': 'Buffer',
'Buffer Spring': 'Buffer Spring',
'Stock': 'Stock',
'Magazine': 'Magazine',
'Sights': 'Sights',
'Optic': 'Sights',
'Scope': 'Sights',
'Red Dot': 'Sights',
};
return componentMap[productCategory] || '';
};
export default function Home() {
const searchParams = useSearchParams();
const router = useRouter();
const [selectedCategory, setSelectedCategory] = useState('All');
const [selectedBrand, setSelectedBrand] = useState('All');
const [selectedVendor, setSelectedVendor] = useState('All');
const [priceRange, setPriceRange] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [selectedRestriction, setSelectedRestriction] = useState('');
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
const [addedPartIds, setAddedPartIds] = useState<string[]>([]);
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
const selectPartForComponent = useBuildStore((state) => state.selectPartForComponent);
const selectedParts = useBuildStore((state) => state.selectedParts);
const removePartForComponent = useBuildStore((state) => state.removePartForComponent);
// Read category from URL parameter on page load
useEffect(() => {
const categoryParam = searchParams.get('category');
if (categoryParam && categories.includes(categoryParam)) {
setSelectedCategory(categoryParam);
}
}, [searchParams]);
// Filter parts based on selected criteria
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);
const matchesSearch = !searchTerm ||
part.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
part.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
part.brand.name.toLowerCase().includes(searchTerm.toLowerCase());
// 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;
if (priceRange) {
const lowestPrice = Math.min(...part.offers.map(offer => offer.price));
switch (priceRange) {
case 'under-100':
matchesPrice = lowestPrice < 100;
break;
case '100-300':
matchesPrice = lowestPrice >= 100 && lowestPrice <= 300;
break;
case '300-500':
matchesPrice = lowestPrice > 300 && lowestPrice <= 500;
break;
case 'over-500':
matchesPrice = lowestPrice > 500;
break;
}
}
return matchesCategory && matchesBrand && matchesVendor && matchesSearch && matchesPrice && matchesRestriction;
});
// Sort parts
const sortedParts = [...filteredParts].sort((a, b) => {
let aValue: any, bValue: any;
if (sortField === 'price') {
aValue = Math.min(...a.offers.map(offer => offer.price));
bValue = Math.min(...b.offers.map(offer => offer.price));
} else if (sortField === 'category') {
aValue = a.category.name.toLowerCase();
bValue = b.category.name.toLowerCase();
} else {
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
}
if (sortDirection === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) {
return '↕️';
}
return sortDirection === 'asc' ? '↑' : '↓';
};
const clearFilters = () => {
setSelectedCategory('All');
setSelectedBrand('All');
setSelectedVendor('All');
setSearchTerm('');
setPriceRange('');
setSelectedRestriction('');
};
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;
};
const handleAdd = (part: Product) => {
setAddedPartIds((prev) => [...prev, part.id]);
setTimeout(() => setAddedPartIds((prev) => prev.filter((id) => id !== part.id)), 1500);
};
return (
<main className="min-h-screen bg-neutral-50 dark:bg-neutral-900">
{/* Page Title */}
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white">
Parts Catalog
{selectedCategory !== 'All' && (
<span className="text-primary-600 dark:text-primary-400 ml-2 text-2xl">
- {selectedCategory}
</span>
)}
</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-2">
{selectedCategory !== 'All'
? `Showing ${selectedCategory} parts for your build`
: 'Browse and filter firearm parts for your build'
}
</p>
</div>
</div>
{/* Search and Filters */}
<div className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
{/* Search Row */}
<div className="mb-3 flex justify-end">
<div className={`transition-all duration-300 ease-in-out flex justify-end ${isSearchExpanded ? 'w-1/2' : 'w-auto'}`}>
{isSearchExpanded ? (
<div className="flex items-center gap-2 w-full justify-end">
<div className="flex-1 max-w-md">
<SearchInput
label=""
value={searchTerm}
onChange={setSearchTerm}
placeholder="Search parts..."
/>
</div>
<button
onClick={() => {
setIsSearchExpanded(false);
setSearchTerm('');
}}
className="p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors flex-shrink-0"
aria-label="Close search"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
) : (
<button
onClick={() => setIsSearchExpanded(true)}
className="p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700"
aria-label="Open search"
>
<MagnifyingGlassIcon className="h-5 w-5" />
</button>
)}
</div>
</div>
{/* Filters Row */}
<div className="grid grid-cols-2 md:grid-cols-6 lg:grid-cols-7 gap-3">
{/* Category Dropdown */}
<div className="col-span-1">
<Dropdown
label="Category"
value={selectedCategory}
onChange={setSelectedCategory}
options={categories}
placeholder="All categories"
/>
</div>
{/* Brand Dropdown */}
<div className="col-span-1">
<Dropdown
label="Brand"
value={selectedBrand}
onChange={setSelectedBrand}
options={brands}
placeholder="All brands"
/>
</div>
{/* Vendor Dropdown */}
<div className="col-span-1">
<Dropdown
label="Vendor"
value={selectedVendor}
onChange={setSelectedVendor}
options={vendors}
placeholder="All vendors"
/>
</div>
{/* Price Range */}
<div className="col-span-1">
<Listbox value={priceRange} onChange={setPriceRange}>
<div className="relative">
<Listbox.Label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
Price Range
</Listbox.Label>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-neutral-800 py-1.5 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">
{priceRange === '' ? 'Select price range' :
priceRange === 'under-100' ? 'Under $100' :
priceRange === '100-300' ? '$100 - $300' :
priceRange === '300-500' ? '$300 - $500' :
priceRange === 'over-500' ? '$500+' : priceRange}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-4 w-4 text-neutral-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as="div"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="absolute z-20 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>
{[
{ value: '', label: 'Select price range' },
{ value: 'under-100', label: 'Under $100' },
{ value: '100-300', label: '$100 - $300' },
{ value: '300-500', label: '$300 - $500' },
{ value: 'over-500', label: '$500+' }
].map((option, optionIdx) => (
<Listbox.Option
key={optionIdx}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100' : 'text-neutral-900 dark:text-white'
}`
}
value={option.value}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400">
<CheckIcon className="h-4 w-4" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
{/* Restriction Filter */}
<div className="col-span-1">
<Dropdown
label="Restriction"
value={selectedRestriction}
onChange={setSelectedRestriction}
options={restrictionOptions}
placeholder="All restrictions"
/>
</div>
{/* Clear Filters */}
<div className="col-span-1 flex items-end">
<button
onClick={clearFilters}
disabled={!hasActiveFilters}
className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${
hasActiveFilters
? 'bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 text-white'
: 'bg-neutral-200 dark:bg-neutral-700 text-neutral-400 dark:text-neutral-500 cursor-not-allowed'
}`}
>
<XMarkIcon className="h-3.5 w-3.5" />
Clear All
</button>
</div>
</div>
</div>
</div>
{/* 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')}
aria-label="Table view"
>
<TableCellsIcon className="h-5 w-5" />
</button>
<button
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
onClick={() => setViewMode('cards')}
aria-label="Card view"
>
<Squares2X2Icon className="h-5 w-5" />
</button>
</div>
</div>
</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 max-h-screen overflow-y-auto">
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
<thead className="bg-neutral-50 dark:bg-neutral-700 sticky top-0 z-10 shadow-sm">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Product
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Category
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-600"
onClick={() => handleSort('price')}
>
<div className="flex items-center space-x-1">
<span>Price</span>
<span className="text-sm">{getSortIcon('price')}</span>
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{sortedParts.map((part) => (
<tr key={part.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors">
<td className="px-6 py-4 whitespace-nowrap flex items-center gap-3 min-w-[180px]">
<div className="w-12 h-12 flex-shrink-0 rounded bg-neutral-100 dark:bg-neutral-700 overflow-hidden flex items-center justify-center border border-neutral-200 dark:border-neutral-700">
<Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" />
</div>
<div>
<Link href={`/products/${part.id}`} className="text-sm font-semibold text-primary hover:underline dark:text-primary-400">
{part.name}
</Link>
<div className="text-xs text-neutral-500 dark:text-neutral-400">{part.brand.name}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
{part.category.name}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-semibold text-neutral-900 dark:text-white">
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{(() => {
// Find if this part is already selected for any component
const selectedComponentId = Object.entries(selectedParts).find(([_, selectedPart]) => selectedPart?.id === part.id)?.[0];
// Find the appropriate component based on category match
const matchingComponentName = getMatchingComponentName(part.category.name);
const matchingComponent = (buildGroups as {components: any[]}[]).flatMap((group) => group.components).find((component: any) =>
component.name === matchingComponentName && !selectedParts[component.id]
);
if (selectedComponentId) {
return (
<button
className="btn btn-outline btn-sm"
onClick={() => removePartForComponent(selectedComponentId)}
>
Remove
</button>
);
} else if (matchingComponent && !selectedParts[matchingComponent.id]) {
return (
<button
className="btn btn-neutral btn-sm flex items-center gap-1"
onClick={() => {
selectPartForComponent(matchingComponent.id, {
id: part.id,
name: part.name,
image_url: part.image_url,
brand: part.brand,
category: part.category,
offers: part.offers,
});
router.push('/build');
}}
>
<span className="text-lg leading-none">+</span>
<span className="text-xs font-normal">to build</span>
</button>
);
} else {
return (
<span className="text-xs text-gray-400">Part Selected</span>
);
}
})()}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Table Footer */}
<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 {mockProducts.length} parts
{hasActiveFilters && (
<span className="ml-2 text-primary-600 dark:text-primary-400">
(filtered)
</span>
)}
</div>
<div className="text-sm text-neutral-500 dark:text-neutral-400">
Total Value: ${sortedParts.reduce((sum, part) => sum + Math.min(...part.offers.map(offer => offer.price)), 0).toFixed(2)}
</div>
</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} onAdd={() => handleAdd(part)} added={addedPartIds.includes(part.id)} />
))}
</div>
)}
</div>
{/* Compact Restriction Legend */}
<div className="mt-8 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-center gap-4 text-xs text-neutral-500 dark:text-neutral-400">
<span className="font-medium">Restrictions:</span>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-red-600 text-white">🔒NFA</div>
<span>National Firearms Act</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-orange-600 text-white">📏SBR</div>
<span>Short Barrel Rifle</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-purple-600 text-white">🔇Suppressor</div>
<span>Sound Suppressor</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-blue-600 text-white">🏪FFL</div>
<span>FFL Required</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-600 text-black">🗺State</div>
<span>State Restrictions</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-pink-600 text-white">🥁High Cap</div>
<span>High Capacity</span>
</div>
<div className="flex items-center gap-1">
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-green-600 text-white">🤝SilencerShop</div>
<span>SilencerShop Partner</span>
</div>
</div>
</div>
</main>
);
}