Expand branding controls and logo upload
This commit is contained in:
@@ -7,6 +7,15 @@ const defaults = {
|
||||
background: '#ffffff',
|
||||
surface: '#f0f0f0',
|
||||
mode: 'auto' as const,
|
||||
buttonStyle: 'filled' as const,
|
||||
buttonRadius: 12,
|
||||
buttonPrimary: '#111111',
|
||||
buttonSecondary: '#222222',
|
||||
linkColor: '#222222',
|
||||
fontSize: 'm' as const,
|
||||
logoMode: 'upload' as const,
|
||||
logoPosition: 'left' as const,
|
||||
logoSize: 'm' as const,
|
||||
};
|
||||
|
||||
describe('extractBrandingForm', () => {
|
||||
@@ -34,6 +43,7 @@ describe('extractBrandingForm', () => {
|
||||
expect(result.background).toBe('#000000');
|
||||
expect(result.surface).toBe('#111111');
|
||||
expect(result.mode).toBe('dark');
|
||||
expect(result.fontSize).toBe('m');
|
||||
});
|
||||
|
||||
it('falls back to legacy keys and defaults', () => {
|
||||
@@ -52,5 +62,62 @@ describe('extractBrandingForm', () => {
|
||||
expect(result.background).toBe('#abcdef');
|
||||
expect(result.surface).toBe('#abcdef');
|
||||
expect(result.mode).toBe('light');
|
||||
expect(result.buttonStyle).toBe(defaults.buttonStyle);
|
||||
expect(result.buttonRadius).toBe(defaults.buttonRadius);
|
||||
});
|
||||
|
||||
it('extracts buttons, logo, and typography settings', () => {
|
||||
const settings = {
|
||||
branding: {
|
||||
typography: {
|
||||
heading: 'Display Font',
|
||||
body: 'Body Font',
|
||||
size: 'l',
|
||||
},
|
||||
buttons: {
|
||||
style: 'outline',
|
||||
radius: 24,
|
||||
primary: '#333333',
|
||||
secondary: '#444444',
|
||||
link_color: '#555555',
|
||||
},
|
||||
logo: {
|
||||
mode: 'emoticon',
|
||||
value: '🎉',
|
||||
position: 'center',
|
||||
size: 'l',
|
||||
},
|
||||
use_default_branding: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = extractBrandingForm(settings, defaults);
|
||||
|
||||
expect(result.headingFont).toBe('Display Font');
|
||||
expect(result.bodyFont).toBe('Body Font');
|
||||
expect(result.fontSize).toBe('l');
|
||||
expect(result.buttonStyle).toBe('outline');
|
||||
expect(result.buttonRadius).toBe(24);
|
||||
expect(result.buttonPrimary).toBe('#333333');
|
||||
expect(result.buttonSecondary).toBe('#444444');
|
||||
expect(result.linkColor).toBe('#555555');
|
||||
expect(result.logoMode).toBe('emoticon');
|
||||
expect(result.logoValue).toBe('🎉');
|
||||
expect(result.logoPosition).toBe('center');
|
||||
expect(result.logoSize).toBe('l');
|
||||
expect(result.useDefaultBranding).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes stored logo paths for previews', () => {
|
||||
const settings = {
|
||||
branding: {
|
||||
logo_url: 'branding/logos/event-123.png',
|
||||
logo_mode: 'upload',
|
||||
},
|
||||
};
|
||||
|
||||
const result = extractBrandingForm(settings, defaults);
|
||||
|
||||
expect(result.logoDataUrl).toBe('/storage/branding/logos/event-123.png');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,38 @@ export type BrandingFormValues = {
|
||||
surface: string;
|
||||
headingFont: string;
|
||||
bodyFont: string;
|
||||
fontSize: 's' | 'm' | 'l';
|
||||
logoDataUrl: string;
|
||||
logoValue: string;
|
||||
logoMode: 'upload' | 'emoticon';
|
||||
logoPosition: 'left' | 'center' | 'right';
|
||||
logoSize: 's' | 'm' | 'l';
|
||||
mode: 'light' | 'dark' | 'auto';
|
||||
buttonStyle: 'filled' | 'outline';
|
||||
buttonRadius: number;
|
||||
buttonPrimary: string;
|
||||
buttonSecondary: string;
|
||||
linkColor: string;
|
||||
useDefaultBranding: boolean;
|
||||
};
|
||||
|
||||
export type BrandingFormDefaults = Pick<BrandingFormValues, 'primary' | 'accent' | 'background' | 'surface' | 'mode'>;
|
||||
export type BrandingFormDefaults = Pick<
|
||||
BrandingFormValues,
|
||||
| 'primary'
|
||||
| 'accent'
|
||||
| 'background'
|
||||
| 'surface'
|
||||
| 'mode'
|
||||
| 'buttonStyle'
|
||||
| 'buttonRadius'
|
||||
| 'buttonPrimary'
|
||||
| 'buttonSecondary'
|
||||
| 'linkColor'
|
||||
| 'fontSize'
|
||||
| 'logoMode'
|
||||
| 'logoPosition'
|
||||
| 'logoSize'
|
||||
>;
|
||||
|
||||
type BrandingRecord = Record<string, unknown>;
|
||||
|
||||
@@ -23,11 +50,48 @@ const readHexColor = (value: unknown, fallback: string): string => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const readEnum = <T extends string>(value: unknown, allowed: readonly T[], fallback: T): T => (
|
||||
allowed.includes(value as T) ? (value as T) : fallback
|
||||
);
|
||||
|
||||
const readNumber = (value: unknown, fallback: number): number => (
|
||||
typeof value === 'number' && !Number.isNaN(value) ? value : fallback
|
||||
);
|
||||
|
||||
const resolveAssetPreviewUrl = (value: string | null | undefined): string => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('data:') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const normalized = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed;
|
||||
|
||||
if (normalized.startsWith('storage/')) {
|
||||
return `/${normalized}`;
|
||||
}
|
||||
|
||||
if (normalized.startsWith('branding/') || normalized.startsWith('tenant-branding/')) {
|
||||
return `/storage/${normalized}`;
|
||||
}
|
||||
|
||||
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
||||
};
|
||||
|
||||
export function extractBrandingForm(settings: unknown, defaults: BrandingFormDefaults): BrandingFormValues {
|
||||
const settingsRecord = isRecord(settings) ? settings : {};
|
||||
const branding = isRecord(settingsRecord.branding) ? (settingsRecord.branding as BrandingRecord) : settingsRecord;
|
||||
const palette = isRecord(branding.palette) ? (branding.palette as BrandingRecord) : {};
|
||||
const typography = isRecord(branding.typography) ? (branding.typography as BrandingRecord) : {};
|
||||
const buttons = isRecord(branding.buttons) ? (branding.buttons as BrandingRecord) : {};
|
||||
const logo = isRecord(branding.logo) ? (branding.logo as BrandingRecord) : {};
|
||||
|
||||
const primary = readHexColor(palette.primary, readHexColor(branding.primary_color, defaults.primary));
|
||||
const accent = readHexColor(
|
||||
@@ -39,9 +103,27 @@ export function extractBrandingForm(settings: unknown, defaults: BrandingFormDef
|
||||
|
||||
const headingFont = typeof typography.heading === 'string' ? typography.heading : (branding.heading_font as string | undefined);
|
||||
const bodyFont = typeof typography.body === 'string' ? typography.body : (branding.body_font as string | undefined);
|
||||
const mode = branding.mode === 'light' || branding.mode === 'dark' || branding.mode === 'auto'
|
||||
? branding.mode
|
||||
: defaults.mode;
|
||||
const mode = readEnum(branding.mode, ['light', 'dark', 'auto'], defaults.mode);
|
||||
const fontSize = readEnum(typography.size ?? branding.font_size, ['s', 'm', 'l'], defaults.fontSize);
|
||||
|
||||
const logoMode = readEnum(logo.mode ?? branding.logo_mode, ['upload', 'emoticon'], defaults.logoMode);
|
||||
const logoValue = logoMode === 'emoticon'
|
||||
? (typeof logo.value === 'string' ? logo.value : (branding.logo_value as string | undefined) ?? '')
|
||||
: '';
|
||||
const logoPosition = readEnum(logo.position ?? branding.logo_position, ['left', 'center', 'right'], defaults.logoPosition);
|
||||
const logoSize = readEnum(logo.size ?? branding.logo_size, ['s', 'm', 'l'], defaults.logoSize);
|
||||
const logoUploadValue =
|
||||
typeof branding.logo_data_url === 'string'
|
||||
? branding.logo_data_url
|
||||
: logoMode === 'upload'
|
||||
? (typeof logo.value === 'string' ? logo.value : (branding.logo_url as string | undefined))
|
||||
: undefined;
|
||||
|
||||
const buttonStyle = readEnum(buttons.style ?? branding.button_style, ['filled', 'outline'], defaults.buttonStyle);
|
||||
const buttonRadius = readNumber(buttons.radius ?? branding.button_radius, defaults.buttonRadius);
|
||||
const buttonPrimary = readHexColor(buttons.primary, readHexColor(branding.button_primary_color, primary));
|
||||
const buttonSecondary = readHexColor(buttons.secondary, readHexColor(branding.button_secondary_color, accent));
|
||||
const linkColor = readHexColor(buttons.link_color ?? buttons.linkColor, readHexColor(branding.link_color, accent));
|
||||
|
||||
return {
|
||||
primary,
|
||||
@@ -50,7 +132,18 @@ export function extractBrandingForm(settings: unknown, defaults: BrandingFormDef
|
||||
surface,
|
||||
headingFont: headingFont ?? '',
|
||||
bodyFont: bodyFont ?? '',
|
||||
logoDataUrl: typeof branding.logo_data_url === 'string' ? branding.logo_data_url : '',
|
||||
fontSize,
|
||||
logoDataUrl: resolveAssetPreviewUrl(logoUploadValue),
|
||||
logoValue,
|
||||
logoMode,
|
||||
logoPosition,
|
||||
logoSize,
|
||||
mode,
|
||||
buttonStyle,
|
||||
buttonRadius,
|
||||
buttonPrimary,
|
||||
buttonSecondary,
|
||||
linkColor,
|
||||
useDefaultBranding: branding.use_default_branding === true,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user