most working data fetch.

This commit is contained in:
2025-06-30 13:34:27 -04:00
parent c3151f380b
commit 5c046874a8
7 changed files with 128 additions and 149 deletions

View File

@@ -1,3 +1,58 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @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-zinc-100 dark:bg-zinc-800;
}
::-webkit-scrollbar-thumb {
@apply bg-zinc-300 dark:bg-zinc-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-400 dark:bg-zinc-500;
}
/* Focus styles for better accessibility */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-zinc-900;
}
/* Card styles */
.card {
@apply bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200;
}
/* Button styles */
/* Removed custom .btn-primary to avoid DaisyUI conflict */
/* Input styles */
.input-field {
@apply w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors duration-200;
}
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.animate-float {
animation: float 4s ease-in-out infinite;
}

View File

@@ -1,93 +1,30 @@
// Auto-generated mapping from product feed categories (all_categories.csv) // Minimal, future-proof category mapping for builder logic
// to standardized builder component types (component_type.csv)
// Refine as needed for your builder logic
export const categoryToComponentType: Record<string, string> = { export const categoryToComponentType: Record<string, string> = {
"Sporting Bolt Action Centerfire Rifles": "N/A", "Muzzle Devices": "Muzzle Device",
"HANDGUN SIGHTS": "N/A", "Receiver Parts": "Lower Receiver",
"Synthetic Holsters": "N/A", "Barrel Parts": "Barrel",
"Scope Mounts": "Accessories", "Stock Parts": "Stock",
"Short Barrel Shotguns": "N/A", "Bolt Parts": "Bolt Carrier Group",
"Polymer Centerfire Conceal Carry Pistols": "N/A", "Triggers Parts": "Trigger",
"LESS LETHAL AMMO": "N/A", "Sights": "Accessories"
"LESS LETHAL PISTOL": "N/A",
"Rifle/Shotgun Combos": "N/A",
"Leveraction Shotguns": "N/A",
"Specialty Pistols": "N/A",
"Miscellaneous Accessories": "N/A",
"LESS LETHAL ACCESSORIES": "N/A",
"Tactical Rimfire Semi-Auto Rifles": "N/A",
"Sporting Semi-Auto Rimfire Rifles": "N/A",
"Lower Receivers": "Lower Receiver",
"Handgun Magazines": "Magazine",
"Non-Magnified Optic Mounts": "Accessories",
"Magnified Tactical Optics": "Accessories",
"BOLT ACTION SHOTGUN": "N/A",
"Sporting Semi-Auto Shotguns": "N/A",
"Metal Frame Centerfire Pistols": "N/A",
"THERMAL OPTICS": "Accessories",
"Sporting Leveraction Rimfire Rifles": "N/A",
"Pump Rimfire Rifles": "N/A",
"TACTICAL CENTERFIRE SEMI-AUTO PISTOLS": "N/A",
"Single Action Centerfire Revolvers": "N/A",
"Range Finders": "N/A",
"Metal Frame Rimfire Pistols": "N/A",
"Sporting Bolt Action Rimfire Rifles": "N/A",
"Side by Side Shotguns": "N/A",
"Lasers and Lights": "N/A",
"Tactical Pump Shotguns": "N/A",
"Binoculars": "N/A",
"Double Action Centerfire Revolvers": "N/A",
"Tactical Semi-Auto Shotguns": "N/A",
"Scopes": "Accessories",
"Silencer Mounts": "N/A",
"Double Action Centrfire Conceal Revolver": "N/A",
"Sporting Semi-Auto Centerfire Rifles": "N/A",
"FIRE CONTROL UNIT": "N/A",
"Spotting Scopes": "N/A",
"Single Shot Centerfire Rifles": "N/A",
"Derringers": "N/A",
"Pump Centerfire Rifles": "N/A",
"Double Action Rimfire Revolvers": "N/A",
"Tactical Centerfire Semi-Auto Rifles": "N/A",
"Handgun Accessories": "N/A",
"Sporting Leveraction Centerfire Rifles": "N/A",
"Single Shot Shotguns": "N/A",
"Polymer Rimfire Pistols": "N/A",
"LESS LETHAL RIFLE": "N/A",
"Short Barrel Rifles": "N/A",
"Black Powder Guns": "N/A",
"Over/Under Shotguns": "N/A",
"TACTICAL RIMFIRE SEMI-AUTO PISTOL": "N/A",
"Non-Magnified Optic Accessories": "N/A",
"Scope Accessories": "N/A",
"Scope Rings": "N/A",
"Rimfire Silencers": "N/A",
"Non-Magnified Optics": "N/A",
"Metal Frame Centerfire Conceal Pistols": "N/A",
"LONG GUN SIGHTS": "N/A",
"UPPER RECEIVERS": "Upper Receiver",
"Double Action Rimfire Conceal Revolvers": "N/A",
"Rifle Magazines": "Magazine",
"Rifle Accessories": "N/A",
"Silencer Pistons": "N/A",
"Shotgun Silencers": "N/A",
"Tactical Bolt Action Rifles": "N/A",
"Centerfire Ammo": "N/A",
"Single Action Rimfire Revolvers": "N/A",
"Leather Holsters": "N/A",
"AR Style Centerfire Rifles": "N/A",
"Centerfire Pistol Silencers": "N/A",
"Single Shot Rimfire Rifles": "N/A",
"Silencer Accessories": "N/A",
"Sporting Pump Shotguns": "N/A",
"Single Shot Handguns": "N/A",
"Centerfire Rifle Silencers": "N/A",
"Polymer Centerfire Pistols": "N/A",
"Magnified Tactical Optic Mounts": "N/A",
"SHOTGUN MAGAZINES": "Magazine",
"BLACK POWDER FIREARMS (ATF CONTROLLED)": "N/A"
}; };
// Map category to builder component type, with fallback heuristics
export function mapToBuilderType(category: string): string {
if (categoryToComponentType[category]) {
return categoryToComponentType[category];
}
// Fallback: guess based on keywords
if (category?.toLowerCase().includes('barrel')) return 'Barrel';
if (category?.toLowerCase().includes('stock')) return 'Stock';
if (category?.toLowerCase().includes('bolt')) return 'Bolt Carrier Group';
if (category?.toLowerCase().includes('trigger')) return 'Trigger';
if (category?.toLowerCase().includes('sight') || category?.toLowerCase().includes('optic')) return 'Accessories';
// Log for future mapping
console.warn('Unmapped category:', category);
return 'Accessories';
}
// List of standardized builder component types (from component_type.csv) // List of standardized builder component types (from component_type.csv)
export const standardizedComponentTypes = [ export const standardizedComponentTypes = [
"Upper Receiver", "Upper Receiver",
@@ -110,17 +47,6 @@ export const standardizedComponentTypes = [
"Accessories" "Accessories"
]; ];
// Hybrid mapping function: prefer subcategory, fallback to category
export function mapToBuilderType(category: string, subcategory: string): string {
if (standardizedComponentTypes.includes(subcategory)) {
return subcategory;
}
if (standardizedComponentTypes.includes(category)) {
return category;
}
return "N/A";
}
// Builder category hierarchy for filters // Builder category hierarchy for filters
export const builderCategories = [ export const builderCategories = [
{ {

View File

@@ -132,11 +132,11 @@ const Dropdown = ({
<div className="relative"> <div className="relative">
<Listbox value={value} onChange={onChange}> <Listbox value={value} onChange={onChange}>
<div className="relative"> <div className="relative">
<Listbox.Label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> <Listbox.Label className="block text-sm font-medium text-zinc-700 mb-1">
{label} {label}
</Listbox.Label> </Listbox.Label>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-zinc-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm"> <Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
<span className="block truncate text-zinc-900 dark:text-white"> <span className="block truncate text-zinc-900">
{value || placeholder} {value || placeholder}
</span> </span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
@@ -151,7 +151,7 @@ const Dropdown = ({
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-zinc-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
> >
<Listbox.Options> <Listbox.Options>
{options.map((option, optionIdx) => ( {options.map((option, optionIdx) => (
@@ -159,7 +159,7 @@ const Dropdown = ({
key={optionIdx} key={optionIdx}
className={({ active }) => className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${ `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-zinc-900 dark:text-white' active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
}` }`
} }
value={option.value} value={option.value}
@@ -170,7 +170,7 @@ const Dropdown = ({
{option.label} {option.label}
</span> </span>
{selected ? ( {selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400"> <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<CheckIcon className="h-4 w-4" aria-hidden="true" /> <CheckIcon className="h-4 w-4" aria-hidden="true" />
</span> </span>
) : null} ) : null}
@@ -439,19 +439,19 @@ export default function Home() {
}, [products]); }, [products]);
return ( return (
<main className="min-h-screen bg-zinc-50 dark:bg-zinc-900"> <main className="min-h-screen bg-zinc-50">
{/* Page Title */} {/* Page Title */}
<div className="bg-white dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700"> <div className="bg-white border-b border-zinc-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white"> <h1 className="text-3xl font-bold text-zinc-900">
Parts Catalog Parts Catalog
{selectedCategory && selectedCategoryId !== 'all' && ( {selectedCategory && selectedCategoryId !== 'all' && (
<span className="text-primary-600 dark:text-primary-400 ml-2 text-2xl"> <span className="text-primary-600 ml-2 text-2xl">
- {selectedCategory.name} - {selectedCategory.name}
</span> </span>
)} )}
</h1> </h1>
<p className="text-zinc-600 dark:text-zinc-400 mt-2"> <p className="text-zinc-600 mt-2">
{selectedCategory && selectedCategoryId !== 'all' {selectedCategory && selectedCategoryId !== 'all'
? `Showing ${selectedCategory.name} parts for your build` ? `Showing ${selectedCategory.name} parts for your build`
: 'Browse and filter firearm parts for your build' : 'Browse and filter firearm parts for your build'
@@ -461,7 +461,7 @@ export default function Home() {
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-white dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700"> <div className="bg-white border-b border-zinc-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
{/* Search Row */} {/* Search Row */}
<div className="mb-3 flex justify-end"> <div className="mb-3 flex justify-end">
@@ -481,7 +481,7 @@ export default function Home() {
setIsSearchExpanded(false); setIsSearchExpanded(false);
setSearchTerm(''); setSearchTerm('');
}} }}
className="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors flex-shrink-0" className="p-2 text-zinc-500 hover:text-zinc-700 transition-colors flex-shrink-0"
aria-label="Close search" aria-label="Close search"
> >
<XMarkIcon className="h-5 w-5" /> <XMarkIcon className="h-5 w-5" />
@@ -490,7 +490,7 @@ export default function Home() {
) : ( ) : (
<button <button
onClick={() => setIsSearchExpanded(true)} onClick={() => setIsSearchExpanded(true)}
className="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700" className="p-2 text-zinc-500 hover:text-zinc-700 transition-colors rounded-lg hover:bg-zinc-100"
aria-label="Open search" aria-label="Open search"
> >
<MagnifyingGlassIcon className="h-5 w-5" /> <MagnifyingGlassIcon className="h-5 w-5" />
@@ -551,11 +551,11 @@ export default function Home() {
<div className="col-span-1"> <div className="col-span-1">
<Listbox value={priceRange} onChange={setPriceRange}> <Listbox value={priceRange} onChange={setPriceRange}>
<div className="relative"> <div className="relative">
<Listbox.Label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> <Listbox.Label className="block text-sm font-medium text-zinc-700 mb-1">
Price Range Price Range
</Listbox.Label> </Listbox.Label>
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white dark:bg-zinc-800 py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm"> <Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-1.5 pl-3 pr-10 text-left shadow-sm ring-1 ring-inset ring-zinc-300 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm">
<span className="block truncate text-zinc-900 dark:text-white"> <span className="block truncate text-zinc-900">
{priceRange === '' ? 'Select price range' : {priceRange === '' ? 'Select price range' :
priceRange === 'under-100' ? 'Under $100' : priceRange === 'under-100' ? 'Under $100' :
priceRange === '100-300' ? '$100 - $300' : priceRange === '100-300' ? '$100 - $300' :
@@ -574,7 +574,7 @@ export default function Home() {
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-zinc-800 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
> >
<Listbox.Options> <Listbox.Options>
{[ {[
@@ -588,7 +588,7 @@ export default function Home() {
key={optionIdx} key={optionIdx}
className={({ active }) => className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${ `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-zinc-900 dark:text-white' active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
}` }`
} }
value={option.value} value={option.value}
@@ -599,7 +599,7 @@ export default function Home() {
{option.label} {option.label}
</span> </span>
{selected ? ( {selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600 dark:text-primary-400"> <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<CheckIcon className="h-4 w-4" aria-hidden="true" /> <CheckIcon className="h-4 w-4" aria-hidden="true" />
</span> </span>
) : null} ) : null}
@@ -631,8 +631,8 @@ export default function Home() {
disabled={!hasActiveFilters} disabled={!hasActiveFilters}
className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${ className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${
hasActiveFilters hasActiveFilters
? 'bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 text-white' ? 'bg-accent-600 hover:bg-accent-700 text-white'
: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500 cursor-not-allowed' : 'bg-zinc-200 text-zinc-400 cursor-not-allowed'
}`} }`}
> >
<XMarkIcon className="h-3.5 w-3.5" /> <XMarkIcon className="h-3.5 w-3.5" />
@@ -647,10 +647,10 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* View Toggle and Results Count */} {/* View Toggle and Results Count */}
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<div className="text-sm text-zinc-700 dark:text-zinc-300"> <div className="text-sm text-zinc-700">
{loading ? 'Loading...' : `Showing ${sortedParts.length} of ${products.length} parts`} {loading ? 'Loading...' : `Showing ${sortedParts.length} of ${products.length} parts`}
{hasActiveFilters && !loading && ( {hasActiveFilters && !loading && (
<span className="ml-2 text-primary-600 dark:text-primary-400"> <span className="ml-2 text-primary-600">
(filtered) (filtered)
</span> </span>
)} )}
@@ -659,7 +659,7 @@ export default function Home() {
{/* View Toggle */} {/* View Toggle */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-zinc-600 dark:text-zinc-400">View:</span> <span className="text-sm text-zinc-600">View:</span>
<div className="btn-group"> <div className="btn-group">
<button <button
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`} className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
@@ -681,19 +681,19 @@ export default function Home() {
{/* Table View */} {/* Table View */}
{viewMode === 'table' && ( {viewMode === 'table' && (
<div className="bg-white dark:bg-zinc-800 shadow-sm rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-700"> <div className="bg-white shadow-sm rounded-lg overflow-hidden border border-zinc-200">
<div className="overflow-x-auto max-h-screen overflow-y-auto"> <div className="overflow-x-auto max-h-screen overflow-y-auto">
<table className="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700"> <table className="min-w-full divide-y divide-zinc-200">
<thead className="bg-zinc-50 dark:bg-zinc-700 sticky top-0 z-10 shadow-sm"> <thead className="bg-zinc-50 sticky top-0 z-10 shadow-sm">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">
Product Product
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">
Category Category
</th> </th>
<th <th
className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-600" className="px-6 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider cursor-pointer hover:bg-zinc-100"
onClick={() => handleSort('price')} onClick={() => handleSort('price')}
> >
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
@@ -701,31 +701,31 @@ export default function Home() {
<span className="text-sm">{getSortIcon('price')}</span> <span className="text-sm">{getSortIcon('price')}</span>
</div> </div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-300 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">
Actions Actions
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700"> <tbody className="bg-white divide-y divide-zinc-200">
{sortedParts.map((part) => ( {sortedParts.map((part) => (
<tr key={part.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"> <tr key={part.id} className="hover:bg-zinc-50 transition-colors">
<td className="px-0 py-2 flex items-center gap-2 align-top"> <td className="px-0 py-2 flex items-center gap-2 align-top">
<div className="w-12 h-12 flex-shrink-0 rounded bg-zinc-100 dark:bg-zinc-700 overflow-hidden flex items-center justify-center border border-zinc-200 dark:border-zinc-700"> <div className="w-12 h-12 flex-shrink-0 rounded bg-zinc-100 overflow-hidden flex items-center justify-center border border-zinc-200">
<Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" /> <Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" />
</div> </div>
<div className="max-w-md break-words whitespace-normal"> <div className="max-w-md break-words whitespace-normal">
<Link href={`/products/${part.slug}`} className="text-sm font-semibold text-primary hover:underline dark:text-primary-400"> <Link href={`/products/${part.slug}`} className="text-sm font-semibold text-primary hover:underline">
{part.name} {part.name}
</Link> </Link>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <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"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
{part.category.name} {part.category.name}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-semibold text-zinc-900 dark:text-white"> <div className="text-sm font-semibold text-zinc-900">
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)} ${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
</div> </div>
</td> </td>
@@ -783,12 +783,12 @@ export default function Home() {
</div> </div>
{/* Table Footer */} {/* Table Footer */}
<div className="bg-zinc-50 dark:bg-zinc-700 px-6 py-3 border-t border-zinc-200 dark:border-zinc-600"> <div className="bg-zinc-50 px-6 py-3 border-t border-zinc-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm text-zinc-700 dark:text-zinc-300"> <div className="text-sm text-zinc-700">
Showing {sortedParts.length} of {products.length} parts Showing {sortedParts.length} of {products.length} parts
{hasActiveFilters && ( {hasActiveFilters && (
<span className="ml-2 text-primary-600 dark:text-primary-400"> <span className="ml-2 text-primary-600">
(filtered) (filtered)
</span> </span>
)} )}
@@ -809,8 +809,8 @@ export default function Home() {
</div> </div>
{/* Compact Restriction Legend */} {/* Compact Restriction Legend */}
<div className="mt-8 pt-4 border-t border-zinc-200 dark:border-zinc-700"> <div className="mt-8 pt-4 border-t border-zinc-200">
<div className="flex items-center justify-center gap-4 text-xs text-zinc-500 dark:text-zinc-400"> <div className="flex items-center justify-center gap-4 text-xs text-zinc-500">
<span className="font-medium">Restrictions:</span> <span className="font-medium">Restrictions:</span>
<div className="flex items-center gap-1"> <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> <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>

View File

@@ -10,7 +10,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
<SessionProvider> <SessionProvider>
<AuthProvider> <AuthProvider>
<ThemeProvider> <ThemeProvider>
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-900 transition-colors duration-200"> <div className="min-h-screen bg-zinc-50 transition-colors duration-200">
<NavigationWrapper /> <NavigationWrapper />
{children} {children}
</div> </div>

View File

@@ -15,14 +15,12 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light'); const [theme, setTheme] = useState<Theme>('light');
useEffect(() => { useEffect(() => {
// Check for saved theme preference or default to light mode // Only use saved theme preference, otherwise default to light
const savedTheme = localStorage.getItem('theme') as Theme; const savedTheme = localStorage.getItem('theme') as Theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) { if (savedTheme) {
setTheme(savedTheme); setTheme(savedTheme);
} else if (prefersDark) { } else {
setTheme('dark'); setTheme('light');
} }
}, []); }, []);

View File

@@ -9,8 +9,8 @@ export default function ThemeSwitcher() {
return ( return (
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode. Default is light unless changed.`}
> >
<span <span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${ className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${