Neue Seite /team (Tab „Team", g t): horizontale Tages-Timeline mit einer Spur
pro Person. Zeit-Blöcke werden nach Start/Ende auf einer Stundenachse
positioniert, eingefärbt nach Kategorie; überlappende Blöcke landen auf
parallelen Sub-Spuren ("parallele Zeitstränge"). Laufende Blöcke (heute) ticken
live und sind rot umrandet. Datums-Navigation (Tag vor/zurück, Picker, „Heute"),
Hover-Tooltip mit Label/Zeitraum/Dauer, Kategorie-Legende.
Endpoint time.teamTimeline({date?}) liefert pro Person die Roh-Einträge
(Start/Ende/Kategorie/Label); Achse & Sub-Spuren rechnet das Frontend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
import { createBrowserRouter, RouterProvider } from 'react-router';
|
|
import { lazy, Suspense, type ComponentType } from 'react';
|
|
import { AppShell } from '../components/layout/AppShell';
|
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
|
import { Button } from '../components/ui/Button';
|
|
|
|
// Nach einem Deploy zeigen bereits offene Tabs auf alte, inzwischen gelöschte
|
|
// Chunk-Dateien (gehashte Dateinamen). Der nächste Lazy-Import schlägt dann fehl
|
|
// („Failed to fetch dynamically imported module"). Ein einmaliger Reload holt die
|
|
// frische index.html mit den neuen Hashes. sessionStorage-Guard verhindert eine
|
|
// Reload-Schleife, falls der Fehler eine andere Ursache hat (z. B. offline).
|
|
const RELOAD_KEY = 'ebw:chunk-reload';
|
|
|
|
function reloadOnceForStaleChunk(): boolean {
|
|
const last = Number(sessionStorage.getItem(RELOAD_KEY) ?? '0');
|
|
if (Date.now() - last > 10_000) {
|
|
sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
|
|
window.location.reload();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function lazyWithReload<T extends ComponentType>(factory: () => Promise<{ default: T }>) {
|
|
return lazy(() =>
|
|
factory().catch((err: unknown) => {
|
|
if (reloadOnceForStaleChunk()) {
|
|
// Hängt bis der Reload greift — vermeidet ein kurzes Aufblitzen des Fehlers.
|
|
return new Promise<{ default: T }>(() => {});
|
|
}
|
|
throw err;
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Vite feuert dieses Event bei fehlgeschlagenem modulepreload (gleiche Ursache).
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('vite:preloadError', (e) => {
|
|
if (reloadOnceForStaleChunk()) e.preventDefault();
|
|
});
|
|
}
|
|
|
|
const Dashboard = lazyWithReload(() =>
|
|
import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute })),
|
|
);
|
|
const Heute = lazyWithReload(() => import('../routes/heute').then((m) => ({ default: m.HeuteRoute })));
|
|
const Reflexion = lazyWithReload(() =>
|
|
import('../routes/reflexion').then((m) => ({ default: m.ReflexionRoute })),
|
|
);
|
|
const Bereich = lazyWithReload(() =>
|
|
import('../routes/bereich').then((m) => ({ default: m.BereichRoute })),
|
|
);
|
|
const Projekt = lazyWithReload(() =>
|
|
import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })),
|
|
);
|
|
const Leads = lazyWithReload(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute })));
|
|
const Lead = lazyWithReload(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute })));
|
|
const TeamView = lazyWithReload(() =>
|
|
import('../routes/team-view').then((m) => ({ default: m.TeamViewRoute })),
|
|
);
|
|
const Historie = lazyWithReload(() =>
|
|
import('../routes/historie').then((m) => ({ default: m.HistorieRoute })),
|
|
);
|
|
const Archiv = lazyWithReload(() =>
|
|
import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })),
|
|
);
|
|
const NotFound = lazyWithReload(() =>
|
|
import('../routes/not-found').then((m) => ({ default: m.NotFoundRoute })),
|
|
);
|
|
const Login = lazyWithReload(() => import('../routes/login').then((m) => ({ default: m.LoginRoute })));
|
|
|
|
function withSuspense(node: React.ReactNode) {
|
|
return (
|
|
<Suspense fallback={<SkeletonCard className="mx-auto mt-12 max-w-2xl" />}>{node}</Suspense>
|
|
);
|
|
}
|
|
|
|
function RouteError() {
|
|
return (
|
|
<div className="mx-auto mt-16 max-w-md text-center">
|
|
<h1 className="text-lg font-semibold">Etwas ist schiefgelaufen.</h1>
|
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
|
Wahrscheinlich wurde gerade eine neue Version veröffentlicht. Lade die Seite neu.
|
|
</p>
|
|
<Button className="mt-4" onClick={() => window.location.reload()}>
|
|
Neu laden
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const router = createBrowserRouter([
|
|
{
|
|
path: '/login',
|
|
element: withSuspense(<Login />),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/',
|
|
element: <AppShell />,
|
|
errorElement: <RouteError />,
|
|
children: [
|
|
{ index: true, element: withSuspense(<Dashboard />) },
|
|
{ path: 'dashboard', element: withSuspense(<Dashboard />) },
|
|
{ path: 'heute', element: withSuspense(<Heute />) },
|
|
{ path: 'reflexion', element: withSuspense(<Reflexion />) },
|
|
{ path: 'bereich', element: withSuspense(<Bereich />) },
|
|
{ path: 'bereich/:slug', element: withSuspense(<Bereich />) },
|
|
{ path: 'projekt/:id', element: withSuspense(<Projekt />) },
|
|
{ path: 'leads', element: withSuspense(<Leads />) },
|
|
{ path: 'lead/:id', element: withSuspense(<Lead />) },
|
|
{ path: 'team', element: withSuspense(<TeamView />) },
|
|
{ path: 'historie', element: withSuspense(<Historie />) },
|
|
{ path: 'archiv', element: withSuspense(<Archiv />) },
|
|
{ path: '*', element: withSuspense(<NotFound />) },
|
|
],
|
|
},
|
|
]);
|
|
|
|
export function Router() {
|
|
return <RouterProvider router={router} />;
|
|
}
|