From f4e09ca886060227c2af7692574c2e22846d92af Mon Sep 17 00:00:00 2001 From: Aurealibe <220354117+Aurealibe@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:06:20 +0100 Subject: [PATCH] Add educational censorship-resistance quiz as a native Astro page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the standalone-HTML quiz from PR #7 into the site's real architecture so it actually ships and respects every project rule. - /quiz, /fr/quiz, /nl/quiz — prerendered per-locale routes with correct canonical + hreflang (new optional `path` prop on Base.astro). - Progressive enhancement: the twelve questions and the full scoring key (bands + per-area guidance with deep links into the guide) are server-rendered and readable with JavaScript disabled; the client script only computes the live score and reveals the result panel. - CSP-clean: bundled same-origin script (no inline handlers), bar widths set via a CSS custom property through the CSSOM (no inline styles), no third-party request. docker/csp.conf needs no new hash. - i18n via the existing core+overlay model: src/data/quiz.json (neutral scoring) + src/i18n/content//quiz.json (prose), loaded by a dedicated loadQuiz() that checks question/option/band parity in all three locales. Page chrome added to the `quiz` namespace (key parity tested). - Share is copy-link + native Web Share + X (already allowlisted); the third-party mastodonshare.com redirector from PR #7 is dropped. - TopBar "Contents" link made absolute (/#toc) so it works off the guide, plus a new "Quiz" nav link. New tests/e2e/quiz.spec.ts covers no-JS readability, scoring, the TOC link and zero third-party requests. Co-Authored-By: arnaudom <1535627+arnaudom@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 +- src/components/Quiz.astro | 470 ++++++++++++++++++++++++++++++++++ src/components/TopBar.astro | 11 +- src/data/quiz.json | 131 ++++++++++ src/i18n/content/en/quiz.json | 302 ++++++++++++++++++++++ src/i18n/content/fr/quiz.json | 302 ++++++++++++++++++++++ src/i18n/content/nl/quiz.json | 303 ++++++++++++++++++++++ src/i18n/en.json | 27 +- src/i18n/fr.json | 27 +- src/i18n/nl.json | 27 +- src/layouts/Base.astro | 19 +- src/lib/content.ts | 122 +++++++++ src/pages/[lang]/quiz.astro | 15 ++ src/pages/quiz.astro | 9 + src/styles/components.css | 44 ++++ tests/e2e/quiz.spec.ts | 60 +++++ 16 files changed, 1860 insertions(+), 12 deletions(-) create mode 100644 src/components/Quiz.astro create mode 100644 src/data/quiz.json create mode 100644 src/i18n/content/en/quiz.json create mode 100644 src/i18n/content/fr/quiz.json create mode 100644 src/i18n/content/nl/quiz.json create mode 100644 src/pages/[lang]/quiz.astro create mode 100644 src/pages/quiz.astro create mode 100644 tests/e2e/quiz.spec.ts diff --git a/README.md b/README.md index 4892589..0814cd4 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ In July 2026, the European Parliament let the "Chat Control" machinery advance Its promises, all enforced by tests: - **Static and self-contained** — no tracker, no cookie, not a single request to a third-party domain. -- **Readable with JavaScript disabled** (Tor Browser "safest" mode included) — scripts only power the theme, filters and checklist. +- **Readable with JavaScript disabled** (Tor Browser "safest" mode included) — scripts only power the theme, filters, checklist and quiz. - **Multilingual by design** — every language is a real prerendered route with correct `hreflang`; adding one means adding translation files, not touching code. +- **Educational quiz** — `/quiz` scores your censorship resistance on a 0–100 scale and points you at concrete fixes in the guide. Server-rendered questions and scoring key (readable with JS off); the score itself is computed locally in the browser, nothing sent or stored. - **Printable and mirrorable** — a single-file offline version ships with every build. ## Stack diff --git a/src/components/Quiz.astro b/src/components/Quiz.astro new file mode 100644 index 0000000..48a1653 --- /dev/null +++ b/src/components/Quiz.astro @@ -0,0 +1,470 @@ +--- +// Censorship-resistance quiz — a progressive-enhancement scorer. EVERYTHING +// the reader needs is server-rendered: the twelve questions (native radios, +// selectable with JS off) and the full scoring key (what each band means + +// where to improve, with deep links into the guide). The client script only +// computes the live score and reveals the result panel; with JS off the +// static key still teaches the whole thing. No third-party request, no inline +// styles (bar widths come from a CSS custom property set via the CSSOM, which +// style-src 'self' allows), no inline handlers (delegated on document). +import { localePath, useT, type Locale } from '../i18n/config' +import { loadQuiz } from '../lib/content' +import Ext from './Ext.astro' + +interface Props { + locale: Locale +} + +const { locale } = Astro.props +const t = useT(locale) +const { bands, questions } = loadQuiz(locale) + +const localeRoot = `${localePath(locale)}/`.replace('//', '/') +const guideHref = (anchor: string) => `${localeRoot}#${anchor}` +const total = questions.length +const progress0 = t('quiz.progress').replace('{done}', '0').replace('{total}', String(total)) +--- + +
+
+

{t('quiz.eyebrow')}

+

{t('quiz.title')}

+

{t('quiz.lede')}

+

{t('quiz.privacyNote')}

+ +
+ + + +
+

{t('quiz.privacyNote')}

+
    + { + questions.map((question, i) => ( +
  1. +
    + + {String(i + 1).padStart(2, '0')} {question.prompt} + +
    + {question.options.map((option) => ( + + ))} +
    +
    +
  2. + )) + } +
+
+ + + +
+

{t('quiz.bandsHeading')}

+
    + { + bands.map((band) => ( +
  • + + + {band.title} + + + {band.min}–{band.max} + + +

    + {band.summary} +

    +
  • + )) + } +
+ +

{t('quiz.categoriesHeading')}

+ +
+
+ + diff --git a/src/components/TopBar.astro b/src/components/TopBar.astro index 2b61b84..9229ac1 100644 --- a/src/components/TopBar.astro +++ b/src/components/TopBar.astro @@ -17,9 +17,18 @@ const t = useT(locale) > EXIT CHAT CONTROL - + {t('nav.toc')} + + {t('nav.quiz')} +