das marketing frontend wurde auf lokalisierte urls umgestellt.

This commit is contained in:
Codex Agent
2025-11-03 15:50:10 +01:00
parent c0c1d31385
commit 55c606bdd4
47 changed files with 1592 additions and 251 deletions

View File

@@ -9,21 +9,26 @@ import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';
import { Toaster } from 'react-hot-toast';
import { ConsentProvider } from './contexts/consent';
import React from 'react';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => title ? `${title} - ${appName}` : appName,
resolve: (name) => resolvePageComponent(
`./pages/${name}.tsx`,
import.meta.glob('./pages/**/*.tsx')
).then((page) => {
if (page) {
const PageComponent = (page as any).default;
return (props: any) => <AppLayout><PageComponent {...props} /></AppLayout>;
}
return null;
}),
resolve: (name) =>
resolvePageComponent(
`./pages/${name}.tsx`,
import.meta.glob('./pages/**/*.tsx')
).then((page: any) => {
if (page?.default) {
const Component = page.default;
if (!Component.layout) {
Component.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>;
}
}
return page;
}),
setup({ el, App, props }) {
const root = createRoot(el);

View File

@@ -1,31 +1,75 @@
import { useLocale } from './useLocale';
import { usePage } from '@inertiajs/react';
import { useLocale } from './useLocale';
type LocalizedPathInput = string | null | undefined;
export const useLocalizedRoutes = () => {
const page = usePage<{ supportedLocales?: string[] }>();
const locale = useLocale();
const supportedLocales = (page.props as any)?.supportedLocales ?? [];
const localizedPath = (path: LocalizedPathInput) => {
const fallbackLocale = (() => {
if (locale && supportedLocales.includes(locale)) {
return locale;
}
if (supportedLocales.length > 0) {
return supportedLocales[0];
}
return 'de';
})();
const pathRewrites: Record<string, Record<string, string>> = {
'/kontakt': { en: '/contact' },
'/contact': { de: '/kontakt' },
'/so-funktionierts': { en: '/how-it-works' },
'/how-it-works': { de: '/so-funktionierts' },
'/anlaesse': { en: '/occasions' },
'/anlaesse/hochzeit': { en: '/occasions/wedding' },
'/anlaesse/geburtstag': { en: '/occasions/birthday' },
'/anlaesse/firmenevent': { en: '/occasions/corporate-event' },
'/anlaesse/konfirmation': { en: '/occasions/confirmation' },
'/occasions/wedding': { de: '/anlaesse/hochzeit' },
'/occasions/birthday': { de: '/anlaesse/geburtstag' },
'/occasions/corporate-event': { de: '/anlaesse/firmenevent' },
'/occasions/confirmation': { de: '/anlaesse/konfirmation' },
};
const rewriteForLocale = (path: string, targetLocale: string): string => {
const key = path === '' ? '/' : path;
const normalizedKey = key.startsWith('/') ? key : `/${key}`;
const rewrites = pathRewrites[normalizedKey] ?? {};
return rewrites[targetLocale] ?? normalizedKey;
};
const localizedPath = (path: LocalizedPathInput, targetLocale?: string) => {
if (typeof path !== 'string' || path.trim().length === 0) {
// Diagnose cases where components pass falsy / non-string hrefs (e.g. legacy localized routes, pagination links)
// This log allows us to correlate console errors from Inertia with offending components.
console.error('[useLocalizedRoutes] Invalid path input detected', {
path,
locale,
stack: new Error().stack,
});
return '/';
return `/${fallbackLocale}`;
}
const nextLocale = targetLocale && supportedLocales.includes(targetLocale)
? targetLocale
: fallbackLocale;
const trimmed = path.trim();
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
const [rawPath, rawQuery] = trimmed.split('?');
const normalizedPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
const rewritten = rewriteForLocale(normalizedPath, nextLocale);
// console.debug('[useLocalizedRoutes] Resolved path', { input: path, normalized, locale });
const base = rewritten === '/' ? `/${nextLocale}` : `/${nextLocale}${rewritten}`;
const sanitisedBase = base.replace(/\/{2,}/g, '/');
const query = rawQuery ? `?${rawQuery}` : '';
// Since prefix-free, return plain path. Locale is handled via session.
return normalized;
return `${sanitisedBase}${query}`;
};
return { localizedPath };
};
};

View File

@@ -1,66 +1,96 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Link } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import { useConsent } from '@/contexts/consent';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
const Footer: React.FC = () => {
const { t } = useTranslation(['marketing', 'legal', 'common']);
const { openPreferences } = useConsent();
const { t } = useTranslation(['marketing', 'legal', 'common']);
const { openPreferences } = useConsent();
const { localizedPath } = useLocalizedRoutes();
const links = useMemo(() => ({
home: localizedPath('/'),
impressum: localizedPath('/impressum'),
datenschutz: localizedPath('/datenschutz'),
agb: localizedPath('/agb'),
kontakt: localizedPath('/kontakt'),
}), [localizedPath]);
const currentYear = new Date().getFullYear();
return (
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<div className="flex items-center gap-4">
<img src="/logo-transparent-md.png" alt="FotoSpiel.App Logo" className="h-12 w-auto" />
<div>
<Link href="/" className="text-2xl font-bold font-display text-pink-500">
Die FotoSpiel.App
</Link>
<p className="text-gray-600 font-sans-marketing mt-2">
Deine Plattform für Event-Fotos.
</p>
</div>
</div>
</div>
<footer className="mt-auto border-t border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div>
<div className="flex items-center gap-4">
<img src="/logo-transparent-md.png" alt="FotoSpiel.App Logo" className="h-12 w-auto" />
<div>
<Link href={links.home} className="font-display text-2xl font-bold text-pink-500">
Die FotoSpiel.App
</Link>
<p className="mt-2 font-sans-marketing text-gray-600">
{t('marketing:footer.company', 'Fotospiel GmbH')}
</p>
</div>
</div>
</div>
<div>
<h3 className="font-semibold font-display text-gray-900 mb-4">Rechtliches</h3>
<ul className="space-y-2 text-sm text-gray-600 font-sans-marketing">
<li><Link href="/impressum" className="hover:text-pink-500 transition-colors">{t('legal:impressum')}</Link></li>
<li><Link href="/datenschutz" className="hover:text-pink-500 transition-colors">{t('legal:datenschutz')}</Link></li>
<li><Link href="/agb" className="hover:text-pink-500 transition-colors">{t('legal:agb')}</Link></li>
<li><Link href="/kontakt" className="hover:text-pink-500 transition-colors">{t('marketing:nav.contact')}</Link></li>
<li>
<button
type="button"
onClick={openPreferences}
className="hover:text-pink-500 transition-colors"
>
{t('common:consent.footer.manage_link')}
</button>
</li>
</ul>
</div>
<div>
<h3 className="font-display mb-4 font-semibold text-gray-900">
{t('legal:headline', 'Rechtliches')}
</h3>
<ul className="font-sans-marketing space-y-2 text-sm text-gray-600">
<li>
<Link href={links.impressum} className="transition-colors hover:text-pink-500">
{t('legal:impressum')}
</Link>
</li>
<li>
<Link href={links.datenschutz} className="transition-colors hover:text-pink-500">
{t('legal:datenschutz')}
</Link>
</li>
<li>
<Link href={links.agb} className="transition-colors hover:text-pink-500">
{t('legal:agb')}
</Link>
</li>
<li>
<Link href={links.kontakt} className="transition-colors hover:text-pink-500">
{t('marketing:nav.contact')}
</Link>
</li>
<li>
<button
type="button"
onClick={openPreferences}
className="transition-colors hover:text-pink-500"
>
{t('common:consent.footer.manage_link')}
</button>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold font-display text-gray-900 mb-4">Social</h3>
<ul className="space-y-2 text-sm text-gray-600 font-sans-marketing">
<li><a href="#" className="hover:text-pink-500">Instagram</a></li>
<li><a href="#" className="hover:text-pink-500">Facebook</a></li>
<li><a href="#" className="hover:text-pink-500">YouTube</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 mt-8 pt-8 text-center text-sm text-gray-500 font-sans-marketing">
&copy; 2025 Die FotoSpiel.App - Alle Rechte vorbehalten.
</div>
</div>
</footer>
<div>
<h3 className="font-display mb-4 font-semibold text-gray-900">
{t('marketing:footer.social', 'Social')}
</h3>
<ul className="font-sans-marketing space-y-2 text-sm text-gray-600">
<li><a href="#" className="hover:text-pink-500">Instagram</a></li>
<li><a href="#" className="hover:text-pink-500">Facebook</a></li>
<li><a href="#" className="hover:text-pink-500">YouTube</a></li>
</ul>
</div>
</div>
<div className="font-sans-marketing mt-8 border-t border-gray-200 pt-8 text-center text-sm text-gray-500">
&copy; {currentYear} Die FotoSpiel.App {t('marketing:footer.rights_reserved', 'Alle Rechte vorbehalten')}.
</div>
</div>
</footer>
);
};

View File

@@ -1,8 +1,14 @@
import React, { useEffect } from 'react';
import { Head, usePage, router } from '@inertiajs/react';
import React, { useEffect, useMemo, useState } from 'react';
import { Head, Link, router, usePage } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import MatomoTracker, { MatomoConfig } from '@/components/analytics/MatomoTracker';
import CookieBanner from '@/components/consent/CookieBanner';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
import Footer from '@/layouts/app/Footer';
import { useAppearance } from '@/hooks/use-appearance';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { MoreHorizontal, Sun, Moon, Languages, LayoutDashboard, LogOut, LogIn, UserPlus } from 'lucide-react';
interface MarketingLayoutProps {
children: React.ReactNode;
@@ -14,11 +20,76 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
translations?: Record<string, Record<string, string>>;
locale?: string;
analytics?: { matomo?: MatomoConfig };
supportedLocales?: string[];
appUrl?: string;
}>();
const { url } = page;
const { t } = useTranslation('marketing');
const i18n = useTranslation();
const { locale, analytics } = page.props;
const { locale, analytics, supportedLocales = ['de', 'en'], appUrl, auth } = page.props as any;
const user = auth?.user ?? null;
const { localizedPath } = useLocalizedRoutes();
const { appearance, updateAppearance } = useAppearance();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const occasionLinks = useMemo(() => ([
{
key: 'wedding',
label: t('nav.occasions_types.weddings', 'Hochzeiten'),
href: localizedPath('/anlaesse/hochzeit'),
},
{
key: 'birthday',
label: t('nav.occasions_types.birthdays', 'Geburtstage'),
href: localizedPath('/anlaesse/geburtstag'),
},
{
key: 'corporate',
label: t('nav.occasions_types.corporate', 'Firmenevents'),
href: localizedPath('/anlaesse/firmenevent'),
},
{
key: 'confirmation',
label: t('nav.occasions_types.confirmation', 'Konfirmation'),
href: localizedPath('/anlaesse/konfirmation'),
},
]), [localizedPath, t]);
const navLinks = useMemo(() => ([
{
key: 'how',
label: t('nav.how_it_works', "So funktioniert's"),
href: localizedPath('/so-funktionierts'),
},
{
key: 'packages',
label: t('nav.packages', 'Pakete'),
href: localizedPath('/packages'),
},
{
key: 'occasions',
label: t('nav.occasions', 'Anlässe'),
children: occasionLinks,
},
{
key: 'blog',
label: t('nav.blog', 'Blog'),
href: localizedPath('/blog'),
},
{
key: 'contact',
label: t('nav.contact', 'Kontakt'),
href: localizedPath('/kontakt'),
},
]), [localizedPath, occasionLinks, t]);
const ctaHref = localizedPath('/demo');
const themeIsDark = appearance === 'dark';
const themeLabel = themeIsDark ? t('nav.theme_light', 'Helles Design') : t('nav.theme_dark', 'Dunkles Design');
const toggleTheme = () => updateAppearance(themeIsDark ? 'light' : 'dark');
const handleLogout = () => {
router.post('/logout');
};
useEffect(() => {
if (locale && i18n.i18n.language !== locale) {
@@ -33,18 +104,37 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
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 activeLocale = locale || supportedLocales[0] || 'de';
const baseUrl = (typeof appUrl === 'string' && appUrl.length > 0)
? appUrl.replace(/\/+$/, '')
: 'https://fotospiel.app';
const [rawPath, rawQuery = ''] = url.split('?');
const localePattern = supportedLocales.length > 0 ? supportedLocales.join('|') : 'de|en';
const localeRegex = new RegExp(`^/(${localePattern})(?=/|$)`, 'i');
const relativePath = rawPath.replace(localeRegex, '') || '/';
const canonicalPath = localizedPath(relativePath, activeLocale);
const canonicalUrl = `${baseUrl}${canonicalPath}${rawQuery ? `?${rawQuery}` : ''}`;
const buildAlternateUrl = (targetLocale: string) => {
const alternatePath = localizedPath(relativePath, targetLocale);
return `${baseUrl}${alternatePath}${rawQuery ? `?${rawQuery}` : ''}`;
};
const alternates = supportedLocales.reduce<Record<string, string>>((acc, currentLocale) => {
acc[currentLocale] = buildAlternateUrl(currentLocale);
return acc;
}, {});
const handleLocaleChange = (nextLocale: string) => {
router.post('/set-locale', { locale: nextLocale }, {
preserveState: true,
const targetPath = localizedPath(relativePath, nextLocale);
const targetUrl = `${targetPath}${rawQuery ? `?${rawQuery}` : ''}`;
i18n.i18n.changeLanguage(nextLocale);
setMobileMenuOpen(false);
router.visit(targetUrl, {
replace: true,
onSuccess: () => {
i18n.i18n.changeLanguage(nextLocale);
},
preserveState: false,
});
};
@@ -62,22 +152,261 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
content={t('meta.description', getString('description', 'Sammle Gastfotos für Events mit QR-Codes'))}
/>
<meta property="og:url" content={canonicalUrl} />
<meta property="og:locale" content={activeLocale === 'de' ? 'de_DE' : `${activeLocale}_${activeLocale.toUpperCase()}`} />
{supportedLocales
.filter((code) => code !== activeLocale)
.map((code) => (
<meta key={`og:locale:${code}`} property="og:locale:alternate" content={code === 'de' ? 'de_DE' : `${code}_${code.toUpperCase()}`} />
))}
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hrefLang="x-default" href="https://fotospiel.app/" />
<link rel="alternate" hrefLang="x-default" href={buildAlternateUrl(supportedLocales[0] || 'de')} />
{Object.entries(alternates).map(([code, href]) => (
<link key={code} rel="alternate" hrefLang={code} href={href} />
))}
</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">
<header className="sticky top-0 z-40 border-b border-gray-200/60 bg-white/95 backdrop-blur">
<div className="container mx-auto flex items-center justify-between px-4 py-4">
<Link
href={localizedPath('/')}
className="flex items-center gap-3 text-gray-900"
onClick={() => setMobileMenuOpen(false)}
>
<img src="/logo-transparent-md.png" alt="FotoSpiel.App Logo" className="h-10 w-auto" />
<span className="font-display text-2xl font-semibold tracking-tight text-pink-500 sm:text-3xl">
Die FotoSpiel.App
</span>
</Link>
<nav className="hidden items-center gap-6 md:flex">
{navLinks.map((item) => (
item.children ? (
<div key={item.key} className="relative group">
<span className="inline-flex cursor-default items-center gap-1 text-sm font-semibold text-gray-700 transition-colors group-hover:text-pink-600 font-sans-marketing">
{item.label}
<svg
className="h-4 w-4 text-gray-400 transition group-hover:text-pink-500"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M6 8L10 12L14 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</span>
<div className="absolute left-0 top-full hidden min-w-[220px] flex-col gap-1 rounded-xl border border-gray-100 bg-white p-3 text-sm shadow-xl shadow-rose-200/50 transition group-hover:flex group-focus-within:flex">
{item.children.map((child) => (
<Link
key={child.key}
href={child.href}
className="rounded-lg px-3 py-2 font-medium text-gray-600 transition hover:bg-rose-50 hover:text-pink-600 font-sans-marketing"
onClick={() => setMobileMenuOpen(false)}
>
{child.label}
</Link>
))}
</div>
</div>
) : (
<Link
key={item.key}
href={item.href}
className="text-sm font-semibold text-gray-700 transition hover:text-pink-600 font-sans-marketing"
onClick={() => setMobileMenuOpen(false)}
>
{item.label}
</Link>
)
))}
</nav>
<div className="flex items-center gap-2">
<Button
asChild
className="hidden rounded-full bg-pink-500 px-5 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-pink-600 font-sans-marketing md:inline-flex"
>
<Link href={ctaHref}>
{t('nav.cta_demo', 'Jetzt ausprobieren')}
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full border-gray-200 text-gray-600 transition hover:border-pink-200 hover:text-pink-500"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">{t('nav.preferences', 'Einstellungen')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 space-y-1 p-2">
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400">
{t('nav.preferences', 'Einstellungen')}
</DropdownMenuLabel>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
toggleTheme();
}}
className="flex items-center gap-2 font-sans-marketing"
>
{themeIsDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
<span>{themeLabel}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400">
{t('nav.language', 'Sprache')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup value={activeLocale} onValueChange={handleLocaleChange}>
{supportedLocales.map((code) => (
<DropdownMenuRadioItem
key={code}
value={code}
className="flex items-center gap-2 font-sans-marketing"
>
<Languages className="h-4 w-4" />
<span>{code.toUpperCase()}</span>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
{user ? (
<>
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400">
{user.name ?? user.email}
</DropdownMenuLabel>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
router.visit('/event-admin');
}}
className="flex items-center gap-2 font-sans-marketing"
>
<LayoutDashboard className="h-4 w-4" />
<span>{t('nav.dashboard', 'Zum Admin-Bereich')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
handleLogout();
}}
className="flex items-center gap-2 font-sans-marketing"
>
<LogOut className="h-4 w-4" />
<span>{t('nav.logout', 'Abmelden')}</span>
</DropdownMenuItem>
</>
) : (
<>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
router.visit(localizedPath('/login'));
}}
className="flex items-center gap-2 font-sans-marketing"
>
<LogIn className="h-4 w-4" />
<span>{t('nav.login', 'Anmelden')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
router.visit(localizedPath('/register'));
}}
className="flex items-center gap-2 font-sans-marketing"
>
<UserPlus className="h-4 w-4" />
<span>{t('nav.register', 'Registrieren')}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-700 shadow-sm md:hidden"
onClick={() => setMobileMenuOpen((open) => !open)}
aria-label={mobileMenuOpen ? t('nav.close_menu', 'Menü schließen') : t('nav.open_menu', 'Menü öffnen')}
>
{mobileMenuOpen ? (
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M5 5L15 15M15 5L5 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
) : (
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M3 6H17M3 10H17M3 14H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
)}
</button>
</div>
</div>
{mobileMenuOpen && (
<div className="border-t border-gray-200 bg-white md:hidden">
<div className="container mx-auto space-y-4 px-4 py-4">
{navLinks.map((item) => (
item.children ? (
<div key={item.key} className="space-y-2">
<p className="text-sm font-semibold text-gray-500">{item.label}</p>
<div className="flex flex-col gap-2">
{item.children.map((child) => (
<Link
key={child.key}
href={child.href}
className="rounded-lg border border-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-pink-200 hover:bg-rose-50 hover:text-pink-600"
onClick={() => setMobileMenuOpen(false)}
>
{child.label}
</Link>
))}
</div>
</div>
) : (
<Link
key={item.key}
href={item.href}
className="block rounded-lg border border-gray-100 px-3 py-2 text-sm font-semibold text-gray-700 transition hover:border-pink-200 hover:bg-rose-50 hover:text-pink-600 font-sans-marketing"
onClick={() => setMobileMenuOpen(false)}
>
{item.label}
</Link>
)
))}
<div className="pt-2">
<Button
asChild
className="w-full rounded-full bg-pink-500 px-5 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-pink-600 font-sans-marketing"
>
<Link href={ctaHref} onClick={() => setMobileMenuOpen(false)}>
{t('nav.cta_demo', 'Jetzt ausprobieren')}
</Link>
</Button>
</div>
<div className="pt-2">
<label htmlFor="marketing-language-select-mobile" className="sr-only">
{t('nav.language', 'Sprache')}
</label>
<select
id="marketing-language-select-mobile"
value={activeLocale}
onChange={(event) => handleLocaleChange(event.target.value)}
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm focus:border-pink-400 focus:outline-none focus:ring focus:ring-pink-200"
>
{supportedLocales.map((code) => (
<option key={code} value={code}>
{code.toUpperCase()}
</option>
))}
</select>
</div>
</div>
</div>
)}
</header>
<main>
{children}
</main>
{/* Footer kommt von Footer.tsx */}
<Footer />
</div>
</>
);

View File

@@ -11,7 +11,7 @@ type LegalShowProps = {
slug: string;
};
export default function LegalShow(props: LegalShowProps) {
const LegalShow: React.FC<LegalShowProps> = (props) => {
const { seoTitle, title, content, effectiveFromLabel, versionLabel } = props;
return (
@@ -41,4 +41,8 @@ export default function LegalShow(props: LegalShowProps) {
</section>
</MarketingLayout>
);
}
};
LegalShow.layout = (page: React.ReactNode) => page;
export default LegalShow;

View File

@@ -184,4 +184,6 @@ const Blog: React.FC<Props> = ({ posts }) => {
);
};
export default Blog;
Blog.layout = (page: React.ReactNode) => page;
export default Blog;

View File

@@ -111,4 +111,6 @@ const BlogShow: React.FC<Props> = ({ post }) => {
);
};
export default BlogShow;
BlogShow.layout = (page: React.ReactNode) => page;
export default BlogShow;

View File

@@ -21,13 +21,13 @@ interface CheckoutWizardPageProps {
};
}
export default function CheckoutWizardPage({
const CheckoutWizardPage: React.FC<CheckoutWizardPageProps> = ({
package: initialPackage,
packageOptions,
privacyHtml,
googleAuth,
paddle,
}: CheckoutWizardPageProps) {
}) => {
const page = usePage<{ auth?: { user?: { id: number; email: string; name?: string; pending_purchase?: boolean } | null } }>();
const currentUser = page.props.auth?.user ?? null;
const googleProfile = googleAuth?.profile ?? null;
@@ -75,4 +75,8 @@ export default function CheckoutWizardPage({
</div>
</MarketingLayout>
);
}
};
CheckoutWizardPage.layout = (page: React.ReactNode) => page;
export default CheckoutWizardPage;

View File

@@ -122,4 +122,6 @@ const DemoPage: React.FC = () => {
);
};
DemoPage.layout = (page: React.ReactNode) => page;
export default DemoPage;

View File

@@ -576,4 +576,6 @@ const Home: React.FC<Props> = ({ packages }) => {
);
};
Home.layout = (page: React.ReactNode) => page;
export default Home;

View File

@@ -420,4 +420,6 @@ const ExperiencePanel: React.FC<{ data: ExperienceGroup }> = ({ data }) => {
);
};
HowItWorks.layout = (page: React.ReactNode) => page;
export default HowItWorks;

View File

@@ -95,4 +95,6 @@ const Kontakt: React.FC = () => {
);
};
export default Kontakt;
Kontakt.layout = (page: React.ReactNode) => page;
export default Kontakt;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { Head, Link } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import MarketingLayout from '@/layouts/mainWebsite';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
interface NotFoundProps {
requestedPath?: string;
}
const NotFound: React.FC<NotFoundProps> = ({ requestedPath }) => {
const { t } = useTranslation('marketing');
const { localizedPath } = useLocalizedRoutes();
const tips = t('not_found.tips', { returnObjects: true }) as string[];
return (
<MarketingLayout title={t('not_found.title')}>
<Head title={t('not_found.title')} />
<section className="relative min-h-screen overflow-hidden bg-gradient-to-br from-slate-900 via-gray-900 to-black text-white">
<div className="absolute inset-0 pointer-events-none">
<div className="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-pink-500/30 blur-3xl" />
<div className="absolute right-0 top-1/3 h-80 w-80 rounded-full bg-purple-500/20 blur-3xl" />
<div className="absolute bottom-0 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-blue-500/20 blur-3xl" />
</div>
<div className="relative z-10 mx-auto flex min-h-screen max-w-5xl flex-col items-center justify-center px-6 py-16 text-center">
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold uppercase tracking-widest text-pink-200 shadow-lg shadow-pink-500/30 backdrop-blur">
404 · {t('not_found.title')}
</div>
<h1 className="text-balance text-4xl font-bold leading-tight text-white sm:text-5xl md:text-6xl">
{t('not_found.subtitle')}
</h1>
{requestedPath && (
<p className="mt-4 text-sm text-white/60">
{t('not_found.requested_path_label', 'Angefragter Pfad')}: <span className="font-mono">{requestedPath}</span>
</p>
)}
<p className="mt-6 max-w-2xl text-lg text-white/70 sm:text-xl">
{t('not_found.description')}
</p>
<div className="mt-10 flex flex-col gap-6 rounded-3xl border border-white/10 bg-white/5 p-8 text-left shadow-lg shadow-black/40 backdrop-blur md:flex-row md:gap-8">
<div className="md:w-1/3">
<h2 className="text-lg font-semibold uppercase tracking-widest text-pink-200">
{t('not_found.tip_heading')}
</h2>
<div className="mt-4 h-1 w-16 rounded-full bg-pink-400" />
</div>
<ul className="space-y-3 text-base text-white/80 md:w-2/3">
{tips.map((tip, index) => (
<li key={`${tip}-${index}`} className="flex items-start gap-3">
<span className="mt-1 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-white/10 text-xs font-semibold text-pink-200">
{index + 1}
</span>
<span>{tip}</span>
</li>
))}
</ul>
</div>
<div className="mt-12 flex flex-col gap-3 sm:flex-row">
<Link
href={localizedPath('/')}
className="inline-flex items-center justify-center rounded-full bg-white px-6 py-3 font-medium text-gray-900 shadow-lg shadow-pink-500/30 transition hover:-translate-y-1 hover:bg-pink-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{t('not_found.cta_home')}
</Link>
<Link
href={localizedPath('/packages')}
className="inline-flex items-center justify-center rounded-full border border-white/40 px-6 py-3 font-medium text-white transition hover:-translate-y-1 hover:border-white hover:bg-white/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{t('not_found.cta_packages')}
</Link>
<Link
href={localizedPath('/kontakt')}
className="inline-flex items-center justify-center rounded-full border border-transparent bg-gradient-to-r from-pink-500 to-purple-500 px-6 py-3 font-medium text-white shadow-lg shadow-pink-500/30 transition hover:-translate-y-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{t('not_found.cta_contact')}
</Link>
</div>
</div>
</section>
</MarketingLayout>
);
};
NotFound.layout = (page: React.ReactNode) => page;
export default NotFound;

View File

@@ -59,7 +59,7 @@ const Occasions: React.FC<OccasionsProps> = ({ type }) => {
},
};
const content = occasionsContent[type as keyof typeof occasionsContent] || occasionsContent.hochzeit;
const content = occasionsContent[type as keyof typeof occasionsContent] || occasionsContent.hochzeit;
return (
<MarketingLayout title={content.title}>
@@ -89,4 +89,6 @@ const Occasions: React.FC<OccasionsProps> = ({ type }) => {
);
};
Occasions.layout = (page: React.ReactNode) => page;
export default Occasions;

View File

@@ -1026,4 +1026,6 @@ function PackageCard({
);
};
Packages.layout = (page: React.ReactNode) => page;
export default Packages;

View File

@@ -80,4 +80,6 @@ const Success: React.FC = () => {
);
};
Success.layout = (page: React.ReactNode) => page;
export default Success;

View File

@@ -30,6 +30,7 @@ return [
'data_security' => 'Datensicherheit',
'data_security_desc' => 'Wir verwenden HTTPS, verschlüsselte Speicherung (Passwörter hashed) und regelmäßige Backups. Zugriff auf Daten ist rollebasierend beschränkt (Tenant vs SuperAdmin).',
'agb' => 'Allgemeine Geschäftsbedingungen',
'headline' => 'Rechtliches',
'effective_from' => 'Gültig seit :date',
'version' => 'Version :version',
'and' => 'und',

View File

@@ -76,17 +76,35 @@ return [
'weddings' => 'Hochzeiten',
'birthdays' => 'Geburtstage',
'corporate' => 'Firmenevents',
'confirmation' => 'Konfirmation & Jugendweihe',
'family' => 'Familienfeiern',
],
'blog' => 'Blog',
'packages' => 'Packages',
'contact' => 'Kontakt',
'discover_packages' => 'Packages entdecken',
'language' => 'Sprache',
'open_menu' => 'Menü öffnen',
'close_menu' => 'Menü schließen',
'cta_demo' => 'Jetzt ausprobieren',
'preferences' => 'Einstellungen',
'toggle_theme' => 'Darstellung wechseln',
'theme_light' => 'Helles Design',
'theme_dark' => 'Dunkles Design',
'dashboard' => 'Zum Admin-Bereich',
'logout' => 'Abmelden',
'login' => 'Anmelden',
'register' => 'Registrieren',
],
'footer' => [
'company' => 'Fotospiel GmbH',
'rights_reserved' => 'Alle Rechte vorbehalten',
],
'legal' => [
'imprint' => 'Impressum',
'privacy' => 'Datenschutz',
'terms' => 'AGB',
],
'blog' => [
'title' => 'Fotospiel - Blog',
'hero_title' => 'Fotospiel Blog',
@@ -147,6 +165,21 @@ return [
],
'not_found' => 'Anlass nicht gefunden.',
],
'not_found' => [
'title' => 'Seite nicht gefunden',
'subtitle' => 'Ups! Diese Seite existiert nicht mehr.',
'description' => 'Vielleicht wurde der Link verschoben oder der Inhalt existiert nicht mehr. Hier sind ein paar Optionen, wie du weitermachen kannst.',
'tip_heading' => 'Was du tun kannst',
'tips' => [
'Prüfe die URL auf mögliche Tippfehler.',
'Gehe zurück zur Startseite und entdecke unsere Funktionen.',
'Kontaktiere uns, wenn du etwas Bestimmtes suchst.',
],
'cta_home' => 'Zur Startseite',
'cta_packages' => 'Pakete entdecken',
'cta_contact' => 'Kontakt aufnehmen',
'requested_path_label' => 'Angefragter Pfad',
],
'success' => [
'title' => 'Erfolgreich',
'verify_email' => 'E-Mail verifizieren',

View File

@@ -30,6 +30,7 @@ return [
'data_security' => 'Data Security',
'data_security_desc' => 'We use HTTPS, encrypted storage (passwords hashed) and regular backups. Access to data is role-based restricted (Tenant vs SuperAdmin).',
'agb' => 'Terms & Conditions',
'headline' => 'Legal',
'effective_from' => 'Effective from :date',
'version' => 'Version :version',
];

View File

@@ -76,17 +76,35 @@ return [
'weddings' => 'Weddings',
'birthdays' => 'Birthdays',
'corporate' => 'Corporate Events',
'confirmation' => 'Confirmations',
'family' => 'Family Celebrations',
],
'blog' => 'Blog',
'packages' => 'Packages',
'contact' => 'Contact',
'discover_packages' => 'Discover Packages',
'language' => 'Language',
'open_menu' => 'Open menu',
'close_menu' => 'Close menu',
'cta_demo' => 'Try it now',
'preferences' => 'Preferences',
'toggle_theme' => 'Toggle appearance',
'theme_light' => 'Light mode',
'theme_dark' => 'Dark mode',
'dashboard' => 'Go to Admin',
'logout' => 'Sign out',
'login' => 'Log in',
'register' => 'Register',
],
'footer' => [
'company' => 'Fotospiel GmbH',
'rights_reserved' => 'All rights reserved',
],
'legal' => [
'imprint' => 'Imprint',
'privacy' => 'Privacy',
'terms' => 'Terms & Conditions',
],
'blog' => [
'title' => 'Fotospiel - Blog',
'hero_title' => 'Fotospiel Blog',
@@ -147,6 +165,21 @@ return [
],
'not_found' => 'Occasion not found.',
],
'not_found' => [
'title' => 'Page not found',
'subtitle' => 'Oops! This page is nowhere to be found.',
'description' => 'It may have moved or never existed. Try one of the options below to get back on track.',
'tip_heading' => 'What you can do',
'tips' => [
'Double-check the URL for typos.',
'Head back to the homepage to continue exploring.',
'Reach out to us if you need a specific page.',
],
'cta_home' => 'Back to homepage',
'cta_packages' => 'Explore packages',
'cta_contact' => 'Get in touch',
'requested_path_label' => 'Requested path',
],
'success' => [
'title' => 'Success',
'verify_email' => 'Verify Email',

View File

@@ -0,0 +1,88 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ __('marketing.not_found.title', [], app()->getLocale()) }} · Fotospiel</title>
@vite(['resources/css/app.css'])
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-900 via-gray-900 to-black text-white antialiased">
@php
$tips = trans('marketing.not_found.tips');
if (! is_array($tips)) {
$tips = [
__('Prüfe die URL auf Tippfehler.', [], 'de'),
];
}
@endphp
<main class="relative mx-auto flex min-h-screen max-w-5xl flex-col items-center justify-center px-6 py-16 text-center">
<div class="pointer-events-none absolute inset-0">
<div class="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-pink-500/30 blur-3xl"></div>
<div class="absolute right-0 top-1/3 h-80 w-80 rounded-full bg-purple-500/20 blur-3xl"></div>
<div class="absolute bottom-0 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-blue-500/20 blur-3xl"></div>
</div>
<section class="relative z-10 w-full space-y-8 rounded-3xl border border-white/10 bg-white/5 p-10 shadow-2xl shadow-black/40 backdrop-blur">
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-pink-200 shadow-lg shadow-pink-500/30">
404 · {{ __('marketing.not_found.title') }}
</div>
<h1 class="text-balance text-4xl font-bold leading-tight text-white sm:text-5xl md:text-6xl">
{{ __('marketing.not_found.subtitle') }}
</h1>
@if(request()->path())
<p class="text-sm text-white/60">
{{ __('marketing.not_found.requested_path_label') }}:
<span class="font-mono">{{ request()->getRequestUri() }}</span>
</p>
@endif
<p class="mx-auto max-w-2xl text-lg text-white/70 sm:text-xl">
{{ __('marketing.not_found.description') }}
</p>
<div class="flex flex-col gap-6 rounded-3xl border border-white/10 bg-white/10 p-8 text-left shadow-lg shadow-black/30 backdrop-blur md:flex-row md:gap-8">
<div class="md:w-1/3">
<h2 class="text-lg font-semibold uppercase tracking-widest text-pink-200">
{{ __('marketing.not_found.tip_heading') }}
</h2>
<div class="mt-4 h-1 w-16 rounded-full bg-pink-400"></div>
</div>
<ul class="space-y-3 text-base text-white/80 md:w-2/3">
@foreach($tips as $index => $tip)
<li class="flex items-start gap-3">
<span class="mt-1 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-white/10 text-xs font-semibold text-pink-200">
{{ $index + 1 }}
</span>
<span>{{ $tip }}</span>
</li>
@endforeach
</ul>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:justify-center">
<a
href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full bg-white px-6 py-3 font-medium text-gray-900 shadow-lg shadow-pink-500/30 transition hover:-translate-y-1 hover:bg-pink-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.not_found.cta_home') }}
</a>
<a
href="{{ route('packages', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full border border-white/40 px-6 py-3 font-medium text-white transition hover:-translate-y-1 hover:border-white hover:bg-white/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.not_found.cta_packages') }}
</a>
<a
href="{{ app()->getLocale() === 'en' ? route('marketing.contact', ['locale' => app()->getLocale()]) : route('kontakt', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full border border-transparent bg-gradient-to-r from-pink-500 to-purple-500 px-6 py-3 font-medium text-white shadow-lg shadow-pink-500/30 transition hover:-translate-y-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.not_found.cta_contact') }}
</a>
</div>
</section>
</main>
</body>
</html>

View File

@@ -6,7 +6,7 @@
<h2 class="text-xl font-semibold mb-2">{{ __('legal.payments') }}</h2>
<p class="mb-4">{{ __('legal.payments_desc') }} <a href="https://stripe.com/de/privacy" target="_blank" rel="noopener noreferrer">{{ __('legal.stripe_privacy') }}</a> {{ __('legal.and') }} <a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full" target="_blank" rel="noopener noreferrer">{{ __('legal.paypal_privacy') }}</a>.</p>
<p class="mb-4">{{ __('legal.data_retention') }}</p>
<p class="mb-4">{{ __('legal.rights') }} <a href="{{ route('kontakt') }}">{{ __('legal.contact') }}</a>.</p>
<p class="mb-4">{{ __('legal.rights') }} <a href="{{ route('kontakt', ['locale' => app()->getLocale()]) }}">{{ __('legal.contact') }}</a>.</p>
<p class="mb-4">{{ __('legal.cookies') }}</p>
<h2 class="text-xl font-semibold mb-2">{{ __('legal.personal_data') }}</h2>
@@ -17,4 +17,4 @@
<h2 class="text-xl font-semibold mb-2">{{ __('legal.data_security') }}</h2>
<p class="mb-4">{{ __('legal.data_security_desc') }}</p>
</div>
</div>

View File

@@ -11,7 +11,7 @@
<h2 class="text-xl font-semibold mb-2">{{ __('legal.payments') }}</h2>
<p class="mb-4">{{ __('legal.payments_desc') }} <a href="https://stripe.com/de/privacy" target="_blank">{{ __('legal.stripe_privacy') }}</a> {{ __('legal.and') }} <a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full" target="_blank">{{ __('legal.paypal_privacy') }}</a>.</p>
<p class="mb-4">{{ __('legal.data_retention') }}</p>
<p class="mb-4">{{ __('legal.rights') }} <a href="{{ route('kontakt') }}">{{ __('legal.contact') }}</a>.</p>
<p class="mb-4">{{ __('legal.rights') }} <a href="{{ route('kontakt', ['locale' => app()->getLocale()]) }}">{{ __('legal.contact') }}</a>.</p>
<p class="mb-4">{{ __('legal.cookies') }}</p>
<h2 class="text-xl font-semibold mb-2">{{ __('legal.personal_data') }}</h2>
@@ -23,4 +23,4 @@
<h2 class="text-xl font-semibold mb-2">{{ __('legal.data_security') }}</h2>
<p class="mb-4">{{ __('legal.data_security_desc') }}</p>
</div>
@endsection
@endsection

View File

@@ -10,7 +10,7 @@
{{ __('legal.company') }}<br>
{{ __('legal.address') }}<br>
{{ __('legal.representative') }}<br>
{{ __('legal.contact') }}: <a href="{{ route('kontakt') }}">{{ __('legal.contact') }}</a>
{{ __('legal.contact') }}: <a href="{{ route('kontakt', ['locale' => app()->getLocale()]) }}">{{ __('legal.contact') }}</a>
</p>
<p class="mb-4">{{ __('legal.vat_id') }}</p>
<h2 class="text-xl font-semibold mb-2">{{ __('legal.monetization') }}</h2>
@@ -18,4 +18,4 @@
<p class="mb-4">{{ __('legal.register_court') }}</p>
<p class="mb-4">{{ __('legal.commercial_register') }}</p>
</div>
@endsection
@endsection

View File

@@ -92,8 +92,8 @@
</div>
</div>
@else
<p class="text-center">{{ __('marketing.occasions.not_found') }} <a href="{{ route('marketing') }}">{{ __('nav.home') }}</a>.</p>
<p class="text-center">{{ __('marketing.occasions.not_found') }} <a href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}">{{ __('nav.home') }}</a>.</p>
@endif
</div>
</section>
@endsection
@endsection

View File

@@ -1,29 +1,29 @@
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<a href="{{ route('marketing') }}" class="text-2xl font-bold text-gray-900">Die Fotospiel.App</a>
<a href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}" class="text-2xl font-bold text-gray-900">Die Fotospiel.App</a>
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<nav class="hidden md:flex space-x-6 items-center">
<a href="{{ route('marketing') }}#how-it-works" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.how_it_works') }}</a>
<a href="{{ route('marketing') }}#features" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.features') }}</a>
<a href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}#how-it-works" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.how_it_works') }}</a>
<a href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}#features" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.features') }}</a>
<div x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false" @click.away="open = false" class="relative">
<button class="text-gray-600 hover:text-gray-900" @click.stop="open = !open">{{ __('marketing.nav.occasions') }}</button>
<div x-show="open" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform scale-95" x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-95" class="absolute top-full left-0 mt-2 bg-white border rounded shadow-lg z-10">
<a href="{{ route('anlaesse.type', ['type' => 'hochzeit']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.weddings') }}</a>
<a href="{{ route('anlaesse.type', ['type' => 'geburtstag']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.birthdays') }}</a>
<a href="{{ route('anlaesse.type', ['type' => 'firmenevent']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.corporate') }}</a>
<a href="{{ route('anlaesse.type', ['locale' => app()->getLocale(), 'type' => 'hochzeit']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.weddings') }}</a>
<a href="{{ route('anlaesse.type', ['locale' => app()->getLocale(), 'type' => 'geburtstag']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.birthdays') }}</a>
<a href="{{ route('anlaesse.type', ['locale' => app()->getLocale(), 'type' => 'firmenevent']) }}" class="block px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition">{{ __('marketing.nav.occasions_types.corporate') }}</a>
</div>
</div>
<a href="{{ route('blog') }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.blog') }}</a>
<a href="{{ route('packages') }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.packages') }}</a>
<a href="{{ route('kontakt') }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.contact') }}</a>
<a href="{{ route('packages') }}" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold hover:bg-[#FF69B4] transition">{{ __('marketing.nav.discover_packages') }}</a>
<a href="{{ route('blog', ['locale' => app()->getLocale()]) }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.blog') }}</a>
<a href="{{ route('packages', ['locale' => app()->getLocale()]) }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.packages') }}</a>
<a href="{{ route('kontakt', ['locale' => app()->getLocale()]) }}" class="text-gray-600 hover:text-gray-900">{{ __('marketing.nav.contact') }}</a>
<a href="{{ route('packages', ['locale' => app()->getLocale()]) }}" class="bg-[#FFB6C1] text-white px-6 py-2 rounded-full font-semibold hover:bg-[#FF69B4] transition">{{ __('marketing.nav.discover_packages') }}</a>
</nav>
<!-- Mobile Menu Placeholder (Hamburger) -->
<button class="md:hidden text-gray-600"></button>
</div>
</header>
</header>