Files
fotospiel-app/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx
2025-11-24 17:17:39 +01:00

255 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
AlertTriangle,
Camera,
ClipboardList,
PlugZap,
QrCode,
Sparkles,
CalendarDays,
} from 'lucide-react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import type { TenantEvent, DashboardSummary } from '../../api';
import type { LimitWarning } from '../../lib/limitWarnings';
import { resolveEventDisplayName, formatEventDate, formatEventStatusLabel, resolveEngagementMode } from '../../lib/events';
type DashboardEventFocusCardProps = {
event: TenantEvent | null;
limitWarnings: LimitWarning[];
summary: DashboardSummary | null;
dateLocale: string;
onCreateEvent: () => void;
onOpenEvent: () => void;
onOpenPhotos: () => void;
onOpenInvites: () => void;
onOpenTasks: () => void;
onOpenPhotobooth: () => void;
};
export function DashboardEventFocusCard({
event,
limitWarnings,
summary,
dateLocale,
onCreateEvent,
onOpenEvent,
onOpenPhotos,
onOpenInvites,
onOpenTasks,
onOpenPhotobooth,
}: DashboardEventFocusCardProps) {
const { t } = useTranslation('dashboard', { keyPrefix: 'dashboard.eventFocus' });
const { t: tc } = useTranslation('common');
if (!event) {
return (
<Card className="border border-dashed border-rose-200/80 bg-white/80 shadow-sm shadow-rose-100/40 dark:border-white/20 dark:bg-white/5">
<CardHeader>
<div className="flex items-center gap-2 text-sm font-semibold text-rose-600 dark:text-rose-200">
<Sparkles className="h-4 w-4" />
{t('empty.eyebrow', 'Noch kein Event aktiv')}
</div>
<CardTitle className="text-lg text-slate-900 dark:text-white">
{t('empty.title', 'Leg mit deinem ersten Event los')}
</CardTitle>
<CardDescription className="text-sm text-slate-600 dark:text-slate-300">
{t('empty.description', 'Importiere ein Mission Pack, lege Branding fest und teile sofort den Gästelink.')}
</CardDescription>
</CardHeader>
<CardContent>
<Button className="rounded-full bg-brand-rose px-6 text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]" onClick={onCreateEvent}>
{t('empty.cta', 'Event anlegen')}
</Button>
</CardContent>
</Card>
);
}
const eventName = resolveEventDisplayName(event);
const dateLabel = formatEventDate(event.event_date, dateLocale) ?? t('noDate', 'Kein Datum gesetzt');
const statusLabel = formatEventStatusLabel(event.status ?? null, tc);
const isLive = Boolean(event.is_active || event.status === 'published');
const engagementMode = resolveEngagementMode(event);
const overviewStats = [
{
key: 'uploads',
label: t('stats.uploads', 'Uploads gesamt'),
value: Number(event.photo_count ?? 0).toLocaleString(),
},
{
key: 'likes',
label: t('stats.likes', 'Likes'),
value: Number(event.like_count ?? 0).toLocaleString(),
},
{
key: 'tasks',
label: t('stats.tasks', 'Aktive Aufgaben'),
value: Number(event.tasks_count ?? 0).toLocaleString(),
},
{
key: 'invites',
label: t('stats.invites', 'Einladungen live'),
value: Number(event.active_invites_count ?? event.total_invites_count ?? 0).toLocaleString(),
},
];
const quickActions = [
{
key: 'photos',
label: t('actions.photos', 'Uploads prüfen'),
description: t('actions.photosHint', 'Neueste Uploads ansehen und verstecken.'),
icon: Camera,
handler: onOpenPhotos,
},
{
key: 'invites',
label: t('actions.invites', 'QR & Einladungen'),
description: t('actions.invitesHint', 'Layouts exportieren oder Links kopieren.'),
icon: QrCode,
handler: onOpenInvites,
},
{
key: 'tasks',
label: t('actions.tasks', 'Mission Packs & Emotionen'),
description: t('actions.tasksHint', 'Kollektionen importieren und Emotionen aktivieren.'),
icon: ClipboardList,
handler: onOpenTasks,
},
{
key: 'photobooth',
label: t('actions.photobooth', 'Photobooth binden'),
description: t('actions.photoboothHint', 'FTP-Daten freigeben und Rate-Limit prüfen.'),
icon: PlugZap,
handler: onOpenPhotobooth,
},
];
const latestUploads = summary?.new_photos ?? 0;
return (
<div className="space-y-4">
<Card className="border border-slate-200 bg-white/90 shadow-lg shadow-rose-100/40 dark:border-white/10 dark:bg-white/5">
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2 text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
{t('eyebrow', 'Aktuelles Event')}
</div>
<CardTitle className="mt-2 text-2xl font-semibold text-slate-900 dark:text-white">
{eventName}
</CardTitle>
<CardDescription className="mt-1 text-sm text-slate-600 dark:text-slate-300">
{t('dateLabel', { defaultValue: 'Eventdatum: {{date}}', date: dateLabel })}
</CardDescription>
<div className="mt-3 flex flex-wrap items-center gap-2">
<Badge className={isLive ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-800'}>
{statusLabel}
</Badge>
<Badge variant="outline" className="text-xs font-semibold">
{isLive ? t('badges.live', 'Live für Gäste') : t('badges.hidden', 'Noch versteckt')}
</Badge>
{engagementMode === 'photo_only' ? (
<Badge variant="outline" className="text-xs font-semibold">
{t('badges.photoOnly', 'Nur Foto-Modus')}
</Badge>
) : (
<Badge variant="outline" className="text-xs font-semibold">
{t('badges.missionMode', 'Mission Cards aktiv')}
</Badge>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" className="rounded-full border-brand-rose-soft text-brand-rose hover:bg-brand-rose-soft/40" onClick={onOpenEvent}>
<CalendarDays className="mr-2 h-4 w-4" />
{t('viewEvent', 'Event öffnen')}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{overviewStats.map((stat) => (
<div key={stat.key} className="rounded-2xl border border-slate-200 bg-white/80 p-4 text-sm text-slate-600 dark:border-white/10 dark:bg-white/5">
<p className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-300">{stat.label}</p>
<p className="mt-2 text-2xl font-semibold text-slate-900 dark:text-white">{stat.value}</p>
</div>
))}
</div>
<div className="grid gap-3 lg:grid-cols-2">
{quickActions.map((action) => (
<button
key={action.key}
type="button"
onClick={action.handler}
className="flex items-start gap-3 rounded-2xl border border-slate-200 bg-white/90 p-4 text-left transition hover:border-rose-200 hover:bg-rose-50 dark:border-white/10 dark:bg-white/5"
>
<action.icon className="mt-1 h-5 w-5 text-rose-500 dark:text-rose-200" />
<div>
<p className="text-sm font-semibold text-slate-900 dark:text-white">{action.label}</p>
<p className="text-xs text-slate-500 dark:text-slate-300">{action.description}</p>
</div>
</button>
))}
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="rounded-2xl border border-slate-200 bg-sky-50 p-4 text-slate-800 shadow-inner shadow-sky-100 dark:border-white/10 dark:bg-white/10 dark:text-white">
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">{t('latestUploads.title', 'Neueste Uploads')}</p>
<p className="mt-2 text-3xl font-semibold">{latestUploads}</p>
<p className="text-xs text-slate-600 dark:text-slate-300">{t('latestUploads.hint', 'Gerade eingetroffen prüfe sie schnell.')}</p>
<Button size="sm" variant="secondary" className="mt-4" onClick={onOpenPhotos}>
{t('actions.photos', 'Uploads prüfen')}
</Button>
</div>
<div className="rounded-2xl border border-slate-200 bg-white/80 p-4 text-slate-800 shadow-inner shadow-slate-100 dark:border-white/10 dark:bg-white/5 dark:text-white">
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">{t('invitesCard.title', 'Galerie & Einladungen')}</p>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-200">
{t('invitesCard.description', 'Kopiere den Gästelink oder exportiere QR-Karten.')}
</p>
<div className="mt-3 flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={onOpenInvites}>
{t('invitesCard.cta', 'Links verwalten')}
</Button>
<Button size="sm" variant="ghost" onClick={onOpenPhotobooth}>
{t('invitesCard.secondaryCta', 'Photobooth öffnen')}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{limitWarnings.length > 0 && (
<div className="grid gap-3 md:grid-cols-2">
{limitWarnings.map((warning) => (
<Alert
key={warning.id}
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
className={warning.tone === 'warning' ? 'border-amber-400/50 bg-amber-50 text-amber-900 dark:border-amber-300/40 dark:bg-amber-500/10 dark:text-amber-100' : undefined}
>
<AlertTitle className="flex items-center gap-2 text-sm font-semibold">
<AlertTriangle className="h-4 w-4" />
{t(`limitWarnings.${warning.scope}`, {
defaultValue:
warning.scope === 'photos'
? 'Fotos'
: warning.scope === 'guests'
? 'Gäste'
: 'Galerie',
})}
</AlertTitle>
<AlertDescription className="text-sm">{warning.message}</AlertDescription>
</Alert>
))}
</div>
)}
</div>
);
}