- Reworked the tenant admin login page
- Updated the User model to implement Filament’s tenancy contracts - Seeded a ready-to-use demo tenant (user, tenant, active package, purchase) - Introduced a branded, translated 403 error page to replace the generic forbidden message for unauthorised admin hits - Removed the public “Register” links from the marketing header - hardened join event logic and improved error handling in the guest pwa.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter, Outlet, useParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { createBrowserRouter, Outlet, useParams, Link } from 'react-router-dom';
|
||||
import Header from './components/Header';
|
||||
import BottomNav from './components/BottomNav';
|
||||
import { useEventData } from './hooks/useEventData';
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react';
|
||||
import type { FetchEventErrorCode } from './services/eventApi';
|
||||
import { EventStatsProvider } from './context/EventStatsContext';
|
||||
import { GuestIdentityProvider } from './context/GuestIdentityContext';
|
||||
import LandingPage from './pages/LandingPage';
|
||||
@@ -36,15 +40,7 @@ function HomeLayout() {
|
||||
|
||||
return (
|
||||
<GuestIdentityProvider eventKey={token}>
|
||||
<EventStatsProvider eventKey={token}>
|
||||
<div className="pb-16">
|
||||
<Header slug={token} />
|
||||
<div className="px-4 py-3">
|
||||
<Outlet />
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</EventStatsProvider>
|
||||
<EventBoundary token={token} />
|
||||
</GuestIdentityProvider>
|
||||
);
|
||||
}
|
||||
@@ -78,6 +74,30 @@ export const router = createBrowserRouter([
|
||||
{ path: '*', element: <NotFoundPage /> },
|
||||
]);
|
||||
|
||||
function EventBoundary({ token }: { token: string }) {
|
||||
const { event, status, error, errorCode } = useEventData();
|
||||
|
||||
if (status === 'loading') {
|
||||
return <EventLoadingView />;
|
||||
}
|
||||
|
||||
if (status === 'error' || !event) {
|
||||
return <EventErrorView code={errorCode} message={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventStatsProvider eventKey={token}>
|
||||
<div className="pb-16">
|
||||
<Header slug={token} />
|
||||
<div className="px-4 py-3">
|
||||
<Outlet />
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</EventStatsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupLayout() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
if (!token) return null;
|
||||
@@ -93,6 +113,95 @@ function SetupLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
function EventLoadingView() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-muted-foreground" aria-hidden />
|
||||
<div className="space-y-1">
|
||||
<p className="text-lg font-semibold text-foreground">Wir prüfen deinen Zugang...</p>
|
||||
<p className="text-sm text-muted-foreground">Einen Moment bitte.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventErrorViewProps {
|
||||
code: FetchEventErrorCode | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
function EventErrorView({ code, message }: EventErrorViewProps) {
|
||||
const content = getErrorContent(code, message);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100 text-red-600">
|
||||
<AlertTriangle className="h-8 w-8" aria-hidden />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold text-foreground">{content.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{content.description}</p>
|
||||
{content.hint && (
|
||||
<p className="text-xs text-muted-foreground">{content.hint}</p>
|
||||
)}
|
||||
</div>
|
||||
{content.ctaHref && content.ctaLabel && (
|
||||
<Button asChild>
|
||||
<Link to={content.ctaHref}>{content.ctaLabel}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getErrorContent(
|
||||
code: FetchEventErrorCode | null,
|
||||
message: string | null,
|
||||
) {
|
||||
const base = (fallbackTitle: string, fallbackDescription: string, options?: { ctaLabel?: string; ctaHref?: string; hint?: string }) => ({
|
||||
title: fallbackTitle,
|
||||
description: message ?? fallbackDescription,
|
||||
ctaLabel: options?.ctaLabel,
|
||||
ctaHref: options?.ctaHref,
|
||||
hint: options?.hint ?? null,
|
||||
});
|
||||
|
||||
switch (code) {
|
||||
case 'invalid_token':
|
||||
return base('Zugriffscode ungültig', 'Der eingegebene Code konnte nicht verifiziert werden.', {
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
ctaHref: '/event',
|
||||
});
|
||||
case 'token_revoked':
|
||||
return base('Zugriffscode deaktiviert', 'Dieser Code wurde zurückgezogen. Bitte fordere einen neuen Code an.', {
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
ctaHref: '/event',
|
||||
});
|
||||
case 'token_expired':
|
||||
return base('Zugriffscode abgelaufen', 'Der Code ist nicht mehr gültig. Aktualisiere deinen Code, um fortzufahren.', {
|
||||
ctaLabel: 'Code aktualisieren',
|
||||
ctaHref: '/event',
|
||||
});
|
||||
case 'token_rate_limited':
|
||||
return base('Zu viele Versuche', 'Es gab zu viele Eingaben in kurzer Zeit. Warte kurz und versuche es erneut.', {
|
||||
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten möglich.',
|
||||
});
|
||||
case 'event_not_public':
|
||||
return base('Event nicht öffentlich', 'Dieses Event ist aktuell nicht öffentlich zugänglich.', {
|
||||
hint: 'Nimm Kontakt mit den Veranstalter:innen auf, um Zugang zu erhalten.',
|
||||
});
|
||||
case 'network_error':
|
||||
return base('Verbindungsproblem', 'Wir konnten keine Verbindung zum Server herstellen. Prüfe deine Internetverbindung und versuche es erneut.');
|
||||
case 'server_error':
|
||||
return base('Server nicht erreichbar', 'Der Server reagiert derzeit nicht. Versuche es später erneut.');
|
||||
default:
|
||||
return base('Event nicht erreichbar', 'Wir konnten dein Event nicht laden. Bitte versuche es erneut.', {
|
||||
ctaLabel: 'Zur Code-Eingabe',
|
||||
ctaHref: '/event',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function SimpleLayout({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
|
||||
Reference in New Issue
Block a user