- Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads, attach packages, and surface localized success/error states. - Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/ PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent creation, webhooks, and the wizard CTA. - Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/ useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages, Checkout) with localized copy and experiment tracking. - Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations. - Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke test for the hero CTA while reconciling outstanding checklist items.
This commit is contained in:
@@ -13,6 +13,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
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 {
|
||||
@@ -50,11 +52,15 @@ interface PackagesProps {
|
||||
const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackages }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPackage, setSelectedPackage] = useState<Package | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState('step1');
|
||||
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);
|
||||
@@ -65,7 +71,7 @@ const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackag
|
||||
if (pkg) {
|
||||
setSelectedPackage(pkg);
|
||||
setOpen(true);
|
||||
setCurrentStep('step1');
|
||||
setCurrentStep('overview');
|
||||
}
|
||||
}
|
||||
}, [endcustomerPackages, resellerPackages]);
|
||||
@@ -78,27 +84,34 @@ const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackag
|
||||
|
||||
const allPackages = [...endcustomerPackages, ...resellerPackages];
|
||||
|
||||
const highlightEndcustomerId = useMemo(() => {
|
||||
if (!endcustomerPackages.length) {
|
||||
const selectHighlightPackageId = (packages: Package[]): number | null => {
|
||||
const count = packages.length;
|
||||
if (count <= 1) {
|
||||
return null;
|
||||
}
|
||||
const best = endcustomerPackages.reduce((prev, current) => {
|
||||
if (!prev) return current;
|
||||
return current.price > prev.price ? current : prev;
|
||||
}, null as Package | null);
|
||||
return best?.id ?? endcustomerPackages[0].id;
|
||||
}, [endcustomerPackages]);
|
||||
|
||||
const highlightResellerId = useMemo(() => {
|
||||
if (!resellerPackages.length) {
|
||||
return null;
|
||||
const sortedByPrice = [...packages].sort((a, b) => a.price - b.price);
|
||||
|
||||
if (count === 2) {
|
||||
return sortedByPrice[1]?.id ?? null;
|
||||
}
|
||||
const best = resellerPackages.reduce((prev, current) => {
|
||||
if (!prev) return current;
|
||||
return current.price > prev.price ? current : prev;
|
||||
}, null as Package | null);
|
||||
return best?.id ?? resellerPackages[0].id;
|
||||
}, [resellerPackages]);
|
||||
|
||||
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;
|
||||
@@ -113,12 +126,29 @@ const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackag
|
||||
? isHighlightedPackage(selectedPackage, selectedVariant)
|
||||
: false;
|
||||
|
||||
const handleCardClick = (pkg: Package) => {
|
||||
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('step1');
|
||||
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) => {
|
||||
@@ -428,8 +458,24 @@ function PackageCard({
|
||||
<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" className="bg-white dark:bg-gray-800 text-[#FFB6C1] px-8 py-4 rounded-full font-semibold text-lg font-sans-marketing hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
{t('packages.cta_explore')}
|
||||
<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>
|
||||
@@ -447,7 +493,7 @@ function PackageCard({
|
||||
pkg={pkg}
|
||||
variant="endcustomer"
|
||||
highlight={pkg.id === highlightEndcustomerId}
|
||||
onSelect={handleCardClick}
|
||||
onSelect={(pkg) => handleCardClick(pkg, 'endcustomer')}
|
||||
className="h-full"
|
||||
/>
|
||||
</CarouselItem>
|
||||
@@ -465,7 +511,7 @@ function PackageCard({
|
||||
pkg={pkg}
|
||||
variant="endcustomer"
|
||||
highlight={pkg.id === highlightEndcustomerId}
|
||||
onSelect={handleCardClick}
|
||||
onSelect={(pkg) => handleCardClick(pkg, 'endcustomer')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -615,7 +661,7 @@ function PackageCard({
|
||||
pkg={pkg}
|
||||
variant="reseller"
|
||||
highlight={pkg.id === highlightResellerId}
|
||||
onSelect={handleCardClick}
|
||||
onSelect={(pkg) => handleCardClick(pkg, 'reseller')}
|
||||
className="h-full"
|
||||
/>
|
||||
</CarouselItem>
|
||||
@@ -633,7 +679,7 @@ function PackageCard({
|
||||
pkg={pkg}
|
||||
variant="reseller"
|
||||
highlight={pkg.id === highlightResellerId}
|
||||
onSelect={handleCardClick}
|
||||
onSelect={(pkg) => handleCardClick(pkg, 'reseller')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -715,15 +761,20 @@ function PackageCard({
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<Tabs value={currentStep} onValueChange={setCurrentStep} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 rounded-full bg-white/60 p-1 text-sm shadow-sm dark:bg-gray-900/60">
|
||||
<TabsTrigger className="rounded-full" value="step1">{t('packages.details')}</TabsTrigger>
|
||||
<TabsTrigger className="rounded-full" value="step2">{t('packages.customer_opinions')}</TabsTrigger>
|
||||
<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="step1" className="mt-6 space-y-6">
|
||||
<TabsContent value="overview" className="mt-6 space-y-6">
|
||||
{(() => {
|
||||
const accent = getAccentTheme(selectedVariant);
|
||||
const metrics = resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon);
|
||||
const descriptionEntries = selectedPackage.description_breakdown ?? [];
|
||||
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)]">
|
||||
@@ -740,64 +791,177 @@ function PackageCard({
|
||||
'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 space-y-4">
|
||||
<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.features_label')}
|
||||
</Badge>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedPackage.features.map((feature) => (
|
||||
<Badge
|
||||
key={feature}
|
||||
variant="outline"
|
||||
className="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 dark:bg-gray-800/70 dark:text-gray-200"
|
||||
<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"
|
||||
>
|
||||
{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>
|
||||
{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="space-y-6">
|
||||
{descriptionEntries.length > 0 && (
|
||||
<div className="rounded-3xl border border-gray-200/80 bg-white/90 p-6 shadow-xl dark:border-gray-700/70 dark:bg-gray-900/90">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.3em] text-gray-400 dark:text-gray-500">
|
||||
{t('packages.breakdown_label')}
|
||||
</h3>
|
||||
<div className="mt-4 grid gap-3">
|
||||
{descriptionEntries.map((entry, index) => (
|
||||
<div
|
||||
key={`${entry.title}-${index}`}
|
||||
className="rounded-2xl bg-gradient-to-r from-rose-50/90 via-white to-white p-4 shadow-sm dark:from-gray-800 dark:via-gray-900 dark:to-gray-900"
|
||||
>
|
||||
{entry.title && (
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-rose-500 dark:text-rose-200">
|
||||
{entry.title}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">{entry.value}</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 className="rounded-3xl border border-gray-200/80 bg-white/90 p-6 shadow-xl dark:border-gray-700/70 dark:bg-gray-900/90">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.3em] text-gray-400 dark:text-gray-500">
|
||||
</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
|
||||
@@ -811,41 +975,59 @@ function PackageCard({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<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={() => {
|
||||
localStorage.setItem('preferred_package', JSON.stringify(selectedPackage));
|
||||
}}
|
||||
>
|
||||
{t('packages.to_order')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-xs text-center text-gray-500 dark:text-gray-400">
|
||||
{t('packages.order_hint')}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
{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="step2" className="mt-6">
|
||||
<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="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
Reference in New Issue
Block a user