mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-05 18:46:45 -05:00
most working data fetch.
This commit is contained in:
@@ -1,3 +1,58 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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;
|
||||
}
|
||||
@@ -1,93 +1,30 @@
|
||||
// Auto-generated mapping from product feed categories (all_categories.csv)
|
||||
// to standardized builder component types (component_type.csv)
|
||||
// Refine as needed for your builder logic
|
||||
// Minimal, future-proof category mapping for builder logic
|
||||
export const categoryToComponentType: Record<string, string> = {
|
||||
"Sporting Bolt Action Centerfire Rifles": "N/A",
|
||||
"HANDGUN SIGHTS": "N/A",
|
||||
"Synthetic Holsters": "N/A",
|
||||
"Scope Mounts": "Accessories",
|
||||
"Short Barrel Shotguns": "N/A",
|
||||
"Polymer Centerfire Conceal Carry Pistols": "N/A",
|
||||
"LESS LETHAL AMMO": "N/A",
|
||||
"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"
|
||||
"Muzzle Devices": "Muzzle Device",
|
||||
"Receiver Parts": "Lower Receiver",
|
||||
"Barrel Parts": "Barrel",
|
||||
"Stock Parts": "Stock",
|
||||
"Bolt Parts": "Bolt Carrier Group",
|
||||
"Triggers Parts": "Trigger",
|
||||
"Sights": "Accessories"
|
||||
};
|
||||
|
||||
// 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)
|
||||
export const standardizedComponentTypes = [
|
||||
"Upper Receiver",
|
||||
@@ -110,17 +47,6 @@ export const standardizedComponentTypes = [
|
||||
"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
|
||||
export const builderCategories = [
|
||||
{
|
||||
|
||||
@@ -132,11 +132,11 @@ const Dropdown = ({
|
||||
<div className="relative">
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
<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}
|
||||
</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">
|
||||
<span className="block truncate text-zinc-900 dark:text-white">
|
||||
<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">
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<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"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-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>
|
||||
{options.map((option, optionIdx) => (
|
||||
@@ -159,7 +159,7 @@ const Dropdown = ({
|
||||
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-zinc-900 dark:text-white'
|
||||
active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
@@ -170,7 +170,7 @@ const Dropdown = ({
|
||||
{option.label}
|
||||
</span>
|
||||
{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" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -439,19 +439,19 @@ export default function Home() {
|
||||
}, [products]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-zinc-50 dark:bg-zinc-900">
|
||||
<main className="min-h-screen bg-zinc-50">
|
||||
{/* 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">
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">
|
||||
<h1 className="text-3xl font-bold text-zinc-900">
|
||||
Parts Catalog
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-zinc-600 dark:text-zinc-400 mt-2">
|
||||
<p className="text-zinc-600 mt-2">
|
||||
{selectedCategory && selectedCategoryId !== 'all'
|
||||
? `Showing ${selectedCategory.name} parts for your build`
|
||||
: 'Browse and filter firearm parts for your build'
|
||||
@@ -461,7 +461,7 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{/* Search Row */}
|
||||
<div className="mb-3 flex justify-end">
|
||||
@@ -481,7 +481,7 @@ export default function Home() {
|
||||
setIsSearchExpanded(false);
|
||||
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"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
@@ -490,7 +490,7 @@ export default function Home() {
|
||||
) : (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-5 w-5" />
|
||||
@@ -551,11 +551,11 @@ export default function Home() {
|
||||
<div className="col-span-1">
|
||||
<Listbox value={priceRange} onChange={setPriceRange}>
|
||||
<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
|
||||
</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">
|
||||
<span className="block truncate text-zinc-900 dark:text-white">
|
||||
<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">
|
||||
{priceRange === '' ? 'Select price range' :
|
||||
priceRange === 'under-100' ? 'Under $100' :
|
||||
priceRange === '100-300' ? '$100 - $300' :
|
||||
@@ -574,7 +574,7 @@ export default function Home() {
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-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>
|
||||
{[
|
||||
@@ -588,7 +588,7 @@ export default function Home() {
|
||||
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-zinc-900 dark:text-white'
|
||||
active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
@@ -599,7 +599,7 @@ export default function Home() {
|
||||
{option.label}
|
||||
</span>
|
||||
{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" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -631,8 +631,8 @@ export default function Home() {
|
||||
disabled={!hasActiveFilters}
|
||||
className={`w-full px-3 py-1.5 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm ${
|
||||
hasActiveFilters
|
||||
? 'bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 text-white'
|
||||
: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500 cursor-not-allowed'
|
||||
? 'bg-accent-600 hover:bg-accent-700 text-white'
|
||||
: 'bg-zinc-200 text-zinc-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
{/* View Toggle and Results Count */}
|
||||
<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`}
|
||||
{hasActiveFilters && !loading && (
|
||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||
<span className="ml-2 text-primary-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
@@ -659,7 +659,7 @@ export default function Home() {
|
||||
|
||||
{/* View Toggle */}
|
||||
<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">
|
||||
<button
|
||||
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
||||
@@ -681,19 +681,19 @@ export default function Home() {
|
||||
|
||||
{/* Table View */}
|
||||
{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">
|
||||
<table className="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<thead className="bg-zinc-50 dark:bg-zinc-700 sticky top-0 z-10 shadow-sm">
|
||||
<table className="min-w-full divide-y divide-zinc-200">
|
||||
<thead className="bg-zinc-50 sticky top-0 z-10 shadow-sm">
|
||||
<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
|
||||
</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
|
||||
</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')}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
@@ -701,31 +701,31 @@ export default function Home() {
|
||||
<span className="text-sm">{getSortIcon('price')}</span>
|
||||
</div>
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
</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) => (
|
||||
<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">
|
||||
<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" />
|
||||
</div>
|
||||
<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}
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
|
||||
<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}
|
||||
</span>
|
||||
</td>
|
||||
<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)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -783,12 +783,12 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* 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="text-sm text-zinc-700 dark:text-zinc-300">
|
||||
<div className="text-sm text-zinc-700">
|
||||
Showing {sortedParts.length} of {products.length} parts
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-2 text-primary-600 dark:text-primary-400">
|
||||
<span className="ml-2 text-primary-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
@@ -809,8 +809,8 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* Compact Restriction Legend */}
|
||||
<div className="mt-8 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<div className="flex items-center justify-center gap-4 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
<SessionProvider>
|
||||
<AuthProvider>
|
||||
<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 />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -15,14 +15,12 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>('light');
|
||||
|
||||
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 prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else if (prefersDark) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ export default function ThemeSwitcher() {
|
||||
return (
|
||||
<button
|
||||
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"
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
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. Default is light unless changed.`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
|
||||
|
||||
Reference in New Issue
Block a user