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:
Codex Agent
2025-11-25 19:31:52 +01:00
parent 4d31eb4d42
commit 9bde8f3f32
38 changed files with 2420 additions and 104 deletions

View File

@@ -8,6 +8,7 @@ import { fetchGalleryMeta, fetchGalleryPhotos, type GalleryMetaResponse, type Ga
import { useTranslation } from '../i18n/useTranslation';
import { DEFAULT_LOCALE, isLocaleCode } from '../i18n/messages';
import { AlertTriangle, Download, Loader2, X } from 'lucide-react';
import { getContrastingTextColor } from '../lib/color';
interface GalleryState {
meta: GalleryMetaResponse | null;
@@ -90,6 +91,29 @@ export default function PublicGalleryPage(): React.ReactElement | null {
loadInitial();
}, [loadInitial]);
useEffect(() => {
const mode = state.meta?.branding.mode;
if (!mode || typeof document === 'undefined') {
return;
}
const wasDark = document.documentElement.classList.contains('dark');
if (mode === 'dark') {
document.documentElement.classList.add('dark');
} else if (mode === 'light') {
document.documentElement.classList.remove('dark');
}
return () => {
if (wasDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
};
}, [state.meta?.branding.mode]);
const loadMore = useCallback(async () => {
if (!token || !state.cursor || state.loadingMore) {
return;
@@ -140,10 +164,17 @@ export default function PublicGalleryPage(): React.ReactElement | null {
return {} as React.CSSProperties;
}
const palette = state.meta.branding.palette ?? {};
const primary = palette.primary ?? state.meta.branding.primary_color;
const secondary = palette.secondary ?? state.meta.branding.secondary_color;
const background = palette.background ?? state.meta.branding.background_color;
const surface = palette.surface ?? state.meta.branding.surface_color ?? background;
return {
'--gallery-primary': state.meta.branding.primary_color,
'--gallery-secondary': state.meta.branding.secondary_color,
'--gallery-background': state.meta.branding.background_color,
'--gallery-primary': primary,
'--gallery-secondary': secondary,
'--gallery-background': background,
'--gallery-surface': surface,
} as React.CSSProperties & Record<string, string>;
}, [state.meta]);
@@ -151,9 +182,13 @@ export default function PublicGalleryPage(): React.ReactElement | null {
if (!state.meta) {
return {};
}
const palette = state.meta.branding.palette ?? {};
const primary = palette.primary ?? state.meta.branding.primary_color;
const secondary = palette.secondary ?? state.meta.branding.secondary_color ?? primary;
const textColor = getContrastingTextColor(primary ?? '#f43f5e', '#0f172a', '#ffffff');
return {
background: state.meta.branding.primary_color,
color: '#ffffff',
background: `linear-gradient(135deg, ${primary}, ${secondary})`,
color: textColor,
} satisfies React.CSSProperties;
}, [state.meta]);
@@ -162,7 +197,7 @@ export default function PublicGalleryPage(): React.ReactElement | null {
return {};
}
return {
color: state.meta.branding.primary_color,
color: (state.meta.branding.palette?.primary ?? state.meta.branding.primary_color),
} satisfies React.CSSProperties;
}, [state.meta]);
@@ -171,7 +206,7 @@ export default function PublicGalleryPage(): React.ReactElement | null {
return {};
}
return {
backgroundColor: state.meta.branding.background_color,
backgroundColor: state.meta.branding.palette?.background ?? state.meta.branding.background_color,
} satisfies React.CSSProperties;
}, [state.meta]);