- 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.
167 lines
6.1 KiB
TypeScript
167 lines
6.1 KiB
TypeScript
import React from 'react';
|
|
import { NavLink, useParams, useLocation, Link } from 'react-router-dom';
|
|
import { CheckSquare, GalleryHorizontal, Home, Trophy, Camera } from 'lucide-react';
|
|
import { useEventData } from '../hooks/useEventData';
|
|
import { useTranslation } from '../i18n/useTranslation';
|
|
import { useEventBranding } from '../context/EventBrandingContext';
|
|
|
|
function TabLink({
|
|
to,
|
|
children,
|
|
isActive,
|
|
accentColor,
|
|
radius,
|
|
style,
|
|
compact = false,
|
|
}: {
|
|
to: string;
|
|
children: React.ReactNode;
|
|
isActive: boolean;
|
|
accentColor: string;
|
|
radius: number;
|
|
style?: React.CSSProperties;
|
|
compact?: boolean;
|
|
}) {
|
|
const activeStyle = isActive
|
|
? {
|
|
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}cc)`,
|
|
color: '#ffffff',
|
|
boxShadow: `0 12px 30px ${accentColor}33`,
|
|
borderRadius: radius,
|
|
...style,
|
|
}
|
|
: { borderRadius: radius, ...style };
|
|
|
|
return (
|
|
<NavLink
|
|
to={to}
|
|
className={`
|
|
flex ${compact ? 'h-10 text-[10px]' : 'h-14 text-xs'} flex-col items-center justify-center gap-1 rounded-lg border border-transparent p-2 font-medium transition-all duration-200 ease-out
|
|
touch-manipulation backdrop-blur-md
|
|
${isActive ? 'scale-[1.04]' : 'text-white/70 hover:text-white'}
|
|
`}
|
|
style={activeStyle}
|
|
>
|
|
{children}
|
|
</NavLink>
|
|
);
|
|
}
|
|
|
|
export default function BottomNav() {
|
|
const { token } = useParams();
|
|
const location = useLocation();
|
|
const { event, status } = useEventData();
|
|
const { t } = useTranslation();
|
|
const { branding } = useEventBranding();
|
|
const radius = branding.buttons?.radius ?? 12;
|
|
const buttonStyle = branding.buttons?.style ?? 'filled';
|
|
const linkColor = branding.buttons?.linkColor ?? branding.secondaryColor;
|
|
const surface = branding.palette?.surface ?? branding.backgroundColor;
|
|
|
|
const isReady = status === 'ready' && !!event;
|
|
|
|
if (!token || !isReady) return null;
|
|
|
|
const base = `/e/${encodeURIComponent(token)}`;
|
|
const currentPath = location.pathname;
|
|
|
|
const labels = {
|
|
home: t('navigation.home'),
|
|
tasks: t('navigation.tasks'),
|
|
achievements: t('navigation.achievements'),
|
|
gallery: t('navigation.gallery'),
|
|
upload: t('home.actions.items.upload.label'),
|
|
};
|
|
|
|
const isHomeActive = currentPath === base || currentPath === `/${token}`;
|
|
const isTasksActive = currentPath.startsWith(`${base}/tasks`);
|
|
const isAchievementsActive = currentPath.startsWith(`${base}/achievements`);
|
|
const isGalleryActive = currentPath.startsWith(`${base}/gallery`) || currentPath.startsWith(`${base}/photos`);
|
|
const isUploadActive = currentPath.startsWith(`${base}/upload`);
|
|
|
|
const compact = isUploadActive;
|
|
|
|
return (
|
|
<div
|
|
className={`fixed inset-x-0 bottom-0 z-30 border-t border-white/20 bg-gradient-to-t from-black/40 via-black/20 to-transparent px-4 shadow-xl backdrop-blur-2xl transition-all duration-200 dark:border-white/10 dark:from-gray-950/90 dark:via-gray-900/70 dark:to-gray-900/35 ${
|
|
compact ? 'pb-1 pt-1 translate-y-3' : 'pb-3 pt-2'
|
|
}`}
|
|
>
|
|
<div className="mx-auto flex max-w-lg items-center gap-3">
|
|
<div className="flex flex-1 justify-evenly gap-2">
|
|
<TabLink
|
|
to={`${base}`}
|
|
isActive={isHomeActive}
|
|
accentColor={branding.primaryColor}
|
|
radius={radius}
|
|
compact={compact}
|
|
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
|
>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<Home className="h-5 w-5" aria-hidden />
|
|
<span>{labels.home}</span>
|
|
</div>
|
|
</TabLink>
|
|
<TabLink
|
|
to={`${base}/tasks`}
|
|
isActive={isTasksActive}
|
|
accentColor={branding.primaryColor}
|
|
radius={radius}
|
|
compact={compact}
|
|
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
|
>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<CheckSquare className="h-5 w-5" aria-hidden />
|
|
<span>{labels.tasks}</span>
|
|
</div>
|
|
</TabLink>
|
|
</div>
|
|
|
|
<Link
|
|
to={`${base}/upload`}
|
|
aria-label={labels.upload}
|
|
className={`relative flex ${compact ? 'h-12 w-12' : 'h-16 w-16'} items-center justify-center rounded-full text-white shadow-2xl transition ${
|
|
isUploadActive ? 'scale-105' : 'hover:scale-105'
|
|
}`}
|
|
style={{
|
|
background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`,
|
|
boxShadow: `0 20px 35px ${branding.primaryColor}44`,
|
|
borderRadius: radius,
|
|
}}
|
|
>
|
|
<Camera className="h-6 w-6" aria-hidden />
|
|
</Link>
|
|
|
|
<div className="flex flex-1 justify-evenly gap-2">
|
|
<TabLink
|
|
to={`${base}/achievements`}
|
|
isActive={isAchievementsActive}
|
|
accentColor={branding.primaryColor}
|
|
radius={radius}
|
|
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
|
compact={compact}
|
|
>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<Trophy className="h-5 w-5" aria-hidden />
|
|
<span>{labels.achievements}</span>
|
|
</div>
|
|
</TabLink>
|
|
<TabLink
|
|
to={`${base}/gallery`}
|
|
isActive={isGalleryActive}
|
|
accentColor={branding.primaryColor}
|
|
radius={radius}
|
|
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
|
compact={compact}
|
|
>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<GalleryHorizontal className="h-5 w-5" aria-hidden />
|
|
<span>{labels.gallery}</span>
|
|
</div>
|
|
</TabLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|