Files
nav-carte/pages/agences.vue
T
Jules NenyandClaude Opus 5.5 d4e72942fc fix(vue-fiches): accessibilité et contrôles M8 (Lighthouse 24/24 ≥ 95)
Nom accessible de la carte-fiche = son contenu ; résumé et « Sans localisation »
en color-mix(--text 78 %) ; libellé du tri gardé pour les lecteurs d'écran sous
768 px ; lien « sans adresse » hors du parcours clavier quand le compteur est
masqué ; bouton Chatbot flottant en fond plein ; option sélectionnée explicite
des <select> (rendu serveur) ; pastille « Sans adresse » ; cluster allumé au
survol ; pas d'init Leaflet sur un conteneur détaché.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCt6e8wWiYqRXLJAuKqP5i
2026-10-02 18:10:35 +02:00

275 lines
11 KiB
Vue

<template>
<div class="relative h-full overflow-hidden" style="background: var(--nav-bg);">
<!-- IntentionBanner s'auto-affiche via Teleport (overlay plein écran) -->
<IntentionBanner />
<!-- ═══════════════════════════════════════ VUE FICHES (B11) : Carte · Mixte · Fiches -->
<VueFiches
:vf="vf"
intention="120 réseaux, collectifs et agences où des pensées écologiques deviennent des pratiques d'architecture."
libelle-recherche="Rechercher une structure…"
:pending="pending"
:selected-id="selectedId"
@select="(f) => onSelectStructure(String(f.id))"
>
<template #carte>
<!-- Métropole -->
<div v-show="vf.etat.sousVue === 'metropole'" class="relative flex-1 min-h-0">
<ClientOnly>
<NavMapV2
:structures="metropoleStructures"
:selectedId="selectedId"
:survolId="vf.survolId == null ? null : String(vf.survolId)"
@select-structure="onSelectStructure"
@survol="(id) => (vf.survolId = id)"
/>
<template #fallback>
<div class="w-full h-full flex items-center justify-center" style="background: var(--nav-bg-alt); color: var(--nav-text-muted);">Chargement de la carte…</div>
</template>
</ClientOnly>
</div>
<!-- Outre-mer : pas de structure V2 en DOM-TOM pour l'instant (composant gardé vide) -->
<div v-show="vf.etat.sousVue === 'outremer'" class="flex-1 min-h-0 overflow-y-auto" style="background: var(--nav-bg);">
<ClientOnly>
<OutremerMap :orgs="[]" :selectedId="null" @select-org="() => {}" />
<template #fallback>
<div class="flex items-center justify-center h-48 text-sm" style="color: var(--nav-text-muted);">Chargement…</div>
</template>
</ClientOnly>
</div>
<!-- Vue graphique -->
<div v-show="vf.etat.sousVue === 'graphe'" class="relative flex-1 min-h-0 overflow-hidden">
<ClientOnly>
<GraphView
:data="bifurcationData"
:allHashtags="allHashtags"
:active="vf.etat.sousVue === 'graphe' && vf.vueAffichee !== 'fiches'"
@select-structure="onSelectStructure"
/>
<template #fallback>
<div class="flex items-center justify-center h-full" style="color: var(--nav-text-muted);">Chargement du graphe…</div>
</template>
</ClientOnly>
</div>
<!-- Chatbot sous la carte (ordinateur) -->
<div v-if="vf.etat.sousVue !== 'outremer'" class="hidden lg:block shrink-0">
<ChatbotPlaceholder
@highlightOrgs="() => {}"
@applyHashtag="(tag) => vf.ajouterFiltre('tags', sansDiese(tag))"
/>
</div>
</template>
</VueFiches>
<!-- ═══════════════════════════════════════ MODAL FICHE V2 -->
<FicheModalV2
v-model="ficheModalOpen"
:structureId="ficheModalId"
:data="bifurcationData"
@update:structureId="ficheModalId = $event"
/>
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
<button
class="lg:hidden fixed right-4 z-[1000] flex items-center gap-2 px-4 rounded-full shadow-lg"
:style="`
bottom: ${basFlottants}px;
height: 48px;
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
box-shadow: 0 4px 16px rgba(26,34,56,0.25);
font-family: var(--nav-font);
font-size: 0.875rem;
font-weight: 600;
`"
aria-label="Ouvrir l'assistant Chatbot"
@click="chatbotOpen = true"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<span>Chatbot</span>
</button>
<!-- ═══════════════════════════════════════ CHATBOT BOTTOM SHEET (mobile) -->
<ChatbotReseaux
:modelValue="chatbotOpen"
@update:modelValue="chatbotOpen = $event"
/>
<!-- ═══════════════════════════════════════ POP-UP MISSION RÉSEAUX AEP -->
<button
class="reseaux-info-btn"
type="button"
:style="vf.largeur === 'telephone' ? `bottom: ${basFlottants}px;` : undefined"
@click="missionOpen = true"
aria-label="À propos des réseaux AEP cartographiés"
title="À propos de cette carte"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
</button>
<MissionPopup
:modelValue="missionOpen"
@update:modelValue="missionOpen = $event"
title="Réseaux AEP — l'architecture qui s'engage"
ctaLabel="Explorer les 120 réseaux"
storageKey="aep_reseaux_seen"
echelle="M"
>
<p class="mission-text">
Cette carte rassemble <strong>120 réseaux, collectifs et agences</strong> qui pratiquent une architecture engagée — écologique, politique, biorégionale. Ce ne sont pas seulement des agences «&nbsp;vertes&nbsp;»&nbsp;: ce sont celles et ceux qui assument des positions, refusent des projets, expérimentent des modèles de gouvernance, mettent leurs ressources et leurs savoirs en commun.
</p>
<p class="mission-text">
Six familles structurent la cartographie&nbsp;: militants, agences engagées, collectifs de production, ressources communes, recherche, formations alternatives. Filtre par hashtag, ouvre la fiche d'une structure, navigue le graphe («&nbsp;Vue graphique&nbsp;» dans le sélecteur de carte) pour voir les affinités. Si tu animes ou connais un réseau qui devrait y être&nbsp;: <NuxtLink to="/contribuer" @click.stop>propose-le</NuxtLink>.
</p>
</MissionPopup>
</div>
</template>
<script setup lang="ts">
import type { ReseauxBifurcationData, StructureV2 } from '~/types/structure-v2'
import type { ConfigVueFiches, ValeurFiltre } from '~/utils/vueFiches'
import { sansDiese } from '~/composables/useFicheAdapter'
// ── Données V2 - JSON statique ─────────────────────────────────────────────
const bifurcationData = ref<ReseauxBifurcationData | null>(null)
const pending = ref(true)
onMounted(async () => {
try {
bifurcationData.value = await $fetch<ReseauxBifurcationData>('/data/reseaux-bifurcation.json')
} catch (e) {
console.error('Erreur chargement reseaux-bifurcation.json', e)
} finally {
pending.value = false
}
})
const structures = computed<StructureV2[]>(() => bifurcationData.value?.structures ?? [])
// Tous les hashtags uniques triés (le graphe les attend avec leur « # »)
const allHashtags = computed<string[]>(() => {
const set = new Set<string>()
structures.value.forEach(s => s.hashtags.forEach(h => set.add(h)))
return Array.from(set).sort()
})
// ── Vue fiches (B11) : l'état vit dans l'URL (tags=, famille=, carte=…) ────
// Familles : 1-5 = famille principale ou secondaire, 6 = badge « recherche politique »
const FAMILLES: ValeurFiltre[] = [
{ valeur: '1', libelle: 'Réemploi' },
{ valeur: '2', libelle: 'Frugalité' },
{ valeur: '3', libelle: 'Social' },
{ valeur: '4', libelle: 'Collectifs' },
{ valeur: '5', libelle: 'Urbanisme' },
{ valeur: '6', libelle: 'Recherche' },
]
const PAYS: Record<string, string> = {
FR: 'France', BE: 'Belgique', DE: 'Allemagne', UK: 'Royaume-Uni', ES: 'Espagne', IT: 'Italie',
PT: 'Portugal', CH: 'Suisse', AT: 'Autriche', DK: 'Danemark', PL: 'Pologne', NL: 'Pays-Bas',
SE: 'Suède', USA: 'États-Unis', INT: 'International',
}
/** Valeurs d'une étiquette, de la plus fréquente à la plus rare (ordre stable des chips et des groupes). */
function parFrequence(cle: (s: StructureV2) => string[], libelle: (v: string) => string): ValeurFiltre[] {
const n = new Map<string, number>()
structures.value.forEach(s => cle(s).forEach(v => n.set(v, (n.get(v) ?? 0) + 1)))
return [...n.entries()]
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'fr'))
.map(([v]) => ({ valeur: v, libelle: libelle(v) }))
}
const config = computed<ConfigVueFiches>(() => ({
dimensions: [
{
cle: 'tags', libelle: 'Hashtag', variante: 'fonction',
valeurs: parFrequence(s => s.hashtags.map(sansDiese), v => `#${v}`),
},
{ cle: 'famille', libelle: 'Famille', variante: 'echelle', valeurs: FAMILLES },
],
tris: ['pertinence', 'nom'],
groupes: [
{ cle: 'famille', libelle: 'Famille', dimension: 'famille', sans: 'Sans famille' },
{ cle: 'pays', libelle: 'Pays', dimension: 'pays', sans: 'Pays inconnu', valeurs: parFrequence(s => [s.pays], v => PAYS[v] ?? v) },
],
etats: ['carte', 'mixte', 'fiches'],
sousVues: {
parametre: 'carte',
defaut: 'metropole',
valeurs: [
{ valeur: 'metropole', libelle: 'Métropole' },
{ valeur: 'outremer', libelle: 'Outre-mer' },
{ valeur: 'graphe', libelle: 'Vue graphique' },
],
},
}))
const { toMini } = useFicheAdapter()
const fichesMini = computed(() => structures.value.map((s) => toMini(s)))
const vf = useVueFiches(fichesMini, config)
// La carte ne montre que le résultat courant (recherche + filtres)
const metropoleStructures = computed<StructureV2[]>(() => structures.value.filter((s) => vf.idsResultat.has(s.id)))
// ── Sélection : FicheModalV2 à toutes les largeurs (comportement d'avant B11) ──
const selectedId = ref<string | null>(null)
const ficheModalOpen = ref(false)
const ficheModalId = ref<string | null>(null)
function onSelectStructure(id: string) {
const ordinateur = typeof window !== 'undefined' && window.innerWidth >= 1024
selectedId.value = ordinateur && selectedId.value === id ? null : id
ficheModalId.value = id
ficheModalOpen.value = true
}
// ── Boutons flottants (téléphone) : au-dessus du pied « Voir les N fiches » en état Carte ──
const chatbotOpen = ref(false)
const missionOpen = ref(false)
const basFlottants = computed(() => (vf.largeur === 'telephone' && vf.vueAffichee === 'carte' ? 76 : 24))
onMounted(() => {
try {
if (!localStorage.getItem('aep_reseaux_seen')) missionOpen.value = true
} catch {}
})
useHead({ title: "AEP - Réseaux de bifurcation architecturale" })
</script>
<style scoped>
.reseaux-info-btn {
position: fixed;
bottom: 24px;
left: 16px;
z-index: 1000;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: var(--nav-surface);
color: var(--nav-text-muted);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 12px rgba(26,34,56,0.18);
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
.reseaux-info-btn:hover { opacity: 0.85; transform: translateY(-1px); color: var(--nav-text); }
@media (min-width: 1024px) {
.reseaux-info-btn { bottom: 10px; left: auto; right: 52px; }
}
</style>