platz zu begrenzt im aufnahmemodus - vollbildmodus möglich? Menü und Kopfleiste ausblenden? Bild aus eigener galerie auswählen - Upload schlägt fehl (zu groß? evtl fehlende Rechte - aber browser hat rechte auf bilder und dateien!) hochgeladene bilder tauchen in der galerie nicht beim filter "Meine Bilder" auf - fotos werden auch nicht gezählt in den stats und achievements zeigen keinen fortschriftt. geteilte fotos: ruft man den Link auf, bekommt man die meldung "Link abgelaufen" der im startbildschirm gewählte name mit Umlauten (Sören) ist nach erneutem aufruf der pwa ohne umlaut (Sren). Aufgabenseite verbessert (Zwischenstand)
230 lines
9.1 KiB
TypeScript
230 lines
9.1 KiB
TypeScript
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 Aufgaben-Set, 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', 'QR-Codes 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,
|
|
disabled: !isLive,
|
|
},
|
|
{
|
|
key: 'invites',
|
|
label: t('actions.invites', 'QR-Codes'),
|
|
description: t('actions.invitesHint', 'Layouts exportieren oder Links kopieren.'),
|
|
icon: QrCode,
|
|
handler: onOpenInvites,
|
|
},
|
|
{
|
|
key: 'tasks',
|
|
label: t('actions.tasks', 'Aufgaben-Sets & 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,
|
|
},
|
|
];
|
|
|
|
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-2 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-3 text-xs text-slate-600 dark:border-white/10 dark:bg-white/5">
|
|
<p className="text-[11px] uppercase tracking-wide text-slate-500 dark:text-slate-300">{stat.label}</p>
|
|
<p className="mt-1 text-lg 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}
|
|
disabled={action.disabled}
|
|
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 disabled:cursor-not-allowed disabled:opacity-60 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>
|
|
</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>
|
|
);
|
|
}
|