'use client' import { useEffect, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { paletteText } from '@/lib/i18n/shell' import { stampColor } from '@/components/StampMark' type Treffer = { href: string; primary: string; secondary?: string; classification?: number } const SEITEN: Treffer[] = paletteText.pages.map((seite) => ({ href: seite.href, primary: seite.label, })) /** * Die Kommandopalette. * * Aktenzeichen eintippen und hinspringen, mehr will sie nicht. Die Treffer * kommen serverseitig gefiltert; was hier ankommt, darf der Nutzer sehen. * * Eine laufende Anfrage wird bei neuer Eingabe abgebrochen — sonst überholt * eine langsame Antwort eine schnellere und zeigt Treffer zu einem Begriff, * der nicht mehr im Feld steht. Siehe docs/09-performance-und-ux.md. */ export function CommandPalette({ onClose }: { onClose: () => void }) { const router = useRouter() const [text, setText] = useState('') const [treffer, setTreffer] = useState<{ label: string; eintraege: Treffer[] }[]>([]) const [auswahl, setAuswahl] = useState(0) const [laeuft, setLaeuft] = useState(false) const feld = useRef(null) useEffect(() => { feld.current?.focus() }, []) useEffect(() => { const begriff = text.trim() const seiten = SEITEN.filter((seite) => seite.primary.toLowerCase().includes(begriff.toLowerCase()), ) if (begriff.length < 2) { setTreffer([{ label: paletteText.sections.pages!, eintraege: SEITEN }]) setAuswahl(0) return } const controller = new AbortController() const timer = setTimeout(async () => { setLaeuft(true) try { const antwort = await fetch(`/api/search/quick?q=${encodeURIComponent(begriff)}`, { signal: controller.signal, }) const daten = (await antwort.json()) as { cases: { id: string; caseNumber: string; title: string; classification: number }[] personnel: { id: string; displayName: string; rank: string | null; classification: number }[] } const gruppen: { label: string; eintraege: Treffer[] }[] = [] if (daten.cases.length > 0) { gruppen.push({ label: paletteText.sections.cases!, eintraege: daten.cases.map((akte) => ({ href: `/faelle/${akte.caseNumber}`, primary: akte.caseNumber, secondary: akte.title, classification: akte.classification, })), }) } if (daten.personnel.length > 0) { gruppen.push({ label: paletteText.sections.personnel!, eintraege: daten.personnel.map((person) => ({ href: `/personalakten/${person.id}`, primary: person.displayName, secondary: person.rank ?? undefined, classification: person.classification, })), }) } if (seiten.length > 0) { gruppen.push({ label: paletteText.sections.pages!, eintraege: seiten }) } setTreffer(gruppen) setAuswahl(0) } catch { // Abgebrochen oder Netz weg: die alten Treffer bleiben stehen. } finally { setLaeuft(false) } }, 150) return () => { clearTimeout(timer) controller.abort() } }, [text]) const flach = treffer.flatMap((gruppe) => gruppe.eintraege) function springen(ziel: Treffer | undefined) { if (!ziel) return onClose() router.push(ziel.href) } return (
event.stopPropagation()} > setText(event.target.value)} onKeyDown={(event) => { if (event.key === 'Escape') onClose() if (event.key === 'ArrowDown') { event.preventDefault() setAuswahl((i) => Math.min(i + 1, flach.length - 1)) } if (event.key === 'ArrowUp') { event.preventDefault() setAuswahl((i) => Math.max(i - 1, 0)) } if (event.key === 'Enter') { event.preventDefault() springen(flach[auswahl]) } }} placeholder={paletteText.placeholder} aria-label={paletteText.placeholder} className="w-full border-b border-line bg-transparent px-4 py-3 text-sm outline-none" />
{flach.length === 0 ? (

{paletteText.empty}

) : ( treffer.map((gruppe) => (

{gruppe.label}

    {gruppe.eintraege.map((eintrag) => { const index = flach.indexOf(eintrag) return (
  • ) })}
)) )}
) }