From 900b6d076c2b8c96dfef45a51faa7b910af79f84 Mon Sep 17 00:00:00 2001 From: Chandula Adhikari Date: Wed, 28 Oct 2026 19:47:18 +0530 Subject: [PATCH] Add profile form docs and guide UX This change adds the MARC profile form to the documentation and explains how it syncs with the people and project repositories. It introduces a reusable ProfileFormLink component, adds form metadata in src/data/site.ts, updates the docs index and guide pages to describe both the form and GitHub-based editing flows, and adds always-open external links in MDX with screen-reader announcements. It also adds copy-to-clipboard buttons for documentation code blocks and styles them to match the docs theme, while preserving plain HTML when JavaScript is unavailable. --- astro.config.mjs | 13 +++++- package-lock.json | 31 +++++++++++++ package.json | 1 + src/components/ProfileFormLink.astro | 67 ++++++++++++++++++++++++++++ src/data/site.ts | 8 +++- src/layouts/DocsLayout.astro | 60 +++++++++++++++++++++++++ src/pages/docs/index.astro | 10 ++--- src/pages/docs/profiles.mdx | 30 +++++++++++-- src/pages/docs/site.mdx | 18 ++++++++ src/scripts/copy-code.ts | 55 +++++++++++++++++++++++ 10 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 src/components/ProfileFormLink.astro create mode 100644 src/scripts/copy-code.ts diff --git a/astro.config.mjs b/astro.config.mjs index 5ec547d..5a62ed9 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,6 +4,7 @@ import tailwindcss from "@tailwindcss/vite"; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import mdx from "@astrojs/mdx"; +import rehypeExternalLinks from "rehype-external-links"; import sitemap from "@astrojs/sitemap"; import icon from "astro-icon"; import path from "node:path"; @@ -41,7 +42,17 @@ export default defineConfig({ }, integrations: [ - mdx(), + // MDX is only used by the /docs guides: links to GitHub and other sites open in a new tab + // (announced to screen readers), so the guide stays open beside the file being edited. + mdx({ + rehypePlugins: [ + [rehypeExternalLinks, { + target: '_blank', + rel: ['noopener'], + content: { type: 'element', tagName: 'span', properties: { className: ['sr-only'] }, children: [{ type: 'text', value: ' (opens in a new tab)' }] }, + }], + ], + }), preact(), icon(), sitemap({ diff --git a/package-lock.json b/package-lock.json index 56cf098..c9aa926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "daisyui": "^5.5.14", "katex": "^0.16.47", "preact": "^10.28.1", + "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", "tailwindcss": "^4.1.18" @@ -4078,6 +4079,18 @@ "url": "https://github.com/sponsors/brc-dd" } }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -6158,6 +6171,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-katex": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", diff --git a/package.json b/package.json index 1005bbf..8551f73 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "daisyui": "^5.5.14", "katex": "^0.16.47", "preact": "^10.28.1", + "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", "tailwindcss": "^4.1.18" diff --git a/src/components/ProfileFormLink.astro b/src/components/ProfileFormLink.astro new file mode 100644 index 0000000..513f0b0 --- /dev/null +++ b/src/components/ProfileFormLink.astro @@ -0,0 +1,67 @@ +--- +// "Use the profile form" box for the documentation guides (forms.profile in src/data/site.ts). +import Button from "./ui/Button.astro"; +import Icon from "./ui/Icon.astro"; +import Todo from "./ui/Todo.astro"; +import { forms, site } from "../data/site"; +--- + + +{!forms.profile && Set forms.profile in src/data/site.ts to the form's share link (marc_people/google-form/README.md, step 7).} + + diff --git a/src/data/site.ts b/src/data/site.ts index 8bd7973..404e6ff 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -130,8 +130,14 @@ export const contentRepos = { website: "https://github.com/pdnMARC/pdnmarc.github.io", }; +// Google Forms used by the documentation. Set `profile` to the form's share link once it exists +// (marc_people/google-form/README.md, step 7); until then the guides ask people to contact MARC. +export const forms = { + profile: "https://docs.google.com/forms/d/e/1FAIpQLSeXOCqNmVEKqgairmF4IRnbU388iqUDekWeDIOYbHRi7tNWUQ/viewform" as string | null, +}; + export const docs = [ - { href: "/docs/profiles/", label: "People profiles", icon: "material-symbols:person-outline", text: "Add yourself or update your photo, role and biography." }, + { href: "/docs/profiles/", label: "People profiles", icon: "material-symbols:person-outline", text: "Use the profile form, or edit on GitHub: photo, role, biography and links." }, { href: "/docs/projects/", label: "Projects", icon: "material-symbols:science-outline", text: "Add a project, its team, cover image and description." }, { href: "/docs/news/", label: "News & vacancies", icon: "material-symbols:newspaper", text: "Post a news article, a vacancy with a deadline, or a social feed item." }, { href: "/docs/publications/", label: "Publications", icon: "material-symbols:menu-book-outline", text: "Add papers to the publications sheet and link them to projects and people." }, diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro index 91dce56..53da09f 100644 --- a/src/layouts/DocsLayout.astro +++ b/src/layouts/DocsLayout.astro @@ -47,6 +47,10 @@ const current = Astro.url.pathname.replace(/\/?$/, "/"); + + diff --git a/src/pages/docs/index.astro b/src/pages/docs/index.astro index 5024826..3e320e1 100644 --- a/src/pages/docs/index.astro +++ b/src/pages/docs/index.astro @@ -5,7 +5,7 @@ import Icon from "../../components/ui/Icon.astro"; import { docs, contentRepos, site } from "../../data/site"; const where = [ - { what: "People profiles", place: "marc_people repository, profiles/ folder", href: contentRepos.people, guide: "/docs/profiles/" }, + { what: "People profiles", place: "Profile form, or marc_people repository, profiles/ folder", href: contentRepos.people, guide: "/docs/profiles/" }, { what: "Projects", place: "marc_projects repository, projects/ folder", href: contentRepos.projects, guide: "/docs/projects/" }, { what: "News articles and vacancies", place: "marc_news repository, articles/ folder", href: contentRepos.news, guide: "/docs/news/" }, { what: "Social feed items", place: "Website data Google Sheet, News tab", guide: "/docs/news/#social-feed-items" }, @@ -18,8 +18,8 @@ const tasks = [ { title: "A new member joins MARC and a project", steps: [ - { where: "marc_people", href: "/docs/profiles/#add-a-new-profile", text: "add their profile, with their email and name, plus any other addresses (otherEmails) and the forms of their name used in their papers (otherNames)." }, - { where: "marc_projects", href: "/docs/profiles/#add-yourself-to-your-projects", text: "add the same email to each project they work on." }, + { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "they fill it in once: profile, photo, other emails and names, and the projects they work on." }, + { where: "Or on GitHub", href: "/docs/profiles/#add-a-new-profile", text: "add the profile in marc_people and their email to each project file in marc_projects." }, { where: "Their papers", href: "/docs/publications/#linking-papers-to-people", text: "check each paper appears on their profile; if not, add the author-list form of their name to otherNames." }, ], }, @@ -32,13 +32,13 @@ const tasks = [ { title: "Someone's email address changes", steps: [ - { where: "marc_people", href: "/docs/profiles/#changing-your-email-or-name", text: "put the new address in email and move the old one to otherEmails. Project files stay as they are." }, + { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "enter the new address under Other email addresses. Project files stay as they are." }, ], }, { title: "Someone leaves MARC", steps: [ - { where: "marc_people", href: "/docs/profiles/#when-you-leave-marc", text: "set status: past and, if known, currentPosition. Don't delete the profile." }, + { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "choose Past member and say where they are now (or set status: past on GitHub). Don't delete the profile." }, { where: "marc_projects", href: "/docs/profiles/#when-you-leave-marc", text: "nothing to change: they stay on their projects, marked as a past member." }, ], }, diff --git a/src/pages/docs/profiles.mdx b/src/pages/docs/profiles.mdx index bd232c0..ca0b089 100644 --- a/src/pages/docs/profiles.mdx +++ b/src/pages/docs/profiles.mdx @@ -5,10 +5,34 @@ description: Add yourself to the People page, or update your photo, role and bio --- import Callout from "../../components/ui/Callout.astro"; +import ProfileFormLink from "../../components/ProfileFormLink.astro"; -Each person at MARC keeps their own profile up to date. Profiles live in the -[marc_people repository](https://github.com/pdnMARC/marc_people) on GitHub: one Markdown file per person -in the `profiles/` folder, with photos in `profiles/photos/`. +Each person at MARC keeps their own profile up to date, either with the **profile form** (no GitHub +account needed) or by editing it on GitHub. + +## Use the profile form + + + +The form adds a new profile or updates yours, and keeps every link intact: + +- **Updating?** Fill in only what should change. Blank answers keep what is already there. +- **Other email addresses and other forms of your name** are added to your profile, never removed, so + projects and papers that use them stay linked to you. +- **Photo:** upload a JPG, PNG or WebP image. It is resized and its camera and location data removed. +- **Projects:** tick the projects you work on and you are added to their teams. +- **Leaving MARC?** Choose *Past member* and say where you are now. + +Every night at midnight (Sri Lanka time) your answers become a change request. It goes live once a MARC +maintainer approves it. If something can't be used, for example an update from an address that is not in +any profile, a maintainer will be in touch. + +The rest of this guide is for editing on GitHub, and explains what each field means. + +## Edit on GitHub + +Profiles live in the [marc_people repository](https://github.com/pdnMARC/marc_people) on GitHub: one +Markdown file per person in the `profiles/` folder, with photos in `profiles/photos/`. ```text marc_people/ diff --git a/src/pages/docs/site.mdx b/src/pages/docs/site.mdx index 85f87f1..df7051d 100644 --- a/src/pages/docs/site.mdx +++ b/src/pages/docs/site.mdx @@ -26,6 +26,24 @@ A few values here point at content in the other repositories and must match it e +## Profile form + +MARC members can add or update their profile with a Google Form instead of GitHub. Every night at 00:00 +Sri Lanka time, the **Profile Form Sync** workflow in marc_people and in marc_projects reads the form's +response sheet and opens pull requests: + +- in **marc_people**, one per response, with the new or updated profile and its photo; +- in **marc_projects**, adding the person to the projects they ticked. + +Responses that can't be used become an issue instead. Each pull request lists what changed, who sent it +and the validator result, with a short review checklist. **Merge** to publish or **close** to reject; +a response is never proposed twice. To process new responses straight away, run the workflow from the +repository's **Actions** tab. + +Setup (the form, sharing, the `GOOGLE_FORM_CSV_LINK` secret) and maintenance are described in +[marc_people/google-form/README.md](https://github.com/pdnMARC/marc_people/blob/main/google-form/README.md). +Once the form exists, put its share link in `forms.profile` in `src/data/site.ts` so the guides show it. + ## Where things are | What | Where | diff --git a/src/scripts/copy-code.ts b/src/scripts/copy-code.ts new file mode 100644 index 0000000..41abb83 --- /dev/null +++ b/src/scripts/copy-code.ts @@ -0,0 +1,55 @@ +// "Copy" buttons for the code blocks in the documentation guides (DocsLayout). Folder trees +// (```text) are left alone. Added by script, so without JavaScript the blocks are simply shown. +const blocks = document.querySelectorAll(".docs__body pre[data-language]:not([data-language='text'])"); + +async function copyText(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Older browsers / insecure contexts: copy through a temporary selection. + const area = document.createElement("textarea"); + area.value = text; + area.setAttribute("readonly", ""); + area.style.position = "fixed"; + area.style.opacity = "0"; + document.body.append(area); + area.select(); + const ok = document.execCommand("copy"); + area.remove(); + return ok; + } +} + +blocks.forEach((pre) => { + const wrap = document.createElement("div"); + wrap.className = "code-copy"; + pre.before(wrap); + wrap.append(pre); + + const button = document.createElement("button"); + button.type = "button"; + button.className = "code-copy__btn"; + button.setAttribute("aria-label", "Copy code to clipboard"); + button.innerHTML = + 'Copy'; + const label = button.querySelector("span")!; + const status = document.createElement("span"); + status.className = "sr-only"; + status.setAttribute("role", "status"); + + let timer: number | undefined; + button.addEventListener("click", async () => { + const ok = await copyText(pre.querySelector("code")?.textContent ?? pre.textContent ?? ""); + label.textContent = ok ? "Copied" : "Copy failed"; + status.textContent = ok ? "Code copied to clipboard" : "Could not copy the code"; + button.classList.toggle("is-done", ok); + window.clearTimeout(timer); + timer = window.setTimeout(() => { + label.textContent = "Copy"; + status.textContent = ""; + button.classList.remove("is-done"); + }, 2000); + }); + wrap.append(button, status); +});