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.
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { Head, usePage, router } from '@inertiajs/react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import MatomoTracker, { MatomoConfig } from '@/components/analytics/MatomoTracker';
|
|
import CookieBanner from '@/components/consent/CookieBanner';
|
|
|
|
interface MarketingLayoutProps {
|
|
children: React.ReactNode;
|
|
title?: string;
|
|
}
|
|
|
|
const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) => {
|
|
const page = usePage<{
|
|
translations?: Record<string, Record<string, string>>;
|
|
locale?: string;
|
|
analytics?: { matomo?: MatomoConfig };
|
|
}>();
|
|
const { url } = page;
|
|
const { t } = useTranslation('marketing');
|
|
const i18n = useTranslation();
|
|
const { locale, analytics } = page.props;
|
|
|
|
useEffect(() => {
|
|
if (locale && i18n.i18n.language !== locale) {
|
|
i18n.i18n.changeLanguage(locale);
|
|
}
|
|
}, [locale, i18n]);
|
|
|
|
const marketing = page.props.translations?.marketing ?? {};
|
|
|
|
const getString = (key: string, fallback: string) => {
|
|
const value = marketing[key];
|
|
return typeof value === 'string' ? value : fallback;
|
|
};
|
|
|
|
const activeLocale = locale || 'de';
|
|
const alternateLocale = activeLocale === 'de' ? 'en' : 'de';
|
|
const path = url.replace(/^\/(de|en)/, '');
|
|
const canonicalUrl = `https://fotospiel.app${path || '/'}`;
|
|
|
|
const handleLocaleChange = (nextLocale: string) => {
|
|
router.post('/set-locale', { locale: nextLocale }, {
|
|
preserveState: true,
|
|
replace: true,
|
|
onSuccess: () => {
|
|
i18n.i18n.changeLanguage(nextLocale);
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{title || t('meta.title', getString('title', 'Fotospiel'))}</title>
|
|
<meta
|
|
name="description"
|
|
content={t('meta.description', getString('description', 'Sammle Gastfotos für Events mit QR-Codes'))}
|
|
/>
|
|
<meta property="og:title" content={title || t('meta.title', getString('title', 'Fotospiel'))} />
|
|
<meta
|
|
property="og:description"
|
|
content={t('meta.description', getString('description', 'Sammle Gastfotos für Events mit QR-Codes'))}
|
|
/>
|
|
<meta property="og:url" content={canonicalUrl} />
|
|
<link rel="canonical" href={canonicalUrl} />
|
|
<link rel="alternate" hrefLang="x-default" href="https://fotospiel.app/" />
|
|
</Head>
|
|
<MatomoTracker config={analytics?.matomo} />
|
|
<CookieBanner />
|
|
<div className="min-h-screen bg-white">
|
|
<header className="bg-white shadow-sm">
|
|
<div className="container mx-auto px-4 py-4">
|
|
|
|
</div>
|
|
</header>
|
|
<main>
|
|
{children}
|
|
</main>
|
|
{/* Footer kommt von Footer.tsx */}
|
|
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default MarketingLayout;
|