import React from 'react'; import { useNavigate } from 'react-router-dom'; import { CalendarDays, MapPin, Plus, Search, Camera, Users, Sparkles } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { useTranslation } from 'react-i18next'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, PillBadge, CTAButton, FloatingActionButton, SkeletonCard } from './components/Primitives'; import { MobileInput } from './components/FormControls'; import { getEvents, TenantEvent } from '../api'; import { adminPath } from '../constants'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { useBackNavigation } from './hooks/useBackNavigation'; import { buildEventStatusCounts, filterEventsByStatus, resolveEventStatusKey, type EventStatusKey } from './lib/eventFilters'; import { buildEventListStats } from './lib/eventListStats'; import { useAdminTheme } from './theme'; export default function MobileEventsPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const [events, setEvents] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [query, setQuery] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); const searchRef = React.useRef(null); const back = useBackNavigation(); const { text, muted, subtle, border, primary, danger, surface, accentSoft, accent } = useAdminTheme(); React.useEffect(() => { (async () => { try { setEvents(await getEvents()); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.'))); } } finally { setLoading(false); } })(); }, [t]); return ( searchRef.current?.focus()} ariaLabel={t('events.list.search', 'Search events')}> } > {error ? ( {error} ) : null} setQuery(e.target.value)} placeholder={t('events.list.search', 'Search events')} compact style={{ marginBottom: 12 }} /> {loading ? ( {Array.from({ length: 3 }).map((_, idx) => ( ))} ) : events.length === 0 ? ( {t('events.list.empty.title', 'Noch kein Event angelegt')} {t('events.list.empty.description', 'Starte jetzt mit deinem ersten Event.')} navigate(adminPath('/events/new'))} /> ) : ( navigate(adminPath(`/mobile/events/${slug}`))} onEdit={(slug) => navigate(adminPath(`/mobile/events/${slug}/edit`))} /> )} navigate(adminPath('/mobile/events/new'))} /> ); } function EventsList({ events, query, statusFilter, onStatusChange, onOpen, onEdit, }: { events: TenantEvent[]; query: string; statusFilter: EventStatusKey; onStatusChange: (value: EventStatusKey) => void; onOpen: (slug: string) => void; onEdit: (slug: string) => void; }) { const { t } = useTranslation('management'); const { text, muted, subtle, border, primary, surface, accentSoft, accent } = useAdminTheme(); const activeBg = accentSoft; const activeBorder = accent; const statusCounts = React.useMemo(() => buildEventStatusCounts(events), [events]); const filteredByStatus = React.useMemo( () => filterEventsByStatus(events, statusFilter), [events, statusFilter] ); const filteredEvents = React.useMemo(() => { if (!query.trim()) return filteredByStatus; const needle = query.toLowerCase(); return filteredByStatus.filter((event) => { const hay = `${event.name ?? ''} ${event.location ?? ''}`.toLowerCase(); return hay.includes(needle); }); }, [filteredByStatus, query]); const filters: Array<{ key: EventStatusKey; label: string; count: number }> = [ { key: 'all', label: t('events.list.filters.all', 'All'), count: statusCounts.all }, { key: 'upcoming', label: t('events.list.filters.upcoming', 'Upcoming'), count: statusCounts.upcoming }, { key: 'draft', label: t('events.list.filters.draft', 'Draft'), count: statusCounts.draft }, { key: 'past', label: t('events.list.filters.past', 'Past'), count: statusCounts.past }, ]; return ( {filters.map((filter) => { const active = filter.key === statusFilter; return ( onStatusChange(filter.key)} style={{ flexGrow: 1 }}> {filter.label} {filter.count} ); })} {filteredEvents.length === 0 ? ( {t('events.list.empty.filtered', 'No events match this filter.')} {t('events.list.empty.filteredHint', 'Try a different status or clear your search.')} onStatusChange('all')} /> ) : ( filteredEvents.map((event) => { const statusKey = resolveEventStatusKey(event); const statusLabel = statusKey === 'draft' ? t('events.list.filters.draft', 'Draft') : statusKey === 'past' ? t('events.list.filters.past', 'Past') : t('events.list.filters.upcoming', 'Upcoming'); const statusTone = statusKey === 'draft' ? 'warning' : statusKey === 'past' ? 'muted' : 'success'; return ( ); }) )} ); } function EventRow({ event, text, muted, subtle, border, primary, statusLabel, statusTone, onOpen, onEdit, }: { event: TenantEvent; text: string; muted: string; subtle: string; border: string; primary: string; statusLabel: string; statusTone: 'success' | 'warning' | 'muted'; onOpen: (slug: string) => void; onEdit: (slug: string) => void; }) { const { t } = useTranslation('management'); const stats = buildEventListStats(event); return ( {renderName(event.name)} {formatDate(event.event_date)} {resolveLocation(event)} {statusLabel} onEdit(event.slug)}> ˅ onOpen(event.slug)} style={{ marginTop: 8 }}> {t('events.list.actions.open', 'Open event')} ); } function EventStatChip({ icon: Icon, label, value, muted, }: { icon: typeof Camera; label: string; value: number; muted: string; }) { return ( {value} {label} ); } function renderName(name: TenantEvent['name']): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Unbenanntes Event'; } return 'Unbenanntes Event'; } function resolveLocation(event: TenantEvent): string { const settings = (event.settings ?? {}) as Record; const candidate = (settings.location as string | undefined) ?? (settings.address as string | undefined) ?? (settings.city as string | undefined); if (candidate && candidate.trim()) { return candidate; } return 'Location'; } function formatDate(iso: string | null): string { if (!iso) return 'Date tbd'; const date = new Date(iso); if (Number.isNaN(date.getTime())) { return 'Date tbd'; } return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }