Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,7 +9,9 @@ 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, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Sun, Moon, Menu, X, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
@@ -17,10 +19,12 @@ const Header: React.FC = () => {
|
||||
const { t } = useTranslation('auth');
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
const { localizedPath } = useLocalizedRoutes();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newAppearance = appearance === 'dark' ? 'light' : 'dark';
|
||||
updateAppearance(newAppearance);
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleLanguageChange = useCallback(async (value: string) => {
|
||||
@@ -41,70 +45,104 @@ const Header: React.FC = () => {
|
||||
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');
|
||||
router.post('/logout', {}, {
|
||||
onFinish: () => setMobileMenuOpen(false),
|
||||
});
|
||||
};
|
||||
|
||||
const navItems = useMemo(() => ([
|
||||
{
|
||||
key: 'home',
|
||||
label: t('header.home', 'Home'),
|
||||
href: localizedPath('/'),
|
||||
},
|
||||
{
|
||||
key: 'packages',
|
||||
label: t('header.packages', 'Pakete'),
|
||||
href: localizedPath('/packages'),
|
||||
},
|
||||
{
|
||||
key: 'blog',
|
||||
label: t('header.blog', 'Blog'),
|
||||
href: localizedPath('/blog'),
|
||||
},
|
||||
{
|
||||
key: 'occasions',
|
||||
label: t('header.occasions.label', 'Anlässe'),
|
||||
children: [
|
||||
{
|
||||
key: 'wedding',
|
||||
label: t('header.occasions.wedding', 'Hochzeit'),
|
||||
href: localizedPath('/anlaesse/hochzeit'),
|
||||
},
|
||||
{
|
||||
key: 'birthday',
|
||||
label: t('header.occasions.birthday', 'Geburtstag'),
|
||||
href: localizedPath('/anlaesse/geburtstag'),
|
||||
},
|
||||
{
|
||||
key: 'corporate',
|
||||
label: t('header.occasions.corporate', 'Firmenevent'),
|
||||
href: localizedPath('/anlaesse/firmenevent'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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 justify-between items-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href={localizedPath('/')} className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
Die Fotospiel.App
|
||||
</Link>
|
||||
<nav className="flex space-x-8">
|
||||
<Button asChild variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
<Link href={localizedPath('/')}>
|
||||
{t('header.home', 'Home')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
<Link href={localizedPath('/packages')}>
|
||||
{t('header.packages', 'Pakete')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
<Link href={localizedPath('/blog')}>
|
||||
{t('header.blog', 'Blog')}
|
||||
</Link>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
Anlässe
|
||||
<nav className="hidden lg:flex items-center space-x-8">
|
||||
{navItems.map((item) => (
|
||||
item.children ? (
|
||||
<DropdownMenu key={item.key}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
{item.label}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{item.children.map((child) => (
|
||||
<DropdownMenuItem asChild key={child.key}>
|
||||
<Link href={child.href} className="w-full flex items-center">
|
||||
{child.label}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
key={item.key}
|
||||
variant="ghost"
|
||||
className="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 text-lg font-medium transition-all duration-200"
|
||||
>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={localizedPath('/anlaesse/hochzeit')} className="w-full flex items-center">
|
||||
{t('header.occasions.wedding', 'Hochzeit')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={localizedPath('/anlaesse/geburtstag')} className="w-full flex items-center">
|
||||
{t('header.occasions.birthday', 'Geburtstag')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={localizedPath('/anlaesse/firmenevent')} className="w-full flex items-center">
|
||||
{t('header.occasions.corporate', 'Firmenevent')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button asChild variant="ghost" className="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 text-lg font-medium transition-all duration-200">
|
||||
<Link href={localizedPath('/kontakt')}>
|
||||
{t('header.contact', 'Kontakt')}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -175,10 +213,138 @@ const Header: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navItems.map((item) => (
|
||||
item.children ? (
|
||||
<div key={item.key} className="space-y-2">
|
||||
<p className="text-sm font-semibold uppercase text-muted-foreground">{item.label}</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{item.children.map((child) => (
|
||||
<SheetClose asChild key={child.key}>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="flex items-center justify-between rounded-md border border-transparent bg-gray-50 px-3 py-2 text-base font-medium 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"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
<span>{child.label}</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-4">
|
||||
<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-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>
|
||||
<div className="flex flex-col gap-3">
|
||||
{auth.user ? (
|
||||
<>
|
||||
<SheetClose asChild>
|
||||
<Link
|
||||
href={localizedPath('/profile')}
|
||||
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"
|
||||
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"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
Bestellungen
|
||||
</Link>
|
||||
</SheetClose>
|
||||
<Button variant="destructive" onClick={handleLogout}>
|
||||
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"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
{t('header.login')}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
<SheetClose asChild>
|
||||
<Link
|
||||
href={localizedPath('/register')}
|
||||
className="rounded-md bg-pink-500 px-3 py-2 text-base font-semibold text-white transition hover:bg-pink-600"
|
||||
onClick={handleNavSelect}
|
||||
>
|
||||
{t('header.register')}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
Reference in New Issue
Block a user