f4e09ca886
CI / ci (push) Failing after 6m53s
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/<locale>/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) <noreply@anthropic.com>
119 lines
4.6 KiB
Plaintext
119 lines
4.6 KiB
Plaintext
---
|
|
import { ClientRouter } from 'astro:transitions'
|
|
import Footer from '../components/Footer.astro'
|
|
import TopBar from '../components/TopBar.astro'
|
|
import { localePath, locales, useT, type Locale } from '../i18n/config'
|
|
import '../styles/global.css'
|
|
import '../styles/components.css'
|
|
|
|
interface Props {
|
|
locale: Locale
|
|
// Subpage slug appended after the locale prefix ('' = home). Drives the
|
|
// canonical + hreflang URLs so /quiz and /fr/quiz point at themselves.
|
|
path?: string
|
|
}
|
|
|
|
const { locale, path = '' } = Astro.props
|
|
const t = useT(locale)
|
|
const site = Astro.site!
|
|
|
|
const localeRoot = (l: Locale) => `${localePath(l)}/`.replace('//', '/')
|
|
const localeHref = (l: Locale) =>
|
|
new URL(path ? `${localeRoot(l)}${path}` : localeRoot(l), site).href
|
|
|
|
const canonical = new URL(localeHref(locale))
|
|
const alternates = locales.map((l) => ({ hreflang: l, href: localeHref(l) }))
|
|
const xDefaultHref = new URL(path ? `/${path}` : '/', site).href
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang={locale} data-theme="">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>{t('site.title')}</title>
|
|
<meta name="description" content={t('site.description')} />
|
|
<link rel="canonical" href={canonical.href} />
|
|
{alternates.map((a) => <link rel="alternate" hreflang={a.hreflang} href={a.href} />)}
|
|
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
|
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
<link rel="apple-touch-icon" href="/favicon.png" />
|
|
<link rel="manifest" href="/site.webmanifest" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:site_name" content="Exit Chat Control" />
|
|
<meta property="og:title" content={t('site.title')} />
|
|
<meta property="og:description" content={t('site.description')} />
|
|
<meta property="og:url" content={canonical.href} />
|
|
<meta property="og:image" content={new URL('/og.png', site).href} />
|
|
<meta property="og:locale" content={locale} />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<ClientRouter />
|
|
<script is:inline>
|
|
/* Boot script — kept tiny and dependency-free; its sha256 hash is
|
|
pinned in the CSP header (docker/nginx.conf). Block comments only:
|
|
this tag is inlined into prerendered HTML.
|
|
1) Theme: apply the stored override before first paint.
|
|
2) Language: on the default-locale root only, honor a stored
|
|
explicit choice, else follow the browser language once.
|
|
No JS = English, which is the fallback anyway. */
|
|
;(() => {
|
|
const LANGS = ['en', 'fr', 'nl']
|
|
const read = (key) => {
|
|
try {
|
|
return localStorage.getItem(key) || ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
const applyTheme = () => {
|
|
const theme = read('ecc-theme')
|
|
if (theme === 'light' || theme === 'dark') {
|
|
document.documentElement.setAttribute('data-theme', theme)
|
|
}
|
|
}
|
|
applyTheme()
|
|
document.addEventListener('astro:after-swap', applyTheme)
|
|
if (location.pathname === '/') {
|
|
const stored = read('ecc-lang')
|
|
let target = ''
|
|
if (stored) {
|
|
if (stored !== 'en' && LANGS.includes(stored)) target = stored
|
|
} else {
|
|
const nav = (navigator.languages || [navigator.language || ''])
|
|
.map((l) => String(l).slice(0, 2).toLowerCase())
|
|
.find((l) => LANGS.includes(l))
|
|
if (nav && nav !== 'en') target = nav
|
|
}
|
|
if (target) location.replace('/' + target + '/')
|
|
}
|
|
})()
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<a href="#content" class="skip-link">{t('nav.skip')}</a>
|
|
<div class="sheet">
|
|
<TopBar locale={locale} />
|
|
<noscript>
|
|
<p class="noscript-note">{t('noscript.notice')}</p>
|
|
</noscript>
|
|
<slot />
|
|
<Footer locale={locale} />
|
|
</div>
|
|
<script>
|
|
// Print: closed <details> don't print their content (print.css can't
|
|
// open them). Open every closed one on beforeprint, restore the
|
|
// reader's state on afterprint. Registered once, survives ClientRouter
|
|
// swaps (window listeners are not tied to the swapped DOM).
|
|
let printOpened: Element[] = []
|
|
window.addEventListener('beforeprint', () => {
|
|
printOpened = [...document.querySelectorAll('details:not([open])')]
|
|
for (const d of printOpened) d.setAttribute('open', '')
|
|
})
|
|
window.addEventListener('afterprint', () => {
|
|
for (const d of printOpened) d.removeAttribute('open')
|
|
printOpened = []
|
|
})
|
|
</script>
|
|
</body>
|
|
</html>
|