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, createEventPhotoboothConnectCode, PhotoboothStatus, TenantEvent, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { formatEventDate, formatEventDateTime } 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 [loading, setLoading] = React.useState(true); const [updating, setUpdating] = React.useState(false); const [error, setError] = React.useState(null); const [connectCode, setConnectCode] = React.useState(null); const [connectExpiresAt, setConnectExpiresAt] = React.useState(null); const [connectLoading, setConnectLoading] = React.useState(false); 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); } 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]); const handleEnable = async () => { if (!slug) return; const nextMode = 'sparkbooth'; setUpdating(true); try { const result = await enableEventPhotobooth(slug, { mode: nextMode }); setStatus(result); 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 = 'sparkbooth'; setUpdating(true); try { const result = await disableEventPhotobooth(slug, { mode }); setStatus(result); 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 = 'sparkbooth'; setUpdating(true); try { const result = await rotateEventPhotobooth(slug, { mode }); setStatus(result); 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 handleGenerateConnectCode = async () => { if (!slug) return; setConnectLoading(true); try { const result = await createEventPhotoboothConnectCode(slug); setConnectCode(result.code || null); setConnectExpiresAt(result.expires_at ?? null); toast.success(t('photobooth.connectCode.actions.generated', 'Verbindungscode erstellt')); } catch (err) { if (!isAuthError(err)) { toast.error( getApiErrorMessage(err, t('photobooth.connectCode.errors.failed', 'Verbindungscode konnte nicht erstellt werden.')) ); } } finally { setConnectLoading(false); } }; const spark = status?.sparkbooth ?? null; const metrics = spark?.metrics ?? null; const expiresAt = spark?.expires_at ?? status?.expires_at; const lastUploadAt = metrics?.last_upload_at; const uploads24h = metrics?.uploads_24h ?? metrics?.uploads_today; const uploadsTotal = metrics?.uploads_total; const uploadUrl = spark?.upload_url ?? status?.upload_url; const username = spark?.username ?? status?.username ?? null; const password = spark?.password ?? status?.password ?? null; const modeLabel = t('photobooth.mode.uploader', 'Uploader App (HTTP)'); const isActive = Boolean(status?.enabled); const title = t('photobooth.title', 'Photobooth'); const handleToggle = (checked: boolean) => { if (!slug || updating) return; if (checked) { void handleEnable(); } 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', 'Connection')} {t('photobooth.selector.description', 'Use the Fotospiel uploader app for HTTP uploads.')} {t('photobooth.uploaderDownload.title', 'Fotospiel Uploader App')} {t( 'photobooth.uploaderDownload.description', 'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.' )} { const url = new URL('/downloads/PhotoboothUploader-win-x64.exe', window.location.origin).toString(); window.open(url, '_blank', 'noopener,noreferrer'); }} /> { const url = new URL('/downloads/PhotoboothUploader-macos-x64', window.location.origin).toString(); window.open(url, '_blank', 'noopener,noreferrer'); }} /> { const url = new URL('/downloads/PhotoboothUploader-linux-x64', window.location.origin).toString(); window.open(url, '_blank', 'noopener,noreferrer'); }} /> {t('photobooth.credentials.uploaderTitle', 'Uploader App (HTTP)')} {t('photobooth.uploader.hint', 'POST with media file or base64 "media" field; app uses these credentials.')} {t('photobooth.connectCode.description', 'Create a 6-digit code for the uploader app.')} } disabled={!isActive || updating || connectLoading} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> {connectCode ? ( ) : null} {connectExpiresAt ? ( {t('photobooth.connectCode.expires', 'Expires: {{date}}', { date: formatEventDateTime(connectExpiresAt, locale), })} ) : null} handleRotate()} iconLeft={} disabled={updating} style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} /> (isActive ? handleDisable() : handleEnable())} 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} ); }