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:
@@ -14,6 +14,7 @@ import { useToast } from '../components/ToastHost';
|
||||
import { localizeTaskLabel } from '../lib/localizeTaskLabel';
|
||||
import { createPhotoShareLink } from '../services/photosApi';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useEventBranding } from '../context/EventBrandingContext';
|
||||
|
||||
const allGalleryFilters: GalleryFilter[] = ['latest', 'popular', 'mine', 'photobooth'];
|
||||
type GalleryPhoto = {
|
||||
@@ -285,36 +286,41 @@ export default function GalleryPage() {
|
||||
|
||||
return (
|
||||
<Page title="">
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-pink-500/10 text-pink-500">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-pink-500/10 text-pink-500" style={{ borderRadius: radius }}>
|
||||
<ImageIcon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">{t('galleryPage.title')}</h1>
|
||||
<h1 className="text-2xl font-semibold text-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>{t('galleryPage.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t('galleryPage.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
|
||||
{newCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledgeNew}
|
||||
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold transition ${badgeEmphasisClass}`}
|
||||
style={{ borderRadius: radius }}
|
||||
>
|
||||
{newPhotosBadgeText}
|
||||
</button>
|
||||
) : (
|
||||
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ${badgeEmphasisClass}`}>
|
||||
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ${badgeEmphasisClass}`} style={{ borderRadius: radius }}>
|
||||
{newPhotosBadgeText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<FiltersBar value={filter} onChange={setFilter} className="mt-2" showPhotobooth={showPhotoboothFilter} />
|
||||
{loading && <p className="px-4">{t('galleryPage.loading', 'Lade…')}</p>}
|
||||
<FiltersBar
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
className="mt-2"
|
||||
showPhotobooth={showPhotoboothFilter}
|
||||
styleOverride={{ borderRadius: radius, fontFamily: headingFont }}
|
||||
/>
|
||||
{loading && <p className="px-4" style={bodyFont ? { fontFamily: bodyFont } : undefined}>{t('galleryPage.loading', 'Lade…')}</p>}
|
||||
<div className="grid gap-3 px-4 pb-16 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{list.map((p: GalleryPhoto) => {
|
||||
const imageUrl = normalizeImageUrl(p.thumbnail_path || p.file_path);
|
||||
@@ -344,7 +350,8 @@ export default function GalleryPage() {
|
||||
openPhoto();
|
||||
}
|
||||
}}
|
||||
className="group relative overflow-hidden rounded-[28px] border border-white/20 bg-gray-950 text-white shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-400"
|
||||
className="group relative overflow-hidden border border-white/20 bg-gray-950 text-white shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-400"
|
||||
style={{ borderRadius: radius }}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
@@ -356,16 +363,16 @@ export default function GalleryPage() {
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/85 via-black/20 to-transparent" aria-hidden />
|
||||
<div className="absolute inset-x-0 bottom-0 space-y-2 px-4 pb-4">
|
||||
{localizedTaskTitle && <p className="text-sm font-medium leading-tight line-clamp-2 text-white">{localizedTaskTitle}</p>}
|
||||
<div className="flex items-center justify-between text-xs text-white/90">
|
||||
<div className="absolute inset-x-0 bottom-0 space-y-2 px-4 pb-4" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
{localizedTaskTitle && <p className="text-sm font-medium leading-tight line-clamp-2 text-white" style={headingFont ? { fontFamily: headingFont } : undefined}>{localizedTaskTitle}</p>}
|
||||
<div className="flex items-center justify-between text-xs text-white/90" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
<span className="truncate">{createdLabel}</span>
|
||||
<span className="ml-3 truncate text-right">{p.uploader_name || t('galleryPage.photo.anonymous', 'Gast')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-3 top-3 z-10 flex flex-col items-start gap-2">
|
||||
{localizedTaskTitle && (
|
||||
<span className="rounded-full bg-white/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-white shadow">
|
||||
<span className="rounded-full bg-white/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-white shadow" style={{ borderRadius: radius }}>
|
||||
{localizedTaskTitle}
|
||||
</span>
|
||||
)}
|
||||
@@ -378,11 +385,17 @@ export default function GalleryPage() {
|
||||
onShare(p);
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-9 w-9 items-center justify-center rounded-full border border-white/40 bg-black/40 text-white transition backdrop-blur',
|
||||
'flex h-9 w-9 items-center justify-center border text-white transition backdrop-blur',
|
||||
shareTargetId === p.id ? 'opacity-60' : 'hover:bg-white/10'
|
||||
)}
|
||||
aria-label={t('galleryPage.photo.shareAria', 'Foto teilen')}
|
||||
disabled={shareTargetId === p.id}
|
||||
style={{
|
||||
borderRadius: radius,
|
||||
background: buttonStyle === 'outline' ? 'transparent' : '#00000066',
|
||||
border: buttonStyle === 'outline' ? `1px solid ${linkColor}` : '1px solid rgba(255,255,255,0.4)',
|
||||
color: buttonStyle === 'outline' ? linkColor : undefined,
|
||||
}}
|
||||
>
|
||||
<Share2 className="h-4 w-4" aria-hidden />
|
||||
</button>
|
||||
@@ -393,10 +406,16 @@ export default function GalleryPage() {
|
||||
onLike(p.id);
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-full border border-white/40 bg-black/40 px-3 py-1 text-sm font-medium text-white transition backdrop-blur',
|
||||
'flex items-center gap-1 px-3 py-1 text-sm font-medium transition backdrop-blur',
|
||||
liked.has(p.id) ? 'text-pink-300' : 'text-white'
|
||||
)}
|
||||
aria-label={t('galleryPage.photo.likeAria', 'Foto liken')}
|
||||
style={{
|
||||
borderRadius: radius,
|
||||
background: buttonStyle === 'outline' ? 'transparent' : '#00000066',
|
||||
border: buttonStyle === 'outline' ? `1px solid ${linkColor}` : '1px solid rgba(255,255,255,0.4)',
|
||||
color: buttonStyle === 'outline' ? linkColor : undefined,
|
||||
}}
|
||||
>
|
||||
<Heart className={`h-4 w-4 ${liked.has(p.id) ? 'fill-current' : ''}`} aria-hidden />
|
||||
{likeCount}
|
||||
@@ -491,3 +510,9 @@ export default function GalleryPage() {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
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 headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||
|
||||
Reference in New Issue
Block a user