das marketing frontend wurde auf lokalisierte urls umgestellt.
This commit is contained in:
@@ -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">
|
||||
© 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">
|
||||
© {currentYear} Die FotoSpiel.App – {t('marketing:footer.rights_reserved', 'Alle Rechte vorbehalten')}.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user