Files
temetro/frontend/components/sidebar-02/nav-user.tsx
T
Claude 1ecbd16404 Wire frontend to the backend: auth, organizations, real patient API
Connects the UI-only app to the new backend over Better Auth + a small
patient API client, replacing the in-memory fixture and placeholder identity.

- Better Auth React client (lib/auth-client.ts) + shared access-control
  roles; API client (lib/api-client.ts) sending credentials cross-origin.
- Designed (auth) route group: login, signup, verify-email, forgot/reset
  password, clinic onboarding, and accept-invite.
- proxy.ts (this Next's renamed middleware) optimistic redirect + an
  authoritative client AppAuthGuard requiring a session and active clinic.
- Real identity in nav-user (useSession + sign out); the unused team
  switcher repurposed as an organization (clinic) switcher; Care-team
  settings tab manages members and invitations.
- lib/patients.ts now calls the org-scoped API (types unchanged); patients
  table and create/edit dialog updated for async create/update.
- next.config: standalone output + Dockerfile for containerized runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:28:02 +03:00

138 lines
4.6 KiB
TypeScript

"use client";
import { ChevronsUpDown, LogOut, Settings as SettingsIcon, Sun } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { authClient } from "@/lib/auth-client";
// Open-source repo (placeholder).
const REPO_URL = "https://github.com/temetro/temetro";
function initialsFromName(name: string): string {
const letters = name
.split(/\s+/)
.map((w) => w[0])
.filter(Boolean)
.join("")
.slice(0, 2);
return (letters || "?").toUpperCase();
}
function GitHubIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 .5C5.37.5 0 5.78 0 12.29c0 5.2 3.44 9.6 8.21 11.16.6.11.82-.25.82-.55 0-.27-.01-1.16-.02-2.1-3.34.73-4.04-1.42-4.04-1.42-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.21.09 1.84 1.25 1.84 1.25 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.61-2.67-.31-5.47-1.34-5.47-5.95 0-1.31.47-2.39 1.24-3.23-.12-.31-.54-1.54.12-3.21 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.67.24 2.9.12 3.21.77.84 1.24 1.92 1.24 3.23 0 4.62-2.81 5.64-5.49 5.94.43.37.82 1.1.82 2.22 0 1.6-.02 2.89-.02 3.29 0 .31.21.69.83.57A12.02 12.02 0 0 0 24 12.29C24 5.78 18.63.5 12 .5Z" />
</svg>
);
}
export function NavUser() {
const { isMobile, state } = useSidebar();
const isCollapsed = state === "collapsed";
const router = useRouter();
const { data } = authClient.useSession();
const name = data?.user?.name ?? "Clinician";
const email = data?.user?.email ?? "";
const initials = initialsFromName(name);
const signOut = async () => {
await authClient.signOut();
router.push("/login");
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
tooltip={name}
/>
}
>
<Avatar className="size-8">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
{!isCollapsed && (
<>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{name}</span>
<span className="truncate text-xs text-muted-foreground">
{email}
</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-56"
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
sideOffset={8}
>
<DropdownMenuLabel className="flex items-center gap-2 py-2 text-foreground">
<Avatar className="size-8">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{name}</span>
<span className="truncate text-xs text-muted-foreground">
{email}
</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem render={<Link href="/settings" />}>
<SettingsIcon />
Settings
</DropdownMenuItem>
<DropdownMenuItem
render={<a href={REPO_URL} rel="noreferrer" target="_blank" />}
>
<GitHubIcon className="size-4" />
Docs &amp; GitHub
</DropdownMenuItem>
<DropdownMenuItem>
<Sun />
Theme
<DropdownMenuShortcut>Dark</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOut} variant="destructive">
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}