import { useEffect, useMemo, useRef, useState } from 'react'; import { getPosts } from '../../lib/api'; import { type SiteMode, copy } from '../../lib/siteMode'; interface Post { slug: string; date: string; title?: string; excerpt?: string; tags: string[]; draft: boolean; reading_time: number; } function formatSlug(slug: string) { if (!slug) return ''; return slug .split('-') .map(w => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); } function formatDate(date: string) { return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); } export default function Search({ mode = 'atelier' }: { mode?: SiteMode }) { const c = copy(mode); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [posts, setPosts] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activeIdx, setActiveIdx] = useState(0); const [isMac, setIsMac] = useState(false); const inputRef = useRef(null); const listRef = useRef(null); useEffect(() => { setIsMac(/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)); }, []); // Global Cmd/Ctrl+K + Esc listener useEffect(() => { function onKey(e: KeyboardEvent) { const mod = isMac ? e.metaKey : e.ctrlKey; if (mod && e.key.toLowerCase() === 'k') { e.preventDefault(); setOpen(o => !o); } else if (e.key === 'Escape' && open) { setOpen(false); } } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, isMac]); // Lazy fetch posts on first open useEffect(() => { if (!open || posts !== null || loading) return; setLoading(true); getPosts() .then(p => setPosts(p as Post[])) .catch(e => setError(e instanceof Error ? e.message : 'Failed to load')) .finally(() => setLoading(false)); }, [open, posts, loading]); // Focus input on open useEffect(() => { if (open) { setQuery(''); setActiveIdx(0); requestAnimationFrame(() => inputRef.current?.focus()); } }, [open]); // Lock body scroll when open useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [open]); const results = useMemo(() => { if (!posts) return []; const q = query.trim().toLowerCase(); if (!q) return posts; return posts.filter(p => { const hay = `${p.title || formatSlug(p.slug)} ${p.excerpt || ''} ${p.tags?.join(' ') || ''}`.toLowerCase(); return hay.includes(q); }); }, [posts, query]); // Reset active index when results change useEffect(() => { setActiveIdx(0); }, [query, posts]); function navigate(slug: string) { window.location.href = `/posts/${encodeURIComponent(slug)}`; } function onInputKey(e: React.KeyboardEvent) { if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIdx(i => Math.min(i + 1, Math.max(0, results.length - 1))); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIdx(i => Math.max(i - 1, 0)); } else if (e.key === 'Enter' && results[activeIdx]) { e.preventDefault(); navigate(results[activeIdx].slug); } } // Scroll active result into view useEffect(() => { if (!open || !listRef.current) return; const el = listRef.current.children[activeIdx] as HTMLElement | undefined; el?.scrollIntoView({ block: 'nearest' }); }, [activeIdx, open]); return ( <> {open && (
setOpen(false)} > )} ); }