überarbeitung des event-admins fortgesetzt

This commit is contained in:
Codex Agent
2025-11-25 13:03:42 +01:00
parent fd788ef770
commit 596dcbf18a
20 changed files with 998 additions and 2210 deletions

View File

@@ -5,16 +5,7 @@ import { AlertTriangle, ArrowRight, CalendarDays, Camera, Heart, Plus } from 'lu
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
TenantHeroCard,
FrostedSurface,
tenantHeroPrimaryButtonClass,
tenantHeroSecondaryButtonClass,
SectionCard,
SectionHeader,
StatCarousel,
ActionGrid,
} from '../components/tenant';
import { FrostedSurface, SectionCard } from '../components/tenant';
import { cn } from '@/lib/utils';
import { AdminLayout } from '../components/AdminLayout';
@@ -23,15 +14,12 @@ import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
import {
adminPath,
ADMIN_SETTINGS_PATH,
ADMIN_WELCOME_BASE_PATH,
ADMIN_EVENT_VIEW_PATH,
ADMIN_EVENT_EDIT_PATH,
ADMIN_EVENT_PHOTOS_PATH,
ADMIN_EVENT_MEMBERS_PATH,
ADMIN_EVENT_TASKS_PATH,
ADMIN_EVENT_INVITES_PATH,
ADMIN_EVENT_TOOLKIT_PATH,
ADMIN_EVENT_PHOTOBOOTH_PATH,
} from '../constants';
import { buildLimitWarnings } from '../lib/limitWarnings';
@@ -77,143 +65,8 @@ export default function EventsPage() {
() => rows.filter((event) => event.status === 'published').length,
[rows],
);
const nextEvent = React.useMemo(() => {
return (
rows
.filter((event) => event.event_date)
.slice()
.sort((a, b) => {
const dateA = a.event_date ? new Date(a.event_date).getTime() : Infinity;
const dateB = b.event_date ? new Date(b.event_date).getTime() : Infinity;
return dateA - dateB;
})[0] ?? null
);
}, [rows]);
const statItems = React.useMemo(
() => [
{
key: 'total',
label: t('events.list.stats.total', 'Events gesamt'),
value: totalEvents,
},
{
key: 'published',
label: t('events.list.stats.published', 'Veröffentlicht'),
value: publishedEvents,
},
{
key: 'drafts',
label: t('events.list.stats.drafts', 'Entwürfe'),
value: Math.max(0, totalEvents - publishedEvents),
},
nextEvent
? {
key: 'next',
label: t('events.list.stats.nextEvent', 'Nächstes Event'),
value: formatDate(nextEvent.event_date),
}
: null,
].filter(Boolean) as { key: string; label: string; value: string | number }[],
[t, totalEvents, publishedEvents, nextEvent],
);
const actionItems = React.useMemo(
() => [
{
key: 'new',
label: t('events.list.actions.create', 'Neues Event'),
description: t('events.list.actions.createDescription', 'Starte mit einem frischen Setup.'),
onClick: () => navigate(adminPath('/events/new')),
},
{
key: 'welcome',
label: t('events.list.actions.guidedSetup', 'Geführte Einrichtung'),
description: t('events.list.actions.guidedSetupDescription', 'Springe zurück in den Onboarding-Flow.'),
onClick: () => navigate(ADMIN_WELCOME_BASE_PATH),
},
{
key: 'settings',
label: t('events.list.actions.settings', 'Einstellungen öffnen'),
description: t('events.list.actions.settingsDescription', 'Passe Farben, Branding und Aufgabenpakete an.'),
onClick: () => navigate(ADMIN_SETTINGS_PATH),
},
],
[t, navigate],
);
const pageTitle = translateManagement('events.list.title', 'Deine Events');
const pageSubtitle = translateManagement(
'events.list.subtitle',
'Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen.'
);
const heroDescription = t(
'events.list.hero.description',
'Aktiviere Storytelling, Moderation und Galerie-Workflows für jeden Anlass in wenigen Minuten.'
);
const heroSummaryCopy = totalEvents > 0
? t('events.list.hero.summary', ':count Events aktiv verwaltet halte Aufgaben und Uploads im Blick.', { count: totalEvents })
: t('events.list.hero.summary_empty', 'Noch keine Events starte jetzt mit deinem ersten Konzept.');
const heroSecondaryCopy = t(
'events.list.hero.secondary',
'Erstelle Events im Admin, begleite Gäste live vor Ort und prüfe Kennzahlen im Marketing-Dashboard.'
);
const heroBadge = t('events.list.badge.dashboard', 'Tenant Dashboard');
const heroPrimaryAction = (
<Button
size="sm"
className={tenantHeroPrimaryButtonClass}
onClick={() => navigate(adminPath('/events/new'))}
>
{t('events.list.actions.create', 'Neues Event')}
</Button>
);
const heroSecondaryAction = (
<Button size="sm" className={tenantHeroSecondaryButtonClass} asChild>
<Link to={ADMIN_SETTINGS_PATH}>
{t('events.list.actions.settings', 'Einstellungen')}
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
);
const heroAside = (
<FrostedSurface className="w-full rounded-2xl border-slate-200 bg-white p-5 text-slate-900 shadow-lg shadow-pink-200/20 dark:border-white/20 dark:bg-white/10">
<div className="space-y-4">
<div>
<p className="text-xs uppercase tracking-wide text-slate-500">
{t('events.list.hero.published_label', 'Veröffentlichte Events')}
</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">{publishedEvents}</p>
<p className="text-xs text-slate-500">
{t('events.list.hero.total_label', ':count insgesamt', { count: totalEvents })}
</p>
</div>
{nextEvent ? (
<div className="rounded-xl border border-pink-100 bg-pink-50/70 p-4 text-slate-900 shadow-inner shadow-pink-200/50">
<p className="text-xs uppercase tracking-wide text-pink-600">
{t('events.list.hero.next_label', 'Nächstes Event')}
</p>
<p className="mt-1 text-sm font-semibold">{renderName(nextEvent.name)}</p>
<p className="text-xs text-slate-600">{formatDate(nextEvent.event_date)}</p>
{nextEvent.slug ? (
<Button
variant="ghost"
size="sm"
className="mt-3 h-8 justify-start px-3 text-pink-600 hover:bg-pink-100"
onClick={() => navigate(ADMIN_EVENT_VIEW_PATH(nextEvent.slug))}
>
{t('events.list.hero.open_event', 'Event öffnen')}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
) : null}
</div>
) : (
<p className="rounded-xl border border-dashed border-pink-200/70 bg-white/70 p-4 text-xs text-slate-600">
{t('events.list.hero.no_upcoming', 'Plane ein Datum, um hier die nächste Station zu sehen.')}
</p>
)}
</div>
</FrostedSurface>
);
const draftEvents = totalEvents - publishedEvents;
const [statusFilter, setStatusFilter] = React.useState<'all' | 'published' | 'draft'>('all');
const filteredRows = React.useMemo(() => {
@@ -225,21 +78,6 @@ export default function EventsPage() {
}
return rows;
}, [rows, statusFilter]);
const overviewDescription = React.useMemo(() => {
if (loading) {
return t('events.list.overview.loading', 'Wir sammeln gerade deine Event-Details …');
}
if (filteredRows.length === 0) {
if (statusFilter === 'published') {
return t('events.list.overview.empty_published', 'Noch keine veröffentlichten Events.');
}
if (statusFilter === 'draft') {
return t('events.list.overview.empty_drafts', 'Keine Entwürfe nutze die Zeit für dein nächstes Event.');
}
return t('events.list.overview.empty', 'Noch keine Events - starte jetzt und lege dein erstes Event an.');
}
return t('events.list.overview.count', '{{count}} Events aktiv verwaltet.', { count: filteredRows.length });
}, [filteredRows.length, loading, statusFilter, t]);
const filterOptions: Array<{ key: 'all' | 'published' | 'draft'; label: string; count: number }> = [
{ key: 'all', label: t('events.list.filters.all', 'Alle'), count: totalEvents },
{ key: 'published', label: t('events.list.filters.published', 'Live'), count: publishedEvents },
@@ -247,7 +85,7 @@ export default function EventsPage() {
];
return (
<AdminLayout title={pageTitle} subtitle={pageSubtitle}>
<AdminLayout title={pageTitle}>
{error && (
<Alert variant="destructive">
<AlertTitle>Fehler beim Laden</AlertTitle>
@@ -255,32 +93,7 @@ export default function EventsPage() {
</Alert>
)}
<TenantHeroCard
badge={heroBadge}
title={pageTitle}
description={heroDescription}
supporting={[heroSummaryCopy, heroSecondaryCopy]}
primaryAction={heroPrimaryAction}
secondaryAction={heroSecondaryAction}
aside={heroAside}
/>
<SectionCard className="space-y-4">
<SectionHeader
eyebrow={t('events.list.badge', 'Events')}
title={t('events.list.overview.title', 'Übersicht')}
description={overviewDescription}
endSlot={(
<button
type="button"
className="rounded-full border border-slate-200 bg-white px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-slate-800 shadow-inner shadow-white/20 dark:border-white/15 dark:bg-white/10 dark:text-white"
>
{t('events.list.badge.dashboard', 'Tenant Dashboard')}
</button>
)}
/>
<StatCarousel items={statItems} />
<ActionGrid items={actionItems} columns={1} />
<div className="flex gap-2 overflow-x-auto pb-1">
{filterOptions.map((option) => (
<button
@@ -370,7 +183,7 @@ function EventCard({
{ key: 'tasks', label: translate('events.list.actions.tasks', 'Tasks'), to: ADMIN_EVENT_TASKS_PATH(slug) },
{ key: 'invites', label: translate('events.list.actions.invites', 'QR-Einladungen'), to: ADMIN_EVENT_INVITES_PATH(slug) },
{ key: 'photobooth', label: translate('events.list.actions.photobooth', 'Photobooth'), to: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) },
{ key: 'toolkit', label: translate('events.list.actions.toolkit', 'Toolkit'), to: ADMIN_EVENT_TOOLKIT_PATH(slug) },
{ key: 'toolkit', label: translate('events.list.actions.toolkit', 'Toolkit'), to: ADMIN_EVENT_VIEW_PATH(slug) },
];
return (