fixed errors in branding and invite page, added an error route for better react error display
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ fotospiel-tenant-app
|
|||||||
/public/build
|
/public/build
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
|
/public/fonts/google
|
||||||
/resources/js/actions
|
/resources/js/actions
|
||||||
/resources/js/routes
|
/resources/js/routes
|
||||||
/resources/js/wayfinder
|
/resources/js/wayfinder
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { getContrastingTextColor } from '../../guest/lib/color';
|
|||||||
import { buildEventTabs } from '../lib/eventTabs';
|
import { buildEventTabs } from '../lib/eventTabs';
|
||||||
import { ensureFontLoaded, useTenantFonts } from '../lib/fonts';
|
import { ensureFontLoaded, useTenantFonts } from '../lib/fonts';
|
||||||
|
|
||||||
|
const DEFAULT_FONT_VALUE = '__default';
|
||||||
|
const CUSTOM_FONT_VALUE = '__custom';
|
||||||
|
|
||||||
type BrandingForm = {
|
type BrandingForm = {
|
||||||
useDefault: boolean;
|
useDefault: boolean;
|
||||||
palette: {
|
palette: {
|
||||||
@@ -318,12 +321,12 @@ export default function EventBrandingPage(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolveFontSelectValue = (current: string): string => {
|
const resolveFontSelectValue = (current: string): string => {
|
||||||
if (!current) return '';
|
if (!current) return DEFAULT_FONT_VALUE;
|
||||||
return availableFonts.some((font) => font.family === current) ? current : '__custom';
|
return availableFonts.some((font) => font.family === current) ? current : CUSTOM_FONT_VALUE;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFontSelect = (key: 'heading' | 'body', value: string) => {
|
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 } }));
|
setForm((prev) => ({ ...prev, typography: { ...prev.typography, [key]: resolved } }));
|
||||||
const font = availableFonts.find((entry) => entry.family === resolved);
|
const font = availableFonts.find((entry) => entry.family === resolved);
|
||||||
if (font) {
|
if (font) {
|
||||||
@@ -446,11 +449,11 @@ export default function EventBrandingPage(): React.ReactElement {
|
|||||||
<SelectValue placeholder={t('branding.fontDefault', 'Standard (Tenant)')} />
|
<SelectValue placeholder={t('branding.fontDefault', 'Standard (Tenant)')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="">{t('branding.fontDefault', 'Standard (Tenant)')}</SelectItem>
|
<SelectItem value={DEFAULT_FONT_VALUE}>{t('branding.fontDefault', 'Standard (Tenant)')}</SelectItem>
|
||||||
{availableFonts.map((font) => (
|
{availableFonts.map((font) => (
|
||||||
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectItem value="__custom">{t('branding.fontCustom', 'Eigene Schrift eingeben')}</SelectItem>
|
<SelectItem value={CUSTOM_FONT_VALUE}>{t('branding.fontCustom', 'Eigene Schrift eingeben')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
@@ -471,11 +474,11 @@ export default function EventBrandingPage(): React.ReactElement {
|
|||||||
<SelectValue placeholder={t('branding.fontDefault', 'Standard (Tenant)')} />
|
<SelectValue placeholder={t('branding.fontDefault', 'Standard (Tenant)')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="">{t('branding.fontDefault', 'Standard (Tenant)')}</SelectItem>
|
<SelectItem value={DEFAULT_FONT_VALUE}>{t('branding.fontDefault', 'Standard (Tenant)')}</SelectItem>
|
||||||
{availableFonts.map((font) => (
|
{availableFonts.map((font) => (
|
||||||
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectItem value="__custom">{t('branding.fontCustom', 'Eigene Schrift eingeben')}</SelectItem>
|
<SelectItem value={CUSTOM_FONT_VALUE}>{t('branding.fontCustom', 'Eigene Schrift eingeben')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ensureFontLoaded, useTenantFonts } from '../../lib/fonts';
|
import { ensureFontLoaded, useTenantFonts } from '../../lib/fonts';
|
||||||
|
|
||||||
|
const DEFAULT_FONT_VALUE = '__default';
|
||||||
|
|
||||||
import type { EventQrInvite, EventQrInviteLayout } from '../../api';
|
import type { EventQrInvite, EventQrInviteLayout } from '../../api';
|
||||||
import { authorizedFetch } from '../../auth/tokens';
|
import { authorizedFetch } from '../../auth/tokens';
|
||||||
|
|
||||||
@@ -271,19 +273,6 @@ export function InviteLayoutCustomizerPanel({
|
|||||||
const appliedLayoutRef = React.useRef<string | null>(null);
|
const appliedLayoutRef = React.useRef<string | null>(null);
|
||||||
const appliedInviteRef = React.useRef<number | 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(() => {
|
React.useEffect(() => {
|
||||||
if (!availableFonts.length || !elements.length) {
|
if (!availableFonts.length || !elements.length) {
|
||||||
return;
|
return;
|
||||||
@@ -609,6 +598,19 @@ export function InviteLayoutCustomizerPanel({
|
|||||||
[commitElements]
|
[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(() => {
|
React.useEffect(() => {
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
setAvailableLayouts([]);
|
setAvailableLayouts([]);
|
||||||
@@ -1340,15 +1342,15 @@ export function InviteLayoutCustomizerPanel({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t('invites.customizer.elements.fontFamily', 'Schriftart')}</Label>
|
<Label>{t('invites.customizer.elements.fontFamily', 'Schriftart')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value={availableFonts.some((font) => font.family === element.fontFamily) ? element.fontFamily ?? '' : ''}
|
value={availableFonts.some((font) => font.family === element.fontFamily) ? element.fontFamily ?? DEFAULT_FONT_VALUE : DEFAULT_FONT_VALUE}
|
||||||
onValueChange={(value) => handleElementFontChange(element.id, value)}
|
onValueChange={(value) => handleElementFontChange(element.id, value === DEFAULT_FONT_VALUE ? '' : value)}
|
||||||
disabled={fontsLoading}
|
disabled={fontsLoading}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('invites.customizer.elements.fontPlaceholder', 'Standard')} />
|
<SelectValue placeholder={t('invites.customizer.elements.fontPlaceholder', 'Standard')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="">{t('invites.customizer.elements.fontPlaceholder', 'Standard')}</SelectItem>
|
<SelectItem value={DEFAULT_FONT_VALUE}>{t('invites.customizer.elements.fontPlaceholder', 'Standard')}</SelectItem>
|
||||||
{availableFonts.map((font) => (
|
{availableFonts.map((font) => (
|
||||||
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
<SelectItem key={font.family} value={font.family}>{font.family}</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ADMIN_LOGIN_START_PATH,
|
ADMIN_LOGIN_START_PATH,
|
||||||
ADMIN_PUBLIC_LANDING_PATH,
|
ADMIN_PUBLIC_LANDING_PATH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||||
const LoginPage = React.lazy(() => import('./pages/LoginPage'));
|
const LoginPage = React.lazy(() => import('./pages/LoginPage'));
|
||||||
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
|
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
|
||||||
const EventsPage = React.lazy(() => import('./pages/EventsPage'));
|
const EventsPage = React.lazy(() => import('./pages/EventsPage'));
|
||||||
@@ -86,6 +87,7 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: ADMIN_BASE_PATH,
|
path: ADMIN_BASE_PATH,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
|
errorElement: <RouteErrorElement />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <LandingGate /> },
|
{ index: true, element: <LandingGate /> },
|
||||||
{ path: 'login', element: <LoginPage /> },
|
{ path: 'login', element: <LoginPage /> },
|
||||||
@@ -124,5 +126,6 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
element: <Navigate to={ADMIN_PUBLIC_LANDING_PATH} replace />,
|
element: <Navigate to={ADMIN_PUBLIC_LANDING_PATH} replace />,
|
||||||
|
errorElement: <RouteErrorElement />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
50
resources/js/components/RouteErrorElement.tsx
Normal file
50
resources/js/components/RouteErrorElement.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
|
||||||
|
<div className="w-full max-w-lg rounded-2xl border border-slate-200 bg-white p-6 shadow-lg dark:border-slate-800 dark:bg-slate-900">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200">
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-50">Unerwarteter Fehler</p>
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-300">{statusText}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-slate-700 dark:text-slate-200">
|
||||||
|
Etwas ist schiefgelaufen. Du kannst es erneut versuchen oder zur letzten Seite zurückkehren.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 flex flex-wrap gap-2">
|
||||||
|
<Button variant="outline" onClick={() => navigate(-1)}>
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => window.location.reload()}>
|
||||||
|
<RotateCcw className="mr-2 h-4 w-4" /> Neu laden
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteErrorElement;
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ import { useTranslation, type TranslateFn } from './i18n/useTranslation';
|
|||||||
import type { EventBranding } from './types/event-branding';
|
import type { EventBranding } from './types/event-branding';
|
||||||
import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi';
|
import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi';
|
||||||
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
||||||
|
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||||
|
|
||||||
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
||||||
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
||||||
@@ -57,19 +58,21 @@ function HomeLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{ path: '/event', element: <SimpleLayout title="Event"><LandingPage /></SimpleLayout> },
|
{ path: '/event', element: <SimpleLayout title="Event"><LandingPage /></SimpleLayout>, errorElement: <RouteErrorElement /> },
|
||||||
{ path: '/share/:slug', element: <SharedPhotoPage /> },
|
{ path: '/share/:slug', element: <SharedPhotoPage />, errorElement: <RouteErrorElement /> },
|
||||||
{
|
{
|
||||||
path: '/setup/:token',
|
path: '/setup/:token',
|
||||||
element: <SetupLayout />,
|
element: <SetupLayout />,
|
||||||
|
errorElement: <RouteErrorElement />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <ProfileSetupPage /> },
|
{ index: true, element: <ProfileSetupPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/g/:token', element: <PublicGalleryPage /> },
|
{ path: '/g/:token', element: <PublicGalleryPage />, errorElement: <RouteErrorElement /> },
|
||||||
{
|
{
|
||||||
path: '/e/:token',
|
path: '/e/:token',
|
||||||
element: <HomeLayout />,
|
element: <HomeLayout />,
|
||||||
|
errorElement: <RouteErrorElement />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <HomePage /> },
|
{ index: true, element: <HomePage /> },
|
||||||
{ path: 'tasks', element: <TaskPickerPage /> },
|
{ path: 'tasks', element: <TaskPickerPage /> },
|
||||||
@@ -84,11 +87,11 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'help/:slug', element: <HelpArticlePage /> },
|
{ path: 'help/:slug', element: <HelpArticlePage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/settings', element: <SimpleLayout title="Einstellungen"><SettingsPage /></SimpleLayout> },
|
{ path: '/settings', element: <SimpleLayout title="Einstellungen"><SettingsPage /></SimpleLayout>, errorElement: <RouteErrorElement /> },
|
||||||
{ path: '/legal/:page', element: <SimpleLayout title="Rechtliches"><LegalPage /></SimpleLayout> },
|
{ path: '/legal/:page', element: <SimpleLayout title="Rechtliches"><LegalPage /></SimpleLayout>, errorElement: <RouteErrorElement /> },
|
||||||
{ path: '/help', element: <HelpStandalone /> },
|
{ path: '/help', element: <HelpStandalone />, errorElement: <RouteErrorElement /> },
|
||||||
{ path: '/help/:slug', element: <HelpArticleStandalone /> },
|
{ path: '/help/:slug', element: <HelpArticleStandalone />, errorElement: <RouteErrorElement /> },
|
||||||
{ path: '*', element: <NotFoundPage /> },
|
{ path: '*', element: <NotFoundPage />, errorElement: <RouteErrorElement /> },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function EventBoundary({ token }: { token: string }) {
|
function EventBoundary({ token }: { token: string }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user