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:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user