Files
kit-ui/components/units/SearchUnits.tsx

141 lines
5.0 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { Search, X } from 'lucide-react';
import Fuse from 'fuse.js';
import {
getAllMeasures,
getUnitsForMeasure,
getUnitInfo,
formatMeasureName,
type Measure,
type UnitInfo,
} from '@/lib/units/units';
import { cn } from '@/lib/utils';
interface SearchResult {
unitInfo: UnitInfo;
measure: Measure;
}
interface SearchUnitsProps {
onSelectUnit: (unit: string, measure: Measure) => void;
className?: string;
}
export default function SearchUnits({ onSelectUnit, className }: SearchUnitsProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const searchIndex = useRef<Fuse<SearchResult> | null>(null);
useEffect(() => {
const allData: SearchResult[] = [];
const measures = getAllMeasures();
for (const measure of measures) {
for (const unit of getUnitsForMeasure(measure)) {
const unitInfo = getUnitInfo(unit);
if (unitInfo) allData.push({ unitInfo, measure });
}
}
searchIndex.current = new Fuse(allData, {
keys: [
{ name: 'unitInfo.abbr', weight: 2 },
{ name: 'unitInfo.singular', weight: 1.5 },
{ name: 'unitInfo.plural', weight: 1.5 },
{ name: 'measure', weight: 1 },
],
threshold: 0.3,
includeScore: true,
});
}, []);
useEffect(() => {
if (!query.trim() || !searchIndex.current) {
setResults([]);
setIsOpen(false);
return;
}
setResults(searchIndex.current.search(query).map((r) => r.item).slice(0, 10));
setIsOpen(true);
}, [query]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSelectUnit = (unit: string, measure: Measure) => {
onSelectUnit(unit, measure);
setQuery('');
setIsOpen(false);
inputRef.current?.blur();
};
return (
<div ref={containerRef} className={cn('relative w-full', className)}>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground/40 pointer-events-none" />
<input
ref={inputRef}
type="text"
placeholder="Search all units…"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query && setIsOpen(true)}
className="w-full bg-transparent border border-border/40 rounded-lg pl-8 pr-7 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors placeholder:text-muted-foreground/30"
/>
{query && (
<button
onClick={() => { setQuery(''); setIsOpen(false); }}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
>
<X className="w-3 h-3" />
</button>
)}
</div>
{isOpen && results.length > 0 && (
<div className="absolute z-50 w-full mt-1.5 bg-popover border border-border/60 rounded-xl shadow-xl max-h-72 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent">
{results.map((result, index) => (
<button
key={`${result.measure}-${result.unitInfo.abbr}`}
onClick={() => handleSelectUnit(result.unitInfo.abbr, result.measure)}
className={cn(
'w-full px-3 py-2.5 text-left hover:bg-primary/8 hover:text-foreground transition-colors',
'flex items-center justify-between gap-3',
index !== 0 && 'border-t border-border/20'
)}
>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium font-mono truncate">{result.unitInfo.plural}</div>
<div className="text-[10px] text-muted-foreground/50 flex items-center gap-1.5 mt-0.5">
<span className="font-mono">{result.unitInfo.abbr}</span>
<span>·</span>
<span>{formatMeasureName(result.measure)}</span>
</div>
</div>
<span className="text-[10px] text-muted-foreground/30 font-mono shrink-0">
{result.measure}
</span>
</button>
))}
</div>
)}
{isOpen && query && results.length === 0 && (
<div className="absolute z-50 w-full mt-1.5 bg-popover border border-border/60 rounded-xl p-4 text-center">
<p className="text-xs text-muted-foreground/40 font-mono italic">No units found for &quot;{query}&quot;</p>
</div>
)}
</div>
);
}