Files
fotospiel-app/resources/js/admin/components/UserMenu.tsx

162 lines
5.9 KiB
TypeScript

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { HelpCircle, LogOut, Monitor, Moon, Settings, Sun, User, Languages, CreditCard } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { useAuth } from '../auth/context';
import { ADMIN_FAQ_PATH, ADMIN_PROFILE_PATH, ADMIN_SETTINGS_PATH, ADMIN_BILLING_PATH } from '../constants';
import { useAppearance } from '@/hooks/use-appearance';
import { SUPPORTED_LANGUAGES, getCurrentLocale, switchLocale, type SupportedLocale } from '../lib/locale';
export function UserMenu() {
const { user, logout } = useAuth();
const { appearance, updateAppearance } = useAppearance();
const navigate = useNavigate();
const { t } = useTranslation('common');
const [pendingLocale, setPendingLocale] = React.useState<SupportedLocale | null>(null);
const currentLocale = getCurrentLocale();
const initials = React.useMemo(() => {
if (user?.name) {
return user.name
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0])
.join('')
.toUpperCase();
}
if (user?.email) {
return user.email.charAt(0).toUpperCase();
}
return 'TU';
}, [user?.name, user?.email]);
const changeLanguage = React.useCallback(async (locale: SupportedLocale) => {
if (locale === currentLocale) {
return;
}
setPendingLocale(locale);
try {
await switchLocale(locale);
} finally {
setPendingLocale(null);
}
}, [currentLocale]);
const changeAppearance = React.useCallback(
(mode: 'light' | 'dark' | 'system') => {
updateAppearance(mode);
},
[updateAppearance]
);
const goTo = React.useCallback((path: string) => navigate(path), [navigate]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2 rounded-full border border-transparent px-2">
<Avatar className="h-8 w-8">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<span className="hidden text-sm font-semibold sm:inline">
{user?.name || user?.email || t('app.userMenu')}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuLabel>
<p className="text-sm font-semibold">{user?.name ?? t('user.unknown')}</p>
{user?.email ? <p className="text-xs text-slate-500">{user.email}</p> : null}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => goTo(ADMIN_PROFILE_PATH)}>
<User className="h-4 w-4" />
{t('navigation.profile', { defaultValue: 'Profil' })}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => goTo(ADMIN_SETTINGS_PATH)}>
<Settings className="h-4 w-4" />
{t('navigation.settings', { defaultValue: 'Einstellungen' })}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => goTo(ADMIN_BILLING_PATH)}>
<CreditCard className="h-4 w-4" />
{t('navigation.billing', { defaultValue: 'Billing' })}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="h-4 w-4" />
<span>{t('app.languageSwitch')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent align="end">
{SUPPORTED_LANGUAGES.map(({ code, labelKey }) => (
<DropdownMenuItem
key={code}
className="flex items-center justify-between"
onSelect={(event) => {
event.preventDefault();
changeLanguage(code);
}}
disabled={pendingLocale === code}
>
<span>{t(labelKey)}</span>
{currentLocale === code ? <span className="text-xs text-rose-500">{t('app.languageActive', { defaultValue: 'Aktiv' })}</span> : null}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{appearance === 'dark' ? <Moon className="h-4 w-4" /> : appearance === 'light' ? <Sun className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
<span>{t('app.theme', { defaultValue: 'Darstellung' })}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent align="end">
{(['light', 'dark', 'system'] as const).map((mode) => (
<DropdownMenuItem key={mode} onSelect={() => changeAppearance(mode)}>
{mode === 'light' ? <Sun className="h-4 w-4" /> : mode === 'dark' ? <Moon className="h-4 w-4" /> : <Monitor className="h-4 w-4" />}
<span>{t(`app.theme_${mode}`, { defaultValue: mode })}</span>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => goTo(ADMIN_FAQ_PATH)}>
<HelpCircle className="h-4 w-4" />
{t('app.help', { defaultValue: 'FAQ & Hilfe' })}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={(event) => {
event.preventDefault();
logout();
}}
>
<LogOut className="h-4 w-4" />
{t('app.logout', { defaultValue: 'Abmelden' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}