added missing translations

This commit is contained in:
Codex Agent
2025-11-26 14:41:39 +01:00
parent ff168834b4
commit ecac9507a4
35 changed files with 2812 additions and 256 deletions

View File

@@ -59,6 +59,8 @@ import {
ADMIN_EVENT_BRANDING_PATH,
buildEngagementTabPath,
} from '../constants';
import { buildEventTabs } from '../lib/eventTabs';
import { formatEventDate } from '../lib/events';
import {
SectionCard,
SectionHeader,
@@ -207,6 +209,18 @@ export default function EventDetailPage() {
const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event');
const subtitle = t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.');
const currentTabKey = 'overview';
const eventTabs = React.useMemo(() => {
if (!event) return [];
const translateMenu = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const counts = {
photos: stats?.uploads_total ?? event.photo_count ?? undefined,
tasks: toolkitData?.tasks?.summary?.total ?? event.tasks_count ?? undefined,
invites: event.active_invites_count ?? event.total_invites_count ?? undefined,
};
return buildEventTabs(event, translateMenu, counts);
}, [event, stats?.uploads_total, toolkitData?.tasks?.summary?.total, t]);
const limitWarnings = React.useMemo(
() => (event?.limits ? buildLimitWarnings(event.limits, (key, options) => tCommon(`limits.${key}`, options)) : []),
@@ -352,6 +366,8 @@ const shownWarningToasts = React.useRef<Set<string>>(new Set());
<AdminLayout
title={eventName}
subtitle={subtitle}
tabs={eventTabs}
currentTabKey={currentTabKey}
>
{error && (
<Alert variant="destructive">
@@ -538,6 +554,159 @@ function resolveName(name: TenantEvent['name']): string {
return 'Event';
}
type RecapContentProps = {
event: TenantEvent;
stats: EventStats;
busy: boolean;
onToggleEvent: () => void;
onExtendGallery: () => void;
};
function RecapContent({ event, stats, busy, onToggleEvent, onExtendGallery }: RecapContentProps) {
const { t } = useTranslation('management');
const navigate = useNavigate();
const galleryExpiresAt = event.package?.expires_at ?? event.limits?.gallery?.expires_at ?? null;
const galleryStatusLabel = event.is_active
? t('events.recap.galleryOpen', 'Galerie geöffnet')
: t('events.recap.galleryClosed', 'Galerie geschlossen');
const counts = {
photos: stats.uploads_total ?? stats.total ?? 0,
pending: stats.pending_photos ?? 0,
likes: stats.likes_total ?? stats.likes ?? 0,
};
return (
<div className="space-y-6">
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-3xl border border-slate-200 bg-white/85 p-5 shadow-sm dark:border-white/10 dark:bg-white/5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-emerald-500">
{t('events.recap.galleryTitle', 'Galerie-Status')}
</p>
<p className="mt-1 text-lg font-semibold text-slate-900 dark:text-white">{galleryStatusLabel}</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
{t('events.recap.galleryCounts', '{{photos}} Fotos, {{pending}} offen, {{likes}} Likes', counts)}
</p>
</div>
<Badge variant={event.is_active ? 'default' : 'outline'} className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-100">
{event.is_active ? t('events.recap.open', 'Offen') : t('events.recap.closed', 'Geschlossen')}
</Badge>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<Button size="sm" variant={event.is_active ? 'secondary' : 'default'} disabled={busy} onClick={onToggleEvent}>
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Sparkles className="mr-2 h-4 w-4" />}
{event.is_active ? t('events.recap.closeGallery', 'Galerie schließen') : t('events.recap.openGallery', 'Galerie öffnen')}
</Button>
<Button size="sm" variant="outline" onClick={() => navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))}>
<Camera className="mr-2 h-4 w-4" />
{t('events.recap.moderate', 'Uploads ansehen')}
</Button>
</div>
{event.public_url ? (
<div className="mt-4 flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<span className="truncate" title={event.public_url}>{event.public_url}</span>
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={() => navigator.clipboard.writeText(event.public_url!)}>
<QrCode className="h-4 w-4" />
</Button>
</div>
) : null}
</div>
<div className="rounded-3xl border border-slate-200 bg-white/85 p-5 shadow-sm dark:border-white/10 dark:bg-white/5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-indigo-500">
{t('events.recap.exportTitle', 'Export & Backup')}
</p>
<p className="mt-1 text-lg font-semibold text-slate-900 dark:text-white">
{t('events.recap.exportCopy', 'Alle Assets sichern')}
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
{t('events.recap.exportHint', 'Zip/CSV Export und Backup anstoßen.')}
</p>
</div>
<Badge variant="outline" className="border-indigo-200 text-indigo-700 dark:border-indigo-800 dark:text-indigo-200">
{t('events.recap.backup', 'Backup')}
</Badge>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<Button size="sm" onClick={() => navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))}>
<ArrowLeft className="mr-2 h-4 w-4 rotate-180" />
{t('events.recap.exportAll', 'Alles exportieren')}
</Button>
<Button size="sm" variant="outline" onClick={() => navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))}>
<Printer className="mr-2 h-4 w-4" />
{t('events.recap.exportHighlights', 'Highlights exportieren')}
</Button>
</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-3xl border border-slate-200 bg-white/85 p-5 shadow-sm dark:border-white/10 dark:bg-white/5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-amber-500">
{t('events.recap.retentionTitle', 'Aufbewahrung & Verlängerung')}
</p>
<p className="mt-1 text-lg font-semibold text-slate-900 dark:text-white">
{galleryExpiresAt
? t('events.recap.expiresAt', 'Läuft ab am {{date}}', { date: formatEventDate(galleryExpiresAt, undefined) })
: t('events.recap.noExpiry', 'Ablaufdatum nicht gesetzt')}
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
{t('events.recap.retentionHint', 'Verlängere die Galerie-Laufzeit mit einem Add-on. Verlängerungen addieren sich.')}
</p>
</div>
<Badge variant="outline" className="border-amber-200 text-amber-700 dark:border-amber-800 dark:text-amber-100">
{t('events.recap.expiry', 'Ablauf')}
</Badge>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<Button size="sm" variant="default" onClick={onExtendGallery}>
<Clock3 className="mr-2 h-4 w-4" />
{t('events.recap.extend30', '+30 Tage verlängern')}
</Button>
<Button size="sm" variant="outline" onClick={() => navigate(ADMIN_EVENT_EDIT_PATH(event.slug))}>
{t('events.recap.archive', 'Archivieren/Löschen')}
</Button>
</div>
</div>
<div className="rounded-3xl border border-slate-200 bg-white/85 p-5 shadow-sm dark:border-white/10 dark:bg-white/5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-rose-500">
{t('events.recap.commsTitle', 'Kommunikation')}
</p>
<p className="mt-1 text-lg font-semibold text-slate-900 dark:text-white">
{t('events.recap.commsCopy', 'Kein Gast-Newsletter aktiv')}
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
{t('events.recap.commsHint', 'Push wirkt vor allem live. Teile den Link manuell mit deinem Team oder auf Social Media.')}
</p>
</div>
<Badge variant="outline" className="border-rose-200 text-rose-700 dark:border-rose-800 dark:text-rose-100">
{t('events.recap.manual', 'Manuell')}
</Badge>
</div>
{event.public_url ? (
<div className="mt-4 flex items-center gap-2 rounded-2xl border border-dashed border-slate-200 bg-white/70 p-3 text-sm text-slate-700 dark:border-white/10 dark:bg-white/10 dark:text-slate-200">
<div className="flex-1 truncate" title={event.public_url}>{event.public_url}</div>
<Button size="sm" variant="ghost" onClick={() => navigator.clipboard.writeText(event.public_url!)}>
{t('events.recap.copyLink', 'Link kopieren')}
</Button>
</div>
) : null}
</div>
</div>
</div>
);
}
function QuickActionsMenu({ slug, navigate }: { slug: string; navigate: ReturnType<typeof useNavigate> }) {
const { t } = useTranslation('management');
@@ -664,6 +833,7 @@ function TaskOverviewCard({ tasks, navigateToTasks }: { tasks: EventToolkit['tas
}
function TaskRow({ task }: { task: EventToolkitTask }) {
const { t } = useTranslation('management');
return (
<div className="flex items-start justify-between rounded-lg border border-pink-100 bg-white/80 px-3 py-2 text-xs text-slate-600">
<div className="space-y-1">
@@ -671,7 +841,7 @@ function TaskRow({ task }: { task: EventToolkitTask }) {
{task.description ? <p>{task.description}</p> : null}
</div>
<Badge variant={task.is_completed ? 'default' : 'outline'} className={task.is_completed ? 'bg-emerald-500/20 text-emerald-600' : 'border-pink-200 text-pink-600'}>
{task.is_completed ? 'Erledigt' : 'Offen'}
{task.is_completed ? t('events.tasks.status.completed', 'Done') : t('events.tasks.status.open', 'Open')}
</Badge>
</div>
);