Files
fotospiel-app/resources/js/pages/marketing/Packages.tsx

1032 lines
49 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, { useState, useEffect, useMemo } from 'react';
import { Head, Link, usePage } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import MarketingLayout from '@/layouts/mainWebsite';
import { useAnalytics } from '@/hooks/useAnalytics';
import { useCtaExperiment } from '@/hooks/useCtaExperiment';
import { ArrowRight, ShoppingCart, Check, X, Users, Image, Shield, Star, Sparkles } from 'lucide-react';
interface Package {
id: number;
name: string;
slug: string;
description: string;
description_breakdown: DescriptionEntry[];
gallery_duration_label?: string;
price: number;
events: number | null;
features: string[];
max_events_per_year?: number | null;
limits?: {
max_photos?: number;
max_guests?: number;
max_tenants?: number;
max_events_per_year?: number;
gallery_days?: number;
};
watermark_allowed?: boolean;
branding_allowed?: boolean;
}
type DescriptionEntry = {
title?: string | null;
value: string;
};
interface PackagesProps {
endcustomerPackages: Package[];
resellerPackages: Package[];
}
const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackages }) => {
const [open, setOpen] = useState(false);
const [selectedPackage, setSelectedPackage] = useState<Package | null>(null);
const [currentStep, setCurrentStep] = useState<'overview' | 'deep' | 'testimonials'>('overview');
const { props } = usePage();
const { auth } = props as any;
const { t } = useTranslation('marketing');
const { t: tCommon } = useTranslation('common');
const {
variant: packagesHeroVariant,
trackClick: trackPackagesHeroClick,
} = useCtaExperiment('packages_hero_cta');
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const packageId = urlParams.get('package_id');
if (packageId) {
const id = parseInt(packageId);
const pkg = [...endcustomerPackages, ...resellerPackages].find(p => p.id === id);
if (pkg) {
setSelectedPackage(pkg);
setOpen(true);
setCurrentStep('overview');
}
}
}, [endcustomerPackages, resellerPackages]);
const testimonials = [
{ name: tCommon('testimonials.anna.name'), text: t('packages.testimonials.anna'), rating: 5 },
{ name: tCommon('testimonials.max.name'), text: t('packages.testimonials.max'), rating: 5 },
{ name: tCommon('testimonials.lisa.name'), text: t('packages.testimonials.lisa'), rating: 5 },
];
const allPackages = [...endcustomerPackages, ...resellerPackages];
const selectHighlightPackageId = (packages: Package[]): number | null => {
const count = packages.length;
if (count <= 1) {
return null;
}
const sortedByPrice = [...packages].sort((a, b) => a.price - b.price);
if (count === 2) {
return sortedByPrice[1]?.id ?? null;
}
if (count === 3) {
return sortedByPrice[1]?.id ?? null;
}
return sortedByPrice[count - 2]?.id ?? null;
};
const highlightEndcustomerId = useMemo(
() => selectHighlightPackageId(endcustomerPackages),
[endcustomerPackages],
);
const highlightResellerId = useMemo(
() => selectHighlightPackageId(resellerPackages),
[resellerPackages],
);
function isHighlightedPackage(pkg: Package, variant: 'endcustomer' | 'reseller') {
return variant === 'reseller' ? pkg.id === highlightResellerId : pkg.id === highlightEndcustomerId;
}
const selectedVariant = useMemo<'endcustomer' | 'reseller'>(() => {
if (!selectedPackage) return 'endcustomer';
return resellerPackages.some((pkg) => pkg.id === selectedPackage.id) ? 'reseller' : 'endcustomer';
}, [selectedPackage, resellerPackages]);
const selectedHighlight = selectedPackage
? isHighlightedPackage(selectedPackage, selectedVariant)
: false;
const { trackEvent } = useAnalytics();
const handleCardClick = (pkg: Package, variant: 'endcustomer' | 'reseller') => {
trackEvent({
category: 'marketing_packages',
action: 'open_dialog',
name: `${variant}:${pkg.name}`,
value: pkg.price,
});
setSelectedPackage(pkg);
setCurrentStep('overview');
setOpen(true);
};
const handleCtaClick = (pkg: Package, variant: 'endcustomer' | 'reseller') => {
trackEvent({
category: 'marketing_packages',
action: 'cta_dialog',
name: `${variant}:${pkg.name}`,
value: pkg.price,
});
};
// nextStep entfernt, da Tabs nun parallel sind
const getFeatureIcon = (feature: string) => {
switch (feature) {
case 'basic_uploads': return <Image className="w-4 h-4" />;
case 'unlimited_sharing': return <ArrowRight className="w-4 h-4" />;
case 'no_watermark': return <Shield className="w-4 h-4" />;
case 'custom_tasks': return <Check className="w-4 h-4" />;
case 'advanced_analytics': return <Star className="w-4 h-4" />;
case 'priority_support': return <Users className="w-4 h-4" />;
case 'reseller_dashboard': return <ShoppingCart className="w-4 h-4" />;
case 'custom_branding': return <Image className="w-4 h-4" />;
default: return <Check className="w-4 h-4" />;
}
};
const getAccentTheme = (variant: 'endcustomer' | 'reseller') => (
variant === 'reseller'
? {
gradient: 'from-amber-100/80 via-white to-white',
ring: 'ring-amber-200 dark:ring-amber-500/40',
badge: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-100',
price: 'text-amber-500 dark:text-amber-300',
buttonHighlight: 'bg-gradient-to-r from-amber-500 via-rose-400 to-pink-500 text-white hover:from-amber-500/95 hover:via-rose-400/95 hover:to-pink-500/95',
buttonDefault: 'bg-gradient-to-r from-amber-50 via-white to-rose-50 text-amber-600 border border-amber-100/80 shadow-sm hover:from-amber-100 hover:via-rose-50 hover:to-white hover:text-amber-600 dark:from-amber-500/20 dark:via-amber-500/10 dark:to-rose-500/20 dark:text-amber-200 dark:border-amber-500/30',
highlightShadow: 'shadow-[0_28px_65px_-20px_rgba(245,158,11,0.55)]',
topBar: 'from-amber-400 via-rose-300 to-pink-400',
ctaShadow: 'shadow-lg shadow-amber-500/25',
}
: {
gradient: 'from-rose-100/80 via-white to-white',
ring: 'ring-rose-200 dark:ring-rose-500/40',
badge: 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-100',
price: 'text-rose-500 dark:text-rose-300',
buttonHighlight: 'bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400 text-white hover:from-rose-500/95 hover:via-pink-500/95 hover:to-amber-400/95',
buttonDefault: 'bg-gradient-to-r from-rose-50 via-white to-pink-50 text-rose-600 border border-rose-100/80 shadow-sm hover:from-rose-100 hover:via-white hover:to-pink-100 hover:text-rose-600 dark:from-rose-500/15 dark:via-rose-500/10 dark:to-rose-500/20 dark:text-rose-200 dark:border-rose-500/30',
highlightShadow: 'shadow-[0_28px_70px_-25px_rgba(244,63,94,0.55)]',
topBar: 'from-rose-500 via-pink-400 to-amber-300',
ctaShadow: 'shadow-lg shadow-rose-500/30',
}
);
type PackageMetric = {
key: string;
label: string;
value: string;
};
const resolvePackageMetrics = (
pkg: Package,
variant: 'endcustomer' | 'reseller',
t: TFunction,
tCommon: TFunction,
): PackageMetric[] => {
if (variant === 'reseller') {
return [
{
key: 'max_tenants',
label: t('packages.max_tenants'),
value: pkg.limits?.max_tenants
? pkg.limits.max_tenants.toLocaleString()
: tCommon('unlimited'),
},
{
key: 'max_events_per_year',
label: t('packages.max_events_year'),
value: pkg.limits?.max_events_per_year
? pkg.limits.max_events_per_year.toLocaleString()
: tCommon('unlimited'),
},
{
key: 'branding',
label: t('packages.feature_custom_branding'),
value: pkg.branding_allowed ? tCommon('included') : t('packages.feature_no_branding'),
},
];
}
return [
{
key: 'max_photos',
label: t('packages.max_photos_label'),
value: pkg.limits?.max_photos
? pkg.limits.max_photos.toLocaleString()
: tCommon('unlimited'),
},
{
key: 'max_guests',
label: t('packages.max_guests_label'),
value: pkg.limits?.max_guests
? pkg.limits.max_guests.toLocaleString()
: tCommon('unlimited'),
},
{
key: 'gallery_days',
label: t('packages.gallery_days_label'),
value: pkg.gallery_duration_label
?? (pkg.limits?.gallery_days
? pkg.limits.gallery_days.toLocaleString()
: tCommon('unlimited')),
},
];
};
interface PackageCardProps {
pkg: Package;
variant: 'endcustomer' | 'reseller';
highlight?: boolean;
onSelect?: (pkg: Package) => void;
className?: string;
showCTA?: boolean;
ctaLabel?: string;
compact?: boolean;
}
function PackageCard({
pkg,
variant,
highlight = false,
onSelect,
className,
showCTA = true,
ctaLabel,
compact = false,
}: PackageCardProps) {
const { t } = useTranslation('marketing');
const { t: tCommon } = useTranslation('common');
const accent = getAccentTheme(variant);
const priceLabel =
pkg.price === 0
? t('packages.free')
: `${pkg.price.toLocaleString()} ${t('packages.currency.euro')}`;
const cadenceLabel =
variant === 'reseller'
? t('packages.billing_per_year')
: t('packages.billing_per_event');
const typeLabel =
variant === 'reseller' ? t('packages.subscription') : t('packages.one_time');
const badgeLabel = highlight
? (variant === 'reseller'
? t('packages.badge_best_value')
: t('packages.badge_most_popular'))
: pkg.price === 0
? t('packages.badge_starter')
: null;
const featureBadges = pkg.features.slice(0, 4);
const extraFeatureCount = Math.max(pkg.features.length - featureBadges.length, 0);
const metrics = resolvePackageMetrics(pkg, variant, t, tCommon);
return (
<Card
className={cn(
'group relative h-full overflow-hidden border border-gray-200/80 bg-white/95 px-0 pb-6 pt-14 transition-all duration-500 dark:border-gray-700 dark:bg-gray-900',
highlight && `bg-gradient-to-br ${accent.gradient} ${accent.highlightShadow} ${accent.ring}`,
!highlight && !compact && 'hover:-translate-y-2 hover:shadow-2xl',
compact && 'pt-10 shadow-none hover:translate-y-0',
className,
)}
>
<div
className={cn(
'pointer-events-none absolute inset-x-0 top-0 h-1 bg-gradient-to-r',
accent.topBar,
highlight ? 'opacity-100' : 'opacity-0 transition-opacity duration-500 group-hover:opacity-90',
)}
/>
<CardHeader className="relative flex flex-col gap-3 px-6">
<div className="flex items-center justify-between">
<Badge
variant="outline"
className={cn(
'rounded-full border-transparent px-3 py-1 text-xs font-semibold uppercase tracking-wider',
accent.badge,
)}
>
{typeLabel}
</Badge>
{badgeLabel && (
<Badge
className={cn(
'flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wider shadow-sm',
highlight
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
: accent.badge,
)}
>
{highlight && <Sparkles className="h-3.5 w-3.5" aria-hidden />}
{badgeLabel}
</Badge>
)}
</div>
<CardTitle className="text-2xl font-display text-gray-900 dark:text-white">
{pkg.name}
</CardTitle>
<CardDescription className="text-sm text-gray-600 dark:text-gray-300">
{pkg.description}
</CardDescription>
</CardHeader>
<CardContent className={cn('mt-2 flex flex-col gap-6 px-6', compact && 'gap-4')}>
<div className="space-y-2">
<div className="flex items-baseline gap-2">
<span className={cn('text-4xl font-bold', accent.price)}>{priceLabel}</span>
{pkg.price !== 0 && (
<span className="text-sm font-medium text-muted-foreground">
/ {cadenceLabel}
</span>
)}
</div>
{variant === 'endcustomer' && (
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-gray-400 dark:text-gray-500">
{pkg.events} × {t('packages.one_time')}
</p>
)}
</div>
<Separator className="bg-gray-200/80 dark:bg-gray-700/60" />
<div className={cn('flex flex-wrap gap-2', compact && 'gap-1.5')}>
{featureBadges.map((feature) => (
<Badge
key={feature}
variant="outline"
className={cn(
'flex items-center gap-1 rounded-full border-transparent bg-white/70 px-3 py-1 text-xs font-medium text-gray-700 shadow-sm transition-colors group-hover:bg-white dark:bg-gray-800/70 dark:text-gray-200',
highlight && 'bg-white/80 dark:bg-white/10',
)}
>
{getFeatureIcon(feature)}
<span>{t(`packages.feature_${feature}`)}</span>
</Badge>
))}
{pkg.watermark_allowed === false && (
<Badge
variant="outline"
className="rounded-full border-transparent bg-emerald-100/80 px-3 py-1 text-xs font-medium text-emerald-700 shadow-sm dark:bg-emerald-500/20 dark:text-emerald-100"
>
<Shield className="mr-1 h-3.5 w-3.5" aria-hidden />
{t('packages.no_watermark')}
</Badge>
)}
{pkg.branding_allowed && (
<Badge
variant="outline"
className="rounded-full border-transparent bg-sky-100/80 px-3 py-1 text-xs font-medium text-sky-700 shadow-sm dark:bg-sky-500/20 dark:text-sky-100"
>
<Sparkles className="mr-1 h-3.5 w-3.5" aria-hidden />
{t('packages.custom_branding')}
</Badge>
)}
{extraFeatureCount > 0 && (
<Badge
variant="outline"
className="rounded-full border-dashed border-gray-300 bg-transparent px-3 py-1 text-xs font-medium text-gray-500 dark:border-gray-600 dark:text-gray-300"
>
{t('packages.more_features', { count: extraFeatureCount })}
</Badge>
)}
</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{metrics.map((metric) => (
<div
key={metric.key}
className={cn(
'rounded-xl border border-gray-200/70 bg-white/80 px-3 py-3 text-center shadow-sm transition-colors dark:border-gray-700/70 dark:bg-gray-800/70',
highlight && 'border-transparent bg-white/90 dark:bg-white/5',
)}
>
<p className="text-lg font-semibold text-gray-900 dark:text-white">
{metric.value}
</p>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{metric.label}
</p>
</div>
))}
</div>
</CardContent>
{showCTA && onSelect && (
<CardFooter className="mt-auto px-6 pt-0">
<Button
onClick={() => onSelect(pkg)}
className={cn(
'w-full justify-center gap-2 text-sm font-semibold tracking-wide transition-all duration-300',
accent.ctaShadow,
highlight ? accent.buttonHighlight : accent.buttonDefault,
)}
>
{ctaLabel ?? t('packages.view_details')}
<ArrowRight className="h-4 w-4" aria-hidden />
</Button>
</CardFooter>
)}
</Card>
);
}
return (
<MarketingLayout title={t('packages.title')}>
{/* Hero Section */}
<section className="bg-aurora-enhanced text-gray-900 dark:text-gray-100 py-20 px-4">
<div className="container mx-auto text-center">
<h1 className="text-4xl md:text-6xl font-bold mb-4 font-display">{t('packages.hero_title')}</h1>
<p className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto font-sans-marketing">{t('packages.hero_description')}</p>
<Link
href="#endcustomer"
onClick={() => {
trackPackagesHeroClick();
trackEvent({
category: 'marketing_packages',
action: 'hero_cta',
name: `endcustomer:${packagesHeroVariant}`,
});
}}
className={cn(
'rounded-full px-8 py-4 text-lg font-semibold font-sans-marketing transition duration-300',
packagesHeroVariant === 'gradient'
? 'bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400 text-white shadow-lg shadow-rose-500/40 hover:from-rose-500/95 hover:via-pink-500/95 hover:to-amber-400/95'
: 'bg-white text-[#FFB6C1] hover:bg-gray-100 dark:bg-gray-800 dark:text-rose-200 dark:hover:bg-gray-700',
)}
>
{packagesHeroVariant === 'gradient' ? t('packages.cta_explore_highlight') : t('packages.cta_explore')}
</Link>
</div>
</section>
<section id="endcustomer" className="py-20 px-4 dark:bg-gray-600">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12 font-display">{t('packages.section_endcustomer')}</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:gap-8 lg:grid-cols-3">
{endcustomerPackages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
variant="endcustomer"
highlight={pkg.id === highlightEndcustomerId}
onSelect={(pkg) => handleCardClick(pkg, 'endcustomer')}
/>
))}
</div>
</div>
{/* Comparison Section for Endcustomer */}
<div className="mt-12">
<h3 className="text-2xl font-bold text-center mb-6 font-display">{t('packages.comparison_title')}</h3>
<div className="block md:hidden">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="price">
<AccordionTrigger className="font-sans-marketing">{t('packages.price')}</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-3 gap-4 p-4">
{endcustomerPackages.map((pkg) => (
<div key={pkg.id} className="text-center">
<p className="font-bold">{pkg.name}</p>
<p>{pkg.price === 0 ? t('packages.free') : `${pkg.price} ${t('packages.currency.euro')}`}</p>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="max-photos">
<AccordionTrigger className="font-sans-marketing">{t('packages.max_photos_label')} {getFeatureIcon('max_photos')}</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-3 gap-4 p-4">
{endcustomerPackages.map((pkg) => (
<div key={pkg.id} className="text-center">
<p className="font-bold">{pkg.name}</p>
<p>{pkg.limits?.max_photos || tCommon('unlimited')}</p>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="max-guests">
<AccordionTrigger className="font-sans-marketing">{t('packages.max_guests_label')} {getFeatureIcon('max_guests')}</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-3 gap-4 p-4">
{endcustomerPackages.map((pkg) => (
<div key={pkg.id} className="text-center">
<p className="font-bold">{pkg.name}</p>
<p>{pkg.limits?.max_guests || tCommon('unlimited')}</p>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="gallery-days">
<AccordionTrigger className="font-sans-marketing">{t('packages.gallery_days_label')} {getFeatureIcon('gallery_days')}</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-3 gap-4 p-4">
{endcustomerPackages.map((pkg) => (
<div key={pkg.id} className="text-center">
<p className="font-bold">{pkg.name}</p>
<p>{pkg.limits?.gallery_days || tCommon('unlimited')}</p>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="watermark">
<AccordionTrigger className="font-sans-marketing">{t('packages.watermark_label')} {getFeatureIcon('no_watermark')}</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-3 gap-4 p-4">
{endcustomerPackages.map((pkg) => (
<div key={pkg.id} className="text-center">
<p className="font-bold">{pkg.name}</p>
{pkg.watermark_allowed === false ? <Check className="w-4 h-4 text-green-500 mx-auto" /> : <X className="w-4 h-4 text-red-500 mx-auto" />}
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="hidden md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('packages.feature')}</TableHead>
{endcustomerPackages.map((pkg) => (
<TableHead key={pkg.id} className="text-center">
{pkg.name}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-semibold">{t('packages.price')}</TableCell>
{endcustomerPackages.map((pkg) => (
<TableCell key={pkg.id} className="text-center">
{pkg.price === 0 ? t('packages.free') : `${pkg.price} ${t('packages.currency.euro')}`}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell className="font-semibold">{t('packages.max_photos_label')} {getFeatureIcon('max_photos')}</TableCell>
{endcustomerPackages.map((pkg) => (
<TableCell key={pkg.id} className="text-center">
{pkg.limits?.max_photos || tCommon('unlimited')}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell className="font-semibold">{t('packages.max_guests_label')} {getFeatureIcon('max_guests')}</TableCell>
{endcustomerPackages.map((pkg) => (
<TableCell key={pkg.id} className="text-center">
{pkg.limits?.max_guests || tCommon('unlimited')}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell className="font-semibold">{t('packages.gallery_days_label')} {getFeatureIcon('gallery_days')}</TableCell>
{endcustomerPackages.map((pkg) => (
<TableCell key={pkg.id} className="text-center">
{pkg.limits?.gallery_days || tCommon('unlimited')}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell className="font-semibold">{t('packages.watermark_label')} {getFeatureIcon('no_watermark')}</TableCell>
{endcustomerPackages.map((pkg) => (
<TableCell key={pkg.id} className="text-center">
{pkg.watermark_allowed === false ? <Check className="w-4 h-4 text-green-500" /> : <X className="w-4 h-4 text-red-500" />}
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
</div>
</div>
</section>
<section className="py-20 px-4 bg-gray-50 dark:bg-gray-900">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12 font-display">{t('packages.section_reseller')}</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:gap-8 xl:grid-cols-3">
{resellerPackages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
variant="reseller"
highlight={pkg.id === highlightResellerId}
onSelect={(pkg) => handleCardClick(pkg, 'reseller')}
/>
))}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-20 px-4 dark:bg-gray-700">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12 font-display">{t('packages.faq_title')}</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2 font-display">{t('packages.faq_free')}</h3>
<p className="text-gray-600 dark:text-gray-300 font-sans-marketing">{t('packages.faq_free_desc')}</p>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2 font-display">{t('packages.faq_upgrade')}</h3>
<p className="text-gray-600 dark:text-gray-300 font-sans-marketing">{t('packages.faq_upgrade_desc')}</p>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2 font-display">{t('packages.faq_reseller')}</h3>
<p className="text-gray-600 dark:text-gray-300 font-sans-marketing">{t('packages.faq_reseller_desc')}</p>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-2 font-display">{t('packages.faq_payment')}</h3>
<p className="text-gray-600 dark:text-gray-300 font-sans-marketing">{t('packages.faq_payment_desc')}</p>
</div>
</div>
</div>
</section>
{/* Modal */}
{selectedPackage && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto border border-rose-100/70 bg-gradient-to-br from-white via-rose-50/60 to-amber-50/40 p-0 shadow-2xl dark:border-gray-700 dark:from-gray-950 dark:via-gray-900/95 dark:to-gray-900">
<div className="space-y-6 p-6 md:p-8">
<DialogHeader className="space-y-4 text-left">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex flex-1 flex-col gap-3">
<div className="flex flex-wrap items-center gap-3">
<Badge
variant="outline"
className="rounded-full border border-rose-200/70 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-rose-600 shadow-sm dark:border-rose-500/30 dark:bg-gray-900/80 dark:text-rose-100"
>
{selectedVariant === 'reseller' ? t('packages.subscription') : t('packages.one_time')}
</Badge>
{selectedHighlight && (
<Badge className="flex items-center gap-1 rounded-full bg-gray-900 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-white dark:text-gray-900">
<Sparkles className="h-3.5 w-3.5" />
{selectedVariant === 'reseller'
? t('packages.badge_best_value')
: t('packages.badge_most_popular')}
</Badge>
)}
</div>
<div className="flex flex-wrap items-end gap-4">
<DialogTitle className="text-3xl font-display font-semibold text-gray-900 dark:text-white">
{selectedPackage.name}
</DialogTitle>
<div className="flex items-baseline gap-2">
<span className={cn('text-3xl font-bold', getAccentTheme(selectedVariant).price)}>
{selectedPackage.price === 0
? t('packages.free')
: `${selectedPackage.price.toLocaleString()} ${t('packages.currency.euro')}`}
</span>
{selectedPackage.price !== 0 && (
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
/ {selectedVariant === 'reseller' ? t('packages.billing_per_year') : t('packages.billing_per_event')}
</span>
)}
</div>
</div>
{selectedPackage.description && (
<p className="text-sm text-gray-600 dark:text-gray-300">
{selectedPackage.description}
</p>
)}
</div>
</div>
</DialogHeader>
<Tabs value={currentStep} onValueChange={setCurrentStep} className="w-full">
<TabsList className="grid w-full grid-cols-3 rounded-full bg-white/60 p-1 text-sm shadow-sm dark:bg-gray-900/60">
<TabsTrigger className="rounded-full" value="overview">{t('packages.details')}</TabsTrigger>
<TabsTrigger className="rounded-full" value="deep">{t('packages.more_details_tab')}</TabsTrigger>
<TabsTrigger className="rounded-full" value="testimonials">{t('packages.customer_opinions')}</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-6 space-y-6">
{(() => {
const accent = getAccentTheme(selectedVariant);
const metrics = resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon);
const topFeatureBadges = selectedPackage.features.slice(0, 3);
const hasMoreFeatures = selectedPackage.features.length > topFeatureBadges.length;
const quickFacts = metrics.slice(0, 2);
const showDeepLink =
hasMoreFeatures || (selectedPackage.description_breakdown?.length ?? 0) > 0;
return (
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr),minmax(0,0.8fr)]">
<div
className={cn(
'relative overflow-hidden rounded-3xl border border-gray-200/80 bg-white/95 p-6 shadow-xl dark:border-gray-700/70 dark:bg-gray-900/90',
selectedHighlight && `bg-gradient-to-br ${accent.gradient} ${accent.highlightShadow}`,
)}
>
<div
className="pointer-events-none absolute inset-0 opacity-70 mix-blend-overlay"
style={{
background:
'radial-gradient(circle at top left, rgba(255,182,193,0.45), transparent 55%), radial-gradient(circle at bottom right, rgba(250,204,21,0.35), transparent 55%)',
}}
/>
<div className="relative flex h-full flex-col justify-between gap-5">
<div className="space-y-5">
<Badge className="inline-flex w-fit items-center gap-1 rounded-full bg-gray-900/90 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-white shadow-md dark:bg-white/90 dark:text-gray-900">
<Sparkles className="h-3.5 w-3.5" />
{t('packages.feature_highlights')}
</Badge>
<div className="flex flex-wrap gap-2">
{topFeatureBadges.map((feature) => (
<Badge
key={feature}
variant="outline"
className={cn(
'flex items-center gap-1 rounded-full border-transparent bg-white/80 px-3 py-1 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:bg-white dark:bg-gray-800/70 dark:text-gray-200',
selectedHighlight && 'bg-white/85 dark:bg-white/10',
)}
>
{getFeatureIcon(feature)}
<span>{t(`packages.feature_${feature}`)}</span>
</Badge>
))}
{selectedPackage.watermark_allowed === false && (
<Badge className="flex items-center gap-1 rounded-full bg-emerald-100/80 px-3 py-1 text-xs font-medium text-emerald-700 shadow-sm dark:bg-emerald-500/20 dark:text-emerald-100">
<Shield className="h-3.5 w-3.5" />
{t('packages.no_watermark')}
</Badge>
)}
{selectedPackage.branding_allowed && (
<Badge className="flex items-center gap-1 rounded-full bg-sky-100/80 px-3 py-1 text-xs font-medium text-sky-700 shadow-sm dark:bg-sky-500/20 dark:text-sky-100">
<Sparkles className="h-3.5 w-3.5" />
{t('packages.custom_branding')}
</Badge>
)}
</div>
</div>
<div className="space-y-3">
{showDeepLink && (
<button
type="button"
onClick={() => setCurrentStep('deep')}
className="inline-flex items-center gap-2 text-sm font-semibold text-rose-500 transition-colors hover:text-rose-600 dark:text-rose-300 dark:hover:text-rose-200"
>
{t('packages.more_details_link')}
<ArrowRight className="h-4 w-4" />
</button>
)}
<Button
asChild
className={cn(
'w-full justify-center gap-2 rounded-full py-3 text-base font-semibold transition-all duration-300',
accent.ctaShadow,
selectedHighlight ? accent.buttonHighlight : accent.buttonDefault,
)}
>
<Link
href={`/purchase-wizard/${selectedPackage.id}`}
onClick={() => {
if (selectedPackage) {
handleCtaClick(selectedPackage, selectedVariant);
}
localStorage.setItem('preferred_package', JSON.stringify(selectedPackage));
}}
>
{t('packages.to_order')}
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<p className="text-xs text-center text-gray-500 dark:text-gray-400">
{t('packages.order_hint')}
</p>
</div>
</div>
</div>
<div className="flex h-full flex-col gap-4 rounded-3xl border border-gray-200/70 bg-white/90 p-6 shadow-lg dark:border-gray-700/70 dark:bg-gray-900/85">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('packages.quick_facts')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300">
{t('packages.quick_facts_hint')}
</p>
<ul className="space-y-3">
{quickFacts.map((metric) => (
<li
key={metric.key}
className="rounded-2xl border border-gray-200/70 bg-white/80 p-4 dark:border-gray-700/70 dark:bg-gray-800/70"
>
<p className="text-lg font-semibold text-gray-900 dark:text-white">{metric.value}</p>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{metric.label}
</p>
</li>
))}
</ul>
{showDeepLink && (
<Button
variant="outline"
className="mt-auto w-full justify-center rounded-full border-rose-200/70 text-rose-600 hover:bg-rose-50 dark:border-rose-500/40 dark:text-rose-200 dark:hover:bg-rose-500/10"
onClick={() => setCurrentStep('deep')}
>
{t('packages.more_details_link')}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
)}
</div>
</div>
);
})()}
</TabsContent>
<TabsContent value="deep" className="mt-6 space-y-6">
{(() => {
const accent = getAccentTheme(selectedVariant);
const metrics = resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon);
const descriptionEntries = selectedPackage.description_breakdown ?? [];
const entriesWithTitle = descriptionEntries.filter((entry) => entry.title);
const entriesWithoutTitle = descriptionEntries.filter((entry) => !entry.title);
return (
<div className="space-y-6">
<section className="rounded-3xl border border-gray-200/70 bg-white/95 p-6 shadow-lg dark:border-gray-700/70 dark:bg-gray-900/85">
<div className="flex items-center gap-3">
<Badge className="rounded-full bg-gray-900/90 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-white shadow-md dark:bg-white/90 dark:text-gray-900">
{t('packages.features_label')}
</Badge>
{selectedHighlight && (
<Badge className="rounded-full bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-white shadow-sm">
{t('packages.badge_deep_dive')}
</Badge>
)}
</div>
<div className="mt-4 flex flex-wrap gap-2">
{selectedPackage.features.map((feature) => (
<Badge
key={feature}
variant="outline"
className={cn(
'flex items-center gap-1 rounded-full border-transparent bg-white/85 px-3 py-1 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:bg-white dark:bg-gray-800/70 dark:text-gray-200',
selectedHighlight && 'bg-white/85 dark:bg-white/10',
)}
>
{getFeatureIcon(feature)}
<span>{t(`packages.feature_${feature}`)}</span>
</Badge>
))}
{selectedPackage.watermark_allowed === false && (
<Badge className="flex items-center gap-1 rounded-full bg-emerald-100/80 px-3 py-1 text-xs font-medium text-emerald-700 shadow-sm dark:bg-emerald-500/20 dark:text-emerald-100">
<Shield className="h-3.5 w-3.5" />
{t('packages.no_watermark')}
</Badge>
)}
{selectedPackage.branding_allowed && (
<Badge className="flex items-center gap-1 rounded-full bg-sky-100/80 px-3 py-1 text-xs font-medium text-sky-700 shadow-sm dark:bg-sky-500/20 dark:text-sky-100">
<Sparkles className="h-3.5 w-3.5" />
{t('packages.custom_branding')}
</Badge>
)}
</div>
</section>
{metrics.length > 0 && (
<section
className={cn(
'rounded-3xl border border-gray-200/70 bg-white/95 p-6 shadow-lg dark:border-gray-700/70 dark:bg-gray-900/85',
selectedHighlight && `ring-2 ${accent.ring}`,
)}
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('packages.limits_label')}
</h3>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-300">
{t('packages.limits_label_hint')}
</p>
<div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
{metrics.map((metric) => (
<div
key={metric.key}
className="rounded-2xl border border-gray-200/70 bg-white/80 p-4 text-center shadow-sm dark:border-gray-700/70 dark:bg-gray-800/70"
>
<p className="text-lg font-semibold text-gray-900 dark:text-white">{metric.value}</p>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{metric.label}
</p>
</div>
))}
</div>
</section>
)}
{descriptionEntries.length > 0 && (
<section className="rounded-3xl border border-gray-200/70 bg-white/95 p-6 shadow-lg dark:border-gray-700/70 dark:bg-gray-900/85">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('packages.breakdown_label')}
</h3>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-300">
{t('packages.breakdown_label_hint')}
</p>
{entriesWithTitle.length > 0 && (
<Accordion type="single" collapsible className="mt-4 space-y-3">
{entriesWithTitle.map((entry, index) => (
<AccordionItem
key={`${entry.title}-${index}`}
value={`entry-${index}`}
className="overflow-hidden rounded-2xl border border-gray-200/70 bg-white/85 shadow-sm dark:border-gray-700/70 dark:bg-gray-900/80"
>
<AccordionTrigger className="px-4 text-left text-sm font-semibold text-gray-900 hover:no-underline dark:text-white">
{entry.title}
</AccordionTrigger>
<AccordionContent className="px-4 pb-4 text-sm text-gray-600 dark:text-gray-300 whitespace-pre-line">
{entry.value}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
{entriesWithoutTitle.length > 0 && (
<div className="mt-4 space-y-3">
{entriesWithoutTitle.map((entry, index) => (
<div
key={`plain-${index}`}
className="rounded-2xl border border-gray-200/70 bg-white/85 p-4 text-sm text-gray-600 shadow-sm dark:border-gray-700/70 dark:bg-gray-900/80 dark:text-gray-300 whitespace-pre-line"
>
{entry.value}
</div>
))}
</div>
)}
</section>
)}
</div>
);
})()}
</TabsContent>
<TabsContent value="testimonials" className="mt-6">
<div className="space-y-6">
<h3 className="text-xl font-semibold font-display text-gray-900 dark:text-white">
{t('packages.what_customers_say')}
</h3>
<div className="flex flex-col gap-4">
{testimonials.map((testimonial, index) => (
<div
key={index}
className="rounded-2xl border border-gray-200/60 bg-white/90 p-5 shadow-md dark:border-gray-700/60 dark:bg-gray-900/80"
>
<p className="text-sm text-gray-600 dark:text-gray-300">{testimonial.text}</p>
<div className="mt-3 flex items-center justify-between">
<span className="font-semibold text-gray-900 dark:text-white">{testimonial.name}</span>
<div className="flex gap-1">
{[...Array(testimonial.rating)].map((_, i) => (
<Star key={i} className="h-3.5 w-3.5 text-amber-400" fill="currentColor" />
))}
</div>
</div>
</div>
))}
</div>
<div className="text-center">
<Button
variant="ghost"
className="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => setOpen(false)}
>
{t('packages.close')}
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
)}
{/* Testimonials Section entfernt, da nun im Dialog */}
</MarketingLayout>
);
};
Packages.layout = (page: React.ReactNode) => page;
export default Packages;