import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { Camera, Loader2, Sparkles, Trash2, AlertTriangle, ShoppingCart } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import toast from 'react-hot-toast'; import { AddonsPicker } from '../components/Addons/AddonsPicker'; import { AddonSummaryList } from '../components/Addons/AddonSummaryList'; import { getAddonCatalog, getEvent, type EventAddonCatalogItem, type EventAddonSummary } from '../api'; import { AdminLayout } from '../components/AdminLayout'; import { createEventAddonCheckout, deletePhoto, featurePhoto, getEventPhotos, TenantPhoto, unfeaturePhoto } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage, isApiError } from '../lib/apiError'; import { buildLimitWarnings, type EventLimitSummary } from '../lib/limitWarnings'; import { useTranslation } from 'react-i18next'; import { ADMIN_EVENTS_PATH, ADMIN_EVENT_VIEW_PATH } from '../constants'; export default function EventPhotosPage() { const params = useParams<{ slug?: string }>(); const [searchParams] = useSearchParams(); const slug = params.slug ?? searchParams.get('slug') ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); const { t: tCommon } = useTranslation('common'); const translateLimits = React.useCallback( (key: string, options?: Record) => tCommon(`limits.${key}`, options), [tCommon] ); const [photos, setPhotos] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(undefined); const [busyId, setBusyId] = React.useState(null); const [limits, setLimits] = React.useState(null); const [addons, setAddons] = React.useState([]); const [catalogError, setCatalogError] = React.useState(undefined); const [searchParams, setSearchParams] = React.useState(() => new URLSearchParams(window.location.search)); const [eventAddons, setEventAddons] = React.useState([]); const load = React.useCallback(async () => { if (!slug) { setLoading(false); return; } setLoading(true); setError(undefined); try { const [photoResult, eventData, catalog] = await Promise.all([ getEventPhotos(slug), getEvent(slug), getAddonCatalog(), ]); setPhotos(photoResult.photos); setLimits(photoResult.limits ?? null); setEventAddons(eventData.addons ?? []); setAddons(catalog); setCatalogError(undefined); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, 'Fotos konnten nicht geladen werden.')); } } finally { setLoading(false); } }, [slug]); React.useEffect(() => { load(); }, [load]); React.useEffect(() => { const success = searchParams.get('addon_success'); if (success && slug) { toast(translateLimits('addonApplied', { defaultValue: 'Add-on angewendet. Limits aktualisieren sich in Kürze.' })); void load(); const params = new URLSearchParams(searchParams); params.delete('addon_success'); setSearchParams(params); navigate(window.location.pathname, { replace: true }); } }, [searchParams, slug, load, navigate, translateLimits]); async function handleToggleFeature(photo: TenantPhoto) { if (!slug) return; setBusyId(photo.id); try { const updated = photo.is_featured ? await unfeaturePhoto(slug, photo.id) : await featurePhoto(slug, photo.id); setPhotos((prev) => prev.map((entry) => (entry.id === photo.id ? updated : entry))); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, 'Feature-Aktion fehlgeschlagen.')); } } finally { setBusyId(null); } } async function handleDelete(photo: TenantPhoto) { if (!slug) return; setBusyId(photo.id); try { await deletePhoto(slug, photo.id); setPhotos((prev) => prev.filter((entry) => entry.id !== photo.id)); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, 'Foto konnte nicht entfernt werden.')); } } finally { setBusyId(null); } } if (!slug) { return ( Kein Slug in der URL gefunden. Kehre zur Event-Liste zurück und wähle dort ein Event aus. ); } const actions = ( ); return ( {error && ( {t('photos.alerts.errorTitle', 'Aktion fehlgeschlagen')} {error} )} {eventAddons.length > 0 && ( {t('events.sections.addons.title', 'Add-ons & Upgrades')} {t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')} t(key as any, fallback)} /> )} {t('photos.gallery.title', 'Galerie')} {t('photos.gallery.description', 'Klick auf ein Foto, um es hervorzuheben oder zu löschen.')} {loading ? ( ) : photos.length === 0 ? ( ) : (
{photos.map((photo) => (
{photo.original_name {photo.is_featured && ( Featured )}
Likes: {photo.likes_count} Uploader: {photo.uploader_name ?? 'Unbekannt'}
))}
)}
); } function LimitWarningsBanner({ limits, translate, eventSlug, addons, }: { limits: EventLimitSummary | null; translate: (key: string, options?: Record) => string; eventSlug: string | null; addons: EventAddonCatalogItem[]; }) { const warnings = React.useMemo(() => buildLimitWarnings(limits, translate), [limits, translate]); const [busyScope, setBusyScope] = React.useState(null); const handleCheckout = React.useCallback( async (scopeOrKey: 'photos' | 'gallery' | string) => { if (!eventSlug) return; const scope = scopeOrKey === 'gallery' || scopeOrKey === 'photos' ? scopeOrKey : (scopeOrKey.includes('gallery') ? 'gallery' : 'photos'); setBusyScope(scope); const addonKey = scopeOrKey === 'photos' || scopeOrKey === 'gallery' ? (() => { const fallbackKey = scope === 'photos' ? 'extra_photos_500' : 'extend_gallery_30d'; const candidates = addons.filter((addon) => addon.price_id && addon.key.includes(scope === 'photos' ? 'photos' : 'gallery')); return candidates[0]?.key ?? fallbackKey; })() : scopeOrKey; try { const currentUrl = window.location.origin + window.location.pathname; const successUrl = `${currentUrl}?addon_success=1`; const checkout = await createEventAddonCheckout(eventSlug, { addon_key: addonKey, quantity: 1, success_url: successUrl, cancel_url: currentUrl, }); if (checkout.checkout_url) { window.location.href = checkout.checkout_url; } } catch (err) { toast(getApiErrorMessage(err, 'Checkout fehlgeschlagen.')); } finally { setBusyScope(null); } }, [eventSlug, addons], ); if (!warnings.length) { return null; } return (
{warnings.map((warning) => (
{warning.message} {warning.scope === 'photos' || warning.scope === 'gallery' ? (
{ void handleCheckout(key); }} busy={busyScope === warning.scope} t={(key, fallback) => translate(key, { defaultValue: fallback })} />
) : null}
))}
); } function GallerySkeleton() { return (
{Array.from({ length: 6 }).map((_, index) => (
))}
); } function EmptyGallery({ title, description }: { title: string; description: string }) { return (

{title}

{description}

); }