import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Image as ImageIcon, RefreshCcw, Save } 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 { MobileScaffold } from './components/Scaffold'; import { MobileCard, CTAButton } from './components/Primitives'; import { BottomNav } from './components/BottomNav'; import { TenantEvent, getEvent, updateEvent, getTenantFonts, TenantFont } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { useMobileNav } from './hooks/useMobileNav'; import { MobileSheet } from './components/Sheet'; type BrandingForm = { primary: string; accent: string; headingFont: string; bodyFont: string; }; export default function MobileBrandingPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); const [event, setEvent] = React.useState(null); const [form, setForm] = React.useState({ primary: '#007AFF', accent: '#5AD2F4', headingFont: '', bodyFont: '', }); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const { go } = useMobileNav(slug); const [showFontsSheet, setShowFontsSheet] = React.useState(false); const [fonts, setFonts] = React.useState([]); const [fontsLoading, setFontsLoading] = React.useState(false); React.useEffect(() => { if (!slug) return; (async () => { setLoading(true); try { const data = await getEvent(slug); setEvent(data); setForm(extractBranding(data)); setError(null); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Branding konnte nicht geladen werden.'))); } } finally { setLoading(false); } })(); }, [slug, t]); React.useEffect(() => { (async () => { setFontsLoading(true); try { const data = await getTenantFonts(); setFonts(data ?? []); } catch { // non-fatal } finally { setFontsLoading(false); } })(); }, []); const previewTitle = event ? renderName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); async function handleSave() { if (!event?.slug) return; setSaving(true); setError(null); try { const settings = { ...(event.settings ?? {}) }; settings.branding = { ...(typeof settings.branding === 'object' ? (settings.branding as Record) : {}), primary_color: form.primary, accent_color: form.accent, heading_font: form.headingFont, body_font: form.bodyFont, }; const updated = await updateEvent(event.slug, { settings }); setEvent(updated); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.saveFailed', 'Branding konnte nicht gespeichert werden.'))); } } finally { setSaving(false); } } function handleReset() { if (event) { setForm(extractBranding(event)); } } return ( navigate(-1)} rightSlot={ handleSave()}> } footer={ } > {error ? ( {error} ) : null} {t('events.branding.previewTitle', 'Guest App Preview')} {previewTitle} {t('events.branding.previewSubtitle', 'Aktuelle Farben & Schriften')} {t('events.branding.colors', 'Colors')} setForm((prev) => ({ ...prev, primary: value }))} /> setForm((prev) => ({ ...prev, accent: value }))} /> {t('events.branding.fonts', 'Fonts')} setForm((prev) => ({ ...prev, headingFont: value }))} /> setForm((prev) => ({ ...prev, bodyFont: value }))} /> setShowFontsSheet(true)}> {t('events.branding.chooseFont', 'Choose from installed fonts')} {t('events.branding.logo', 'Logo')} {t('events.branding.logoHint', 'Logo Upload folgt – nutze Farben/Schriften.')} handleSave()} /> {t('events.branding.reset', 'Reset to Defaults')} setShowFontsSheet(false)} title={t('events.branding.fontPicker', 'Select font')} footer={null} bottomOffsetPx={120} > {fontsLoading ? ( Array.from({ length: 4 }).map((_, idx) => ) ) : fonts.length === 0 ? ( {t('events.branding.noFonts', 'Keine Schriftarten gefunden.')} ) : ( fonts.map((font) => ( { setForm((prev) => ({ ...prev, headingFont: font.family, bodyFont: font.family })); setShowFontsSheet(false); }} > {font.family} {font.variants?.length ? ( {font.variants.map((v) => v.style ?? v.weight ?? '').filter(Boolean).join(', ')} ) : null} {form.headingFont === font.family || form.bodyFont === font.family ? ( {t('common.active', 'Active')} ) : null} )) )} ); } function extractBranding(event: TenantEvent): BrandingForm { const source = (event.settings as Record) ?? {}; const branding = (source.branding as Record) ?? source; const readColor = (key: string, fallback: string) => { const value = branding[key]; return typeof value === 'string' && value.startsWith('#') ? value : fallback; }; const readText = (key: string) => { const value = branding[key]; return typeof value === 'string' ? value : ''; }; return { primary: readColor('primary_color', '#007AFF'), accent: readColor('accent_color', '#5AD2F4'), headingFont: readText('heading_font'), bodyFont: readText('body_font'), }; } 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] ?? ''; } return ''; } function ColorField({ label, value, onChange }: { label: string; value: string; onChange: (next: string) => void }) { return ( {label} onChange(event.target.value)} style={{ width: 52, height: 52, borderRadius: 12, border: '1px solid #e5e7eb', background: 'white' }} /> {value} ); } function ColorSwatch({ color, label }: { color: string; label: string }) { return ( {label} ); } function InputField({ label, value, placeholder, onChange, }: { label: string; value: string; placeholder?: string; onChange: (next: string) => void; }) { return ( {label} onChange(event.target.value)} style={{ width: '100%', height: 48, borderRadius: 12, border: '1px solid #e5e7eb', padding: '0 12px', fontSize: 14, }} /> ); }