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 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;
}

View File

@@ -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 = [
{

View File

@@ -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>

View File

@@ -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>

View File

@@ -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');
}
}, []);

View File

@@ -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 ${