Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
3 changes: 3 additions & 0 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const routeDescriptions: Record<string, string> = {
"/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.",
Expand Down
18 changes: 18 additions & 0 deletions src/app/factsheets/[slug]/page.tsx
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} />;
}
7 changes: 7 additions & 0 deletions src/app/factsheets/layout.tsx
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>;
}
12 changes: 12 additions & 0 deletions src/app/factsheets/page.tsx
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.",
};
Comment on lines +5 to +8

Copy link
Copy Markdown
Contributor

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/factsheets/page.tsx` around lines 5 - 8, Update the metadata
description in the factsheets page’s Metadata declaration to describe the
content as sample layouts only, removing the claim that it contains approved
local resources and clarifying that it is not clinical guidance.


export default function FactsheetsPage() {
return <FactsheetsHomePage />;
}
9 changes: 9 additions & 0 deletions src/app/factsheets/search/page.tsx
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 />;
}
109 changes: 109 additions & 0 deletions src/components/factsheets/factsheet-detail-page.tsx
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>
);
}
37 changes: 37 additions & 0 deletions src/components/factsheets/factsheet-shell.tsx
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>
);
}
84 changes: 84 additions & 0 deletions src/components/factsheets/factsheets-data.ts
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);
}
Loading
Loading