feat(frontend): Oberfläche mit RRULE-Editor und Logo-Auswahl

- Vite, React 18, TypeScript und Tailwind mit dunklem Standard-Theme über
  CSS-Variablen, heller Modus umschaltbar und in localStorage gemerkt
- Anmeldung, erzwungener Passwortwechsel, Layout mit Seitenleiste
- API-Client mit stiller Token-Erneuerung, TanStack Query mit Fehler-Toasts
- Seiten Recurrences (inkl. Detail-Drawer), Transactions, Merchants, Settings
- Geführter RRULE-Editor mit Vorlagen, Expertenmodus, deutschem Klartext und
  Live-Vorschau der nächsten Termine vom preview-Endpunkt
- Firmen-Kachelgrid mit Logo, Markenfarbe und Logo-Auswahldialog samt Upload
- Beträge durchgängig in de-DE, Eingabe in beiden Schreibweisen
- 49 Tests, tsc --noEmit und eslint sauber, Produktionsbundle baut

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
moneyfy
2026-09-09 16:23:04 +02:00
co-authored by Claude Opus 5
parent 8edb69fe6a
commit 8d6fcfeb58
57 changed files with 13064 additions and 2 deletions
@@ -0,0 +1,51 @@
import { NavLink } from "react-router-dom";
import { Building2, type LucideIcon, Receipt, Repeat, Settings } from "lucide-react";
import { cn } from "@/lib/cn";
interface NavEintrag {
to: string;
label: string;
icon: LucideIcon;
}
const NAVIGATION: NavEintrag[] = [
{ to: "/recurrences", label: "Wiederkehrend", icon: Repeat },
{ to: "/transactions", label: "Buchungen", icon: Receipt },
{ to: "/merchants", label: "Firmen", icon: Building2 },
{ to: "/settings", label: "Einstellungen", icon: Settings },
];
export function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
return (
<nav aria-label="Hauptnavigation" className="flex h-full flex-col gap-1 p-3">
<div className="mb-4 flex items-center gap-2 px-2 py-1">
<span
aria-hidden
className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent text-sm font-bold text-accent-ink"
>
m
</span>
<span className="text-base font-semibold tracking-tight text-ink">moneyfy</span>
</div>
{NAVIGATION.map(({ to, label, icon: Icon }) => (
<NavLink
key={to}
to={to}
onClick={onNavigate}
className={({ isActive }) =>
cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition",
isActive ? "bg-accent/15 text-accent" : "text-muted hover:bg-raised hover:text-ink",
)
}
>
<Icon aria-hidden className="h-4 w-4 shrink-0" />
{label}
</NavLink>
))}
</nav>
);
}