überarbeitung des event-admins fortgesetzt
This commit is contained in:
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Bell,
|
||||
Camera,
|
||||
@@ -15,10 +14,10 @@ import {
|
||||
Printer,
|
||||
QrCode,
|
||||
PlugZap,
|
||||
RefreshCw,
|
||||
Smile,
|
||||
Sparkles,
|
||||
ShoppingCart,
|
||||
Menu,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -61,21 +60,13 @@ import {
|
||||
import {
|
||||
SectionCard,
|
||||
SectionHeader,
|
||||
ActionGrid,
|
||||
TenantHeroCard,
|
||||
} from '../components/tenant';
|
||||
import { AddonsPicker } from '../components/Addons/AddonsPicker';
|
||||
import { AddonSummaryList } from '../components/Addons/AddonSummaryList';
|
||||
import { EventAddonCatalogItem, getAddonCatalog } from '../api';
|
||||
import { GuestBroadcastCard } from '../components/GuestBroadcastCard';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { filterEmotionsByEventType } from '../lib/emotions';
|
||||
import { buildEventTabs } from '../lib/eventTabs';
|
||||
|
||||
type EventDetailPageProps = {
|
||||
mode?: 'detail' | 'toolkit';
|
||||
};
|
||||
|
||||
type ToolkitState = {
|
||||
data: EventToolkit | null;
|
||||
loading: boolean;
|
||||
@@ -90,7 +81,7 @@ type WorkspaceState = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProps) {
|
||||
export default function EventDetailPage() {
|
||||
const { slug: slugParam } = useParams<{ slug?: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -214,9 +205,7 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
const toolkitData = toolkit.data;
|
||||
|
||||
const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event');
|
||||
const subtitle = mode === 'toolkit'
|
||||
? t('events.workspace.toolkitSubtitle', 'Moderation, Aufgaben und Einladungen für deinen Eventtag bündeln.')
|
||||
: t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.');
|
||||
const subtitle = t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.');
|
||||
|
||||
const limitWarnings = React.useMemo(
|
||||
() => (event?.limits ? buildLimitWarnings(event.limits, (key, options) => tCommon(`limits.${key}`, options)) : []),
|
||||
@@ -266,23 +255,6 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
[event?.slug],
|
||||
);
|
||||
|
||||
const eventTabs = React.useMemo(() => {
|
||||
if (!event) {
|
||||
return [];
|
||||
}
|
||||
const translateMenu = (key: string, fallback: string) => t(key, { defaultValue: fallback });
|
||||
return buildEventTabs(event, translateMenu, {
|
||||
photos: toolkitData?.photos?.pending?.length ?? event.photo_count ?? 0,
|
||||
tasks: toolkitData?.tasks?.summary.total ?? event.tasks_count ?? 0,
|
||||
invites: toolkitData?.invites?.summary.active ?? event.active_invites_count ?? event.total_invites_count ?? 0,
|
||||
});
|
||||
}, [event, toolkitData?.photos?.pending?.length, toolkitData?.tasks?.summary.total, toolkitData?.invites?.summary.active, t]);
|
||||
|
||||
const isRecapRoute = React.useMemo(
|
||||
() => location.pathname.endsWith('/recap'),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
//const [addonBusyId, setAddonBusyId] = React.useState<string | null>(null);
|
||||
|
||||
@@ -379,8 +351,6 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
<AdminLayout
|
||||
title={eventName}
|
||||
subtitle={subtitle}
|
||||
tabs={eventTabs}
|
||||
currentTabKey={isRecapRoute ? 'recap' : 'overview'}
|
||||
>
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
@@ -455,70 +425,71 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
<WorkspaceSkeleton />
|
||||
) : event ? (
|
||||
<div className="space-y-6">
|
||||
<EventHeroCardSection
|
||||
event={event}
|
||||
stats={stats}
|
||||
onRefresh={() => { void load(); }}
|
||||
loading={state.busy}
|
||||
navigate={navigate}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue={isRecapRoute ? 'recap' : 'overview'} className="space-y-6">
|
||||
<TabsList className="grid gap-2 rounded-2xl bg-slate-100/80 p-1 dark:bg-white/5 sm:grid-cols-3">
|
||||
<TabsTrigger value="overview">{t('events.workspace.tabs.overview', 'Überblick')}</TabsTrigger>
|
||||
<TabsTrigger value="setup">{t('events.workspace.tabs.setup', 'Vorbereitung')}</TabsTrigger>
|
||||
<TabsTrigger value="recap">{t('events.workspace.tabs.recap', 'Nachbereitung')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.6fr)_minmax(0,0.6fr)]">
|
||||
<StatusCard event={event} stats={stats} busy={busy} onToggle={handleToggle} />
|
||||
<QuickActionsCard slug={event.slug} busy={busy} onToggle={handleToggle} navigate={navigate} />
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
|
||||
{t('events.workspace.hero.badge', 'Event')}
|
||||
</p>
|
||||
<h1 className="text-xl font-semibold text-slate-900 dark:text-white">{resolveName(event.name)}</h1>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{t('events.workspace.hero.description', 'Konzentriere dich auf Aufgaben, Moderation und Einladungen für dieses Event.')}
|
||||
</p>
|
||||
</div>
|
||||
<MetricsGrid metrics={toolkitData?.metrics} stats={stats} />
|
||||
</TabsContent>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(ADMIN_EVENTS_PATH)} className="rounded-full border-pink-200 text-pink-600 hover:bg-pink-50">
|
||||
<ArrowLeft className="h-4 w-4" /> {t('events.actions.backToList', 'Zurück zur Liste')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate(ADMIN_EVENT_EDIT_PATH(event.slug))} className="rounded-full border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50">
|
||||
<Sparkles className="h-4 w-4" /> {t('events.actions.edit', 'Bearbeiten')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { void handleToggle(); }}
|
||||
disabled={busy}
|
||||
className="rounded-full border-slate-200"
|
||||
>
|
||||
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : event.is_active ? <Circle className="mr-2 h-4 w-4" /> : <CheckCircle2 className="mr-2 h-4 w-4" />}
|
||||
{event.is_active ? t('events.workspace.actions.pause', 'Event pausieren') : t('events.workspace.actions.activate', 'Event aktivieren')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="setup" className="space-y-6">
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.4fr)_minmax(0,0.8fr)]">
|
||||
<TaskOverviewCard tasks={toolkitData?.tasks} navigateToTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} />
|
||||
<InviteSummary
|
||||
invites={toolkitData?.invites}
|
||||
navigateToInvites={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
<StatusCard event={event} stats={stats} busy={busy} onToggle={handleToggle} />
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.4fr)_minmax(0,0.8fr)]">
|
||||
<TaskOverviewCard tasks={toolkitData?.tasks} navigateToTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} />
|
||||
<InviteSummary
|
||||
invites={toolkitData?.invites}
|
||||
navigateToInvites={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
/>
|
||||
</div>
|
||||
<BrandingMissionCard
|
||||
event={event}
|
||||
invites={toolkitData?.invites}
|
||||
emotions={emotions}
|
||||
onOpenBranding={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
onOpenCollections={() => navigate(buildEngagementTabPath('collections'))}
|
||||
onOpenTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))}
|
||||
onOpenEmotions={() => navigate(buildEngagementTabPath('emotions'))}
|
||||
/>
|
||||
{event.addons?.length ? (
|
||||
<SectionCard>
|
||||
<SectionHeader
|
||||
title={t('events.sections.addons.title', 'Add-ons & Upgrades')}
|
||||
description={t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BrandingMissionCard
|
||||
event={event}
|
||||
invites={toolkitData?.invites}
|
||||
emotions={emotions}
|
||||
onOpenBranding={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
onOpenCollections={() => navigate(buildEngagementTabPath('collections'))}
|
||||
onOpenTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))}
|
||||
onOpenEmotions={() => navigate(buildEngagementTabPath('emotions'))}
|
||||
/>
|
||||
|
||||
{event.addons?.length ? (
|
||||
<SectionCard>
|
||||
<SectionHeader
|
||||
title={t('events.sections.addons.title', 'Add-ons & Upgrades')}
|
||||
description={t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')}
|
||||
/>
|
||||
<AddonSummaryList addons={event.addons} t={(key, fallback) => t(key, fallback)} />
|
||||
</SectionCard>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="recap" className="space-y-6">
|
||||
<GalleryShareCard
|
||||
invites={toolkitData?.invites}
|
||||
onManageInvites={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
/>
|
||||
{event.limits?.gallery ? (
|
||||
<GalleryStatusCard gallery={event.limits.gallery} />
|
||||
) : null}
|
||||
<FeedbackCard slug={event.slug} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<AddonSummaryList addons={event.addons} t={(key, fallback) => t(key, fallback)} />
|
||||
</SectionCard>
|
||||
) : null}
|
||||
<GalleryShareCard
|
||||
invites={toolkitData?.invites}
|
||||
onManageInvites={() => navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)}
|
||||
/>
|
||||
{event.limits?.gallery ? <GalleryStatusCard gallery={event.limits.gallery} /> : null}
|
||||
<FeedbackCard slug={event.slug} />
|
||||
<QuickActionsMenu slug={event.slug} navigate={navigate} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SectionCard>
|
||||
@@ -543,78 +514,6 @@ function resolveName(name: TenantEvent['name']): string {
|
||||
return 'Event';
|
||||
}
|
||||
|
||||
function EventHeroCardSection({ event, stats, onRefresh, loading, navigate }: {
|
||||
event: TenantEvent;
|
||||
stats: EventStats | null;
|
||||
onRefresh: () => void;
|
||||
loading: boolean;
|
||||
navigate: ReturnType<typeof useNavigate>;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const statusLabel = getStatusLabel(event, t);
|
||||
const supporting = [
|
||||
t('events.workspace.hero.status', { defaultValue: 'Status: {{status}}', status: statusLabel }),
|
||||
t('events.workspace.hero.date', { defaultValue: 'Eventdatum: {{date}}', date: formatDate(event.event_date) }),
|
||||
t('events.workspace.hero.metrics', {
|
||||
defaultValue: 'Uploads gesamt: {{count}} · Likes: {{likes}}',
|
||||
count: stats?.uploads_total ?? stats?.total ?? 0,
|
||||
likes: stats?.likes_total ?? stats?.likes ?? 0,
|
||||
}),
|
||||
];
|
||||
|
||||
const aside = (
|
||||
<div className="space-y-3 text-sm text-slate-700 dark:text-slate-200">
|
||||
<InfoRow
|
||||
icon={<Sparkles className="h-4 w-4 text-pink-500" />}
|
||||
label={t('events.workspace.fields.status', 'Status')}
|
||||
value={statusLabel}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CalendarIcon />}
|
||||
label={t('events.workspace.fields.date', 'Eventdatum')}
|
||||
value={formatDate(event.event_date)}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<Users className="h-4 w-4 text-sky-500" />}
|
||||
label={t('events.workspace.fields.active', 'Aktiv für Gäste')}
|
||||
value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TenantHeroCard
|
||||
badge={t('events.workspace.hero.badge', 'Event')}
|
||||
title={resolveName(event.name)}
|
||||
description={t('events.workspace.hero.description', 'Konzentriere dich auf Aufgaben, Moderation und Einladungen für dieses Event.')}
|
||||
supporting={supporting}
|
||||
primaryAction={(
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_EVENTS_PATH)} className="rounded-full border-pink-200 text-pink-600 hover:bg-pink-50">
|
||||
<ArrowLeft className="h-4 w-4" /> {t('events.actions.backToList', 'Zurück zur Liste')}
|
||||
</Button>
|
||||
)}
|
||||
secondaryAction={(
|
||||
<Button variant="outline" onClick={() => navigate(ADMIN_EVENT_EDIT_PATH(event.slug))} className="rounded-full border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50">
|
||||
<Sparkles className="h-4 w-4" /> {t('events.actions.edit', 'Bearbeiten')}
|
||||
</Button>
|
||||
)}
|
||||
aside={aside}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
className="rounded-full border-slate-200"
|
||||
>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
||||
{t('events.actions.refresh', 'Aktualisieren')}
|
||||
</Button>
|
||||
</div>
|
||||
</TenantHeroCard>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stats: EventStats | null; busy: boolean; onToggle: () => void }) {
|
||||
const { t } = useTranslation('management');
|
||||
|
||||
@@ -656,125 +555,53 @@ function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stat
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button onClick={onToggle} disabled={busy} className="bg-gradient-to-r from-pink-500 via-rose-500 to-purple-500 text-white shadow-md shadow-rose-200/60">
|
||||
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : event.is_active ? <Circle className="mr-2 h-4 w-4" /> : <CheckCircle2 className="mr-2 h-4 w-4" />}
|
||||
{event.is_active ? t('events.workspace.actions.pause', 'Event pausieren') : t('events.workspace.actions.activate', 'Event aktivieren')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickActionsCard({ slug, busy, onToggle, navigate }: { slug: string; busy: boolean; onToggle: () => void | Promise<void>; navigate: ReturnType<typeof useNavigate> }) {
|
||||
function QuickActionsMenu({ slug, navigate }: { slug: string; navigate: ReturnType<typeof useNavigate> }) {
|
||||
const { t } = useTranslation('management');
|
||||
|
||||
const gridItems = [
|
||||
{
|
||||
key: 'photos',
|
||||
icon: <Camera className="h-4 w-4" />,
|
||||
label: t('events.quickActions.moderate', 'Fotos moderieren'),
|
||||
description: t('events.quickActions.moderateDesc', 'Prüfe Uploads und veröffentliche Highlights.'),
|
||||
onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)),
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
icon: <Sparkles className="h-4 w-4" />,
|
||||
label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'),
|
||||
description: t('events.quickActions.tasksDesc', 'Passe Story-Prompts und Moderation an.'),
|
||||
onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)),
|
||||
},
|
||||
{
|
||||
key: 'invites',
|
||||
icon: <QrCode className="h-4 w-4" />,
|
||||
label: t('events.quickActions.invites', 'Layouts & QR verwalten'),
|
||||
description: t('events.quickActions.invitesDesc', 'Aktualisiere QR-Kits und Einladungslayouts.'),
|
||||
onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`),
|
||||
},
|
||||
{
|
||||
key: 'roles',
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
label: t('events.quickActions.roles', 'Team & Rollen anpassen'),
|
||||
description: t('events.quickActions.rolesDesc', 'Verwalte Moderatoren und Co-Leads.'),
|
||||
onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)),
|
||||
},
|
||||
{
|
||||
key: 'photobooth',
|
||||
icon: <PlugZap className="h-4 w-4" />,
|
||||
label: t('events.quickActions.photobooth', 'Photobooth anbinden'),
|
||||
description: t('events.quickActions.photoboothDesc', 'FTP-Link aktivieren und Zugangsdaten kopieren.'),
|
||||
onClick: () => navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(slug)),
|
||||
},
|
||||
{
|
||||
key: 'print',
|
||||
icon: <Printer className="h-4 w-4" />,
|
||||
label: t('events.quickActions.print', 'Layouts als PDF drucken'),
|
||||
description: t('events.quickActions.printDesc', 'Exportiere QR-Sets für den Druck.'),
|
||||
onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`),
|
||||
},
|
||||
const actions = [
|
||||
{ key: 'photos', icon: <Camera className="h-4 w-4" />, label: t('events.quickActions.moderate', 'Fotos moderieren'), onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)) },
|
||||
{ key: 'tasks', icon: <Sparkles className="h-4 w-4" />, label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'), onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)) },
|
||||
{ key: 'invites', icon: <QrCode className="h-4 w-4" />, label: t('events.quickActions.invites', 'Layouts & QR verwalten'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`) },
|
||||
{ key: 'roles', icon: <Users className="h-4 w-4" />, label: t('events.quickActions.roles', 'Team & Rollen anpassen'), onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)) },
|
||||
{ key: 'photobooth', icon: <PlugZap className="h-4 w-4" />, label: t('events.quickActions.photobooth', 'Photobooth anbinden'), onClick: () => navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(slug)) },
|
||||
{ key: 'print', icon: <Printer className="h-4 w-4" />, label: t('events.quickActions.print', 'Layouts als PDF drucken'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`) },
|
||||
];
|
||||
|
||||
return (
|
||||
<SectionCard className="space-y-4">
|
||||
<SectionHeader
|
||||
eyebrow={t('events.quickActions.badge', 'Schnellaktionen')}
|
||||
title={t('events.quickActions.title', 'Schnellaktionen')}
|
||||
description={t('events.quickActions.subtitle', 'Nutze die wichtigsten Schritte vor und während deines Events.')}
|
||||
/>
|
||||
<ActionGrid items={gridItems} columns={1} />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => { void onToggle(); }} disabled={busy} variant="outline" className="rounded-full border-rose-200 text-rose-600 hover:bg-rose-50">
|
||||
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <CheckCircle2 className="mr-2 h-4 w-4" />}
|
||||
{t('events.quickActions.toggle', 'Status ändern')}
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="fixed bottom-6 right-6 z-40 h-12 w-12 rounded-full bg-rose-500 text-white shadow-xl shadow-rose-300/50 hover:bg-rose-600 focus-visible:ring-2 focus-visible:ring-rose-300"
|
||||
aria-label={t('events.quickActions.badge', 'Schnellaktionen')}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricsGrid({ metrics, stats }: { metrics: EventToolkit['metrics'] | null | undefined; stats: EventStats | null }) {
|
||||
const { t } = useTranslation('management');
|
||||
|
||||
const cards = [
|
||||
{
|
||||
icon: <Camera className="h-5 w-5 text-emerald-500" />,
|
||||
label: t('events.metrics.uploadsTotal', 'Uploads gesamt'),
|
||||
value: metrics?.uploads_total ?? stats?.uploads_total ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <Camera className="h-5 w-5 text-sky-500" />,
|
||||
label: t('events.metrics.uploads24h', 'Uploads (24h)'),
|
||||
value: metrics?.uploads_24h ?? stats?.uploads_24h ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
|
||||
label: t('events.metrics.pending', 'Fotos in Moderation'),
|
||||
value: metrics?.pending_photos ?? stats?.pending_photos ?? 0,
|
||||
},
|
||||
{
|
||||
icon: <QrCode className="h-5 w-5 text-indigo-500" />,
|
||||
label: t('events.metrics.activeInvites', 'Aktive Einladungen'),
|
||||
value: metrics?.active_invites ?? 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<SectionCard key={card.label} className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-100 dark:bg-white/10">
|
||||
{card.icon}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">{card.label}</p>
|
||||
<p className="text-2xl font-semibold text-slate-900 dark:text-white">{card.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
))}
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="bottom" className="rounded-t-3xl border border-slate-200 bg-white/95 p-4 text-slate-900 dark:border-white/10 dark:bg-slate-950">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-lg font-semibold">{t('events.quickActions.title', 'Starte in die Moderation')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-4 grid gap-2">
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
key={action.key}
|
||||
variant="ghost"
|
||||
className="flex items-center justify-start gap-3 rounded-2xl border border-slate-200 bg-white/90 text-left text-sm font-semibold text-slate-800 hover:border-rose-200 hover:bg-rose-50 dark:border-white/10 dark:bg-white/5 dark:text-white"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user