- 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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user