Scrum4Me/components/shared/user-menu.tsx
Madhura68 5cbf543c16 fix: call logoutAction directly via useTransition instead of form-ref submit
De form-ref-dance werkte niet betrouwbaar in de huidige base-ui:
- onSelect vuurde requestSubmit() op een hidden form
- Form zat eerst binnen DropdownMenuContent (form geunmount → ref null)
- Form daarna naar top-level verplaatst — vuurde nog steeds geen request af,
  vermoedelijk doordat onSelect in deze base-ui-build niet (consistent) een
  click-event genereerde dat de form-API trigger'de

Vervang door directe call: Server Actions kunnen sinds Next.js 14 als async
functie worden aangeroepen vanuit Client Components. useTransition voorkomt
dat de UI bevriest tijdens de redirect.

Naast onSelect ook onClick als veiligheid voor het geval base-ui later weer
van event-prop wisselt — beide handlers wijzen naar dezelfde idempotente
function (handleLogout via startTransition).

Pendingstate ('Uitloggen…' label, disabled item) zodat dubbele klikken niet
dubbele logoutAction-calls afvuren.

Quality gates: lint 0 errors, tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:40:42 +02:00

126 lines
4.2 KiB
TypeScript

'use client'
import { useTransition } from 'react'
import Link from 'next/link'
import { Settings, Sun, Globe, LogOut } from 'lucide-react'
import { logoutAction } from '@/actions/auth'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
const ROLE_LABELS: Record<string, string> = {
PRODUCT_OWNER: 'Product Owner',
SCRUM_MASTER: 'Scrum Master',
DEVELOPER: 'Developer',
}
interface UserMenuProps {
userId: string
username: string
email: string | null
roles: string[]
}
export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
const initials = username.slice(0, 2).toUpperCase()
const roleLabels = roles.map((r) => ROLE_LABELS[r]).filter(Boolean)
const subtitle = email?.trim() ? email.trim() : 'Lokaal account'
const [pendingLogout, startLogout] = useTransition()
// Server Action direct aanroepen — geen form/ref-dance. Eerdere implementatie
// gebruikte een hidden form binnen DropdownMenuContent; die unmount op
// onSelect en in deze base-ui-versie kwam de submit niet door.
function handleLogout() {
startLogout(async () => {
await logoutAction()
})
}
return (
<DropdownMenu>
<DropdownMenuTrigger
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
aria-label="Accountmenu openen"
>
<Avatar size="default">
<AvatarImage src={`/api/users/${userId}/avatar`} alt={username} />
<AvatarFallback className="bg-primary-container text-primary-container-foreground">
{initials}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} className="w-72">
<div className="flex items-center gap-3 px-2 py-2">
<Avatar size="lg">
<AvatarImage src={`/api/users/${userId}/avatar`} alt={username} />
<AvatarFallback className="bg-primary-container text-primary-container-foreground">
{initials}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-foreground truncate">{username}</div>
<div className="text-xs text-muted-foreground truncate">{subtitle}</div>
</div>
</div>
{roleLabels.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs uppercase tracking-wide text-muted-foreground">
Rollen
</DropdownMenuLabel>
<div className="px-2 pb-2 text-sm text-foreground">
{roleLabels.join(', ')}
</div>
</DropdownMenuGroup>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem render={<Link href="/settings" />}>
<Settings className="mr-2 h-4 w-4" />
<span>Instellingen</span>
</DropdownMenuItem>
<DropdownMenuItem disabled className="opacity-60">
<Sun className="mr-2 h-4 w-4" />
<span>Thema: licht</span>
<Badge className="ml-auto bg-muted text-muted-foreground text-[10px] px-1.5 py-0">
Binnenkort
</Badge>
</DropdownMenuItem>
<DropdownMenuItem disabled className="opacity-60">
<Globe className="mr-2 h-4 w-4" />
<span>Taal: Nederlands</span>
<Badge className="ml-auto bg-muted text-muted-foreground text-[10px] px-1.5 py-0">
Binnenkort
</Badge>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleLogout}
onSelect={handleLogout}
disabled={pendingLogout}
className="cursor-pointer"
>
<LogOut className="mr-2 h-4 w-4" />
<span>{pendingLogout ? 'Uitloggen…' : 'Uitloggen'}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}