first commit
This commit is contained in:
+875
-98
@@ -1,103 +1,880 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// 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://picsum.photos/300/200?random=1',
|
||||
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 receiver with M4 feed ramps and T-markings',
|
||||
image_url: 'https://picsum.photos/300/200?random=2',
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Aero Precision Lower Receiver',
|
||||
description: 'Mil-spec forged lower receiver with trigger guard and threaded bolt catch',
|
||||
image_url: 'https://picsum.photos/300/200?random=3',
|
||||
brand: {
|
||||
id: 'b3',
|
||||
name: 'Aero Precision',
|
||||
},
|
||||
category: {
|
||||
id: 'c3',
|
||||
name: 'Lower Receiver',
|
||||
},
|
||||
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://picsum.photos/300/200?random=4',
|
||||
brand: {
|
||||
id: 'b4',
|
||||
name: 'Toolcraft',
|
||||
},
|
||||
category: {
|
||||
id: 'c4',
|
||||
name: 'BCG',
|
||||
},
|
||||
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://picsum.photos/300/200?random=5',
|
||||
brand: {
|
||||
id: 'b5',
|
||||
name: 'Geissele',
|
||||
},
|
||||
category: {
|
||||
id: 'c5',
|
||||
name: 'Trigger',
|
||||
},
|
||||
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://picsum.photos/300/200?random=6',
|
||||
brand: {
|
||||
id: 'b6',
|
||||
name: 'Magpul',
|
||||
},
|
||||
category: {
|
||||
id: 'c6',
|
||||
name: 'Stock',
|
||||
},
|
||||
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://picsum.photos/300/200?random=7',
|
||||
brand: {
|
||||
id: 'b7',
|
||||
name: 'Radian Weapons',
|
||||
},
|
||||
category: {
|
||||
id: 'c7',
|
||||
name: 'Charging Handle',
|
||||
},
|
||||
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://picsum.photos/300/200?random=8',
|
||||
brand: {
|
||||
id: 'b2',
|
||||
name: 'BCM',
|
||||
},
|
||||
category: {
|
||||
id: 'c8',
|
||||
name: 'Handguard',
|
||||
},
|
||||
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://picsum.photos/300/200?random=9',
|
||||
brand: {
|
||||
id: 'b8',
|
||||
name: 'SureFire',
|
||||
},
|
||||
category: {
|
||||
id: 'c9',
|
||||
name: 'Muzzle Device',
|
||||
},
|
||||
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://picsum.photos/300/200?random=10',
|
||||
brand: {
|
||||
id: 'b3',
|
||||
name: 'Aero Precision',
|
||||
},
|
||||
category: {
|
||||
id: 'c10',
|
||||
name: 'Gas Block',
|
||||
},
|
||||
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://picsum.photos/300/200?random=11',
|
||||
brand: {
|
||||
id: 'b2',
|
||||
name: 'BCM',
|
||||
},
|
||||
category: {
|
||||
id: 'c11',
|
||||
name: 'Gas Tube',
|
||||
},
|
||||
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://picsum.photos/300/200?random=12',
|
||||
brand: {
|
||||
id: 'b6',
|
||||
name: 'Magpul',
|
||||
},
|
||||
category: {
|
||||
id: 'c12',
|
||||
name: 'Pistol Grip',
|
||||
},
|
||||
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://picsum.photos/300/200?random=13',
|
||||
brand: {
|
||||
id: 'b2',
|
||||
name: 'BCM',
|
||||
},
|
||||
category: {
|
||||
id: 'c13',
|
||||
name: 'Buffer Tube',
|
||||
},
|
||||
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://picsum.photos/300/200?random=14',
|
||||
brand: {
|
||||
id: 'b9',
|
||||
name: 'Spikes Tactical',
|
||||
},
|
||||
category: {
|
||||
id: 'c14',
|
||||
name: 'Buffer',
|
||||
},
|
||||
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://picsum.photos/300/200?random=15',
|
||||
brand: {
|
||||
id: 'b10',
|
||||
name: 'Sprinco',
|
||||
},
|
||||
category: {
|
||||
id: 'c15',
|
||||
name: 'Buffer Spring',
|
||||
},
|
||||
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://picsum.photos/300/200?random=16',
|
||||
brand: {
|
||||
id: 'b6',
|
||||
name: 'Magpul',
|
||||
},
|
||||
category: {
|
||||
id: 'c16',
|
||||
name: 'Magazine',
|
||||
},
|
||||
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://picsum.photos/300/200?random=17',
|
||||
brand: {
|
||||
id: 'b11',
|
||||
name: 'Troy Industries',
|
||||
},
|
||||
category: {
|
||||
id: 'c17',
|
||||
name: 'Sights',
|
||||
},
|
||||
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://picsum.photos/300/200?random=18',
|
||||
brand: {
|
||||
id: 'b3',
|
||||
name: 'Aero Precision',
|
||||
},
|
||||
category: {
|
||||
id: 'c18',
|
||||
name: 'Trigger Guard',
|
||||
},
|
||||
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://picsum.photos/300/200?random=19',
|
||||
brand: {
|
||||
id: 'b12',
|
||||
name: 'LaRue Tactical',
|
||||
},
|
||||
category: {
|
||||
id: 'c5',
|
||||
name: 'Trigger',
|
||||
},
|
||||
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://picsum.photos/300/200?random=20',
|
||||
brand: {
|
||||
id: 'b13',
|
||||
name: 'Daniel Defense',
|
||||
},
|
||||
category: {
|
||||
id: 'c1',
|
||||
name: 'Barrel',
|
||||
},
|
||||
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://picsum.photos/300/200?random=21',
|
||||
brand: {
|
||||
id: 'b2',
|
||||
name: 'BCM',
|
||||
},
|
||||
category: {
|
||||
id: 'c3',
|
||||
name: 'Lower Receiver',
|
||||
},
|
||||
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://picsum.photos/300/200?random=22',
|
||||
brand: {
|
||||
id: 'b14',
|
||||
name: 'Aimpoint',
|
||||
},
|
||||
category: {
|
||||
id: 'c17',
|
||||
name: 'Sights',
|
||||
},
|
||||
offers: [
|
||||
{
|
||||
price: 449.99,
|
||||
url: 'https://aimpoint.com/pro',
|
||||
vendor: {
|
||||
name: 'Aimpoint',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
// 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))))];
|
||||
|
||||
type SortField = 'name' | 'category' | 'price';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const searchParams = useSearchParams();
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [selectedBrand, setSelectedBrand] = useState('All');
|
||||
const [selectedVendor, setSelectedVendor] = useState('All');
|
||||
const [sortField, setSortField] = useState<SortField>('name');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [minPrice, setMinPrice] = useState('');
|
||||
const [maxPrice, setMaxPrice] = useState('');
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
// Read category from URL parameter on page load
|
||||
useEffect(() => {
|
||||
const categoryParam = searchParams.get('category');
|
||||
if (categoryParam && categories.includes(categoryParam)) {
|
||||
setSelectedCategory(categoryParam);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// Filter parts by all criteria
|
||||
const filteredParts = parts.filter(part => {
|
||||
// Category filter
|
||||
if (selectedCategory !== 'All' && part.category.name !== selectedCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Brand filter
|
||||
if (selectedBrand !== 'All' && part.brand.name !== selectedBrand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vendor filter
|
||||
if (selectedVendor !== 'All' && !part.offers.some(offer => offer.vendor.name === selectedVendor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (searchTerm && !part.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||
!part.description.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Price range filter
|
||||
const minPriceNum = minPrice ? parseFloat(minPrice) : 0;
|
||||
const maxPriceNum = maxPrice ? parseFloat(maxPrice) : Infinity;
|
||||
const partPrice = Math.min(...part.offers.map(offer => offer.price));
|
||||
|
||||
if (partPrice < minPriceNum || partPrice > maxPriceNum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 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('');
|
||||
setMinPrice('');
|
||||
setMaxPrice('');
|
||||
};
|
||||
|
||||
const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || minPrice || maxPrice;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50">
|
||||
{/* Page Title */}
|
||||
<div className="bg-white border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Parts Catalog
|
||||
{selectedCategory !== 'All' && (
|
||||
<span className="text-blue-600 ml-2 text-2xl">
|
||||
- {selectedCategory}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{selectedCategory !== 'All'
|
||||
? `Showing ${selectedCategory} parts for your build`
|
||||
: 'Browse and filter firearm parts for your build'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="bg-white border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
{/* Search Row */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search parts..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
{/* Category Dropdown */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<option key={category} value={category}>
|
||||
{category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Brand Dropdown */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Brand</label>
|
||||
<select
|
||||
value={selectedBrand}
|
||||
onChange={(e) => setSelectedBrand(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{brands.map((brand) => (
|
||||
<option key={brand} value={brand}>
|
||||
{brand}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Vendor Dropdown */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Vendor</label>
|
||||
<select
|
||||
value={selectedVendor}
|
||||
onChange={(e) => setSelectedVendor(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{vendors.map((vendor) => (
|
||||
<option key={vendor} value={vendor}>
|
||||
{vendor}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Min Price */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Min Price</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={minPrice}
|
||||
onChange={(e) => setMinPrice(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Price */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Max Price</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
value={maxPrice}
|
||||
onChange={(e) => setMaxPrice(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Clear Filters */}
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
disabled={!hasActiveFilters}
|
||||
className={`w-full px-4 py-2 rounded-lg transition-colors ${
|
||||
hasActiveFilters
|
||||
? 'bg-red-600 text-white hover:bg-red-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parts Table */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Category
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>Name</span>
|
||||
<span className="text-sm">{getSortIcon('name')}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Brand
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
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-gray-500 uppercase tracking-wider">
|
||||
Vendor
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedParts.length > 0 ? (
|
||||
sortedParts.map((part) => (
|
||||
<tr key={part.id} className="hover:bg-gray-50">
|
||||
<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-blue-100 text-blue-800">
|
||||
{part.category.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{part.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">
|
||||
{part.brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-500 max-w-xs truncate">
|
||||
{part.description}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-semibold text-gray-900">
|
||||
${Math.min(...part.offers.map(offer => offer.price))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{part.offers[0]?.vendor.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<button className="bg-blue-600 text-white py-1 px-3 rounded text-xs hover:bg-blue-700 transition-colors">
|
||||
Add to Build
|
||||
</button>
|
||||
<button className="bg-gray-100 text-gray-700 py-1 px-2 rounded text-xs hover:bg-gray-200 transition-colors">
|
||||
⭐
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-12 text-center">
|
||||
<div className="text-gray-500">
|
||||
<div className="text-lg font-medium mb-2">No parts found</div>
|
||||
<div className="text-sm">Try adjusting your filters or search terms</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Table Footer */}
|
||||
<div className="bg-gray-50 px-6 py-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700">
|
||||
Showing {sortedParts.length} of {parts.length} parts
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="text-sm text-gray-500">
|
||||
Total Value: ${sortedParts.reduce((sum, part) => sum + Math.min(...part.offers.map(offer => offer.price)), 0).toFixed(2)}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user