diff --git a/.gitignore b/.gitignore index 14a6e94..c657b21 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ fotospiel-tenant-app /public/build /public/hot /public/storage +/public/fonts/google /resources/js/actions /resources/js/routes /resources/js/wayfinder diff --git a/resources/js/admin/pages/EventBrandingPage.tsx b/resources/js/admin/pages/EventBrandingPage.tsx index afae40b..163d5d8 100644 --- a/resources/js/admin/pages/EventBrandingPage.tsx +++ b/resources/js/admin/pages/EventBrandingPage.tsx @@ -21,6 +21,9 @@ import { getContrastingTextColor } from '../../guest/lib/color'; import { buildEventTabs } from '../lib/eventTabs'; import { ensureFontLoaded, useTenantFonts } from '../lib/fonts'; +const DEFAULT_FONT_VALUE = '__default'; +const CUSTOM_FONT_VALUE = '__custom'; + type BrandingForm = { useDefault: boolean; palette: { @@ -318,12 +321,12 @@ export default function EventBrandingPage(): React.ReactElement { } const resolveFontSelectValue = (current: string): string => { - if (!current) return ''; - return availableFonts.some((font) => font.family === current) ? current : '__custom'; + if (!current) return DEFAULT_FONT_VALUE; + return availableFonts.some((font) => font.family === current) ? current : CUSTOM_FONT_VALUE; }; const handleFontSelect = (key: 'heading' | 'body', value: string) => { - const resolved = value === '__custom' ? '' : value; + const resolved = value === CUSTOM_FONT_VALUE || value === DEFAULT_FONT_VALUE ? '' : value; setForm((prev) => ({ ...prev, typography: { ...prev.typography, [key]: resolved } })); const font = availableFonts.find((entry) => entry.family === resolved); if (font) { @@ -446,11 +449,11 @@ export default function EventBrandingPage(): React.ReactElement { - {t('branding.fontDefault', 'Standard (Tenant)')} + {t('branding.fontDefault', 'Standard (Tenant)')} {availableFonts.map((font) => ( {font.family} ))} - {t('branding.fontCustom', 'Eigene Schrift eingeben')} + {t('branding.fontCustom', 'Eigene Schrift eingeben')} - {t('branding.fontDefault', 'Standard (Tenant)')} + {t('branding.fontDefault', 'Standard (Tenant)')} {availableFonts.map((font) => ( {font.family} ))} - {t('branding.fontCustom', 'Eigene Schrift eingeben')} + {t('branding.fontCustom', 'Eigene Schrift eingeben')} (null); const appliedInviteRef = React.useRef(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; @@ -609,6 +598,19 @@ export function InviteLayoutCustomizerPanel({ [commitElements] ); + 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 (!invite) { setAvailableLayouts([]); @@ -1340,15 +1342,15 @@ export function InviteLayoutCustomizerPanel({ {t('invites.customizer.elements.fontFamily', 'Schriftart')} font.family === element.fontFamily) ? element.fontFamily ?? '' : ''} - onValueChange={(value) => handleElementFontChange(element.id, value)} + value={availableFonts.some((font) => font.family === element.fontFamily) ? element.fontFamily ?? DEFAULT_FONT_VALUE : DEFAULT_FONT_VALUE} + onValueChange={(value) => handleElementFontChange(element.id, value === DEFAULT_FONT_VALUE ? '' : value)} disabled={fontsLoading} > - {t('invites.customizer.elements.fontPlaceholder', 'Standard')} + {t('invites.customizer.elements.fontPlaceholder', 'Standard')} {availableFonts.map((font) => ( {font.family} ))} diff --git a/resources/js/admin/router.tsx b/resources/js/admin/router.tsx index 26b660c..2dd0526 100644 --- a/resources/js/admin/router.tsx +++ b/resources/js/admin/router.tsx @@ -8,6 +8,7 @@ import { ADMIN_LOGIN_START_PATH, ADMIN_PUBLIC_LANDING_PATH, } from './constants'; +import RouteErrorElement from '@/components/RouteErrorElement'; const LoginPage = React.lazy(() => import('./pages/LoginPage')); const DashboardPage = React.lazy(() => import('./pages/DashboardPage')); const EventsPage = React.lazy(() => import('./pages/EventsPage')); @@ -86,6 +87,7 @@ export const router = createBrowserRouter([ { path: ADMIN_BASE_PATH, element: , + errorElement: , children: [ { index: true, element: }, { path: 'login', element: }, @@ -124,5 +126,6 @@ export const router = createBrowserRouter([ { path: '*', element: , + errorElement: , }, ]); diff --git a/resources/js/components/RouteErrorElement.tsx b/resources/js/components/RouteErrorElement.tsx new file mode 100644 index 0000000..1f9bd72 --- /dev/null +++ b/resources/js/components/RouteErrorElement.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router-dom'; +import { AlertTriangle, RotateCcw } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; + +export function RouteErrorElement(): React.ReactElement { + const error = useRouteError(); + const navigate = useNavigate(); + + const statusText = (() => { + if (isRouteErrorResponse(error)) { + return `${error.status} ${error.statusText}`; + } + if (error instanceof Error) { + return error.message; + } + return 'Unerwarteter Fehler'; + })(); + + return ( + + + + + + + + Unerwarteter Fehler + {statusText} + + + + Etwas ist schiefgelaufen. Du kannst es erneut versuchen oder zur letzten Seite zurückkehren. + + + navigate(-1)}> + Zurück + + window.location.reload()}> + Neu laden + + + + + ); +} + +export default RouteErrorElement; + diff --git a/resources/js/guest/router.tsx b/resources/js/guest/router.tsx index ac7df7e..011f506 100644 --- a/resources/js/guest/router.tsx +++ b/resources/js/guest/router.tsx @@ -14,6 +14,7 @@ import { useTranslation, type TranslateFn } from './i18n/useTranslation'; import type { EventBranding } from './types/event-branding'; import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi'; import { NotificationCenterProvider } from './context/NotificationCenterContext'; +import RouteErrorElement from '@/components/RouteErrorElement'; const LandingPage = React.lazy(() => import('./pages/LandingPage')); const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage')); @@ -57,19 +58,21 @@ function HomeLayout() { } export const router = createBrowserRouter([ - { path: '/event', element: }, - { path: '/share/:slug', element: }, + { path: '/event', element: , errorElement: }, + { path: '/share/:slug', element: , errorElement: }, { path: '/setup/:token', - element: , + element: , + errorElement: , children: [ { index: true, element: }, ], }, - { path: '/g/:token', element: }, + { path: '/g/:token', element: , errorElement: }, { path: '/e/:token', element: , + errorElement: , children: [ { index: true, element: }, { path: 'tasks', element: }, @@ -84,11 +87,11 @@ export const router = createBrowserRouter([ { path: 'help/:slug', element: }, ], }, - { path: '/settings', element: }, - { path: '/legal/:page', element: }, - { path: '/help', element: }, - { path: '/help/:slug', element: }, - { path: '*', element: }, + { path: '/settings', element: , errorElement: }, + { path: '/legal/:page', element: , errorElement: }, + { path: '/help', element: , errorElement: }, + { path: '/help/:slug', element: , errorElement: }, + { path: '*', element: , errorElement: }, ]); function EventBoundary({ token }: { token: string }) {
Unerwarteter Fehler
{statusText}
+ Etwas ist schiefgelaufen. Du kannst es erneut versuchen oder zur letzten Seite zurückkehren. +