From c1bd4c1eb30d05ca0fe1a54bed87c469f9edf29b Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Dec 2025 17:02:03 +0100 Subject: [PATCH] noscript varianten eingebaut, matomo integration erweitert und als konfiguration aufgenommen. --- .env.example | 7 ++ .../Controllers/Api/EventPublicController.php | 17 ++++- app/Providers/AppServiceProvider.php | 4 +- config/services.php | 8 ++ .../dashboard/DashboardEventFocusCard.tsx | 24 ------ .../js/admin/i18n/locales/de/common.json | 4 +- .../js/admin/i18n/locales/en/common.json | 4 +- resources/js/admin/main.tsx | 53 +++++++------ resources/js/admin/pages/DashboardPage.tsx | 24 +----- .../js/components/analytics/MatomoTracker.tsx | 31 ++++++-- resources/js/guest/main.tsx | 3 + resources/views/admin.blade.php | 61 +++++++++++++++ resources/views/app.blade.php | 47 ++++++++++++ resources/views/guest.blade.php | 74 +++++++++++++++++++ resources/views/layouts/marketing.blade.php | 65 +++++++++++++--- 15 files changed, 331 insertions(+), 95 deletions(-) diff --git a/.env.example b/.env.example index 0567a0c..f3cf3a5 100644 --- a/.env.example +++ b/.env.example @@ -132,6 +132,13 @@ PHOTOBOOTH_IMPORT_ROOT=/var/www/storage/app/photobooth PHOTOBOOTH_IMPORT_MAX_FILES=50 PHOTOBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp +# Matomo Analytics +MATOMO_ENABLED=false +MATOMO_URL= +MATOMO_SITE_ID_MARKETING= +MATOMO_SITE_ID_GUEST= +MATOMO_SITE_ID_ADMIN= + DOKPLOY_API_BASE_URL= DOKPLOY_API_KEY= DOKPLOY_WEB_URL= diff --git a/app/Http/Controllers/Api/EventPublicController.php b/app/Http/Controllers/Api/EventPublicController.php index 31da30e..791abd1 100644 --- a/app/Http/Controllers/Api/EventPublicController.php +++ b/app/Http/Controllers/Api/EventPublicController.php @@ -463,10 +463,17 @@ class EventPublicController extends BaseController $fallbacks = $this->localeFallbackChain($locale, $eventDefaultLocale); $rows = DB::table('tasks') - ->join('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id') - ->join('event_task_collection', 'task_collection_task.task_collection_id', '=', 'event_task_collection.task_collection_id') + ->leftJoin('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id') + ->leftJoin('event_task_collection', 'task_collection_task.task_collection_id', '=', 'event_task_collection.task_collection_id') + ->leftJoin('event_task', function ($join) use ($eventId) { + $join->on('tasks.id', '=', 'event_task.task_id') + ->where('event_task.event_id', '=', $eventId); + }) ->leftJoin('emotions', 'tasks.emotion_id', '=', 'emotions.id') - ->where('event_task_collection.event_id', $eventId) + ->where(function ($query) use ($eventId) { + $query->where('event_task_collection.event_id', $eventId) + ->orWhereNotNull('event_task.event_id'); + }) ->select([ 'tasks.id', 'tasks.title', @@ -474,10 +481,12 @@ class EventPublicController extends BaseController 'tasks.example_text', 'tasks.emotion_id', 'tasks.sort_order', + 'event_task.sort_order as direct_sort_order', + 'event_task_collection.sort_order as collection_sort_order', 'emotions.name as emotion_name', 'emotions.id as emotion_lookup_id', ]) - ->orderBy('event_task_collection.sort_order') + ->orderByRaw('COALESCE(event_task_collection.sort_order, event_task.sort_order, tasks.sort_order, 0)') ->orderBy('tasks.sort_order') ->limit(20) ->get(); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f402849..e402945 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -182,7 +182,7 @@ class AppServiceProvider extends ServiceProvider Inertia::share('analytics', static function () { $config = config('services.matomo'); - if (! ($config['enabled'] ?? false)) { + if (! ($config['enabled'] ?? false) || empty($config['url']) || empty($config['site_id_marketing'])) { return [ 'matomo' => [ 'enabled' => false, @@ -194,7 +194,7 @@ class AppServiceProvider extends ServiceProvider 'matomo' => [ 'enabled' => true, 'url' => rtrim((string) ($config['url'] ?? ''), '/'), - 'siteId' => (string) ($config['site_id'] ?? ''), + 'siteId' => (string) ($config['site_id_marketing'] ?? ''), ], ]; }); diff --git a/config/services.php b/config/services.php index 92e4337..2183765 100644 --- a/config/services.php +++ b/config/services.php @@ -64,6 +64,14 @@ return [ 'redirect' => env('GOOGLE_REDIRECT_URI', rtrim(env('APP_URL', ''), '/').'/checkout/auth/google/callback'), ], + 'matomo' => [ + 'enabled' => env('MATOMO_ENABLED', false), + 'url' => env('MATOMO_URL'), + 'site_id_marketing' => env('MATOMO_SITE_ID_MARKETING'), + 'site_id_guest' => env('MATOMO_SITE_ID_GUEST'), + 'site_id_admin' => env('MATOMO_SITE_ID_ADMIN'), + ], + 'revenuecat' => [ 'webhook' => env('REVENUECAT_WEBHOOK_SECRET', ''), 'product_mappings' => env('REVENUECAT_PRODUCT_MAPPINGS', ''), diff --git a/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx b/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx index 11576a0..c329d2a 100644 --- a/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx +++ b/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx @@ -200,30 +200,6 @@ export function DashboardEventFocusCard({ - {limitWarnings.length > 0 && ( -
- {limitWarnings.map((warning) => ( - - - - {t(`limitWarnings.${warning.scope}`, { - defaultValue: - warning.scope === 'photos' - ? 'Fotos' - : warning.scope === 'guests' - ? 'Gäste' - : 'Galerie', - })} - - {warning.message} - - ))} -
- )} ); } diff --git a/resources/js/admin/i18n/locales/de/common.json b/resources/js/admin/i18n/locales/de/common.json index 18cc8bd..a9e8e8f 100644 --- a/resources/js/admin/i18n/locales/de/common.json +++ b/resources/js/admin/i18n/locales/de/common.json @@ -71,10 +71,10 @@ }, "limits": { "photosTitle": "Foto-Limit", - "photosWarning": "Nur noch {remaining} von {limit} Foto-Uploads verfügbar.", + "photosWarning": "Nur noch {{remaining}} von {{limit}} Foto-Uploads verfügbar.", "photosBlocked": "Foto-Uploads sind blockiert. Bitte Paket upgraden oder erweitern.", "guestsTitle": "Gäste-Limit", - "guestsWarning": "Nur noch {remaining} von {limit} Gästelinks verfügbar.", + "guestsWarning": "Nur noch {{remaining}} von {{limit}} Gästelinks verfügbar.", "guestsBlocked": "Alle Gästeplätze sind belegt. Upgrade euer Paket oder erweitert das Kontingent, um weitere Gäste zuzulassen.", "galleryTitle": "Galerie", "galleryWarningDay": "Galerie läuft in {days} Tag ab.", diff --git a/resources/js/admin/i18n/locales/en/common.json b/resources/js/admin/i18n/locales/en/common.json index c9f60dd..a007a2c 100644 --- a/resources/js/admin/i18n/locales/en/common.json +++ b/resources/js/admin/i18n/locales/en/common.json @@ -71,10 +71,10 @@ }, "limits": { "photosTitle": "Photo limit", - "photosWarning": "Only {remaining} of {limit} photo uploads remaining.", + "photosWarning": "Only {{remaining}} of {{limit}} photo uploads remaining.", "photosBlocked": "Photo uploads are blocked. Please upgrade or extend your package.", "guestsTitle": "Guest limit", - "guestsWarning": "Only {remaining} of {limit} guest invites remaining.", + "guestsWarning": "Only {{remaining}} of {{limit}} guest invites remaining.", "guestsBlocked": "All guest slots are filled. Upgrade your package or extend the quota to welcome more guests.", "galleryTitle": "Gallery", "galleryWarningDay": "Gallery expires in {days} day.", diff --git a/resources/js/admin/main.tsx b/resources/js/admin/main.tsx index 879e1ce..52bf486 100644 --- a/resources/js/admin/main.tsx +++ b/resources/js/admin/main.tsx @@ -11,6 +11,9 @@ import './dev-tools'; import { initializeTheme } from '@/hooks/use-appearance'; import { OnboardingProgressProvider } from './onboarding'; import { EventProvider } from './context/EventContext'; +import MatomoTracker from '@/components/analytics/MatomoTracker'; +import { ConsentProvider } from '@/contexts/consent'; +import CookieBanner from '@/components/consent/CookieBanner'; const DevTenantSwitcher = React.lazy(() => import('./components/DevTenantSwitcher')); @@ -40,28 +43,32 @@ if ('serviceWorker' in navigator) { createRoot(rootEl).render( - - - - - - Oberfläche wird geladen … - - )} - > - - - - - - - {enableDevSwitcher ? ( - - - - ) : null} - + + + + + + + + Oberfläche wird geladen … + + )} + > + + + + + + + + {enableDevSwitcher ? ( + + + + ) : null} + + ); diff --git a/resources/js/admin/pages/DashboardPage.tsx b/resources/js/admin/pages/DashboardPage.tsx index 0c6b13e..b62d2b5 100644 --- a/resources/js/admin/pages/DashboardPage.tsx +++ b/resources/js/admin/pages/DashboardPage.tsx @@ -13,8 +13,6 @@ import { ClipboardList, Package as PackageIcon, } from 'lucide-react'; -import toast from 'react-hot-toast'; - import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -254,20 +252,7 @@ export default function DashboardPage() { const shownToastsRef = React.useRef>(new Set()); - React.useEffect(() => { - limitWarnings.forEach((warning) => { - const toastKey = `${warning.id}-${warning.message}`; - if (shownToastsRef.current.has(toastKey)) { - return; - } - - shownToastsRef.current.add(toastKey); - toast(warning.message, { - icon: warning.tone === 'danger' ? '🚨' : '⚠️', - id: toastKey, - }); - }); - }, [limitWarnings]); + // Limit warnings werden ausschließlich in der Limits-Karte angezeigt; keine Toasts mehr const limitScopeLabels = React.useMemo( () => ({ @@ -533,13 +518,6 @@ export default function DashboardPage() { return ( - {errorMessage && ( - - {t('dashboard.alerts.errorTitle')} - {errorMessage} - - )} - {loading ? ( ) : ( diff --git a/resources/js/components/analytics/MatomoTracker.tsx b/resources/js/components/analytics/MatomoTracker.tsx index b9d2ea4..278865f 100644 --- a/resources/js/components/analytics/MatomoTracker.tsx +++ b/resources/js/components/analytics/MatomoTracker.tsx @@ -21,10 +21,29 @@ interface MatomoTrackerProps { } const MatomoTracker: React.FC = ({ config }) => { - const page = usePage<{ security?: { csp?: { scriptNonce?: string } } }>(); - const { hasConsent } = useConsent(); - const scriptNonce = (page.props.security as { csp?: { scriptNonce?: string } } | undefined)?.csp?.scriptNonce; - const analyticsConsent = hasConsent('analytics'); + let scriptNonce: string | undefined; + let analyticsConsent = false; + let pageUrl = typeof window !== 'undefined' ? window.location.pathname + window.location.search : ''; + + try { + const page = usePage<{ security?: { csp?: { scriptNonce?: string } } }>(); + scriptNonce = (page.props.security as { csp?: { scriptNonce?: string } } | undefined)?.csp?.scriptNonce; + pageUrl = page.url || pageUrl; + } catch { + // Not in Inertia context; fall back to window values/meta tags + const metaNonce = typeof document !== 'undefined' + ? document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') + : undefined; + scriptNonce = metaNonce ?? undefined; + } + + try { + const consent = useConsent(); + analyticsConsent = consent.hasConsent('analytics'); + } catch { + // No consent provider available; default to no analytics + analyticsConsent = false; + } useEffect(() => { if (!config?.enabled || !config.url || !config.siteId || typeof window === 'undefined') { @@ -92,14 +111,14 @@ const MatomoTracker: React.FC = ({ config }) => { window._paq = window._paq || []; const { _paq } = window; const currentUrl = - typeof window !== 'undefined' ? `${window.location.origin}${page.url}` : page.url; + typeof window !== 'undefined' ? `${window.location.origin}${pageUrl}` : pageUrl; _paq.push(['setCustomUrl', currentUrl]); if (typeof document !== 'undefined') { _paq.push(['setDocumentTitle', document.title]); } _paq.push(['trackPageView']); - }, [config, analyticsConsent, page.url]); + }, [config, analyticsConsent, pageUrl]); if (!config?.enabled || !config.url || !config.siteId || !analyticsConsent) { return null; diff --git a/resources/js/guest/main.tsx b/resources/js/guest/main.tsx index 32770a0..f5a95cc 100644 --- a/resources/js/guest/main.tsx +++ b/resources/js/guest/main.tsx @@ -25,6 +25,8 @@ const appRoot = async () => { const { router } = await import('./router'); const { ToastProvider } = await import('./components/ToastHost'); const { LocaleProvider } = await import('./i18n/LocaleContext'); + const { default: MatomoTracker } = await import('@/components/analytics/MatomoTracker'); + const matomoConfig = (window as any).__MATOMO_GUEST__ as { enabled?: boolean; url?: string; siteId?: string }; // Register a minimal service worker for background sync (best-effort) if ('serviceWorker' in navigator) { @@ -45,6 +47,7 @@ const appRoot = async () => { + diff --git a/resources/views/admin.blade.php b/resources/views/admin.blade.php index f49ab7b..9103aee 100644 --- a/resources/views/admin.blade.php +++ b/resources/views/admin.blade.php @@ -13,8 +13,69 @@ @viteReactRefresh @vite(['resources/css/app.css', 'resources/js/admin/main.tsx']) + @php + $matomoConfig = config('services.matomo'); + $matomoAdmin = ($matomoConfig['enabled'] ?? false) && !empty($matomoConfig['url']) && !empty($matomoConfig['site_id_admin']) + ? [ + 'enabled' => true, + 'url' => rtrim($matomoConfig['url'], '/'), + 'siteId' => (string) $matomoConfig['site_id_admin'], + ] + : ['enabled' => false]; + @endphp + + @php + $noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de'; + @endphp +
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 097e3b3..35b6f67 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -41,6 +41,53 @@ @inertiaHead + @php + $noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de'; + @endphp + @inertia diff --git a/resources/views/guest.blade.php b/resources/views/guest.blade.php index 715fee9..f2bce2f 100644 --- a/resources/views/guest.blade.php +++ b/resources/views/guest.blade.php @@ -15,12 +15,86 @@ 'vapidPublicKey' => config('push.vapid.public_key'), ], ]; + $matomoConfig = config('services.matomo'); + $matomoGuest = ($matomoConfig['enabled'] ?? false) && !empty($matomoConfig['url']) && !empty($matomoConfig['site_id_guest']) + ? [ + 'enabled' => true, + 'url' => rtrim($matomoConfig['url'], '/'), + 'siteId' => (string) $matomoConfig['site_id_guest'], + ] + : ['enabled' => false]; @endphp + @php + $noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de'; + @endphp +
diff --git a/resources/views/layouts/marketing.blade.php b/resources/views/layouts/marketing.blade.php index baf56d1..bf4a965 100644 --- a/resources/views/layouts/marketing.blade.php +++ b/resources/views/layouts/marketing.blade.php @@ -33,16 +33,63 @@ + @php + $noscriptLocale = in_array($currentLocale ?? app()->getLocale(), ['de', 'en'], true) ? ($currentLocale ?? app()->getLocale()) : 'de'; + @endphp