165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
/**
|
|
* 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>
|
|
);
|
|
} |