From a0d7f53b2c4a85d363260b80fbd1aadd91362cfb Mon Sep 17 00:00:00 2001 From: David Taing Date: Sat, 15 Aug 2026 16:05:12 +1000 Subject: [PATCH 1/9] Redraw the directory as a roster against the credential model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The directory card was written before `docs/spec/profile-and-credentials.md` existed and rendered a model that no longer does: a profile-level `verified`, a `certified` flag that was declared and never read, and a `role` field the schema calls `headline`. Same failure `docs/scope.md` had before #54 — written against a superseded vocabulary, and reading as current because it once was. Three variants were prototyped on a throwaway route and the roster won, so this commits the roster and deletes the rest. Rows rather than a grid of cards, because the job here is comparing practitioners and a grid is a poor shape for it: each card is read on its own and nothing lines up between them. In rows the credentials sit in a column, so "who has actually been checked" is a vertical scan rather than eight separate readings. Two changes are load-bearing rather than stylistic. The badge now sits on the credential column instead of beside the person's name — the spec calls that a UI constraint the directory work inherits, because a narrow badge placed where it reads as a whole-profile endorsement is misleading no matter what the clearing rule does, and no schema rule can fix that reading. And the badge is derived rather than read: there is no profile-level `verified` column, so `hasVerifiedBadge` computes it from the credential rows, as does the "Verified only" filter. The types now mirror the columns `anon` is granted and nothing else, so a component cannot reach for a field the directory will never be able to select. That adds `countryCode` and per-credential `verified`, `earnedAt` and `evidenceUrl`, and removes `certified` — which the spec derives rather than stores. The location filter groups on `countryCode`, not on `location`. `location` is deliberately free text at whatever granularity the practitioner chose, so "Sydney" beside "Bengaluru, Karnataka, India (remote)" will never collapse into a usable set of chips; `country_code` is the column that exists for this. Filters are now grouped and labelled, because a country, a focus area and "Verified only" are three different kinds of claim and one flat row of chips read as an undifferentiated pile once countries were added. Two states the old card could not draw now render: a profile with no credentials at all, which nothing in the DDL forbids, and one that is entirely working-towards, which must not read as a failed check because that group is one the directory exists to include. The Enquire button carries `?about=` to `/contact`, which names the practitioner in the page, the mail subject and the body. Enquiries still route through Bluehex — no address is published on a profile. This makes `/contact` server-rendered per request rather than prerendered; the alternative, reading the param client-side under Suspense, keeps the static shell but flashes a fallback over the page's main content. Verified by temporarily populating the directory with four profiles covering the awkward cases and re-running the suite, since an empty directory exercises no rows at all: axe clean on the row markup in both viewports, the badge on exactly the profile whose earned credentials were all verified and absent on the mixed one. No flag is drawn yet — `countryCode` powers the filter, and the SVG assets are their own item in scope.md. `Practitioner` keeps its name despite CONTEXT.md noting it conflates the human with the record; that rename reaches the component, its file and its props, and belongs with #53. --- src/app/contact/contact-form.tsx | 19 +- src/app/contact/page.tsx | 12 +- src/components/practitioner-directory.tsx | 362 +++++++++++++++++----- src/lib/practitioners.ts | 94 +++++- 4 files changed, 391 insertions(+), 96 deletions(-) diff --git a/src/app/contact/contact-form.tsx b/src/app/contact/contact-form.tsx index 3abfea8..aaf4b58 100644 --- a/src/app/contact/contact-form.tsx +++ b/src/app/contact/contact-form.tsx @@ -15,8 +15,13 @@ const fieldClasses = * looks like it worked and silently loses the enquiry. Replacing this with a * route handler or a form service is tracked in issue #2 — when that lands, * the mailto fallback should stay for anyone with JavaScript disabled. + * + * `about` is the practitioner a directory enquiry concerns. Enquiries route + * through Bluehex rather than to the practitioner directly — no address is + * ever published on a profile — so this only has to say who was meant, and the + * mail still comes here. */ -export function ContactForm({ email }: { email: string }) { +export function ContactForm({ email, about }: { email: string; about?: string }) { const onSubmit = (event: React.FormEvent) => { event.preventDefault(); @@ -27,12 +32,15 @@ export function ContactForm({ email }: { email: string }) { `Name: ${value("name")}`, `Email: ${value("email")}`, `Phone: ${value("phone")}`, + ...(about ? [`About: ${about}`] : []), "", value("message"), ].join("\n"); const query = new URLSearchParams({ - subject: `Enquiry from ${value("name") || "the website"}`, + subject: about + ? `Enquiry about ${about}, from ${value("name") || "the website"}` + : `Enquiry from ${value("name") || "the website"}`, body, }); @@ -41,6 +49,13 @@ export function ContactForm({ email }: { email: string }) { return (
+ {about ? ( +

+ Enquiring about {about}. + Bluehex passes it on — practitioners are not contacted directly. +

+ ) : null} +