- NextAuth session/jwt callbacks no longer return null (the client useSession() hook chokes on a null session, crashing the app on sign-out for deactivated/refreshed sessions) — fixes #57 - UserMenu/Header sign-out handlers close the Manage Users overlay and popover before calling signOut(), and no longer let a rejected signOut() promise go unhandled - Added an app-level error boundary as a safety net for any remaining client render errors - Registration and forgot-password flows now tell the user honestly when the confirmation/reset email failed to send instead of always claiming success — related to #56 - lib/email.ts throws a clear, actionable error when neither SMTP nor Resend is configured, instead of a cryptic SDK error - Documented required SMTP/Resend env vars in DEPLOYMENT_CHECKLIST.md (were missing entirely, which is why no confirmation/reset emails were ever sent in deployed environments) — fixes #56
180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useSession, signOut } from 'next-auth/react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { User, Upload, Users, LogIn, UserPlus } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import { LoginDialog } from '@/components/LoginDialog';
|
|
import { RegisterDialog } from '@/components/RegisterDialog';
|
|
import { ManageUsersPageClient } from '@/app/admin/users/ManageUsersPageClient';
|
|
|
|
function UserMenu() {
|
|
const { data: session, status } = useSession();
|
|
const router = useRouter();
|
|
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
|
|
const [registerDialogOpen, setRegisterDialogOpen] = useState(false);
|
|
const [manageUsersOpen, setManageUsersOpen] = useState(false);
|
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
|
|
const handleSignOut = async () => {
|
|
// Close any overlays tied to the authenticated state (e.g. Manage Users)
|
|
// *before* the session clears, otherwise their effects/portals can unmount
|
|
// mid-transition and throw (issue #57).
|
|
setPopoverOpen(false);
|
|
setManageUsersOpen(false);
|
|
try {
|
|
await signOut({ callbackUrl: '/' });
|
|
} catch (error) {
|
|
console.error('[SIGN_OUT] Failed to sign out cleanly:', error);
|
|
router.push('/');
|
|
}
|
|
};
|
|
|
|
if (status === 'loading') {
|
|
return <div className="h-9 w-9 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700" />;
|
|
}
|
|
|
|
if (session?.user) {
|
|
return (
|
|
<>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
asChild
|
|
className="bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800"
|
|
>
|
|
<Link href="/upload" aria-label="Upload Photos">
|
|
<Upload className="h-5 w-5" />
|
|
</Link>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Upload your own photos</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9 rounded-full"
|
|
aria-label="Account menu"
|
|
>
|
|
<User className="h-5 w-5 text-orange-600" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-56 p-2 z-[110]" align="end">
|
|
<div className="space-y-1">
|
|
<div className="px-2 py-1.5">
|
|
<p className="text-sm font-medium text-secondary">
|
|
{session.user.name || 'User'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{session.user.email}
|
|
</p>
|
|
</div>
|
|
<div className="border-t pt-1">
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
|
|
onClick={() => {
|
|
setPopoverOpen(false);
|
|
router.push('/upload');
|
|
}}
|
|
>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
Upload Photos
|
|
</Button>
|
|
{session.user.isAdmin && (
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
|
|
onClick={() => {
|
|
setPopoverOpen(false);
|
|
setManageUsersOpen(true);
|
|
}}
|
|
>
|
|
<Users className="mr-2 h-4 w-4" />
|
|
Manage Users
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-start text-sm text-secondary hover:text-secondary hover:bg-secondary/10"
|
|
onClick={() => {
|
|
setPopoverOpen(false);
|
|
handleSignOut();
|
|
}}
|
|
>
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
{manageUsersOpen && (
|
|
<ManageUsersPageClient onClose={() => setManageUsersOpen(false)} />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setLoginDialogOpen(true)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<LogIn className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Sign in</span>
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => setRegisterDialogOpen(true)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<UserPlus className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Sign up</span>
|
|
</Button>
|
|
<LoginDialog
|
|
open={loginDialogOpen}
|
|
onOpenChange={(open) => {
|
|
setLoginDialogOpen(open);
|
|
}}
|
|
onOpenRegister={() => {
|
|
setLoginDialogOpen(false);
|
|
setRegisterDialogOpen(true);
|
|
}}
|
|
/>
|
|
<RegisterDialog
|
|
open={registerDialogOpen}
|
|
onOpenChange={(open) => {
|
|
setRegisterDialogOpen(open);
|
|
}}
|
|
onOpenLogin={() => {
|
|
setRegisterDialogOpen(false);
|
|
setLoginDialogOpen(true);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default UserMenu;
|