← back to Omega Watches 2
src/components/panels/WatchSelector.tsx
222 lines
'use client';
import { useState, useMemo } from 'react';
import { formatCurrency } from '@/lib/utils';
interface WatchRef {
id: string;
brand: string;
collection: string;
model_name: string;
reference_number: string;
calibre: string;
case_material: string;
case_diameter_mm: string;
dial_color: string;
year_introduced: number;
notes?: string;
event_count: string;
msrp_count: string;
latest_msrp: string | null;
avg_market_price: string | null;
latest_sale_price: string | null;
latest_sale_date: string | null;
}
interface WatchSelectorProps {
references: WatchRef[];
selected: WatchRef | null;
compareList: WatchRef[];
onSelect: (ref: WatchRef) => void;
onToggleCompare: (ref: WatchRef) => void;
}
const COLLECTION_ICONS: Record<string, string> = {
Speedmaster: 'S',
Seamaster: 'W',
Constellation: 'C',
'De Ville': 'D',
Vintage: 'V',
};
function Sparkline({ prices, color = '#c89b3c' }: { prices: number[]; color?: string }) {
if (prices.length < 2) return null;
const min = Math.min(...prices);
const max = Math.max(...prices);
const range = max - min || 1;
const w = 60;
const h = 20;
const points = prices.map((p, i) => {
const x = (i / (prices.length - 1)) * w;
const y = h - ((p - min) / range) * h;
return `${x},${y}`;
}).join(' ');
return (
<svg width={w} height={h} className="inline-block">
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default function WatchSelector({ references, selected, compareList, onSelect, onToggleCompare }: WatchSelectorProps) {
const [search, setSearch] = useState('');
const [expandedCollection, setExpandedCollection] = useState<string | null>(null);
const grouped = useMemo(() => {
const groups: Record<string, WatchRef[]> = {};
for (const ref of references) {
const col = ref.collection || 'Other';
if (!groups[col]) groups[col] = [];
groups[col].push(ref);
}
return groups;
}, [references]);
const filtered = useMemo(() => {
if (!search) return null;
const s = search.toLowerCase();
return references.filter(r =>
r.reference_number.toLowerCase().includes(s) ||
r.model_name.toLowerCase().includes(s) ||
r.collection.toLowerCase().includes(s) ||
r.dial_color?.toLowerCase().includes(s)
);
}, [search, references]);
const isCompared = (ref: WatchRef) => compareList.some(c => c.id === ref.id);
const renderRef = (ref: WatchRef) => {
const msrp = ref.latest_msrp ? parseFloat(ref.latest_msrp) : null;
const avg = ref.avg_market_price ? parseFloat(ref.avg_market_price) : null;
const premium = msrp && avg ? ((avg - msrp) / msrp) * 100 : null;
const isActive = selected?.id === ref.id;
return (
<div
key={ref.id}
className={`group cursor-pointer rounded-lg border p-2.5 transition-all ${
isActive
? 'border-omega-500/50 bg-omega-500/10'
: 'border-transparent hover:border-navy-600 hover:bg-navy-800/50'
}`}
onClick={() => onSelect(ref)}
>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="font-mono text-xs text-omega-400">{ref.reference_number}</span>
{isCompared(ref) && (
<span className="rounded bg-blue-500/20 px-1 text-[9px] font-medium text-blue-400">CMP</span>
)}
</div>
<div className="mt-0.5 truncate text-sm font-medium text-gray-200">{ref.model_name}</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-gray-500">
<span>{ref.case_diameter_mm}mm</span>
<span>{ref.dial_color}</span>
<span>{ref.event_count} events</span>
</div>
</div>
<div className="ml-2 text-right">
{avg && (
<div className="text-sm font-medium text-gray-200">{formatCurrency(avg)}</div>
)}
{premium !== null && (
<div className={`text-[11px] font-medium ${premium >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{premium >= 0 ? '+' : ''}{premium.toFixed(1)}%
</div>
)}
</div>
</div>
{/* Compare toggle on hover */}
<button
onClick={(e) => { e.stopPropagation(); onToggleCompare(ref); }}
className={`mt-1.5 w-full rounded py-0.5 text-[10px] font-medium transition-colors ${
isCompared(ref)
? 'bg-blue-500/20 text-blue-400 hover:bg-red-500/20 hover:text-red-400'
: 'bg-navy-700/50 text-gray-500 opacity-0 group-hover:opacity-100 hover:text-white'
}`}
>
{isCompared(ref) ? 'Remove from compare' : '+ Compare'}
</button>
</div>
);
};
return (
<div className="flex h-full flex-col rounded-xl border border-navy-700 bg-navy-800/30">
{/* Header */}
<div className="border-b border-navy-700 p-3">
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-400">
Watch Selector
</h3>
<input
type="text"
placeholder="Search models, refs..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full rounded-md border border-navy-600 bg-navy-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 outline-none focus:border-omega-500"
/>
{compareList.length > 0 && (
<div className="mt-2 text-[11px] text-blue-400">
Comparing {compareList.length} model{compareList.length > 1 ? 's' : ''}
</div>
)}
</div>
{/* Tree / search results */}
<div className="flex-1 overflow-y-auto p-2">
{filtered ? (
<div className="space-y-1">
{filtered.length === 0 ? (
<p className="py-4 text-center text-xs text-gray-500">No matches found</p>
) : (
filtered.map(renderRef)
)}
</div>
) : (
<div className="space-y-1">
{Object.entries(grouped).map(([collection, refs]) => (
<div key={collection}>
<button
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs font-medium text-gray-300 hover:bg-navy-800"
onClick={() => setExpandedCollection(expandedCollection === collection ? null : collection)}
>
<span className={`flex h-5 w-5 items-center justify-center rounded text-[10px] font-bold ${
expandedCollection === collection ? 'bg-omega-500 text-white' : 'bg-navy-700 text-gray-400'
}`}>
{COLLECTION_ICONS[collection] || collection[0]}
</span>
<span className="flex-1">{collection}</span>
<span className="text-[10px] text-gray-500">{refs.length}</span>
<span className="text-gray-500">{expandedCollection === collection ? '▾' : '▸'}</span>
</button>
{expandedCollection === collection && (
<div className="ml-2 mt-1 space-y-1 border-l border-navy-700 pl-2">
{refs.map(renderRef)}
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Summary footer */}
<div className="border-t border-navy-700 p-2.5 text-center">
<div className="text-[11px] text-gray-500">
{references.length} references across {Object.keys(grouped).length} collections
</div>
</div>
</div>
);
}