checkout: buttons verbessert, paddle zahlungsschritt schicker gemacht, schritt 4 optimiert+schick gemacht. Dashboard: translations ergänzt. Startseite vom Event Admin optimiert.
This commit is contained in:
@@ -1,31 +1,148 @@
|
||||
import React from 'react';
|
||||
import { Sparkles, Users, Camera, Heart, ArrowRight } from 'lucide-react';
|
||||
import {
|
||||
ArrowRight,
|
||||
Camera,
|
||||
Clock3,
|
||||
Heart,
|
||||
MessageCircle,
|
||||
Moon,
|
||||
QrCode,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
SunMedium,
|
||||
Users,
|
||||
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 highlights = [
|
||||
const heroStats = [
|
||||
{ label: 'Events begleitet', value: '2.100+' },
|
||||
{ label: 'Fotos kuratiert', value: '680k' },
|
||||
{ label: 'Mission Cards live', value: '120+' },
|
||||
];
|
||||
|
||||
const featureCards = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: 'Momente lenken, nicht das Handy',
|
||||
badge: 'Welcome Flow',
|
||||
title: 'Geführte Einrichtung',
|
||||
description:
|
||||
'Fotospiel liefert euch spielerische Aufgaben, damit eure Gäste das Fest genießen und gleichzeitig emotionale Motive festhalten.',
|
||||
'Ein roter Faden von eurer ersten Mission bis zum Event-Branding. Alles läuft dort, wo ihr später auch Events steuert.',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Alle Gäste auf einer Reise',
|
||||
icon: QrCode,
|
||||
badge: 'Gäste einladen',
|
||||
title: 'Links & QR-Cards teilen',
|
||||
description:
|
||||
'Einladungslinks und QR-Codes führen direkt in eure Event-Galerie. Kein Technik-Know-how nötig – nur teilen und loslegen.',
|
||||
'Mit einem Fingertipp entstehen Einladungslinks, QR-Codes und TWA-Links für Android & iOS – ganz ohne App Store Hürden.',
|
||||
},
|
||||
{
|
||||
icon: Camera,
|
||||
title: 'Live-Galerie und Moderation',
|
||||
badge: 'Live Galerie',
|
||||
title: 'Moderieren wie im Dashboard',
|
||||
description:
|
||||
'Sammelt Bilder in Echtzeit, markiert Highlights und entscheidet gemeinsam, welche Erinnerungen groß rauskommen.',
|
||||
'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) {
|
||||
@@ -39,90 +156,339 @@ export default function WelcomeTeaserPage() {
|
||||
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));
|
||||
window.location.href = `${target.pathname}${target.search}`;
|
||||
const nextHref = `${target.pathname}${target.search}`;
|
||||
navigateToHref(nextHref);
|
||||
}, [isRedirecting]);
|
||||
|
||||
const handleScrollToFlow = React.useCallback(() => {
|
||||
flowSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 via-white to-sky-50 text-slate-800">
|
||||
<header className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 pb-12 pt-16 text-center md:pt-20">
|
||||
<div className="mx-auto w-fit rounded-full border border-rose-100 bg-white/80 px-4 py-1 text-sm font-medium text-rose-500 shadow-sm">
|
||||
Willkommen bei Fotospiel
|
||||
</div>
|
||||
<h1 className="font-display text-4xl font-semibold tracking-tight text-slate-900 md:text-5xl">
|
||||
Eure Gäste als Geschichtenerzähler – ohne Technikstress
|
||||
</h1>
|
||||
<p className="mx-auto max-w-2xl text-base leading-relaxed text-slate-600 md:text-lg">
|
||||
Dieses Kontrollzentrum zeigt euch, wie ihr Fotospiel für Hochzeit, Jubiläum oder Team-Event einsetzt.
|
||||
Wir führen euch Schritt für Schritt durch Aufgaben, Event-Setup und Einladungen.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center gap-3 md:flex-row">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group inline-flex items-center justify-center gap-2 rounded-full border border-transparent bg-white px-6 py-3 text-base font-semibold text-rose-500 shadow-sm transition hover:border-rose-200 hover:text-rose-600"
|
||||
onClick={handleLoginRedirect}
|
||||
disabled={isRedirecting}
|
||||
>
|
||||
{isRedirecting ? 'Weiterleitung ...' : 'Ich habe bereits Zugang'}
|
||||
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<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)} />
|
||||
|
||||
<main className="mx-auto w-full max-w-5xl space-y-12 px-6 pb-16">
|
||||
<section className="grid gap-6 rounded-3xl border border-white/60 bg-white/80 p-6 shadow-xl shadow-rose-100/40 backdrop-blur-md md:grid-cols-3 md:p-8">
|
||||
{highlights.map((item) => (
|
||||
<article key={item.title} className="flex flex-col gap-4 rounded-2xl bg-white/70 p-5 shadow-sm">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-rose-100 text-rose-500">
|
||||
<item.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">{item.title}</h2>
|
||||
<p className="text-sm leading-relaxed text-slate-600">{item.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 rounded-3xl border border-sky-100 bg-gradient-to-br from-white via-sky-50 to-white p-6 shadow-lg md:grid-cols-2 md:p-10">
|
||||
<div className="space-y-5">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-sky-100 px-3 py-1 text-xs font-semibold text-sky-700">
|
||||
<Heart className="h-4 w-4" />
|
||||
So startet ihr
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-slate-900 md:text-3xl">In drei Schritten zu eurer Story</h2>
|
||||
<ol className="space-y-4 text-sm leading-relaxed text-slate-600">
|
||||
<li>
|
||||
<span className="font-semibold text-slate-900">1. Aufgaben entdecken </span>
|
||||
Stellt euer erstes Aufgabenpaket zusammen, das zu eurer Feier passt.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-slate-900">2. Event anlegen </span>
|
||||
Benennt euer Event, legt Farben fest und erstellt den QR-Einladungslink.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-slate-900">3. Link teilen </span>
|
||||
Gäste scannen, laden Fotos hoch und ihr entscheidet, was in euer Album kommt.
|
||||
</li>
|
||||
</ol>
|
||||
<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>
|
||||
<aside className="space-y-4 rounded-2xl border border-rose-100 bg-white/90 p-6 text-sm leading-relaxed text-slate-600 shadow-md">
|
||||
<p>
|
||||
Ihr könnt jederzeit unterbrechen und später weiter machen. Falls ihr Fragen habt, meldet euch unter{' '}
|
||||
<a className="font-medium text-rose-500 underline" href="mailto:hallo@fotospiel.de">
|
||||
hallo@fotospiel.de
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Nach dem Login geleiten wir euch automatisch zur geführten Einrichtung. Dort entscheidet ihr auch,
|
||||
wann eure Gästegalerie sichtbar wird.
|
||||
</p>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<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>
|
||||
|
||||
<footer className="border-t border-white/60 bg-white/70 py-6 text-center text-xs text-slate-500">
|
||||
Fotospiel • Eure Gäste gestalten eure Lieblingsmomente
|
||||
</footer>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import WelcomeTeaserPage from '../WelcomeTeaserPage';
|
||||
|
||||
const navigateMock = vi.fn();
|
||||
|
||||
vi.mock('../../components/LanguageSwitcher', () => ({
|
||||
LanguageSwitcher: () => <div data-testid="language-switcher" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/navigation', () => ({
|
||||
navigateToHref: (href: string) => navigateMock(href),
|
||||
}));
|
||||
|
||||
describe('WelcomeTeaserPage', () => {
|
||||
afterEach(() => {
|
||||
document.body.classList.remove('tenant-admin-theme', 'tenant-admin-welcome-theme');
|
||||
vi.clearAllMocks();
|
||||
navigateMock.mockReset();
|
||||
});
|
||||
|
||||
it('applies the tenant admin theme classes while mounted', () => {
|
||||
const { unmount } = render(<WelcomeTeaserPage />);
|
||||
|
||||
expect(document.body.classList.contains('tenant-admin-theme')).toBe(true);
|
||||
expect(document.body.classList.contains('tenant-admin-welcome-theme')).toBe(true);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.body.classList.contains('tenant-admin-theme')).toBe(false);
|
||||
expect(document.body.classList.contains('tenant-admin-welcome-theme')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the hero stats and triggers the login redirect CTA', async () => {
|
||||
render(<WelcomeTeaserPage />);
|
||||
|
||||
expect(screen.getByText('Events begleitet')).toBeInTheDocument();
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByRole('button', { name: /event admin öffnen/i }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith(expect.stringContaining('/event-admin/login'));
|
||||
});
|
||||
|
||||
it('allows switching between light and dark presentation modes', async () => {
|
||||
render(<WelcomeTeaserPage />);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const toggle = screen.getByRole('button', { name: /light mode/i });
|
||||
|
||||
await user.click(toggle);
|
||||
|
||||
expect(toggle).toHaveTextContent(/dark mode/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user