Wire guest branding theme
This commit is contained in:
@@ -16,13 +16,22 @@ import toast from 'react-hot-toast';
|
||||
import { adminPath } from '../constants';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
import { ADMIN_COLORS, ADMIN_GRADIENTS, useAdminTheme } from './theme';
|
||||
import { extractBrandingForm, type BrandingFormValues } from '../lib/brandingForm';
|
||||
import { getContrastingTextColor } from '@/guest/lib/color';
|
||||
|
||||
type BrandingForm = {
|
||||
primary: string;
|
||||
accent: string;
|
||||
headingFont: string;
|
||||
bodyFont: string;
|
||||
logoDataUrl: string;
|
||||
const BRANDING_FORM_DEFAULTS = {
|
||||
primary: ADMIN_COLORS.primary,
|
||||
accent: ADMIN_COLORS.accent,
|
||||
background: '#ffffff',
|
||||
surface: '#ffffff',
|
||||
mode: 'auto' as const,
|
||||
};
|
||||
|
||||
const BRANDING_FORM_BASE: BrandingFormValues = {
|
||||
...BRANDING_FORM_DEFAULTS,
|
||||
headingFont: '',
|
||||
bodyFont: '',
|
||||
logoDataUrl: '',
|
||||
};
|
||||
|
||||
type WatermarkPosition =
|
||||
@@ -58,13 +67,7 @@ export default function MobileBrandingPage() {
|
||||
const { textStrong, muted, subtle, border, primary, accentSoft, danger, surfaceMuted, surface } = useAdminTheme();
|
||||
|
||||
const [event, setEvent] = React.useState<TenantEvent | null>(null);
|
||||
const [form, setForm] = React.useState<BrandingForm>({
|
||||
primary: ADMIN_COLORS.primary,
|
||||
accent: ADMIN_COLORS.accent,
|
||||
headingFont: '',
|
||||
bodyFont: '',
|
||||
logoDataUrl: '',
|
||||
});
|
||||
const [form, setForm] = React.useState<BrandingFormValues>(BRANDING_FORM_BASE);
|
||||
const [watermarkForm, setWatermarkForm] = React.useState<WatermarkForm>({
|
||||
mode: 'base',
|
||||
assetPath: '',
|
||||
@@ -94,7 +97,7 @@ export default function MobileBrandingPage() {
|
||||
try {
|
||||
const data = await getEvent(slug);
|
||||
setEvent(data);
|
||||
setForm(extractBranding(data));
|
||||
setForm(extractBrandingForm(data.settings ?? {}, BRANDING_FORM_DEFAULTS));
|
||||
setWatermarkForm(extractWatermark(data));
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
@@ -122,6 +125,7 @@ export default function MobileBrandingPage() {
|
||||
const previewTitle = event ? renderName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event');
|
||||
const previewHeadingFont = form.headingFont || 'Fraunces';
|
||||
const previewBodyFont = form.bodyFont || 'Manrope';
|
||||
const previewSurfaceText = getContrastingTextColor(form.surface, '#ffffff', '#0f172a');
|
||||
const watermarkAllowed = event?.package?.watermark_allowed !== false;
|
||||
const brandingAllowed = isBrandingAllowed(event ?? null);
|
||||
const watermarkLocked = watermarkAllowed && !brandingAllowed;
|
||||
@@ -158,9 +162,13 @@ export default function MobileBrandingPage() {
|
||||
settings.branding = {
|
||||
...(typeof settings.branding === 'object' ? (settings.branding as Record<string, unknown>) : {}),
|
||||
primary_color: form.primary,
|
||||
secondary_color: form.accent,
|
||||
accent_color: form.accent,
|
||||
background_color: form.background,
|
||||
surface_color: form.surface,
|
||||
heading_font: form.headingFont,
|
||||
body_font: form.bodyFont,
|
||||
mode: form.mode,
|
||||
typography: {
|
||||
...(typeof (settings.branding as Record<string, unknown> | undefined)?.typography === 'object'
|
||||
? ((settings.branding as Record<string, unknown>).typography as Record<string, unknown>)
|
||||
@@ -174,6 +182,8 @@ export default function MobileBrandingPage() {
|
||||
: {}),
|
||||
primary: form.primary,
|
||||
secondary: form.accent,
|
||||
background: form.background,
|
||||
surface: form.surface,
|
||||
},
|
||||
logo_data_url: form.logoDataUrl || null,
|
||||
logo: form.logoDataUrl
|
||||
@@ -217,7 +227,7 @@ export default function MobileBrandingPage() {
|
||||
|
||||
function handleReset() {
|
||||
if (event) {
|
||||
setForm(extractBranding(event));
|
||||
setForm(extractBrandingForm(event.settings ?? {}, BRANDING_FORM_DEFAULTS));
|
||||
setWatermarkForm(extractWatermark(event));
|
||||
}
|
||||
}
|
||||
@@ -435,25 +445,50 @@ export default function MobileBrandingPage() {
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{t('events.branding.previewTitle', 'Guest App Preview')}
|
||||
</Text>
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={border} backgroundColor={surfaceMuted} padding="$3" space="$2" alignItems="center">
|
||||
<YStack width="100%" borderRadius={12} backgroundColor={surface} borderWidth={1} borderColor={border} overflow="hidden">
|
||||
<YStack borderRadius={16} borderWidth={1} borderColor={border} backgroundColor={form.background} padding="$3" space="$2" alignItems="center">
|
||||
<YStack width="100%" borderRadius={12} backgroundColor={form.surface} borderWidth={1} borderColor={border} overflow="hidden">
|
||||
<YStack backgroundColor={form.primary} height={64} />
|
||||
<YStack padding="$3" space="$1.5">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong} style={{ fontFamily: previewHeadingFont }}>
|
||||
<Text fontSize="$md" fontWeight="800" color={previewSurfaceText} style={{ fontFamily: previewHeadingFont }}>
|
||||
{previewTitle}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted} style={{ fontFamily: previewBodyFont }}>
|
||||
<Text fontSize="$sm" color={previewSurfaceText} style={{ fontFamily: previewBodyFont, opacity: 0.7 }}>
|
||||
{t('events.branding.previewSubtitle', 'Aktuelle Farben & Schriften')}
|
||||
</Text>
|
||||
<XStack space="$2" marginTop="$1">
|
||||
<ColorSwatch color={form.primary} label={t('events.branding.primary', 'Primary')} />
|
||||
<ColorSwatch color={form.accent} label={t('events.branding.accent', 'Accent')} />
|
||||
<ColorSwatch color={form.background} label={t('events.branding.background', 'Background')} />
|
||||
<ColorSwatch color={form.surface} label={t('events.branding.surface', 'Surface')} />
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.mode', 'Theme')}
|
||||
</Text>
|
||||
<XStack space="$2">
|
||||
<ModeButton
|
||||
label={t('events.branding.modeLight', 'Light')}
|
||||
active={form.mode === 'light'}
|
||||
onPress={() => setForm((prev) => ({ ...prev, mode: 'light' }))}
|
||||
/>
|
||||
<ModeButton
|
||||
label={t('events.branding.modeAuto', 'Auto')}
|
||||
active={form.mode === 'auto'}
|
||||
onPress={() => setForm((prev) => ({ ...prev, mode: 'auto' }))}
|
||||
/>
|
||||
<ModeButton
|
||||
label={t('events.branding.modeDark', 'Dark')}
|
||||
active={form.mode === 'dark'}
|
||||
onPress={() => setForm((prev) => ({ ...prev, mode: 'dark' }))}
|
||||
/>
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{t('events.branding.colors', 'Colors')}
|
||||
@@ -468,6 +503,16 @@ export default function MobileBrandingPage() {
|
||||
value={form.accent}
|
||||
onChange={(value) => setForm((prev) => ({ ...prev, accent: value }))}
|
||||
/>
|
||||
<ColorField
|
||||
label={t('events.branding.backgroundColor', 'Background Color')}
|
||||
value={form.background}
|
||||
onChange={(value) => setForm((prev) => ({ ...prev, background: value }))}
|
||||
/>
|
||||
<ColorField
|
||||
label={t('events.branding.surfaceColor', 'Surface Color')}
|
||||
value={form.surface}
|
||||
onChange={(value) => setForm((prev) => ({ ...prev, surface: value }))}
|
||||
/>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
@@ -671,26 +716,6 @@ export default function MobileBrandingPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function extractBranding(event: TenantEvent): BrandingForm {
|
||||
const source = (event.settings as Record<string, unknown>) ?? {};
|
||||
const branding = (source.branding as Record<string, unknown>) ?? 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', ADMIN_COLORS.primary),
|
||||
accent: readColor('accent_color', ADMIN_COLORS.accent),
|
||||
headingFont: readText('heading_font'),
|
||||
bodyFont: readText('body_font'),
|
||||
logoDataUrl: readText('logo_data_url'),
|
||||
};
|
||||
}
|
||||
|
||||
function extractWatermark(event: TenantEvent): WatermarkForm {
|
||||
const settings = (event.settings as Record<string, unknown>) ?? {};
|
||||
const wm = (settings.watermark as Record<string, unknown>) ?? {};
|
||||
@@ -1078,3 +1103,24 @@ function TabButton({ label, active, onPress }: { label: string; active: boolean;
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function ModeButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
||||
const { backdrop, surfaceMuted, border, surface } = useAdminTheme();
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ flex: 1 }}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
paddingVertical="$2"
|
||||
borderRadius={10}
|
||||
backgroundColor={active ? backdrop : surfaceMuted}
|
||||
borderWidth={1}
|
||||
borderColor={active ? backdrop : border}
|
||||
>
|
||||
<Text fontSize="$xs" color={active ? surface : backdrop} fontWeight="700">
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user