Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).

Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
Codex Agent
2025-11-01 19:50:17 +01:00
parent 2c14493604
commit 79b209de9a
55 changed files with 3348 additions and 462 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { ArrowRight, CalendarDays, Plus, Settings, Sparkles, Share2 } from 'lucide-react';
import { AlertTriangle, ArrowRight, CalendarDays, Plus, Settings, Sparkles, Share2 } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { AdminLayout } from '../components/AdminLayout';
import { getEvents, TenantEvent } from '../api';
import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
import {
adminPath,
ADMIN_SETTINGS_PATH,
@@ -21,8 +22,12 @@ import {
ADMIN_EVENT_INVITES_PATH,
ADMIN_EVENT_TOOLKIT_PATH,
} from '../constants';
import { buildLimitWarnings, type EventLimitSummary } from '../lib/limitWarnings';
import { useTranslation } from 'react-i18next';
export default function EventsPage() {
const { t } = useTranslation('management');
const { t: tCommon } = useTranslation('common');
const [rows, setRows] = React.useState<TenantEvent[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
@@ -34,7 +39,7 @@ export default function EventsPage() {
setRows(await getEvents());
} catch (err) {
if (!isAuthError(err)) {
setError('Laden fehlgeschlagen. Bitte später erneut versuchen.');
setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.')));
}
} finally {
setLoading(false);
@@ -48,11 +53,11 @@ export default function EventsPage() {
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
onClick={() => navigate(adminPath('/events/new'))}
>
<Plus className="h-4 w-4" /> Neues Event
<Plus className="h-4 w-4" /> {t('events.list.actions.create', 'Neues Event')}
</Button>
<Link to={ADMIN_SETTINGS_PATH}>
<Button variant="outline" className="border-pink-200 text-pink-600 hover:bg-pink-50">
<Settings className="h-4 w-4" /> Einstellungen
<Settings className="h-4 w-4" /> {t('events.list.actions.settings', 'Einstellungen')}
</Button>
</Link>
</>
@@ -60,8 +65,8 @@ export default function EventsPage() {
return (
<AdminLayout
title="Deine Events"
subtitle="Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen."
title={t('events.list.title', 'Deine Events')}
subtitle={t('events.list.subtitle', 'Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen.')}
actions={actions}
>
{error && (
@@ -74,15 +79,15 @@ export default function EventsPage() {
<Card className="border-0 bg-white/80 shadow-xl shadow-pink-100/60">
<CardHeader className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-xl font-semibold text-slate-900">Übersicht</CardTitle>
<CardTitle className="text-xl font-semibold text-slate-900">{t('events.list.overview.title', bersicht')}</CardTitle>
<CardDescription className="text-slate-600">
{rows.length === 0
? 'Noch keine Events - starte jetzt und lege dein erstes Event an.'
: `${rows.length} ${rows.length === 1 ? 'Event' : 'Events'} aktiv verwaltet.`}
? t('events.list.overview.empty', 'Noch keine Events - starte jetzt und lege dein erstes Event an.')
: t('events.list.overview.count', '{{count}} Events aktiv verwaltet.', { count: rows.length })}
</CardDescription>
</div>
<div className="flex items-center gap-2 text-sm text-pink-600">
<Sparkles className="h-4 w-4" /> Tenant Dashboard
<Sparkles className="h-4 w-4" /> {t('events.list.badge.dashboard', 'Tenant Dashboard')}
</div>
</CardHeader>
<CardContent className="space-y-4">
@@ -93,7 +98,7 @@ export default function EventsPage() {
) : (
<div className="space-y-4">
{rows.map((event) => (
<EventCard key={event.id} event={event} />
<EventCard key={event.id} event={event} translateCommon={tCommon} />
))}
</div>
)}
@@ -103,14 +108,41 @@ export default function EventsPage() {
);
}
function EventCard({ event }: { event: TenantEvent }) {
function EventCard({
event,
translateCommon,
}: {
event: TenantEvent;
translateCommon: (key: string, options?: Record<string, unknown>) => string;
}) {
const slug = event.slug;
const isPublished = event.status === 'published';
const photoCount = event.photo_count ?? 0;
const likeCount = event.like_count ?? 0;
const limitWarnings = React.useMemo(
() => buildLimitWarnings(event.limits ?? null, (key, opts) => translateCommon(`limits.${key}`, opts)),
[event.limits, translateCommon],
);
return (
<div className="rounded-2xl border border-white/80 bg-white/90 p-5 shadow-md shadow-pink-200/40 transition-transform hover:-translate-y-0.5 hover:shadow-lg">
{limitWarnings.length > 0 && (
<div className="mb-3 space-y-1">
{limitWarnings.map((warning) => (
<Alert
key={warning.id}
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
className={warning.tone === 'warning' ? 'border-amber-400/40 bg-amber-50 text-amber-900' : undefined}
>
<AlertDescription className="flex items-center gap-2 text-xs">
<AlertTriangle className="h-4 w-4" />
{warning.message}
</AlertDescription>
</Alert>
))}
</div>
)}
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<h3 className="text-lg font-semibold text-slate-900">{renderName(event.name)}</h3>