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

@@ -181,6 +181,19 @@ export type EventAddonCatalogItem = {
increments?: Record<string, number>;
};
export type TenantFontVariant = {
variant: string | null;
weight: number;
style: string;
url: string;
};
export type TenantFont = {
family: string;
category?: string | null;
variants: TenantFontVariant[];
};
export type EventAddonSummary = {
id: number;
key: string;
@@ -1266,6 +1279,18 @@ export async function getAddonCatalog(): Promise<EventAddonCatalogItem[]> {
return data.data ?? [];
}
export async function getTenantFonts(): Promise<TenantFont[]> {
return cachedFetch(
CacheKeys.fonts,
async () => {
const response = await authorizedFetch('/api/v1/tenant/fonts');
const data = await jsonOrThrow<{ data?: TenantFont[] }>(response, 'Failed to load fonts');
return data.data ?? [];
},
6 * 60 * 60 * 1000,
);
}
export async function getEventTypes(): Promise<TenantEventType[]> {
const response = await authorizedFetch('/api/v1/tenant/event-types');
const data = await jsonOrThrow<{ data?: JsonValue[] }>(response, 'Failed to load event types');
@@ -1672,6 +1697,27 @@ export async function getDashboardSummary(options?: { force?: boolean }): Promis
);
}
export type TenantSettingsPayload = {
id: number;
settings: Record<string, unknown>;
updated_at: string | null;
};
export async function getTenantSettings(): Promise<TenantSettingsPayload> {
const response = await authorizedFetch('/api/v1/tenant/settings');
const data = await jsonOrThrow<{ data?: { id?: number; settings?: Record<string, unknown>; updated_at?: string | null } }>(
response,
'Failed to load tenant settings',
);
const payload = (data.data ?? {}) as Record<string, unknown>;
return {
id: Number(payload.id ?? 0),
settings: (payload.settings ?? {}) as Record<string, unknown>,
updated_at: (payload.updated_at ?? null) as string | null,
};
}
export async function getTenantPackagesOverview(options?: { force?: boolean }): Promise<{
packages: TenantPackageSummary[];
activePackage: TenantPackageSummary | null;
@@ -2236,6 +2282,7 @@ const CacheKeys = {
dashboard: 'tenant:dashboard',
events: 'tenant:events',
packages: 'tenant:packages',
fonts: 'tenant:fonts',
} as const;
function cachedFetch<T>(