import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3 } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Switch } from '@tamagui/switch'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { getEvent, getEventPhotoboothStatus, enableEventPhotobooth, disableEventPhotobooth, rotateEventPhotobooth, PhotoboothStatus, TenantEvent, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { formatEventDate, resolveEventDisplayName } from '../lib/events'; import toast from 'react-hot-toast'; import { adminPath } from '../constants'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; export default function MobileEventPhotoboothPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const navigate = useNavigate(); const { t, i18n } = useTranslation('management'); const { text, muted, border, surface, danger } = useAdminTheme(); const [event, setEvent] = React.useState(null); const [status, setStatus] = React.useState(null); const [selectedMode, setSelectedMode] = React.useState<'ftp' | 'sparkbooth'>('ftp'); const [loading, setLoading] = React.useState(true); const [updating, setUpdating] = React.useState(false); const [error, setError] = React.useState(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; const load = React.useCallback(async () => { if (!slug) return; setLoading(true); setError(null); try { const [eventData, statusData] = await Promise.all([getEvent(slug), getEventPhotoboothStatus(slug)]); setEvent(eventData); setStatus(statusData); setSelectedMode(statusData.mode ?? 'ftp'); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('management.photobooth.errors.loadFailed', 'Photobooth-Link konnte nicht geladen werden.'))); } } finally { setLoading(false); } }, [slug, t]); React.useEffect(() => { void load(); }, [load]); React.useEffect(() => { if (status?.mode) { setSelectedMode(status.mode); } }, [status?.mode]); const handleEnable = async (mode?: 'ftp' | 'sparkbooth') => { if (!slug) return; const nextMode = mode ?? selectedMode ?? status?.mode ?? 'ftp'; setUpdating(true); try { const result = await enableEventPhotobooth(slug, { mode: nextMode }); setStatus(result); setSelectedMode(result.mode ?? nextMode); toast.success(t('management.photobooth.actions.enable', 'Zugang aktiviert')); } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('management.photobooth.errors.enableFailed', 'Zugang konnte nicht aktiviert werden.'))); } } finally { setUpdating(false); } }; const handleDisable = async () => { if (!slug) return; const mode = status?.mode ?? selectedMode ?? 'ftp'; setUpdating(true); try { const result = await disableEventPhotobooth(slug, { mode }); setStatus(result); setSelectedMode(result.mode ?? mode); toast.success(t('management.photobooth.actions.disable', 'Zugang deaktiviert')); } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('management.photobooth.errors.disableFailed', 'Zugang konnte nicht deaktiviert werden.'))); } } finally { setUpdating(false); } }; const handleRotate = async () => { if (!slug) return; const mode = selectedMode ?? status?.mode ?? 'ftp'; setUpdating(true); try { const result = await rotateEventPhotobooth(slug, { mode }); setStatus(result); setSelectedMode(result.mode ?? mode); toast.success(t('management.photobooth.presets.actions.rotate', 'Zugang zurückgesetzt')); } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('management.photobooth.errors.rotateFailed', 'Zugangsdaten konnten nicht neu generiert werden.'))); } } finally { setUpdating(false); } }; const activeMode = selectedMode ?? status?.mode ?? 'ftp'; const isSpark = activeMode === 'sparkbooth'; const spark = status?.sparkbooth ?? null; const ftp = status?.ftp ?? null; const metrics = isSpark ? spark?.metrics ?? null : status?.metrics ?? null; const expiresAt = isSpark ? spark?.expires_at ?? status?.expires_at : status?.expires_at ?? spark?.expires_at; const lastUploadAt = metrics?.last_upload_at; const uploads24h = metrics?.uploads_24h ?? metrics?.uploads_today; const uploadsTotal = metrics?.uploads_total; const connectionPath = status?.path ?? '—'; const ftpUrl = status?.ftp_url ?? '—'; const uploadUrl = isSpark ? spark?.upload_url ?? status?.upload_url : null; const responseFormat = spark?.response_format ?? 'json'; const username = isSpark ? spark?.username ?? status?.username : status?.username ?? spark?.username ?? null; const password = isSpark ? spark?.password ?? status?.password : status?.password ?? spark?.password ?? null; const modeLabel = activeMode === 'sparkbooth' ? t('photobooth.credentials.sparkboothTitle', 'Sparkbooth / HTTP') : t('photobooth.credentials.heading', 'FTP (Classic)'); const isActive = Boolean(status?.enabled); const title = event ? resolveEventDisplayName(event) : t('management.header.appName', 'Event Admin'); const subtitle = event?.event_date ? formatEventDate(event.event_date, locale) : t('header.selectEvent', 'Select an event to continue'); const handleToggle = (checked: boolean) => { if (!slug || updating) return; if (checked) { void handleEnable(status?.mode ?? 'ftp'); } else { void handleDisable(); } }; return ( load()} ariaLabel={t('common.refresh', 'Refresh')}> } > {error ? ( {error} ) : null} {loading ? ( {Array.from({ length: 3 }).map((_, idx) => ( ))} ) : ( {t('photobooth.title', 'Photobooth')} {t('photobooth.credentials.description', 'Share these credentials with your photobooth software.')} {t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })} {isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')} {isActive ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')} {t('photobooth.stats.lastUpload', 'Last upload')} {lastUploadAt ? formatEventDate(lastUploadAt, locale) : t('photobooth.status.never', 'Never')} {t('photobooth.status.expires', 'Access expires')} {expiresAt ? formatEventDate(expiresAt, locale) : '—'} {t('photobooth.selector.title', 'Choose adapter')} {t( 'photobooth.selector.description', 'FTP (Classic) works with most booths. Sparkbooth uses HTTP POST without FTP.' )} setSelectedMode('ftp')} disabled={updating} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> setSelectedMode('sparkbooth')} disabled={updating} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> {isSpark ? t('photobooth.credentials.sparkboothTitle', 'Sparkbooth upload (HTTP)') : t('photobooth.credentials.heading', 'FTP credentials')} {!isSpark && ftp?.require_ftps ? {t('photobooth.credentials.ftps', 'FTPS required')} : null} {isSpark ? ( <> {t('photobooth.sparkbooth.hint', 'POST with media file or base64 "media" field; username/password required.')} ) : ( <> {t('photobooth.credentials.ftpsHint', 'Use FTPS if required; uploads go into the target folder for this event.')} )} handleRotate()} iconLeft={} disabled={updating} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> (isActive ? handleDisable() : handleEnable(selectedMode))} tone={isActive ? 'ghost' : 'primary'} iconLeft={isActive ? : } disabled={updating} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> {t('photobooth.status.heading', 'Status')} } label={t('photobooth.status.mode', 'Mode')} value={modeLabel} /> } label={t('photobooth.status.heading', 'Status')} value={isActive ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')} /> } label={t('photobooth.rateLimit.label', 'Rate limit (uploads/min)')} value={status?.rate_limit_per_minute != null ? String(status.rate_limit_per_minute) : '—'} /> } label={t('photobooth.status.expires', 'Access expires')} value={expiresAt ? formatEventDate(expiresAt, locale) ?? '—' : '—'} /> {lastUploadAt ? ( } label={t('photobooth.stats.lastUpload', 'Last upload')} value={formatEventDate(lastUploadAt, locale) ?? '—'} /> ) : null} {uploads24h != null ? ( } label={t('photobooth.stats.uploads24h', 'Uploads last 24h')} value={String(uploads24h)} /> ) : null} {uploadsTotal != null ? ( } label={t('photobooth.stats.uploadsTotal', 'Uploads total')} value={String(uploadsTotal)} /> ) : null} )} ); } function CredentialRow({ label, value, border, masked }: { label: string; value: string; border: string; masked?: boolean }) { const { t } = useTranslation('management'); const { muted, text } = useAdminTheme(); return ( {label} {masked ? '••••••••' : value} { try { await navigator.clipboard.writeText(value); toast.success(t('common.copied', 'Kopiert')); } catch { toast.error(t('common.copyFailed', 'Kopieren fehlgeschlagen')); } }} > ); } function StatusRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { const { text } = useAdminTheme(); return ( {icon} {label} {value} ); }