Medical Profile

Complete medical profile template.

Installation

npx shadcn@latest add @boardcn/medical-profile
npx shadcn@latest add @boardcn/medical-profile

npm packages

  • @remixicon/react

What it composes

20 BoardCN components, installed automatically.

Source

The 5 files the CLI copies into your project.

components/templates/medical-profile/medical-profile.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import {
  RiFileAddLine,
  RiFilter3Line,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { ImportantAlertsCard, type ImportantAlert } from "@/components/blocks/medical/important-alerts-card";
import { PatientInfoCard, type PatientDetail } from "@/components/blocks/medical/patient-info-card";
import { NotificationCenter } from "@/components/blocks/notification-center/notification-center";
import type { NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
import { Avatar } from "@/components/base/avatar/avatar";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import {
  Dropdown,
  DropdownPopover,
  DropdownTrigger,
} from "@/components/base/dropdown/dropdown";
import { ActivityRingsCard } from "@/components/charts/activity-rings-card";
import type { ActivityDay, ActivityMetric } from "@/components/charts/activity-rings-card";
import { MostActiveDaysCard } from "@/components/charts/most-active-days-card";
import { cx } from "@/utils/cx";
import { PatientsTable, type MedicalPatient, type PatientActions } from "./patients-table";
import { SleepScoreCard, type SleepMetric } from "./sleep-score-card";
import { StepsCard, type StepsWeekDatum } from "./steps-card";

function MobileNavigation({ open, onClose }: { open: boolean; onClose: () => void }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (!open) return;
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const frame = requestAnimationFrame(() => closeRef.current?.focus());
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        onClose();
        return;
      }
      if (event.key !== "Tab") return;
      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
        'button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"])',
      );
      if (!focusable?.length) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
      else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
    };
    document.addEventListener("keydown", onKeyDown);
    return () => {
      cancelAnimationFrame(frame);
      document.body.style.overflow = previousOverflow;
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [onClose, open]);

  return (
    <div className={cx("fixed inset-0 z-50 lg:hidden", open ? "pointer-events-auto" : "pointer-events-none")} aria-hidden={!open}>
      <button
        type="button"
        aria-label="Close navigation"
        onClick={onClose}
        className={cx("absolute inset-0 bg-black/40 transition-opacity duration-300", open ? "opacity-100" : "opacity-0")}
        tabIndex={open ? 0 : -1}
      />
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label="Navigation"
        className={cx(
          "absolute inset-y-0 left-0 flex w-[min(88vw,288px)] flex-col bg-background-full p-3 shadow-sidebar transition-transform duration-300 ease-in-out",
          open ? "translate-x-0" : "-translate-x-full",
        )}
      >
        <button ref={closeRef} type="button" onClick={onClose} className="sr-only">Close navigation</button>
        <DashboardSidebar mobile flat selected="medical" className="min-h-0 flex-1" />
      </div>
    </div>
  );
}

export interface MedicalProfileTemplateProps extends PatientActions {
  className?: string;
  onFilters?: () => void;
  onFileReport?: () => void;
  patientName: string;
  patientInitials: string;
  patientDetails: readonly PatientDetail[];
  alerts: readonly ImportantAlert[];
  alertsWeekCount: number;
  alertsWeekRangeLabel: string;
  patients: readonly MedicalPatient[];
  stepsWeeks: readonly StepsWeekDatum[];
  sleepMetrics: readonly SleepMetric[];
  sleepWeekRangeLabel: string;
  sleepScore?: number;
  sleepLabel?: string;
  notifications: readonly NotificationCenterItem[];
  activityMetrics: readonly ActivityMetric[];
  mostActiveDaysTotalSteps: number;
}

/**
 * Complete BoardCN medical profile template.
 * Patient and alert content are supplied by the consumer.
 */
export function MedicalProfileTemplate({
  className,
  onFilters,
  onFileReport,
  onViewChart,
  onEditPatient,
  onMorePatientActions,
  onViewPatient,
  onCopyMedicalId,
  onRemovePatient,
  patientName,
  patientInitials,
  patientDetails,
  alerts,
  alertsWeekCount,
  alertsWeekRangeLabel,
  patients,
  stepsWeeks,
  sleepMetrics,
  sleepWeekRangeLabel,
  sleepScore,
  sleepLabel,
  notifications,
  activityMetrics,
  mostActiveDaysTotalSteps,
}: MedicalProfileTemplateProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [selectedDay, setSelectedDay] = useState<ActivityDay>({ month: 6, day: 10 });
  const navigationTriggerRef = useRef<HTMLButtonElement>(null);

  const closeNavigation = () => {
    setNavigationOpen(false);
    requestAnimationFrame(() => navigationTriggerRef.current?.focus());
  };

  return (
    <div className={cx("flex min-h-screen w-full bg-background-full p-3", className)}>
      <DashboardSidebar selected="medical" className="sticky top-3 hidden h-[calc(100vh-24px)] lg:flex" />
      <MobileNavigation open={navigationOpen} onClose={closeNavigation} />

      <main className="relative z-20 -my-3 -mr-3 flex min-w-0 flex-1 justify-center overflow-x-hidden overflow-y-auto bg-background-full p-3 will-change-transform sm:pt-6 lg:z-0 lg:!transform-none lg:!rounded-none lg:overflow-visible">
        <div className="flex w-full max-w-[1300px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <Breadcrumb>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="B" /> BoardCN team
              </BreadcrumbItem>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="M" /> Mertcan
              </BreadcrumbItem>
              <BreadcrumbItem current>Medical Profile</BreadcrumbItem>
            </Breadcrumb>

            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex items-center gap-2">
                <button
                  ref={navigationTriggerRef}
                  type="button"
                  aria-label="Open navigation"
                  onClick={() => setNavigationOpen(true)}
                  className="inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs lg:hidden"
                >
                  <RiMenuLine className="size-5" aria-hidden />
                </button>
                <h1 className="text-title-2-medium text-text-primary">Medical Profile</h1>
              </div>

              <div className="flex items-center gap-2">
                <Dropdown>
                  <DropdownTrigger
                    aria-label="Notifications"
                    className="relative inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs hover:bg-background-primary-hover"
                  >
                    <RiNotification3Line className="size-5" aria-hidden />
                    <span className="absolute -top-1 -right-1 flex min-w-4 items-center justify-center rounded-full bg-button-danger px-1 text-[10px] leading-4 font-semibold text-white">5</span>
                  </DropdownTrigger>
                  <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(420px,calc(100vw-24px))] p-0">
                    <NotificationCenter notifications={[...notifications]} className="max-h-[520px] overflow-y-auto" />
                  </DropdownPopover>
                </Dropdown>
                <Button variant="secondary" size="small" leadingIcon={RiFilter3Line} onClick={() => {
                  onFilters?.();
                  document.getElementById("patients-table")?.scrollIntoView?.({ behavior: "smooth", block: "start" });
                }}>
                  Filters
                </Button>
                <Button size="small" leadingIcon={RiFileAddLine} onClick={onFileReport}>
                  File a report
                </Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
              <PatientInfoCard name={patientName} initials={patientInitials} details={patientDetails} />
              <StepsCard weeks={stepsWeeks} />
              <SleepScoreCard metrics={sleepMetrics} weekRangeLabel={sleepWeekRangeLabel} score={sleepScore} label={sleepLabel} />
              <MostActiveDaysCard
                totalSteps={mostActiveDaysTotalSteps}
                selectedDay={selectedDay}
                onSelectDay={setSelectedDay}
              />
              <ActivityRingsCard metrics={activityMetrics} selectedDay={selectedDay} />
              <ImportantAlertsCard
                alerts={alerts}
                weekCount={alertsWeekCount}
                weekRangeLabel={alertsWeekRangeLabel}
              />
            </div>
            <div id="patients-table" className="scroll-mt-3">
              <PatientsTable
                patients={patients}
                onViewChart={onViewChart}
                onEditPatient={onEditPatient}
                onMorePatientActions={onMorePatientActions}
                onViewPatient={onViewPatient}
                onCopyMedicalId={onCopyMedicalId}
                onRemovePatient={onRemovePatient}
              />
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

export default MedicalProfileTemplate;
"use client";

import { useEffect, useRef, useState } from "react";
import {
  RiFileAddLine,
  RiFilter3Line,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { ImportantAlertsCard, type ImportantAlert } from "@/components/blocks/medical/important-alerts-card";
import { PatientInfoCard, type PatientDetail } from "@/components/blocks/medical/patient-info-card";
import { NotificationCenter } from "@/components/blocks/notification-center/notification-center";
import type { NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
import { Avatar } from "@/components/base/avatar/avatar";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import {
  Dropdown,
  DropdownPopover,
  DropdownTrigger,
} from "@/components/base/dropdown/dropdown";
import { ActivityRingsCard } from "@/components/charts/activity-rings-card";
import type { ActivityDay, ActivityMetric } from "@/components/charts/activity-rings-card";
import { MostActiveDaysCard } from "@/components/charts/most-active-days-card";
import { cx } from "@/utils/cx";
import { PatientsTable, type MedicalPatient, type PatientActions } from "./patients-table";
import { SleepScoreCard, type SleepMetric } from "./sleep-score-card";
import { StepsCard, type StepsWeekDatum } from "./steps-card";

function MobileNavigation({ open, onClose }: { open: boolean; onClose: () => void }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (!open) return;
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const frame = requestAnimationFrame(() => closeRef.current?.focus());
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        onClose();
        return;
      }
      if (event.key !== "Tab") return;
      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
        'button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"])',
      );
      if (!focusable?.length) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
      else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
    };
    document.addEventListener("keydown", onKeyDown);
    return () => {
      cancelAnimationFrame(frame);
      document.body.style.overflow = previousOverflow;
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [onClose, open]);

  return (
    <div className={cx("fixed inset-0 z-50 lg:hidden", open ? "pointer-events-auto" : "pointer-events-none")} aria-hidden={!open}>
      <button
        type="button"
        aria-label="Close navigation"
        onClick={onClose}
        className={cx("absolute inset-0 bg-black/40 transition-opacity duration-300", open ? "opacity-100" : "opacity-0")}
        tabIndex={open ? 0 : -1}
      />
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label="Navigation"
        className={cx(
          "absolute inset-y-0 left-0 flex w-[min(88vw,288px)] flex-col bg-background-full p-3 shadow-sidebar transition-transform duration-300 ease-in-out",
          open ? "translate-x-0" : "-translate-x-full",
        )}
      >
        <button ref={closeRef} type="button" onClick={onClose} className="sr-only">Close navigation</button>
        <DashboardSidebar mobile flat selected="medical" className="min-h-0 flex-1" />
      </div>
    </div>
  );
}

export interface MedicalProfileTemplateProps extends PatientActions {
  className?: string;
  onFilters?: () => void;
  onFileReport?: () => void;
  patientName: string;
  patientInitials: string;
  patientDetails: readonly PatientDetail[];
  alerts: readonly ImportantAlert[];
  alertsWeekCount: number;
  alertsWeekRangeLabel: string;
  patients: readonly MedicalPatient[];
  stepsWeeks: readonly StepsWeekDatum[];
  sleepMetrics: readonly SleepMetric[];
  sleepWeekRangeLabel: string;
  sleepScore?: number;
  sleepLabel?: string;
  notifications: readonly NotificationCenterItem[];
  activityMetrics: readonly ActivityMetric[];
  mostActiveDaysTotalSteps: number;
}

/**
 * Complete BoardCN medical profile template.
 * Patient and alert content are supplied by the consumer.
 */
export function MedicalProfileTemplate({
  className,
  onFilters,
  onFileReport,
  onViewChart,
  onEditPatient,
  onMorePatientActions,
  onViewPatient,
  onCopyMedicalId,
  onRemovePatient,
  patientName,
  patientInitials,
  patientDetails,
  alerts,
  alertsWeekCount,
  alertsWeekRangeLabel,
  patients,
  stepsWeeks,
  sleepMetrics,
  sleepWeekRangeLabel,
  sleepScore,
  sleepLabel,
  notifications,
  activityMetrics,
  mostActiveDaysTotalSteps,
}: MedicalProfileTemplateProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [selectedDay, setSelectedDay] = useState<ActivityDay>({ month: 6, day: 10 });
  const navigationTriggerRef = useRef<HTMLButtonElement>(null);

  const closeNavigation = () => {
    setNavigationOpen(false);
    requestAnimationFrame(() => navigationTriggerRef.current?.focus());
  };

  return (
    <div className={cx("flex min-h-screen w-full bg-background-full p-3", className)}>
      <DashboardSidebar selected="medical" className="sticky top-3 hidden h-[calc(100vh-24px)] lg:flex" />
      <MobileNavigation open={navigationOpen} onClose={closeNavigation} />

      <main className="relative z-20 -my-3 -mr-3 flex min-w-0 flex-1 justify-center overflow-x-hidden overflow-y-auto bg-background-full p-3 will-change-transform sm:pt-6 lg:z-0 lg:!transform-none lg:!rounded-none lg:overflow-visible">
        <div className="flex w-full max-w-[1300px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <Breadcrumb>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="B" /> BoardCN team
              </BreadcrumbItem>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="M" /> Mertcan
              </BreadcrumbItem>
              <BreadcrumbItem current>Medical Profile</BreadcrumbItem>
            </Breadcrumb>

            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex items-center gap-2">
                <button
                  ref={navigationTriggerRef}
                  type="button"
                  aria-label="Open navigation"
                  onClick={() => setNavigationOpen(true)}
                  className="inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs lg:hidden"
                >
                  <RiMenuLine className="size-5" aria-hidden />
                </button>
                <h1 className="text-title-2-medium text-text-primary">Medical Profile</h1>
              </div>

              <div className="flex items-center gap-2">
                <Dropdown>
                  <DropdownTrigger
                    aria-label="Notifications"
                    className="relative inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs hover:bg-background-primary-hover"
                  >
                    <RiNotification3Line className="size-5" aria-hidden />
                    <span className="absolute -top-1 -right-1 flex min-w-4 items-center justify-center rounded-full bg-button-danger px-1 text-[10px] leading-4 font-semibold text-white">5</span>
                  </DropdownTrigger>
                  <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(420px,calc(100vw-24px))] p-0">
                    <NotificationCenter notifications={[...notifications]} className="max-h-[520px] overflow-y-auto" />
                  </DropdownPopover>
                </Dropdown>
                <Button variant="secondary" size="small" leadingIcon={RiFilter3Line} onClick={() => {
                  onFilters?.();
                  document.getElementById("patients-table")?.scrollIntoView?.({ behavior: "smooth", block: "start" });
                }}>
                  Filters
                </Button>
                <Button size="small" leadingIcon={RiFileAddLine} onClick={onFileReport}>
                  File a report
                </Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
              <PatientInfoCard name={patientName} initials={patientInitials} details={patientDetails} />
              <StepsCard weeks={stepsWeeks} />
              <SleepScoreCard metrics={sleepMetrics} weekRangeLabel={sleepWeekRangeLabel} score={sleepScore} label={sleepLabel} />
              <MostActiveDaysCard
                totalSteps={mostActiveDaysTotalSteps}
                selectedDay={selectedDay}
                onSelectDay={setSelectedDay}
              />
              <ActivityRingsCard metrics={activityMetrics} selectedDay={selectedDay} />
              <ImportantAlertsCard
                alerts={alerts}
                weekCount={alertsWeekCount}
                weekRangeLabel={alertsWeekRangeLabel}
              />
            </div>
            <div id="patients-table" className="scroll-mt-3">
              <PatientsTable
                patients={patients}
                onViewChart={onViewChart}
                onEditPatient={onEditPatient}
                onMorePatientActions={onMorePatientActions}
                onViewPatient={onViewPatient}
                onCopyMedicalId={onCopyMedicalId}
                onRemovePatient={onRemovePatient}
              />
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

export default MedicalProfileTemplate;
View this template on GitHub