feat(nav): Réseaux AEP V2 + onglets Métropole/Outremer Carte1 + reorder nav
- pages/agences.vue : carte V2 complète restaurée (517L, 120 structures) - pages/index.vue : onglets Métropole/Outre-mer + desktopMapView + chatbot outremer - app.vue : ordre nav → Entraide / Réseaux AEP / Jobs / Codev / RAG (en construction) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
16
app.vue
16
app.vue
@@ -41,14 +41,6 @@
|
|||||||
>
|
>
|
||||||
Réseaux AEP
|
Réseaux AEP
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
|
||||||
to="/rag"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path === '/rag' }"
|
|
||||||
>
|
|
||||||
RAG
|
|
||||||
<span class="nav-tab-badge">en construction</span>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/trouver-du-taf"
|
to="/trouver-du-taf"
|
||||||
class="nav-tab"
|
class="nav-tab"
|
||||||
@@ -63,6 +55,14 @@
|
|||||||
>
|
>
|
||||||
Codev
|
Codev
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/rag"
|
||||||
|
class="nav-tab"
|
||||||
|
:class="{ 'nav-tab--active': route.path === '/rag' }"
|
||||||
|
>
|
||||||
|
RAG
|
||||||
|
<span class="nav-tab-badge">en construction</span>
|
||||||
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- ── Barre recherche mobile (640px–1024px) — masquée < 640px car accessible dans la sheet -->
|
<!-- ── Barre recherche mobile (640px–1024px) — masquée < 640px car accessible dans la sheet -->
|
||||||
|
|||||||
@@ -1,39 +1,517 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center h-full gap-6" style="background: var(--nav-bg);">
|
<div class="flex h-full overflow-hidden" style="background: var(--nav-bg);">
|
||||||
<div class="text-center max-w-md px-6">
|
|
||||||
|
<!-- ═══════════════════════════════════════ SIDEBAR DESKTOP (>= 1024px) -->
|
||||||
|
<div class="hidden lg:block overflow-y-auto" style="width: 320px; min-width: 320px; flex-shrink: 0; border-right: 1px solid var(--nav-bg-alt); height: 100%;">
|
||||||
|
|
||||||
|
<!-- IntentionBanner s'auto-affiche via Teleport (overlay plein ecran) -->
|
||||||
|
<IntentionBanner />
|
||||||
|
|
||||||
|
<!-- Filtres familles + hashtags -->
|
||||||
|
<HashtagFilter
|
||||||
|
:allHashtags="allHashtags"
|
||||||
|
:selectedHashtags="selectedHashtags"
|
||||||
|
:selectedFamille="selectedFamille"
|
||||||
|
@update:selectedHashtags="selectedHashtags = $event"
|
||||||
|
@update:selectedFamille="selectedFamille = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Separateur -->
|
||||||
|
<div style="height: 1px; background: var(--nav-bg-alt);"></div>
|
||||||
|
|
||||||
|
<!-- Barre de recherche -->
|
||||||
|
<div class="px-3 py-2" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<label class="sidebar-search-label" aria-label="Rechercher une structure">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="sidebar-search-icon">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
v-model="search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Rechercher une structure..."
|
||||||
|
class="sidebar-search-input"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="search"
|
||||||
|
type="button"
|
||||||
|
class="sidebar-search-clear"
|
||||||
|
aria-label="Effacer"
|
||||||
|
@click.stop="search = ''"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header compteur + reset -->
|
||||||
|
<div class="flex items-center justify-between px-4 py-2" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<span class="text-xs font-bold uppercase tracking-widest" style="color: var(--nav-text-muted);">
|
||||||
|
{{ filtered.length }} structure{{ filtered.length > 1 ? 's' : '' }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
@click="resetFilters"
|
||||||
|
class="text-xs underline hover:opacity-70"
|
||||||
|
style="color: var(--nav-text-muted);"
|
||||||
|
>Effacer les filtres</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste fiches (sidebar entiere scroll - pas de scroll interne) -->
|
||||||
|
<div class="px-3 py-2 space-y-1.5">
|
||||||
|
<div v-if="pending" class="flex items-center justify-center py-8" style="color: var(--nav-text-muted);">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="filtered.length === 0" class="text-center py-8">
|
||||||
|
<p class="text-xs" style="color: var(--nav-text-muted);">Aucun résultat</p>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-5"
|
v-for="structure in filtered"
|
||||||
style="background: var(--nav-bg-alt);"
|
:key="structure.id"
|
||||||
|
class="rounded-lg px-3 py-2 cursor-pointer transition-all"
|
||||||
|
:style="selectedId === structure.id
|
||||||
|
? `background: var(--nav-bg-alt); border-left: 3px solid ${familleColor(structure.famille_principale)}; padding-left: 9px;`
|
||||||
|
: 'background: var(--nav-bg); border-left: 3px solid transparent; padding-left: 9px;'"
|
||||||
|
@click="onSelectStructure(structure.id)"
|
||||||
|
@mouseenter="hoveredId = structure.id"
|
||||||
|
@mouseleave="hoveredId = null"
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--nav-text-muted);">
|
<div class="flex items-start justify-between gap-1.5">
|
||||||
<rect x="3" y="3" width="7" height="7"/>
|
<span class="font-semibold text-sm leading-snug" style="color: var(--nav-text);">{{ structure.nom }}</span>
|
||||||
<rect x="14" y="3" width="7" height="7"/>
|
<span
|
||||||
<rect x="14" y="14" width="7" height="7"/>
|
class="shrink-0 w-2.5 h-2.5 rounded-full mt-1"
|
||||||
<rect x="3" y="14" width="7" height="7"/>
|
:style="`background: ${familleColor(structure.famille_principale)};`"
|
||||||
</svg>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold mb-3" style="color: var(--nav-text);">Agences Inspirantes</h1>
|
<div class="mt-0.5 text-xs" style="color: var(--nav-text-muted);">{{ structure.type_principal }} - {{ structure.ville }}</div>
|
||||||
<p class="text-sm leading-relaxed mb-6" style="color: var(--nav-text-muted);">
|
<div v-if="structure.hashtags.length" class="mt-1 flex flex-wrap gap-1">
|
||||||
Cette section répertoriera les agences d'architecture qui incarnent une pratique engagée — écologie politique, auto-construction, architectures vernaculaires, sobriété.
|
<span
|
||||||
</p>
|
v-for="tag in structure.hashtags.slice(0, 2)"
|
||||||
<p class="text-xs font-semibold uppercase tracking-widest mb-6" style="color: var(--nav-text-muted); opacity: 0.6;">
|
:key="tag"
|
||||||
Bientôt disponible
|
class="text-xs"
|
||||||
</p>
|
style="color: var(--nav-text-muted);"
|
||||||
<NuxtLink
|
>{{ tag }}</span>
|
||||||
to="/"
|
</div>
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all hover:opacity-80"
|
</div>
|
||||||
style="background: var(--nav-primary); color: var(--nav-text-on-primary);"
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════ ZONE CENTRALE (carte) -->
|
||||||
|
<main class="flex-1 flex flex-col overflow-hidden relative">
|
||||||
|
|
||||||
|
<!-- ── VUE DESKTOP : Onglets Métro/Outre-mer ── -->
|
||||||
|
<div class="hidden lg:flex lg:flex-1 lg:flex-col lg:overflow-hidden">
|
||||||
|
<!-- Onglets desktop -->
|
||||||
|
<div class="shrink-0 flex" style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<button
|
||||||
|
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="desktopMapView === 'metropole'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="desktopMapView = 'metropole'"
|
||||||
|
>Métropolitain</button>
|
||||||
|
<button
|
||||||
|
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="desktopMapView === 'outremer'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="desktopMapView = 'outremer'"
|
||||||
|
>Outre-mer</button>
|
||||||
|
<button
|
||||||
|
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="desktopMapView === 'graphe'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="desktopMapView = 'graphe'"
|
||||||
|
>Vue graphique</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte Métropole desktop -->
|
||||||
|
<div v-show="desktopMapView === 'metropole'" class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div class="relative flex-1" style="min-height: 200px;">
|
||||||
|
<ClientOnly>
|
||||||
|
<NavMapV2
|
||||||
|
ref="navMapRef"
|
||||||
|
:structures="metropoleStructures"
|
||||||
|
:selectedId="selectedId"
|
||||||
|
@select-structure="onSelectStructure"
|
||||||
|
/>
|
||||||
|
<template #fallback>
|
||||||
|
<div
|
||||||
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
Chargement de la carte…
|
||||||
<line x1="19" y1="12" x2="5" y2="12"/>
|
|
||||||
<polyline points="12 19 5 12 12 5"/>
|
|
||||||
</svg>
|
|
||||||
Retour à l'écosystème
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
<ChatbotPlaceholder
|
||||||
|
@highlightOrgs="() => {}"
|
||||||
|
@applyHashtag="(tag) => { if (!selectedHashtags.includes(tag)) selectedHashtags = [...selectedHashtags, tag] }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte Outre-mer desktop -->
|
||||||
|
<div v-show="desktopMapView === 'outremer'" class="flex-1 overflow-y-auto" style="background: var(--nav-bg);">
|
||||||
|
<ClientOnly>
|
||||||
|
<OutremerMap
|
||||||
|
:orgs="outremerOrgsLegacy"
|
||||||
|
:selectedId="selectedIdLegacyNum"
|
||||||
|
@select-org="() => {}"
|
||||||
|
/>
|
||||||
|
<template #fallback>
|
||||||
|
<div class="flex items-center justify-center h-full text-sm" style="color: var(--nav-text-muted);">
|
||||||
|
Chargement…
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vue graphique desktop -->
|
||||||
|
<div v-show="desktopMapView === 'graphe'" class="flex-1 overflow-hidden flex flex-col">
|
||||||
|
<div class="flex-1 overflow-hidden relative">
|
||||||
|
<ClientOnly>
|
||||||
|
<GraphView
|
||||||
|
:data="bifurcationData"
|
||||||
|
:allHashtags="allHashtags"
|
||||||
|
:active="desktopMapView === 'graphe'"
|
||||||
|
@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>
|
||||||
|
<ChatbotPlaceholder
|
||||||
|
@highlightOrgs="() => {}"
|
||||||
|
@applyHashtag="(tag) => { if (!selectedHashtags.includes(tag)) selectedHashtags = [...selectedHashtags, tag] }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── VUE MOBILE : Onglets Métro/Outre-mer + sheet swipable ── -->
|
||||||
|
<div class="lg:hidden shrink-0 flex" style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<button
|
||||||
|
class="flex-1 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="mobileMapView === 'metropole'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="mobileMapView = 'metropole'"
|
||||||
|
>Métropolitain</button>
|
||||||
|
<button
|
||||||
|
class="flex-1 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="mobileMapView === 'outremer'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="mobileMapView = 'outremer'"
|
||||||
|
>Outre-mer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:hidden flex-1 relative overflow-hidden">
|
||||||
|
<!-- Carte mobile Métropole -->
|
||||||
|
<div v-show="mobileMapView === 'metropole'" class="absolute inset-0">
|
||||||
|
<ClientOnly>
|
||||||
|
<NavMapV2
|
||||||
|
ref="navMapMobileRef"
|
||||||
|
:structures="metropoleStructures"
|
||||||
|
:selectedId="selectedId"
|
||||||
|
@select-structure="onSelectStructureMobile"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Carte mobile Outre-mer -->
|
||||||
|
<div v-show="mobileMapView === 'outremer'" class="absolute inset-0 overflow-y-auto" style="background: var(--nav-bg);">
|
||||||
|
<ClientOnly>
|
||||||
|
<OutremerMap
|
||||||
|
:orgs="outremerOrgsLegacy"
|
||||||
|
:selectedId="selectedIdLegacyNum"
|
||||||
|
@select-org="() => {}"
|
||||||
|
/>
|
||||||
|
<template #fallback>
|
||||||
|
<div class="flex items-center justify-center h-48" style="color: var(--nav-text-muted);">
|
||||||
|
Chargement…
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom sheet swipable -->
|
||||||
|
<ClientOnly>
|
||||||
|
<MobileSheet :resultCount="filtered.length" :pending="pending">
|
||||||
|
<!-- Bandeau intention mobile -->
|
||||||
|
<div class="px-3 py-2" style="background: var(--bifurc-banner-bg, #faf8f5); border-bottom: 1px solid var(--bifurc-banner-border, #e0d8cc);">
|
||||||
|
<p class="text-xs leading-relaxed" style="color: var(--bifurc-banner-text, #2c2416); margin: 0;">
|
||||||
|
120 réseaux, collectifs et agences où des pensées écologiques deviennent des pratiques d'architecture.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtres hashtags mobile -->
|
||||||
|
<div class="px-3 py-2" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<HashtagFilter
|
||||||
|
:allHashtags="allHashtags"
|
||||||
|
:selectedHashtags="selectedHashtags"
|
||||||
|
:selectedFamille="selectedFamille"
|
||||||
|
@update:selectedHashtags="selectedHashtags = $event"
|
||||||
|
@update:selectedFamille="selectedFamille = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre recherche mobile -->
|
||||||
|
<div class="px-3 pt-2 pb-2" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<label class="mobile-search-label" aria-label="Rechercher une structure">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--nav-text-muted); flex-shrink: 0;">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
v-model="search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Rechercher…"
|
||||||
|
class="mobile-search-input"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="search"
|
||||||
|
type="button"
|
||||||
|
class="mobile-search-clear"
|
||||||
|
aria-label="Effacer"
|
||||||
|
@click.stop="search = ''"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
@click="resetFilters"
|
||||||
|
class="mt-1 text-xs"
|
||||||
|
style="color: var(--nav-text-muted); text-decoration: underline;"
|
||||||
|
>Effacer les filtres</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste fiches mobile -->
|
||||||
|
<div class="px-3 py-2">
|
||||||
|
<div class="text-xs font-bold uppercase tracking-wide mb-2" style="color: var(--nav-text-muted);">
|
||||||
|
{{ filtered.length }} structure{{ filtered.length > 1 ? 's' : '' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="pending" class="flex items-center justify-center py-8" style="color: var(--nav-text-muted);">
|
||||||
|
Chargement des fiches…
|
||||||
|
</div>
|
||||||
|
<div v-else-if="filtered.length === 0" class="text-center py-8">
|
||||||
|
<p class="text-sm mb-2" style="color: var(--nav-text-muted);">Aucun résultat pour ces filtres.</p>
|
||||||
|
<button @click="resetFilters" class="text-sm underline" style="color: var(--nav-primary-solid);">
|
||||||
|
Effacer les filtres
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="structure in filtered"
|
||||||
|
:key="structure.id"
|
||||||
|
class="block rounded-lg p-3 transition-all cursor-pointer"
|
||||||
|
:style="selectedId === structure.id
|
||||||
|
? `background: var(--nav-bg-alt); border-left: 3px solid ${familleColor(structure.famille_principale)};`
|
||||||
|
: 'background: var(--nav-surface); border-left: 3px solid transparent;'"
|
||||||
|
@click="onSelectStructureMobile(structure.id)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<span class="font-semibold text-sm leading-snug" style="color: var(--nav-text);">{{ structure.nom }}</span>
|
||||||
|
<span
|
||||||
|
class="shrink-0 w-2.5 h-2.5 rounded-full mt-1"
|
||||||
|
:style="`background: ${familleColor(structure.famille_principale)};`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5 text-xs" style="color: var(--nav-text-muted);">{{ structure.type_principal }} · {{ structure.ville }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MobileSheet>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════ MODAL FICHE V2 (desktop) -->
|
||||||
|
<FicheModalV2
|
||||||
|
v-model="ficheModalOpen"
|
||||||
|
:structureId="ficheModalId"
|
||||||
|
:data="bifurcationData"
|
||||||
|
@update:structureId="ficheModalId = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
||||||
|
<button
|
||||||
|
class="lg:hidden fixed bottom-6 right-4 z-[1000] flex items-center gap-2 px-4 rounded-full shadow-lg"
|
||||||
|
style="
|
||||||
|
height: 48px;
|
||||||
|
background: var(--nav-primary);
|
||||||
|
opacity: 0.92;
|
||||||
|
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) -->
|
||||||
|
<ChatbotSheet
|
||||||
|
:modelValue="chatbotOpen"
|
||||||
|
@update:modelValue="chatbotOpen = $event"
|
||||||
|
@highlightOrgs="() => {}"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({ title: 'Agences Inspirantes — AEP (bientôt disponible)' })
|
import type { ReseauxBifurcationData, StructureV2 } from '~/types/structure-v2'
|
||||||
|
|
||||||
|
// ── Couleurs familles ──────────────────────────────────────────────────────
|
||||||
|
const FAMILLE_COLORS: Record<number, string> = {
|
||||||
|
1: '#a85d3e',
|
||||||
|
2: '#c4a472',
|
||||||
|
3: '#d4a017',
|
||||||
|
4: '#5a7a4a',
|
||||||
|
5: '#3d6a8c',
|
||||||
|
6: '#6b3fa0',
|
||||||
|
}
|
||||||
|
|
||||||
|
function familleColor(f: number): string {
|
||||||
|
return FAMILLE_COLORS[f] ?? '#888'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── État UI ────────────────────────────────────────────────────────────────
|
||||||
|
const selectedId = ref<string | null>(null)
|
||||||
|
const hoveredId = ref<string | null>(null)
|
||||||
|
const ficheModalOpen = ref(false)
|
||||||
|
const ficheModalId = ref<string | null>(null)
|
||||||
|
const chatbotOpen = ref(false)
|
||||||
|
const mobileMapView = ref<'metropole' | 'outremer'>('metropole')
|
||||||
|
const desktopMapView = ref<'metropole' | 'outremer' | 'graphe'>('metropole')
|
||||||
|
|
||||||
|
// Filtres
|
||||||
|
const search = ref('')
|
||||||
|
const selectedFamille = ref<number | null>(null)
|
||||||
|
const selectedHashtags = ref<string[]>([])
|
||||||
|
|
||||||
|
// Refs cartes
|
||||||
|
const navMapRef = ref<any>(null)
|
||||||
|
const navMapMobileRef = ref<any>(null)
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Filtrage ───────────────────────────────────────────────────────────────
|
||||||
|
const filtered = computed<StructureV2[]>(() => {
|
||||||
|
let result = structures.value
|
||||||
|
|
||||||
|
// Filtre texte
|
||||||
|
if (search.value.trim()) {
|
||||||
|
const q = search.value.toLowerCase()
|
||||||
|
result = result.filter(
|
||||||
|
s =>
|
||||||
|
s.nom.toLowerCase().includes(q) ||
|
||||||
|
s.ville.toLowerCase().includes(q) ||
|
||||||
|
s.description_courte.toLowerCase().includes(q) ||
|
||||||
|
s.hashtags.some(h => h.toLowerCase().includes(q))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre famille - F6 = badge_f6_recherche_politique, pas famille_principale
|
||||||
|
if (selectedFamille.value !== null) {
|
||||||
|
if (selectedFamille.value === 6) {
|
||||||
|
result = result.filter(s => (s.badges as any)?.f6_recherche_politique === true)
|
||||||
|
} else {
|
||||||
|
result = result.filter(
|
||||||
|
s => s.famille_principale === selectedFamille.value ||
|
||||||
|
(s.familles_secondaires ?? []).includes(selectedFamille.value!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre hashtags (AND logique si plusieurs)
|
||||||
|
if (selectedHashtags.value.length) {
|
||||||
|
result = result.filter(
|
||||||
|
s => selectedHashtags.value.every(h => s.hashtags.includes(h))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasActiveFilters = computed(
|
||||||
|
() => !!search.value || selectedFamille.value !== null || selectedHashtags.value.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
search.value = ''
|
||||||
|
selectedFamille.value = null
|
||||||
|
selectedHashtags.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structures métropole (pays != DOM-TOM, et avec coordonnées)
|
||||||
|
// Pour simplifier : toutes les structures (la carte gère les sans-coords)
|
||||||
|
const metropoleStructures = computed<StructureV2[]>(() => filtered.value)
|
||||||
|
|
||||||
|
// Outre-mer : pas de structures V2 DOM-TOM pour l'instant - garder le composant existant vide
|
||||||
|
// OutremerMap attend le format Org legacy - on passe un tableau vide
|
||||||
|
const outremerOrgsLegacy = computed(() => [])
|
||||||
|
const selectedIdLegacyNum = computed(() => null)
|
||||||
|
|
||||||
|
// ── Sélection ─────────────────────────────────────────────────────────────
|
||||||
|
function onSelectStructure(id: string) {
|
||||||
|
selectedId.value = selectedId.value === id ? null : id
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
||||||
|
ficheModalId.value = id
|
||||||
|
ficheModalOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectStructureMobile(id: string) {
|
||||||
|
selectedId.value = id
|
||||||
|
ficheModalId.value = id
|
||||||
|
ficheModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead({ title: "AEP - Réseaux de bifurcation architecturale" })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,10 +40,28 @@
|
|||||||
Mode dev — données seed
|
Mode dev — données seed
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── VUE DESKTOP : Métropole pleine largeur + DOM-TOM row en bas ── -->
|
<!-- ── VUE DESKTOP : Onglets Métropole / Outre-mer ── -->
|
||||||
<div class="hidden lg:flex lg:flex-1 lg:flex-col lg:overflow-hidden">
|
<div class="hidden lg:flex lg:flex-1 lg:flex-col lg:overflow-hidden">
|
||||||
<!-- Carte Métropole — pleine largeur -->
|
<!-- Barre onglets desktop -->
|
||||||
<div class="flex flex-col flex-1 overflow-hidden">
|
<div class="shrink-0 flex" style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
|
<button
|
||||||
|
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="desktopMapView === 'metropole'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="desktopMapView = 'metropole'"
|
||||||
|
>Métropolitain</button>
|
||||||
|
<button
|
||||||
|
class="px-5 py-2 text-sm font-medium transition-colors"
|
||||||
|
:style="desktopMapView === 'outremer'
|
||||||
|
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
||||||
|
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
||||||
|
@click="desktopMapView = 'outremer'"
|
||||||
|
>Outre-mer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte Métropole desktop -->
|
||||||
|
<div v-show="desktopMapView === 'metropole'" class="flex-1 flex flex-col overflow-hidden">
|
||||||
<div class="relative flex-1" style="min-height: 200px;">
|
<div class="relative flex-1" style="min-height: 200px;">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<NavMap
|
<NavMap
|
||||||
@@ -53,23 +71,16 @@
|
|||||||
@select-org="onSelectOrg"
|
@select-org="onSelectOrg"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div
|
<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>
|
||||||
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>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
<ChatbotPlaceholder @highlightOrgs="onHighlightOrgs" />
|
<ChatbotPlaceholder @highlightOrgs="onHighlightOrgs" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bandeau DOM-TOM — row horizontale pleine largeur, hauteur fixe -->
|
<!-- Carte Outre-mer desktop -->
|
||||||
<div
|
<div v-show="desktopMapView === 'outremer'" class="flex-1 flex flex-col overflow-hidden">
|
||||||
class="shrink-0"
|
<div class="flex-1 overflow-y-auto">
|
||||||
style="height: 140px; border-top: 1px solid var(--nav-bg-alt);"
|
|
||||||
>
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<OutremerMap
|
<OutremerMap
|
||||||
:orgs="outremerOrgs"
|
:orgs="outremerOrgs"
|
||||||
@@ -77,15 +88,12 @@
|
|||||||
@select-org="onSelectOrg"
|
@select-org="onSelectOrg"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div
|
<div class="flex items-center justify-center h-full text-sm" style="color: var(--nav-text-muted);">Chargement…</div>
|
||||||
class="flex items-center justify-center h-full text-sm"
|
|
||||||
style="color: var(--nav-text-muted);"
|
|
||||||
>
|
|
||||||
Chargement…
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
<ChatbotPlaceholder @highlightOrgs="onHighlightOrgs" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── VUE MOBILE : Onglets Métro/Outre-mer + carte pleine hauteur + sheet swipable ── -->
|
<!-- ── VUE MOBILE : Onglets Métro/Outre-mer + carte pleine hauteur + sheet swipable ── -->
|
||||||
@@ -330,6 +338,7 @@ const territoireMode = ref<string>(
|
|||||||
(route.query.mode as string) === 'outremer' ? 'outremer' : 'metropole'
|
(route.query.mode as string) === 'outremer' ? 'outremer' : 'metropole'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const desktopMapView = ref<'metropole' | 'outremer'>('metropole')
|
||||||
const selectedId = ref<number | null>(null)
|
const selectedId = ref<number | null>(null)
|
||||||
const chatbotOpen = ref(false)
|
const chatbotOpen = ref(false)
|
||||||
const ficheModalOpen = ref(false)
|
const ficheModalOpen = ref(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user