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>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import { NavLink, useLocation } from 'react-router';
|
|
import { clsx } from 'clsx';
|
|
import { Flame } from 'lucide-react';
|
|
import { ToolCtrlLogo } from '../ui/ToolCtrlLogo';
|
|
import { TimerPill } from '../time/TimerPill';
|
|
import { voice } from '../../lib/voice';
|
|
import { useCurrentUser } from '../../hooks/useCurrentUser';
|
|
|
|
const TABS = [
|
|
{ to: '/dashboard', label: 'Dashboard', shortcut: 'g d', matchPrefix: '/dashboard' },
|
|
{ to: '/bereich', label: 'Bereich', shortcut: 'g b', matchPrefix: '/bereich' },
|
|
{ to: '/heute', label: 'Heute', shortcut: 'g h', matchPrefix: '/heute' },
|
|
{ to: '/team', label: 'Team', shortcut: 'g t', matchPrefix: '/team' },
|
|
{ to: '/historie', label: 'Historie', shortcut: 'g i', matchPrefix: '/historie' },
|
|
{ to: '/archiv', label: 'Archiv', shortcut: 'g a', matchPrefix: '/archiv' },
|
|
] as const;
|
|
|
|
function getInitials(name: string): string {
|
|
return name
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((p) => p[0]?.toUpperCase() ?? '')
|
|
.join('');
|
|
}
|
|
|
|
export function TopNav() {
|
|
const me = useCurrentUser();
|
|
const location = useLocation();
|
|
|
|
return (
|
|
<header
|
|
className="sticky top-0 z-30 bg-[var(--color-bg)] border-b border-[var(--color-border)]"
|
|
role="banner"
|
|
>
|
|
<nav
|
|
className="mx-auto flex h-14 max-w-[1280px] items-center gap-2 px-6"
|
|
aria-label="Hauptnavigation"
|
|
>
|
|
<NavLink to="/dashboard" className="flex items-center gap-1.5 font-bold text-[var(--color-ink)]">
|
|
<span>{voice.appName}</span>
|
|
<ToolCtrlLogo size={18} />
|
|
</NavLink>
|
|
|
|
<ul className="flex items-center gap-0.5 ml-6">
|
|
{TABS.map((tab) => {
|
|
const isActive = location.pathname.startsWith(tab.matchPrefix);
|
|
return (
|
|
<li key={tab.to}>
|
|
<NavLink
|
|
to={tab.to}
|
|
className={clsx(
|
|
'inline-flex h-9 items-center rounded-[var(--radius-md)] px-3 text-sm font-medium',
|
|
'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-black/5',
|
|
isActive &&
|
|
'text-[var(--color-ink)] border-b-2 border-[var(--color-ink)] rounded-b-none',
|
|
)}
|
|
aria-keyshortcuts={tab.shortcut}
|
|
>
|
|
{tab.label}
|
|
</NavLink>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
|
|
<div className="ml-auto flex items-center gap-3">
|
|
{me && <TimerPill />}
|
|
{me ? (
|
|
<>
|
|
<span
|
|
className={clsx(
|
|
'inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium',
|
|
me.streak > 0
|
|
? 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]'
|
|
: 'bg-black/5 text-[var(--color-ink-muted)]',
|
|
)}
|
|
title={`Aktuelle Update-Serie: ${me.streak} ${me.streak === 1 ? 'Tag' : 'Tage'}`}
|
|
>
|
|
<Flame size={12} fill={me.streak > 0 ? 'currentColor' : 'none'} />
|
|
<span className="font-mono tabular-nums">{me.streak}</span>
|
|
</span>
|
|
<div
|
|
className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--color-brand)] text-xs font-semibold text-[var(--color-brand-fg)]"
|
|
aria-label={me.name}
|
|
title={me.name}
|
|
>
|
|
{getInitials(me.name)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<a
|
|
href="/api/auth/login"
|
|
className="text-sm font-medium text-[var(--color-ink)] hover:underline"
|
|
>
|
|
Login
|
|
</a>
|
|
)}
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
);
|
|
}
|