feat(reseaux): /agences branché sur VueFiches ; recherche, tags, famille et carte= dans l'URL (B11-M6)
Hashtags désormais en OU dans leur dimension (ET avant), ET avec la famille. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCt6e8wWiYqRXLJAuKqP5i
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2e82d412ed
commit
a98342c5f1
+45
-10
@@ -21,10 +21,14 @@ const FAMILLE_COLORS: Record<number, string> = {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
structures: StructureV2[]
|
structures: StructureV2[]
|
||||||
selectedId?: string | null
|
selectedId?: string | null
|
||||||
|
/** Fiche survolée dans la liste (B11) : épingle cerclée de safran, sans recentrer. */
|
||||||
|
survolId?: string | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'select-structure': [id: string]
|
'select-structure': [id: string]
|
||||||
|
/** Survol d'une épingle (B11) : la liste allume la carte-fiche. */
|
||||||
|
survol: [id: string | null]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mapContainer = ref<HTMLElement | null>(null)
|
const mapContainer = ref<HTMLElement | null>(null)
|
||||||
@@ -32,17 +36,18 @@ let mapInstance: Map | null = null
|
|||||||
let clusterGroup: any = null
|
let clusterGroup: any = null
|
||||||
const markers = new Map<string, Marker>()
|
const markers = new Map<string, Marker>()
|
||||||
let tileLayerInstance: any = null
|
let tileLayerInstance: any = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
function getFamilleColor(famille: number): string {
|
function getFamilleColor(famille: number): string {
|
||||||
return FAMILLE_COLORS[famille] ?? '#888888'
|
return FAMILLE_COLORS[famille] ?? '#888888'
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPinIcon(famille: number, isSelected = false): DivIcon {
|
function createPinIcon(famille: number, isSelected = false, isSurvol = false): DivIcon {
|
||||||
const L = (window as any).L
|
const L = (window as any).L
|
||||||
const bg = getFamilleColor(famille)
|
const bg = getFamilleColor(famille)
|
||||||
const size = isSelected ? 20 : 14
|
const size = isSelected || isSurvol ? 20 : 14
|
||||||
const border = isSelected ? 'white' : 'rgba(255,255,255,0.7)'
|
const border = isSurvol ? '#f5b342' : isSelected ? 'white' : 'rgba(255,255,255,0.7)'
|
||||||
const shadow = isSelected ? `0 0 0 4px ${bg}55` : 'none'
|
const shadow = isSurvol ? '0 0 0 4px rgba(245,179,66,0.6)' : isSelected ? `0 0 0 4px ${bg}55` : 'none'
|
||||||
|
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
className: '',
|
className: '',
|
||||||
@@ -76,6 +81,7 @@ async function initMap() {
|
|||||||
;(window as any).L = L
|
;(window as any).L = L
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await import('leaflet.markercluster')
|
await import('leaflet.markercluster')
|
||||||
|
if (!mapContainer.value?.isConnected) return // page quittée pendant le chargement (ex. ?random=1)
|
||||||
const MarkerClusterGroup = L.MarkerClusterGroup
|
const MarkerClusterGroup = L.MarkerClusterGroup
|
||||||
|
|
||||||
mapInstance = L.map(mapContainer.value, {
|
mapInstance = L.map(mapContainer.value, {
|
||||||
@@ -121,6 +127,14 @@ async function initMap() {
|
|||||||
|
|
||||||
mapInstance.addLayer(clusterGroup)
|
mapInstance.addLayer(clusterGroup)
|
||||||
updateMarkers(L)
|
updateMarkers(L)
|
||||||
|
|
||||||
|
// B11 : la carte change de largeur sans que la fenêtre bouge (Mixte ↔ Carte ↔ Fiches)
|
||||||
|
resizeObserver = new ResizeObserver(() => mapInstance?.invalidateSize({ pan: false }))
|
||||||
|
resizeObserver.observe(mapContainer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconeDe(s: StructureV2): DivIcon {
|
||||||
|
return createPinIcon(s.famille_principale, s.id === props.selectedId, s.id === props.survolId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMarkers(L?: any) {
|
function updateMarkers(L?: any) {
|
||||||
@@ -136,10 +150,7 @@ function updateMarkers(L?: any) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
structuresWithCoords.forEach((structure) => {
|
structuresWithCoords.forEach((structure) => {
|
||||||
const isSelected = structure.id === props.selectedId
|
const marker = leaflet.marker([structure.latitude!, structure.longitude!], { icon: iconeDe(structure), title: structure.nom })
|
||||||
const icon = createPinIcon(structure.famille_principale, isSelected)
|
|
||||||
|
|
||||||
const marker = leaflet.marker([structure.latitude!, structure.longitude!], { icon })
|
|
||||||
|
|
||||||
const hashtagsHtml = structure.hashtags.slice(0, 2)
|
const hashtagsHtml = structure.hashtags.slice(0, 2)
|
||||||
.map(h => `<span style="font-size:10px;color:var(--nav-text-muted);">${h}</span>`)
|
.map(h => `<span style="font-size:10px;color:var(--nav-text-muted);">${h}</span>`)
|
||||||
@@ -165,6 +176,8 @@ function updateMarkers(L?: any) {
|
|||||||
`, { maxWidth: 260 })
|
`, { maxWidth: 260 })
|
||||||
|
|
||||||
marker.on('click', () => emit('select-structure', structure.id))
|
marker.on('click', () => emit('select-structure', structure.id))
|
||||||
|
marker.on('mouseover', () => emit('survol', structure.id))
|
||||||
|
marker.on('mouseout', () => emit('survol', null))
|
||||||
|
|
||||||
markers.set(structure.id, marker)
|
markers.set(structure.id, marker)
|
||||||
clusterGroup.addLayer(marker)
|
clusterGroup.addLayer(marker)
|
||||||
@@ -193,14 +206,14 @@ watch(
|
|||||||
const oldMarker = markers.get(oldId)
|
const oldMarker = markers.get(oldId)
|
||||||
const oldStructure = props.structures.find(s => s.id === oldId)
|
const oldStructure = props.structures.find(s => s.id === oldId)
|
||||||
if (oldMarker && oldStructure) {
|
if (oldMarker && oldStructure) {
|
||||||
oldMarker.setIcon(createPinIcon(oldStructure.famille_principale, false))
|
oldMarker.setIcon(iconeDe(oldStructure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newId != null) {
|
if (newId != null) {
|
||||||
const newMarker = markers.get(newId)
|
const newMarker = markers.get(newId)
|
||||||
const newStructure = props.structures.find(s => s.id === newId)
|
const newStructure = props.structures.find(s => s.id === newId)
|
||||||
if (newMarker && newStructure) {
|
if (newMarker && newStructure) {
|
||||||
newMarker.setIcon(createPinIcon(newStructure.famille_principale, true))
|
newMarker.setIcon(iconeDe(newStructure))
|
||||||
const latLng = newMarker.getLatLng()
|
const latLng = newMarker.getLatLng()
|
||||||
mapInstance.panTo(latLng, { animate: true })
|
mapInstance.panTo(latLng, { animate: true })
|
||||||
}
|
}
|
||||||
@@ -208,6 +221,27 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Survol depuis la liste : épingle cerclée de safran, sans recentrer la carte
|
||||||
|
watch(
|
||||||
|
() => props.survolId,
|
||||||
|
(newId, oldId) => {
|
||||||
|
for (const id of [oldId, newId]) {
|
||||||
|
if (id == null) continue
|
||||||
|
const m = markers.get(id)
|
||||||
|
const s = props.structures.find(x => x.id === id)
|
||||||
|
if (m && s) m.setIcon(iconeDe(s))
|
||||||
|
allumerCluster(m, id === newId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Épingle regroupée dans un cluster : c'est le cluster visible qui s'allume
|
||||||
|
function allumerCluster(m: Marker | undefined, on: boolean) {
|
||||||
|
const parent = m && clusterGroup?.getVisibleParent(m)
|
||||||
|
const el = parent && parent !== m ? (parent as any)._icon?.firstElementChild as HTMLElement | undefined : undefined
|
||||||
|
if (el) el.style.boxShadow = on ? '0 0 0 4px #f5b342' : ''
|
||||||
|
}
|
||||||
|
|
||||||
function updateTileTheme(dark: boolean) {
|
function updateTileTheme(dark: boolean) {
|
||||||
if (!mapInstance || !tileLayerInstance) return
|
if (!mapInstance || !tileLayerInstance) return
|
||||||
const L = (window as any).L
|
const L = (window as any).L
|
||||||
@@ -232,6 +266,7 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('nav-v2-select', onNavV2Select as EventListener)
|
document.removeEventListener('nav-v2-select', onNavV2Select as EventListener)
|
||||||
themeObserver?.disconnect()
|
themeObserver?.disconnect()
|
||||||
|
resizeObserver?.disconnect()
|
||||||
if (mapInstance) {
|
if (mapInstance) {
|
||||||
mapInstance.remove()
|
mapInstance.remove()
|
||||||
mapInstance = null
|
mapInstance = null
|
||||||
|
|||||||
+115
-537
@@ -1,433 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full overflow-hidden" style="background: var(--nav-bg);">
|
<div class="relative h-full overflow-hidden" style="background: var(--nav-bg);">
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════ SIDEBAR DESKTOP (>= 1024px) -->
|
<!-- IntentionBanner s'auto-affiche via Teleport (overlay plein écran) -->
|
||||||
<!-- Masquée en vue « Toutes les fiches » (B3-M3, décision Jules axe 2) :
|
<IntentionBanner />
|
||||||
le panneau EST la liste (+ ses propres chips hashtags), la sidebar
|
|
||||||
ferait doublon. IntentionBanner reste monté (Teleport, insensible au
|
<!-- ═══════════════════════════════════════ VUE FICHES (B11) : Carte · Mixte · Fiches -->
|
||||||
v-show de son conteneur). -->
|
<VueFiches
|
||||||
<div
|
:vf="vf"
|
||||||
v-show="desktopMapView !== 'fiches'"
|
intention="120 réseaux, collectifs et agences où des pensées écologiques deviennent des pratiques d'architecture."
|
||||||
class="hidden lg:block overflow-y-auto"
|
libelle-recherche="Rechercher une structure…"
|
||||||
style="width: 320px; min-width: 320px; flex-shrink: 0; border-right: 1px solid var(--nav-bg-alt); height: 100%;"
|
:pending="pending"
|
||||||
|
:selected-id="selectedId"
|
||||||
|
@select="(f) => onSelectStructure(String(f.id))"
|
||||||
>
|
>
|
||||||
|
<template #carte>
|
||||||
<!-- IntentionBanner s'auto-affiche via Teleport (overlay plein ecran) -->
|
<!-- Métropole -->
|
||||||
<IntentionBanner />
|
<div v-show="vf.etat.sousVue === 'metropole'" class="relative flex-1 min-h-0">
|
||||||
|
|
||||||
<!-- 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
|
|
||||||
v-for="structure in filtered"
|
|
||||||
: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"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-1.5">
|
|
||||||
<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>
|
|
||||||
<ChipGroup v-if="structure.hashtags.length" class="mt-1" :aria-label="`Hashtags de ${structure.nom}`">
|
|
||||||
<Chip
|
|
||||||
v-for="tag in structure.hashtags.slice(0, 2)"
|
|
||||||
:key="tag"
|
|
||||||
as="span"
|
|
||||||
variant="fonction"
|
|
||||||
:label="tag"
|
|
||||||
/>
|
|
||||||
</ChipGroup>
|
|
||||||
</div>
|
|
||||||
</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="setDesktopView('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="setDesktopView('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="setDesktopView('graphe')"
|
|
||||||
>Vue graphique</button>
|
|
||||||
<button
|
|
||||||
class="px-5 py-2 text-sm font-medium transition-colors"
|
|
||||||
:style="desktopMapView === 'fiches'
|
|
||||||
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
|
||||||
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
|
||||||
@click="setDesktopView('fiches')"
|
|
||||||
>Toutes les fiches</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);"
|
|
||||||
>
|
|
||||||
Chargement de la carte…
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Panneau « Toutes les fiches » desktop (B3-M3) — la sidebar est
|
|
||||||
masquée dans cette vue (décision Jules axe 2), le panneau EST
|
|
||||||
la liste, avec ses propres chips hashtags sur le même état. -->
|
|
||||||
<div v-show="desktopMapView === 'fiches'" class="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<FichesPanel
|
|
||||||
:fiches="allStructuresMini"
|
|
||||||
:hashtags="allHashtags"
|
|
||||||
:selected="selectedHashtags"
|
|
||||||
:query="search"
|
|
||||||
intention="Toutes les agences et réseaux du répertoire, filtrables par hashtag."
|
|
||||||
@update:selected="selectedHashtags = $event"
|
|
||||||
@open="(fiche) => onSelectStructure(String(fiche.id))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── VUE MOBILE : Onglets Métro/Outre-mer/Graphique/Fiches + 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="setMobileView('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="setMobileView('outremer')"
|
|
||||||
>Outre-mer</button>
|
|
||||||
<button
|
|
||||||
class="flex-1 py-2 text-sm font-medium transition-colors"
|
|
||||||
:style="mobileMapView === 'graphe'
|
|
||||||
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
|
||||||
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
|
||||||
@click="setMobileView('graphe')"
|
|
||||||
>Graphe</button>
|
|
||||||
<button
|
|
||||||
class="flex-1 py-2 text-sm font-medium transition-colors"
|
|
||||||
:style="mobileMapView === 'fiches'
|
|
||||||
? 'color: var(--nav-text); border-bottom: 2px solid var(--nav-primary-solid);'
|
|
||||||
: 'color: var(--nav-text-muted); border-bottom: 2px solid transparent;'"
|
|
||||||
@click="setMobileView('fiches')"
|
|
||||||
>Fiches</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>
|
<ClientOnly>
|
||||||
<NavMapV2
|
<NavMapV2
|
||||||
ref="navMapMobileRef"
|
|
||||||
:structures="metropoleStructures"
|
:structures="metropoleStructures"
|
||||||
:selectedId="selectedId"
|
:selectedId="selectedId"
|
||||||
@select-structure="onSelectStructureMobile"
|
:survolId="vf.survolId == null ? null : String(vf.survolId)"
|
||||||
|
@select-structure="onSelectStructure"
|
||||||
|
@survol="(id) => (vf.survolId = id)"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="w-full h-full flex items-center justify-center" style="background: var(--nav-bg-alt); color: var(--nav-text-muted);">
|
<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>
|
||||||
Chargement de la carte…
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Carte mobile Outre-mer -->
|
<!-- Outre-mer : pas de structure V2 en DOM-TOM pour l'instant (composant gardé vide) -->
|
||||||
<div v-show="mobileMapView === 'outremer'" class="absolute inset-0 overflow-y-auto" style="background: var(--nav-bg);">
|
<div v-show="vf.etat.sousVue === 'outremer'" class="flex-1 min-h-0 overflow-y-auto" style="background: var(--nav-bg);">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<OutremerMap
|
<OutremerMap :orgs="[]" :selectedId="null" @select-org="() => {}" />
|
||||||
:orgs="outremerOrgsLegacy"
|
|
||||||
:selectedId="selectedIdLegacyNum"
|
|
||||||
@select-org="() => {}"
|
|
||||||
/>
|
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="flex items-center justify-center h-48" style="color: var(--nav-text-muted);">
|
<div class="flex items-center justify-center h-48 text-sm" style="color: var(--nav-text-muted);">Chargement…</div>
|
||||||
Chargement…
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Vue graphique mobile -->
|
<!-- Vue graphique -->
|
||||||
<div v-show="mobileMapView === 'graphe'" class="absolute inset-0 overflow-hidden" style="background: var(--nav-bg);">
|
<div v-show="vf.etat.sousVue === 'graphe'" class="relative flex-1 min-h-0 overflow-hidden">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<GraphView
|
<GraphView
|
||||||
:data="bifurcationData"
|
:data="bifurcationData"
|
||||||
:allHashtags="allHashtags"
|
:allHashtags="allHashtags"
|
||||||
:active="mobileMapView === 'graphe'"
|
:active="vf.etat.sousVue === 'graphe' && vf.vueAffichee !== 'fiches'"
|
||||||
@select-structure="onSelectStructureMobile"
|
@select-structure="onSelectStructure"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="flex items-center justify-center h-48" style="color: var(--nav-text-muted);">
|
<div class="flex items-center justify-center h-full" style="color: var(--nav-text-muted);">Chargement du graphe…</div>
|
||||||
Chargement du graphe…
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Zone « Fiches » mobile (B3-M3) — pleine hauteur sous la barre d'onglets,
|
<!-- Chatbot sous la carte (ordinateur) -->
|
||||||
pas de carte dessous donc pas de sheet demi-hauteur (même logique que
|
<div v-if="vf.etat.sousVue !== 'outremer'" class="hidden lg:block shrink-0">
|
||||||
la vue graphe qui masque déjà le MobileSheet). Fix 2026-09-25 : la
|
<ChatbotPlaceholder
|
||||||
version précédente empilait un second MobileSheet (état half par
|
@highlightOrgs="() => {}"
|
||||||
défaut) au-dessus d'un fond neutre plein écran, laissant ~45% de
|
@applyHashtag="(tag) => vf.ajouterFiltre('tags', sansDiese(tag))"
|
||||||
hauteur vide au-dessus du panneau. -->
|
|
||||||
<div
|
|
||||||
v-show="mobileMapView === 'fiches'"
|
|
||||||
class="absolute inset-0 flex flex-col overflow-hidden"
|
|
||||||
style="background: var(--nav-bg-alt);"
|
|
||||||
>
|
|
||||||
<FichesPanel
|
|
||||||
:fiches="allStructuresMini"
|
|
||||||
:hashtags="allHashtags"
|
|
||||||
:selected="selectedHashtags"
|
|
||||||
:query="search"
|
|
||||||
intention="Toutes les agences et réseaux du répertoire, filtrables par hashtag."
|
|
||||||
@update:selected="selectedHashtags = $event"
|
|
||||||
@open="(fiche) => onSelectStructureMobile(String(fiche.id))"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</VueFiches>
|
||||||
|
|
||||||
<!-- Bottom sheet swipable (masqué en vue graphique et en vue fiches) -->
|
<!-- ═══════════════════════════════════════ MODAL FICHE V2 -->
|
||||||
<ClientOnly v-if="mobileMapView !== 'graphe' && mobileMapView !== 'fiches'">
|
|
||||||
<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
|
<FicheModalV2
|
||||||
v-model="ficheModalOpen"
|
v-model="ficheModalOpen"
|
||||||
:structureId="ficheModalId"
|
:structureId="ficheModalId"
|
||||||
@@ -437,8 +75,9 @@
|
|||||||
|
|
||||||
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
||||||
<button
|
<button
|
||||||
class="lg:hidden fixed bottom-6 right-4 z-[1000] flex items-center gap-2 px-4 rounded-full shadow-lg"
|
class="lg:hidden fixed right-4 z-[1000] flex items-center gap-2 px-4 rounded-full shadow-lg"
|
||||||
style="
|
:style="`
|
||||||
|
bottom: ${basFlottants}px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
background: var(--nav-primary);
|
background: var(--nav-primary);
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
@@ -447,7 +86,7 @@
|
|||||||
font-family: var(--nav-font);
|
font-family: var(--nav-font);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
"
|
`"
|
||||||
aria-label="Ouvrir l'assistant Chatbot"
|
aria-label="Ouvrir l'assistant Chatbot"
|
||||||
@click="chatbotOpen = true"
|
@click="chatbotOpen = true"
|
||||||
>
|
>
|
||||||
@@ -467,6 +106,7 @@
|
|||||||
<button
|
<button
|
||||||
class="reseaux-info-btn"
|
class="reseaux-info-btn"
|
||||||
type="button"
|
type="button"
|
||||||
|
:style="vf.largeur === 'telephone' ? `bottom: ${basFlottants}px;` : undefined"
|
||||||
@click="missionOpen = true"
|
@click="missionOpen = true"
|
||||||
aria-label="À propos des réseaux AEP cartographiés"
|
aria-label="À propos des réseaux AEP cartographiés"
|
||||||
title="À propos de cette carte"
|
title="À propos de cette carte"
|
||||||
@@ -490,7 +130,7 @@
|
|||||||
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.
|
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>
|
||||||
<p class="mission-text">
|
<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 (3<sup>e</sup> onglet) 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>.
|
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>
|
</p>
|
||||||
</MissionPopup>
|
</MissionPopup>
|
||||||
|
|
||||||
@@ -499,73 +139,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReseauxBifurcationData, StructureV2 } from '~/types/structure-v2'
|
import type { ReseauxBifurcationData, StructureV2 } from '~/types/structure-v2'
|
||||||
|
import type { ConfigVueFiches, ValeurFiltre } from '~/utils/vueFiches'
|
||||||
// ── Couleurs familles ──────────────────────────────────────────────────────
|
import { sansDiese } from '~/composables/useFicheAdapter'
|
||||||
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 ────────────────────────────────────────────────────────────────
|
|
||||||
type MapView = 'metropole' | 'outremer' | 'graphe' | 'fiches'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
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<MapView>((route.query.vue as string) === 'fiches' ? 'fiches' : 'metropole')
|
|
||||||
const desktopMapView = ref<MapView>((route.query.vue as string) === 'fiches' ? 'fiches' : 'metropole')
|
|
||||||
const missionOpen = ref(false)
|
|
||||||
|
|
||||||
// Sync URL — B3-M3 : pose/retire ?vue=fiches uniquement (agences.vue n'avait
|
|
||||||
// aucune synchronisation URL avant B3 pour search/famille/hashtags/vue ; hors
|
|
||||||
// périmètre de cette mission de l'étendre, seul ?vue=fiches est ajouté ici).
|
|
||||||
function setDesktopView(v: MapView) {
|
|
||||||
desktopMapView.value = v
|
|
||||||
syncViewUrl()
|
|
||||||
}
|
|
||||||
function setMobileView(v: MapView) {
|
|
||||||
mobileMapView.value = v
|
|
||||||
syncViewUrl()
|
|
||||||
}
|
|
||||||
function syncViewUrl() {
|
|
||||||
const q: Record<string, string> = { ...(route.query as Record<string, string>) }
|
|
||||||
if (desktopMapView.value === 'fiches' || mobileMapView.value === 'fiches') {
|
|
||||||
q.vue = 'fiches'
|
|
||||||
} else {
|
|
||||||
delete q.vue
|
|
||||||
}
|
|
||||||
router.replace({ query: Object.keys(q).length ? q : undefined })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
try {
|
|
||||||
if (!localStorage.getItem('aep_reseaux_seen')) {
|
|
||||||
missionOpen.value = true
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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 ─────────────────────────────────────────────
|
// ── Données V2 - JSON statique ─────────────────────────────────────────────
|
||||||
const bifurcationData = ref<ReseauxBifurcationData | null>(null)
|
const bifurcationData = ref<ReseauxBifurcationData | null>(null)
|
||||||
@@ -583,90 +158,93 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const structures = computed<StructureV2[]>(() => bifurcationData.value?.structures ?? [])
|
const structures = computed<StructureV2[]>(() => bifurcationData.value?.structures ?? [])
|
||||||
|
|
||||||
// Tous les hashtags uniques triés
|
// Tous les hashtags uniques triés (le graphe les attend avec leur « # »)
|
||||||
const allHashtags = computed<string[]>(() => {
|
const allHashtags = computed<string[]>(() => {
|
||||||
const set = new Set<string>()
|
const set = new Set<string>()
|
||||||
structures.value.forEach(s => s.hashtags.forEach(h => set.add(h)))
|
structures.value.forEach(s => s.hashtags.forEach(h => set.add(h)))
|
||||||
return Array.from(set).sort()
|
return Array.from(set).sort()
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Filtrage ───────────────────────────────────────────────────────────────
|
// ── Vue fiches (B11) : l'état vit dans l'URL (tags=, famille=, carte=…) ────
|
||||||
const filtered = computed<StructureV2[]>(() => {
|
// Familles : 1-5 = famille principale ou secondaire, 6 = badge « recherche politique »
|
||||||
let result = structures.value
|
const FAMILLES: ValeurFiltre[] = [
|
||||||
|
{ valeur: '1', libelle: 'Réemploi' },
|
||||||
// Filtre texte
|
{ valeur: '2', libelle: 'Frugalité' },
|
||||||
if (search.value.trim()) {
|
{ valeur: '3', libelle: 'Social' },
|
||||||
const q = search.value.toLowerCase()
|
{ valeur: '4', libelle: 'Collectifs' },
|
||||||
result = result.filter(
|
{ valeur: '5', libelle: 'Urbanisme' },
|
||||||
s =>
|
{ valeur: '6', libelle: 'Recherche' },
|
||||||
s.nom.toLowerCase().includes(q) ||
|
]
|
||||||
s.ville.toLowerCase().includes(q) ||
|
const PAYS: Record<string, string> = {
|
||||||
s.description_courte.toLowerCase().includes(q) ||
|
FR: 'France', BE: 'Belgique', DE: 'Allemagne', UK: 'Royaume-Uni', ES: 'Espagne', IT: 'Italie',
|
||||||
s.hashtags.some(h => h.toLowerCase().includes(q))
|
PT: 'Portugal', CH: 'Suisse', AT: 'Autriche', DK: 'Danemark', PL: 'Pologne', NL: 'Pays-Bas',
|
||||||
)
|
SE: 'Suède', USA: 'États-Unis', INT: 'International',
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
/** Valeurs d'une étiquette, de la plus fréquente à la plus rare (ordre stable des chips et des groupes). */
|
||||||
// Pour simplifier : toutes les structures (la carte gère les sans-coords)
|
function parFrequence(cle: (s: StructureV2) => string[], libelle: (v: string) => string): ValeurFiltre[] {
|
||||||
const metropoleStructures = computed<StructureV2[]>(() => filtered.value)
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// Panneau « Toutes les fiches » (B3-M3) — mêmes structures que `filtered`,
|
|
||||||
// sans filtrer par géolocalisation (adaptateur mutualisé avec `/`).
|
|
||||||
const { toMini } = useFicheAdapter()
|
const { toMini } = useFicheAdapter()
|
||||||
const allStructuresMini = computed(() => filtered.value.map((s) => toMini(s)))
|
const fichesMini = computed(() => structures.value.map((s) => toMini(s)))
|
||||||
|
const vf = useVueFiches(fichesMini, config)
|
||||||
|
|
||||||
// Outre-mer : pas de structures V2 DOM-TOM pour l'instant - garder le composant existant vide
|
// La carte ne montre que le résultat courant (recherche + filtres)
|
||||||
// OutremerMap attend le format Org legacy - on passe un tableau vide
|
const metropoleStructures = computed<StructureV2[]>(() => structures.value.filter((s) => vf.idsResultat.has(s.id)))
|
||||||
const outremerOrgsLegacy = computed(() => [])
|
|
||||||
const selectedIdLegacyNum = computed(() => null)
|
// ── 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)
|
||||||
|
|
||||||
// ── Sélection ─────────────────────────────────────────────────────────────
|
|
||||||
function onSelectStructure(id: string) {
|
function onSelectStructure(id: string) {
|
||||||
selectedId.value = selectedId.value === id ? null : id
|
const ordinateur = typeof window !== 'undefined' && window.innerWidth >= 1024
|
||||||
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
selectedId.value = ordinateur && selectedId.value === id ? null : id
|
||||||
ficheModalId.value = id
|
|
||||||
ficheModalOpen.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectStructureMobile(id: string) {
|
|
||||||
selectedId.value = id
|
|
||||||
ficheModalId.value = id
|
ficheModalId.value = id
|
||||||
ficheModalOpen.value = true
|
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" })
|
useHead({ title: "AEP - Réseaux de bifurcation architecturale" })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -692,6 +270,6 @@ useHead({ title: "AEP - Réseaux de bifurcation architecturale" })
|
|||||||
.reseaux-info-btn:hover { opacity: 0.85; transform: translateY(-1px); color: var(--nav-text); }
|
.reseaux-info-btn:hover { opacity: 0.85; transform: translateY(-1px); color: var(--nav-text); }
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.reseaux-info-btn { bottom: 16px; left: 340px; }
|
.reseaux-info-btn { bottom: 10px; left: auto; right: 52px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user