-
Notifications
You must be signed in to change notification settings - Fork 0
Add patient information factsheet vertical slice (home, search, detail) #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
39841d9
feat: add patient factsheet experience
BigSimmo a6ae1fd
Merge branch 'main' into codex/design-three-patient-information-pages
BigSimmo 1263a5f
fix: add factsheets route descriptions to generate-site-map and regen…
Copilot b9d0510
fix: resolve lint errors and site-map order for factsheets
cursoragent 4e03bbe
Merge remote-tracking branch 'origin/main' into codex/design-three-pa…
cursoragent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import type { Metadata } from "next"; | ||
| import { notFound } from "next/navigation"; | ||
|
|
||
| import { FactsheetDetailPage } from "@/components/factsheets/factsheet-detail-page"; | ||
| import { findFactsheet } from "@/components/factsheets/factsheets-data"; | ||
|
|
||
| export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { | ||
| const { slug } = await params; | ||
| const factsheet = findFactsheet(slug); | ||
| return { title: factsheet ? `${factsheet.title} | Patient Information` : "Factsheet not found" }; | ||
| } | ||
|
|
||
| export default async function FactsheetInfoPage({ params }: { params: Promise<{ slug: string }> }) { | ||
| const { slug } = await params; | ||
| const factsheet = findFactsheet(slug); | ||
| if (!factsheet) notFound(); | ||
| return <FactsheetDetailPage factsheet={factsheet} />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| import { FactsheetShell } from "@/components/factsheets/factsheet-shell"; | ||
|
|
||
| export default function FactsheetsLayout({ children }: { children: ReactNode }) { | ||
| return <FactsheetShell>{children}</FactsheetShell>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import type { Metadata } from "next"; | ||
|
|
||
| import { FactsheetsHomePage } from "@/components/factsheets/factsheets-home-page"; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: "Patient Information Sheets | Clinical KB", | ||
| description: "Browse patient information sheet layouts and approved local resources.", | ||
| }; | ||
|
|
||
| export default function FactsheetsPage() { | ||
| return <FactsheetsHomePage />; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import type { Metadata } from "next"; | ||
|
|
||
| import { FactsheetsSearchPage } from "@/components/factsheets/factsheets-search-page"; | ||
|
|
||
| export const metadata: Metadata = { title: "Search Patient Information | Clinical KB" }; | ||
|
|
||
| export default function FactsheetsSearchRoute() { | ||
| return <FactsheetsSearchPage />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| "use client"; | ||
|
|
||
| import Link from "next/link"; | ||
| import { ArrowLeft, Check, Copy, FileText, Printer, Share2 } from "lucide-react"; | ||
| import { useRef, useState } from "react"; | ||
|
|
||
| import type { Factsheet } from "@/components/factsheets/factsheets-data"; | ||
| import { Sheet } from "@/components/ui/sheet"; | ||
| import { cn, floatingControl, metadataPill, primaryControl, quietPanel } from "@/components/ui-primitives"; | ||
|
|
||
| export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) { | ||
| const [shareOpen, setShareOpen] = useState(false); | ||
| const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "error">("idle"); | ||
| const shareButtonRef = useRef<HTMLButtonElement>(null); | ||
|
|
||
| async function copyLink() { | ||
| try { | ||
| await navigator.clipboard.writeText(window.location.href); | ||
| setCopyStatus("copied"); | ||
| } catch { | ||
| setCopyStatus("error"); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className="mx-auto w-full max-w-7xl px-4 py-6 pb-[calc(2rem+env(safe-area-inset-bottom))] sm:px-6 sm:py-10 lg:px-8"> | ||
| <Link | ||
| href="/factsheets/search" | ||
| className="inline-flex min-h-tap items-center gap-2 text-sm font-semibold text-[color:var(--text-muted)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" | ||
| > | ||
| <ArrowLeft className="size-icon-md" aria-hidden="true" /> All factsheets | ||
| </Link> | ||
| <div className="mt-6 grid gap-8 lg:grid-cols-[minmax(0,44rem)_18rem] lg:items-start"> | ||
| <article className="min-w-0"> | ||
| <div className="flex flex-wrap items-center gap-2"> | ||
| <span className={metadataPill}>{factsheet.topic}</span> | ||
| <span className="text-xs font-semibold text-[color:var(--text-soft)]">{factsheet.readTime}</span> | ||
| </div> | ||
| <h1 className="mt-4 max-w-3xl text-3xl font-semibold tracking-tight text-[color:var(--text-heading)] sm:text-4xl"> | ||
| {factsheet.title} | ||
| </h1> | ||
| <p className="mt-4 max-w-2xl text-base leading-7 text-[color:var(--text-muted)]">{factsheet.summary}</p> | ||
| <div className={cn(quietPanel, "mt-7 flex gap-3 p-4")}> | ||
| <span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"> | ||
| <FileText className="size-icon-md" aria-hidden="true" /> | ||
| </span> | ||
| <p className="text-sm leading-6 text-[color:var(--text-muted)]"> | ||
| <strong className="font-semibold text-[color:var(--text-heading)]">Sample layout only.</strong> This page | ||
| demonstrates hierarchy and interaction; it is not clinical guidance and must be replaced with approved | ||
| local content before publication. | ||
| </p> | ||
| </div> | ||
| <div className="mt-9 space-y-8"> | ||
| {factsheet.sections.map((section) => ( | ||
| <section key={section.heading} className="max-w-2xl"> | ||
| <h2 className="text-xl font-semibold text-[color:var(--text-heading)]">{section.heading}</h2> | ||
| <p className="mt-3 text-base leading-7 text-[color:var(--text-muted)]">{section.body}</p> | ||
| </section> | ||
| ))} | ||
| </div> | ||
| </article> | ||
| <aside className={cn(quietPanel, "space-y-4 p-4 lg:sticky lg:top-6")} aria-label="Factsheet details"> | ||
| <div> | ||
| <p className="text-2xs font-semibold uppercase tracking-[0.06em] text-[color:var(--text-soft)]">For</p> | ||
| <p className="mt-1 text-sm font-semibold text-[color:var(--text-heading)]">{factsheet.audience}</p> | ||
| </div> | ||
| <div className="border-t border-[color:var(--border)] pt-4"> | ||
| <p className="text-2xs font-semibold uppercase tracking-[0.06em] text-[color:var(--text-soft)]">Status</p> | ||
| <p className="mt-1 text-sm font-semibold text-[color:var(--text-heading)]">{factsheet.updated}</p> | ||
| </div> | ||
| <div className="grid gap-2 border-t border-[color:var(--border)] pt-4"> | ||
| <button ref={shareButtonRef} type="button" onClick={() => setShareOpen(true)} className={primaryControl}> | ||
| <Share2 className="size-icon-md" aria-hidden="true" /> Share sheet | ||
| </button> | ||
| <button type="button" onClick={() => window.print()} className={floatingControl}> | ||
| <Printer className="size-icon-md" aria-hidden="true" /> Print | ||
| </button> | ||
| </div> | ||
| </aside> | ||
| </div> | ||
| <Sheet | ||
| open={shareOpen} | ||
| onClose={() => setShareOpen(false)} | ||
| title="Share this factsheet" | ||
| description="Copy a link for a colleague or patient to open this sample layout." | ||
| returnFocusRef={shareButtonRef} | ||
| > | ||
| <div className="space-y-4"> | ||
| <p className="text-sm leading-6 text-[color:var(--text-muted)]"> | ||
| Only share published, governance-approved patient information. | ||
| </p> | ||
| <button type="button" onClick={copyLink} className={primaryControl}> | ||
| {copyStatus === "copied" ? ( | ||
| <Check className="size-icon-md" aria-hidden="true" /> | ||
| ) : ( | ||
| <Copy className="size-icon-md" aria-hidden="true" /> | ||
| )} | ||
| {copyStatus === "copied" ? "Link copied" : "Copy link"} | ||
| </button> | ||
| {copyStatus === "error" ? ( | ||
| <p role="alert" className="text-sm text-[color:var(--danger)]"> | ||
| Could not copy the link. Please copy it from your browser address bar. | ||
| </p> | ||
| ) : null} | ||
| </div> | ||
| </Sheet> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import Link from "next/link"; | ||
| import { BookOpenText, Search } from "lucide-react"; | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| import { cn, navPill } from "@/components/ui-primitives"; | ||
|
|
||
| export function FactsheetShell({ children }: { children: ReactNode }) { | ||
| return ( | ||
| <div className="min-h-dvh bg-[color:var(--background)] text-[color:var(--text)]"> | ||
| <header className="border-b border-[color:var(--border)] bg-[color:var(--surface)]"> | ||
| <div className="mx-auto flex min-h-16 max-w-7xl items-center justify-between gap-3 px-4 sm:px-6 lg:px-8"> | ||
| <Link | ||
| href="/factsheets" | ||
| className="inline-flex min-h-tap min-w-0 items-center gap-2 rounded-lg text-sm font-semibold text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" | ||
| > | ||
| <span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"> | ||
| <BookOpenText className="size-icon-md" aria-hidden="true" /> | ||
| </span> | ||
| <span className="truncate">Patient information</span> | ||
| </Link> | ||
| <nav aria-label="Factsheet navigation" className="flex shrink-0 items-center gap-1.5"> | ||
| <Link href="/factsheets" className={cn(navPill, "hidden sm:inline-flex")}> | ||
| Browse | ||
| </Link> | ||
| <Link href="/factsheets/search" className={navPill}> | ||
| <Search className="size-icon-sm" aria-hidden="true" /> | ||
| Search | ||
| </Link> | ||
| </nav> | ||
| </div> | ||
| </header> | ||
| <main id="main-content">{children}</main> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| export type Factsheet = { | ||
| slug: string; | ||
| title: string; | ||
| summary: string; | ||
| topic: string; | ||
| audience: string; | ||
| readTime: string; | ||
| updated: string; | ||
| sections: Array<{ heading: string; body: string }>; | ||
| }; | ||
|
|
||
| /** | ||
| * Intentionally non-clinical sample content for the factsheet UI slice. These | ||
| * records demonstrate the layout without presenting unreviewed patient advice. | ||
| */ | ||
| export const factsheets: Factsheet[] = [ | ||
| { | ||
| slug: "appointment-planning", | ||
| title: "Planning a follow-up appointment", | ||
| summary: "A simple, patient-friendly structure for preparing questions and sharing what matters.", | ||
| topic: "Appointments", | ||
| audience: "Patients and supporters", | ||
| readTime: "3 min read", | ||
| updated: "Sample content", | ||
| sections: [ | ||
| { | ||
| heading: "Before you arrive", | ||
| body: "Use this space for approved local information about preparing for an appointment, including what a patient may want to bring or ask.", | ||
| }, | ||
| { | ||
| heading: "Questions that matter to you", | ||
| body: "A factsheet can help patients capture concerns in their own words and agree on the next step with their care team.", | ||
| }, | ||
| { | ||
| heading: "After the conversation", | ||
| body: "Add locally approved contact details, review timing, and support options here before publishing this factsheet.", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| slug: "support-network", | ||
| title: "Involving your support network", | ||
| summary: "A calm overview of choosing who to involve in a care conversation.", | ||
| topic: "Support", | ||
| audience: "Patients and supporters", | ||
| readTime: "2 min read", | ||
| updated: "Sample content", | ||
| sections: [ | ||
| { | ||
| heading: "Choose what feels helpful", | ||
| body: "Use approved local copy to explain options for inviting a trusted person to a conversation, while respecting privacy and patient choice.", | ||
| }, | ||
| { | ||
| heading: "Make a shared plan", | ||
| body: "A short, clear summary can help everyone understand the agreed next step and where to find further support.", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| slug: "care-plan-notes", | ||
| title: "Keeping notes about your care plan", | ||
| summary: "A practical page for recording agreed actions in plain language.", | ||
| topic: "Care planning", | ||
| audience: "Patients", | ||
| readTime: "2 min read", | ||
| updated: "Sample content", | ||
| sections: [ | ||
| { | ||
| heading: "Keep it clear", | ||
| body: "This sample section shows the intended reading rhythm. Replace it with approved, service-specific patient information before use.", | ||
| }, | ||
| { | ||
| heading: "Know where to get help", | ||
| body: "Include verified local contacts and escalation information only after governance review.", | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| export const factsheetTopics = ["Appointments", "Support", "Care planning"] as const; | ||
|
|
||
| export function findFactsheet(slug: string) { | ||
| return factsheets.find((factsheet) => factsheet.slug === slug); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not describe sample content as approved resources.
Line 7 claims the route contains “approved local resources,” while the rendered page explicitly says it contains sample layouts only. Metadata can appear outside the app, so make this description clearly sample-only and not clinical guidance.
🤖 Prompt for AI Agents