From ec6a0861f016e3700fc249f34bde4511fe3c4996 Mon Sep 17 00:00:00 2001 From: Sean S Date: Sun, 29 Jun 2025 07:43:18 -0400 Subject: [PATCH] tailwinds custom theme and tw plus components --- package-lock.json | 232 ++++++++++ package.json | 2 + src/app/build/page.tsx | 23 +- src/app/builds/page.tsx | 11 +- src/app/globals.css | 55 ++- src/app/layout.tsx | 31 +- src/app/page.tsx | 753 ++++++++++++++++++++++--------- src/components/Navbar.tsx | 68 +-- src/components/SearchInput.tsx | 39 ++ src/components/ThemeProvider.tsx | 52 +++ src/components/ThemeSwitcher.tsx | 28 ++ tailwind.config.js | 100 +++- 12 files changed, 1114 insertions(+), 280 deletions(-) create mode 100644 src/components/SearchInput.tsx create mode 100644 src/components/ThemeProvider.tsx create mode 100644 src/components/ThemeSwitcher.tsx diff --git a/package-lock.json b/package-lock.json index f2ad914..452d908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "pew-builder-nextjs", "version": "0.1.0", "dependencies": { + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", "next": "15.3.4", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -225,6 +227,88 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", + "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -974,6 +1058,103 @@ "node": ">=14" } }, + "node_modules/@react-aria/focus": { + "version": "3.20.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz", + "integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.3", + "@react-aria/utils": "^3.29.1", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz", + "integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-aria/utils": "^3.29.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", + "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.7", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", + "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", + "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1003,6 +1184,33 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -2225,6 +2433,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -5887,6 +6104,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", @@ -6311,6 +6534,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 26682c0..77fdaaa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", "next": "15.3.4", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/src/app/build/page.tsx b/src/app/build/page.tsx index afc3543..7216a07 100644 --- a/src/app/build/page.tsx +++ b/src/app/build/page.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import Link from 'next/link'; import React from 'react'; +import SearchInput from '@/components/SearchInput'; // AR-15 Build Requirements grouped by main categories const buildGroups = [ @@ -333,13 +334,11 @@ export default function BuildPage() { {/* Search Row */}
- - 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" + onChange={setSearchTerm} + placeholder="Search components..." />
@@ -459,23 +458,23 @@ export default function BuildPage() { return ( {/* Group Header */} - +
-
- +
+ {group.name === 'Upper Parts' ? '🔫' : group.name === 'Lower Parts' ? '🔧' : '📦'}
-

{group.name}

-

{group.description}

+

{group.name}

+

{group.description}

-
+
{groupComponents.length} components
diff --git a/src/app/builds/page.tsx b/src/app/builds/page.tsx index c05ac20..28ae0d4 100644 --- a/src/app/builds/page.tsx +++ b/src/app/builds/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import SearchInput from '@/components/SearchInput'; // Sample build data const sampleBuilds = [ @@ -252,13 +253,11 @@ export default function BuildsPage() { {/* Search Row */}
- - 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" + onChange={setSearchTerm} + placeholder="Search builds..." />
diff --git a/src/app/globals.css b/src/app/globals.css index bd6213e..c2f4d43 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,56 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer base { + html { + scroll-behavior: smooth; + } + + body { + @apply transition-colors duration-200; + } +} + +@layer components { + /* Custom scrollbar for webkit browsers */ + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + @apply bg-neutral-100 dark:bg-neutral-800; + } + + ::-webkit-scrollbar-thumb { + @apply bg-neutral-300 dark:bg-neutral-600 rounded-full; + } + + ::-webkit-scrollbar-thumb:hover { + @apply bg-neutral-400 dark:bg-neutral-500; + } + + /* Focus styles for better accessibility */ + .focus-ring { + @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900; + } + + /* Card styles */ + .card { + @apply bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200; + } + + /* Button styles */ + .btn-primary { + @apply bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus-ring; + } + + .btn-secondary { + @apply bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus-ring; + } + + /* Input styles */ + .input-field { + @apply w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-500 dark:placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors duration-200; + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 21ca771..a5a869c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,21 +1,14 @@ import "./globals.css"; import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import { Navbar } from "@/components/Navbar"; +import { Inter } from "next/font/google"; +import Navbar from "@/components/Navbar"; +import { ThemeProvider } from "@/components/ThemeProvider"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Pew Builder - Firearm Parts Catalog", - description: "Build your dream AR-15 with our comprehensive parts catalog and build checklist", + title: "Pew Builder - Firearm Parts Catalog & Build Management", + description: "Professional firearm parts catalog and AR-15 build management system", }; export default function RootLayout({ @@ -24,10 +17,14 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - {children} + + + +
+ + {children} +
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index c730ff4..f05c36e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,9 @@ import { useState, useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; +import { Listbox, Transition } from '@headlessui/react'; +import { ChevronUpDownIcon, CheckIcon, XMarkIcon } from '@heroicons/react/20/solid'; +import SearchInput from '@/components/SearchInput'; // Sample firearm parts data const parts = [ @@ -9,7 +12,7 @@ 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', + image_url: 'https://placehold.co/300x200/1f2937/ffffff?text=Barrel', brand: { id: 'b1', name: 'Faxon Firearms', @@ -18,6 +21,7 @@ const parts = [ id: 'c1', name: 'Barrel', }, + restrictions: [], offers: [ { price: 189.99, @@ -32,7 +36,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/374151/ffffff?text=Upper+Receiver', brand: { id: 'b2', name: 'BCM', @@ -41,6 +45,7 @@ const parts = [ id: 'c2', name: 'Upper Receiver', }, + restrictions: [], offers: [ { price: 129.99, @@ -55,7 +60,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/4b5563/ffffff?text=Lower+Receiver', brand: { id: 'b3', name: 'Aero Precision', @@ -64,6 +69,7 @@ const parts = [ id: 'c3', name: 'Lower Receiver', }, + restrictions: ['FFL_REQUIRED'], offers: [ { price: 89.99, @@ -78,7 +84,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/6b7280/ffffff?text=BCG', brand: { id: 'b4', name: 'Toolcraft', @@ -87,6 +93,7 @@ const parts = [ id: 'c4', name: 'BCG', }, + restrictions: [], offers: [ { price: 149.99, @@ -101,7 +108,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/1e40af/ffffff?text=Trigger', brand: { id: 'b5', name: 'Geissele', @@ -110,6 +117,7 @@ const parts = [ id: 'c5', name: 'Trigger', }, + restrictions: [], offers: [ { price: 249.99, @@ -124,7 +132,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/059669/ffffff?text=Stock', brand: { id: 'b6', name: 'Magpul', @@ -133,6 +141,7 @@ const parts = [ id: 'c6', name: 'Stock', }, + restrictions: [], offers: [ { price: 69.99, @@ -147,7 +156,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/7c3aed/ffffff?text=Charging+Handle', brand: { id: 'b7', name: 'Radian Weapons', @@ -156,6 +165,7 @@ const parts = [ id: 'c7', name: 'Charging Handle', }, + restrictions: [], offers: [ { price: 89.99, @@ -170,7 +180,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/dc2626/ffffff?text=Handguard', brand: { id: 'b2', name: 'BCM', @@ -179,6 +189,7 @@ const parts = [ id: 'c8', name: 'Handguard', }, + restrictions: [], offers: [ { price: 199.99, @@ -193,7 +204,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/ea580c/ffffff?text=Muzzle+Device', brand: { id: 'b8', name: 'SureFire', @@ -202,6 +213,7 @@ const parts = [ id: 'c9', name: 'Muzzle Device', }, + restrictions: [], offers: [ { price: 159.99, @@ -216,7 +228,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/be185d/ffffff?text=Gas+Block', brand: { id: 'b3', name: 'Aero Precision', @@ -225,6 +237,7 @@ const parts = [ id: 'c10', name: 'Gas Block', }, + restrictions: [], offers: [ { price: 49.99, @@ -239,7 +252,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/0891b2/ffffff?text=Gas+Tube', brand: { id: 'b2', name: 'BCM', @@ -248,6 +261,7 @@ const parts = [ id: 'c11', name: 'Gas Tube', }, + restrictions: [], offers: [ { price: 19.99, @@ -262,7 +276,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/166534/ffffff?text=Pistol+Grip', brand: { id: 'b6', name: 'Magpul', @@ -271,6 +285,7 @@ const parts = [ id: 'c12', name: 'Pistol Grip', }, + restrictions: [], offers: [ { price: 24.99, @@ -285,7 +300,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/92400e/ffffff?text=Buffer+Tube', brand: { id: 'b2', name: 'BCM', @@ -294,6 +309,7 @@ const parts = [ id: 'c13', name: 'Buffer Tube', }, + restrictions: [], offers: [ { price: 44.99, @@ -308,7 +324,7 @@ const parts = [ id: '14', name: 'H2 Buffer Weight', description: 'Heavy buffer weight for improved recoil management', - image_url: 'https://picsum.photos/300/200?random=14', + image_url: 'https://placehold.co/300x200/7c2d12/ffffff?text=Buffer', brand: { id: 'b9', name: 'Spikes Tactical', @@ -317,6 +333,7 @@ const parts = [ id: 'c14', name: 'Buffer', }, + restrictions: [], offers: [ { price: 29.99, @@ -331,7 +348,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/1e3a8a/ffffff?text=Buffer+Spring', brand: { id: 'b10', name: 'Sprinco', @@ -340,6 +357,7 @@ const parts = [ id: 'c15', name: 'Buffer Spring', }, + restrictions: [], offers: [ { price: 19.99, @@ -354,7 +372,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/065f46/ffffff?text=Magazine', brand: { id: 'b6', name: 'Magpul', @@ -363,6 +381,7 @@ const parts = [ id: 'c16', name: 'Magazine', }, + restrictions: ['STATE_RESTRICTIONS'], offers: [ { price: 14.99, @@ -377,7 +396,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/581c87/ffffff?text=Sights', brand: { id: 'b11', name: 'Troy Industries', @@ -386,6 +405,7 @@ const parts = [ id: 'c17', name: 'Sights', }, + restrictions: [], offers: [ { price: 129.99, @@ -400,7 +420,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/991b1b/ffffff?text=Trigger+Guard', brand: { id: 'b3', name: 'Aero Precision', @@ -409,6 +429,7 @@ const parts = [ id: 'c18', name: 'Trigger Guard', }, + restrictions: [], offers: [ { price: 12.99, @@ -423,7 +444,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/0c4a6e/ffffff?text=Trigger', brand: { id: 'b12', name: 'LaRue Tactical', @@ -432,6 +453,7 @@ const parts = [ id: 'c5', name: 'Trigger', }, + restrictions: [], offers: [ { price: 89.99, @@ -446,7 +468,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/1f2937/ffffff?text=Barrel', brand: { id: 'b13', name: 'Daniel Defense', @@ -455,6 +477,7 @@ const parts = [ id: 'c1', name: 'Barrel', }, + restrictions: [], offers: [ { price: 399.99, @@ -469,7 +492,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/374151/ffffff?text=Lower+Receiver', brand: { id: 'b2', name: 'BCM', @@ -478,6 +501,7 @@ const parts = [ id: 'c3', name: 'Lower Receiver', }, + restrictions: ['FFL_REQUIRED'], offers: [ { price: 449.99, @@ -492,7 +516,7 @@ const parts = [ 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', + image_url: 'https://placehold.co/300x200/4b5563/ffffff?text=Red+Dot+Sight', brand: { id: 'b14', name: 'Aimpoint', @@ -501,6 +525,7 @@ const parts = [ id: 'c17', name: 'Sights', }, + restrictions: [], offers: [ { price: 449.99, @@ -510,6 +535,102 @@ const parts = [ }, }, ], + }, + { + 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', + }, + }, + ], } ]; @@ -517,20 +638,194 @@ const parts = [ const categories = ['All', ...Array.from(new Set(parts.map(part => part.category.name)))]; const brands = ['All', ...Array.from(new Set(parts.map(part => part.brand.name)))]; const vendors = ['All', ...Array.from(new Set(parts.flatMap(part => part.offers.map(offer => offer.vendor.name))))]; +const restrictions = [...new Set(parts.flatMap(part => part.restrictions))]; 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 ( +
+ {config.icon} + {config.label} +
+ ); +}; + +// Product card component +const ProductCard = ({ product }: { product: any }) => { + const [imageError, setImageError] = useState(false); + const lowestPrice = Math.min(...product.offers.map((offer: any) => offer.price)); + + return ( +
+
+ {product.name} setImageError(true)} + /> + {product.restrictions && product.restrictions.length > 0 && ( +
+ {product.restrictions.map((restriction: string) => ( + + ))} +
+ )} +
+
+

{product.name}

+

{product.description}

+
+ {product.brand.name} + ${lowestPrice.toFixed(2)} +
+
+ {product.category.name} + +
+
+
+ ); +}; + +// 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 ( +
+ +
+ + {label} + + + + {value || placeholder} + + + + + + + {options.map((option, optionIdx) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-blue-100 text-blue-900' : 'text-gray-900' + }` + } + value={option} + > + {({ selected }) => ( + <> + + {option} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+ ); +}; + export default function Home() { const searchParams = useSearchParams(); 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('name'); const [sortDirection, setSortDirection] = useState('asc'); - const [searchTerm, setSearchTerm] = useState(''); - const [minPrice, setMinPrice] = useState(''); - const [maxPrice, setMaxPrice] = useState(''); // Read category from URL parameter on page load useEffect(() => { @@ -540,39 +835,38 @@ export default function Home() { } }, [searchParams]); - // Filter parts by all criteria + // Filter parts based on selected criteria const filteredParts = parts.filter(part => { - // Category filter - if (selectedCategory !== 'All' && part.category.name !== selectedCategory) { - return false; + 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()); + const matchesRestriction = !selectedRestriction || part.restrictions.includes(selectedRestriction); + + // 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; + } } - - // 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; + + return matchesCategory && matchesBrand && matchesVendor && matchesSearch && matchesPrice && matchesRestriction; }); // Sort parts @@ -618,26 +912,26 @@ export default function Home() { setSelectedBrand('All'); setSelectedVendor('All'); setSearchTerm(''); - setMinPrice(''); - setMaxPrice(''); + setPriceRange(''); + setSelectedRestriction(''); }; - const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || minPrice || maxPrice; + const hasActiveFilters = selectedCategory !== 'All' || selectedBrand !== 'All' || selectedVendor !== 'All' || searchTerm || priceRange || selectedRestriction; return ( -
+
{/* Page Title */} -
+
-

+

Parts Catalog {selectedCategory !== 'All' && ( - + - {selectedCategory} )}

-

+

{selectedCategory !== 'All' ? `Showing ${selectedCategory} parts for your build` : 'Browse and filter firearm parts for your build' @@ -647,18 +941,16 @@ export default function Home() {

{/* Search and Filters */} -
+
{/* Search Row */}
- - 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" + onChange={setSearchTerm} + placeholder="Search parts..." />
@@ -666,88 +958,119 @@ export default function Home() { {/* Filters Row */}
{/* Category Dropdown */} -
- - -
+ {/* Brand Dropdown */} -
- - -
+ {/* Vendor Dropdown */} -
- - + + + {/* Price Range */} +
+ +
+ + Price Range + + + + {priceRange === '' ? 'Select price range' : + priceRange === 'under-100' ? 'Under $100' : + priceRange === '100-300' ? '$100 - $300' : + priceRange === '300-500' ? '$300 - $500' : + priceRange === 'over-500' ? '$500+' : priceRange} + + + + + + + {[ + { 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) => ( + + `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 }) => ( + <> + + {option.label} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
- {/* Min Price */} -
- - 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" - /> -
- - {/* Max Price */} -
- - 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" - /> -
+ {/* Restriction Filter */} + {/* Clear Filters */}
@@ -757,16 +1080,16 @@ export default function Home() { {/* Parts Table */}
-
+
- - +
+ - - - - - - - {sortedParts.length > 0 ? ( - sortedParts.map((part) => ( - - - - - - - - - - )) - ) : ( - - + {sortedParts.map((part) => ( + + + + + + + - )} + ))}
+ Category handleSort('name')} >
@@ -774,14 +1097,14 @@ export default function Home() { {getSortIcon('name')}
+ Brand + Description handleSort('price')} >
@@ -789,92 +1112,106 @@ export default function Home() { {getSortIcon('price')}
- Vendor - + Actions
- - {part.category.name} - - -
- {part.name} -
-
-
- {part.brand.name} -
-
-
- {part.description} -
-
-
- ${Math.min(...part.offers.map(offer => offer.price))} -
-
-
- {part.offers[0]?.vendor.name} -
-
-
- - -
-
-
-
No parts found
-
Try adjusting your filters or search terms
+
+ + {part.category.name} + + +
+ {part.name} +
+
+ {part.brand.name}
+
+ {part.brand.name} +
+
+
+ {part.description} +
+
+
+ ${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)} +
+
+ +
{/* Table Footer */} -
+
-
+
Showing {sortedParts.length} of {parts.length} parts {hasActiveFilters && ( - + (filtered) )}
- {/*
+
Total Value: ${sortedParts.reduce((sum, part) => sum + Math.min(...part.offers.map(offer => offer.price)), 0).toFixed(2)} -
*/} +
+ + {/* Compact Restriction Legend */} +
+
+ Restrictions: +
+
🔒NFA
+ National Firearms Act +
+
+
📏SBR
+ Short Barrel Rifle +
+
+
🔇Suppressor
+ Sound Suppressor +
+
+
🏪FFL
+ FFL Required +
+
+
🗺️State
+ State Restrictions +
+
+
🥁High Cap
+ High Capacity +
+
+
🤝SilencerShop
+ SilencerShop Partner +
+
+
); } \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 43b6222..a8bf62c 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -2,52 +2,56 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import ThemeSwitcher from './ThemeSwitcher'; -export function Navbar() { +export default function Navbar() { const pathname = usePathname(); const navItems = [ - { name: 'Parts Catalog', href: '/' }, - { name: 'Build Checklist', href: '/build' }, - { name: 'My Builds', href: '/builds' }, + { href: '/', label: 'Parts Catalog' }, + { href: '/build', label: 'Build Checklist' }, + { href: '/builds', label: 'My Builds' }, ]; return ( -