Patients page, clinical Settings re-theme, Polar-style user footer

- Patients: new /patients directory table (name, MRN, age·sex, status,
  last seen, allergies) with search + Add patient; clicking a row opens
  the record in the chat via /?patient=<#>. chat-panel reads that search
  param (Suspense-wrapped) and runs the lookup once on arrival. Sidebar
  "Patients" now links to /patients; lib/patients gains listPatients().
- Settings: re-themed to clinical tabs (Profile · Records · Signing ·
  Care team · Developers) — clinician profile, patient notifications,
  patient-owned-storage/signed-records features, and a Signing key panel.
  Same layout/components, only content changed.
- Sidebar footer: Polar-style NavUser (avatar + name + chevron) opening a
  menu upward — Settings · Docs & GitHub · Theme · Log out. Collapsed
  ("locked"): avatar only, name on hover (tooltip), menu opens to the
  right. Placeholder identity (no auth yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-01 21:55:33 +03:00
parent a2654a87e1
commit dec150c77d
10 changed files with 379 additions and 124 deletions
+6 -1
View File
@@ -1,10 +1,15 @@
import { Suspense } from "react";
import { SidebarInset } from "@/components/ui/sidebar"; import { SidebarInset } from "@/components/ui/sidebar";
import { ChatPanel } from "@/components/chat/chat-panel"; import { ChatPanel } from "@/components/chat/chat-panel";
export default function Home() { export default function Home() {
return ( return (
<SidebarInset className="flex flex-1 flex-col overflow-hidden"> <SidebarInset className="flex flex-1 flex-col overflow-hidden">
<ChatPanel /> {/* ChatPanel reads the `?patient=` search param, so it needs a Suspense boundary. */}
<Suspense>
<ChatPanel />
</Suspense>
</SidebarInset> </SidebarInset>
); );
} }
+10
View File
@@ -0,0 +1,10 @@
import { SidebarInset } from "@/components/ui/sidebar";
import { PatientsView } from "@/components/patients/patients-view";
export default function PatientsPage() {
return (
<SidebarInset className="flex flex-1 flex-col overflow-y-auto">
<PatientsView />
</SidebarInset>
);
}
+14 -1
View File
@@ -2,7 +2,8 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { ChatStatus } from "ai"; import type { ChatStatus } from "ai";
import { useCallback, useState } from "react"; import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { import {
Conversation, Conversation,
@@ -106,6 +107,18 @@ export function ChatPanel() {
const handleStop = useCallback(() => setStatus("ready"), []); const handleStop = useCallback(() => setStatus("ready"), []);
// Opening a patient from the Patients page lands here as `/?patient=<file#>`;
// run the lookup once on arrival.
const searchParams = useSearchParams();
const requestedPatient = searchParams.get("patient");
const handledPatientRef = useRef<string | null>(null);
useEffect(() => {
if (requestedPatient && handledPatientRef.current !== requestedPatient) {
handledPatientRef.current = requestedPatient;
send(`/patient ${requestedPatient}`);
}
}, [requestedPatient, send]);
const promptInput = ( const promptInput = (
<ChatInput onStop={handleStop} onSubmit={send} status={status} /> <ChatInput onStop={handleStop} onSubmit={send} status={status} />
); );
@@ -0,0 +1,136 @@
"use client";
import { Plus, Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { listPatients, type Patient } from "@/lib/patients";
type BadgeVariant = "secondary" | "destructive" | "outline";
const statusVariant: Record<Patient["status"], BadgeVariant> = {
active: "secondary",
inpatient: "destructive",
discharged: "outline",
};
export function PatientsView() {
const router = useRouter();
const [query, setQuery] = useState("");
const [addOpen, setAddOpen] = useState(false);
// Bumped on open so the create dialog remounts with a fresh file # / form.
const [addKey, setAddKey] = useState(0);
const q = query.trim().toLowerCase();
const patients = listPatients().filter(
(p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q)
);
const open = (fileNumber: string) => router.push(`/?patient=${fileNumber}`);
return (
<div className="mx-auto w-full max-w-4xl px-6 py-10">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Patients</h1>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
<Input
className="w-full pl-9 sm:w-64"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search name or MRN"
value={query}
/>
</div>
<Button
className="rounded-3xl"
onClick={() => {
setAddKey((k) => k + 1);
setAddOpen(true);
}}
type="button"
>
<Plus className="size-4" />
Add patient
</Button>
</div>
</div>
<div className="mt-8 overflow-hidden rounded-2xl border border-border bg-card/30">
<table className="w-full text-sm">
<thead>
<tr className="border-border border-b text-left text-xs text-muted-foreground uppercase">
<th className="px-4 py-3 font-medium">Name</th>
<th className="px-4 py-3 font-medium">MRN</th>
<th className="px-4 py-3 font-medium">Age · Sex</th>
<th className="px-4 py-3 font-medium">Status</th>
<th className="px-4 py-3 font-medium">Last seen</th>
<th className="px-4 py-3 font-medium">Allergies</th>
</tr>
</thead>
<tbody>
{patients.length === 0 ? (
<tr>
<td
className="px-4 py-10 text-center text-muted-foreground"
colSpan={6}
>
No patients found.
</td>
</tr>
) : (
patients.map((p) => (
<tr
className="cursor-pointer border-border/50 border-b transition-colors last:border-0 hover:bg-accent/50"
key={p.fileNumber}
onClick={() => open(p.fileNumber)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
open(p.fileNumber);
}
}}
role="button"
tabIndex={0}
>
<td className="px-4 py-3 font-medium text-foreground">
{p.name}
</td>
<td className="px-4 py-3 text-muted-foreground">
{p.fileNumber}
</td>
<td className="px-4 py-3 text-muted-foreground">
{p.age} · {p.sex}
</td>
<td className="px-4 py-3">
<Badge className="capitalize" variant={statusVariant[p.status]}>
{p.status}
</Badge>
</td>
<td className="px-4 py-3 text-muted-foreground">
{p.encounters[0]?.date ?? "—"}
</td>
<td className="px-4 py-3 text-muted-foreground">
{p.allergies.length || "—"}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<PatientFormDialog
key={addKey}
mode="create"
onCreated={(fileNumber) => open(fileNumber)}
onOpenChange={setAddOpen}
open={addOpen}
/>
</div>
);
}
@@ -4,58 +4,52 @@ import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
CopyField,
SettingsCard, SettingsCard,
SettingsSection, SettingsSection,
whiteButton, whiteButton,
} from "@/components/settings/settings-parts"; } from "@/components/settings/settings-parts";
export function BillingPanel() { export function SigningPanel() {
return ( return (
<> <>
<SettingsCard className="flex flex-col gap-6 p-6 sm:flex-row sm:items-start sm:justify-between"> <SettingsCard className="flex flex-col gap-6 p-6 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h3 className="text-xl font-semibold tracking-tight">Early Member</h3> <h3 className="text-xl font-semibold tracking-tight">Signing key</h3>
<Badge className="bg-emerald-500/15 text-emerald-400">Active</Badge> <Badge className="bg-emerald-500/15 text-emerald-400">Active</Badge>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
For our founding community of early members. Every change you make to a patient record is signed with this key, so patients
can verify it came from you before approving it.
</p> </p>
<Button className={cn("rounded-lg", whiteButton)}>Change plan</Button> <Button className={cn("rounded-lg", whiteButton)}>Rotate key</Button>
</div> </div>
<div className="sm:text-right"> <div className="sm:text-right">
<p className="text-3xl font-semibold tracking-tight">Free</p> <p className="text-3xl font-semibold tracking-tight">Ed25519</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Created May 28, 2026</p>
4.00% + $0.40 per transaction
</p>
</div> </div>
</SettingsCard> </SettingsCard>
<SettingsSection <SettingsSection
action={ description="The public key patients use to verify your signatures"
<Button className="rounded-lg" variant="secondary"> title="Signing identity"
Add payment method
</Button>
}
description="Cards used to pay for your temetro subscription"
title="Payment methods"
> >
<SettingsCard className="flex items-center justify-center p-12"> <SettingsCard className="p-5">
<p className="text-sm text-muted-foreground">No payment methods on file</p> <CopyField
description="Share or publish this fingerprint so patients can trust your changes"
label="Public key fingerprint"
value="ed25519:9f86 d081 884c 7d65 9a2f eaa0 c55a d015"
/>
</SettingsCard> </SettingsCard>
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
action={ description="Changes you've signed that are waiting on the patient's approval"
<Button className="rounded-lg" variant="secondary"> title="Signed records"
Edit
</Button>
}
description="Used on invoices for your temetro subscription"
title="Billing address"
> >
<SettingsCard className="p-5"> <SettingsCard className="flex items-center justify-center p-12">
<p className="text-sm">khalid</p> <p className="text-sm text-muted-foreground">No pending signatures</p>
</SettingsCard> </SettingsCard>
</SettingsSection> </SettingsSection>
</> </>
@@ -2,7 +2,6 @@
import { ChevronDown, Plus } from "lucide-react"; import { ChevronDown, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -12,59 +11,54 @@ import {
SettingsCard, SettingsCard,
SettingsSection, SettingsSection,
ToggleRow, ToggleRow,
whiteButton,
} from "@/components/settings/settings-parts"; } from "@/components/settings/settings-parts";
const customerNotifications = [ const patientNotifications = [
{ {
title: "Order confirmation", title: "New lab result",
description: "Sent when a customer completes a one-time purchase", description: "Sent when a new lab result is available on a patient's chart",
}, },
{ {
title: "Subscription confirmation", title: "Record updated",
description: "Sent when a customer starts a new subscription", description: "Sent when a patient's record is updated by a member of the care team",
}, },
{ {
title: "Subscription cycled", title: "Approval requested",
description: "Sent when a subscription automatically renews", description: "Sent when a signed change is awaiting the patient's approval",
}, },
{ {
title: "Trial converted", title: "Change approved",
description: "Sent when a trial ends and the subscription becomes paid", description: "Sent when a patient approves a pending change to their record",
}, },
{ {
title: "Renewal reminder", title: "New message",
description: "Sent 7 days before a subscription with a long billing cycle renews", description: "Sent when a patient or another clinician sends a message",
}, },
{ {
title: "Trial conversion reminder", title: "Visit scheduled",
description: "Sent before a trial ends and converts to a paid subscription", description: "Sent when an upcoming visit is added to a patient's record",
},
{
title: "Subscription updated",
description: "Sent when a customer changes their subscription to a different product",
}, },
]; ];
export function PreferencesPanel() { export function ProfilePanel() {
return ( return (
<> <>
<SettingsSection title="Organization"> <SettingsSection title="Clinician profile">
<SettingsCard className="space-y-6 p-5"> <SettingsCard className="space-y-6 p-5">
<CopyField <CopyField
description="Unique identifier for your organization" description="Your unique clinician identifier, used when signing records"
label="Identifier" label="Clinician ID"
value="62a5278f-91c6-4912-b711-ee1c9c2f0a73" value="62a5278f-91c6-4912-b711-ee1c9c2f0a73"
/> />
<CopyField <CopyField
description="Used for Customer Portal, Transaction Statements, etc." description="Used in your public profile and the patient portal"
label="Organization Slug" label="Handle"
value="khalid" value="dr-khalid"
/> />
<div className="flex items-end gap-4"> <div className="flex items-end gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<FieldLabel>Logo</FieldLabel> <FieldLabel>Avatar</FieldLabel>
<Avatar className="size-10 rounded-xl" size="lg"> <Avatar className="size-10 rounded-xl" size="lg">
<AvatarFallback className="rounded-xl bg-muted text-sm font-medium"> <AvatarFallback className="rounded-xl bg-muted text-sm font-medium">
K K
@@ -72,55 +66,55 @@ export function PreferencesPanel() {
</Avatar> </Avatar>
</div> </div>
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<FieldLabel required>Organization Name</FieldLabel> <FieldLabel required>Display name</FieldLabel>
<Input defaultValue="khalid" /> <Input defaultValue="Dr. Khalid" />
</div> </div>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<FieldLabel>Country</FieldLabel> <FieldLabel>Specialty</FieldLabel>
<button <button
className="flex h-9 w-full items-center justify-between rounded-3xl bg-input/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-input/70" className="flex h-9 w-full items-center justify-between rounded-3xl bg-input/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-input/70"
type="button" type="button"
> >
Select country Select specialty
<ChevronDown className="size-4" /> <ChevronDown className="size-4" />
</button> </button>
</div> </div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<FieldLabel required>Website</FieldLabel> <FieldLabel>Clinic / practice</FieldLabel>
<Input placeholder="https://acme.com" /> <Input placeholder="e.g. Main Hospital" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<FieldLabel required>Support Email</FieldLabel> <FieldLabel required>Contact email</FieldLabel>
<Input placeholder="support@acme.com" /> <Input placeholder="clinician@example.org" />
</div> </div>
</div> </div>
<div className="space-y-2.5"> <div className="space-y-2.5">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FieldLabel>Social Media</FieldLabel> <FieldLabel>Professional links</FieldLabel>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Your personal social media links are used for identity verification. They Registry or institutional profiles used to verify your identity. They are
will never be shown publicly. never shown to patients.
</p> </p>
</div> </div>
<Button className="rounded-lg" size="sm" variant="outline"> <Button className="rounded-lg" size="sm" variant="outline">
<Plus className="size-4" /> <Plus className="size-4" />
Add Social Add link
</Button> </Button>
</div> </div>
</SettingsCard> </SettingsCard>
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
description="Emails automatically sent to customers for purchases, renewals, and other subscription lifecycle events" description="Emails sent to patients about their records, results, and pending approvals"
title="Customer notifications" title="Patient notifications"
> >
<div className="space-y-3"> <div className="space-y-3">
{customerNotifications.map((item) => ( {patientNotifications.map((item) => (
<ToggleRow <ToggleRow
defaultChecked defaultChecked
description={item.description} description={item.description}
@@ -132,63 +126,49 @@ export function PreferencesPanel() {
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
description="Emails sent to members of your organization for account and product activity" description="Notifications sent to you about your patients and the care team"
title="Account notifications" title="Account notifications"
> >
<div className="space-y-3"> <div className="space-y-3">
<ToggleRow <ToggleRow
defaultChecked defaultChecked
description="Send a notification when new orders are created" description="Notify me when a patient approves or rejects a pending change"
title="New Orders" title="Pending approvals"
/> />
<ToggleRow <ToggleRow
defaultChecked defaultChecked
description="Send a notification when new subscriptions are created" description="Notify me when a patient shares a record with me"
title="New Subscriptions" title="Records shared with me"
/> />
</div> </div>
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
description="Manage alpha & beta features for your organization" description="Manage alpha & beta features for your account"
title="Features" title="Features"
> >
<div className="space-y-3"> <div className="space-y-3">
<ToggleRow <ToggleRow
description="Show translated checkouts to your customers" description="Write records to the patient's own device instead of your database"
title="Localized Checkout" title="Patient-owned storage (beta)"
/> />
<ToggleRow <ToggleRow
description="Enable seat-based pricing for subscription products. Requires the member model to be enabled." description="Require a signature on every change you make to a patient record"
title="Seat-Based Billing" title="Require signed records"
/> />
</div> </div>
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
description="Manage access tokens to authenticate with the temetro API" description="Irreversible actions for your account"
title="Developers"
>
<SettingsCard className="flex flex-col items-start gap-4 p-6">
<p className="text-sm text-muted-foreground">
You don&apos;t have any active organization access tokens.
</p>
<Button className={cn("rounded-lg", whiteButton)} size="sm">
Create token
</Button>
</SettingsCard>
</SettingsSection>
<SettingsSection
description="Irreversible actions for this organization"
title="Danger Zone" title="Danger Zone"
> >
<SettingsCard className="flex items-center justify-between gap-4 p-4"> <SettingsCard className="flex items-center justify-between gap-4 p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<p className="text-sm font-medium">Delete Organization</p> <p className="text-sm font-medium">Delete account</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Permanently delete this organization and all associated data. This action Permanently delete your temetro account and any locally stored signing
cannot be undone. keys. This action cannot be undone.
</p> </p>
</div> </div>
<Button className="rounded-lg bg-destructive text-white hover:bg-destructive/90"> <Button className="rounded-lg bg-destructive text-white hover:bg-destructive/90">
+19 -19
View File
@@ -7,15 +7,15 @@ import {
SettingsCard, SettingsCard,
SettingsSection, SettingsSection,
} from "@/components/settings/settings-parts"; } from "@/components/settings/settings-parts";
import { BillingPanel } from "@/components/settings/settings-billing"; import { SigningPanel } from "@/components/settings/settings-billing";
import { PreferencesPanel } from "@/components/settings/settings-preferences"; import { ProfilePanel } from "@/components/settings/settings-preferences";
const TABS = [ const TABS = [
"Preferences", "Profile",
"Billing", "Records",
"Members", "Signing",
"Webhooks", "Care team",
"Custom Fields", "Developers",
] as const; ] as const;
type Tab = (typeof TABS)[number]; type Tab = (typeof TABS)[number];
@@ -37,7 +37,7 @@ function PlaceholderPanel({
} }
export function SettingsView() { export function SettingsView() {
const [tab, setTab] = useState<Tab>("Preferences"); const [tab, setTab] = useState<Tab>("Profile");
return ( return (
<div className="mx-auto w-full max-w-3xl px-6 py-10"> <div className="mx-auto w-full max-w-3xl px-6 py-10">
@@ -63,24 +63,24 @@ export function SettingsView() {
</div> </div>
<div className="mt-10 space-y-12"> <div className="mt-10 space-y-12">
{tab === "Preferences" && <PreferencesPanel />} {tab === "Profile" && <ProfilePanel />}
{tab === "Billing" && <BillingPanel />} {tab === "Records" && (
{tab === "Members" && (
<PlaceholderPanel <PlaceholderPanel
description="Invite and manage members of your organization" description="How patient records are sourced, stored, and displayed"
title="Members" title="Records"
/> />
)} )}
{tab === "Webhooks" && ( {tab === "Signing" && <SigningPanel />}
{tab === "Care team" && (
<PlaceholderPanel <PlaceholderPanel
description="Send event notifications to your own endpoints" description="Clinicians with access to this workspace"
title="Webhooks" title="Care team"
/> />
)} )}
{tab === "Custom Fields" && ( {tab === "Developers" && (
<PlaceholderPanel <PlaceholderPanel
description="Collect additional information from your customers" description="Access tokens for the temetro API"
title="Custom Fields" title="Developers"
/> />
)} )}
</div> </div>
@@ -15,6 +15,7 @@ import Image from "next/image";
import type { Route } from "./nav-main"; import type { Route } from "./nav-main";
import DashboardNavigation from "@/components/sidebar-02/nav-main"; import DashboardNavigation from "@/components/sidebar-02/nav-main";
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications"; import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
import { NavUser } from "@/components/sidebar-02/nav-user";
const sampleNotifications = [ const sampleNotifications = [
{ {
@@ -51,7 +52,7 @@ const dashboardRoutes: Route[] = [
id: "patients", id: "patients",
title: "Patients", title: "Patients",
icon: <Users className="size-4" />, icon: <Users className="size-4" />,
link: "#", link: "/patients",
}, },
{ {
id: "settings", id: "settings",
@@ -109,12 +110,7 @@ export function DashboardSidebar() {
<DashboardNavigation routes={dashboardRoutes} /> <DashboardNavigation routes={dashboardRoutes} />
</SidebarContent> </SidebarContent>
<SidebarFooter className="px-2"> <SidebarFooter className="px-2">
{!isCollapsed && ( <NavUser />
<div className="flex items-baseline gap-2 px-2 py-1.5">
<span className="font-semibold text-foreground">temetro</span>
<span className="text-xs text-muted-foreground">open source</span>
</div>
)}
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
); );
+116
View File
@@ -0,0 +1,116 @@
"use client";
import { ChevronsUpDown, LogOut, Settings as SettingsIcon, Sun } from "lucide-react";
import Link from "next/link";
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";
// Placeholder identity — there is no auth backend yet.
const user = { name: "Dr. Khalid", role: "Clinician", initials: "K" };
// Open-source repo (placeholder).
const REPO_URL = "https://github.com/temetro/temetro";
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";
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
tooltip={user.name}
/>
}
>
<Avatar className="size-8">
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
{!isCollapsed && (
<>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.role}
</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>{user.initials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.role}
</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 variant="destructive">
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
+5
View File
@@ -261,6 +261,11 @@ const PATIENTS: Record<string, Patient> = {
export const SAMPLE_FILE_NUMBERS = Object.keys(PATIENTS); export const SAMPLE_FILE_NUMBERS = Object.keys(PATIENTS);
// Live list of every patient in the store (includes ones added this session).
export function listPatients(): Patient[] {
return Object.values(PATIENTS);
}
export async function getPatient(fileNumber: string): Promise<Patient | null> { export async function getPatient(fileNumber: string): Promise<Patient | null> {
// Simulate retrieval latency so the loading (skeleton) state is visible. // Simulate retrieval latency so the loading (skeleton) state is visible.
await new Promise((resolve) => setTimeout(resolve, 700)); await new Promise((resolve) => setTimeout(resolve, 700));