Gemessen statt vermutet: die Datenschicht war nie das Problem. resolvePermissions unter 1 ms, loadDashboard 11 ms, searchCases 5 ms, die Abteilungsabfragen 3–4 ms. Die Sekunden kamen aus zwei Ecken. **Fehlende Ladegrenzen.** Den meisten Routen fehlte loading.tsx - unter anderem der Übersicht, der Suche und dem gesamten Adminbereich. Ohne diese Datei hat der Router keine Grenze, wartet stumm auf die fertige Serverantwort, und bis dahin steht die alte Seite: man klickt, und scheinbar passiert nichts. Neunzehn Skelette nachgetragen, jede Route hat jetzt eine. Dazu quittiert der geklickte Link den Klick selbst, mit einem ruhig pulsierenden Punkt statt eines Kreisels. Regel 5 verbietet den Blockade-Spinner, der Inhalt ersetzt; eine Quittung im Link ersetzt nichts, und die eigentliche Antwort auf das Warten bleibt das Skelett. **Webpack.** Der Entwicklungsserver übersetzte jede Route beim ersten Aufruf neu: 8 s für die Übersicht, 2,8 s für die Fallliste. Mit Turbopack sind es 0,43 s beim ersten Aufruf und 0,1 s danach. Nur pnpm dev, der Produktionsbau bleibt unverändert. Gegengeprüft, dass die Anmeldeseite mit CSS und ohne Fehler im Log rendert. Nebenbei die eine Abfrage, die schlecht skalierte: die Übersicht zählte ungelesene Nachrichten mit einer Abfrage je Ort - bei hundert Akten hundert Hin- und Rückwege. Jetzt ein einziges groupBy mit der Lesemarke im ODER-Zweig des jeweiligen Ortes. Ein Test prüft, was dabei leicht verrutscht: die Marke gilt je Ort, und eigene Nachrichten zählen nie mit.
48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { Pending } from '@/components/Pending'
|
|
|
|
/**
|
|
* Eine Reiterleiste.
|
|
*
|
|
* Sie hebt hervor, wo man gerade ist. Vorher taten das weder die Reiter der
|
|
* Fallakte noch die der Verwaltung — fünf gleich aussehende Links, und man
|
|
* musste an der Seite selbst ablesen, welcher davon offen war.
|
|
*
|
|
* Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse:
|
|
* Suchparameter gehören zur Ansicht, nicht zum Reiter.
|
|
*/
|
|
export function Tabs({ tabs }: { tabs: { href: string; label: string }[] }) {
|
|
const pfad = usePathname()
|
|
|
|
return (
|
|
<nav className="mt-8 flex flex-wrap gap-2 border-b border-line pb-3">
|
|
{tabs.map((reiter) => {
|
|
// Der längste passende Eintrag gewinnt, damit „/admin" nicht jeden
|
|
// Unterbereich für sich beansprucht.
|
|
const passt = pfad === reiter.href || pfad.startsWith(reiter.href + '/')
|
|
const laenger = tabs.some(
|
|
(anderer) =>
|
|
anderer.href.length > reiter.href.length &&
|
|
(pfad === anderer.href || pfad.startsWith(anderer.href + '/')),
|
|
)
|
|
const aktiv = passt && !laenger
|
|
|
|
return (
|
|
<Link
|
|
key={reiter.href}
|
|
href={reiter.href}
|
|
aria-current={aktiv ? 'page' : undefined}
|
|
className="btn btn-sm"
|
|
>
|
|
{reiter.label}
|
|
<Pending />
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
)
|
|
}
|