diff --git a/docs/site-map.md b/docs/site-map.md index 3b751644..f59686b4 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -14,6 +14,9 @@ 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`. +- `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/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`. diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 16b019bd..8bea4b41 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -52,6 +52,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/[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/app/factsheets/[slug]/page.tsx b/src/app/factsheets/[slug]/page.tsx new file mode 100644 index 00000000..5c6b998c --- /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 00000000..5abf7581 --- /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 00000000..958b7f05 --- /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 00000000..daf39496 --- /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 00000000..6f64db99 --- /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 00000000..4ec50947 --- /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 00000000..8deee0a3 --- /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 00000000..57a4eb9e --- /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 00000000..06530543 --- /dev/null +++ b/src/components/factsheets/factsheets-search-page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import Link from "next/link"; +import { FileSearch, FileText, Search, X } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { FormEvent, useMemo, useState } from "react"; + +import { factsheets } from "@/components/factsheets/factsheets-data"; +import { + EmptyState, + 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 [prevQuery, setPrevQuery] = useState(submittedQuery); + if (prevQuery !== submittedQuery) { + setPrevQuery(submittedQuery); + setInput(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 +

+
+ +
+
+
+ +
+
+ +
+ {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 00000000..15b49e41 --- /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); + }); +});