91 lines
3.5 KiB
JavaScript
91 lines
3.5 KiB
JavaScript
'use client';
|
|
|
|
/**
|
|
* User menu: avatar + name with optional dropdown (account link, logout).
|
|
* Can receive user from server (e.g. from getSession()) or use useCurrentUser() on client.
|
|
*
|
|
* @param {Object} props
|
|
* @param {Object} [props.user] - User from server (getSession().user). If not provided, useCurrentUser() is used.
|
|
* @param {string} [props.accountHref='/dashboard/account'] - Link for "My account"
|
|
* @param {string} [props.logoutHref='/auth/logout'] - Link for logout
|
|
* @param {string} [props.className] - Extra classes for the menu wrapper
|
|
*/
|
|
|
|
import { Fragment } from 'react';
|
|
import { Menu, Transition } from '@headlessui/react';
|
|
import UserAvatar from './UserAvatar.js';
|
|
import { useCurrentUser } from './useCurrentUser.js';
|
|
|
|
export default function UserMenu({
|
|
user: userProp,
|
|
accountHref = '/dashboard/account',
|
|
logoutHref = '/auth/logout',
|
|
className = '',
|
|
}) {
|
|
const { user: userFromHook, loading } = useCurrentUser();
|
|
const user = userProp ?? userFromHook;
|
|
|
|
if (loading && !userProp) {
|
|
return (
|
|
<div className={`flex items-center gap-2 ${className}`}>
|
|
<div className="w-10 h-10 rounded-full bg-neutral-700 animate-pulse" />
|
|
<div className="h-4 w-24 bg-neutral-700 rounded animate-pulse" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Menu as="div" className={`relative ${className}`}>
|
|
<Menu.Button className="flex items-center gap-2 sm:gap-3 px-2 py-1.5 rounded-lg hover:bg-black/10 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-400 transition-colors">
|
|
<UserAvatar user={user} size="md" />
|
|
<span className="text-sm font-medium text-inherit truncate max-w-[120px] sm:max-w-[160px]">
|
|
{user.name || user.email || 'Account'}
|
|
</span>
|
|
<svg className="w-4 h-4 text-neutral-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</Menu.Button>
|
|
<Transition
|
|
as={Fragment}
|
|
enter="transition ease-out duration-100"
|
|
enterFrom="opacity-0 scale-95"
|
|
enterTo="opacity-100 scale-100"
|
|
leave="transition ease-in duration-75"
|
|
leaveFrom="opacity-100 scale-100"
|
|
leaveTo="opacity-0 scale-95"
|
|
>
|
|
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right rounded-lg bg-white dark:bg-neutral-900 shadow-lg border border-neutral-200 dark:border-neutral-700 py-1 focus:outline-none z-50">
|
|
<div className="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
|
<p className="text-sm font-medium text-neutral-900 dark:text-white truncate">{user.name || 'User'}</p>
|
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate">{user.email}</p>
|
|
</div>
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<a
|
|
href={accountHref}
|
|
className={`block px-4 py-2 text-sm ${active ? 'bg-neutral-100 dark:bg-neutral-800' : ''} text-neutral-700 dark:text-neutral-300`}
|
|
>
|
|
My account
|
|
</a>
|
|
)}
|
|
</Menu.Item>
|
|
<Menu.Item>
|
|
{({ active }) => (
|
|
<a
|
|
href={logoutHref}
|
|
className={`block px-4 py-2 text-sm ${active ? 'bg-neutral-100 dark:bg-neutral-800' : ''} text-neutral-700 dark:text-neutral-300`}
|
|
>
|
|
Log out
|
|
</a>
|
|
)}
|
|
</Menu.Item>
|
|
</Menu.Items>
|
|
</Transition>
|
|
</Menu>
|
|
);
|
|
}
|