194 lines
5.3 KiB
TypeScript
194 lines
5.3 KiB
TypeScript
// components/UserMenu.tsx
|
|
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
|
import { signOut, useSession } from 'next-auth/react';
|
|
import PersonAvatar from '@/components/ui/UserAvatar';
|
|
import ProfileAvatarModal from '@/components/ProfileAvatarModal';
|
|
|
|
export type UserMenuProps = {
|
|
displayName: string;
|
|
avatarName: string;
|
|
avatarUrl?: string | null;
|
|
};
|
|
|
|
const userNavigation = [
|
|
{ name: 'Profilbild ändern', href: '#' },
|
|
{ name: 'Abmelden', href: '#' },
|
|
];
|
|
|
|
export default function UserMenu({
|
|
displayName,
|
|
avatarName,
|
|
avatarUrl,
|
|
}: UserMenuProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [avatarModalOpen, setAvatarModalOpen] = useState(false);
|
|
|
|
const { update } = useSession();
|
|
|
|
const [currentAvatarUrl, setCurrentAvatarUrl] = useState<string | null | undefined>(avatarUrl ?? null);
|
|
|
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
// Klick außerhalb / Escape => Menü schließen
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
const target = event.target as Node | null;
|
|
if (
|
|
menuRef.current &&
|
|
!menuRef.current.contains(target) &&
|
|
buttonRef.current &&
|
|
!buttonRef.current.contains(target)
|
|
) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
setOpen(false);
|
|
buttonRef.current?.focus();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
const handleToggle = () => {
|
|
setOpen((prev) => !prev);
|
|
};
|
|
|
|
const handleItemClick = (itemName: string) => {
|
|
setOpen(false);
|
|
|
|
if (itemName === 'Profilbild ändern') {
|
|
setAvatarModalOpen(true);
|
|
return;
|
|
}
|
|
|
|
if (itemName === 'Abmelden') {
|
|
void signOut({ callbackUrl: '/login' });
|
|
return;
|
|
}
|
|
};
|
|
|
|
const handleAvatarSelected = async (file: File) => {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
|
|
const res = await fetch('/api/profile/avatar', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error('Avatar-Upload fehlgeschlagen');
|
|
return;
|
|
}
|
|
|
|
|
|
const data = await res.json();
|
|
const newUrl = data.avatarUrl as string | null;
|
|
|
|
// 1) Lokal sofort aktualisieren
|
|
setCurrentAvatarUrl(newUrl);
|
|
|
|
// 2) NextAuth-Session aktualisieren → layout.tsx bekommt neue avatarUrl
|
|
await update({ avatarUrl: newUrl });
|
|
};
|
|
|
|
const handleAvatarDelete = async () => {
|
|
const res = await fetch('/api/profile/avatar', {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error('Avatar-Löschung fehlgeschlagen');
|
|
return;
|
|
}
|
|
|
|
// 1) Lokal zurück auf null
|
|
setCurrentAvatarUrl(null);
|
|
|
|
// 2) Session updaten → überall Fallback-Initialen
|
|
await update({ avatarUrl: null });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<button
|
|
ref={buttonRef}
|
|
type="button"
|
|
onClick={handleToggle}
|
|
className="relative flex items-center focus:outline-none"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
aria-controls="user-menu-dropdown"
|
|
>
|
|
<span className="absolute -inset-1.5" />
|
|
<span className="sr-only">Open user menu</span>
|
|
|
|
<PersonAvatar name={avatarName} avatarUrl={currentAvatarUrl} size="md" />
|
|
|
|
<span className="hidden lg:flex lg:items-center">
|
|
<span
|
|
aria-hidden="true"
|
|
className="ml-4 text-sm/6 font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
{displayName}
|
|
</span>
|
|
<ChevronDownIcon
|
|
aria-hidden="true"
|
|
className="ml-2 size-5 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div
|
|
ref={menuRef}
|
|
id="user-menu-dropdown"
|
|
role="menu"
|
|
aria-orientation="vertical"
|
|
className="absolute right-0 z-10 mt-2.5 w-40 origin-top-right rounded-md bg-white py-2 shadow-lg outline-1 outline-gray-900/5 dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10"
|
|
>
|
|
{userNavigation.map((item) => (
|
|
<button
|
|
key={item.name}
|
|
type="button"
|
|
role="menuitem"
|
|
onClick={() => handleItemClick(item.name)}
|
|
className="block w-full px-3 py-1 text-left text-sm/6 text-gray-900 hover:bg-gray-50 focus:outline-none dark:text-white dark:hover:bg-white/5"
|
|
>
|
|
{item.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Profilbild-Modal */}
|
|
<ProfileAvatarModal
|
|
open={avatarModalOpen}
|
|
onClose={() => setAvatarModalOpen(false)}
|
|
avatarName={avatarName}
|
|
avatarUrl={currentAvatarUrl ?? undefined}
|
|
onAvatarSelected={handleAvatarSelected}
|
|
onAvatarDelete={handleAvatarDelete}
|
|
/>
|
|
</>
|
|
);
|
|
}
|