'use client'; import * as React from 'react'; import Fuse from 'fuse.js'; import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; import type { ASCIIFont } from '@/types/ascii'; import { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites'; export interface FontSelectorProps { fonts: ASCIIFont[]; selectedFont: string; onSelectFont: (fontName: string) => void; onRandomFont?: () => void; className?: string; } type FilterType = 'all' | 'favorites' | 'recent'; const FILTERS: { value: FilterType; icon: React.ElementType; label: string }[] = [ { value: 'all', icon: List, label: 'All' }, { value: 'favorites', icon: Heart, label: 'Fav' }, { value: 'recent', icon: Clock, label: 'Recent' }, ]; export function FontSelector({ fonts, selectedFont, onSelectFont, onRandomFont, className, }: FontSelectorProps) { const [searchQuery, setSearchQuery] = React.useState(''); const [filter, setFilter] = React.useState('all'); const [favorites, setFavorites] = React.useState([]); const [recentFonts, setRecentFonts] = React.useState([]); const selectedRef = React.useRef(null); React.useEffect(() => { setFavorites(getFavorites()); setRecentFonts(getRecentFonts()); }, []); // Keep selected item in view when font changes externally (e.g. random) React.useEffect(() => { selectedRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }, [selectedFont]); const fuse = React.useMemo( () => new Fuse(fonts, { keys: ['name', 'fileName'], threshold: 0.3, includeScore: true }), [fonts] ); const filteredFonts = React.useMemo(() => { let base = fonts; if (filter === 'favorites') { base = fonts.filter((f) => favorites.includes(f.name)); } else if (filter === 'recent') { base = [...fonts.filter((f) => recentFonts.includes(f.name))].sort( (a, b) => recentFonts.indexOf(a.name) - recentFonts.indexOf(b.name) ); } if (!searchQuery) return base; const hits = fuse.search(searchQuery).map((r) => r.item); return filter === 'all' ? hits : hits.filter((f) => base.includes(f)); }, [fonts, searchQuery, fuse, filter, favorites, recentFonts]); const handleToggleFavorite = (fontName: string, e: React.MouseEvent) => { e.stopPropagation(); toggleFavorite(fontName); setFavorites(getFavorites()); }; const emptyMessage = filter === 'favorites' ? 'No favorites yet — click ♥ to save' : filter === 'recent' ? 'No recent fonts' : searchQuery ? 'No fonts match your search' : 'Loading fonts…'; return (
{/* ── Header ────────────────────────────────────────────── */}
Fonts
{fonts.length} {onRandomFont && ( )}
{/* ── Filter tabs ───────────────────────────────────────── */}
{FILTERS.map(({ value, icon: Icon, label }) => ( ))}
{/* ── Search ────────────────────────────────────────────── */}
setSearchQuery(e.target.value)} 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" /> {searchQuery && ( )}
{/* ── Font list ─────────────────────────────────────────── */}
{filteredFonts.length === 0 ? (

{emptyMessage}

) : ( filteredFonts.map((font) => { const isSelected = selectedFont === font.name; const fav = isFavorite(font.name); return (
); }) )}
{/* ── Footer ────────────────────────────────────────────── */}
{filteredFonts.length} font{filteredFonts.length !== 1 ? 's' : ''} {filter === 'favorites' && ( {favorites.length} saved )} {filter === 'recent' && ( {recentFonts.length} recent )}
); }