die tenant admin oauth authentifizierung wurde implementiert und funktioniert jetzt. Zudem wurde das marketing frontend dashboard implementiert.

This commit is contained in:
Codex Agent
2025-11-04 16:14:07 +01:00
parent 55c606bdd4
commit 92e64c361a
11 changed files with 407 additions and 156 deletions

View File

@@ -9,8 +9,10 @@ import { useGuestIdentity } from '../context/GuestIdentityContext';
import { useEventStats } from '../context/EventStatsContext';
import { useEventData } from '../hooks/useEventData';
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
import { Sparkles, UploadCloud, Images, CheckCircle2, Users, TimerReset } from 'lucide-react';
import { Sparkles, UploadCloud, Images, CheckCircle2, Users, TimerReset, X } from 'lucide-react';
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { useEventBranding } from '../context/EventBrandingContext';
import type { EventBranding } from '../types/event-branding';
export default function HomePage() {
const { token } = useParams<{ token: string }>();
@@ -19,12 +21,51 @@ export default function HomePage() {
const { event } = useEventData();
const { completedCount } = useGuestTaskProgress(token);
const { t } = useTranslation();
const { branding } = useEventBranding();
if (!token) return null;
const heroStorageKey = token ? `guestHeroDismissed_${token}` : 'guestHeroDismissed';
const [heroVisible, setHeroVisible] = React.useState(() => {
if (typeof window === 'undefined') {
return true;
}
try {
return window.sessionStorage.getItem(heroStorageKey) !== '1';
} catch {
return true;
}
});
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
setHeroVisible(window.sessionStorage.getItem(heroStorageKey) !== '1');
} catch {
setHeroVisible(true);
}
}, [heroStorageKey]);
const dismissHero = React.useCallback(() => {
setHeroVisible(false);
if (typeof window === 'undefined') {
return;
}
try {
window.sessionStorage.setItem(heroStorageKey, '1');
} catch {
// ignore storage exceptions (e.g. private mode)
}
}, [heroStorageKey]);
const displayName = hydrated && name ? name : t('home.fallbackGuestName');
const eventNameDisplay = event?.name ?? t('home.hero.defaultEventName');
const latestUploadText = formatLatestUpload(stats.latestPhotoAt, t);
const accentColor = branding.primaryColor;
const secondaryAccent = branding.secondaryColor;
const primaryActions = React.useMemo(
() => [
@@ -32,19 +73,19 @@ export default function HomePage() {
to: 'tasks',
label: t('home.actions.items.tasks.label'),
description: t('home.actions.items.tasks.description'),
icon: <Sparkles className="h-5 w-5" />,
icon: <Sparkles className="h-5 w-5" aria-hidden />,
},
{
to: 'upload',
label: t('home.actions.items.upload.label'),
description: t('home.actions.items.upload.description'),
icon: <UploadCloud className="h-5 w-5" />,
icon: <UploadCloud className="h-5 w-5" aria-hidden />,
},
{
to: 'gallery',
label: t('home.actions.items.gallery.label'),
description: t('home.actions.items.gallery.description'),
icon: <Images className="h-5 w-5" />,
icon: <Images className="h-5 w-5" aria-hidden />,
},
],
[t],
@@ -59,42 +100,54 @@ export default function HomePage() {
[t],
);
if (!token) {
return null;
}
return (
<div className="space-y-6 pb-24">
<HeroCard
name={displayName}
eventName={eventNameDisplay}
tasksCompleted={completedCount}
t={t}
/>
{heroVisible && (
<HeroCard
name={displayName}
eventName={eventNameDisplay}
tasksCompleted={completedCount}
t={t}
branding={branding}
onDismiss={dismissHero}
/>
)}
<Card>
<Card style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
<CardContent className="grid grid-cols-1 gap-4 py-4 sm:grid-cols-4">
<StatTile
icon={<Users className="h-4 w-4" />}
icon={<Users className="h-4 w-4" aria-hidden />}
label={t('home.stats.online')}
value={`${stats.onlineGuests}`}
accentColor={accentColor}
/>
<StatTile
icon={<Sparkles className="h-4 w-4" />}
icon={<Sparkles className="h-4 w-4" aria-hidden />}
label={t('home.stats.tasksSolved')}
value={`${stats.tasksSolved}`}
accentColor={accentColor}
/>
<StatTile
icon={<TimerReset className="h-4 w-4" />}
icon={<TimerReset className="h-4 w-4" aria-hidden />}
label={t('home.stats.lastUpload')}
value={latestUploadText}
accentColor={accentColor}
/>
<StatTile
icon={<CheckCircle2 className="h-4 w-4" />}
icon={<CheckCircle2 className="h-4 w-4" aria-hidden />}
label={t('home.stats.completedTasks')}
value={`${completedCount}`}
accentColor={accentColor}
/>
</CardContent>
</Card>
<section className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{t('home.actions.title')}
</h2>
@@ -102,14 +155,20 @@ export default function HomePage() {
</div>
<div className="grid gap-3 sm:grid-cols-2">
{primaryActions.map((action) => (
<Link to={action.to} key={action.to} className="block">
<Link to={action.to} key={action.to} className="block touch-manipulation">
<Card className="transition-all hover:shadow-lg">
<CardContent className="flex items-center gap-3 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-pink-100 text-pink-600">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg shadow-sm"
style={{
backgroundColor: `${secondaryAccent}1a`,
color: secondaryAccent,
}}
>
{action.icon}
</div>
<div className="flex flex-col">
<span className="text-base font-semibold">{action.label}</span>
<span className="text-base font-semibold" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>{action.label}</span>
<span className="text-sm text-muted-foreground">{action.description}</span>
</div>
</CardContent>
@@ -117,20 +176,25 @@ export default function HomePage() {
</Link>
))}
</div>
<Button variant="outline" asChild className="w-full">
<Button
variant="outline"
asChild
className="w-full touch-manipulation"
style={{ borderColor: `${accentColor}33` }}
>
<Link to="queue">{t('home.actions.queueButton')}</Link>
</Button>
</section>
<Card>
<CardHeader>
<CardHeader style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
<CardTitle>{t('home.checklist.title')}</CardTitle>
<CardDescription>{t('home.checklist.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{checklistItems.map((item) => (
<div key={item} className="flex items-start gap-3">
<CheckCircle2 className="mt-0.5 h-5 w-5 text-green-500" />
<CheckCircle2 className="mt-0.5 h-5 w-5 text-green-500" aria-hidden />
<span className="text-sm leading-relaxed text-muted-foreground">{item}</span>
</div>
))}
@@ -151,11 +215,15 @@ function HeroCard({
eventName,
tasksCompleted,
t,
branding,
onDismiss,
}: {
name: string;
eventName: string;
tasksCompleted: number;
t: TranslateFn;
branding: EventBranding;
onDismiss: () => void;
}) {
const heroTitle = t('home.hero.title').replace('{name}', name);
const heroDescription = t('home.hero.description').replace('{eventName}', eventName);
@@ -163,22 +231,41 @@ function HeroCard({
? t('home.hero.progress.some').replace('{count}', `${tasksCompleted}`)
: t('home.hero.progress.none');
const style = React.useMemo(() => ({
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
color: '#ffffff',
fontFamily: branding.fontFamily ?? undefined,
}), [branding.fontFamily, branding.primaryColor, branding.secondaryColor]);
return (
<Card className="overflow-hidden border-0 bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-md">
<CardHeader className="space-y-1">
<Card className="relative overflow-hidden border-0 text-white shadow-md" style={style}>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-3 top-3 h-8 w-8 rounded-full bg-white/20 text-white hover:bg-white/30"
onClick={onDismiss}
>
<X className="h-4 w-4" aria-hidden />
<span className="sr-only">{t('common.actions.close')}</span>
</Button>
<CardHeader className="space-y-2 pr-10">
<CardDescription className="text-sm text-white/80">{t('home.hero.subtitle')}</CardDescription>
<CardTitle className="text-2xl font-bold">{heroTitle}</CardTitle>
<p className="text-sm text-white/80">{heroDescription}</p>
<CardTitle className="text-2xl font-bold leading-snug">{heroTitle}</CardTitle>
<p className="text-sm text-white/85">{heroDescription}</p>
<p className="text-sm font-medium text-white/90">{progressMessage}</p>
</CardHeader>
</Card>
);
}
function StatTile({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
function StatTile({ icon, label, value, accentColor }: { icon: React.ReactNode; label: string; value: string; accentColor: string }) {
return (
<div className="flex items-center gap-3 rounded-lg border bg-muted/30 px-3 py-2">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white text-pink-600 shadow-sm">
<div
className="flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-sm"
style={{ color: accentColor }}
>
{icon}
</div>
<div className="flex flex-col">