Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGNSEFZdoKvbdsGyfSnu5S
281 lines
12 KiB
Vue
281 lines
12 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"
|
|
>
|
|
<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)"
|
|
:centrage="centrageCarte"
|
|
@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>
|
|
|
|
<!-- Détail (B12) : le corps de l'ancienne FicheModalV2 ; une structure liée s'ouvre dans le même panneau -->
|
|
<template #detail="{ fiche, filtrer }">
|
|
<FicheReseauContenu
|
|
:structure-id="String(fiche.id)"
|
|
:data="bifurcationData"
|
|
:etiquettes-actives="(vf.etat.filtres.tags ?? []).map((t) => `#${t}`)"
|
|
@etiquette="(tag) => filtrer('tags', sansDiese(tag))"
|
|
@voisin="(id) => vf.ouvrirFiche(id)"
|
|
/>
|
|
</template>
|
|
</VueFiches>
|
|
|
|
<!-- ═══════════════════════════════════════ 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 « vertes » : 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 : militants, agences engagées, collectifs de production, ressources communes, recherche, formations alternatives. Filtre par hashtag, ouvre la fiche d'une structure, navigue le graphe (« Vue graphique » dans le sélecteur de carte) pour voir les affinités. Si tu animes ou connais un réseau qui devrait y être : <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 (B12) : la fiche ouverte vit dans l'URL (`fiche=`), détail en panneau ──
|
|
const selectedId = computed<string | null>(() => vf.ficheId)
|
|
|
|
function onSelectStructure(id: string) {
|
|
vf.ouvrirFiche(id)
|
|
}
|
|
|
|
// « Voir sur la carte » : l'épingle est sur la carte Métropole (pas dans le graphe)
|
|
const centrageCarte = ref<{ id: string; n: number } | null>(null)
|
|
watch(() => vf.centrage, (c) => {
|
|
if (!c) return
|
|
if (vf.etat.sousVue !== 'metropole') vf.setSousVue('metropole')
|
|
centrageCarte.value = { id: c.id, n: c.n }
|
|
})
|
|
|
|
// ── 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>
|