überarbeitung des event-admins fortgesetzt
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user