Collapse sidebar footer into the user menu; tweak sub-nav & appointments

- Footer is now a single user row. The ⌘K command hint moved into the user
  menu below Theme, and the clinic switcher became a hover submenu below that
  (revealing the active clinic's name/slug/count, the switch list, and
  Create clinic). Removed SidebarCommandButton and deleted team-switcher.tsx.
- Patients sub-nav (Patients / Appointments & Schedule): no icons, no
  background change on hover/active — only the text color changes.
- Appointments & Schedule: added an "Add" button that opens the create-patient
  dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-05 01:09:08 +03:00
parent 9588d16869
commit 346954db3f
6 changed files with 145 additions and 235 deletions
@@ -1,10 +1,12 @@
"use client";
import { CalendarClock, Clock, Stethoscope, Users } from "lucide-react";
import type { ReactNode } from "react";
import { CalendarClock, Clock, Plus, Stethoscope, Users } from "lucide-react";
import { type ReactNode, useState } from "react";
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
// All figures here are mock/placeholder data — there is no scheduling backend.
@@ -199,15 +201,32 @@ function Section({
}
export function AppointmentsView() {
const [addOpen, setAddOpen] = useState(false);
// Bumped on open so the create dialog remounts with a fresh file # / form.
const [addKey, setAddKey] = useState(0);
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-10">
<div>
<h1 className="font-semibold text-2xl tracking-tight">
Appointments &amp; Schedule
</h1>
<p className="text-muted-foreground text-sm">
Today&apos;s clinic schedule and what&apos;s coming up. Sample data.
</p>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="font-semibold text-2xl tracking-tight">
Appointments &amp; Schedule
</h1>
<p className="text-muted-foreground text-sm">
Today&apos;s clinic schedule and what&apos;s coming up. Sample data.
</p>
</div>
<Button
className="rounded-3xl"
onClick={() => {
setAddKey((k) => k + 1);
setAddOpen(true);
}}
type="button"
>
<Plus className="size-4" />
Add
</Button>
</div>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
@@ -225,6 +244,14 @@ export function AppointmentsView() {
<ScheduleList items={group.items} />
</Section>
))}
<PatientFormDialog
key={addKey}
mode="create"
onCreated={() => setAddOpen(false)}
onOpenChange={setAddOpen}
open={addOpen}
/>
</div>
);
}
+1 -32
View File
@@ -1,11 +1,6 @@
"use client";
import {
ArrowDownIcon,
ArrowUpIcon,
CornerDownLeftIcon,
Search,
} from "lucide-react";
import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import {
createContext,
@@ -32,7 +27,6 @@ import {
CommandPanel,
} from "@/components/ui/command";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import { useSidebar } from "@/components/ui/sidebar";
import { navItems } from "@/lib/nav";
type CommandPaletteContextValue = { open: () => void };
@@ -171,28 +165,3 @@ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
</CommandPaletteContext.Provider>
);
}
// Sidebar-footer affordance: shows the ⌘K hint and opens the palette on click.
// Hidden when the sidebar is collapsed to its icon rail.
export function SidebarCommandButton() {
const { open } = useCommandPalette();
const { state } = useSidebar();
const { t } = useTranslation();
if (state === "collapsed") return null;
return (
<button
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-sidebar-muted hover:text-foreground"
onClick={open}
type="button"
>
<Search className="size-4" />
<span>{t("nav.quickNav")}</span>
<KbdGroup className="ml-auto">
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</button>
);
}
+1 -19
View File
@@ -5,7 +5,6 @@ import {
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
@@ -20,11 +19,9 @@ import { motion } from "framer-motion";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import type { Route } from "./nav-main";
import { SidebarCommandButton } from "@/components/command-palette";
import DashboardNavigation from "@/components/sidebar-02/nav-main";
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
import { NavUser } from "@/components/sidebar-02/nav-user";
import { OrgSwitcher } from "@/components/sidebar-02/team-switcher";
const sampleNotifications = [
{
@@ -117,22 +114,7 @@ export function DashboardSidebar() {
<DashboardNavigation routes={dashboardRoutes} />
</SidebarContent>
<SidebarFooter className="p-2">
<div
className={cn(
"flex flex-col gap-1 rounded-xl border border-sidebar-border bg-sidebar-accent/30 p-1",
isCollapsed && "border-0 bg-transparent p-0"
)}
>
<SidebarCommandButton />
<SidebarSeparator
className={cn("mx-0 my-0.5", isCollapsed && "hidden")}
/>
<OrgSwitcher />
<SidebarSeparator
className={cn("mx-0 my-0.5", isCollapsed && "hidden")}
/>
<NavUser />
</div>
<NavUser />
</SidebarFooter>
</Sidebar>
);
+1 -2
View File
@@ -89,11 +89,10 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
{route.subs?.map((subRoute) => (
<SidebarMenuSubItem key={`${route.id}-${subRoute.link}`}>
<SidebarMenuSubButton
className="text-muted-foreground"
className="text-muted-foreground hover:bg-transparent hover:text-foreground active:bg-transparent data-[active=true]:bg-transparent data-[active=true]:font-medium data-[active=true]:text-foreground"
isActive={isActive(subRoute.link)}
render={<Link href={subRoute.link} prefetch={true} />}
>
{subRoute.icon}
<span className="truncate">{subRoute.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
+106 -3
View File
@@ -1,17 +1,33 @@
"use client";
import {
Building2,
Check,
ChevronsUpDown,
LogOut,
Moon,
Plus,
Search,
Settings as SettingsIcon,
Sun,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { useState } from "react";
import { useCommandPalette } from "@/components/command-palette";
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Dialog,
DialogDescription,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import {
Menu,
MenuGroup,
@@ -20,6 +36,9 @@ import {
MenuPopup,
MenuSeparator,
MenuShortcut,
MenuSub,
MenuSubPopup,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu";
import {
@@ -57,6 +76,9 @@ function GitHubIcon({ className }: { className?: string }) {
);
}
// The sidebar's single footer row. Its menu also hosts the command-palette
// shortcut hint (below Theme) and a clinic switcher submenu (below that) whose
// popup reveals the active clinic's details on hover.
export function NavUser() {
const { isMobile, state } = useSidebar();
const isCollapsed = state === "collapsed";
@@ -64,10 +86,21 @@ export function NavUser() {
const { resolvedTheme, setTheme } = useTheme();
const isDark = resolvedTheme === "dark";
const { data } = authClient.useSession();
const { open: openCommand } = useCommandPalette();
const { data: orgs } = authClient.useListOrganizations();
const { data: activeOrg } = authClient.useActiveOrganization();
const [createOpen, setCreateOpen] = useState(false);
const name = data?.user?.name ?? "Clinician";
const email = data?.user?.email ?? "";
const initials = initialsFromName(name);
const activeName = activeOrg?.name ?? "Select clinic";
const setActive = async (organizationId: string) => {
if (organizationId === activeOrg?.id) return;
await authClient.organization.setActive({ organizationId });
};
const signOut = async () => {
const { error } = await authClient.signOut();
@@ -99,7 +132,7 @@ export function NavUser() {
<>
<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">
<span className="truncate text-muted-foreground text-xs">
{email}
</span>
</div>
@@ -109,7 +142,7 @@ export function NavUser() {
</MenuTrigger>
<MenuPopup
align="start"
className="min-w-56"
className="min-w-60"
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
sideOffset={8}
>
@@ -120,7 +153,7 @@ export function NavUser() {
</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">
<span className="truncate text-muted-foreground text-xs">
{email}
</span>
</div>
@@ -145,6 +178,61 @@ export function NavUser() {
Theme
<MenuShortcut>{isDark ? "Dark" : "Light"}</MenuShortcut>
</MenuItem>
{/* Command palette: hint + shortcut, sits below Theme. */}
<MenuItem onClick={openCommand}>
<Search />
Quick nav
<KbdGroup className="ms-auto">
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</MenuItem>
{/* Clinic switcher: hover reveals the active clinic's details. */}
<MenuSub>
<MenuSubTrigger>
<Building2 />
<span className="truncate">{activeName}</span>
</MenuSubTrigger>
<MenuSubPopup className="min-w-64">
<div className="px-2 py-1.5">
<p className="truncate font-medium text-foreground text-sm">
{activeOrg?.name ?? "No clinic selected"}
</p>
<p className="truncate text-muted-foreground text-xs">
{activeOrg?.slug ? `/${activeOrg.slug}` : "—"} ·{" "}
{orgs?.length ?? 0}{" "}
{orgs?.length === 1 ? "clinic" : "clinics"}
</p>
</div>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel className="text-muted-foreground text-xs">
Switch clinic
</MenuGroupLabel>
{(orgs ?? []).map((org) => (
<MenuItem
className="gap-2"
key={org.id}
onClick={() => setActive(org.id)}
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Building2 className="size-4 shrink-0" />
</div>
<span className="flex-1 truncate">{org.name}</span>
{org.id === activeOrg?.id && <Check className="size-4" />}
</MenuItem>
))}
</MenuGroup>
<MenuSeparator />
<MenuItem className="gap-2" onClick={() => setCreateOpen(true)}>
<Plus />
Create clinic
</MenuItem>
</MenuSubPopup>
</MenuSub>
<MenuSeparator />
<MenuItem onClick={signOut} variant="destructive">
<LogOut />
@@ -153,6 +241,21 @@ export function NavUser() {
</MenuPopup>
</Menu>
</SidebarMenuItem>
{/* Create a new clinic */}
<Dialog onOpenChange={setCreateOpen} open={createOpen}>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create clinic</DialogTitle>
<DialogDescription>
Add a new clinic and switch to it.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<CreateClinicForm onCreated={() => setCreateOpen(false)} />
</DialogPanel>
</DialogPopup>
</Dialog>
</SidebarMenu>
);
}
@@ -1,170 +0,0 @@
"use client";
import { Building2, ChevronsUpDown, Info, Plus } from "lucide-react";
import { useState } from "react";
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
import {
Dialog,
DialogDescription,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { authClient } from "@/lib/auth-client";
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">{label}</span>
<span className="truncate font-medium text-foreground">{value}</span>
</div>
);
}
// Switches the active clinic (organization). Scopes every subsequent patient
// API call. Lives in the sidebar footer; its menu also opens dialogs to view
// clinic info or create a new clinic.
export function OrgSwitcher() {
const { isMobile, state } = useSidebar();
const isCollapsed = state === "collapsed";
const { data: orgs } = authClient.useListOrganizations();
const { data: activeOrg } = authClient.useActiveOrganization();
const [infoOpen, setInfoOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const setActive = async (organizationId: string) => {
if (organizationId === activeOrg?.id) return;
await authClient.organization.setActive({ organizationId });
};
const activeName = activeOrg?.name ?? "Select clinic";
return (
<SidebarMenu>
<SidebarMenuItem>
<Menu>
<MenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
tooltip={activeName}
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-background text-foreground">
<Building2 className="size-4" />
</div>
{!isCollapsed && (
<>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{activeName}</span>
<span className="truncate text-muted-foreground text-xs">
Clinic
</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</>
)}
</MenuTrigger>
<MenuPopup
align="start"
className="min-w-56 rounded-lg"
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
sideOffset={4}
>
<MenuGroup>
<MenuGroupLabel className="text-muted-foreground text-xs">
Clinics
</MenuGroupLabel>
{(orgs ?? []).map((org) => (
<MenuItem
className="gap-2 p-2"
key={org.id}
onClick={() => setActive(org.id)}
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Building2 className="size-4 shrink-0" />
</div>
<span className="truncate">{org.name}</span>
</MenuItem>
))}
</MenuGroup>
<MenuSeparator />
<MenuItem
className="gap-2 p-2"
disabled={!activeOrg}
onClick={() => setInfoOpen(true)}
>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Info className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Clinic info
</div>
</MenuItem>
<MenuItem className="gap-2 p-2" onClick={() => setCreateOpen(true)}>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Create clinic
</div>
</MenuItem>
</MenuPopup>
</Menu>
</SidebarMenuItem>
{/* Read-only clinic details */}
<Dialog onOpenChange={setInfoOpen} open={infoOpen}>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{activeOrg?.name ?? "Clinic"}</DialogTitle>
<DialogDescription>Clinic information</DialogDescription>
</DialogHeader>
<DialogPanel className="flex flex-col gap-3 text-sm">
<InfoRow label="Name" value={activeOrg?.name ?? "—"} />
<InfoRow label="URL slug" value={activeOrg?.slug ?? "—"} />
<InfoRow
label="Clinics you belong to"
value={String(orgs?.length ?? 0)}
/>
</DialogPanel>
</DialogPopup>
</Dialog>
{/* Create a new clinic (replaces the old /onboarding redirect) */}
<Dialog onOpenChange={setCreateOpen} open={createOpen}>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create clinic</DialogTitle>
<DialogDescription>
Add a new clinic and switch to it.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<CreateClinicForm onCreated={() => setCreateOpen(false)} />
</DialogPanel>
</DialogPopup>
</Dialog>
</SidebarMenu>
);
}