first commit
This commit is contained in:
125
apps/frontend/components/id/user-menu.tsx
Normal file
125
apps/frontend/components/id/user-menu.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Heart,
|
||||
LockKeyhole,
|
||||
LogOut,
|
||||
ShieldCheck,
|
||||
Smartphone,
|
||||
UserRound
|
||||
} from 'lucide-react';
|
||||
import { AdminBadge } from '@/components/id/admin-badge';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { useAvatarUrl } from '@/hooks/use-avatar-url';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function maskPhone(phone?: string | null) {
|
||||
if (!phone) return null;
|
||||
const digits = phone.replace(/\D/g, '');
|
||||
if (digits.length < 10) return phone;
|
||||
return `+${digits.slice(0, 1)} ${digits.slice(1, 4)} ***-**-${digits.slice(-2)}`;
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/data', label: 'Личные данные', subtitle: 'ФИО, день рождения, пол', icon: UserRound },
|
||||
{ href: '/security', label: 'Телефон и безопасность', subtitle: 'PIN, пароль, устройства', icon: Smartphone },
|
||||
{ href: '/documents', label: 'Документы', subtitle: 'Паспорт, права, загран', icon: FileText },
|
||||
{ href: '/family', label: 'Семья', subtitle: 'Участники и доступ', icon: Heart },
|
||||
{ href: '/security', label: 'Защита аккаунта', subtitle: 'Способы входа и сессии', icon: LockKeyhole }
|
||||
];
|
||||
|
||||
export function UserMenu({ className }: { className?: string }) {
|
||||
const router = useRouter();
|
||||
const { user, token, logout } = useAuth();
|
||||
const { avatarUrl } = useAvatarUrl(user?.id, user?.hasAvatar, token);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const contactLine = [maskPhone(user.phone), user.username ?? user.email].filter(Boolean).join(' · ');
|
||||
const initials = user.displayName
|
||||
.split(' ')
|
||||
.map((part) => part[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('flex items-center gap-2 rounded-full border border-[#eceef4] bg-white py-1 pl-1 pr-3 shadow-sm transition hover:bg-[#fafbfd]', className)}
|
||||
aria-label="Меню пользователя"
|
||||
>
|
||||
<Avatar className="h-9 w-9">
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt={user.displayName} /> : null}
|
||||
<AvatarFallback className="text-xs font-semibold">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden max-w-[120px] truncate text-sm font-medium md:inline">{user.displayName}</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[min(92vw,360px)] rounded-[28px] border-[#eceef4] p-0 shadow-2xl">
|
||||
<div className="border-b border-[#eceef4] px-5 pb-5 pt-5 text-center">
|
||||
<Avatar className="mx-auto h-20 w-20">
|
||||
{avatarUrl ? <AvatarImage src={avatarUrl} alt={user.displayName} /> : null}
|
||||
<AvatarFallback className="text-lg font-semibold">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="mt-3 flex items-center justify-center gap-2">
|
||||
<p className="text-lg font-semibold">{user.displayName}</p>
|
||||
<AdminBadge user={user} />
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-[#667085]">{contactLine || 'Контакты не указаны'}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
{menuItems.map((item) => (
|
||||
<Link
|
||||
key={`${item.href}-${item.label}`}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-[18px] px-3 py-3 transition hover:bg-[#f4f5f8]"
|
||||
>
|
||||
<item.icon className="h-5 w-5 shrink-0 text-[#667085]" />
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="font-medium">{item.label}</div>
|
||||
<div className="truncate text-xs text-[#667085]">{item.subtitle}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-[#a8adbc]" />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{user.canAccessAdmin ? (
|
||||
<Link href="/admin/users" className="mt-1 flex items-center gap-3 rounded-[18px] px-3 py-3 transition hover:bg-[#f4f5f8]">
|
||||
<ShieldCheck className="h-5 w-5 shrink-0 text-[#667085]" />
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="font-medium">Админ-панель</div>
|
||||
<div className="text-xs text-[#667085]">Пользователи, OAuth и настройки</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-[#a8adbc]" />
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
logout();
|
||||
router.push('/auth/login');
|
||||
}}
|
||||
className="mt-1 flex w-full items-center gap-3 rounded-[18px] px-3 py-3 text-left transition hover:bg-[#f4f5f8]"
|
||||
>
|
||||
<LogOut className="h-5 w-5 shrink-0 text-[#667085]" />
|
||||
<div>
|
||||
<div className="font-medium">Выйти</div>
|
||||
<div className="text-xs text-[#667085]">Завершить текущую сессию</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user