Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
165
components/calendar/CalendarCard.tsx
Normal file
165
components/calendar/CalendarCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user