Files
everythingbutwork/apps/web/src/lib/router.tsx
T
dejanandClaude Opus 4.8 516b35bf15
ci / quality (push) Failing after 33s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 1m17s
feat(team-view): horizontale Team-Timeline (ENTWICKLUN-8)
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>
2026-06-06 17:58:47 +02:00

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} />;
}