geraete/components/UserMenu.tsx
2025-12-05 13:53:29 +01:00

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}
/>
</>
);
}