- 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:
Codex Agent
2025-10-19 11:41:03 +02:00
parent ae9b9160ac
commit a949c8d3af
113 changed files with 5169 additions and 712 deletions

View File

@@ -1,5 +1,6 @@
import React, { useMemo, useState } from "react";
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Check, Package as PackageIcon, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -13,14 +14,19 @@ const currencyFormatter = new Intl.NumberFormat("de-DE", {
minimumFractionDigits: 2,
});
function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
function translateFeature(feature: string, t: TFunction<'marketing'>) {
const fallback = feature.replace(/_/g, ' ');
return t(`packages.feature_${feature}`, { defaultValue: fallback });
}
function PackageSummary({ pkg, t }: { pkg: CheckoutPackage; t: TFunction<'marketing'> }) {
const isFree = pkg.price === 0;
return (
<Card className={`shadow-sm ${isFree ? "opacity-75" : ""}`}>
<Card className={`shadow-sm ${isFree ? 'opacity-75' : ''}`}>
<CardHeader className="space-y-1">
<CardTitle className={`flex items-center gap-3 text-2xl ${isFree ? "text-muted-foreground" : ""}`}>
<PackageIcon className={`h-6 w-6 ${isFree ? "text-muted-foreground" : "text-primary"}`} />
<CardTitle className={`flex items-center gap-3 text-2xl ${isFree ? 'text-muted-foreground' : ''}`}>
<PackageIcon className={`h-6 w-6 ${isFree ? 'text-muted-foreground' : 'text-primary'}`} />
{pkg.name}
</CardTitle>
<CardDescription className="text-base text-muted-foreground">
@@ -29,19 +35,41 @@ function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-baseline gap-2">
<span className={`text-3xl font-semibold ${isFree ? "text-muted-foreground" : ""}`}>
{pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)}
<span className={`text-3xl font-semibold ${isFree ? 'text-muted-foreground' : ''}`}>
{pkg.price === 0 ? t('packages.free') : currencyFormatter.format(pkg.price)}
</span>
<Badge variant={isFree ? "outline" : "secondary"} className="uppercase tracking-wider text-xs">
{pkg.type === "reseller" ? "Reseller" : "Endkunde"}
<Badge variant={isFree ? 'outline' : 'secondary'} className="uppercase tracking-wider text-xs">
{pkg.type === 'reseller' ? t('packages.subscription') : t('packages.one_time')}
</Badge>
</div>
{pkg.gallery_duration_label && (
<div className="rounded-md border border-dashed border-muted px-3 py-2 text-sm text-muted-foreground">
{t('packages.gallery_days_label')}: {pkg.gallery_duration_label}
</div>
)}
{Array.isArray(pkg.description_breakdown) && pkg.description_breakdown.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{t('packages.breakdown_label')}
</h4>
<div className="grid gap-3 md:grid-cols-2">
{pkg.description_breakdown.map((row, index) => (
<div key={index} className="rounded-lg border border-muted/40 bg-muted/20 px-3 py-2">
{row.title && (
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{row.title}</p>
)}
<p className="text-sm text-muted-foreground">{row.value}</p>
</div>
))}
</div>
</div>
)}
{Array.isArray(pkg.features) && pkg.features.length > 0 && (
<ul className="space-y-3">
{pkg.features.map((feature, index) => (
<li key={index} className="flex items-start gap-3 text-sm text-muted-foreground">
<Check className={`mt-0.5 h-4 w-4 ${isFree ? "text-muted-foreground" : "text-primary"}`} />
<span>{feature}</span>
<Check className={`mt-0.5 h-4 w-4 ${isFree ? 'text-muted-foreground' : 'text-primary'}`} />
<span>{translateFeature(feature, t)}</span>
</li>
))}
</ul>
@@ -51,7 +79,7 @@ function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
);
}
function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void }) {
function PackageOption({ pkg, isActive, onSelect, t }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void; t: TFunction<'marketing'> }) {
const isFree = pkg.price === 0;
return (
@@ -69,7 +97,7 @@ function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isAc
<div className="flex items-center justify-between text-sm font-medium">
<span className={isFree ? "text-muted-foreground" : ""}>{pkg.name}</span>
<span className={isFree ? "text-muted-foreground font-normal" : "text-muted-foreground"}>
{pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)}
{pkg.price === 0 ? t('packages.free') : currencyFormatter.format(pkg.price)}
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{pkg.description}</p>
@@ -125,7 +153,7 @@ export const PackageStep: React.FC = () => {
return (
<div className="grid gap-8 lg:grid-cols-[2fr_1fr]">
<div className="space-y-6">
<PackageSummary pkg={selectedPackage} />
<PackageSummary pkg={selectedPackage} t={t} />
<div className="flex justify-end">
<Button size="lg" onClick={handleNextStep} disabled={isLoading}>
{isLoading ? (
@@ -150,6 +178,7 @@ export const PackageStep: React.FC = () => {
pkg={pkg}
isActive={pkg.id === selectedPackage.id}
onSelect={() => handlePackageChange(pkg)}
t={t}
/>
))}
{comparablePackages.length === 0 && (