1032 lines
49 KiB
TypeScript
1032 lines
49 KiB
TypeScript
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;
|