noscript varianten eingebaut, matomo integration erweitert und als konfiguration aufgenommen.
This commit is contained in:
@@ -132,6 +132,13 @@ PHOTOBOOTH_IMPORT_ROOT=/var/www/storage/app/photobooth
|
|||||||
PHOTOBOOTH_IMPORT_MAX_FILES=50
|
PHOTOBOOTH_IMPORT_MAX_FILES=50
|
||||||
PHOTOBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp
|
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_BASE_URL=
|
||||||
DOKPLOY_API_KEY=
|
DOKPLOY_API_KEY=
|
||||||
DOKPLOY_WEB_URL=
|
DOKPLOY_WEB_URL=
|
||||||
|
|||||||
@@ -463,10 +463,17 @@ class EventPublicController extends BaseController
|
|||||||
$fallbacks = $this->localeFallbackChain($locale, $eventDefaultLocale);
|
$fallbacks = $this->localeFallbackChain($locale, $eventDefaultLocale);
|
||||||
|
|
||||||
$rows = DB::table('tasks')
|
$rows = DB::table('tasks')
|
||||||
->join('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id')
|
->leftJoin('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('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')
|
->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([
|
->select([
|
||||||
'tasks.id',
|
'tasks.id',
|
||||||
'tasks.title',
|
'tasks.title',
|
||||||
@@ -474,10 +481,12 @@ class EventPublicController extends BaseController
|
|||||||
'tasks.example_text',
|
'tasks.example_text',
|
||||||
'tasks.emotion_id',
|
'tasks.emotion_id',
|
||||||
'tasks.sort_order',
|
'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.name as emotion_name',
|
||||||
'emotions.id as emotion_lookup_id',
|
'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')
|
->orderBy('tasks.sort_order')
|
||||||
->limit(20)
|
->limit(20)
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
Inertia::share('analytics', static function () {
|
Inertia::share('analytics', static function () {
|
||||||
$config = config('services.matomo');
|
$config = config('services.matomo');
|
||||||
|
|
||||||
if (! ($config['enabled'] ?? false)) {
|
if (! ($config['enabled'] ?? false) || empty($config['url']) || empty($config['site_id_marketing'])) {
|
||||||
return [
|
return [
|
||||||
'matomo' => [
|
'matomo' => [
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
@@ -194,7 +194,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
'matomo' => [
|
'matomo' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'url' => rtrim((string) ($config['url'] ?? ''), '/'),
|
'url' => rtrim((string) ($config['url'] ?? ''), '/'),
|
||||||
'siteId' => (string) ($config['site_id'] ?? ''),
|
'siteId' => (string) ($config['site_id_marketing'] ?? ''),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ return [
|
|||||||
'redirect' => env('GOOGLE_REDIRECT_URI', rtrim(env('APP_URL', ''), '/').'/checkout/auth/google/callback'),
|
'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' => [
|
'revenuecat' => [
|
||||||
'webhook' => env('REVENUECAT_WEBHOOK_SECRET', ''),
|
'webhook' => env('REVENUECAT_WEBHOOK_SECRET', ''),
|
||||||
'product_mappings' => env('REVENUECAT_PRODUCT_MAPPINGS', ''),
|
'product_mappings' => env('REVENUECAT_PRODUCT_MAPPINGS', ''),
|
||||||
|
|||||||
@@ -200,30 +200,6 @@ export function DashboardEventFocusCard({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{limitWarnings.length > 0 && (
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
{limitWarnings.map((warning) => (
|
|
||||||
<Alert
|
|
||||||
key={warning.id}
|
|
||||||
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
|
|
||||||
className={warning.tone === 'warning' ? 'border-amber-400/50 bg-amber-50 text-amber-900 dark:border-amber-300/40 dark:bg-amber-500/10 dark:text-amber-100' : undefined}
|
|
||||||
>
|
|
||||||
<AlertTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
||||||
<AlertTriangle className="h-4 w-4" />
|
|
||||||
{t(`limitWarnings.${warning.scope}`, {
|
|
||||||
defaultValue:
|
|
||||||
warning.scope === 'photos'
|
|
||||||
? 'Fotos'
|
|
||||||
: warning.scope === 'guests'
|
|
||||||
? 'Gäste'
|
|
||||||
: 'Galerie',
|
|
||||||
})}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-sm">{warning.message}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,10 @@
|
|||||||
},
|
},
|
||||||
"limits": {
|
"limits": {
|
||||||
"photosTitle": "Foto-Limit",
|
"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.",
|
"photosBlocked": "Foto-Uploads sind blockiert. Bitte Paket upgraden oder erweitern.",
|
||||||
"guestsTitle": "Gäste-Limit",
|
"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.",
|
"guestsBlocked": "Alle Gästeplätze sind belegt. Upgrade euer Paket oder erweitert das Kontingent, um weitere Gäste zuzulassen.",
|
||||||
"galleryTitle": "Galerie",
|
"galleryTitle": "Galerie",
|
||||||
"galleryWarningDay": "Galerie läuft in {days} Tag ab.",
|
"galleryWarningDay": "Galerie läuft in {days} Tag ab.",
|
||||||
|
|||||||
@@ -71,10 +71,10 @@
|
|||||||
},
|
},
|
||||||
"limits": {
|
"limits": {
|
||||||
"photosTitle": "Photo limit",
|
"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.",
|
"photosBlocked": "Photo uploads are blocked. Please upgrade or extend your package.",
|
||||||
"guestsTitle": "Guest limit",
|
"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.",
|
"guestsBlocked": "All guest slots are filled. Upgrade your package or extend the quota to welcome more guests.",
|
||||||
"galleryTitle": "Gallery",
|
"galleryTitle": "Gallery",
|
||||||
"galleryWarningDay": "Gallery expires in {days} day.",
|
"galleryWarningDay": "Gallery expires in {days} day.",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import './dev-tools';
|
|||||||
import { initializeTheme } from '@/hooks/use-appearance';
|
import { initializeTheme } from '@/hooks/use-appearance';
|
||||||
import { OnboardingProgressProvider } from './onboarding';
|
import { OnboardingProgressProvider } from './onboarding';
|
||||||
import { EventProvider } from './context/EventContext';
|
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'));
|
const DevTenantSwitcher = React.lazy(() => import('./components/DevTenantSwitcher'));
|
||||||
|
|
||||||
@@ -40,28 +43,32 @@ if ('serviceWorker' in navigator) {
|
|||||||
|
|
||||||
createRoot(rootEl).render(
|
createRoot(rootEl).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ConsentProvider>
|
||||||
<AuthProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<EventProvider>
|
<AuthProvider>
|
||||||
<OnboardingProgressProvider>
|
<EventProvider>
|
||||||
<Suspense
|
<OnboardingProgressProvider>
|
||||||
fallback={(
|
<MatomoTracker config={(window as any).__MATOMO_ADMIN__} />
|
||||||
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
<Suspense
|
||||||
Oberfläche wird geladen …
|
fallback={(
|
||||||
</div>
|
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||||
)}
|
Oberfläche wird geladen …
|
||||||
>
|
</div>
|
||||||
<RouterProvider router={router} />
|
)}
|
||||||
</Suspense>
|
>
|
||||||
</OnboardingProgressProvider>
|
<RouterProvider router={router} />
|
||||||
</EventProvider>
|
</Suspense>
|
||||||
</AuthProvider>
|
</OnboardingProgressProvider>
|
||||||
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
</EventProvider>
|
||||||
{enableDevSwitcher ? (
|
</AuthProvider>
|
||||||
<Suspense fallback={null}>
|
<CookieBanner />
|
||||||
<DevTenantSwitcher />
|
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||||
</Suspense>
|
{enableDevSwitcher ? (
|
||||||
) : null}
|
<Suspense fallback={null}>
|
||||||
</QueryClientProvider>
|
<DevTenantSwitcher />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ConsentProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
ClipboardList,
|
ClipboardList,
|
||||||
Package as PackageIcon,
|
Package as PackageIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -254,20 +252,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const shownToastsRef = React.useRef<Set<string>>(new Set());
|
const shownToastsRef = React.useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Limit warnings werden ausschließlich in der Limits-Karte angezeigt; keine Toasts mehr
|
||||||
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]);
|
|
||||||
|
|
||||||
const limitScopeLabels = React.useMemo(
|
const limitScopeLabels = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -533,13 +518,6 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title={adminTitle} subtitle={adminSubtitle}>
|
<AdminLayout title={adminTitle} subtitle={adminSubtitle}>
|
||||||
{errorMessage && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTitle>{t('dashboard.alerts.errorTitle')}</AlertTitle>
|
|
||||||
<AlertDescription>{errorMessage}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<DashboardSkeleton />
|
<DashboardSkeleton />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -21,10 +21,29 @@ interface MatomoTrackerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MatomoTracker: React.FC<MatomoTrackerProps> = ({ config }) => {
|
const MatomoTracker: React.FC<MatomoTrackerProps> = ({ config }) => {
|
||||||
const page = usePage<{ security?: { csp?: { scriptNonce?: string } } }>();
|
let scriptNonce: string | undefined;
|
||||||
const { hasConsent } = useConsent();
|
let analyticsConsent = false;
|
||||||
const scriptNonce = (page.props.security as { csp?: { scriptNonce?: string } } | undefined)?.csp?.scriptNonce;
|
let pageUrl = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '';
|
||||||
const analyticsConsent = hasConsent('analytics');
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!config?.enabled || !config.url || !config.siteId || typeof window === 'undefined') {
|
if (!config?.enabled || !config.url || !config.siteId || typeof window === 'undefined') {
|
||||||
@@ -92,14 +111,14 @@ const MatomoTracker: React.FC<MatomoTrackerProps> = ({ config }) => {
|
|||||||
window._paq = window._paq || [];
|
window._paq = window._paq || [];
|
||||||
const { _paq } = window;
|
const { _paq } = window;
|
||||||
const currentUrl =
|
const currentUrl =
|
||||||
typeof window !== 'undefined' ? `${window.location.origin}${page.url}` : page.url;
|
typeof window !== 'undefined' ? `${window.location.origin}${pageUrl}` : pageUrl;
|
||||||
|
|
||||||
_paq.push(['setCustomUrl', currentUrl]);
|
_paq.push(['setCustomUrl', currentUrl]);
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
_paq.push(['setDocumentTitle', document.title]);
|
_paq.push(['setDocumentTitle', document.title]);
|
||||||
}
|
}
|
||||||
_paq.push(['trackPageView']);
|
_paq.push(['trackPageView']);
|
||||||
}, [config, analyticsConsent, page.url]);
|
}, [config, analyticsConsent, pageUrl]);
|
||||||
|
|
||||||
if (!config?.enabled || !config.url || !config.siteId || !analyticsConsent) {
|
if (!config?.enabled || !config.url || !config.siteId || !analyticsConsent) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const appRoot = async () => {
|
|||||||
const { router } = await import('./router');
|
const { router } = await import('./router');
|
||||||
const { ToastProvider } = await import('./components/ToastHost');
|
const { ToastProvider } = await import('./components/ToastHost');
|
||||||
const { LocaleProvider } = await import('./i18n/LocaleContext');
|
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)
|
// Register a minimal service worker for background sync (best-effort)
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
@@ -45,6 +47,7 @@ const appRoot = async () => {
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<LocaleProvider>
|
<LocaleProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<MatomoTracker config={matomoConfig} />
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={(
|
fallback={(
|
||||||
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -13,8 +13,69 @@
|
|||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
@viteReactRefresh
|
@viteReactRefresh
|
||||||
@vite(['resources/css/app.css', 'resources/js/admin/main.tsx'])
|
@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
|
||||||
|
<script>
|
||||||
|
window.__MATOMO_ADMIN__ = {!! json_encode($matomoAdmin) !!};
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@php
|
||||||
|
$noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de';
|
||||||
|
@endphp
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#root { display: none !important; }
|
||||||
|
</style>
|
||||||
|
<div class="min-h-screen bg-slate-950 text-white" style="background:#0b1224;color:#fff;">
|
||||||
|
<div class="mx-auto flex max-w-4xl flex-col gap-10 px-6 py-14">
|
||||||
|
<header class="space-y-2">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-pink-300">Fotospiel Admin</p>
|
||||||
|
<h1 class="text-3xl font-semibold sm:text-4xl">Admin benötigt JavaScript</h1>
|
||||||
|
<p class="text-base text-white/70 sm:text-lg">Aktiviere JavaScript, um Events zu verwalten, Fotos zu prüfen und Benachrichtigungen zu senden.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur" style="border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);">
|
||||||
|
<h2 class="text-xl font-semibold text-white">Warum JS?</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>• Echtzeit-Listen für Fotos, Tasks und Emotion-Tags</li>
|
||||||
|
<li>• Upload-Status, Background-Sync und Notifications</li>
|
||||||
|
<li>• Live-Filter, Suche und Inline-Moderation</li>
|
||||||
|
<li>• Sichere OAuth2-Session mit PKCE</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur" style="border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);">
|
||||||
|
<h2 class="text-xl font-semibold text-white">Nächste Schritte</h2>
|
||||||
|
<ol class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>1) JavaScript im Browser aktivieren</li>
|
||||||
|
<li>2) Seite neu laden</li>
|
||||||
|
<li>3) Optional: Admin-App zum Homescreen hinzufügen</li>
|
||||||
|
</ol>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-3">
|
||||||
|
<a href="{{ route('marketing.contact', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full bg-pink-500 px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:bg-pink-400" style="color:#fff;text-decoration:none;background:#ec4899;">
|
||||||
|
Support kontaktieren
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('impressum', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Impressum
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('datenschutz', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Datenschutz
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -41,6 +41,53 @@
|
|||||||
@inertiaHead
|
@inertiaHead
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
|
@php
|
||||||
|
$noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de';
|
||||||
|
@endphp
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#app { display: none !important; }
|
||||||
|
</style>
|
||||||
|
<div class="min-h-screen bg-slate-950 text-white" style="background:#0b1224;color:#fff;">
|
||||||
|
<div class="mx-auto flex max-w-4xl flex-col gap-10 px-6 py-14">
|
||||||
|
<header class="space-y-2">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-pink-300">Fotospiel</p>
|
||||||
|
<h1 class="text-3xl font-semibold sm:text-4xl">Diese Anwendung benötigt JavaScript</h1>
|
||||||
|
<p class="text-base text-white/70 sm:text-lg">Aktiviere JavaScript, um die interaktive Oberfläche zu nutzen.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur" style="border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);">
|
||||||
|
<h2 class="text-xl font-semibold text-white">Was sonst fehlt</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>• Live-Navigation und Aktionen</li>
|
||||||
|
<li>• Inline-Formulare und Validierung</li>
|
||||||
|
<li>• Hintergrund-Sync und Benachrichtigungen</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur" style="border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);">
|
||||||
|
<h2 class="text-xl font-semibold text-white">Nächste Schritte</h2>
|
||||||
|
<ol class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>1) JavaScript im Browser aktivieren</li>
|
||||||
|
<li>2) Seite neu laden</li>
|
||||||
|
<li>3) Optional: Admin-App zum Homescreen hinzufügen</li>
|
||||||
|
</ol>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-3">
|
||||||
|
<a href="{{ route('marketing.contact', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full bg-pink-500 px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:bg-pink-400" style="color:#fff;text-decoration:none;background:#ec4899;">
|
||||||
|
Support kontaktieren
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('impressum', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Impressum
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('datenschutz', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Datenschutz
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
@inertia
|
@inertia
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -15,12 +15,86 @@
|
|||||||
'vapidPublicKey' => config('push.vapid.public_key'),
|
'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
|
@endphp
|
||||||
<script>
|
<script>
|
||||||
window.__GUEST_RUNTIME_CONFIG__ = {!! json_encode($guestRuntimeConfig) !!};
|
window.__GUEST_RUNTIME_CONFIG__ = {!! json_encode($guestRuntimeConfig) !!};
|
||||||
|
window.__MATOMO_GUEST__ = {!! json_encode($matomoGuest) !!};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@php
|
||||||
|
$noscriptLocale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de';
|
||||||
|
@endphp
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#root { display: none !important; }
|
||||||
|
</style>
|
||||||
|
<div class="min-h-screen bg-gradient-to-b from-[#0f172a] via-[#111827] to-[#0b1224] text-white" style="background:linear-gradient(180deg,#0f172a 0%,#111827 50%,#0b1224 100%);color:#fff;">
|
||||||
|
<div class="mx-auto flex max-w-5xl flex-col gap-12 px-6 py-14">
|
||||||
|
<header class="space-y-3 text-center">
|
||||||
|
<p class="text-sm font-semibold uppercase tracking-[0.18em] text-pink-300">Fotospiel</p>
|
||||||
|
<h1 class="text-3xl font-semibold sm:text-4xl">Diese Event-Galerie braucht JavaScript</h1>
|
||||||
|
<p class="text-base text-white/70 sm:text-lg">
|
||||||
|
Aktiviere JavaScript, um Fotos anzusehen, zu liken und zu teilen. Ohne JavaScript zeigen wir dir eine schnelle Übersicht.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur">
|
||||||
|
<h2 class="text-xl font-semibold text-white">Was du verpasst</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>• Live-Fotogalerie mit schnellen Filtern</li>
|
||||||
|
<li>• Likes & Teilen via WhatsApp, Nachrichten & Link</li>
|
||||||
|
<li>• Offline-fähige PWA zum Installieren</li>
|
||||||
|
<li>• Sichere QR-Zugänge ohne Anmeldung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 shadow-lg backdrop-blur">
|
||||||
|
<h2 class="text-xl font-semibold text-white">So geht’s weiter</h2>
|
||||||
|
<ol class="mt-3 space-y-2 text-sm text-white/80">
|
||||||
|
<li>1) Aktiviere JavaScript in deinem Browser</li>
|
||||||
|
<li>2) Lade die Seite neu</li>
|
||||||
|
<li>3) Optional: Füge die App deinem Homescreen hinzu</li>
|
||||||
|
</ol>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-3">
|
||||||
|
<a href="{{ route('marketing.contact', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full bg-pink-500 px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:bg-pink-400" style="color:#fff;text-decoration:none;background:#ec4899;">
|
||||||
|
Support kontaktieren
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('impressum', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Impressum
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('datenschutz', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Datenschutz
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">Offline-ready</h3>
|
||||||
|
<p class="mt-2">Fotos bleiben verfügbar, auch wenn das Event-WLAN wackelt.</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">Privat & sicher</h3>
|
||||||
|
<p class="mt-2">Keine öffentlichen Profile, keine Gesichtserkennung, nur Event-Teilnehmende.</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">Schnelles Teilen</h3>
|
||||||
|
<p class="mt-2">Direkter Link für Freund:innen, mit Ablaufsteuerung durch das Event-Team.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -33,16 +33,63 @@
|
|||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 text-gray-900">
|
<body class="bg-gray-50 text-gray-900">
|
||||||
|
@php
|
||||||
|
$noscriptLocale = in_array($currentLocale ?? app()->getLocale(), ['de', 'en'], true) ? ($currentLocale ?? app()->getLocale()) : 'de';
|
||||||
|
@endphp
|
||||||
<noscript>
|
<noscript>
|
||||||
<div class="bg-yellow-100 text-yellow-900 text-sm md:text-base">
|
<style>
|
||||||
<div class="mx-auto max-w-5xl px-4 py-3">
|
main { display: none !important; }
|
||||||
@if ($currentLocale === 'en')
|
</style>
|
||||||
<strong>JavaScript disabled.</strong>
|
<div class="min-h-screen bg-gradient-to-b from-[#0f172a] via-[#0b1224] to-[#0a0f1f] text-white" style="background:linear-gradient(180deg,#0f172a 0%,#0b1224 50%,#0a0f1f 100%);color:#fff;">
|
||||||
This site offers limited functionality without JavaScript. Please enable JavaScript for the full experience.
|
<div class="mx-auto flex max-w-5xl flex-col gap-10 px-6 py-14">
|
||||||
@else
|
<header class="space-y-3">
|
||||||
<strong>JavaScript deaktiviert.</strong>
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-pink-300">Fotospiel</p>
|
||||||
Diese Seite bietet nur eingeschränkte Funktionen ohne JavaScript. Bitte aktiviere JavaScript, um alle Inhalte zu nutzen.
|
<h1 class="text-3xl font-semibold sm:text-4xl">
|
||||||
@endif
|
@if ($currentLocale === 'en')
|
||||||
|
JavaScript required for the full Fotospiel experience
|
||||||
|
@else
|
||||||
|
JavaScript für das volle Fotospiel-Erlebnis
|
||||||
|
@endif
|
||||||
|
</h1>
|
||||||
|
<p class="text-base text-white/70 sm:text-lg">
|
||||||
|
@if ($currentLocale === 'en')
|
||||||
|
Enable JavaScript to explore the live galleries, pricing, and admin demo. Here’s a quick overview.
|
||||||
|
@else
|
||||||
|
Aktiviere JavaScript, um Live-Galerien, Preise und die Admin-Demo zu sehen. Hier eine schnelle Übersicht.
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">PWA für Gäste</h3>
|
||||||
|
<p class="mt-2">Offline-fähig, QR-Login ohne Account, Likes & Teilen mit Ablaufsteuerung.</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">Admin & Branding</h3>
|
||||||
|
<p class="mt-2">Eigene Farben/Fonts, Aufgaben & Emotion-Tags, Freigaben & Moderation.</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/80 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-white">Datenschutz</h3>
|
||||||
|
<p class="mt-2">Keine öffentlichen Profile, keine Gesichtserkennung, DSGVO-ready.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<a href="{{ route('marketing.contact', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full bg-pink-500 px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:bg-pink-400" style="color:#fff;text-decoration:none;background:#ec4899;">
|
||||||
|
@if ($currentLocale === 'en')
|
||||||
|
Contact support
|
||||||
|
@else
|
||||||
|
Support kontaktieren
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('impressum', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Impressum
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('datenschutz', ['locale' => $noscriptLocale]) }}" class="inline-flex items-center justify-center rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white/80 transition hover:border-white/40" style="color:#e5e7eb;text-decoration:none;border:1px solid rgba(255,255,255,0.2);">
|
||||||
|
Datenschutz
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
Reference in New Issue
Block a user