mega-plant/components/AccountMenu.tsx
Dave 2594dead84 feat: add user access and improve plant dashboard
Add PocketBase login, first-sign-in password changes, account settings,
and user activation, reactivation and deletion with self-removal guards.
Protect application pages and APIs and keep private pages out of PWA
caches.

Resolve plant locations from site assignments and improve map controls,
profit chart views, navigation and mobile table layouts.

Validation: production build and TypeScript passed; 28 unit tests passed.
Backend access integration tests and mobile verification are documented.
2026-10-06 04:02:08 +02:00

62 lines
3.2 KiB
TypeScript

"use client";
import { useEffect, useId, useRef, useState } from "react";
import Link from "next/link";
import { ChevronDown, Moon, Settings, Sun, UserRound } from "lucide-react";
import { useTheme } from "./ThemeProvider";
import { LogoutButton } from "./auth/LogoutButton";
export function AccountMenu({ name, email }: { name: string; email: string }) {
const [open, setOpen] = useState(false);
const root = useRef<HTMLDivElement>(null);
const trigger = useRef<HTMLButtonElement>(null);
const panelId = useId();
const { theme, toggle } = useTheme();
useEffect(() => {
if (!open) return;
const closeOutside = (event: Event) => {
if (!root.current?.contains(event.target as Node)) setOpen(false);
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
trigger.current?.focus();
}
};
document.addEventListener("pointerdown", closeOutside);
document.addEventListener("focusin", closeOutside);
document.addEventListener("keydown", closeOnEscape);
return () => {
document.removeEventListener("pointerdown", closeOutside);
document.removeEventListener("focusin", closeOutside);
document.removeEventListener("keydown", closeOnEscape);
};
}, [open]);
const itemClass = "flex min-h-11 w-full items-center gap-3 rounded-lg px-3 text-sm text-text hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-[#30638E] dark:hover:bg-white/5";
return <div ref={root} className="relative">
<button ref={trigger} type="button" aria-label="Account and settings" title="Account and settings" aria-expanded={open} aria-controls={panelId}
onClick={() => setOpen(!open)}
className="flex min-h-11 min-w-11 items-center justify-center gap-2 rounded-xl border border-border px-1.5 text-text transition hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-[#30638E] dark:hover:bg-white/5 md:px-3">
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-[#30638E]/10 text-[#30638E] dark:text-indigo-ink"><UserRound className="h-4 w-4" /></span>
<span className="hidden max-w-32 truncate text-sm font-medium md:block">{name || "Account"}</span>
<ChevronDown className="hidden h-4 w-4 text-text-muted md:block" />
</button>
{open && <div id={panelId} className="absolute right-0 top-full mt-2 w-72 max-w-[calc(100vw-2rem)] rounded-xl border border-border bg-surface p-2 shadow-xl">
<div className="border-b border-border px-3 pb-3 pt-2">
<p className="break-words text-sm font-semibold text-text">{name || "Your account"}</p>
<p className="mt-1 break-all text-xs text-text-muted">{email}</p>
</div>
<nav aria-label="Account options" className="mt-2">
<Link href="/settings" className={itemClass} onClick={() => setOpen(false)}><Settings className="h-4 w-4" />Settings</Link>
<button type="button" onClick={toggle} className={itemClass}>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
{theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
</button>
<div className="mt-2 border-t border-border pt-2"><LogoutButton fullWidth /></div>
</nav>
</div>}
</div>;
}