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

861 lines
35 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, 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;
}
interface PackageComparisonProps {
packages: Package[];
variant: 'endcustomer' | 'reseller';
}
function PackageComparison({ packages, variant }: PackageComparisonProps) {
const { t } = useTranslation('marketing');
const { t: tCommon } = useTranslation('common');
if (packages.length === 0) {
return null;
}
const formatPrice = (pkg: Package) =>
pkg.price === 0 ? t('packages.free') : `${pkg.price.toLocaleString()} ${t('packages.currency.euro')}`;
const limits =
variant === 'endcustomer'
? [
{
key: 'price',
label: t('packages.price'),
value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_event')}`,
},
{
key: 'max_photos',
label: t('packages.max_photos_label'),
value: (pkg: Package) => pkg.limits?.max_photos?.toLocaleString() ?? tCommon('unlimited'),
},
{
key: 'max_guests',
label: t('packages.max_guests_label'),
value: (pkg: Package) => pkg.limits?.max_guests?.toLocaleString() ?? tCommon('unlimited'),
},
{
key: 'gallery_days',
label: t('packages.gallery_days_label'),
value: (pkg: Package) =>
pkg.gallery_duration_label ??
pkg.limits?.gallery_days?.toLocaleString() ??
tCommon('unlimited'),
},
]
: [
{
key: 'price',
label: t('packages.price'),
value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_year')}`,
},
{
key: 'max_tenants',
label: t('packages.max_tenants'),
value: (pkg: Package) => pkg.limits?.max_tenants?.toLocaleString() ?? tCommon('unlimited'),
},
{
key: 'max_events_per_year',
label: t('packages.max_events_year'),
value: (pkg: Package) =>
pkg.limits?.max_events_per_year?.toLocaleString() ?? tCommon('unlimited'),
},
];
const features = [
{
key: 'watermark',
label: t('packages.watermark_label'),
value: (pkg: Package) =>
pkg.watermark_allowed === false ? t('packages.no_watermark') : t('packages.feature_watermark'),
},
{
key: 'branding',
label: t('packages.feature_custom_branding'),
value: (pkg: Package) => (pkg.branding_allowed ? t('packages.available') : t('packages.not_available')),
},
{
key: 'support',
label: t('packages.feature_support'),
value: (pkg: Package) =>
pkg.features.includes('priority_support') ? t('packages.priority_support') : t('packages.standard_support'),
},
];
return (
<div className="space-y-4">
<div>
<h3 className="text-2xl font-semibold font-display">{t('packages.comparison_title')}</h3>
<p className="text-sm text-muted-foreground">{t('packages.comparison_subtitle')}</p>
</div>
<Tabs defaultValue="limits">
<TabsList className="grid w-full max-w-sm grid-cols-2 rounded-full">
<TabsTrigger value="limits">{t('packages.comparison_limits')}</TabsTrigger>
<TabsTrigger value="features">{t('packages.comparison_features')}</TabsTrigger>
</TabsList>
<TabsContent value="limits">
<Card className="overflow-hidden">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('packages.feature')}</TableHead>
{packages.map((pkg) => (
<TableHead key={pkg.id} className="text-center">
{pkg.name}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{limits.map((row) => (
<TableRow key={row.key}>
<TableCell className="font-medium">{row.label}</TableCell>
{packages.map((pkg) => (
<TableCell key={`${row.key}-${pkg.id}`} className="text-center">
{row.value(pkg)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="features">
<Card className="overflow-hidden">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('packages.feature')}</TableHead>
{packages.map((pkg) => (
<TableHead key={pkg.id} className="text-center">
{pkg.name}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{features.map((row) => (
<TableRow key={row.key}>
<TableCell className="font-medium">{row.label}</TableCell>
{packages.map((pkg) => (
<TableCell key={`${row.key}-${pkg.id}`} className="text-center">
{row.value(pkg)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
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' | '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 purchaseUrl = selectedPackage ? `/purchase-wizard/${selectedPackage.id}` : '#';
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'
? {
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',
}
: {
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',
};
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 numericPrice = Number(pkg.price);
const priceLabel =
numericPrice === 0
? t('packages.free')
: `${numericPrice.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 keyFeatures = pkg.features.slice(0, 3);
const metrics = resolvePackageMetrics(pkg, variant, t, tCommon);
return (
<Card
className={cn(
'flex h-full flex-col rounded-2xl border border-gray-100 bg-white shadow-sm transition hover:shadow-lg',
highlight && `${accent.cardBorder} ${accent.highlightShadow}`,
className,
)}
>
<CardHeader className="gap-4">
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-[0.2em] text-gray-500">
<span>{typeLabel}</span>
{badgeLabel && (
<span
className={cn(
'rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-wider',
accent.badge,
)}
>
{badgeLabel}
</span>
)}
</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>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div>
<div className="flex items-baseline gap-2">
<span className={cn('text-4xl font-semibold', accent.price)}>{priceLabel}</span>
{pkg.price !== 0 && (
<span className="text-sm text-gray-500">/ {cadenceLabel}</span>
)}
</div>
{variant === 'endcustomer' && (
<p className="text-xs text-gray-400">
{pkg.events} × {t('packages.one_time')}
</p>
)}
</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>
))}
</div>
<ul className="space-y-2 text-sm text-gray-700">
{keyFeatures.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<Check className="h-4 w-4 text-gray-900" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
{pkg.watermark_allowed === false && (
<li className="flex items-center gap-2">
<Shield className="h-4 w-4 text-gray-900" />
<span>{t('packages.no_watermark')}</span>
</li>
)}
{pkg.branding_allowed && (
<li className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-gray-900" />
<span>{t('packages.custom_branding')}</span>
</li>
)}
</ul>
</CardContent>
{showCTA && onSelect && (
<CardFooter className="mt-auto">
<Button
onClick={() => onSelect(pkg)}
className={cn(
'w-full justify-center rounded-full text-sm font-semibold',
highlight ? accent.buttonHighlight : accent.buttonDefault,
)}
variant={highlight ? 'default' : 'outline'}
>
{ctaLabel ?? t('packages.view_details')}
<ArrowRight className="h-4 w-4" aria-hidden />
</Button>
</CardFooter>
)}
</Card>
);
}
return (
<MarketingLayout title={t('packages.title')}>
<section className="bg-aurora-enhanced text-gray-900 dark:text-gray-100 py-20 px-4">
<div className="container mx-auto text-center space-y-8">
<div className="space-y-4">
<p className="text-sm uppercase tracking-[0.2em] text-gray-600 dark:text-gray-300">
{t('packages.for_endcustomers')}
</p>
<h1 className="text-4xl md:text-6xl font-bold font-display">{t('packages.hero_title')}</h1>
<p className="text-xl md:text-2xl max-w-3xl mx-auto font-sans-marketing text-gray-700 dark:text-gray-200">
{t('packages.hero_description')}
</p>
</div>
<div className="flex flex-col items-center gap-4">
<Link
href="/de/demo"
onClick={() => {
trackPackagesHeroClick();
trackEvent({
category: 'marketing_packages',
action: 'hero_cta',
name: `demo:${packagesHeroVariant}`,
});
}}
className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400 px-10 py-4 text-lg font-semibold text-white shadow-xl shadow-rose-500/40 transition hover:from-rose-500/95 hover:via-pink-500/95 hover:to-amber-400/95"
>
{t('packages.cta_demo')}
<ArrowRight className="h-5 w-5" />
</Link>
<p className="text-sm text-gray-600 dark:text-gray-300">
{t('packages.hero_secondary')}
</p>
</div>
</div>
</section>
<section className="py-20 px-4">
<div className="container mx-auto space-y-12">
<Tabs defaultValue="endcustomer" className="space-y-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<TabsList className="grid w-full max-w-2xl grid-cols-2 rounded-full bg-white/70 p-1 shadow-sm dark:bg-gray-800/70">
<TabsTrigger className="rounded-full font-semibold" value="endcustomer">
{t('packages.tab_endcustomer')}
</TabsTrigger>
<TabsTrigger className="rounded-full font-semibold" value="reseller">
{t('packages.tab_reseller')}
</TabsTrigger>
</TabsList>
<p className="text-sm text-muted-foreground max-w-md">{t('packages.comparison_hint')}</p>
</div>
<TabsContent value="endcustomer" className="space-y-8">
<div className="overflow-x-auto pb-4">
<div className="flex min-w-full gap-6 md:grid md:grid-cols-2">
{endcustomerPackages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
variant="endcustomer"
highlight={pkg.id === highlightEndcustomerId}
onSelect={(selected) => handleCardClick(selected, 'endcustomer')}
className="min-w-[280px]"
compact
/>
))}
</div>
</div>
<PackageComparison packages={endcustomerPackages} variant="endcustomer" />
</TabsContent>
<TabsContent value="reseller" className="space-y-8">
<div className="overflow-x-auto pb-4">
<div className="flex min-w-full gap-6 md:grid md:grid-cols-2 lg:grid-cols-3">
{resellerPackages.map((pkg) => (
<PackageCard
key={pkg.id}
pkg={pkg}
variant="reseller"
highlight={pkg.id === highlightResellerId}
onSelect={(selected) => handleCardClick(selected, 'reseller')}
className="min-w-[280px]"
compact
/>
))}
</div>
</div>
<PackageComparison packages={resellerPackages} variant="reseller" />
</TabsContent>
</Tabs>
</div>
</section>
<section className="py-20 px-4 bg-muted/30 dark:bg-gray-900">
<div className="container mx-auto space-y-10">
<div className="text-center space-y-3">
<h2 className="text-3xl font-bold font-display">{t('packages.faq_title')}</h2>
<p className="text-muted-foreground">{t('packages.faq_lead')}</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{[
{ title: t('packages.faq_free'), body: t('packages.faq_free_desc') },
{ title: t('packages.faq_upgrade'), body: t('packages.faq_upgrade_desc') },
{ title: t('packages.faq_reseller'), body: t('packages.faq_reseller_desc') },
{ title: t('packages.faq_payment'), body: t('packages.faq_payment_desc') },
].map((item) => (
<Card key={item.title} className="h-full border border-gray-200/70 dark:border-gray-800/70">
<CardHeader>
<CardTitle className="text-xl font-display">{item.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-300">{item.body}</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
{/* Modal */}
{selectedPackage && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-6xl border border-gray-100 bg-white px-0 py-0 sm:rounded-[32px]">
<div className="max-h-[88vh] overflow-y-auto space-y-8 p-6 md:p-10">
<DialogHeader className="space-y-3 text-left">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-gray-400">
{selectedVariant === 'reseller' ? t('packages.subscription') : t('packages.one_time')}
</p>
<DialogTitle className="text-3xl font-display text-gray-900">
{selectedPackage.name}
</DialogTitle>
<p className="text-base text-gray-600">{selectedPackage.description}</p>
</DialogHeader>
<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="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">
{Number(selectedPackage.price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {t('packages.currency.euro')}
</p>
{selectedPackage.price > 0 && (
<p className="text-sm text-gray-500">
/ {selectedVariant === 'reseller' ? t('packages.billing_per_year') : t('packages.billing_per_event')}
</p>
)}
</div>
{selectedHighlight && (
<span className="rounded-full bg-gray-900 px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-white">
{selectedVariant === '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">
{resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon).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>
))}
</div>
<Button
asChild
className="mt-6 w-full justify-center rounded-full bg-rose-600/90 py-3 text-base font-semibold text-white shadow-lg shadow-rose-500/30 transition hover:bg-rose-600"
>
<Link
href={purchaseUrl}
onClick={() => {
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="mt-3 text-xs text-gray-500">{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">
{selectedPackage.features.slice(0, 5).map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="mt-1 h-4 w-4 text-gray-900" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
{selectedPackage.watermark_allowed === false && (
<li className="flex items-start gap-2">
<Shield className="mt-1 h-4 w-4 text-gray-900" />
<span>{t('packages.no_watermark')}</span>
</li>
)}
{selectedPackage.branding_allowed && (
<li className="flex items-start gap-2">
<Sparkles className="mt-1 h-4 w-4 text-gray-900" />
<span>{t('packages.custom_branding')}</span>
</li>
)}
</ul>
</div>
</div>
<div className="space-y-4">
<Tabs value={currentStep} onValueChange={setCurrentStep}>
<TabsList className="grid w-full grid-cols-3 rounded-full bg-gray-100 p-1 text-sm">
<TabsTrigger className="rounded-full" value="overview">
{t('packages.details')}
</TabsTrigger>
<TabsTrigger className="rounded-full" value="testimonials">
{t('packages.customer_opinions')}
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-6 space-y-6">
<div className="rounded-2xl border border-gray-100 bg-white p-6">
<h4 className="text-lg font-semibold text-gray-900">{t('packages.quick_facts')}</h4>
<p className="text-sm text-gray-500">{t('packages.quick_facts_hint')}</p>
<div className="mt-4 grid gap-4 md:grid-cols-2">
{resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon).map((metric) => (
<div key={metric.key} className="rounded-xl bg-gray-50 p-4">
<p className="text-xl font-semibold text-gray-900">{metric.value}</p>
<p className="text-xs uppercase tracking-wide text-gray-500">{metric.label}</p>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-gray-100 bg-white p-6 space-y-3">
<h4 className="text-sm font-semibold text-gray-900">{t('packages.feature_highlights')}</h4>
<ul className="space-y-2 text-sm text-gray-700">
{selectedPackage.features.slice(0, 4).map((feature) => (
<li key={feature} className="flex items-center gap-2">
<Check className="h-4 w-4 text-gray-900" />
<span>{t(`packages.feature_${feature}`)}</span>
</li>
))}
{selectedPackage.watermark_allowed === false && (
<li className="flex items-center gap-2">
<Shield className="h-4 w-4 text-gray-900" />
<span>{t('packages.no_watermark')}</span>
</li>
)}
{selectedPackage.branding_allowed && (
<li className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-gray-900" />
<span>{t('packages.custom_branding')}</span>
</li>
)}
</ul>
</div>
</TabsContent>
<TabsContent value="deep" className="mt-6 space-y-6">
{selectedPackage.description_breakdown?.length ? (
<Accordion type="multiple" className="space-y-4">
{selectedPackage.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">
{entry.title ?? t('packages.limits_label')}
</AccordionTrigger>
<AccordionContent className="pb-4 text-sm text-gray-600 whitespace-pre-line">
{entry.value}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<p className="text-sm text-gray-500">{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 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">{selectedPackage.name}</p>
</div>
<div className="flex gap-1 text-amber-400">
{[...Array(testimonial.rating)].map((_, i) => (
<Star key={i} className="h-3.5 w-3.5" fill="currentColor" />
))}
</div>
</div>
<p className="mt-3 text-sm text-gray-600">{testimonial.text}</p>
</div>
))}
<Button
variant="outline"
className="w-full rounded-full border-gray-200 text-sm font-medium text-gray-700"
onClick={() => setOpen(false)}
>
{t('packages.close')}
</Button>
</div>
</TabsContent>
</Tabs>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)}
{/* Testimonials Section entfernt, da nun im Dialog */}
</MarketingLayout>
);
};
Packages.layout = (page: React.ReactNode) => page;
export default Packages;