Files
fotospiel-app/resources/js/admin/pages/WelcomeTeaserPage.tsx

494 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import {
ArrowRight,
Camera,
Clock3,
Heart,
MessageCircle,
Moon,
QrCode,
ShieldCheck,
Sparkles,
SunMedium,
Wand2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { LanguageSwitcher } from '../components/LanguageSwitcher';
import { FrostedSurface } from '../components/tenant';
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH, ADMIN_LOGIN_PATH } from '../constants';
import { encodeReturnTo, resolveReturnTarget } from '../lib/returnTo';
import { navigateToHref } from '../lib/navigation';
const heroStats = [
{ label: 'Events begleitet', value: '2.100+' },
{ label: 'Fotos kuratiert', value: '680k' },
{ label: 'Mission Cards live', value: '120+' },
];
const featureCards = [
{
icon: Sparkles,
badge: 'Welcome Flow',
title: 'Geführte Einrichtung',
description:
'Ein roter Faden von eurer ersten Mission bis zum Event-Branding. Alles läuft dort, wo ihr später auch Events steuert.',
},
{
icon: QrCode,
badge: 'Gäste einladen',
title: 'Links & QR-Cards teilen',
description:
'Mit einem Fingertipp entstehen Einladungslinks, QR-Codes und TWA-Links für Android & iOS ganz ohne App Store Hürden.',
},
{
icon: Camera,
badge: 'Live Galerie',
title: 'Moderieren wie im Dashboard',
description:
'Markiert Highlights, ordnet Emotions oder sperrt Fotos exakt die Tools aus dem Event Admin, nur mit Willkommens-Glow.',
},
];
const timelineSteps = [
{
title: 'Mission Cards wählen',
description: 'Kuratiert Aufgaben, die eure Gäste ins Erzählen bringen. Jeder Schritt speichert automatisch.',
},
{
title: 'Event Branding festlegen',
description: 'Farben, Story, Cover alles in einer Ansicht. Vorschau zeigt sofort, wie der Gastzugang wirkt.',
},
{
title: 'Link teilen & feiern',
description: 'QR-Code oder Link verschicken, fertig. Gäste landen direkt in eurer Galerie und können ohne Login starten.',
},
];
const supportHighlights = [
{
icon: ShieldCheck,
title: 'Datenschutz-ready',
description: 'Keine versteckten Tracker, DSGVO-konformes Hosting und Kontrolle, wer Fotos sieht.',
},
{
icon: Clock3,
title: 'Offline nutzbar',
description: 'Uploads puffern automatisch, falls das WLAN in der Location aussetzt.',
},
{
icon: MessageCircle,
title: 'Crew an eurer Seite',
description: 'Direkter Chat zum Fotospiel-Team aus der App heraus oder via hallo@fotospiel.de.',
},
];
export default function WelcomeTeaserPage() {
const [isRedirecting, setIsRedirecting] = React.useState(false);
const [mode, setMode] = React.useState<'dark' | 'light'>('dark');
const isLightMode = mode === 'light';
const flowSectionRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
document.body.classList.add('tenant-admin-theme', 'tenant-admin-welcome-theme');
return () => {
document.body.classList.remove('tenant-admin-theme', 'tenant-admin-welcome-theme');
};
}, []);
React.useEffect(() => {
document.body.classList.toggle('tenant-admin-welcome-light', isLightMode);
document.body.classList.toggle('tenant-admin-welcome-dark', !isLightMode);
}, [isLightMode]);
const theme = React.useMemo(
() => ({
rootBackground: isLightMode
? 'bg-gradient-to-br from-rose-50 via-white to-sky-50 text-slate-900'
: 'bg-slate-950 text-white',
aurora: isLightMode
? 'bg-[radial-gradient(ellipse_at_top,_rgba(255,179,205,0.55),_transparent_55%),radial-gradient(ellipse_at_bottom,_rgba(148,187,233,0.45),_transparent_60%)]'
: 'bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.25),_transparent_60%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.25),_transparent_62%)]',
overlay: isLightMode
? 'bg-gradient-to-br from-white/85 via-rose-50/70 to-sky-50/70'
: 'bg-gradient-to-br from-slate-950/95 via-slate-950/80 to-[#150b1f]/90',
headerBadge: isLightMode ? 'border-rose-100 bg-white text-rose-500' : 'border-white/30 bg-white/5 text-white',
headerSubtitle: isLightMode ? 'text-slate-600' : 'text-white/70',
heroEyebrow: isLightMode ? 'text-rose-500' : 'text-rose-200',
heroTitle: isLightMode ? 'text-slate-900' : 'text-white',
heroBody: isLightMode ? 'text-slate-600' : 'text-white/75',
ghostButton: isLightMode ? 'text-slate-700 hover:bg-slate-100' : 'text-white/80 hover:bg-white/10',
statCard: isLightMode
? 'border-rose-100/70 bg-white/90 text-slate-900 shadow-rose-100/50'
: 'border-white/15 bg-white/10 text-white shadow-rose-500/30',
featureCard: isLightMode
? 'border-rose-100 bg-white text-slate-900 shadow-rose-100/40'
: 'border-white/15 bg-white/10 text-white shadow-slate-900/40',
timelineCard: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/5 text-white',
timelineDescription: isLightMode ? 'text-slate-600' : 'text-white/70',
supportCard: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/10 text-white',
supportDescription: isLightMode ? 'text-slate-600' : 'text-white/70',
ctaSurface: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/10 text-white',
ctaDetail: isLightMode ? 'text-slate-600' : 'text-white/70',
footer: isLightMode
? 'border-t border-slate-200/80 bg-white/70 text-slate-500'
: 'border-t border-white/10 bg-black/20 text-white/60',
}),
[isLightMode]
);
const toggleMode = React.useCallback(() => {
setMode((prev) => (prev === 'dark' ? 'light' : 'dark'));
}, []);
const handleLoginRedirect = React.useCallback(() => {
if (isRedirecting) {
return;
}
setIsRedirecting(true);
const params = new URLSearchParams(window.location.search);
const rawReturnTo = params.get('return_to');
const { finalTarget, encodedFinal } = resolveReturnTarget(rawReturnTo, ADMIN_DEFAULT_AFTER_LOGIN_PATH);
const target = new URL(ADMIN_LOGIN_PATH, window.location.origin);
target.searchParams.set('return_to', encodedFinal ?? encodeReturnTo(finalTarget));
const nextHref = `${target.pathname}${target.search}`;
navigateToHref(nextHref);
}, [isRedirecting]);
const handleScrollToFlow = React.useCallback(() => {
flowSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, []);
return (
<div className={cn('relative min-h-svh overflow-hidden transition-colors duration-500', theme.rootBackground)}>
<div
aria-hidden
className={cn(
'pointer-events-none absolute inset-0 motion-safe:animate-[aurora_24s_ease-in-out_infinite]',
theme.aurora
)}
/>
<div aria-hidden className={cn('absolute inset-0 transition-colors duration-500', theme.overlay)} />
<div className="relative z-10 flex min-h-svh flex-col">
<header className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-4 px-6 pb-4 pt-8">
<div className="space-y-1">
<Badge
variant="outline"
className={cn(
'border text-[11px] font-semibold uppercase tracking-[0.4em] transition-colors duration-300',
theme.headerBadge
)}
>
Event Admin · Fotospiel
</Badge>
<p className={cn('text-sm transition-colors duration-300', theme.headerSubtitle)}>
Euer mobiles Kontrollzentrum für Events, Workshops und Feiern
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<LanguageSwitcher />
<Button
type="button"
variant="outline"
className={cn(
'items-center gap-2 border text-xs font-semibold uppercase tracking-wide',
isLightMode
? 'border-slate-200 text-slate-700 hover:bg-slate-100'
: 'border-white/30 text-white hover:bg-white/10'
)}
onClick={toggleMode}
>
{isLightMode ? (
<>
<Moon className="h-4 w-4" />
Dark Mode
</>
) : (
<>
<SunMedium className="h-4 w-4" />
Light Mode
</>
)}
</Button>
<Button
type="button"
variant="outline"
className={cn(
'border text-sm font-semibold transition-colors duration-300',
isLightMode ? 'border-slate-200 text-slate-700 hover:bg-slate-100' : 'border-white/30 text-white hover:bg-white/10'
)}
onClick={handleLoginRedirect}
disabled={isRedirecting}
>
{isRedirecting ? 'Weiterleitung …' : 'Login öffnen'}
</Button>
</div>
</header>
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-12 px-6 pb-16 pt-4">
<section className="grid gap-10 lg:grid-cols-[1.1fr_0.9fr]">
<div className="space-y-8">
<div className="space-y-4">
<p
className={cn(
'text-sm font-semibold uppercase tracking-[0.4em] transition-colors duration-300',
theme.heroEyebrow
)}
>
Willkommen im Event-Kontrollzentrum
</p>
<h1
className={cn(
'font-display text-4xl font-semibold leading-tight transition-colors duration-300 md:text-5xl',
theme.heroTitle
)}
>
Alles für eure Gästegeschichte in einer App
</h1>
<p
className={cn(
'text-base leading-relaxed transition-colors duration-300 md:text-lg',
theme.heroBody
)}
>
Der neue Startscreen begrüßt euch wie eine mobile App, führt euch in den Welcome Flow und lässt euch
jederzeit zurück ins Dashboard springen, sobald ihr bereit seid.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button type="button" size="lg" onClick={handleLoginRedirect} disabled={isRedirecting}>
{isRedirecting ? 'Weiterleitung …' : 'Event Admin öffnen'}
<ArrowRight className="h-4 w-4" />
</Button>
<Button
type="button"
size="lg"
variant="ghost"
className={cn('transition-colors duration-300', theme.ghostButton)}
onClick={handleScrollToFlow}
>
Geführten Ablauf ansehen
<Heart className="h-4 w-4" />
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{heroStats.map((stat) => (
<FrostedSurface
key={stat.label}
className={cn(
'rounded-3xl border px-5 py-4 shadow-2xl backdrop-blur transition-colors duration-300',
theme.statCard
)}
>
<p className={cn('text-2xl font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>{stat.value}</p>
<p
className={cn(
'text-xs font-semibold uppercase tracking-wide',
isLightMode ? 'text-slate-500' : 'text-white/70'
)}
>
{stat.label}
</p>
</FrostedSurface>
))}
</div>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-0 -z-10 translate-y-6 scale-105 rounded-[40px] bg-gradient-to-br from-rose-500/40 via-white/5 to-sky-500/30 blur-3xl" />
<div className="relative rounded-[40px] border border-white/15 bg-white/95 p-6 text-slate-900 shadow-[0_40px_90px_rgba(15,23,42,0.55)]">
<div className="flex items-center justify-between text-xs font-semibold text-slate-500">
<span>Fotospiel</span>
<span>Event Admin</span>
</div>
<div className="mt-4 rounded-3xl bg-slate-900/5 p-4">
<div className="flex items-center justify-between text-xs font-semibold text-slate-500">
<span>Heute</span>
<span>Live</span>
</div>
<div className="mt-4 space-y-3">
{['Mission Pack auswählen', 'Event Branding finalisieren', 'Einladungslink teilen'].map((item) => (
<div key={item} className="flex items-center justify-between rounded-2xl bg-white p-3 text-sm shadow">
<div>
<p className="font-semibold text-slate-900">{item}</p>
<p className="text-xs text-slate-500">Welcome Flow</p>
</div>
<Wand2 className="h-5 w-5 text-rose-500" />
</div>
))}
</div>
</div>
<div className="mt-6 rounded-3xl bg-slate-900 text-white">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-3 text-xs uppercase tracking-[0.3em] text-white/70">
<span>Live Moments</span>
<span>+28</span>
</div>
<div className="space-y-4 px-5 py-6">
{[
{ title: 'Anna lädt 6 Fotos hoch', subtitle: 'Event · Gartenpalast' },
{ title: 'Max reagiert auf Mission „Emotionen“', subtitle: 'Tasks · Emotion Board' },
{ title: 'QR-Link 124x gescannt', subtitle: 'Einladungen · Photobooth' },
].map((entry) => (
<div key={entry.title} className="space-y-1">
<p className="text-sm font-semibold">{entry.title}</p>
<p className="text-xs text-white/70">{entry.subtitle}</p>
</div>
))}
</div>
</div>
</div>
</div>
</section>
<section className="grid gap-6 lg:grid-cols-3">
{featureCards.map((feature) => (
<FrostedSurface
key={feature.title}
className={cn(
'flex flex-col gap-4 rounded-3xl border p-6 shadow-lg backdrop-blur transition-colors duration-300',
theme.featureCard
)}
>
<div
className={cn('flex items-center gap-3 text-sm', isLightMode ? 'text-rose-600' : 'text-white/80')}
>
<feature.icon className={cn('h-5 w-5', isLightMode ? 'text-rose-400' : 'text-rose-200')} />
<span>{feature.badge}</span>
</div>
<h2 className={cn('text-2xl font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>
{feature.title}
</h2>
<p
className={cn('text-sm leading-relaxed', isLightMode ? 'text-slate-600' : 'text-white/75')}
>
{feature.description}
</p>
</FrostedSurface>
))}
</section>
<section ref={flowSectionRef} className="grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
<FrostedSurface
className={cn('rounded-3xl border p-6 transition-colors duration-300', theme.timelineCard)}
>
<p
className={cn(
'text-xs font-semibold uppercase tracking-[0.4em]',
isLightMode ? 'text-rose-500' : 'text-rose-200'
)}
>
Guided Journey
</p>
<h3 className={cn('mt-3 text-3xl font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>
So leitet euch der Welcome Flow
</h3>
<div className="mt-6 space-y-4">
{timelineSteps.map((step, index) => (
<div
key={step.title}
className={cn(
'flex gap-4 rounded-2xl border p-4 transition-colors duration-300',
isLightMode ? 'border-slate-200 bg-white' : 'border-white/15 bg-white/5'
)}
>
<div
className={cn(
'flex h-10 w-10 items-center justify-center rounded-2xl text-lg font-semibold',
isLightMode ? 'bg-rose-100 text-rose-600' : 'bg-rose-500/15 text-white'
)}
>
{index + 1}
</div>
<div>
<p className={cn('font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>
{step.title}
</p>
<p className={cn('text-sm', theme.timelineDescription)}>{step.description}</p>
</div>
</div>
))}
</div>
</FrostedSurface>
<div className="space-y-6">
<FrostedSurface className="flex flex-col gap-4 rounded-3xl border-white/15 bg-white/95 p-6 text-slate-900 shadow-2xl">
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-[0.3em] text-slate-500">
<span>Mobile Greeting</span>
<span>Neu</span>
</div>
<p className="text-2xl font-semibold text-slate-900">Fühlt sich an wie eine native App</p>
<p className="text-sm text-slate-600">
Mit großen Touch-Flächen, Animationen und frosted Cards wirkt der Startscreen wie die mobile Version
des Event Admins perfekt für TWA und Capacitor Builds.
</p>
<div className="mt-2 grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl bg-slate-900/5 p-4">
<p className="text-xs text-slate-500">CTA</p>
<p className="text-lg font-semibold text-slate-900">Event Admin öffnen</p>
<p className="text-sm text-slate-600">Leitet direkt zum OAuth Login</p>
</div>
<div className="rounded-2xl bg-slate-900/5 p-4">
<p className="text-xs text-slate-500">Scroll Action</p>
<p className="text-lg font-semibold text-slate-900">Journey ansehen</p>
<p className="text-sm text-slate-600">Smooth Scroll bis zur Timeline</p>
</div>
</div>
</FrostedSurface>
<div className="grid gap-4 sm:grid-cols-3">
{supportHighlights.map((support) => (
<FrostedSurface
key={support.title}
className={cn(
'flex flex-col gap-2 rounded-2xl border p-4 transition-colors duration-300',
theme.supportCard
)}
>
<support.icon className={cn('h-5 w-5', isLightMode ? 'text-rose-500' : 'text-rose-200')} />
<p className={cn('font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>
{support.title}
</p>
<p className={cn('text-sm', theme.supportDescription)}>{support.description}</p>
</FrostedSurface>
))}
</div>
</div>
</section>
<FrostedSurface
className={cn(
'flex flex-col gap-4 rounded-3xl border px-6 py-5 transition-colors duration-300 md:flex-row md:items-center md:justify-between',
theme.ctaSurface
)}
>
<div>
<p className={cn('text-sm uppercase tracking-[0.4em]', theme.ctaDetail)}>Bereit?</p>
<h3 className={cn('text-2xl font-semibold', isLightMode ? 'text-slate-900' : 'text-white')}>
Wechselt ins Dashboard, sobald euer Flow steht.
</h3>
<p className={cn('text-sm', theme.ctaDetail)}>
Alle Schritte lassen sich später direkt aus dem Event Admin wieder öffnen.
</p>
</div>
<Button type="button" size="lg" className="self-start md:self-auto" onClick={handleLoginRedirect} disabled={isRedirecting}>
{isRedirecting ? 'Weiterleitung …' : 'Zum Login'}
<ArrowRight className="h-4 w-4" />
</Button>
</FrostedSurface>
</main>
<footer className={cn('py-6 text-center text-xs transition-colors duration-300', theme.footer)}>
Fotospiel · Eure Gäste gestalten eure Lieblingsmomente
</footer>
</div>
</div>
);
}