Files
fotospiel-app/resources/js/components/consent/CookieBanner.tsx
Codex Agent a949c8d3af - 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.
2025-10-19 11:41:03 +02:00

176 lines
6.1 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
import { useConsent, ConsentPreferences } from '@/contexts/consent';
const CookieBanner: React.FC = () => {
const { t } = useTranslation('common');
const {
showBanner,
acceptAll,
rejectAll,
preferences,
savePreferences,
isPreferencesOpen,
openPreferences,
closePreferences,
} = useConsent();
const [draftPreferences, setDraftPreferences] = useState<ConsentPreferences>(preferences);
useEffect(() => {
if (isPreferencesOpen) {
setDraftPreferences(preferences);
}
}, [isPreferencesOpen, preferences]);
const analyticsDescription = useMemo(
() => t('consent.modal.analytics_desc'),
[t],
);
const handleSave = () => {
savePreferences({ analytics: draftPreferences.analytics });
};
const handleOpenChange = (open: boolean) => {
if (open) {
openPreferences();
return;
}
closePreferences();
};
return (
<>
{showBanner && !isPreferencesOpen && (
<div
className="fixed inset-x-0 bottom-0 z-40 px-4 pb-6 sm:px-6"
role="region"
aria-label={t('consent.accessibility.banner_label')}
>
<div className="mx-auto max-w-5xl rounded-3xl border border-gray-200 bg-white/95 p-6 shadow-2xl backdrop-blur-md dark:border-gray-700 dark:bg-gray-900/95">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('consent.banner.title')}
</h2>
<p className="text-sm text-gray-600 dark:text-gray-300">
{t('consent.banner.body')}
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<Button
variant="outline"
className="min-w-[140px]"
onClick={rejectAll}
>
{t('consent.banner.reject')}
</Button>
<Button
variant="secondary"
className="min-w-[140px]"
onClick={openPreferences}
>
{t('consent.banner.customize')}
</Button>
<Button className="min-w-[140px]" onClick={acceptAll}>
{t('consent.banner.accept')}
</Button>
</div>
</div>
</div>
</div>
)}
<Dialog open={isPreferencesOpen} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-xl md:max-w-2xl">
<DialogHeader>
<DialogTitle>{t('consent.modal.title')}</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
{t('consent.modal.description')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="rounded-2xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-700 dark:bg-gray-800/60">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-gray-900 dark:text-white">
{t('consent.modal.functional')}
</p>
<p className="text-xs text-muted-foreground">
{t('consent.modal.functional_desc')}
</p>
</div>
<span className="rounded-full bg-gray-200 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{t('consent.modal.required')}
</span>
</div>
</div>
<div className="rounded-2xl border border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-gray-900 dark:text-white">
{t('consent.modal.analytics')}
</p>
<p className="text-xs text-muted-foreground">{analyticsDescription}</p>
</div>
<Switch
checked={draftPreferences.analytics}
onCheckedChange={(checked) =>
setDraftPreferences((prev) => ({
...prev,
analytics: checked,
functional: true,
}))
}
aria-label={t('consent.modal.analytics')}
/>
</div>
</div>
</div>
<Separator />
<DialogFooter className="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex w-full flex-wrap gap-2 sm:w-auto">
<Button type="button" variant="outline" onClick={rejectAll}>
{t('consent.modal.reject_all')}
</Button>
<Button type="button" variant="secondary" onClick={acceptAll}>
{t('consent.modal.accept_all')}
</Button>
</div>
<div className="flex w-full flex-wrap gap-2 sm:w-auto sm:justify-end">
<Button
type="button"
variant="ghost"
onClick={() => closePreferences()}
>
{t('consent.modal.cancel')}
</Button>
<Button type="button" onClick={handleSave}>
{t('consent.modal.save')}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
export default CookieBanner;