98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { LogOut, User as UserIcon, Moon, Sun } from "lucide-react";
|
|
import { logoutAction } from "@/actions/auth";
|
|
import { buttonVariants } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { MobileNav } from "./mobile-nav";
|
|
|
|
interface TopBarProps {
|
|
user: { name?: string; email: string };
|
|
}
|
|
|
|
export function TopBar({ user }: TopBarProps) {
|
|
const router = useRouter();
|
|
|
|
function toggleDark() {
|
|
const html = document.documentElement;
|
|
html.classList.toggle("dark");
|
|
localStorage.setItem("theme", html.classList.contains("dark") ? "dark" : "light");
|
|
}
|
|
|
|
// Render the trigger as a plain button (no asChild + Slot). The asChild +
|
|
// Slot + Radix Trigger chain is fragile under React 19 — the click handler
|
|
// doesn't always toggle the menu even though data-state is set. A direct
|
|
// button is reliable and gets the same shadcn styling via buttonVariants().
|
|
return (
|
|
<header className="sticky top-0 z-20 flex h-16 items-center justify-between gap-2 border-b bg-background/95 px-4 md:px-6 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="flex items-center gap-2">
|
|
<MobileNav />
|
|
</div>
|
|
<div className="hidden md:block text-sm text-muted-foreground">
|
|
{user.name || user.email}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={toggleDark}
|
|
aria-label="Toggle theme"
|
|
className={cn(buttonVariants({ variant: "ghost", size: "icon" }), "relative")}
|
|
>
|
|
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
</button>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
className={cn(
|
|
buttonVariants({ variant: "ghost", size: "sm" }),
|
|
"gap-2 px-2"
|
|
)}
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-semibold"
|
|
>
|
|
{(user.name || user.email).charAt(0).toUpperCase()}
|
|
</span>
|
|
<span className="hidden sm:inline text-sm font-medium">
|
|
{user.name || user.email}
|
|
</span>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-medium">{user.name || "User"}</span>
|
|
<span className="text-xs text-muted-foreground">{user.email}</span>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => router.push("/profile")}>
|
|
<UserIcon className="mr-2 h-4 w-4" />
|
|
Profile
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<form action={logoutAction}>
|
|
<DropdownMenuItem asChild>
|
|
<button type="submit" className="w-full text-left">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Log out
|
|
</button>
|
|
</DropdownMenuItem>
|
|
</form>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|