Neue Branding-Page und Gäste-PWA reagiert nun auf Branding-Einstellungen vom event-admin. Implemented local Google Fonts pipeline and admin UI selects for branding and invites.
- Added fonts:sync-google command (uses GOOGLE_FONTS_API_KEY, generates /public/fonts/google files, manifest, CSS, cache flush) and
exposed manifest via new GET /api/v1/tenant/fonts endpoint with fallbacks for existing local fonts.
- Imported generated fonts CSS, added API client + font loader hook, and wired branding page font fields to searchable selects (with
custom override) that auto-load selected fonts.
- Invites layout editor now offers font selection per element with runtime font loading for previews/export alignment.
- New tests cover font sync command and font manifest API.
Tests run: php artisan test --filter=Fonts --testsuite=Feature.
Note: repository already has other modified files (e.g., EventPublicController, SettingsStoreRequest, guest components, etc.); left
untouched. Run php artisan fonts:sync-google after setting the API key to populate /public/fonts/google.
This commit is contained in:
@@ -12,6 +12,28 @@ export const DEFAULT_EVENT_BRANDING: EventBranding = {
|
||||
backgroundColor: '#ffffff',
|
||||
fontFamily: null,
|
||||
logoUrl: null,
|
||||
palette: {
|
||||
primary: '#f43f5e',
|
||||
secondary: '#fb7185',
|
||||
background: '#ffffff',
|
||||
surface: '#ffffff',
|
||||
},
|
||||
typography: {
|
||||
heading: null,
|
||||
body: null,
|
||||
sizePreset: 'm',
|
||||
},
|
||||
logo: {
|
||||
mode: 'emoticon',
|
||||
value: null,
|
||||
position: 'left',
|
||||
size: 'm',
|
||||
},
|
||||
buttons: {
|
||||
style: 'filled',
|
||||
radius: 12,
|
||||
},
|
||||
mode: 'auto',
|
||||
};
|
||||
const DEFAULT_PRIMARY = DEFAULT_EVENT_BRANDING.primaryColor.toLowerCase();
|
||||
const DEFAULT_SECONDARY = DEFAULT_EVENT_BRANDING.secondaryColor.toLowerCase();
|
||||
@@ -33,12 +55,50 @@ function resolveBranding(input?: EventBranding | null): EventBranding {
|
||||
return DEFAULT_EVENT_BRANDING;
|
||||
}
|
||||
|
||||
const palettePrimary = input.palette?.primary ?? input.primaryColor;
|
||||
const paletteSecondary = input.palette?.secondary ?? input.secondaryColor;
|
||||
const paletteBackground = input.palette?.background ?? input.backgroundColor;
|
||||
const paletteSurface = input.palette?.surface ?? input.backgroundColor;
|
||||
|
||||
const headingFont = input.typography?.heading ?? input.fontFamily ?? null;
|
||||
const bodyFont = input.typography?.body ?? input.fontFamily ?? null;
|
||||
const sizePreset = input.typography?.sizePreset ?? 'm';
|
||||
|
||||
const logoMode = input.logo?.mode ?? (input.logoUrl ? 'upload' : 'emoticon');
|
||||
const logoValue = input.logo?.value ?? input.logoUrl ?? null;
|
||||
|
||||
return {
|
||||
primaryColor: normaliseHexColor(input.primaryColor, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||
secondaryColor: normaliseHexColor(input.secondaryColor, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||
backgroundColor: normaliseHexColor(input.backgroundColor, DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||
fontFamily: input.fontFamily?.trim() || null,
|
||||
logoUrl: input.logoUrl?.trim() || null,
|
||||
primaryColor: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||
secondaryColor: normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||
backgroundColor: normaliseHexColor(paletteBackground, DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||
fontFamily: bodyFont?.trim() || null,
|
||||
logoUrl: logoMode === 'upload' ? (logoValue?.trim() || null) : null,
|
||||
palette: {
|
||||
primary: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||
secondary: normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||
background: normaliseHexColor(paletteBackground, DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||
surface: normaliseHexColor(paletteSurface, paletteBackground ?? DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||
},
|
||||
typography: {
|
||||
heading: headingFont?.trim() || null,
|
||||
body: bodyFont?.trim() || null,
|
||||
sizePreset,
|
||||
},
|
||||
logo: {
|
||||
mode: logoMode,
|
||||
value: logoMode === 'upload' ? (logoValue?.trim() || null) : (logoValue ?? null),
|
||||
position: input.logo?.position ?? 'left',
|
||||
size: input.logo?.size ?? 'm',
|
||||
},
|
||||
buttons: {
|
||||
style: input.buttons?.style ?? 'filled',
|
||||
radius: typeof input.buttons?.radius === 'number' ? input.buttons.radius : 12,
|
||||
primary: input.buttons?.primary ?? normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||
secondary: input.buttons?.secondary ?? normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||
linkColor: input.buttons?.linkColor ?? normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||
},
|
||||
mode: input.mode ?? 'auto',
|
||||
useDefaultBranding: input.useDefaultBranding ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,12 +111,26 @@ function applyCssVariables(branding: EventBranding) {
|
||||
root.style.setProperty('--guest-primary', branding.primaryColor);
|
||||
root.style.setProperty('--guest-secondary', branding.secondaryColor);
|
||||
root.style.setProperty('--guest-background', branding.backgroundColor);
|
||||
root.style.setProperty('--guest-surface', branding.palette?.surface ?? branding.backgroundColor);
|
||||
root.style.setProperty('--guest-button-radius', `${branding.buttons?.radius ?? 12}px`);
|
||||
root.style.setProperty('--guest-radius', `${branding.buttons?.radius ?? 12}px`);
|
||||
root.style.setProperty('--guest-link', branding.buttons?.linkColor ?? branding.secondaryColor);
|
||||
root.style.setProperty('--guest-button-style', branding.buttons?.style ?? 'filled');
|
||||
|
||||
if (branding.fontFamily) {
|
||||
root.style.setProperty('--guest-font-family', branding.fontFamily);
|
||||
root.style.setProperty('--guest-heading-font', branding.fontFamily);
|
||||
const headingFont = branding.typography?.heading ?? branding.fontFamily;
|
||||
const bodyFont = branding.typography?.body ?? branding.fontFamily;
|
||||
|
||||
if (bodyFont) {
|
||||
root.style.setProperty('--guest-font-family', bodyFont);
|
||||
root.style.setProperty('--guest-body-font', bodyFont);
|
||||
} else {
|
||||
root.style.removeProperty('--guest-font-family');
|
||||
root.style.removeProperty('--guest-body-font');
|
||||
}
|
||||
|
||||
if (headingFont) {
|
||||
root.style.setProperty('--guest-heading-font', headingFont);
|
||||
} else {
|
||||
root.style.removeProperty('--guest-heading-font');
|
||||
}
|
||||
}
|
||||
@@ -70,10 +144,66 @@ function resetCssVariables() {
|
||||
root.style.removeProperty('--guest-primary');
|
||||
root.style.removeProperty('--guest-secondary');
|
||||
root.style.removeProperty('--guest-background');
|
||||
root.style.removeProperty('--guest-surface');
|
||||
root.style.removeProperty('--guest-button-radius');
|
||||
root.style.removeProperty('--guest-radius');
|
||||
root.style.removeProperty('--guest-link');
|
||||
root.style.removeProperty('--guest-button-style');
|
||||
root.style.removeProperty('--guest-font-family');
|
||||
root.style.removeProperty('--guest-body-font');
|
||||
root.style.removeProperty('--guest-heading-font');
|
||||
}
|
||||
|
||||
function applyThemeMode(mode: EventBranding['mode']) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
const prefersDark = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: false;
|
||||
|
||||
let storedTheme: 'light' | 'dark' | 'system' | null = null;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem('theme');
|
||||
storedTheme = raw === 'light' || raw === 'dark' || raw === 'system' ? raw : null;
|
||||
} catch {
|
||||
storedTheme = null;
|
||||
}
|
||||
|
||||
const applyDark = () => root.classList.add('dark');
|
||||
const applyLight = () => root.classList.remove('dark');
|
||||
|
||||
if (mode === 'dark') {
|
||||
applyDark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'light') {
|
||||
applyLight();
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedTheme === 'dark') {
|
||||
applyDark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedTheme === 'light') {
|
||||
applyLight();
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefersDark) {
|
||||
applyDark();
|
||||
return;
|
||||
}
|
||||
|
||||
applyLight();
|
||||
}
|
||||
|
||||
export function EventBrandingProvider({
|
||||
branding,
|
||||
children,
|
||||
@@ -85,10 +215,20 @@ export function EventBrandingProvider({
|
||||
|
||||
useEffect(() => {
|
||||
applyCssVariables(resolved);
|
||||
const previousDark = typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : false;
|
||||
applyThemeMode(resolved.mode ?? 'auto');
|
||||
|
||||
return () => {
|
||||
if (typeof document !== 'undefined') {
|
||||
if (previousDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
resetCssVariables();
|
||||
applyCssVariables(DEFAULT_EVENT_BRANDING);
|
||||
applyThemeMode(DEFAULT_EVENT_BRANDING.mode ?? 'auto');
|
||||
};
|
||||
}, [resolved]);
|
||||
|
||||
@@ -98,6 +238,7 @@ export function EventBrandingProvider({
|
||||
resolved.primaryColor.toLowerCase() !== DEFAULT_PRIMARY
|
||||
|| resolved.secondaryColor.toLowerCase() !== DEFAULT_SECONDARY
|
||||
|| resolved.backgroundColor.toLowerCase() !== DEFAULT_BACKGROUND,
|
||||
// legacy surface check omitted by intent
|
||||
}), [resolved]);
|
||||
|
||||
return <EventBrandingContext.Provider value={value}>{children}</EventBrandingContext.Provider>;
|
||||
|
||||
Reference in New Issue
Block a user