rework of the event admin UI
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -19,12 +20,8 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import {
|
||||
TenantHeroCard,
|
||||
TenantOnboardingChecklistCard,
|
||||
FrostedSurface,
|
||||
tenantHeroPrimaryButtonClass,
|
||||
SectionCard,
|
||||
SectionHeader,
|
||||
StatCarousel,
|
||||
@@ -51,6 +48,7 @@ import {
|
||||
ADMIN_EVENT_PHOTOS_PATH,
|
||||
ADMIN_EVENT_INVITES_PATH,
|
||||
ADMIN_EVENT_TASKS_PATH,
|
||||
ADMIN_EVENT_PHOTOBOOTH_PATH,
|
||||
ADMIN_BILLING_PATH,
|
||||
ADMIN_SETTINGS_PATH,
|
||||
ADMIN_WELCOME_BASE_PATH,
|
||||
@@ -59,6 +57,7 @@ import {
|
||||
} from '../constants';
|
||||
import { useOnboardingProgress } from '../onboarding';
|
||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||
import { DashboardEventFocusCard } from '../components/dashboard/DashboardEventFocusCard';
|
||||
import type { LimitUsageSummary, GallerySummary } from '../lib/limitWarnings';
|
||||
|
||||
interface DashboardState {
|
||||
@@ -309,7 +308,7 @@ export default function DashboardPage() {
|
||||
},
|
||||
{
|
||||
key: 'newPhotos',
|
||||
label: translate('overview.stats.newPhotos'),
|
||||
label: translate('overview.stats.newPhotos', 'Neueste Uploads'),
|
||||
value: summary?.new_photos ?? 0,
|
||||
icon: <Camera className="h-4 w-4" />,
|
||||
},
|
||||
@@ -458,118 +457,6 @@ export default function DashboardPage() {
|
||||
);
|
||||
const onboardingFallbackCta = translate('onboarding.card.cta_fallback', 'Jetzt starten');
|
||||
|
||||
const heroBadge = singleEvent
|
||||
? translate('overview.eventHero.badge', 'Aktives Event')
|
||||
: translate('overview.title', 'Kurzer Überblick');
|
||||
|
||||
const heroDescription = singleEvent
|
||||
? translate('overview.eventHero.description', { defaultValue: 'Alles richtet sich nach {{event}}. Nächster Termin: {{date}}.', event: singleEventName ?? '', date: singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt') })
|
||||
: translate('overview.description', 'Wichtigste Kennzahlen deines Tenants auf einen Blick.');
|
||||
|
||||
const heroSupportingCopy = onboardingCompletion === 100 ? onboardingCompletedCopy : onboardingCardDescription;
|
||||
const heroSupporting = singleEvent
|
||||
? [
|
||||
translate('overview.eventHero.supporting.status', {
|
||||
defaultValue: 'Status: {{status}}',
|
||||
status: formatEventStatus(singleEvent.status ?? null, tc),
|
||||
}),
|
||||
singleEventDateLabel
|
||||
? translate('overview.eventHero.supporting.date', singleEventDateLabel ?? 'Noch kein Datum festgelegt.')
|
||||
: translate('overview.eventHero.noDate', 'Noch kein Datum festgelegt.'),
|
||||
].filter(Boolean)
|
||||
: [heroSupportingCopy];
|
||||
|
||||
const heroPrimaryAction = (() => {
|
||||
if (onboardingCompletion < 100) {
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
className={tenantHeroPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
if (readiness.hasEvent) {
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
} else {
|
||||
navigate(ADMIN_EVENT_CREATE_PATH);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{translate('onboarding.hero.cta', 'Setup fortsetzen')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleEvent?.slug) {
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
className={tenantHeroPrimaryButtonClass}
|
||||
onClick={() => navigate(ADMIN_EVENT_VIEW_PATH(singleEvent.slug))}
|
||||
>
|
||||
{translate('actions.openEvent', 'Event öffnen')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (readiness.hasEvent) {
|
||||
return (
|
||||
<Button size="sm" className={tenantHeroPrimaryButtonClass} onClick={() => navigate(ADMIN_EVENTS_PATH)}>
|
||||
{translate('quickActions.moderatePhotos.label', 'Fotos moderieren')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="sm" className={tenantHeroPrimaryButtonClass} onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)}>
|
||||
{translate('actions.newEvent')}
|
||||
</Button>
|
||||
);
|
||||
})();
|
||||
|
||||
const heroAside = onboardingCompletion < 100 ? (
|
||||
<FrostedSurface className="w-full rounded-2xl border-slate-200 bg-white p-5 text-slate-900 shadow-lg shadow-rose-300/20 dark:border-white/20 dark:bg-white/10">
|
||||
<div className="flex items-center justify-between text-sm font-medium text-slate-700">
|
||||
<span>{onboardingCardTitle}</span>
|
||||
<span>
|
||||
{completedOnboardingSteps}/{onboardingChecklist.length}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={onboardingCompletion} className="mt-4 h-2 bg-rose-100" />
|
||||
<p className="mt-3 text-xs text-slate-600">{onboardingCardDescription}</p>
|
||||
</FrostedSurface>
|
||||
) : singleEvent ? (
|
||||
<FrostedSurface className="w-full rounded-2xl border-slate-200 bg-white p-5 text-slate-900 shadow-lg shadow-rose-300/20 dark:border-white/20 dark:bg-white/10">
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.35em] text-rose-500">
|
||||
{translate('overview.eventHero.stats.title', 'Momentaufnahme')}
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{formatEventStatus(singleEvent.status ?? null, tc)}
|
||||
</p>
|
||||
</div>
|
||||
<dl className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-xs text-slate-500">{translate('overview.eventHero.stats.date', 'Eventdatum')}</dt>
|
||||
<dd className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{singleEventDateLabel ?? translate('overview.eventHero.noDate', 'Nicht gesetzt')}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-xs text-slate-500">{translate('overview.eventHero.stats.uploads', 'Uploads gesamt')}</dt>
|
||||
<dd className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{Number(singleEvent.photo_count ?? 0).toLocaleString(i18n.language)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-xs text-slate-500">{translate('overview.eventHero.stats.tasks', 'Offene Aufgaben')}</dt>
|
||||
<dd className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{Number(singleEvent.tasks_count ?? 0).toLocaleString(i18n.language)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</FrostedSurface>
|
||||
) : null;
|
||||
const readinessCompleteLabel = translate('readiness.complete', 'Erledigt');
|
||||
const readinessPendingLabel = translate('readiness.pending', 'Noch offen');
|
||||
const hasEventContext = readiness.hasEvent;
|
||||
@@ -610,27 +497,16 @@ export default function DashboardPage() {
|
||||
[translate, navigate, hasEventContext],
|
||||
);
|
||||
|
||||
const layoutActions = singleEvent ? (
|
||||
<Button
|
||||
className="rounded-full bg-brand-rose px-4 text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
onClick={() => {
|
||||
if (singleEvent.slug) {
|
||||
navigate(ADMIN_EVENT_VIEW_PATH(singleEvent.slug));
|
||||
} else {
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{translate('actions.openEvent', 'Event öffnen')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="rounded-full bg-brand-rose px-4 text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)]"
|
||||
onClick={() => navigate(ADMIN_EVENT_CREATE_PATH)}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> {translate('actions.newEvent')}
|
||||
</Button>
|
||||
const dashboardTabs = React.useMemo(
|
||||
() => [
|
||||
{ key: 'overview', label: translate('tabs.overview', 'Überblick'), href: `${ADMIN_HOME_PATH}#overview` },
|
||||
{ key: 'live', label: translate('tabs.live', 'Live'), href: `${ADMIN_HOME_PATH}#live` },
|
||||
{ key: 'setup', label: translate('tabs.setup', 'Vorbereitung'), href: `${ADMIN_HOME_PATH}#setup` },
|
||||
{ key: 'recap', label: translate('tabs.recap', 'Nachbereitung'), href: `${ADMIN_HOME_PATH}#recap` },
|
||||
],
|
||||
[translate]
|
||||
);
|
||||
const currentDashboardTab = React.useMemo(() => (location.hash?.replace('#', '') || 'overview'), [location.hash]);
|
||||
|
||||
const adminTitle = singleEventName ?? greetingTitle;
|
||||
const adminSubtitle = singleEvent
|
||||
@@ -640,22 +516,50 @@ export default function DashboardPage() {
|
||||
})
|
||||
: subtitle;
|
||||
|
||||
const heroTitle = adminTitle;
|
||||
const liveNowTitle = t('liveNow.title', { defaultValue: 'Während des Events' });
|
||||
const liveNowDescription = t('liveNow.description', {
|
||||
defaultValue: 'Direkter Zugriff, solange dein Event läuft.',
|
||||
count: liveEvents.length,
|
||||
});
|
||||
const liveActionLabels = React.useMemo(() => ({
|
||||
photos: t('liveNow.actions.photos', { defaultValue: 'Uploads' }),
|
||||
invites: t('liveNow.actions.invites', { defaultValue: 'QR & Einladungen' }),
|
||||
tasks: t('liveNow.actions.tasks', { defaultValue: 'Aufgaben' }),
|
||||
}), [t]);
|
||||
const liveStatusLabel = t('liveNow.status', { defaultValue: 'Live' });
|
||||
const liveNoDate = t('liveNow.noDate', { defaultValue: 'Kein Datum' });
|
||||
const focusActions = React.useMemo(
|
||||
() => ({
|
||||
createEvent: () => navigate(ADMIN_EVENT_CREATE_PATH),
|
||||
openEvent: () => {
|
||||
if (primaryEvent?.slug) {
|
||||
navigate(ADMIN_EVENT_VIEW_PATH(primaryEvent.slug));
|
||||
} else {
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
}
|
||||
},
|
||||
openPhotos: () => {
|
||||
if (primaryEventSlug) {
|
||||
navigate(ADMIN_EVENT_PHOTOS_PATH(primaryEventSlug));
|
||||
return;
|
||||
}
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
},
|
||||
openInvites: () => {
|
||||
if (primaryEventSlug) {
|
||||
navigate(ADMIN_EVENT_INVITES_PATH(primaryEventSlug));
|
||||
return;
|
||||
}
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
},
|
||||
openTasks: () => {
|
||||
if (primaryEventSlug) {
|
||||
navigate(ADMIN_EVENT_TASKS_PATH(primaryEventSlug));
|
||||
return;
|
||||
}
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
},
|
||||
openPhotobooth: () => {
|
||||
if (primaryEventSlug) {
|
||||
navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(primaryEventSlug));
|
||||
return;
|
||||
}
|
||||
navigate(ADMIN_EVENTS_PATH);
|
||||
},
|
||||
}),
|
||||
[navigate, primaryEvent, primaryEventSlug],
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminLayout title={adminTitle} subtitle={adminSubtitle} actions={layoutActions}>
|
||||
<AdminLayout title={adminTitle} subtitle={adminSubtitle} actions={layoutActions} tabs={dashboardTabs} currentTabKey={currentDashboardTab}>
|
||||
{errorMessage && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>{t('dashboard.alerts.errorTitle')}</AlertTitle>
|
||||
@@ -667,120 +571,41 @@ export default function DashboardPage() {
|
||||
<DashboardSkeleton />
|
||||
) : (
|
||||
<>
|
||||
<TenantHeroCard
|
||||
badge={heroBadge}
|
||||
title={heroTitle}
|
||||
description={heroDescription}
|
||||
supporting={heroSupporting}
|
||||
primaryAction={heroPrimaryAction}
|
||||
aside={heroAside}
|
||||
/>
|
||||
|
||||
{liveEvents.length > 0 && (
|
||||
<Card className="border border-rose-200 bg-rose-50/80 shadow-lg shadow-rose-200/40">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-base font-semibold text-rose-900">{liveNowTitle}</CardTitle>
|
||||
<CardDescription className="text-sm text-rose-700">{liveNowDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
||||
{liveEvents.map((event) => {
|
||||
const name = resolveEventName(event.name, event.slug);
|
||||
const dateLabel = event.event_date ? formatDate(event.event_date, dateLocale) : liveNoDate;
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="rounded-2xl border border-white/70 bg-white/80 p-4 shadow-sm shadow-rose-100/50"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900">{name}</p>
|
||||
<p className="text-xs text-slate-500">{dateLabel}</p>
|
||||
</div>
|
||||
<Badge className="bg-rose-600/90 text-white">{liveStatusLabel}</Badge>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex flex-1 items-center gap-2 border-rose-200 text-rose-700 hover:border-rose-400 hover:text-rose-800"
|
||||
onClick={() => navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))}
|
||||
>
|
||||
<Camera className="h-4 w-4" />
|
||||
{liveActionLabels.photos}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex flex-1 items-center gap-2 border-rose-200 text-rose-700 hover:border-rose-400 hover:text-rose-800"
|
||||
onClick={() => navigate(ADMIN_EVENT_INVITES_PATH(event.slug))}
|
||||
>
|
||||
<QrCode className="h-4 w-4" />
|
||||
{liveActionLabels.invites}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex flex-1 items-center gap-2 border-rose-200 text-rose-700 hover:border-rose-400 hover:text-rose-800"
|
||||
onClick={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))}
|
||||
>
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
{liveActionLabels.tasks}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{events.length === 0 && (
|
||||
<Card className="border-none bg-white/90 shadow-lg shadow-rose-100/50">
|
||||
<CardHeader className="space-y-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold text-slate-900">
|
||||
<Sparkles className="h-5 w-5 text-brand-rose" />
|
||||
{translate('welcomeCard.title')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-slate-600">
|
||||
{translate('welcomeCard.summary')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3 text-sm text-slate-600">
|
||||
<p>{translate('welcomeCard.body1')}</p>
|
||||
<p>{translate('welcomeCard.body2')}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
className="self-start rounded-full bg-brand-rose px-5 text-white shadow-md shadow-rose-300/40"
|
||||
onClick={() => navigate(ADMIN_WELCOME_BASE_PATH)}
|
||||
>
|
||||
{translate('welcomeCard.cta')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('overview.title')}
|
||||
title={translate('overview.title')}
|
||||
description={translate('overview.description')}
|
||||
endSlot={(
|
||||
<Badge className="bg-brand-rose-soft text-brand-rose">
|
||||
{activePackage?.package_name ?? translate('overview.noPackage')}
|
||||
</Badge>
|
||||
)}
|
||||
<div id="overview" className="space-y-6 scroll-mt-32">
|
||||
<DashboardEventFocusCard
|
||||
event={primaryEvent}
|
||||
limitWarnings={limitWarnings}
|
||||
summary={summary}
|
||||
dateLocale={dateLocale}
|
||||
onCreateEvent={focusActions.createEvent}
|
||||
onOpenEvent={focusActions.openEvent}
|
||||
onOpenPhotos={focusActions.openPhotos}
|
||||
onOpenInvites={focusActions.openInvites}
|
||||
onOpenTasks={focusActions.openTasks}
|
||||
onOpenPhotobooth={focusActions.openPhotobooth}
|
||||
/>
|
||||
<StatCarousel items={statItems} />
|
||||
</SectionCard>
|
||||
|
||||
{primaryEventLimits ? (
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('overview.title')}
|
||||
title={translate('overview.title')}
|
||||
description={translate('overview.description')}
|
||||
endSlot={(
|
||||
<Badge className="bg-brand-rose-soft text-brand-rose">
|
||||
{activePackage?.package_name ?? translate('overview.noPackage')}
|
||||
</Badge>
|
||||
)}
|
||||
/>
|
||||
<StatCarousel items={statItems} />
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<div id="live" className="space-y-6 scroll-mt-32">
|
||||
{primaryEventLimits ? (
|
||||
<Card className="border-0 bg-brand-card shadow-brand-primary">
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-xl text-slate-900">
|
||||
<PackageIcon className="h-5 w-5 text-brand-rose" />
|
||||
{translate('limitsCard.title')}
|
||||
</CardTitle>
|
||||
@@ -842,70 +667,75 @@ export default function DashboardPage() {
|
||||
expires: translate('limitsCard.galleryExpires'),
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('quickActions.title')}
|
||||
title={translate('quickActions.title')}
|
||||
description={translate('quickActions.description')}
|
||||
<div id="setup" className="space-y-6 scroll-mt-32">
|
||||
<SectionCard className="space-y-3">
|
||||
<SectionHeader
|
||||
eyebrow={translate('quickActions.title')}
|
||||
title={translate('quickActions.title')}
|
||||
description={translate('quickActions.description')}
|
||||
/>
|
||||
<ActionGrid items={quickActionItems} />
|
||||
</SectionCard>
|
||||
|
||||
<TenantOnboardingChecklistCard
|
||||
title={onboardingCardTitle}
|
||||
description={onboardingCardDescription}
|
||||
steps={onboardingChecklist}
|
||||
completedLabel={readinessCompleteLabel}
|
||||
pendingLabel={readinessPendingLabel}
|
||||
completionPercent={onboardingCompletion}
|
||||
completedCount={completedOnboardingSteps}
|
||||
totalCount={onboardingChecklist.length}
|
||||
emptyCopy={onboardingCompletedCopy}
|
||||
fallbackActionLabel={onboardingFallbackCta}
|
||||
/>
|
||||
<ActionGrid items={quickActionItems} />
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<TenantOnboardingChecklistCard
|
||||
title={onboardingCardTitle}
|
||||
description={onboardingCardDescription}
|
||||
steps={onboardingChecklist}
|
||||
completedLabel={readinessCompleteLabel}
|
||||
pendingLabel={readinessPendingLabel}
|
||||
completionPercent={onboardingCompletion}
|
||||
completedCount={completedOnboardingSteps}
|
||||
totalCount={onboardingChecklist.length}
|
||||
emptyCopy={onboardingCompletedCopy}
|
||||
fallbackActionLabel={onboardingFallbackCta}
|
||||
/>
|
||||
|
||||
<section className="space-y-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 dark:shadow-inner">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
|
||||
{translate('upcoming.title')}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">{translate('upcoming.description')}</p>
|
||||
<div id="recap" className="space-y-6 scroll-mt-32">
|
||||
<section className="space-y-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 dark:shadow-inner">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
|
||||
{translate('upcoming.title')}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">{translate('upcoming.description')}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(ADMIN_SETTINGS_PATH)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
{translate('upcoming.settings')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(ADMIN_SETTINGS_PATH)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
{translate('upcoming.settings')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{upcomingEvents.length === 0 ? (
|
||||
<EmptyState
|
||||
message={translate('upcoming.empty.message')}
|
||||
ctaLabel={translate('upcoming.empty.cta')}
|
||||
onCta={() => navigate(adminPath('/events/new'))}
|
||||
/>
|
||||
) : (
|
||||
upcomingEvents.map((event) => (
|
||||
<UpcomingEventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
onView={() => navigate(ADMIN_EVENT_VIEW_PATH(event.slug))}
|
||||
locale={dateLocale}
|
||||
labels={{
|
||||
live: translate('upcoming.status.live'),
|
||||
planning: translate('upcoming.status.planning'),
|
||||
open: tc('actions.open'),
|
||||
noDate: translate('upcoming.status.noDate'),
|
||||
}}
|
||||
<div className="space-y-3">
|
||||
{upcomingEvents.length === 0 ? (
|
||||
<EmptyState
|
||||
message={translate('upcoming.empty.message')}
|
||||
ctaLabel={translate('upcoming.empty.cta')}
|
||||
onCta={() => navigate(adminPath('/events/new'))}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
upcomingEvents.map((event) => (
|
||||
<UpcomingEventRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
onView={() => navigate(ADMIN_EVENT_VIEW_PATH(event.slug))}
|
||||
locale={dateLocale}
|
||||
labels={{
|
||||
live: translate('upcoming.status.live'),
|
||||
planning: translate('upcoming.status.planning'),
|
||||
open: tc('actions.open'),
|
||||
noDate: translate('upcoming.status.noDate'),
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AdminLayout>
|
||||
@@ -933,17 +763,6 @@ function formatDate(value: string | null, locale: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function formatEventStatus(status: TenantEvent['status'] | null, translateFn: (key: string, options?: Record<string, unknown>) => string): string {
|
||||
const map: Record<string, { key: string; fallback: string }> = {
|
||||
published: { key: 'events.status.published', fallback: 'Veröffentlicht' },
|
||||
draft: { key: 'events.status.draft', fallback: 'Entwurf' },
|
||||
archived: { key: 'events.status.archived', fallback: 'Archiviert' },
|
||||
};
|
||||
|
||||
const target = map[status ?? 'draft'] ?? map.draft;
|
||||
return translateFn(target.key, { defaultValue: target.fallback });
|
||||
}
|
||||
|
||||
function resolveEventName(name: TenantEvent['name'], fallbackSlug: string): string {
|
||||
if (typeof name === 'string' && name.trim().length > 0) {
|
||||
return name;
|
||||
|
||||
Reference in New Issue
Block a user