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:
@@ -31,6 +31,7 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ensureFontLoaded, useTenantFonts } from '../../lib/fonts';
|
||||
|
||||
import type { EventQrInvite, EventQrInviteLayout } from '../../api';
|
||||
import { authorizedFetch } from '../../auth/tokens';
|
||||
@@ -214,6 +215,7 @@ export function InviteLayoutCustomizerPanel({
|
||||
onDraftChange,
|
||||
}: InviteLayoutCustomizerPanelProps): React.JSX.Element {
|
||||
const { t } = useTranslation('management');
|
||||
const { fonts: availableFonts, isLoading: fontsLoading } = useTenantFonts();
|
||||
|
||||
const inviteUrl = invite?.url ?? '';
|
||||
const qrCodeDataUrl = invite?.qr_code_data_url ?? null;
|
||||
@@ -269,6 +271,40 @@ export function InviteLayoutCustomizerPanel({
|
||||
const appliedLayoutRef = React.useRef<string | null>(null);
|
||||
const appliedInviteRef = React.useRef<number | string | null>(null);
|
||||
|
||||
const handleElementFontChange = React.useCallback(
|
||||
(id: string, family: string) => {
|
||||
updateElement(id, { fontFamily: family || null });
|
||||
const font = availableFonts.find((entry) => entry.family === family);
|
||||
if (font) {
|
||||
void ensureFontLoaded(font).then(() => {
|
||||
fabricCanvasRef.current?.requestRenderAll();
|
||||
});
|
||||
}
|
||||
},
|
||||
[availableFonts, updateElement]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!availableFonts.length || !elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const families = Array.from(
|
||||
new Set(
|
||||
elements
|
||||
.map((element) => element.fontFamily)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
),
|
||||
);
|
||||
|
||||
families.forEach((family) => {
|
||||
const font = availableFonts.find((entry) => entry.family === family);
|
||||
if (font) {
|
||||
void ensureFontLoaded(font);
|
||||
}
|
||||
});
|
||||
}, [availableFonts, elements]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
||||
setIsCompact(false);
|
||||
@@ -1269,7 +1305,7 @@ export function InviteLayoutCustomizerPanel({
|
||||
|
||||
if (element.type !== 'qr') {
|
||||
blocks.push(
|
||||
<div className="grid gap-4 sm:grid-cols-2" key={`${element.id}-appearance`}>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3" key={`${element.id}-appearance`}>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('invites.customizer.elements.align', 'Ausrichtung')}</Label>
|
||||
<ToggleGroup
|
||||
@@ -1301,6 +1337,29 @@ export function InviteLayoutCustomizerPanel({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('invites.customizer.elements.fontFamily', 'Schriftart')}</Label>
|
||||
<Select
|
||||
value={availableFonts.some((font) => font.family === element.fontFamily) ? element.fontFamily ?? '' : ''}
|
||||
onValueChange={(value) => handleElementFontChange(element.id, value)}
|
||||
disabled={fontsLoading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('invites.customizer.elements.fontPlaceholder', 'Standard')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">{t('invites.customizer.elements.fontPlaceholder', 'Standard')}</SelectItem>
|
||||
{availableFonts.map((font) => (
|
||||
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={element.fontFamily ?? ''}
|
||||
onChange={(event) => handleElementFontChange(element.id, event.target.value)}
|
||||
placeholder="z. B. Playfair Display"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1322,7 +1381,7 @@ export function InviteLayoutCustomizerPanel({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[elementBindings, form, t, updateElement, updateElementAlign, updateElementContent, updateForm]
|
||||
[availableFonts, elementBindings, form, handleElementFontChange, t, updateElement, updateElementAlign, updateElementContent, updateForm]
|
||||
);
|
||||
|
||||
const renderActionButtons = (mode: 'inline' | 'floating') => (
|
||||
|
||||
Reference in New Issue
Block a user