photobooth funktionen im event admin verlinkt, gäste pwa zeigt photobooth nur noch an, wenn diese aktiviert ist. kontaktformular optimiert. teilen-link mit iMessage und whatsapp erweitert.

This commit is contained in:
Codex Agent
2025-11-23 22:22:06 +01:00
parent 3d9eaa1194
commit df414a31cd
32 changed files with 809 additions and 280 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Head, Link, usePage } from '@inertiajs/react';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
import { useTranslation } from 'react-i18next';

View File

@@ -133,7 +133,7 @@ const BlogShow: React.FC<Props> = ({ post }) => {
{t('breadcrumb_blog')}
</Link>
<span>/</span>
<span className="text-gray-700 dark:text-gray-200">{post.title}</span>
<span className="text-gray-700 dark:text-gray-100">{post.title}</span>
</nav>
<Link href={localizedPath('/blog')} className="text-pink-600 hover:text-pink-700">
{t('back_to_blog')}
@@ -156,7 +156,7 @@ const BlogShow: React.FC<Props> = ({ post }) => {
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-200"
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</div>
<div className="relative">
@@ -193,7 +193,11 @@ const BlogShow: React.FC<Props> = ({ post }) => {
prose-pre:bg-slate-900 prose-pre:text-slate-100
prose-blockquote:border-l-4 prose-blockquote:border-pink-500 prose-blockquote:pl-6 prose-blockquote:italic
prose-ul:text-slate-700 prose-ol:text-slate-700
prose-li:text-slate-700 dark:prose-invert"
prose-li:text-slate-700
dark:prose-invert dark:prose-headings:text-gray-50 dark:prose-p:text-gray-100
dark:prose-strong:text-gray-50 dark:prose-a:text-pink-300 dark:hover:prose-a:text-pink-200
dark:prose-code:bg-gray-800 dark:prose-code:text-gray-100 dark:prose-pre:bg-gray-900 dark:prose-pre:text-gray-100
dark:prose-li:text-gray-100 dark:prose-blockquote:border-pink-400"
dangerouslySetInnerHTML={{ __html: post.content_html }}
/>
</article>
@@ -207,7 +211,7 @@ const BlogShow: React.FC<Props> = ({ post }) => {
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-200"
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</CardContent>
</Card>
@@ -218,7 +222,7 @@ const BlogShow: React.FC<Props> = ({ post }) => {
{t('toc_title')}
</p>
{post.headings && post.headings.length > 0 ? (
<ul className="space-y-3 text-sm text-gray-700 dark:text-gray-200">
<ul className="space-y-3 text-sm text-gray-700 dark:text-gray-100">
{post.headings.map((heading) => (
<li key={heading.slug} className="border-l-2 border-pink-100 pl-3 dark:border-pink-500/40">
<a href={`#${heading.slug}`} className="hover:text-pink-600">
@@ -269,7 +273,7 @@ const BlogShow: React.FC<Props> = ({ post }) => {
<Separator className="my-2" />
<div className="flex flex-col gap-2">
{shareLinks.map((link) => (
<Button key={link.key} variant="outline" size="sm" asChild className="justify-start border-gray-200 text-gray-700 hover:border-pink-200 hover:text-pink-600 dark:border-gray-700 dark:text-gray-50">
<Button key={link.key} variant="outline" size="sm" asChild className="justify-start border-gray-200 text-gray-700 hover:border-pink-200 hover:text-pink-600 dark:border-gray-700 dark:text-gray-100">
<a href={link.href} target="_blank" rel="noreferrer">
{link.label}
</a>

View File

@@ -66,7 +66,7 @@ const CheckoutWizardPage: React.FC<CheckoutWizardPageProps> = ({
return (
<MarketingLayout title="Checkout Wizard">
<Head title="Checkout Wizard" />
<div className="min-h-screen bg-muted/20 py-12">
<div className="min-h-screen bg-muted/20 py-12 dark:bg-gray-950 dark:text-gray-50">
<div className="mx-auto w-full max-w-4xl px-4">
{verificationFlash && (
<Alert

View File

@@ -10,7 +10,8 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { ArrowRight, Camera, QrCode, ShieldCheck, Sparkles, Smartphone } from 'lucide-react';
import { ArrowRight, Camera, QrCode, ShieldCheck, Sparkles, Smartphone, Loader2, CheckCircle2 } from 'lucide-react';
import { usePage } from '@inertiajs/react';
interface Package {
id: number;
@@ -34,10 +35,12 @@ const Home: React.FC<Props> = ({ packages }) => {
variant: heroCtaVariant,
trackClick: trackHeroCtaClick,
} = useCtaExperiment('home_hero_cta');
const { flash } = usePage<{ flash?: { success?: string } }>().props;
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
email: '',
message: '',
nickname: '',
});
const heroBulletsRaw = t('home.hero_bullets', { returnObjects: true });
@@ -447,14 +450,54 @@ const Home: React.FC<Props> = ({ packages }) => {
<section id="contact" className="bg-gray-50 py-20 px-4 dark:bg-gray-950/80">
<div className="container mx-auto max-w-4xl">
<Card className="border-gray-200/70 bg-white/95 shadow-lg shadow-rose-200/40 dark:border-gray-800/60 dark:bg-gray-900/80">
<CardHeader className="text-center">
<Badge className="mx-auto mb-3 w-fit bg-rose-100 px-3 py-1 text-xs font-semibold uppercase text-rose-600 dark:bg-rose-500/20 dark:text-rose-200">
{t('home.contact_title')}
</Badge>
<CardTitle className="text-2xl md:text-3xl">{t('home.contact_lead')}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<CardHeader className="text-center">
<Badge className="mx-auto mb-3 w-fit bg-rose-100 px-3 py-1 text-xs font-semibold uppercase text-rose-600 dark:bg-rose-500/20 dark:text-rose-200">
{t('home.contact_title')}
</Badge>
<CardTitle className="text-2xl md:text-3xl">{t('home.contact_lead')}</CardTitle>
</CardHeader>
<CardContent>
{flash?.success ? (
<div className="mb-4 flex items-center gap-2 rounded-xl border border-emerald-200/70 bg-emerald-50 px-3 py-2 text-sm text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100">
<CheckCircle2 className="h-4 w-4" />
<span>{flash.success}</span>
</div>
) : null}
<div className="grid gap-6 lg:grid-cols-[1fr,1.1fr]">
<div className="space-y-3 rounded-2xl border border-rose-100/70 bg-rose-50/50 p-5 text-left text-sm text-slate-700 shadow-inner dark:border-rose-500/20 dark:bg-rose-500/5 dark:text-slate-200">
<p className="text-xs font-semibold uppercase tracking-wide text-rose-500 dark:text-rose-200">
{t('home.contact_title')}
</p>
<p className="text-base font-semibold text-slate-900 dark:text-white">{t('home.contact_lead')}</p>
<ul className="space-y-2">
<li className="flex items-start gap-3">
<div className="mt-0.5 h-2.5 w-2.5 rounded-full bg-rose-500" />
<span>{t('home.hero_bullets.0', 'Antwort in <24h')}</span>
</li>
<li className="flex items-start gap-3">
<div className="mt-0.5 h-2.5 w-2.5 rounded-full bg-rose-500" />
<span>{t('home.hero_bullets.1', 'Keine Weitergabe, kein Tracking')}</span>
</li>
<li className="flex items-start gap-3">
<div className="mt-0.5 h-2.5 w-2.5 rounded-full bg-rose-500" />
<span>{t('home.contact_privacy')}</span>
</li>
</ul>
<div className="rounded-xl border border-white/60 bg-white/80 p-3 text-xs font-medium text-slate-700 shadow-sm dark:border-white/10 dark:bg-white/10 dark:text-slate-100">
{t('home.contact_privacy')}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<input
type="text"
name="nickname"
value={data.nickname}
onChange={(event) => setData('nickname', event.target.value)}
className="hidden"
tabIndex={-1}
autoComplete="off"
aria-hidden
/>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="name" className="text-sm font-semibold text-gray-600 dark:text-gray-200">
@@ -467,9 +510,11 @@ const Home: React.FC<Props> = ({ packages }) => {
className="h-12 rounded-xl border-gray-200/70 bg-white/90 shadow-inner shadow-gray-200/40 focus-visible:ring-rose-300/60 dark:border-gray-700 dark:bg-gray-900/70"
autoComplete="name"
required
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? 'contact-name-error' : undefined}
/>
{errors.name && (
<p className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.name}</p>
<p id="contact-name-error" className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.name}</p>
)}
</div>
<div className="flex flex-col gap-2">
@@ -484,9 +529,11 @@ const Home: React.FC<Props> = ({ packages }) => {
className="h-12 rounded-xl border-gray-200/70 bg-white/90 shadow-inner shadow-gray-200/40 focus-visible:ring-rose-300/60 dark:border-gray-700 dark:bg-gray-900/70"
autoComplete="email"
required
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? 'contact-email-error' : undefined}
/>
{errors.email && (
<p className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.email}</p>
<p id="contact-email-error" className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.email}</p>
)}
</div>
</div>
@@ -494,31 +541,41 @@ const Home: React.FC<Props> = ({ packages }) => {
<label htmlFor="message" className="text-sm font-semibold text-gray-600 dark:text-gray-200">
{t('home.message_label')} *
</label>
<Textarea
id="message"
rows={5}
value={data.message}
onChange={(event) => setData('message', event.target.value)}
className="rounded-xl border-gray-200/70 bg-white/90 shadow-inner shadow-gray-200/40 focus-visible:ring-rose-300/60 dark:border-gray-700 dark:bg-gray-900/70"
required
/>
{errors.message && (
<p className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.message}</p>
)}
<Textarea
id="message"
rows={5}
value={data.message}
onChange={(event) => setData('message', event.target.value)}
className="rounded-xl border-gray-200/70 bg-white/90 shadow-inner shadow-gray-200/40 focus-visible:ring-rose-300/60 dark:border-gray-700 dark:bg-gray-900/70"
required
aria-invalid={Boolean(errors.message)}
aria-describedby={errors.message ? 'contact-message-error' : undefined}
/>
{errors.message && (
<p id="contact-message-error" className="text-sm font-medium text-rose-600 dark:text-rose-300">{errors.message}</p>
)}
</div>
<div className="space-y-3 text-sm text-muted-foreground">
<p>{t('home.contact_privacy')}</p>
<Button
type="submit"
disabled={processing}
className="h-12 w-full rounded-full bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-base font-semibold text-white shadow-[0_18px_35px_-18px_rgba(236,72,153,0.7)] transition hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5] disabled:cursor-not-allowed disabled:opacity-60"
>
{processing ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{t('home.sending')}
</span>
) : (
t('home.send')
)}
</Button>
</div>
</form>
</div>
<div className="space-y-3 text-sm text-muted-foreground">
<p>{t('home.contact_privacy')}</p>
<Button
type="submit"
disabled={processing}
className="h-12 w-full rounded-full bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-base font-semibold text-white shadow-[0_18px_35px_-18px_rgba(236,72,153,0.7)] transition hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5] disabled:cursor-not-allowed disabled:opacity-60"
>
{processing ? t('home.sending') : t('home.send')}
</Button>
</div>
</form>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
</section>

View File

@@ -3,12 +3,14 @@ import { Head, Link, useForm, usePage } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
import MarketingLayout from '@/layouts/mainWebsite';
import { Loader2, CheckCircle2 } from 'lucide-react';
const Kontakt: React.FC = () => {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
email: '',
message: '',
nickname: '',
});
const { flash } = usePage<{ flash?: { success?: string } }>().props;
@@ -35,7 +37,23 @@ const Kontakt: React.FC = () => {
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold text-center mb-8 font-display text-gray-900 dark:text-gray-100">{t('kontakt.title')}</h1>
<p className="text-center text-gray-600 dark:text-gray-300 mb-8 font-sans-marketing">{t('kontakt.description')}</p>
{flash?.success && (
<div className="mb-4 flex items-center gap-2 rounded-xl border border-emerald-200/70 bg-emerald-50 px-3 py-2 text-sm text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100">
<CheckCircle2 className="h-4 w-4" />
<span>{flash.success}</span>
</div>
)}
<form key={`kontakt-form-${Object.keys(errors).length}`} onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
name="nickname"
value={data.nickname}
onChange={(e) => setData('nickname', e.target.value)}
className="hidden"
tabIndex={-1}
autoComplete="off"
aria-hidden
/>
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 font-sans-marketing">{t('kontakt.name')}</label>
<input
@@ -45,8 +63,10 @@ const Kontakt: React.FC = () => {
onChange={(e) => setData('name', e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? 'kontakt-name-error' : undefined}
/>
{errors.name && <p key={`error-name`} className="text-red-500 text-sm mt-1 font-serif-custom">{errors.name}</p>}
{errors.name && <p id="kontakt-name-error" key="error-name" className="text-red-500 text-sm mt-1 font-serif-custom">{errors.name}</p>}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 font-sans-marketing">{t('kontakt.email')}</label>
@@ -57,8 +77,10 @@ const Kontakt: React.FC = () => {
onChange={(e) => setData('email', e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? 'kontakt-email-error' : undefined}
/>
{errors.email && <p key={`error-email`} className="text-red-500 text-sm mt-1 font-serif-custom">{errors.email}</p>}
{errors.email && <p id="kontakt-email-error" key="error-email" className="text-red-500 text-sm mt-1 font-serif-custom">{errors.email}</p>}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 font-sans-marketing">{t('kontakt.message')}</label>
@@ -69,11 +91,20 @@ const Kontakt: React.FC = () => {
rows={4}
required
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
aria-invalid={Boolean(errors.message)}
aria-describedby={errors.message ? 'kontakt-message-error' : undefined}
></textarea>
{errors.message && <p key={`error-message`} className="text-red-500 text-sm mt-1 font-serif-custom">{errors.message}</p>}
{errors.message && <p id="kontakt-message-error" key="error-message" className="text-red-500 text-sm mt-1 font-serif-custom">{errors.message}</p>}
</div>
<button type="submit" disabled={processing} className="w-full bg-[#FFB6C1] text-white py-3 rounded-md font-semibold hover:bg-[#FF69B4] transition disabled:opacity-50 font-sans-marketing">
{processing ? t('kontakt.sending') : t('kontakt.send')}
{processing ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{t('kontakt.sending')}
</span>
) : (
t('kontakt.send')
)}
</button>
</form>
{flash?.success && <p className="mt-4 text-green-600 dark:text-green-400 text-center font-serif-custom">{flash.success}</p>}

View File

@@ -456,20 +456,20 @@ function selectHighlightPackageId(packages: Package[]): number | null {
const getAccentTheme = (variant: 'endcustomer' | 'reseller') =>
variant === 'reseller'
? {
badge: 'bg-amber-50 text-amber-700',
price: 'text-amber-600',
buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800',
buttonDefault: 'border border-amber-200 text-amber-700 hover:bg-amber-50',
cardBorder: 'border border-amber-100',
highlightShadow: 'shadow-lg shadow-amber-100/60 bg-gradient-to-br from-amber-50/70 via-white to-amber-100/60',
badge: 'bg-amber-50 text-amber-700 dark:bg-amber-500/20 dark:text-amber-100',
price: 'text-amber-600 dark:text-amber-100',
buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800 dark:bg-amber-600 dark:hover:bg-amber-500',
buttonDefault: 'border border-amber-200 text-amber-700 hover:bg-amber-50 dark:border-amber-500/50 dark:text-amber-100 dark:hover:bg-amber-500/10',
cardBorder: 'border border-amber-100 dark:border-amber-500/40',
highlightShadow: 'shadow-lg shadow-amber-100/60 bg-gradient-to-br from-amber-50/70 via-white to-amber-100/60 dark:shadow-amber-900/40 dark:from-amber-900/40 dark:via-gray-900 dark:to-amber-900/20',
}
: {
badge: 'bg-rose-50 text-rose-700',
price: 'text-rose-600',
buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800',
buttonDefault: 'border border-rose-100 text-rose-700 hover:bg-rose-50',
cardBorder: 'border border-rose-100',
highlightShadow: 'shadow-lg shadow-rose-100/60 bg-gradient-to-br from-rose-50/70 via-white to-rose-100/60',
badge: 'bg-rose-50 text-rose-700 dark:bg-pink-500/20 dark:text-pink-100',
price: 'text-rose-600 dark:text-pink-100',
buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800 dark:bg-pink-600 dark:hover:bg-pink-500',
buttonDefault: 'border border-rose-100 text-rose-700 hover:bg-rose-50 dark:border-pink-500/50 dark:text-pink-100 dark:hover:bg-pink-500/10',
cardBorder: 'border border-rose-100 dark:border-pink-500/40',
highlightShadow: 'shadow-lg shadow-rose-100/60 bg-gradient-to-br from-rose-50/70 via-white to-rose-100/60 dark:shadow-pink-900/40 dark:from-pink-900/40 dark:via-gray-900 dark:to-pink-900/10',
};
type PackageMetric = {
@@ -588,37 +588,37 @@ function PackageCard({
const metricList = compact ? (
<div className="flex flex-wrap gap-2">
{metrics.map((metric) => (
<div key={metric.key} className="rounded-full border border-gray-200 px-3 py-1 text-xs font-semibold text-gray-700">
<span className="text-[11px] font-medium uppercase text-gray-400">{metric.label}</span>
<span className="ml-1 text-gray-900">{metric.value}</span>
<div key={metric.key} className="rounded-full border border-gray-200 px-3 py-1 text-xs font-semibold text-gray-700 dark:border-gray-700 dark:text-gray-100">
<span className="text-[11px] font-medium uppercase text-gray-400 dark:text-gray-400">{metric.label}</span>
<span className="ml-1 text-gray-900 dark:text-gray-100">{metric.value}</span>
</div>
))}
</div>
) : (
<div className="grid grid-cols-2 gap-3 text-sm">
{metrics.map((metric) => (
<div key={metric.key} className="rounded-xl bg-gray-50 px-4 py-3">
<p className="text-lg font-semibold text-gray-900">{metric.value}</p>
<p className="text-xs uppercase tracking-wide text-gray-500">{metric.label}</p>
<div key={metric.key} className="rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800">
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{metric.value}</p>
<p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{metric.label}</p>
</div>
))}
</div>
);
const featureList = compact ? (
<ul className="space-y-1 text-sm text-gray-700">
<ul className="space-y-1 text-sm text-gray-700 dark:text-gray-200">
{visibleFeatures.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-xs">
<Check className="mt-0.5 h-3.5 w-3.5 text-gray-900" />
<Check className="mt-0.5 h-3.5 w-3.5 text-gray-900 dark:text-gray-100" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
</ul>
) : (
<ul className="space-y-2 text-sm text-gray-700">
<ul className="space-y-2 text-sm text-gray-700 dark:text-gray-200">
{visibleFeatures.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<Check className="h-4 w-4 text-gray-900" />
<Check className="h-4 w-4 text-gray-900 dark:text-gray-100" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
@@ -628,14 +628,14 @@ function PackageCard({
return (
<Card
className={cn(
'flex h-full flex-col rounded-2xl border border-gray-100 bg-white shadow-sm transition hover:shadow-lg',
'flex h-full flex-col rounded-2xl border border-gray-100 bg-white shadow-sm transition hover:shadow-lg dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50',
compact && 'p-3',
highlight && `${accent.cardBorder} ${accent.highlightShadow}`,
className,
)}
>
<CardHeader className={cn('gap-4', compact && 'gap-3 p-0')}>
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-300">
<span>{typeLabel}</span>
{badgeLabel && (
<span
@@ -649,8 +649,8 @@ function PackageCard({
)}
</div>
<div className="space-y-2">
<CardTitle className="text-2xl text-gray-900">{pkg.name}</CardTitle>
<CardDescription className="text-sm text-gray-600">{pkg.description}</CardDescription>
<CardTitle className="text-2xl text-gray-900 dark:text-gray-50">{pkg.name}</CardTitle>
<CardDescription className="text-sm text-gray-600 dark:text-gray-200">{pkg.description}</CardDescription>
</div>
</CardHeader>
<CardContent className={cn('flex flex-col gap-6', compact && 'gap-4 p-0 pt-2')}>
@@ -658,11 +658,11 @@ function PackageCard({
<div className={cn('flex items-baseline gap-2', compact && 'flex-wrap text-balance')}>
<span className={cn('text-4xl font-semibold', accent.price, compact && 'text-3xl')}>{priceLabel}</span>
{pkg.price !== 0 && (
<span className="text-sm text-gray-500">/ {cadenceLabel}</span>
<span className="text-sm text-gray-500 dark:text-gray-300">/ {cadenceLabel}</span>
)}
</div>
{variant === 'endcustomer' && (
<p className="text-xs text-gray-400">
<p className="text-xs text-gray-400 dark:text-gray-300">
{pkg.events} × {t('packages.one_time')}
</p>
)}
@@ -722,11 +722,11 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
return (
<div className="grid gap-8 lg:grid-cols-[320px,1fr]">
<div className="space-y-6">
<div className="rounded-2xl border border-gray-100 bg-gray-50 p-6">
<div className="rounded-2xl border border-gray-100 bg-gray-50 p-6 dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-gray-500">{t('packages.price')}</p>
<p className="text-4xl font-semibold text-gray-900">
<p className="text-sm font-medium text-gray-500 dark:text-gray-300">{t('packages.price')}</p>
<p className="text-4xl font-semibold text-gray-900 dark:text-gray-100">
{Number(packageData.price).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
@@ -734,22 +734,22 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
{t('packages.currency.euro')}
</p>
{packageData.price > 0 && (
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-300">
/ {variant === 'reseller' ? t('packages.billing_per_year') : t('packages.billing_per_event')}
</p>
)}
</div>
{isHighlight && (
<span className="rounded-full bg-gray-900 px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-white">
<span className="rounded-full bg-gray-900 px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-white dark:bg-pink-600">
{variant === 'reseller' ? t('packages.badge_best_value') : t('packages.badge_most_popular')}
</span>
)}
</div>
<div className="mt-6 grid grid-cols-2 gap-3 text-sm">
{metrics.map((metric) => (
<div key={metric.key} className="rounded-xl bg-white px-4 py-3 text-center shadow-sm">
<p className="text-lg font-semibold text-gray-900">{metric.value}</p>
<p className="text-xs uppercase tracking-wide text-gray-500">{metric.label}</p>
<div key={metric.key} className="rounded-xl bg-white px-4 py-3 text-center shadow-sm dark:bg-gray-800">
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{metric.value}</p>
<p className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{metric.label}</p>
</div>
))}
</div>
@@ -767,23 +767,23 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<p className="mt-3 text-xs text-gray-500">{t('packages.order_hint')}</p>
<p className="mt-3 text-xs text-gray-500 dark:text-gray-400">{t('packages.order_hint')}</p>
</div>
<div className="rounded-2xl border border-gray-100 bg-white p-6">
<h3 className="text-lg font-semibold text-gray-900">{t('packages.feature_highlights')}</h3>
<ul className="mt-4 space-y-3 text-sm text-gray-700">
<div className="rounded-2xl border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-gray-900">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{t('packages.feature_highlights')}</h3>
<ul className="mt-4 space-y-3 text-sm text-gray-700 dark:text-gray-200">
{highlightFeatures.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="mt-1 h-4 w-4 text-gray-900" />
<Check className="mt-1 h-4 w-4 text-gray-900 dark:text-gray-100" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
</ul>
</div>
<div className="rounded-2xl border border-gray-100 bg-white p-6">
<h3 className="text-lg font-semibold text-gray-900">{t('packages.more_details_tab')}</h3>
<div className="rounded-2xl border border-gray-100 bg-white p-6 dark:border-gray-800 dark:bg-gray-900">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{t('packages.more_details_tab')}</h3>
<Tabs defaultValue="breakdown">
<TabsList className="grid grid-cols-2 rounded-full bg-gray-100 p-1 text-sm">
<TabsList className="grid grid-cols-2 rounded-full bg-gray-100 p-1 text-sm dark:bg-gray-800">
<TabsTrigger value="breakdown" className="rounded-full">
{t('packages.breakdown_label')}
</TabsTrigger>
@@ -795,28 +795,28 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
{packageData.description_breakdown?.length ? (
<Accordion type="multiple" className="space-y-4">
{packageData.description_breakdown.map((entry, index) => (
<AccordionItem key={index} value={`detail-${index}`} className="rounded-2xl border border-gray-100 bg-white px-4">
<AccordionTrigger className="text-left text-base font-medium text-gray-900 hover:no-underline">
<AccordionItem key={index} value={`detail-${index}`} className="rounded-2xl border border-gray-100 bg-white px-4 dark:border-gray-800 dark:bg-gray-900">
<AccordionTrigger className="text-left text-base font-medium text-gray-900 hover:no-underline dark:text-gray-100">
{entry.title ?? t('packages.limits_label')}
</AccordionTrigger>
<AccordionContent className="pb-4 text-sm text-gray-600 whitespace-pre-line">
<AccordionContent className="pb-4 text-sm text-gray-600 whitespace-pre-line dark:text-gray-300">
{entry.value}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<p className="text-sm text-gray-500">{t('packages.breakdown_label_hint')}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('packages.breakdown_label_hint')}</p>
)}
</TabsContent>
<TabsContent value="testimonials" className="mt-6">
<div className="space-y-4">
{testimonials.map((testimonial, index) => (
<div key={index} className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
<div key={index} className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-gray-900">{testimonial.name}</p>
<p className="text-xs text-gray-500">{packageData.name}</p>
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{testimonial.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{packageData.name}</p>
</div>
<div className="flex gap-1 text-amber-400">
{[...Array(testimonial.rating)].map((_, i) => (
@@ -824,10 +824,10 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
))}
</div>
</div>
<p className="mt-3 text-sm text-gray-600">{testimonial.text}</p>
<p className="mt-3 text-sm text-gray-600 dark:text-gray-300">{testimonial.text}</p>
</div>
))}
<Button variant="outline" className="w-full rounded-full border-gray-200 text-sm font-medium text-gray-700" onClick={close}>
<Button variant="outline" className="w-full rounded-full border-gray-200 text-sm font-medium text-gray-700 dark:border-gray-700 dark:text-gray-100" onClick={close}>
{t('packages.close')}
</Button>
</div>
@@ -1022,7 +1022,7 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
side="bottom"
className="h-[90vh] overflow-hidden rounded-t-[32px] border border-gray-200 bg-white p-0"
className="h-[90vh] overflow-hidden rounded-t-[32px] border border-gray-200 bg-white p-0 dark:border-gray-800 dark:bg-gray-900"
onOpenAutoFocus={handleDetailAutoFocus}
>
{renderDetailBody('h-full overflow-y-auto space-y-8 p-6')}
@@ -1031,7 +1031,7 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
) : (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
className="max-w-6xl border border-gray-100 bg-white px-0 py-0 sm:rounded-[32px]"
className="max-w-6xl border border-gray-100 bg-white px-0 py-0 sm:rounded-[32px] dark:border-gray-800 dark:bg-gray-900"
onOpenAutoFocus={handleDetailAutoFocus}
>
{renderDetailBody('max-h-[88vh] overflow-y-auto space-y-8 p-6 md:p-10')}

View File

@@ -53,23 +53,23 @@ function translateDetailLabel(label: string | undefined, t: TFunction<'marketing
function PackageSummary({ pkg, t }: { pkg: CheckoutPackage; t: TFunction<'marketing'> }) {
const isFree = pkg.price === 0;
const accentGradient = pkg.type === 'reseller'
? 'border-amber-100 bg-gradient-to-br from-amber-50/80 via-white to-amber-100/70 shadow-lg shadow-amber-100/60'
: 'border-rose-100 bg-gradient-to-br from-rose-50/80 via-white to-rose-100/70 shadow-lg shadow-rose-100/60';
? 'border-amber-100 bg-gradient-to-br from-amber-50/80 via-white to-amber-100/70 shadow-lg shadow-amber-100/60 dark:border-amber-500/40 dark:from-amber-900/40 dark:via-gray-900 dark:to-amber-900/20 dark:shadow-amber-900/40'
: 'border-rose-100 bg-gradient-to-br from-rose-50/80 via-white to-rose-100/70 shadow-lg shadow-rose-100/60 dark:border-pink-500/40 dark:from-pink-900/40 dark:via-gray-900 dark:to-pink-900/10 dark:shadow-pink-900/40';
return (
<Card className={cn('shadow-sm transition', isFree ? 'opacity-75' : accentGradient)}>
<Card className={cn('shadow-sm transition dark:bg-gray-900 dark:border-gray-800', isFree ? 'opacity-80 dark:opacity-90' : accentGradient)}>
<CardHeader className="space-y-1">
<CardTitle className={`flex items-center gap-3 text-2xl ${isFree ? 'text-muted-foreground' : ''}`}>
<PackageIcon className={`h-6 w-6 ${isFree ? 'text-muted-foreground' : 'text-primary'}`} />
<CardTitle className={cn('flex items-center gap-3 text-2xl', isFree ? 'text-muted-foreground' : 'text-gray-900 dark:text-gray-50')}>
<PackageIcon className={cn('h-6 w-6', isFree ? 'text-muted-foreground' : 'text-primary')} />
{pkg.name}
</CardTitle>
<CardDescription className="text-base text-muted-foreground">
<CardDescription className="text-base text-muted-foreground dark:text-gray-200">
{pkg.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-baseline gap-2">
<span className={`text-3xl font-semibold ${isFree ? 'text-muted-foreground' : ''}`}>
<span className={cn('text-3xl font-semibold', isFree ? 'text-muted-foreground' : 'text-gray-900 dark:text-gray-50')}>
{pkg.price === 0 ? t('packages.free') : currencyFormatter.format(pkg.price)}
</span>
<Badge variant={isFree ? 'outline' : 'secondary'} className="uppercase tracking-wider text-xs">
@@ -77,7 +77,7 @@ function PackageSummary({ pkg, t }: { pkg: CheckoutPackage; t: TFunction<'market
</Badge>
</div>
{pkg.gallery_duration_label && (
<div className="rounded-md border border-dashed border-muted px-3 py-2 text-sm text-muted-foreground">
<div className="rounded-md border border-dashed border-muted px-3 py-2 text-sm text-muted-foreground dark:border-gray-700 dark:text-gray-200">
{t('packages.gallery_days_label')}: {pkg.gallery_duration_label}
</div>
)}
@@ -88,7 +88,7 @@ function PackageSummary({ pkg, t }: { pkg: CheckoutPackage; t: TFunction<'market
</h4>
<div className="grid gap-3 md:grid-cols-2">
{pkg.description_breakdown.map((row, index) => (
<div key={index} className="rounded-lg border border-muted/40 bg-muted/20 px-3 py-2">
<div key={index} className="rounded-lg border border-muted/40 bg-muted/20 px-3 py-2 dark:border-gray-700 dark:bg-gray-800/80">
{row.title && (
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{translateDetailLabel(row.title, t)}
@@ -118,29 +118,29 @@ function PackageSummary({ pkg, t }: { pkg: CheckoutPackage; t: TFunction<'market
function PackageOption({ pkg, isActive, onSelect, t }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void; t: TFunction<'marketing'> }) {
const isFree = pkg.price === 0;
const accentGradient = pkg.type === 'reseller'
? 'border-amber-100 bg-gradient-to-r from-amber-50/70 via-white to-amber-100/60 shadow-md shadow-amber-100/60'
: 'border-rose-100 bg-gradient-to-r from-rose-50/70 via-white to-rose-100/60 shadow-md shadow-rose-100/60';
? 'border-amber-100 bg-gradient-to-r from-amber-50/70 via-white to-amber-100/60 shadow-md shadow-amber-100/60 dark:border-amber-500/40 dark:from-amber-900/40 dark:via-gray-900 dark:to-amber-900/20 dark:shadow-amber-900/40'
: 'border-rose-100 bg-gradient-to-r from-rose-50/70 via-white to-rose-100/60 shadow-md shadow-rose-100/60 dark:border-pink-500/40 dark:from-pink-900/40 dark:via-gray-900 dark:to-pink-900/10 dark:shadow-pink-900/40';
return (
<button
type="button"
onClick={onSelect}
className={cn(
'w-full rounded-md border bg-background p-4 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40',
'w-full rounded-md border bg-background p-4 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100',
isActive
? accentGradient
: isFree
? 'border-border hover:border-primary/40 opacity-75 hover:opacity-100'
: 'border-border hover:border-primary/40',
? 'border-border hover:border-primary/40 opacity-75 hover:opacity-100 dark:border-gray-700 dark:hover:border-primary/50'
: 'border-border hover:border-primary/40 dark:border-gray-700 dark:hover:border-primary/50',
)}
>
<div className="flex items-center justify-between text-sm font-medium">
<span className={isFree ? "text-muted-foreground" : ""}>{pkg.name}</span>
<span className={isFree ? "text-muted-foreground font-normal" : "text-muted-foreground"}>
<span className={isFree ? "text-muted-foreground" : "text-gray-900 dark:text-gray-100"}>{pkg.name}</span>
<span className={isFree ? "text-muted-foreground font-normal" : "text-muted-foreground dark:text-gray-200"}>
{pkg.price === 0 ? t('packages.free') : currencyFormatter.format(pkg.price)}
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{pkg.description}</p>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground dark:text-gray-300">{pkg.description}</p>
</button>
);
}