From 39841d940774b614cc3f9d82fbfb465f4cb06836 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 03:26:26 +0800 Subject: [PATCH 1/3] feat: add patient factsheet experience --- docs/site-map.md | 3 + src/app/factsheets/[slug]/page.tsx | 18 +++ src/app/factsheets/layout.tsx | 7 + src/app/factsheets/page.tsx | 12 ++ src/app/factsheets/search/page.tsx | 9 ++ .../factsheets/factsheet-detail-page.tsx | 109 +++++++++++++ src/components/factsheets/factsheet-shell.tsx | 37 +++++ src/components/factsheets/factsheets-data.ts | 84 ++++++++++ .../factsheets/factsheets-home-page.tsx | 118 ++++++++++++++ .../factsheets/factsheets-search-page.tsx | 151 ++++++++++++++++++ tests/factsheets-data.test.ts | 14 ++ 11 files changed, 562 insertions(+) create mode 100644 src/app/factsheets/[slug]/page.tsx create mode 100644 src/app/factsheets/layout.tsx create mode 100644 src/app/factsheets/page.tsx create mode 100644 src/app/factsheets/search/page.tsx create mode 100644 src/components/factsheets/factsheet-detail-page.tsx create mode 100644 src/components/factsheets/factsheet-shell.tsx create mode 100644 src/components/factsheets/factsheets-data.ts create mode 100644 src/components/factsheets/factsheets-home-page.tsx create mode 100644 src/components/factsheets/factsheets-search-page.tsx create mode 100644 tests/factsheets-data.test.ts diff --git a/docs/site-map.md b/docs/site-map.md index 8497dc327..d87261005 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -17,6 +17,9 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`. - `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`. - `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. +- `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`. +- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`. +- `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`. - `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`. - `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`. - `/formulation/builder` - Structured clinical formulation builder. Source: `src/app/formulation/builder/page.tsx`. diff --git a/src/app/factsheets/[slug]/page.tsx b/src/app/factsheets/[slug]/page.tsx new file mode 100644 index 000000000..5c6b998cd --- /dev/null +++ b/src/app/factsheets/[slug]/page.tsx @@ -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 { + 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 ; +} diff --git a/src/app/factsheets/layout.tsx b/src/app/factsheets/layout.tsx new file mode 100644 index 000000000..5abf7581e --- /dev/null +++ b/src/app/factsheets/layout.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from "react"; + +import { FactsheetShell } from "@/components/factsheets/factsheet-shell"; + +export default function FactsheetsLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/app/factsheets/page.tsx b/src/app/factsheets/page.tsx new file mode 100644 index 000000000..958b7f05d --- /dev/null +++ b/src/app/factsheets/page.tsx @@ -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 ; +} diff --git a/src/app/factsheets/search/page.tsx b/src/app/factsheets/search/page.tsx new file mode 100644 index 000000000..daf394964 --- /dev/null +++ b/src/app/factsheets/search/page.tsx @@ -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 ; +} diff --git a/src/components/factsheets/factsheet-detail-page.tsx b/src/components/factsheets/factsheet-detail-page.tsx new file mode 100644 index 000000000..6f64db99b --- /dev/null +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -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(null); + + async function copyLink() { + try { + await navigator.clipboard.writeText(window.location.href); + setCopyStatus("copied"); + } catch { + setCopyStatus("error"); + } + } + + return ( +
+ +
+ ); +} diff --git a/src/components/factsheets/factsheet-shell.tsx b/src/components/factsheets/factsheet-shell.tsx new file mode 100644 index 000000000..4ec509473 --- /dev/null +++ b/src/components/factsheets/factsheet-shell.tsx @@ -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 ( +
+
+
+ + + + Patient information + + +
+
+
{children}
+
+ ); +} diff --git a/src/components/factsheets/factsheets-data.ts b/src/components/factsheets/factsheets-data.ts new file mode 100644 index 000000000..8deee0a34 --- /dev/null +++ b/src/components/factsheets/factsheets-data.ts @@ -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); +} diff --git a/src/components/factsheets/factsheets-home-page.tsx b/src/components/factsheets/factsheets-home-page.tsx new file mode 100644 index 000000000..57a4eb9e5 --- /dev/null +++ b/src/components/factsheets/factsheets-home-page.tsx @@ -0,0 +1,118 @@ +"use client"; + +import Link from "next/link"; +import { ArrowRight, FileText, Search } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { FormEvent, useState } from "react"; + +import { factsheets, factsheetTopics } from "@/components/factsheets/factsheets-data"; +import { + cn, + fieldControlWithIcon, + fieldIcon, + metadataPill, + primaryControl, + quietPanel, +} from "@/components/ui-primitives"; + +export function FactsheetsHomePage() { + const router = useRouter(); + const [query, setQuery] = useState(""); + + function submit(event: FormEvent) { + event.preventDefault(); + const value = query.trim(); + router.push(value ? `/factsheets/search?q=${encodeURIComponent(value)}` : "/factsheets/search"); + } + + return ( +
+
+

+ Patient information library +

+

+ Clear information for the next conversation. +

+

+ Find a short, plain-language factsheet to support a patient conversation. Every published sheet should be + sourced, locally approved, and easy to take away. +

+
+ +
+ +
+
+
+ +
+
+ +
+

+ Browse by topic +

+
+ {factsheetTopics.map((topic) => ( + + {topic} + + ))} +
+
+ +
+
+
+ +

Sample layouts only — not clinical guidance.

+
+ + View all sheets + +
+
+ {factsheets.map((factsheet) => ( + + + +

{factsheet.topic}

+

+ {factsheet.title} +

+

{factsheet.summary}

+

{factsheet.readTime}

+ + ))} +
+
+
+ ); +} diff --git a/src/components/factsheets/factsheets-search-page.tsx b/src/components/factsheets/factsheets-search-page.tsx new file mode 100644 index 000000000..f26d7ca14 --- /dev/null +++ b/src/components/factsheets/factsheets-search-page.tsx @@ -0,0 +1,151 @@ +"use client"; + +import Link from "next/link"; +import { FileSearch, FileText, Search, X } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { FormEvent, useEffect, useMemo, useState } from "react"; + +import { factsheets } from "@/components/factsheets/factsheets-data"; +import { + EmptyState, + LoadingPanel, + cn, + fieldControlWithIcon, + fieldIcon, + metadataPill, + primaryControl, + quietPanel, +} from "@/components/ui-primitives"; + +export function FactsheetsSearchPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const submittedQuery = (searchParams.get("q") ?? "").trim(); + const [input, setInput] = useState(submittedQuery); + const [loading, setLoading] = useState(false); + + useEffect(() => setInput(submittedQuery), [submittedQuery]); + useEffect(() => { + if (!submittedQuery) return; + setLoading(true); + const timeout = window.setTimeout(() => setLoading(false), 180); + return () => window.clearTimeout(timeout); + }, [submittedQuery]); + + const results = useMemo(() => { + const normalized = submittedQuery.toLowerCase(); + if (!normalized) return factsheets; + return factsheets.filter((factsheet) => + [factsheet.title, factsheet.summary, factsheet.topic, factsheet.audience] + .join(" ") + .toLowerCase() + .includes(normalized), + ); + }, [submittedQuery]); + + function submit(event: FormEvent) { + event.preventDefault(); + const query = input.trim(); + router.push(query ? `/factsheets/search?q=${encodeURIComponent(query)}` : "/factsheets/search"); + } + + return ( +
+

+ Find a sheet +

+

+ Search patient information +

+
+ +
+
+
+ +
+
+ +
+ {loading ? ( + + ) : results.length === 0 ? ( +
+ +
+ + Browse sheets + +
+
+ ) : ( + <> +
+

+ {submittedQuery + ? `${results.length} result${results.length === 1 ? "" : "s"} for “${submittedQuery}”` + : `${results.length} sample factsheets`} +

+ {submittedQuery ? ( + +
+
+ {results.map((factsheet) => ( + + + + + + + {factsheet.title} + + {factsheet.topic} + + + {factsheet.summary} + + + {factsheet.audience} · {factsheet.readTime} + + + + ))} +
+ + )} +
+ +
+ ); +} diff --git a/tests/factsheets-data.test.ts b/tests/factsheets-data.test.ts new file mode 100644 index 000000000..15b49e417 --- /dev/null +++ b/tests/factsheets-data.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { factsheets, findFactsheet } from "@/components/factsheets/factsheets-data"; + +describe("factsheet sample records", () => { + it("only resolves the explicitly supplied sample factsheets", () => { + expect(findFactsheet(factsheets[0]!.slug)).toEqual(factsheets[0]!); + expect(findFactsheet("unknown-factsheet")).toBeUndefined(); + }); + + it("labels every record as sample content", () => { + expect(factsheets.every((factsheet) => factsheet.updated === "Sample content")).toBe(true); + }); +}); From 1263a5f02ae8de764216a082fb0ed7edef519382 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:47:54 +0000 Subject: [PATCH 2/3] fix: add factsheets route descriptions to generate-site-map and regenerate docs/site-map.md --- docs/site-map.md | 4 ++-- scripts/generate-site-map.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/site-map.md b/docs/site-map.md index 8e2f186d8..e88f660e9 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -16,10 +16,10 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/dsm` - DSM-5 Diagnosis home. Source: `src/app/dsm/page.tsx`. - `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`. - `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`. -- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. - `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`. -- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`. - `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`. +- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`. +- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. - `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`. - `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`. - `/formulation/builder` - Structured clinical formulation builder. Source: `src/app/formulation/builder/page.tsx`. diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 921cf2b90..edd3f876f 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -51,6 +51,9 @@ const routeDescriptions: Record = { "/documents/search": "Documents search command centre.", "/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.", "/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.", + "/factsheets": "Patient information sheet library.", + "/factsheets/search": "Patient information sheet search.", + "/factsheets/[slug]": "Patient information sheet detail.", "/favourites": "Saved clinical items and sets.", "/forms": "Forms home and search surface.", "/forms/[slug]": "Registry-backed form detail.", From b9d05106b2a82bafbd213ad7b778b1cd069fde3b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 19:56:50 +0000 Subject: [PATCH 3/3] fix: resolve lint errors and site-map order for factsheets - factsheets-search-page: replace synchronous setState-in-effect with the 'adjusting state during rendering' pattern for input sync; remove the artificial loading state (search is synchronous over in-memory data). Fixes two @eslint-react/hooks-extra lint errors. - generate-site-map: register /factsheets, /factsheets/[slug], and /factsheets/search descriptions in routeDescriptions so the generator emits proper descriptions instead of 'Route discovered from app directory'. - docs/site-map.md: correct entry order so factsheets/* sorts before favourites (alphabetical localeCompare). Fixes tests/site-map.test.ts. --- scripts/generate-site-map.ts | 2 +- .../factsheets/factsheets-search-page.tsx | 29 +++++++------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index edd3f876f..ae8b1d4dd 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -52,8 +52,8 @@ const routeDescriptions: Record = { "/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.", "/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.", "/factsheets": "Patient information sheet library.", - "/factsheets/search": "Patient information sheet search.", "/factsheets/[slug]": "Patient information sheet detail.", + "/factsheets/search": "Patient information sheet search.", "/favourites": "Saved clinical items and sets.", "/forms": "Forms home and search surface.", "/forms/[slug]": "Registry-backed form detail.", diff --git a/src/components/factsheets/factsheets-search-page.tsx b/src/components/factsheets/factsheets-search-page.tsx index f26d7ca14..06530543f 100644 --- a/src/components/factsheets/factsheets-search-page.tsx +++ b/src/components/factsheets/factsheets-search-page.tsx @@ -3,12 +3,11 @@ import Link from "next/link"; import { FileSearch, FileText, Search, X } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; -import { FormEvent, useEffect, useMemo, useState } from "react"; +import { FormEvent, useMemo, useState } from "react"; import { factsheets } from "@/components/factsheets/factsheets-data"; import { EmptyState, - LoadingPanel, cn, fieldControlWithIcon, fieldIcon, @@ -22,15 +21,11 @@ export function FactsheetsSearchPage() { const searchParams = useSearchParams(); const submittedQuery = (searchParams.get("q") ?? "").trim(); const [input, setInput] = useState(submittedQuery); - const [loading, setLoading] = useState(false); - - useEffect(() => setInput(submittedQuery), [submittedQuery]); - useEffect(() => { - if (!submittedQuery) return; - setLoading(true); - const timeout = window.setTimeout(() => setLoading(false), 180); - return () => window.clearTimeout(timeout); - }, [submittedQuery]); + const [prevQuery, setPrevQuery] = useState(submittedQuery); + if (prevQuery !== submittedQuery) { + setPrevQuery(submittedQuery); + setInput(submittedQuery); + } const results = useMemo(() => { const normalized = submittedQuery.toLowerCase(); @@ -79,10 +74,8 @@ export function FactsheetsSearchPage() { -
- {loading ? ( - - ) : results.length === 0 ? ( +
+ {results.length === 0 ? (
) : ( - <> +

{submittedQuery - ? `${results.length} result${results.length === 1 ? "" : "s"} for “${submittedQuery}”` + ? `${results.length} result${results.length === 1 ? "" : "s"} for "${submittedQuery}"` : `${results.length} sample factsheets`}

{submittedQuery ? ( @@ -139,7 +132,7 @@ export function FactsheetsSearchPage() { ))}
- +
)}