event admin verfeinert und UI reduziert.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ShoppingCart,
|
||||
Menu,
|
||||
Users,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -85,7 +86,6 @@ export default function EventDetailPage() {
|
||||
const { slug: slugParam } = useParams<{ slug?: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation('management');
|
||||
const { t: tCommon } = useTranslation('common');
|
||||
|
||||
@@ -427,7 +427,7 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
|
||||
{t('events.workspace.hero.badge', 'Event')}
|
||||
</p>
|
||||
@@ -435,6 +435,24 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
<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 className="flex flex-wrap gap-2 text-xs">
|
||||
<Badge variant="secondary" className="gap-1 rounded-full bg-emerald-100 text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-100">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{getStatusLabel(event, t)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 rounded-full border-slate-200 text-slate-700 dark:border-white/10 dark:text-white">
|
||||
<CalendarIcon />
|
||||
{formatDate(event.event_date)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 rounded-full border-slate-200 text-slate-700 dark:border-white/10 dark:text-white">
|
||||
<Circle className={`h-3 w-3 ${event.is_active ? 'text-emerald-500' : 'text-slate-400'}`} />
|
||||
{t('events.workspace.hero.liveBadge', 'Live?')} {event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 rounded-full border-slate-200 text-slate-700 dark:border-white/10 dark:text-white">
|
||||
<Smile className="h-3.5 w-3.5 text-rose-500" />
|
||||
{resolveEventType(event)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -450,13 +468,18 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
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" />}
|
||||
{busy ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : event.is_active ? (
|
||||
<CheckCircle2 className="mr-2 h-4 w-4 text-emerald-500" />
|
||||
) : (
|
||||
<Circle className="mr-2 h-4 w-4 text-slate-500" />
|
||||
)}
|
||||
{event.is_active ? t('events.workspace.actions.pause', 'Event pausieren') : t('events.workspace.actions.activate', 'Event aktivieren')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
@@ -514,52 +537,6 @@ function resolveName(name: TenantEvent['name']): string {
|
||||
return 'Event';
|
||||
}
|
||||
|
||||
function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stats: EventStats | null; busy: boolean; onToggle: () => void }) {
|
||||
const { t } = useTranslation('management');
|
||||
|
||||
const statusLabel = getStatusLabel(event, t);
|
||||
|
||||
return (
|
||||
<SectionCard className="space-y-4">
|
||||
<SectionHeader
|
||||
eyebrow={t('events.workspace.sections.statusBadge', 'Status')}
|
||||
title={t('events.workspace.sections.statusTitle', 'Eventstatus & Sichtbarkeit')}
|
||||
description={t('events.workspace.sections.statusSubtitle', 'Aktiviere dein Event für Gäste oder verstecke es vorübergehend.')}
|
||||
/>
|
||||
<div className="space-y-4 text-sm text-slate-700 dark:text-slate-300">
|
||||
<InfoRow icon={<Sparkles className="h-4 w-4 text-pink-500" />} label={t('events.workspace.fields.status', 'Status')} value={statusLabel} />
|
||||
<InfoRow icon={<Circle className="h-4 w-4 text-amber-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')} />
|
||||
<InfoRow icon={<CalendarIcon />} label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} />
|
||||
<InfoRow icon={<Smile className="h-4 w-4 text-rose-500" />} label={t('events.workspace.fields.eventType', 'Event-Typ')} value={resolveEventType(event)} />
|
||||
|
||||
{stats && (
|
||||
<div className="rounded-xl border border-pink-100 bg-pink-50/60 p-4 text-xs text-pink-900">
|
||||
<p className="font-semibold text-pink-700">{t('events.workspace.fields.insights', 'Letzte Aktivität')}</p>
|
||||
<p>
|
||||
{t('events.workspace.fields.uploadsTotal', {
|
||||
defaultValue: '{{count}} Uploads gesamt',
|
||||
count: stats.uploads_total ?? stats.total ?? 0,
|
||||
})}
|
||||
{' · '}
|
||||
{t('events.workspace.fields.uploadsToday', {
|
||||
defaultValue: '{{count}} Uploads (24h)',
|
||||
count: stats.uploads_24h ?? stats.recent_uploads ?? 0,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('events.workspace.fields.likesTotal', {
|
||||
defaultValue: '{{count}} Likes vergeben',
|
||||
count: stats.likes_total ?? stats.likes ?? 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickActionsMenu({ slug, navigate }: { slug: string; navigate: ReturnType<typeof useNavigate> }) {
|
||||
const { t } = useTranslation('management');
|
||||
|
||||
@@ -577,7 +554,8 @@ function QuickActionsMenu({ slug, navigate }: { slug: string; navigate: ReturnTy
|
||||
<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"
|
||||
className="fixed 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"
|
||||
style={{ bottom: '90px' }}
|
||||
aria-label={t('events.quickActions.badge', 'Schnellaktionen')}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
@@ -1296,18 +1274,6 @@ function getAudienceLabel(scope: string, t: ReturnType<typeof useTranslation>['t
|
||||
return t('events.notifications.audienceAll', 'Alle Gäste');
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-slate-100 bg-white/70 px-3 py-2 text-sm text-slate-700">
|
||||
<span className="mt-0.5 flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-600">{icon}</span>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">{label}</p>
|
||||
<p className="text-sm font-semibold text-slate-900">{value || '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStatusLabel(event: TenantEvent, t: ReturnType<typeof useTranslation>['t']): string {
|
||||
if (event.status === 'published') {
|
||||
return t('events.status.published', 'Veröffentlicht');
|
||||
@@ -1328,13 +1294,24 @@ function formatDate(value: string | null | undefined): string {
|
||||
}
|
||||
|
||||
function resolveEventType(event: TenantEvent): string {
|
||||
if (event.event_type?.name) {
|
||||
if (typeof event.event_type.name === 'string') {
|
||||
return event.event_type.name;
|
||||
}
|
||||
const translations = event.event_type.name as Record<string, string>;
|
||||
const type = event.event_type;
|
||||
if (!type) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
if (type.name_translations && Object.keys(type.name_translations).length > 0) {
|
||||
return type.name_translations.de ?? type.name_translations.en ?? Object.values(type.name_translations)[0] ?? type.name ?? '—';
|
||||
}
|
||||
|
||||
if (typeof type.name === 'string' && type.name.trim().length > 0) {
|
||||
return type.name;
|
||||
}
|
||||
|
||||
if (type.name && typeof type.name === 'object') {
|
||||
const translations = type.name as Record<string, string>;
|
||||
return translations.de ?? translations.en ?? Object.values(translations)[0] ?? '—';
|
||||
}
|
||||
|
||||
return '—';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user