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

@@ -33,6 +33,7 @@ import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { buildLimitSummaries, type LimitSummaryCard } from '../lib/limitSummaries';
import { resolveUploadErrorDialog, type UploadErrorDialog } from '../lib/uploadErrorDialog';
import { useEventStats } from '../context/EventStatsContext';
import { useEventBranding } from '../context/EventBrandingContext';
import { compressPhoto, formatBytes } from '../lib/image';
interface Task {
@@ -113,6 +114,11 @@ export default function UploadPage() {
const { markCompleted } = useGuestTaskProgress(token);
const { t, locale } = useTranslation();
const stats = useEventStats();
const { branding } = useEventBranding();
const radius = branding.buttons?.radius ?? 12;
const buttonStyle = branding.buttons?.style ?? 'filled';
const linkColor = branding.buttons?.linkColor ?? branding.secondaryColor;
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
const taskIdParam = searchParams.get('task');
const emotionSlug = searchParams.get('emotion') || '';
@@ -936,7 +942,10 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
const canRetryCamera = permissionState !== 'unsupported';
return (
<div className="rounded-[32px] border border-white/15 bg-white/85 p-5 text-slate-900 shadow-lg dark:border-white/10 dark:bg-white/5 dark:text-white">
<div
className="rounded-[32px] border border-white/15 bg-white/85 p-5 text-slate-900 shadow-lg dark:border-white/10 dark:bg-white/5 dark:text-white"
style={{ borderRadius: radius, fontFamily: bodyFont }}
>
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900/10 dark:bg-white/10">
<Camera className="h-6 w-6" />
@@ -948,11 +957,20 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
</div>
<div className="mt-4 flex flex-wrap gap-3">
{canRetryCamera && (
<Button onClick={startCamera} size="sm">
<Button
onClick={startCamera}
size="sm"
style={buttonStyle === 'outline' ? { borderRadius: radius, background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : { borderRadius: radius }}
>
{t('upload.buttons.startCamera')}
</Button>
)}
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
<Button
variant="secondary"
size="sm"
onClick={() => fileInputRef.current?.click()}
style={buttonStyle === 'outline' ? { borderRadius: radius, background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : { borderRadius: radius }}
>
{t('upload.galleryButton')}
</Button>
</div>
@@ -962,9 +980,12 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
return renderWithDialog(
<>
<div className="relative pt-8">
<div className="relative pt-8" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
{taskFloatingCard}
<section className="relative overflow-hidden rounded-[32px] border border-white/10 bg-black text-white shadow-2xl">
<section
className="relative overflow-hidden border border-white/10 bg-black text-white shadow-2xl"
style={{ borderRadius: radius }}
>
<div className="relative aspect-[3/4] sm:aspect-video">
<video
ref={videoRef}
@@ -1028,7 +1049,10 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
)}
</div>
<div className="relative z-30 flex flex-col gap-4 bg-gradient-to-t from-black via-black/80 to-transparent p-4">
<div
className="relative z-30 flex flex-col gap-4 bg-gradient-to-t from-black via-black/80 to-transparent p-4"
style={{ fontFamily: bodyFont }}
>
{uploadWarning && (
<Alert className="border-yellow-400/20 bg-yellow-500/10 text-white">
<AlertDescription className="text-xs">