8ebc1da9e4
- Astro statique, EN par defaut + /fr/ + /nl/, detection langue navigateur - i18n par fichiers JSON, ajouter une langue = ajouter des traductions - Contenu original porte a l'identique (diff d'inventaire par langue) - Chronologie legislative corrigee sur sources primaires (PE, Conseil, votes) - Timeline 23 precedents + observatoire 35 items + annuaire 66 outils FOSS, chaque affirmation verifiee et sourcee - Zero requete tierce (teste), lisible JS coupe, axe WCAG AA x2 themes - Version offline monofichier par langue a chaque build - Docker multi-stage vers nginx + CSP stricte auto-generee - CI GitHub Actions (SHA-pinnees) + verification hebdo des liens - CONTRIBUTING : divulgation d'affiliation obligatoire, allowlist de domaines testee en CI
139 lines
5.3 KiB
Plaintext
139 lines
5.3 KiB
Plaintext
---
|
|
// PART 17 — the Big Brother observatory: 35 dated, sourced drift items
|
|
// (src/data/observatory.json + per-locale prose), filterable by region group
|
|
// and by theme. The filtering is attribute-driven CSS (see <style> below):
|
|
// the buttons only set data-obs-region / data-obs-theme on the section root,
|
|
// so the no-JS state is simply "everything visible".
|
|
import { useT, type Locale } from '../i18n/config'
|
|
import { loadDataset, THEMES, type Status } from '../lib/content'
|
|
import { mdToHtml } from '../lib/md'
|
|
import Ext from './Ext.astro'
|
|
import FilterBar from './FilterBar.astro'
|
|
|
|
interface Props {
|
|
locale: Locale
|
|
}
|
|
|
|
const { locale } = Astro.props
|
|
const t = useT(locale)
|
|
|
|
const items = [...loadDataset('observatory', locale)].sort((a, b) => a.date.localeCompare(b.date))
|
|
|
|
// The UI groups the 10 raw regions into the 3 chips the dataset guarantees
|
|
// are never empty (see tests/unit/content.test.ts): EU, France, World.
|
|
const REGION_GROUPS = ['eu', 'fr', 'world'] as const
|
|
type RegionGroup = (typeof REGION_GROUPS)[number]
|
|
const regionGroup = (region: string): RegionGroup =>
|
|
region === 'eu' ? 'eu' : region === 'fr' ? 'fr' : 'world'
|
|
|
|
const STATUS_CLASS: Record<Status, string> = {
|
|
en_vigueur: 'border-warn text-warn',
|
|
adopte: 'border-warn text-warn',
|
|
negociation: 'border-lvl2 text-lvl2',
|
|
propose: 'border-info text-info',
|
|
rejete: 'border-ok text-ok',
|
|
suspendu: 'border-ok text-ok',
|
|
revele: 'border-info text-info',
|
|
}
|
|
---
|
|
|
|
<section id="observatory">
|
|
<p class="eyebrow">{t('sections2.observatory.eyebrow')}</p>
|
|
<h2>{t('sections2.observatory.title')}</h2>
|
|
<p class="text-dim">{t('sections2.observatory.intro')}</p>
|
|
<FilterBar
|
|
group="region"
|
|
label={t('sections2.observatory.filterRegion')}
|
|
allLabel={t('sections2.observatory.all')}
|
|
options={REGION_GROUPS.map((g) => ({
|
|
value: g,
|
|
label: t(`sections2.observatory.regions.${g}`),
|
|
}))}
|
|
/>
|
|
<FilterBar
|
|
group="theme"
|
|
label={t('sections2.observatory.filterTheme')}
|
|
allLabel={t('sections2.observatory.all')}
|
|
options={THEMES.map((th) => ({ value: th, label: t(`sections2.observatory.themes.${th}`) }))}
|
|
/>
|
|
<div class="mt-4">
|
|
{
|
|
items.map((ev) => (
|
|
<article
|
|
id={ev.id}
|
|
class="obs-item my-5"
|
|
data-region={regionGroup(ev.region)}
|
|
data-themes={ev.themes.join(' ')}
|
|
>
|
|
<p class="m-0 flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
<span class="font-mono text-sm font-bold">{ev.date}</span>
|
|
<span class="rounded-dossier border border-rule px-1.5 py-0.5 font-mono text-xs text-dim">
|
|
{t(`sections2.observatory.regions.${regionGroup(ev.region)}`)}
|
|
</span>
|
|
{ev.themes.map((th) => (
|
|
<span class="rounded-dossier border border-rule px-1.5 py-0.5 font-mono text-xs text-dim">
|
|
{t(`sections2.observatory.themes.${th}`)}
|
|
</span>
|
|
))}
|
|
<span
|
|
class={`rounded-dossier border px-1.5 py-0.5 font-mono text-xs tracking-wide uppercase ${STATUS_CLASS[ev.status]}`}
|
|
>
|
|
{t(`sections2.observatory.statuses.${ev.status}`)}
|
|
</span>
|
|
</p>
|
|
<h3 class="mt-1 mb-0 text-base">{ev.title}</h3>
|
|
<p class="my-1 text-sm leading-6" set:html={mdToHtml(ev.body)} />
|
|
<p class="mt-1 mb-0 font-mono text-xs">
|
|
<Ext href={ev.src.href} class="text-dim">
|
|
{ev.src.label} →
|
|
</Ext>
|
|
</p>
|
|
</article>
|
|
))
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
/* Attribute-driven filtering: one rule per chip value, mirroring
|
|
REGION_GROUPS and THEMES above. The buttons only flip the section's
|
|
data attributes — no attribute, nothing hidden (the no-JS state). */
|
|
section[data-obs-region='eu'] .obs-item:not([data-region='eu']),
|
|
section[data-obs-region='fr'] .obs-item:not([data-region='fr']),
|
|
section[data-obs-region='world'] .obs-item:not([data-region='world']),
|
|
section[data-obs-theme='messaging'] .obs-item:not([data-themes~='messaging']),
|
|
section[data-obs-theme='identity'] .obs-item:not([data-themes~='identity']),
|
|
section[data-obs-theme='money'] .obs-item:not([data-themes~='money']),
|
|
section[data-obs-theme='media'] .obs-item:not([data-themes~='media']),
|
|
section[data-obs-theme='aibio'] .obs-item:not([data-themes~='aibio']) {
|
|
display: none;
|
|
}
|
|
/* the printed dossier is always complete, whatever filters are active */
|
|
@media print {
|
|
.obs-item {
|
|
display: block !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Delegated once per page lifecycle (survives ClientRouter swaps): a
|
|
// filter button sets/removes the section's data attribute and mirrors
|
|
// the pressed state on its own group's buttons.
|
|
document.addEventListener('click', (e) => {
|
|
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-obs-filter]')
|
|
if (!btn) return
|
|
const root = btn.closest<HTMLElement>('#observatory')
|
|
if (!root) return
|
|
const group = btn.dataset.obsFilter ?? ''
|
|
const value = btn.dataset.value ?? ''
|
|
if (value) root.setAttribute(`data-obs-${group}`, value)
|
|
else root.removeAttribute(`data-obs-${group}`)
|
|
for (const other of root.querySelectorAll(`[data-obs-filter="${group}"]`)) {
|
|
other.setAttribute('aria-pressed', String(other === btn))
|
|
}
|
|
})
|
|
|
|
export {}
|
|
</script>
|