168 lines
6.1 KiB
TypeScript
168 lines
6.1 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 isMember = user?.role === 'member';
|
|
|
|
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>
|
|
{!isMember && (
|
|
<>
|
|
<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>
|
|
{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>
|
|
{(['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>
|
|
);
|
|
}
|