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:
@@ -27,6 +27,7 @@ import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/Eve
|
||||
import { useOptionalNotificationCenter, type NotificationCenterValue } from '../context/NotificationCenterContext';
|
||||
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
|
||||
import { usePushSubscription } from '../hooks/usePushSubscription';
|
||||
import { getContrastingTextColor, relativeLuminance } from '../lib/color';
|
||||
|
||||
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
heart: Heart,
|
||||
@@ -66,7 +67,26 @@ function getInitials(name: string): string {
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function renderEventAvatar(name: string, icon: unknown, accentColor: string, textColor: string) {
|
||||
function renderEventAvatar(name: string, icon: unknown, accentColor: string, textColor: string, logo?: { mode: 'emoticon' | 'upload'; value: string | null }) {
|
||||
if (logo?.mode === 'upload' && logo.value) {
|
||||
return (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-sm">
|
||||
<img src={logo.value} alt={name} className="h-9 w-9 rounded-full object-contain" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (logo?.mode === 'emoticon' && logo.value && isLikelyEmoji(logo.value)) {
|
||||
return (
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full text-xl shadow-sm"
|
||||
style={{ backgroundColor: accentColor, color: textColor }}
|
||||
>
|
||||
<span aria-hidden>{logo.value}</span>
|
||||
<span className="sr-only">{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
const trimmed = icon.trim();
|
||||
if (trimmed) {
|
||||
@@ -113,7 +133,17 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
const { t } = useTranslation();
|
||||
const brandingContext = useOptionalEventBranding();
|
||||
const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING;
|
||||
const primaryForeground = '#ffffff';
|
||||
const headerTextColor = React.useMemo(() => {
|
||||
const primaryLum = relativeLuminance(branding.primaryColor);
|
||||
const secondaryLum = relativeLuminance(branding.secondaryColor);
|
||||
const avgLum = (primaryLum + secondaryLum) / 2;
|
||||
|
||||
if (avgLum > 0.55) {
|
||||
return getContrastingTextColor(branding.primaryColor, '#0f172a', '#ffffff');
|
||||
}
|
||||
|
||||
return '#ffffff';
|
||||
}, [branding.primaryColor, branding.secondaryColor]);
|
||||
const { event, status } = useEventData();
|
||||
const notificationCenter = useOptionalNotificationCenter();
|
||||
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
|
||||
@@ -169,7 +199,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
|
||||
color: primaryForeground,
|
||||
color: headerTextColor,
|
||||
fontFamily: branding.fontFamily ?? undefined,
|
||||
};
|
||||
|
||||
@@ -199,7 +229,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
style={headerStyle}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{renderEventAvatar(event.name, event.type?.icon, accentColor, primaryForeground)}
|
||||
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
|
||||
<div className="flex flex-col" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
|
||||
<div className="font-semibold text-base">{event.name}</div>
|
||||
{guestName && (
|
||||
|
||||
Reference in New Issue
Block a user