Files
fotospiel-app/resources/js/admin/pages/WelcomeTeaserPage.tsx
2025-11-25 13:03:42 +01:00

494 lines
25 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 { Navigate } from 'react-router-dom';
import {
ArrowRight,
CheckCircle2,
Layers,
ListChecks,
Menu,
Moon,
Palette,
QrCode,
ShieldCheck,
Smartphone,
Sparkles,
Sun,
Wand2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { useAppearance } from '@/hooks/use-appearance';
import { ADMIN_DEFAULT_AFTER_LOGIN_PATH, ADMIN_LOGIN_PATH } from '../constants';
import { useAuth } from '../auth/context';
import { LanguageSwitcher } from '../components/LanguageSwitcher';
import { navigateToHref } from '../lib/navigation';
import { getCurrentLocale } from '../lib/locale';
type Feature = {
key: string;
title: string;
description: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
};
type Step = {
key: string;
title: string;
description: string;
accent: string;
};
type Plan = {
key: string;
title: string;
badge?: string;
highlight?: string;
points: string[];
};
export default function WelcomeTeaserPage() {
const { t } = useTranslation('common');
const { status } = useAuth();
const { appearance, updateAppearance } = useAppearance();
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
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');
};
}, []);
if (status === 'authenticated') {
return <Navigate to={ADMIN_DEFAULT_AFTER_LOGIN_PATH} replace />;
}
const locale = getCurrentLocale();
const packagesHref = `/${locale}/packages`;
const howItWorksHref = locale === 'de' ? `/${locale}/so-funktionierts` : `/${locale}/how-it-works`;
const features: Feature[] = [
{
key: 'branding',
title: t('welcome.features.branding.title', 'Branding & Layout'),
description: t('welcome.features.branding.description', 'Farben, Schriften, QR-Layouts und Einladungen in einem Fluss.'),
icon: Palette,
},
{
key: 'tasks',
title: t('welcome.features.tasks.title', 'Aufgaben & Emotion-Sets'),
description: t('welcome.features.tasks.description', 'Sammlungen importieren oder eigene Aufgaben erstellen mobil abhakbar.'),
icon: ListChecks,
},
{
key: 'moderation',
title: t('welcome.features.moderation.title', 'Foto-Moderation'),
description: t('welcome.features.moderation.description', 'Uploads sofort prüfen, Highlights markieren und Galerie-Link teilen.'),
icon: ShieldCheck,
},
{
key: 'invites',
title: t('welcome.features.invites.title', 'Einladungen & QR'),
description: t('welcome.features.invites.description', 'Links und Druckvorlagen generieren mit Paketlimits im Blick.'),
icon: QrCode,
},
];
const steps: Step[] = [
{
key: 'prepare',
title: t('welcome.steps.prepare.title', 'Vorbereiten'),
description: t('welcome.steps.prepare.description', 'Event anlegen, Branding setzen, Aufgaben aktivieren.'),
accent: t('welcome.steps.prepare.accent', 'Setup'),
},
{
key: 'share',
title: t('welcome.steps.share.title', 'Teilen & Einladen'),
description: t('welcome.steps.share.description', 'QRs/Links verteilen, Missionen auswählen, Team onboarden.'),
accent: t('welcome.steps.share.accent', 'Share'),
},
{
key: 'run',
title: t('welcome.steps.run.title', 'Live moderieren'),
description: t('welcome.steps.run.description', 'Uploads prüfen, Highlights pushen und nach dem Event die Galerie teilen.'),
accent: t('welcome.steps.run.accent', 'Live'),
},
];
const plans: Plan[] = [
{
key: 'starter',
title: t('welcome.plans.starter.title', 'Starter'),
badge: t('welcome.plans.starter.badge', 'Für ein Event'),
points: [
t('welcome.plans.starter.p1', '1 Event, Basis-Branding'),
t('welcome.plans.starter.p2', 'Aufgaben & Einladungen inklusive'),
t('welcome.plans.starter.p3', 'Moderation & Galerie-Link'),
],
},
{
key: 'standard',
title: t('welcome.plans.standard.title', 'Standard'),
badge: t('welcome.plans.standard.badge', 'Beliebt'),
highlight: t('welcome.plans.standard.highlight', 'Mehr Kontingent & Branding'),
points: [
t('welcome.plans.standard.p1', 'Mehr Events pro Jahr'),
t('welcome.plans.standard.p2', 'Erweitertes Branding & Layouts'),
t('welcome.plans.standard.p3', 'Support bei Live-Events'),
],
},
{
key: 'reseller',
title: t('welcome.plans.reseller.title', 'Reseller S'),
badge: t('welcome.plans.reseller.badge', 'Für Dienstleister'),
highlight: t('welcome.plans.reseller.highlight', 'Mehrere Events parallel verwalten'),
points: [
t('welcome.plans.reseller.p1', 'Bis zu 5 Events pro Paket'),
t('welcome.plans.reseller.p2', 'Aufgaben-Sammlungen und Vorlagen'),
t('welcome.plans.reseller.p3', 'Teamrollen & Rechteverwaltung'),
],
},
];
const audienceCards = [
{
key: 'endcustomers',
title: t('welcome.audience.endcustomers.title', 'Endkund:innen'),
description: t('welcome.audience.endcustomers.description', 'Schnell einrichten, mobil moderieren und nach dem Event die Galerie teilen.'),
icon: Smartphone,
},
{
key: 'resellers',
title: t('welcome.audience.resellers.title', 'Reseller & Agenturen'),
description: t('welcome.audience.resellers.description', 'Mehrere Events im Blick behalten, Kontingente überwachen und Vorlagen nutzen.'),
icon: Layers,
},
];
const previewRaw = t('welcome.preview.items', {
defaultValue: [
'Moderation, Aufgaben und Einladungen als Schnellzugriff',
'Sticky Actions auf Mobile für den Eventtag',
'Paket-Status & Limits jederzeit sichtbar',
],
returnObjects: true,
});
const previewBullets = Array.isArray(previewRaw) ? previewRaw : [String(previewRaw)];
const themeLabel = appearance === 'dark' ? t('welcome.theme.dark', 'Dunkel') : t('welcome.theme.light', 'Hell');
const handleLogin = React.useCallback(() => {
navigateToHref(ADMIN_LOGIN_PATH);
}, []);
const handlePackages = React.useCallback(() => navigateToHref(packagesHref), [packagesHref]);
const handleHow = React.useCallback(() => navigateToHref(howItWorksHref), [howItWorksHref]);
const handleThemeToggle = React.useCallback(() => {
updateAppearance(appearance === 'dark' ? 'light' : 'dark');
}, [appearance, updateAppearance]);
const renderMenuActions = () => (
<div className="flex flex-col gap-3">
<Button size="sm" onClick={handleLogin} className="justify-between">
<span>{t('welcome.cta.login', 'Login')}</span>
<ArrowRight className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={handlePackages} className="justify-between">
<span>{t('welcome.cta.packages', 'Pakete ansehen')}</span>
<ArrowRight className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" onClick={handleHow} className="justify-between">
<span>{t('welcome.cta.how', "So funktioniert's")}</span>
<ArrowRight className="h-4 w-4" />
</Button>
<div className="flex items-center justify-between rounded-xl border border-slate-200/70 px-4 py-3 text-sm dark:border-white/10">
<div>
<p className="text-xs text-slate-500 dark:text-slate-400">{t('welcome.theme.label', 'Darstellung')}</p>
<p className="text-sm font-semibold text-slate-900 dark:text-white">{themeLabel}</p>
</div>
<Button variant="outline" size="icon" aria-label={t('welcome.theme.aria', 'Darstellung umschalten')} onClick={handleThemeToggle}>
{appearance === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
<div className="flex items-center justify-between rounded-xl border border-slate-200/70 px-4 py-3 text-sm dark:border-white/10">
<span className="text-slate-700 dark:text-slate-200">{t('app.languageSwitch')}</span>
<LanguageSwitcher />
</div>
</div>
);
return (
<div className="relative min-h-svh overflow-hidden bg-gradient-to-b from-rose-50 via-white to-slate-50 text-slate-900 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 dark:text-white">
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_10%_20%,rgba(255,137,170,0.35),transparent_45%),radial-gradient(circle_at_90%_0%,rgba(96,165,250,0.25),transparent_45%),radial-gradient(circle_at_50%_90%,rgba(16,185,129,0.15),transparent_45%)] opacity-70 dark:opacity-30"
/>
<div aria-hidden className="absolute inset-0 bg-gradient-to-b from-white/70 via-white/40 to-transparent dark:from-slate-950/90 dark:via-slate-950/70 dark:to-slate-950/80" />
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 pb-16 pt-8 sm:px-6 lg:px-10 lg:pt-12">
<header className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-rose-500 text-lg font-semibold text-white shadow-lg shadow-rose-200/60 dark:bg-rose-400 dark:text-slate-900">
FS
</div>
<div className="space-y-1">
<p className="text-[11px] font-semibold uppercase tracking-[0.35em] text-rose-600 dark:text-rose-200">
{t('welcome.eyebrow', 'Event Admin')}
</p>
<p className="text-sm font-semibold text-slate-900 dark:text-white">Fotospiel.app</p>
</div>
</div>
<div className="hidden items-center gap-2 sm:flex">
<Button
variant="ghost"
size="sm"
onClick={handleThemeToggle}
aria-label={t('welcome.theme.aria', 'Darstellung umschalten')}
className="rounded-full border border-slate-200/80 bg-white/80 text-slate-700 hover:border-rose-200 hover:text-rose-700 dark:border-white/10 dark:bg-white/10 dark:text-white"
>
{appearance === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
<span className="hidden sm:inline">{themeLabel}</span>
</Button>
<LanguageSwitcher />
<Button variant="outline" size="sm" onClick={handlePackages} className="border-rose-200 text-rose-700 hover:bg-rose-50">
{t('welcome.cta.packages', 'Pakete ansehen')}
</Button>
<Button size="sm" onClick={handleLogin} className="bg-rose-500 text-white hover:bg-rose-600">
{t('welcome.cta.login', 'Login')}
</Button>
</div>
<div className="sm:hidden">
<Sheet open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label={isMenuOpen ? t('welcome.menu.close', 'Menü schließen') : t('welcome.menu.open', 'Menü öffnen')}
className="rounded-full border-rose-200 bg-white/80 text-slate-700 shadow-sm dark:border-white/10 dark:bg-white/10 dark:text-white"
>
{isMenuOpen ? <Sparkles className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</Button>
</SheetTrigger>
<SheetContent side="right" className="bg-white/95 text-slate-900 dark:bg-slate-950 dark:text-white">
<SheetHeader>
<SheetTitle className="text-lg font-semibold">{t('welcome.menu.title', 'Navigation')}</SheetTitle>
</SheetHeader>
{renderMenuActions()}
</SheetContent>
</Sheet>
</div>
</header>
<main className="flex flex-col gap-12">
<section className="grid gap-8 lg:grid-cols-[1.1fr,0.9fr]">
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-600 dark:text-slate-300">
<Badge variant="secondary" className="bg-rose-100/80 text-rose-700 hover:bg-rose-100 dark:bg-rose-400/20 dark:text-rose-100">
<Sparkles className="h-3.5 w-3.5" />
{t('welcome.badge', 'Fotos, Aufgaben & Einladungen an einem Ort')}
</Badge>
<div className="flex items-center gap-2 rounded-full bg-white/70 px-3 py-1 text-xs font-medium text-slate-700 shadow-sm shadow-rose-200/50 ring-1 ring-slate-200/70 dark:bg-white/5 dark:text-white dark:ring-white/10">
<ShieldCheck className="h-3.5 w-3.5 text-emerald-500" />
{t('welcome.loginPrompt', 'Bereits Kunde? Login oben rechts.')}
</div>
</div>
<div className="space-y-4">
<h1 className="font-display text-3xl font-semibold leading-tight tracking-tight text-slate-900 dark:text-white sm:text-4xl">
{t('welcome.title', 'Event-Branding, Aufgaben & Foto-Moderation in einer App.')}
</h1>
<p className="text-lg text-slate-700 dark:text-slate-200">
{t('welcome.subtitle', 'Bereite dein Event vor, teile Einladungen, moderiere Uploads live und gib die Galerie danach frei.')}
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button size="lg" onClick={handleLogin} className="bg-rose-500 text-white hover:bg-rose-600">
{t('welcome.cta.open', 'Event Admin öffnen')}
<ArrowRight className="h-4 w-4" />
</Button>
<Button variant="outline" size="lg" onClick={handleHow} className="border-rose-200 text-rose-700 hover:bg-rose-50">
{t('welcome.cta.how', "So funktioniert's")}
</Button>
<Button variant="ghost" size="lg" onClick={handlePackages} className="text-slate-700 hover:text-rose-700 dark:text-white">
{t('welcome.cta.packages', 'Pakete ansehen')}
</Button>
</div>
</div>
<div className="relative">
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-rose-100/80 via-white/70 to-sky-100/60 blur-3xl dark:from-rose-400/10 dark:via-slate-900 dark:to-sky-400/10" aria-hidden />
<div className="relative flex h-full flex-col gap-4 rounded-3xl border border-slate-200/60 bg-white/80 p-6 shadow-lg shadow-rose-100/40 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:shadow-none">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-white">
<Wand2 className="h-4 w-4 text-rose-500" />
{t('welcome.preview.title', 'Was dich erwartet')}
</div>
<div className="space-y-3 text-sm text-slate-700 dark:text-slate-200">
{previewBullets.map((item) => (
<div key={item} className="flex items-start gap-2 rounded-2xl border border-slate-200/70 bg-white/70 px-3 py-2 text-left dark:border-white/10 dark:bg-white/5">
<CheckCircle2 className="mt-0.5 h-4 w-4 text-emerald-500" />
<p>{item}</p>
</div>
))}
</div>
<div className="mt-auto grid grid-cols-2 gap-3 text-xs text-slate-600 dark:text-slate-300">
<div className="rounded-2xl border border-dashed border-rose-200/70 bg-rose-50/70 p-4 dark:border-rose-200/30 dark:bg-rose-200/10">
<p className="text-[11px] uppercase tracking-[0.3em] text-rose-500">{t('welcome.highlight.moderation', 'Live-Moderation')}</p>
<p className="mt-2 font-semibold text-slate-900 dark:text-white">{t('welcome.highlight.moderationHint', 'Approve/Hide, Highlights, Galerie-Link')}</p>
</div>
<div className="rounded-2xl border border-dashed border-sky-200/80 bg-sky-50/80 p-4 dark:border-sky-200/40 dark:bg-sky-200/10">
<p className="text-[11px] uppercase tracking-[0.3em] text-sky-600">{t('welcome.highlight.tasks', 'Aufgaben & Emotion-Sets')}</p>
<p className="mt-2 font-semibold text-slate-900 dark:text-white">{t('welcome.highlight.tasksHint', 'Sammlungen importieren oder eigene erstellen')}</p>
</div>
</div>
</div>
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-2">
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">{t('welcome.features.title', 'Was du steuern kannst')}</p>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white sm:text-3xl">{t('welcome.features.subtitle', 'Alles an einem Ort')}</h2>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{features.map((feature) => (
<div
key={feature.key}
className="group flex h-full flex-col gap-3 rounded-2xl border border-slate-200 bg-white/90 p-4 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-rose-200 hover:shadow-md dark:border-white/10 dark:bg-white/5"
>
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
<feature.icon className="h-4 w-4 text-rose-500" />
{feature.title}
</div>
<p className="text-sm text-slate-600 dark:text-slate-300">{feature.description}</p>
</div>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-2">
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">{t('welcome.steps.title', "So funktioniert's")}</p>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white sm:text-3xl">{t('welcome.steps.subtitle', 'In drei Schritten bereit')}</h2>
</div>
<div className="grid gap-4 md:grid-cols-3">
{steps.map((step) => (
<div
key={step.key}
className="flex h-full flex-col gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm dark:border-white/10 dark:bg-white/5"
>
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">
{step.accent}
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">{step.title}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">{step.description}</p>
</div>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-2">
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">{t('welcome.plans.title', 'Pakete im Überblick')}</p>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white sm:text-3xl">{t('welcome.plans.subtitle', 'Wähle das passende Kontingent')}</h2>
<p className="text-sm text-slate-600 dark:text-slate-300">{t('welcome.plans.hint', 'Starter, Standard oder Reseller alles mit Moderation & Einladungen.')}</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{plans.map((plan) => (
<div
key={plan.key}
className={cn(
'flex h-full flex-col gap-3 rounded-2xl border border-slate-200 bg-white/90 p-5 text-left shadow-sm dark:border-white/10 dark:bg-white/5',
plan.highlight ? 'ring-1 ring-rose-200 dark:ring-rose-300/30' : ''
)}
>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-rose-100/80 text-rose-700 dark:bg-rose-300/20 dark:text-rose-100">
{plan.badge ?? t('welcome.plans.badge', 'Paket')}
</Badge>
{plan.highlight ? (
<Badge className="bg-emerald-500/15 text-emerald-700 dark:bg-emerald-400/20 dark:text-emerald-200">
{plan.highlight}
</Badge>
) : null}
</div>
<div className="flex items-center gap-2 text-lg font-semibold text-slate-900 dark:text-white">
<Sparkles className="h-4 w-4 text-rose-500" />
{plan.title}
</div>
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
{plan.points.map((point) => (
<li key={point} className="flex items-start gap-2">
<CheckCircle2 className="mt-0.5 h-4 w-4 text-emerald-500" />
<span>{point}</span>
</li>
))}
</ul>
<div className="mt-auto">
<Button variant="outline" size="sm" className="border-rose-200 text-rose-700 hover:bg-rose-50" onClick={handlePackages}>
{t('welcome.cta.packages', 'Pakete ansehen')} <ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-2">
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">{t('welcome.audience.title', 'Für wen?')}</p>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white sm:text-3xl">{t('welcome.audience.subtitle', 'Endkunden & Reseller im Blick')}</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{audienceCards.map((audience) => (
<div
key={audience.key}
className="flex h-full flex-col gap-3 rounded-2xl border border-slate-200 bg-white/90 p-4 shadow-sm dark:border-white/10 dark:bg-white/5"
>
<div className="flex items-center gap-2 text-lg font-semibold text-slate-900 dark:text-white">
<audience.icon className="h-5 w-5 text-rose-500" />
{audience.title}
</div>
<p className="text-sm text-slate-600 dark:text-slate-300">{audience.description}</p>
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400">
<ArrowRight className="h-3.5 w-3.5" />
{t('welcome.audience.cta', 'Wenige Klicks bis zum Start')}
</div>
</div>
))}
</div>
</section>
<section className="rounded-3xl border border-slate-200 bg-white/85 p-6 shadow-lg shadow-rose-100/40 backdrop-blur dark:border-white/10 dark:bg-white/5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<p className="text-xs uppercase tracking-[0.35em] text-rose-500 dark:text-rose-200">{t('welcome.footer.eyebrow', 'Bereit?')}</p>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">{t('welcome.footer.title', 'Melde dich an oder prüfe die Pakete')}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">{t('welcome.footer.subtitle', 'Login für bestehende Kunden, Pakete für neue Teams.')}</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button size="lg" onClick={handleLogin} className="bg-rose-500 text-white hover:bg-rose-600">
{t('welcome.cta.login', 'Login')} <ArrowRight className="h-4 w-4" />
</Button>
<Button variant="outline" size="lg" onClick={handlePackages} className="border-rose-200 text-rose-700 hover:bg-rose-50">
{t('welcome.cta.packages', 'Pakete ansehen')}
</Button>
</div>
</div>
</section>
</main>
</div>
</div>
);
}