Files
fotospiel-app/resources/js/layouts/app/Header.tsx
2025-11-14 10:53:53 +01:00

425 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useMemo, useState } from 'react';
import { usePage } from '@inertiajs/react';
import { Link, router } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import { useAppearance } from '@/hooks/use-appearance';
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
import profileRoutes from '@/routes/profile';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Separator } from '@/components/ui/separator';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Sun, Moon, Menu, X, Languages, UserRound } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
const Header: React.FC = () => {
const { auth } = usePage().props as any;
const { t } = useTranslation('auth');
const { appearance, updateAppearance } = useAppearance();
const { localizedPath } = useLocalizedRoutes();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const setTheme = useCallback((mode: 'light' | 'dark') => {
if (appearance !== mode) {
updateAppearance(mode);
}
setMobileMenuOpen(false);
}, [appearance, updateAppearance]);
const toggleTheme = () => {
setTheme(appearance === 'dark' ? 'light' : 'dark');
};
const handleLanguageChange = useCallback(async (value: string) => {
//console.log('handleLanguageChange called with:', value);
try {
const response = await fetch('/set-locale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
},
body: JSON.stringify({ locale: value }),
});
//console.log('fetch response:', response.status);
if (response.ok) {
//console.log('calling i18n.changeLanguage with:', value);
i18n.changeLanguage(value);
// Reload only the locale prop to update the page props
router.reload({ only: ['locale'] });
setMobileMenuOpen(false);
}
} catch (error) {
console.error('Failed to change locale:', error);
}
}, [setMobileMenuOpen]);
const handleLogout = () => {
router.post('/logout', {}, {
onFinish: () => setMobileMenuOpen(false),
});
};
const ctaHref = localizedPath('/demo');
const navItems = useMemo(() => {
const homeHref = localizedPath('/');
const howItWorksHref = localizedPath('/so-funktionierts');
return [
{
key: 'howItWorks',
label: t('header.how_it_works', "So geht's"),
href: howItWorksHref,
},
{
key: 'packages',
label: t('header.packages', 'Pakete'),
href: localizedPath('/packages'),
},
{
key: 'occasions',
label: t('header.occasions.label', 'Anlässe'),
children: [
{
key: 'wedding',
label: t('header.occasions.wedding', 'Hochzeiten'),
href: localizedPath('/anlaesse/hochzeit'),
},
{
key: 'birthday',
label: t('header.occasions.birthday', 'Geburtstage'),
href: localizedPath('/anlaesse/geburtstag'),
},
{
key: 'corporate',
label: t('header.occasions.corporate', 'Firmenfeiern'),
href: localizedPath('/anlaesse/firmenevent'),
},
{
key: 'confirmation',
label: t('header.occasions.confirmation', 'Konfirmation/Jugendweihe'),
href: localizedPath('/anlaesse/konfirmation'),
},
],
},
{
key: 'blog',
label: t('header.blog', 'Blog'),
href: localizedPath('/blog'),
},
{
key: 'contact',
label: t('header.contact', 'Kontakt'),
href: localizedPath('/kontakt'),
},
];
}, [localizedPath, t]);
const handleNavSelect = useCallback(() => setMobileMenuOpen(false), []);
return (
<header className="fixed top-0 z-50 w-full bg-white dark:bg-gray-900 shadow-lg border-b-2 border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Link href={localizedPath('/')} className="flex items-center gap-4">
<img src="/logo-transparent-md.png" alt="Fotospiel App Logo" className="h-12 w-auto" />
<span className="text-2xl font-bold font-display text-pink-500">
Die Fotospiel App
</span>
</Link>
<NavigationMenu className="hidden lg:flex flex-1 justify-center" viewport={false}>
<NavigationMenuList className="gap-1.5">
{navItems.map((item) => (
<NavigationMenuItem key={item.key}>
{item.children ? (
<>
<NavigationMenuTrigger className="bg-transparent px-3 py-1.5 !text-lg font-medium text-gray-700 hover:text-pink-600 hover:bg-pink-50 dark:text-gray-300 dark:hover:text-pink-400 dark:hover:bg-pink-950/20 font-sans-marketing">
{item.label}
</NavigationMenuTrigger>
<NavigationMenuContent className="min-w-[260px] rounded-md border bg-popover p-3 shadow-lg">
<ul className="flex flex-col gap-1">
{item.children.map((child) => (
<li key={child.key}>
<NavigationMenuLink asChild>
<Link
href={child.href}
className="flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
>
<span aria-hidden className="mr-2 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
</span>
<span className="whitespace-nowrap">{child.label}</span>
</Link>
</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContent>
</>
) : (
<NavigationMenuLink asChild>
<Link
href={item.href}
className={cn(
navigationMenuTriggerStyle(),
"bg-transparent px-3 py-1.5 !text-lg font-medium text-gray-700 hover:bg-pink-50 hover:text-pink-600 dark:text-gray-300 dark:hover:bg-pink-950/20 dark:hover:text-pink-400 font-sans-marketing"
)}
>
{item.label}
</Link>
</NavigationMenuLink>
)}
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
<div className="hidden lg:flex items-center space-x-2">
<Button asChild size="sm" className="bg-[#FF5F87] hover:bg-[#ff4674] text-white shadow-md shadow-rose-500/20">
<Link href={ctaHref} className="font-sans-marketing font-semibold px-3">
{t('header.cta', 'Jetzt ausprobieren')}
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
aria-label={t('header.utility', 'Darstellung und Sprache öffnen')}
>
<Languages className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="end" forceMount>
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-muted-foreground">
{t('header.appearance', 'Darstellung')}
</DropdownMenuLabel>
<DropdownMenuItem onClick={() => setTheme('light')} className="font-sans-marketing">
<Sun className="mr-2 h-4 w-4" />
{t('header.appearance_light', 'Hell')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')} className="font-sans-marketing">
<Moon className="mr-2 h-4 w-4" />
{t('header.appearance_dark', 'Dunkel')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-muted-foreground">
{t('header.language', 'Sprache')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup value={i18n.language || 'de'} onValueChange={handleLanguageChange}>
<DropdownMenuRadioItem value="de" className="font-sans-marketing">
Deutsch
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="en" className="font-sans-marketing">
English
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
{auth.user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={auth.user?.avatar} alt={auth.user?.name || 'User'} />
<AvatarFallback>{(auth.user?.name || auth.user?.email || 'U').charAt(0)}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{auth.user?.name || auth.user?.email || 'User'}</p>
<p className="text-xs leading-none text-muted-foreground">{auth.user?.email || ''}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="font-sans-marketing">
<Link href={profileRoutes.index().url}>
Profil
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="font-sans-marketing">
<Link href={localizedPath('/profile/orders')}>
Bestellungen
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="font-sans-marketing">
Abmelden
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button asChild variant="ghost" size="icon" className="h-8 w-8">
<Link
href={localizedPath('/login')}
className="flex items-center justify-center text-gray-700 hover:text-pink-600 dark:text-gray-300 dark:hover:text-pink-400"
aria-label={t('header.login')}
>
<UserRound className="h-4 w-4" />
<span className="sr-only">{t('header.login')}</span>
</Link>
</Button>
)}
</div>
<div className="flex items-center lg:hidden">
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label={mobileMenuOpen ? 'Navigation schließen' : 'Navigation öffnen'}
className="h-10 w-10"
>
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</Button>
</SheetTrigger>
<SheetContent side="right" className="flex h-full flex-col gap-6 overflow-y-auto bg-white dark:bg-gray-950">
<SheetHeader className="text-left">
<SheetTitle className="text-xl font-semibold">Menü</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium text-muted-foreground">Sprache</span>
<Select value={i18n.language || 'de'} onValueChange={handleLanguageChange}>
<SelectTrigger className="h-10 w-full">
<SelectValue placeholder={t('common.ui.language_select')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="de">Deutsch</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
<nav className="flex flex-col gap-2">
{navItems.map((item) => (
item.children ? (
<Accordion
key={item.key}
type="single"
collapsible
className="w-full"
>
<AccordionItem value={`${item.key}-group`}>
<AccordionTrigger className="flex w-full items-center justify-between rounded-md border border-transparent bg-gray-50 px-3 py-2 text-base font-semibold text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:bg-gray-900/40 dark:text-gray-200 dark:hover:bg-gray-800 font-sans-marketing">
{item.label}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-2 pt-2">
{item.children.map((child) => (
<SheetClose asChild key={child.key}>
<Link
href={child.href}
className="flex w-full items-center rounded-md border border-transparent px-3 py-2 text-sm font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
onClick={handleNavSelect}
>
<span aria-hidden className="mr-2 text-muted-foreground"></span>
<span>{child.label}</span>
</Link>
</SheetClose>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
) : (
<SheetClose asChild key={item.key}>
<Link
href={item.href}
className="rounded-md border border-transparent px-3 py-2 text-base font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
onClick={handleNavSelect}
>
{item.label}
</Link>
</SheetClose>
)
))}
</nav>
<Separator />
<div className="flex flex-col gap-4">
<SheetClose asChild>
<Link
href={ctaHref}
className="rounded-full bg-[#FF5F87] px-4 py-3 text-center text-base font-semibold text-white shadow-md shadow-rose-500/20 transition hover:bg-[#ff4674] font-sans-marketing"
onClick={handleNavSelect}
>
{t('header.cta', 'Jetzt ausprobieren')}
</Link>
</SheetClose>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Darstellung</span>
<Button
variant="outline"
size="icon"
onClick={toggleTheme}
className="h-9 w-9"
>
<Sun className={cn("h-4 w-4", appearance === "dark" && "hidden")} />
<Moon className={cn("h-4 w-4", appearance !== "dark" && "hidden")} />
<span className="sr-only">Theme Toggle</span>
</Button>
</div>
<div className="flex flex-col gap-3">
{auth.user ? (
<>
<SheetClose asChild>
<Link
href={profileRoutes.index().url}
className="rounded-md border border-transparent px-3 py-2 text-base font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
onClick={handleNavSelect}
>
Profil
</Link>
</SheetClose>
<SheetClose asChild>
<Link
href={localizedPath('/profile/orders')}
className="rounded-md border border-transparent px-3 py-2 text-base font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
onClick={handleNavSelect}
>
Bestellungen
</Link>
</SheetClose>
<Button variant="destructive" onClick={handleLogout} className="font-sans-marketing">
Abmelden
</Button>
</>
) : (
<div className="flex flex-col gap-3">
<SheetClose asChild>
<Link
href={localizedPath('/login')}
className="rounded-md border border-transparent px-3 py-2 text-base font-medium text-gray-700 transition hover:border-gray-200 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900/60 font-sans-marketing"
onClick={handleNavSelect}
>
{t('header.login')}
</Link>
</SheetClose>
</div>
)}
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</div>
</header>
);
};
export default Header;