Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).
Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user