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:
@@ -847,7 +847,7 @@ class EventPublicController extends BaseController
|
||||
}
|
||||
|
||||
// Common relative paths stored in DB (e.g. 'photos/...', 'thumbnails/...', 'events/...')
|
||||
if (str_starts_with($path, 'photos/') || str_starts_with($path, 'thumbnails/') || str_starts_with($path, 'events/')) {
|
||||
if (str_starts_with($path, 'photos/') || str_starts_with($path, 'thumbnails/') || str_starts_with($path, 'events/') || str_starts_with($path, 'branding/')) {
|
||||
return Storage::url($path);
|
||||
}
|
||||
|
||||
@@ -895,42 +895,232 @@ class EventPublicController extends BaseController
|
||||
|
||||
private function buildGalleryBranding(Event $event): array
|
||||
{
|
||||
$defaultPrimary = '#f43f5e';
|
||||
$defaultSecondary = '#fb7185';
|
||||
$defaultBackground = '#ffffff';
|
||||
return $this->resolveBrandingPayload($event);
|
||||
}
|
||||
|
||||
$event->loadMissing('eventPackage.package', 'tenant');
|
||||
private function normalizeHexColor(?string $value, string $fallback): string
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$trimmed = trim($value);
|
||||
|
||||
if (preg_match('/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/', $trimmed) === 1) {
|
||||
return $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array|mixed> $sources
|
||||
*/
|
||||
private function firstStringFromSources(array $sources, array $keys): ?string
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
foreach ($keys as $key) {
|
||||
$value = Arr::get($source ?? [], $key);
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array|mixed> $sources
|
||||
*/
|
||||
private function firstNumberFromSources(array $sources, array $keys): ?float
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
foreach ($keys as $key) {
|
||||
$value = Arr::get($source ?? [], $key);
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* primary_color: string,
|
||||
* secondary_color: string,
|
||||
* background_color: string,
|
||||
* surface_color: string,
|
||||
* font_family: ?string,
|
||||
* heading_font: ?string,
|
||||
* body_font: ?string,
|
||||
* font_size: string,
|
||||
* logo_url: ?string,
|
||||
* logo_mode: string,
|
||||
* logo_value: ?string,
|
||||
* logo_position: string,
|
||||
* logo_size: string,
|
||||
* button_style: string,
|
||||
* button_radius: int,
|
||||
* button_primary_color: string,
|
||||
* button_secondary_color: string,
|
||||
* link_color: string,
|
||||
* mode: string,
|
||||
* use_default_branding: bool,
|
||||
* palette: array{primary:string, secondary:string, background:string, surface:string},
|
||||
* typography: array{heading:?string, body:?string, size:string},
|
||||
* logo: array{mode:string, value:?string, position:string, size:string},
|
||||
* buttons: array{style:string, radius:int, primary:string, secondary:string, link_color:string},
|
||||
* icon: ?string
|
||||
* }
|
||||
*/
|
||||
private function resolveBrandingPayload(Event $event): array
|
||||
{
|
||||
$defaults = [
|
||||
'primary' => '#f43f5e',
|
||||
'secondary' => '#fb7185',
|
||||
'background' => '#ffffff',
|
||||
'surface' => '#ffffff',
|
||||
'font' => null,
|
||||
'size' => 'm',
|
||||
'logo_position' => 'left',
|
||||
'logo_size' => 'm',
|
||||
'button_style' => 'filled',
|
||||
'button_radius' => 12,
|
||||
'mode' => 'auto',
|
||||
];
|
||||
|
||||
$event->loadMissing('eventPackage.package', 'tenant', 'eventType');
|
||||
$brandingAllowed = $this->determineBrandingAllowed($event);
|
||||
|
||||
$eventBranding = $brandingAllowed ? Arr::get($event->settings, 'branding', []) : [];
|
||||
$tenantBranding = $brandingAllowed ? Arr::get($event->tenant?->settings, 'branding', []) : [];
|
||||
|
||||
return [
|
||||
'primary_color' => Arr::get($eventBranding, 'primary_color')
|
||||
?? Arr::get($tenantBranding, 'primary_color')
|
||||
?? $defaultPrimary,
|
||||
'secondary_color' => Arr::get($eventBranding, 'secondary_color')
|
||||
?? Arr::get($tenantBranding, 'secondary_color')
|
||||
?? $defaultSecondary,
|
||||
'background_color' => Arr::get($eventBranding, 'background_color')
|
||||
?? Arr::get($tenantBranding, 'background_color')
|
||||
?? $defaultBackground,
|
||||
];
|
||||
}
|
||||
$useDefault = (bool) Arr::get($eventBranding, 'use_default_branding', Arr::get($eventBranding, 'use_default', false));
|
||||
$sources = $brandingAllowed
|
||||
? ($useDefault ? [$tenantBranding] : [$eventBranding, $tenantBranding])
|
||||
: [[]];
|
||||
|
||||
private function resolveFontFamily(Event $event): ?string
|
||||
{
|
||||
$fontFamily = Arr::get($event->settings, 'branding.font_family')
|
||||
?? Arr::get($event->tenant?->settings, 'branding.font_family');
|
||||
$primary = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['palette.primary', 'primary_color']),
|
||||
$defaults['primary']
|
||||
);
|
||||
$secondary = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['palette.secondary', 'secondary_color']),
|
||||
$defaults['secondary']
|
||||
);
|
||||
$background = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['palette.background', 'background_color']),
|
||||
$defaults['background']
|
||||
);
|
||||
$surface = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['palette.surface', 'surface_color']),
|
||||
$background ?: $defaults['surface']
|
||||
);
|
||||
|
||||
if (! is_string($fontFamily)) {
|
||||
return null;
|
||||
$headingFont = $this->firstStringFromSources($sources, ['typography.heading', 'heading_font', 'font_family']);
|
||||
$bodyFont = $this->firstStringFromSources($sources, ['typography.body', 'body_font', 'font_family']);
|
||||
$fontSize = $this->firstStringFromSources($sources, ['typography.size', 'font_size']) ?? $defaults['size'];
|
||||
$fontSize = in_array($fontSize, ['s', 'm', 'l'], true) ? $fontSize : $defaults['size'];
|
||||
|
||||
$logoMode = $this->firstStringFromSources($sources, ['logo.mode', 'logo_mode']);
|
||||
if (! in_array($logoMode, ['emoticon', 'upload'], true)) {
|
||||
$logoMode = null;
|
||||
}
|
||||
$logoPosition = $this->firstStringFromSources($sources, ['logo.position', 'logo_position']) ?? $defaults['logo_position'];
|
||||
if (! in_array($logoPosition, ['left', 'right', 'center'], true)) {
|
||||
$logoPosition = $defaults['logo_position'];
|
||||
}
|
||||
$logoSize = $this->firstStringFromSources($sources, ['logo.size', 'logo_size']) ?? $defaults['logo_size'];
|
||||
if (! in_array($logoSize, ['s', 'm', 'l'], true)) {
|
||||
$logoSize = $defaults['logo_size'];
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim($fontFamily));
|
||||
$defaultInter = strtolower('Inter, sans-serif');
|
||||
$logoRawValue = $this->firstStringFromSources($sources, ['logo.value', 'logo_url', 'icon'])
|
||||
?? ($event->eventType?->icon ?? null);
|
||||
if (! $logoMode) {
|
||||
$logoMode = $logoRawValue && (preg_match('/^https?:\/\//', $logoRawValue) === 1
|
||||
|| str_starts_with($logoRawValue, '/storage/')
|
||||
|| str_starts_with($logoRawValue, 'storage/'))
|
||||
? 'upload'
|
||||
: 'emoticon';
|
||||
}
|
||||
|
||||
return $normalized === $defaultInter ? null : $fontFamily;
|
||||
$logoValue = $logoMode === 'upload' ? $this->toPublicUrl($logoRawValue) : $logoRawValue;
|
||||
|
||||
$buttonStyle = $this->firstStringFromSources($sources, ['buttons.style', 'button_style']) ?? $defaults['button_style'];
|
||||
if (! in_array($buttonStyle, ['filled', 'outline'], true)) {
|
||||
$buttonStyle = $defaults['button_style'];
|
||||
}
|
||||
|
||||
$buttonRadius = (int) ($this->firstNumberFromSources($sources, ['buttons.radius', 'button_radius']) ?? $defaults['button_radius']);
|
||||
$buttonRadius = $buttonRadius > 0 ? $buttonRadius : $defaults['button_radius'];
|
||||
|
||||
$buttonPrimary = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['buttons.primary', 'button_primary_color']),
|
||||
$primary
|
||||
);
|
||||
$buttonSecondary = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['buttons.secondary', 'button_secondary_color']),
|
||||
$secondary
|
||||
);
|
||||
$linkColor = $this->normalizeHexColor(
|
||||
$this->firstStringFromSources($sources, ['buttons.link_color', 'link_color']),
|
||||
$secondary
|
||||
);
|
||||
|
||||
$mode = $this->firstStringFromSources($sources, ['mode']) ?? $defaults['mode'];
|
||||
if (! in_array($mode, ['light', 'dark', 'auto'], true)) {
|
||||
$mode = $defaults['mode'];
|
||||
}
|
||||
|
||||
return [
|
||||
'primary_color' => $primary,
|
||||
'secondary_color' => $secondary,
|
||||
'background_color' => $background,
|
||||
'surface_color' => $surface,
|
||||
'font_family' => $bodyFont,
|
||||
'heading_font' => $headingFont,
|
||||
'body_font' => $bodyFont,
|
||||
'font_size' => $fontSize,
|
||||
'logo_url' => $logoMode === 'upload' ? $logoValue : null,
|
||||
'logo_mode' => $logoMode,
|
||||
'logo_value' => $logoValue,
|
||||
'logo_position' => $logoPosition,
|
||||
'logo_size' => $logoSize,
|
||||
'button_style' => $buttonStyle,
|
||||
'button_radius' => $buttonRadius,
|
||||
'button_primary_color' => $buttonPrimary,
|
||||
'button_secondary_color' => $buttonSecondary,
|
||||
'link_color' => $linkColor,
|
||||
'mode' => $mode,
|
||||
'use_default_branding' => $useDefault,
|
||||
'palette' => [
|
||||
'primary' => $primary,
|
||||
'secondary' => $secondary,
|
||||
'background' => $background,
|
||||
'surface' => $surface,
|
||||
],
|
||||
'typography' => [
|
||||
'heading' => $headingFont,
|
||||
'body' => $bodyFont,
|
||||
'size' => $fontSize,
|
||||
],
|
||||
'logo' => [
|
||||
'mode' => $logoMode,
|
||||
'value' => $logoValue,
|
||||
'position' => $logoPosition,
|
||||
'size' => $logoSize,
|
||||
],
|
||||
'buttons' => [
|
||||
'style' => $buttonStyle,
|
||||
'radius' => $buttonRadius,
|
||||
'primary' => $buttonPrimary,
|
||||
'secondary' => $buttonSecondary,
|
||||
'link_color' => $linkColor,
|
||||
],
|
||||
'icon' => $logoMode === 'emoticon' ? $logoValue : ($event->eventType?->icon ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
private function encodeGalleryCursor(Photo $photo): string
|
||||
@@ -1393,12 +1583,6 @@ class EventPublicController extends BaseController
|
||||
];
|
||||
|
||||
$branding = $this->buildGalleryBranding($event);
|
||||
$fontFamily = $this->resolveFontFamily($event);
|
||||
$brandingAllowed = $this->determineBrandingAllowed($event);
|
||||
$logoUrl = $brandingAllowed
|
||||
? (Arr::get($event->settings, 'branding.logo_url')
|
||||
?? Arr::get($event->tenant?->settings, 'branding.logo_url'))
|
||||
: null;
|
||||
|
||||
if ($joinToken) {
|
||||
$this->joinTokenService->incrementUsage($joinToken);
|
||||
@@ -1414,13 +1598,7 @@ class EventPublicController extends BaseController
|
||||
'type' => $eventTypeData,
|
||||
'join_token' => $joinToken?->token,
|
||||
'photobooth_enabled' => (bool) $event->photobooth_enabled,
|
||||
'branding' => [
|
||||
'primary_color' => $branding['primary_color'],
|
||||
'secondary_color' => $branding['secondary_color'],
|
||||
'background_color' => $branding['background_color'],
|
||||
'font_family' => $fontFamily,
|
||||
'logo_url' => $this->toPublicUrl($logoUrl),
|
||||
],
|
||||
'branding' => $branding,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user