Initial commit: Kalendartool (Next.js, Prisma, Docker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kühn
2026-10-08 14:38:04 +02:00
commit bc49c3074e
94 changed files with 12812 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
/**
* Karten-Darstellung eines Kalenders (gemeinsames Design).
*
* Wird vom Dashboard und von der Kalenderverwaltung verwendet, damit
* beide Stellen identische Karten ausgeben (Designvorlage: Farbverlauf,
* Titel unten links, Mitgliederzahl mit Gruppen-Icon unten rechts).
* Die ganze Karte ist ein Link; optional kann ein Bearbeiten-Button
* (Stift) eingeblendet werden.
*
* Bewusst ohne 'use client': Die Komponente ist rein darstellend und
* funktioniert sowohl in Server- als auch in Client-Komponenten. Der
* optionale onEdit-Handler wird nur von Client-Komponenten uebergeben.
*/
import Link from 'next/link';
/** Graue Verlaufspalette fuer die Karten (Designvorgabe: Grautoene). */
const CARD_GRADIENTS = [
'from-slate-600 to-slate-800',
'from-slate-500 to-slate-700',
'from-zinc-600 to-zinc-800',
'from-neutral-600 to-neutral-800',
'from-stone-600 to-stone-800',
'from-gray-600 to-gray-800',
] as const;
/**
* Liefert den Gradienten fuer den Karten-Index (zyklisch).
* Fallback auf den ersten Gradienten, falls die Palette leer ist
* (noUncheckedIndexedAccess-sicher).
*/
export function getCardGradient(index: number): string {
return CARD_GRADIENTS[index % CARD_GRADIENTS.length] ?? CARD_GRADIENTS[0]!;
}
/** Eigenschaften der Kalender-Karte. */
export interface CalendarCardProps {
title: string;
/** Kleine Zeile ueber dem Titel (z. B. Besitzer-E-Mail). */
subtitle?: string | null;
isActive: boolean;
memberCount: number;
/** Ziel des Karten-Links (die ganze Karte ist klickbar). */
href: string;
/** Accessibility-Beschriftung des Karten-Links. */
linkLabel: string;
/** Gradient-Klassen (siehe getCardGradient). */
gradient: string;
/** Optionales title-Attribut am Mitglieder-Icon (z. B. mit Reservierungen). */
iconTitle?: string;
/** Optional: Bearbeiten-Button (Stift) mit Handler. */
editLabel?: string;
onEdit?: () => void;
/** Optional: Karten-Klick oeffnet ein Modal statt dem Link. */
onOpen?: () => void;
}
/** Einzige Darstellungslogik fuer Kalender-Karten (DRY). */
export function CalendarCard({
title,
subtitle,
isActive,
memberCount,
href,
linkLabel,
gradient,
iconTitle,
editLabel,
onEdit,
onOpen,
}: CalendarCardProps) {
return (
<div
className={`relative flex h-36 flex-col justify-end overflow-hidden rounded-xl bg-gradient-to-br p-4 text-white shadow-sm transition-shadow hover:shadow-md ${gradient} ${
isActive ? '' : 'opacity-70'
}`}
>
{/* Karten-Klick: entweder Modal oeffnen oder zum Link-Ziel navigieren. */}
{onOpen ? (
<button
type="button"
onClick={onOpen}
className="absolute inset-0 rounded-xl text-left focus:outline-none focus:ring-2 focus:ring-slate-300"
aria-label={linkLabel}
/>
) : (
<Link
href={href}
className="absolute inset-0 rounded-xl focus:outline-none focus:ring-2 focus:ring-slate-300"
aria-label={linkLabel}
/>
)}
{!isActive && (
<span className="absolute left-4 top-3 rounded-full bg-slate-950/50 px-2 py-0.5 text-xs">
Inaktiv
</span>
)}
{onEdit && editLabel && (
<button
type="button"
onClick={onEdit}
aria-label={editLabel}
title="Bearbeiten"
className="absolute right-2 top-2 z-10 rounded-md p-2 text-white/80 transition-colors hover:bg-white/20 hover:text-white"
>
<PencilIcon />
</button>
)}
<div className="relative">
{subtitle && <p className="truncate text-xs text-white/70">{subtitle}</p>}
<div className="mt-1 flex items-end justify-between gap-2">
<h3 className="font-semibold leading-snug">{title}</h3>
<span
className="flex shrink-0 items-center gap-1 text-xs text-white/80"
title={iconTitle ?? `${memberCount} Mitglieder`}
>
<MembersIcon />
{memberCount}
</span>
</div>
</div>
</div>
);
}
/** Stift-Icon (2D, Outline-Stil). */
function PencilIcon() {
return (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M17 3a2.8 2.8 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5z" />
</svg>
);
}
/** Gruppen-Icon (2D, gefuellt) fuer die Mitgliederzahl auf der Karte. */
function MembersIcon() {
return (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<circle cx="12" cy="7.5" r="3" />
<circle cx="5" cy="9" r="2.3" />
<circle cx="19" cy="9" r="2.3" />
<path d="M12 12c-3.3 0-6 1.7-6 4v1.5h12V16c0-2.3-2.7-4-6-4z" />
<path d="M5 12.6c-2 .3-3.5 1.5-3.5 3.2v1.7h3.3V16c0-1.3.5-2.5 1.4-3.3-.4-.1-.8-.1-1.2-.1z" />
<path d="M19 12.6c2 .3 3.5 1.5 3.5 3.2v1.7h-3.3V16c0-1.3-.5-2.5-1.4-3.3.4-.1.8-.1 1.2-.1z" />
</svg>
);
}