Files
fotospiel-app/resources/js/admin/lib/brandingForm.ts
Codex Agent e39ddd2143
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Adjust branding defaults and tenant presets
2026-01-30 18:15:52 +01:00

150 lines
5.3 KiB
TypeScript

export type BrandingFormValues = {
primary: string;
accent: string;
background: string;
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;
};
export type BrandingFormDefaults = Pick<
BrandingFormValues,
| 'primary'
| 'accent'
| 'background'
| 'surface'
| 'headingFont'
| 'bodyFont'
| 'mode'
| 'buttonStyle'
| 'buttonRadius'
| 'buttonPrimary'
| 'buttonSecondary'
| 'linkColor'
| 'fontSize'
| 'logoMode'
| 'logoPosition'
| 'logoSize'
>;
type BrandingRecord = Record<string, unknown>;
const isRecord = (value: unknown): value is BrandingRecord =>
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
const readHexColor = (value: unknown, fallback: string): string => {
if (typeof value === 'string' && value.trim().startsWith('#')) {
return value.trim();
}
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(
palette.secondary,
readHexColor(branding.secondary_color, readHexColor(branding.accent_color, defaults.accent))
);
const background = readHexColor(palette.background, readHexColor(branding.background_color, defaults.background));
const surface = readHexColor(palette.surface, readHexColor(branding.surface_color, background));
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 = 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,
accent,
background,
surface,
headingFont: headingFont ?? defaults.headingFont ?? '',
bodyFont: bodyFont ?? defaults.bodyFont ?? '',
fontSize,
logoDataUrl: resolveAssetPreviewUrl(logoUploadValue),
logoValue,
logoMode,
logoPosition,
logoSize,
mode,
buttonStyle,
buttonRadius,
buttonPrimary,
buttonSecondary,
linkColor,
};
}