mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 02:56:45 -05:00
fixed theming and color
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
|
||||
/* 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;
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-neutral-900;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
@@ -41,16 +41,13 @@
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Removed custom .btn-primary to avoid DaisyUI conflict */
|
||||
.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;
|
||||
@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 focus:border-primary transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang="en" suppressHydrationWarning data-theme="pew">
|
||||
<body className={`${inter.className} antialiased`}>
|
||||
<ThemeProvider>
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-900 transition-colors duration-200">
|
||||
|
||||
686
src/app/page.tsx
686
src/app/page.tsx
@@ -1,640 +1,58 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
import ProductCard from '@/components/ProductCard';
|
||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import BetaTester from "../components/BetaTester";
|
||||
import Link from 'next/link';
|
||||
import { mockProducts } from '@/mock/product';
|
||||
import type { Product } from '@/mock/product';
|
||||
|
||||
// Extract unique values for dropdowns
|
||||
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
||||
const brands = ['All', ...Array.from(new Set(mockProducts.map(part => part.brand.name)))];
|
||||
const vendors = ['All', ...Array.from(new Set(mockProducts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
|
||||
|
||||
// Restrictions for filter dropdown
|
||||
const restrictionOptions = [
|
||||
'',
|
||||
'NFA',
|
||||
'SBR',
|
||||
'SUPPRESSOR',
|
||||
'STATE_RESTRICTIONS',
|
||||
];
|
||||
|
||||
type SortField = 'name' | 'category' | 'price';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
// 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;
|
||||
|
||||
export default function LandingPage() {
|
||||
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 className="bg-white font-sans">
|
||||
{/* SVG Grid Background */}
|
||||
<div className="relative isolate pt-1">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 -z-10 w-full h-full stroke-gray-200 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="grid"
|
||||
width={200}
|
||||
height={200}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M100 200V.5M.5 .5H200" fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#grid)" width="100%" height="100%" strokeWidth={0} />
|
||||
</svg>
|
||||
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-start lg:gap-x-10 lg:px-8 lg:py-40">
|
||||
{/* Left: Headline, Subheading, Button */}
|
||||
<div className="mx-auto max-w-2xl lg:mx-0 lg:flex-auto">
|
||||
<h1 className="mt-10 text-pretty text-5xl font-semibold tracking-tight text-gray-900 sm:text-7xl">
|
||||
A better way to plan your next build
|
||||
</h1>
|
||||
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
|
||||
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo. Elit sunt amet
|
||||
fugiat veniam occaecat fugiat aliqua. Anim aute id magna aliqua ad ad non deserunt sunt.
|
||||
</p>
|
||||
<div className="mt-10 flex items-top gap-x-6">
|
||||
<Link
|
||||
href="/Builder"
|
||||
className="btn btn-primary text-base font-semibold px-6"
|
||||
>
|
||||
Get Building
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right: Product Image */}
|
||||
<div className="mt-16 sm:mt-24 lg:mt-0 lg:shrink-0 lg:grow items-top flex justify-center">
|
||||
<img
|
||||
alt="AR-15 Lower Receiver"
|
||||
src="https://i.imgur.com/IK8FbaI.png"
|
||||
className="max-w-md w-full h-auto object-contain rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Beta Tester CTA */}
|
||||
<BetaTester />
|
||||
</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-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||
<span className="block truncate text-neutral-900 dark:text-white">
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-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-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<Listbox.Options>
|
||||
{options.map((option, optionIdx) => (
|
||||
<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-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<SortField>('name');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
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-4">
|
||||
{/* Search Row */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="w-1/2">
|
||||
<SearchInput
|
||||
label="Search"
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
placeholder="Search parts..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
{/* Category Dropdown */}
|
||||
<Dropdown
|
||||
label="Category"
|
||||
value={selectedCategory}
|
||||
onChange={setSelectedCategory}
|
||||
options={categories}
|
||||
placeholder="All categories"
|
||||
/>
|
||||
|
||||
{/* Brand Dropdown */}
|
||||
<Dropdown
|
||||
label="Brand"
|
||||
value={selectedBrand}
|
||||
onChange={setSelectedBrand}
|
||||
options={brands}
|
||||
placeholder="All brands"
|
||||
/>
|
||||
|
||||
{/* Vendor Dropdown */}
|
||||
<Dropdown
|
||||
label="Vendor"
|
||||
value={selectedVendor}
|
||||
onChange={setSelectedVendor}
|
||||
options={vendors}
|
||||
placeholder="All vendors"
|
||||
/>
|
||||
|
||||
{/* Price Range */}
|
||||
<div className="relative">
|
||||
<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-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||
<span className="block truncate text-neutral-900 dark:text-white">
|
||||
{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-5 w-5 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-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<Listbox.Options>
|
||||
{[
|
||||
{ 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-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
{/* Restriction Filter */}
|
||||
<Dropdown
|
||||
label="Restriction"
|
||||
value={selectedRestriction}
|
||||
onChange={setSelectedRestriction}
|
||||
options={restrictionOptions}
|
||||
placeholder="All restrictions"
|
||||
/>
|
||||
|
||||
{/* Clear Filters */}
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
disabled={!hasActiveFilters}
|
||||
className={`w-full px-4 py-2 rounded-lg transition-colors flex items-center justify-center gap-2 ${
|
||||
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-4 w-4" />
|
||||
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')}
|
||||
>
|
||||
Table
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
|
||||
onClick={() => setViewMode('cards')}
|
||||
>
|
||||
Cards
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Restriction Alert Example */}
|
||||
{sortedParts.some(part => part.restrictions?.nfa) && (
|
||||
<div className="mb-6">
|
||||
<RestrictionAlert
|
||||
type="warning"
|
||||
title="NFA Items Detected"
|
||||
message="Some items in your search require National Firearms Act registration. Please ensure compliance with all federal and state regulations."
|
||||
icon="🔒"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table View */}
|
||||
{viewMode === 'table' && (
|
||||
<div className="bg-white dark:bg-neutral-800 shadow-sm rounded-lg overflow-hidden border border-neutral-200 dark:border-neutral-700">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
<thead className="bg-neutral-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<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('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-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||
Brand
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||
Description
|
||||
</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">
|
||||
<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-medium text-neutral-900 dark:text-white">
|
||||
{part.name}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{part.brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-neutral-900 dark:text-white">
|
||||
{part.brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
{part.description}
|
||||
</div>
|
||||
</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">
|
||||
<Link href={`/products/${part.id}`} legacyBehavior>
|
||||
<a className="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</a>
|
||||
</Link>
|
||||
</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} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
640
src/app/parts/page.tsx
Normal file
640
src/app/parts/page.tsx
Normal file
@@ -0,0 +1,640 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
import ProductCard from '@/components/ProductCard';
|
||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import Link from 'next/link';
|
||||
import { mockProducts } from '@/mock/product';
|
||||
import type { Product } from '@/mock/product';
|
||||
|
||||
// Extract unique values for dropdowns
|
||||
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
||||
const brands = ['All', ...Array.from(new Set(mockProducts.map(part => part.brand.name)))];
|
||||
const vendors = ['All', ...Array.from(new Set(mockProducts.flatMap(part => part.offers.map(offer => offer.vendor.name))))];
|
||||
|
||||
// Restrictions for filter dropdown
|
||||
const restrictionOptions = [
|
||||
'',
|
||||
'NFA',
|
||||
'SBR',
|
||||
'SUPPRESSOR',
|
||||
'STATE_RESTRICTIONS',
|
||||
];
|
||||
|
||||
type SortField = 'name' | 'category' | 'price';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
// 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-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||
<span className="block truncate text-neutral-900 dark:text-white">
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-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-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<Listbox.Options>
|
||||
{options.map((option, optionIdx) => (
|
||||
<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-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<SortField>('name');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
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-4">
|
||||
{/* Search Row */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="w-1/2">
|
||||
<SearchInput
|
||||
label="Search"
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
placeholder="Search parts..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
{/* Category Dropdown */}
|
||||
<Dropdown
|
||||
label="Category"
|
||||
value={selectedCategory}
|
||||
onChange={setSelectedCategory}
|
||||
options={categories}
|
||||
placeholder="All categories"
|
||||
/>
|
||||
|
||||
{/* Brand Dropdown */}
|
||||
<Dropdown
|
||||
label="Brand"
|
||||
value={selectedBrand}
|
||||
onChange={setSelectedBrand}
|
||||
options={brands}
|
||||
placeholder="All brands"
|
||||
/>
|
||||
|
||||
{/* Vendor Dropdown */}
|
||||
<Dropdown
|
||||
label="Vendor"
|
||||
value={selectedVendor}
|
||||
onChange={setSelectedVendor}
|
||||
options={vendors}
|
||||
placeholder="All vendors"
|
||||
/>
|
||||
|
||||
{/* Price Range */}
|
||||
<div className="relative">
|
||||
<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-2 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-neutral-300 dark:ring-neutral-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
|
||||
<span className="block truncate text-neutral-900 dark:text-white">
|
||||
{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-5 w-5 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-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-neutral-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<Listbox.Options>
|
||||
{[
|
||||
{ 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-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
{/* Restriction Filter */}
|
||||
<Dropdown
|
||||
label="Restriction"
|
||||
value={selectedRestriction}
|
||||
onChange={setSelectedRestriction}
|
||||
options={restrictionOptions}
|
||||
placeholder="All restrictions"
|
||||
/>
|
||||
|
||||
{/* Clear Filters */}
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
disabled={!hasActiveFilters}
|
||||
className={`w-full px-4 py-2 rounded-lg transition-colors flex items-center justify-center gap-2 ${
|
||||
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-4 w-4" />
|
||||
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')}
|
||||
>
|
||||
Table
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
|
||||
onClick={() => setViewMode('cards')}
|
||||
>
|
||||
Cards
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Restriction Alert Example */}
|
||||
{sortedParts.some(part => part.restrictions?.nfa) && (
|
||||
<div className="mb-6">
|
||||
<RestrictionAlert
|
||||
type="warning"
|
||||
title="NFA Items Detected"
|
||||
message="Some items in your search require National Firearms Act registration. Please ensure compliance with all federal and state regulations."
|
||||
icon="🔒"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table View */}
|
||||
{viewMode === 'table' && (
|
||||
<div className="bg-white dark:bg-neutral-800 shadow-sm rounded-lg overflow-hidden border border-neutral-200 dark:border-neutral-700">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
<thead className="bg-neutral-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<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('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-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||
Brand
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||
Description
|
||||
</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">
|
||||
<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-medium text-neutral-900 dark:text-white">
|
||||
{part.name}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{part.brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-neutral-900 dark:text-white">
|
||||
{part.brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
{part.description}
|
||||
</div>
|
||||
</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">
|
||||
<Link href={`/products/${part.id}`} legacyBehavior>
|
||||
<a className="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</a>
|
||||
</Link>
|
||||
</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} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
40
src/components/BetaTester.tsx
Normal file
40
src/components/BetaTester.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
export default function BetaTester() {
|
||||
return (
|
||||
<div className="bg-gray-900 py-16 sm:py-24 lg:py-32">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 px-6 lg:grid-cols-12 lg:gap-8 lg:px-8">
|
||||
<h2 className="max-w-xl text-balance text-3xl font-semibold tracking-tight text-white sm:text-4xl lg:col-span-7">
|
||||
Interested in being a beta tester? Join the beta tester list.
|
||||
</h2>
|
||||
<form className="w-full max-w-md lg:col-span-5 lg:pt-2">
|
||||
<div className="flex gap-x-4">
|
||||
<label htmlFor="email-address" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email-address"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
className="min-w-0 flex-auto rounded-md bg-white/5 px-3.5 py-2 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-none rounded-md bg-primary px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||
>
|
||||
Join The List
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-4 text-sm/6 text-gray-300">
|
||||
We care about your data. Read our{' '}
|
||||
<a href="#" className="font-semibold text-white">
|
||||
privacy policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,42 +3,38 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import ThemeSwitcher from './ThemeSwitcher';
|
||||
import { MagnifyingGlassIcon, UserCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function Navbar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Parts Catalog' },
|
||||
{ href: '/parts', label: 'Parts Catalog' },
|
||||
{ href: '/build', label: 'Build Checklist' },
|
||||
{ href: '/builds', label: 'My Builds' },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700 shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-primary-600 dark:bg-primary-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">🔫</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-neutral-900 dark:text-white">
|
||||
Pew Builder
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<>
|
||||
{/* Top Bar */}
|
||||
<div className="w-full bg-[#4B6516] text-white h-10 flex items-center justify-between px-4 sm:px-8">
|
||||
<span className="font-bold text-lg tracking-tight">Pew Builder</span>
|
||||
<UserCircleIcon className="h-7 w-7 text-white opacity-80" />
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{/* Subnav */}
|
||||
<nav 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 flex justify-between items-center h-14">
|
||||
{/* Left: Nav Links */}
|
||||
<div className="flex items-center space-x-6">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
className={`px-2 py-1 rounded-md text-sm font-medium transition-colors ${
|
||||
pathname === item.href
|
||||
? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300'
|
||||
: 'text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-700'
|
||||
? 'text-primary font-semibold underline underline-offset-4'
|
||||
: 'text-neutral-700 dark:text-neutral-200 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
@@ -46,41 +42,18 @@ export default function Navbar() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Theme Switcher */}
|
||||
{/* Right: Sign In + Search */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeSwitcher />
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button className="md:hidden p-2 rounded-md text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-700">
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Link href="/signin" className="btn btn-sm btn-ghost text-sm font-medium">
|
||||
Sign In
|
||||
</Link>
|
||||
<button className="p-2 rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-neutral-700 dark:text-neutral-200" />
|
||||
</button>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,20 +10,6 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Primary brand colors
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
950: '#082f49',
|
||||
},
|
||||
// Secondary accent colors
|
||||
accent: {
|
||||
50: '#fef2f2',
|
||||
@@ -105,50 +91,30 @@ module.exports = {
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
"primary": "#0ea5e9",
|
||||
"primary-content": "#ffffff",
|
||||
"secondary": "#ef4444",
|
||||
"secondary-content": "#ffffff",
|
||||
"accent": "#f59e0b",
|
||||
"accent-content": "#ffffff",
|
||||
"neutral": "#737373",
|
||||
"neutral-content": "#ffffff",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f5f5f5",
|
||||
"base-300": "#e5e5e5",
|
||||
"base-content": "#171717",
|
||||
"info": "#0ea5e9",
|
||||
"success": "#22c55e",
|
||||
"warning": "#f59e0b",
|
||||
"error": "#ef4444",
|
||||
},
|
||||
dark: {
|
||||
"primary": "#38bdf8",
|
||||
"primary-content": "#ffffff",
|
||||
"secondary": "#f87171",
|
||||
"secondary-content": "#ffffff",
|
||||
"accent": "#fbbf24",
|
||||
"accent-content": "#ffffff",
|
||||
"neutral": "#a3a3a3",
|
||||
"neutral-content": "#ffffff",
|
||||
"base-100": "#171717",
|
||||
"base-200": "#262626",
|
||||
"base-300": "#404040",
|
||||
"base-content": "#ffffff",
|
||||
"info": "#38bdf8",
|
||||
"success": "#4ade80",
|
||||
"warning": "#fbbf24",
|
||||
"error": "#f87171",
|
||||
pew: {
|
||||
primary: '#4B6516', // Olive/army green
|
||||
'primary-content': '#fff',
|
||||
accent: '#181C20', // Dark navy for CTA/footer
|
||||
'accent-content': '#fff',
|
||||
neutral: '#222',
|
||||
'base-100': '#fff',
|
||||
'base-200': '#f5f6fa',
|
||||
'base-300': '#e5e7eb',
|
||||
info: '#3ABFF8',
|
||||
success: '#36D399',
|
||||
warning: '#FBBD23',
|
||||
error: '#F87272',
|
||||
},
|
||||
},
|
||||
'dark',
|
||||
],
|
||||
darkTheme: "dark",
|
||||
base: true,
|
||||
styled: true,
|
||||
utils: true,
|
||||
prefix: "",
|
||||
logs: true,
|
||||
themeRoot: ":root",
|
||||
logs: false,
|
||||
rtl: false,
|
||||
prefix: '',
|
||||
// 'pew' is the default theme
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user