feat(fiches): / et /agences ouvrent la fiche dans le panneau ; FicheReseauContenu extrait, FicheModal et FicheModalV2 retirées ; Voir sur la carte (B12-M4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGNSEFZdoKvbdsGyfSnu5S
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bb55538a4
commit
5e0027b861
@@ -6,12 +6,14 @@
|
|||||||
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);"
|
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);"
|
||||||
>
|
>
|
||||||
<!-- Bandeau titre -->
|
<!-- Bandeau titre -->
|
||||||
<div class="px-6 pt-6 pb-4" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
<div :class="compact ? 'px-4 pt-4 pb-3' : 'px-6 pt-6 pb-4'" style="border-bottom: 1px solid var(--nav-bg-alt);">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
<div class="flex flex-col gap-3 mb-3" :class="compact ? '' : 'sm:flex-row sm:items-start sm:justify-between'">
|
||||||
<h1
|
<component
|
||||||
class="text-2xl font-bold leading-snug"
|
:is="compact ? 'h2' : 'h1'"
|
||||||
|
class="font-bold leading-snug"
|
||||||
|
:class="compact ? 'text-xl' : 'text-2xl'"
|
||||||
style="color: var(--nav-text);"
|
style="color: var(--nav-text);"
|
||||||
>{{ org.nom }}</h1>
|
>{{ org.nom }}</component>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-if="org.url"
|
v-if="org.url"
|
||||||
@@ -44,7 +46,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tags fonction -->
|
<!-- Tags fonction -->
|
||||||
<div v-if="fonctionTags.length" class="flex flex-wrap gap-1.5">
|
<ChipGroup v-if="fonctionTags.length && etiquettesCliquables" :aria-label="`Fonctions de ${org.nom}`">
|
||||||
|
<Chip
|
||||||
|
v-for="tag in fonctionTags"
|
||||||
|
:key="tag"
|
||||||
|
variant="fonction"
|
||||||
|
:label="tag"
|
||||||
|
:aria-label="`Filtrer par ${tag}`"
|
||||||
|
@toggle="emit('etiquette', tag)"
|
||||||
|
/>
|
||||||
|
</ChipGroup>
|
||||||
|
<div v-else-if="fonctionTags.length" class="flex flex-wrap gap-1.5">
|
||||||
<span
|
<span
|
||||||
v-for="tag in fonctionTags"
|
v-for="tag in fonctionTags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
@@ -55,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Corps : description + mini-carte -->
|
<!-- Corps : description + mini-carte -->
|
||||||
<div class="p-6 flex flex-col lg:flex-row gap-6">
|
<div class="flex flex-col gap-6" :class="compact ? 'p-4' : 'p-6 lg:flex-row'">
|
||||||
|
|
||||||
<!-- Descriptions -->
|
<!-- Descriptions -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -106,7 +118,8 @@
|
|||||||
<!-- Mini-carte Leaflet -->
|
<!-- Mini-carte Leaflet -->
|
||||||
<div
|
<div
|
||||||
v-if="hasCoords"
|
v-if="hasCoords"
|
||||||
class="shrink-0 lg:w-56 xl:w-64 rounded-xl overflow-hidden"
|
class="shrink-0 rounded-xl overflow-hidden"
|
||||||
|
:class="compact ? '' : 'lg:w-56 xl:w-64'"
|
||||||
style="height: 180px; border: 1px solid var(--nav-bg-alt);"
|
style="height: 180px; border: 1px solid var(--nav-bg-alt);"
|
||||||
>
|
>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
@@ -186,7 +199,15 @@
|
|||||||
const baseTiles = useBaseTiles()
|
const baseTiles = useBaseTiles()
|
||||||
import type { Org } from '~/types/org'
|
import type { Org } from '~/types/org'
|
||||||
|
|
||||||
const props = defineProps<{ org: Org }>()
|
const props = defineProps<{
|
||||||
|
org: Org
|
||||||
|
/** Panneau de détail de la vue fiches (B12, 380 px) : tout en colonne, titre en h2. */
|
||||||
|
compact?: boolean
|
||||||
|
/** Fonctions en chips cliquables : la vue fiches ajoute le filtre. */
|
||||||
|
etiquettesCliquables?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ etiquette: [fonction: string] }>()
|
||||||
|
|
||||||
// ── Champs ──────────────────────────────────────────────────────────────
|
// ── Champs ──────────────────────────────────────────────────────────────
|
||||||
const descriptionUser = computed(() =>
|
const descriptionUser = computed(() =>
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<Transition name="backdrop">
|
|
||||||
<div
|
|
||||||
v-if="modelValue && orgId != null"
|
|
||||||
class="fixed inset-0 z-[1500]"
|
|
||||||
style="background: rgba(26,34,56,0.55);"
|
|
||||||
@click="close"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Modal centré -->
|
|
||||||
<Transition name="modal">
|
|
||||||
<div
|
|
||||||
v-if="modelValue && orgId != null"
|
|
||||||
class="fiche-modal fixed z-[1501] left-1/2 -translate-x-1/2 flex flex-col"
|
|
||||||
style="
|
|
||||||
width: min(768px, 92vw);
|
|
||||||
background: var(--nav-bg);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
|
|
||||||
overflow: hidden;
|
|
||||||
"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-label="org?.nom ?? 'Fiche organisation'"
|
|
||||||
@keydown.esc="close"
|
|
||||||
>
|
|
||||||
<!-- Header modal -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-5 py-3 shrink-0 border-b"
|
|
||||||
style="background: var(--nav-surface); border-color: var(--nav-bg-alt);"
|
|
||||||
>
|
|
||||||
<span class="text-sm font-semibold" style="color: var(--nav-text-muted);">Fiche détaillée</span>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Lien fiche complète -->
|
|
||||||
<a
|
|
||||||
v-if="orgId"
|
|
||||||
:href="`/fiche/${orgId}`"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-opacity hover:opacity-70"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text);"
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
Ouvrir
|
|
||||||
</a>
|
|
||||||
<!-- Fermer -->
|
|
||||||
<button
|
|
||||||
@click="close"
|
|
||||||
class="w-8 h-8 rounded-lg flex items-center justify-center transition-colors"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
|
||||||
aria-label="Fermer"
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenu scrollable -->
|
|
||||||
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
||||||
<!-- Chargement -->
|
|
||||||
<div v-if="pending" class="py-12 text-center text-sm" style="color: var(--nav-text-muted);">
|
|
||||||
Chargement de la fiche…
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Erreur -->
|
|
||||||
<div v-else-if="error" class="py-12 text-center">
|
|
||||||
<p class="text-base font-semibold mb-2" style="color: var(--nav-text);">Fiche introuvable</p>
|
|
||||||
<p class="text-sm" style="color: var(--nav-text-muted);">L'organisation demandée n'existe pas ou a été supprimée.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenu -->
|
|
||||||
<template v-else-if="org">
|
|
||||||
<FicheDetail :org="org" />
|
|
||||||
<div class="mb-5" style="height: 1px; background: var(--nav-bg-alt);"></div>
|
|
||||||
<CommentSection :org-id="org.Id" :refresh="commentRefreshTick" />
|
|
||||||
<CommentForm :org-id="org.Id" @submitted="onCommentSubmitted" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Org } from '~/types/org'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
orgId: number | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fermeture Esc globale
|
|
||||||
onMounted(() => {
|
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && props.modelValue) close()
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', handler)
|
|
||||||
onUnmounted(() => window.removeEventListener('keydown', handler))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fetch fiche quand orgId change
|
|
||||||
const org = ref<Org | null>(null)
|
|
||||||
const pending = ref(false)
|
|
||||||
const error = ref(false)
|
|
||||||
|
|
||||||
watch(() => props.orgId, async (id) => {
|
|
||||||
if (id == null) return
|
|
||||||
pending.value = true
|
|
||||||
error.value = false
|
|
||||||
org.value = null
|
|
||||||
try {
|
|
||||||
org.value = await $fetch<Org>(`/api/fiche/${id}`)
|
|
||||||
} catch {
|
|
||||||
error.value = true
|
|
||||||
} finally {
|
|
||||||
pending.value = false
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
const commentRefreshTick = ref(0)
|
|
||||||
|
|
||||||
function onCommentSubmitted() {
|
|
||||||
commentRefreshTick.value++
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Modal positionnement : centré desktop, descendu sous le header sur mobile */
|
|
||||||
.fiche-modal {
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
max-height: 90vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
|
||||||
.fiche-modal {
|
|
||||||
top: 76px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
max-height: calc(100dvh - 92px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Backdrop */
|
|
||||||
.backdrop-enter-active, .backdrop-leave-active { transition: opacity 0.2s ease; }
|
|
||||||
.backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-enter-active, .modal-leave-active {
|
|
||||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
||||||
}
|
|
||||||
.modal-enter-from, .modal-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -52%);
|
|
||||||
}
|
|
||||||
@media (max-width: 1023px) {
|
|
||||||
.modal-enter-from, .modal-leave-to {
|
|
||||||
transform: translate(-50%, calc(-2% + 76px));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.backdrop-enter-active, .backdrop-leave-active { transition: none; }
|
|
||||||
.modal-enter-active, .modal-leave-active { transition: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<Transition name="backdrop">
|
|
||||||
<div
|
|
||||||
v-if="modelValue && structureId != null"
|
|
||||||
class="fixed inset-0 z-[1500]"
|
|
||||||
style="background: rgba(26,34,56,0.55);"
|
|
||||||
@click="close"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Modal -->
|
|
||||||
<Transition name="modal">
|
|
||||||
<div
|
|
||||||
v-if="modelValue && structureId != null && structure"
|
|
||||||
class="fiche-modal-v2 fixed z-[1501] left-1/2 -translate-x-1/2 flex flex-col"
|
|
||||||
style="
|
|
||||||
width: min(780px, 94vw);
|
|
||||||
background: var(--nav-bg);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
|
|
||||||
overflow: hidden;
|
|
||||||
"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-label="structure?.nom ?? 'Fiche structure'"
|
|
||||||
@keydown.esc="close"
|
|
||||||
>
|
|
||||||
<!-- Header modal -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between px-5 py-3 shrink-0"
|
|
||||||
:style="`border-bottom: 3px solid ${familleColor}; background: var(--nav-surface);`"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<!-- Pastille famille -->
|
|
||||||
<div
|
|
||||||
class="w-3 h-3 rounded-full shrink-0"
|
|
||||||
:style="`background: ${familleColor};`"
|
|
||||||
/>
|
|
||||||
<span class="text-sm font-semibold" style="color: var(--nav-text-muted);">
|
|
||||||
{{ familleLabel }}
|
|
||||||
</span>
|
|
||||||
<!-- Badges -->
|
|
||||||
<div class="flex gap-1.5">
|
|
||||||
<span
|
|
||||||
v-if="structure.badges.centre_ressources"
|
|
||||||
class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
style="background: #2d8a6b22; color: #2d8a6b;"
|
|
||||||
>Centre ressources</span>
|
|
||||||
<span
|
|
||||||
v-if="structure.badges.mouvement_manifeste"
|
|
||||||
class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
style="background: #c44a2f22; color: #c44a2f;"
|
|
||||||
>Manifeste</span>
|
|
||||||
<span
|
|
||||||
v-if="structure.badges.contre_pouvoir_spatial"
|
|
||||||
class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
style="background: #1a3a6b22; color: #1a3a6b;"
|
|
||||||
>Contre-pouvoir</span>
|
|
||||||
<span
|
|
||||||
v-if="structure.badges.f6_recherche_politique"
|
|
||||||
class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
style="background: #6b3fa022; color: #6b3fa0;"
|
|
||||||
>Recherche pol.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
v-if="structure.url"
|
|
||||||
:href="structure.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-opacity hover:opacity-70"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text);"
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
Site web
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
@click="close"
|
|
||||||
class="w-8 h-8 rounded-lg flex items-center justify-center transition-colors"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
|
||||||
aria-label="Fermer"
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenu scrollable -->
|
|
||||||
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
||||||
<!-- Nom + meta -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h2 class="text-xl font-bold mb-1" style="color: var(--nav-text);">{{ structure.nom }}</h2>
|
|
||||||
<div class="flex flex-wrap gap-2 text-sm" style="color: var(--nav-text-muted);">
|
|
||||||
<span>{{ structure.type_principal }}</span>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{{ structure.ville }}, {{ structure.pays }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hashtags -->
|
|
||||||
<div v-if="structure.hashtags.length" class="flex flex-wrap gap-1.5 mb-4">
|
|
||||||
<span
|
|
||||||
v-for="tag in structure.hashtags"
|
|
||||||
:key="tag"
|
|
||||||
class="px-2 py-0.5 rounded-full text-xs"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
|
||||||
>{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description courte -->
|
|
||||||
<p class="text-sm leading-relaxed mb-4" style="color: var(--nav-text);">{{ structure.description_courte }}</p>
|
|
||||||
|
|
||||||
<!-- Description longue (expandable) -->
|
|
||||||
<div v-if="structure.description_longue" class="mb-4">
|
|
||||||
<div
|
|
||||||
class="text-sm leading-relaxed"
|
|
||||||
style="color: var(--nav-text); white-space: pre-wrap;"
|
|
||||||
:style="showFullDesc ? '' : 'max-height: 120px; overflow: hidden;'"
|
|
||||||
>{{ structure.description_longue }}</div>
|
|
||||||
<button
|
|
||||||
@click="showFullDesc = !showFullDesc"
|
|
||||||
class="mt-2 text-xs underline"
|
|
||||||
style="color: var(--nav-text-muted);"
|
|
||||||
>{{ showFullDesc ? 'Réduire' : 'Lire la suite…' }}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pensées rattachées -->
|
|
||||||
<div v-if="structure.pensees && structure.pensees.length" class="mb-4">
|
|
||||||
<h3 class="text-xs font-bold uppercase tracking-wide mb-2" style="color: var(--nav-text-muted);">Pensées rattachées</h3>
|
|
||||||
<div class="flex flex-wrap gap-1.5">
|
|
||||||
<span
|
|
||||||
v-for="pensee in structure.pensees"
|
|
||||||
:key="pensee.id"
|
|
||||||
class="px-2 py-0.5 rounded text-xs"
|
|
||||||
:style="pensee.confiance === 'ia_suggested'
|
|
||||||
? 'background: var(--nav-bg-alt); color: var(--nav-text-muted); border: 1px dashed var(--nav-bg-alt);'
|
|
||||||
: 'background: var(--nav-bg-alt); color: var(--nav-text);'"
|
|
||||||
:title="pensee.confiance === 'ia_suggested' ? 'IA suggéré' : ''"
|
|
||||||
>
|
|
||||||
{{ pensee.label }}<span v-if="pensee.confiance === 'ia_suggested'" class="ml-1 opacity-60">~</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Projets emblématiques -->
|
|
||||||
<div v-if="projetsStructure.length" class="mb-4">
|
|
||||||
<h3 class="text-xs font-bold uppercase tracking-wide mb-2" style="color: var(--nav-text-muted);">Projets emblématiques</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="projet in projetsStructure.slice(0, 5)"
|
|
||||||
:key="projet.id"
|
|
||||||
class="rounded-lg p-3"
|
|
||||||
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
|
||||||
<span class="font-medium text-sm" style="color: var(--nav-text);">{{ projet.nom }}</span>
|
|
||||||
<span v-if="projet.annee" class="text-xs shrink-0" style="color: var(--nav-text-muted);">{{ projet.annee }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="projet.lieu" class="text-xs mt-0.5" style="color: var(--nav-text-muted);">{{ projet.lieu }}</div>
|
|
||||||
<p class="text-xs mt-1 leading-relaxed" style="color: var(--nav-text-muted);">{{ projet.description.slice(0, 120) }}{{ projet.description.length > 120 ? '…' : '' }}</p>
|
|
||||||
<a
|
|
||||||
v-if="projet.url"
|
|
||||||
:href="projet.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-xs mt-1 inline-block"
|
|
||||||
style="color: var(--nav-text-muted); text-decoration: underline;"
|
|
||||||
>En savoir plus →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Structures voisines (graphe) -->
|
|
||||||
<div v-if="structuresVoisines.length" class="mb-4">
|
|
||||||
<h3 class="text-xs font-bold uppercase tracking-wide mb-2" style="color: var(--nav-text-muted);">Structures liées</h3>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
v-for="voisine in structuresVoisines.slice(0, 6)"
|
|
||||||
:key="voisine.id"
|
|
||||||
class="px-2 py-1 rounded text-xs transition-colors hover:opacity-70"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text); border: 1px solid transparent;"
|
|
||||||
@click="emit('update:structureId', voisine.id)"
|
|
||||||
>{{ voisine.nom }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sources -->
|
|
||||||
<div v-if="structure.sources && structure.sources.length" class="mb-4">
|
|
||||||
<h3 class="text-xs font-bold uppercase tracking-wide mb-2" style="color: var(--nav-text-muted);">Sources</h3>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div v-for="(source, i) in structure.sources" :key="i" class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="px-1.5 py-0.5 rounded text-xs shrink-0"
|
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
|
||||||
>{{ source.type }}</span>
|
|
||||||
<a
|
|
||||||
v-if="source.url"
|
|
||||||
:href="source.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-xs underline truncate"
|
|
||||||
style="color: var(--nav-text);"
|
|
||||||
>{{ source.titre }}</a>
|
|
||||||
<span v-else class="text-xs" style="color: var(--nav-text);">{{ source.titre }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTAs -->
|
|
||||||
<div class="flex gap-3 pt-2" style="border-top: 1px solid var(--nav-bg-alt);">
|
|
||||||
<a
|
|
||||||
href="/contribuer"
|
|
||||||
class="text-xs underline"
|
|
||||||
style="color: var(--nav-text-muted);"
|
|
||||||
>Signaler une erreur</a>
|
|
||||||
<a
|
|
||||||
href="/contribuer"
|
|
||||||
class="text-xs underline"
|
|
||||||
style="color: var(--nav-text-muted);"
|
|
||||||
>Réclamer cette fiche</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { ReseauxBifurcationData, StructureV2, ProjetEmblematique } from '~/types/structure-v2'
|
|
||||||
|
|
||||||
const FAMILLE_COLORS: Record<number, string> = {
|
|
||||||
1: '#a85d3e',
|
|
||||||
2: '#c4a472',
|
|
||||||
3: '#d4a017',
|
|
||||||
4: '#5a7a4a',
|
|
||||||
5: '#3d6a8c',
|
|
||||||
}
|
|
||||||
|
|
||||||
const FAMILLE_LABELS: Record<number, string> = {
|
|
||||||
1: 'Réemploi & filières',
|
|
||||||
2: 'Frugalité & low-tech',
|
|
||||||
3: 'Architecture sociale',
|
|
||||||
4: 'Collectifs & AMO',
|
|
||||||
5: 'Urbanisme de transition',
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
structureId: string | null
|
|
||||||
data: ReseauxBifurcationData | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
'update:structureId': [id: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const showFullDesc = ref(false)
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
showFullDesc.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fermeture Esc globale
|
|
||||||
onMounted(() => {
|
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && props.modelValue) close()
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', handler)
|
|
||||||
onUnmounted(() => window.removeEventListener('keydown', handler))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Remettre showFullDesc a false a chaque changement de fiche
|
|
||||||
watch(() => props.structureId, () => {
|
|
||||||
showFullDesc.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
const structure = computed<StructureV2 | null>(() => {
|
|
||||||
if (!props.data || !props.structureId) return null
|
|
||||||
return props.data.structures.find(s => s.id === props.structureId) ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
const familleColor = computed(() =>
|
|
||||||
FAMILLE_COLORS[structure.value?.famille_principale ?? 1] ?? '#888'
|
|
||||||
)
|
|
||||||
|
|
||||||
const familleLabel = computed(() =>
|
|
||||||
FAMILLE_LABELS[structure.value?.famille_principale ?? 1] ?? ''
|
|
||||||
)
|
|
||||||
|
|
||||||
const projetsStructure = computed<ProjetEmblematique[]>(() => {
|
|
||||||
if (!props.data || !props.structureId) return []
|
|
||||||
return props.data.projets?.filter(p => p.structure_parent === props.structureId) ?? []
|
|
||||||
})
|
|
||||||
|
|
||||||
const structuresVoisines = computed<StructureV2[]>(() => {
|
|
||||||
if (!props.data || !props.structureId) return []
|
|
||||||
const edges = props.data.graphe?.edges ?? []
|
|
||||||
const voisineIds = edges
|
|
||||||
.filter(e => e.source === props.structureId || e.target === props.structureId)
|
|
||||||
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
||||||
.map(e => e.source === props.structureId ? e.target : e.source)
|
|
||||||
.slice(0, 8)
|
|
||||||
|
|
||||||
return voisineIds
|
|
||||||
.map(id => props.data!.structures.find(s => s.id === id))
|
|
||||||
.filter(Boolean) as StructureV2[]
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Backdrop */
|
|
||||||
.backdrop-enter-active, .backdrop-leave-active { transition: opacity 0.2s ease; }
|
|
||||||
.backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
|
|
||||||
|
|
||||||
/* Modal positionnement : centré desktop, descendu sous le header sur mobile */
|
|
||||||
.fiche-modal-v2 {
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
max-height: 90vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
|
||||||
.fiche-modal-v2 {
|
|
||||||
top: 76px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
max-height: calc(100dvh - 92px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-enter-active, .modal-leave-active {
|
|
||||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
||||||
}
|
|
||||||
.modal-enter-from, .modal-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -52%);
|
|
||||||
}
|
|
||||||
@media (max-width: 1023px) {
|
|
||||||
.modal-enter-from, .modal-leave-to {
|
|
||||||
transform: translate(-50%, calc(-2% + 76px));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.backdrop-enter-active, .backdrop-leave-active { transition: none; }
|
|
||||||
.modal-enter-active, .modal-leave-active { transition: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<!--
|
||||||
|
FicheReseauContenu — B12-M4. Le corps de l'ancienne FicheModalV2 (retirée),
|
||||||
|
devenu un contenu : la vue fiches le pose dans son panneau de détail
|
||||||
|
(slot #detail de VueFiches). Badges, hashtags, descriptions, pensées,
|
||||||
|
projets, structures liées, sources.
|
||||||
|
|
||||||
|
Hashtag cliqué → `etiquette` (la vue fiches ajoute le filtre) ; structure
|
||||||
|
liée cliquée → `voisin` (la page l'ouvre dans le même panneau).
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<article v-if="structure" class="frc">
|
||||||
|
<div class="frc-famille" :style="`border-bottom-color: ${familleColor};`">
|
||||||
|
<span class="frc-famille__pastille" :style="`background: ${familleColor};`" aria-hidden="true" />
|
||||||
|
<span class="frc-famille__libelle">{{ familleLabel }}</span>
|
||||||
|
<span v-if="structure.badges.centre_ressources" class="frc-badge" style="background: #2d8a6b22; color: #1f6650;">Centre ressources</span>
|
||||||
|
<span v-if="structure.badges.mouvement_manifeste" class="frc-badge" style="background: #c44a2f22; color: #9a3520;">Manifeste</span>
|
||||||
|
<span v-if="structure.badges.contre_pouvoir_spatial" class="frc-badge" style="background: #1a3a6b22; color: #1a3a6b;">Contre-pouvoir</span>
|
||||||
|
<span v-if="structure.badges.f6_recherche_politique" class="frc-badge" style="background: #6b3fa022; color: #5a3388;">Recherche pol.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="frc-nom">{{ structure.nom }}</h2>
|
||||||
|
<p class="frc-meta">
|
||||||
|
<span>{{ structure.type_principal }}</span>
|
||||||
|
<span aria-hidden="true"> · </span>
|
||||||
|
<span>{{ structure.ville }}, {{ structure.pays }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="structure.url"
|
||||||
|
:href="structure.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="frc-site"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</svg>
|
||||||
|
Site web<span class="frc-sr"> (nouvel onglet)</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ChipGroup v-if="structure.hashtags.length" :aria-label="`Hashtags de ${structure.nom}`" class="frc-tags">
|
||||||
|
<Chip
|
||||||
|
v-for="tag in structure.hashtags"
|
||||||
|
:key="tag"
|
||||||
|
variant="fonction"
|
||||||
|
:label="tag"
|
||||||
|
:active="etiquettesActives?.includes(tag)"
|
||||||
|
:aria-label="`Filtrer par ${tag}`"
|
||||||
|
@toggle="emit('etiquette', tag)"
|
||||||
|
/>
|
||||||
|
</ChipGroup>
|
||||||
|
|
||||||
|
<p class="frc-texte">{{ structure.description_courte }}</p>
|
||||||
|
|
||||||
|
<div v-if="structure.description_longue" class="frc-bloc">
|
||||||
|
<div class="frc-texte frc-long" :class="{ 'frc-long--replie': !showFullDesc }">{{ structure.description_longue }}</div>
|
||||||
|
<button type="button" class="frc-lien" :aria-expanded="showFullDesc" @click="showFullDesc = !showFullDesc">
|
||||||
|
{{ showFullDesc ? 'Réduire' : 'Lire la suite…' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="structure.pensees && structure.pensees.length" class="frc-bloc">
|
||||||
|
<h3 class="frc-titre">Pensées rattachées</h3>
|
||||||
|
<div class="frc-liste-inline">
|
||||||
|
<span
|
||||||
|
v-for="pensee in structure.pensees"
|
||||||
|
:key="pensee.id"
|
||||||
|
class="frc-pensee"
|
||||||
|
:class="{ 'frc-pensee--ia': pensee.confiance === 'ia_suggested' }"
|
||||||
|
:title="pensee.confiance === 'ia_suggested' ? 'IA suggéré' : ''"
|
||||||
|
>{{ pensee.label }}<span v-if="pensee.confiance === 'ia_suggested'" aria-hidden="true"> ~</span><span v-if="pensee.confiance === 'ia_suggested'" class="frc-sr"> (suggéré par l'IA)</span></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="projetsStructure.length" class="frc-bloc">
|
||||||
|
<h3 class="frc-titre">Projets emblématiques</h3>
|
||||||
|
<div class="frc-projets">
|
||||||
|
<div v-for="projet in projetsStructure.slice(0, 5)" :key="projet.id" class="frc-projet">
|
||||||
|
<div class="frc-projet__tete">
|
||||||
|
<span class="frc-projet__nom">{{ projet.nom }}</span>
|
||||||
|
<span v-if="projet.annee" class="frc-discret">{{ projet.annee }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="projet.lieu" class="frc-discret">{{ projet.lieu }}</div>
|
||||||
|
<p class="frc-projet__desc">{{ projet.description.slice(0, 120) }}{{ projet.description.length > 120 ? '…' : '' }}</p>
|
||||||
|
<a v-if="projet.url" :href="projet.url" target="_blank" rel="noopener noreferrer" class="frc-lien">En savoir plus<span class="frc-sr"> sur {{ projet.nom }} (nouvel onglet)</span> →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="structuresVoisines.length" class="frc-bloc">
|
||||||
|
<h3 class="frc-titre">Structures liées</h3>
|
||||||
|
<div class="frc-liste-inline">
|
||||||
|
<button
|
||||||
|
v-for="voisine in structuresVoisines.slice(0, 6)"
|
||||||
|
:key="voisine.id"
|
||||||
|
type="button"
|
||||||
|
class="frc-voisin"
|
||||||
|
@click="emit('voisin', voisine.id)"
|
||||||
|
>{{ voisine.nom }}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="structure.sources && structure.sources.length" class="frc-bloc">
|
||||||
|
<h3 class="frc-titre">Sources</h3>
|
||||||
|
<ul class="frc-sources">
|
||||||
|
<li v-for="(source, i) in structure.sources" :key="i">
|
||||||
|
<span class="frc-source__type">{{ source.type }}</span>
|
||||||
|
<a v-if="source.url" :href="source.url" target="_blank" rel="noopener noreferrer" class="frc-lien">{{ source.titre }}</a>
|
||||||
|
<span v-else>{{ source.titre }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="frc-cta">
|
||||||
|
<NuxtLink to="/contribuer" class="frc-lien">Signaler une erreur</NuxtLink>
|
||||||
|
<NuxtLink to="/contribuer" class="frc-lien">Réclamer cette fiche</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ReseauxBifurcationData, StructureV2, ProjetEmblematique } from '~/types/structure-v2'
|
||||||
|
|
||||||
|
const FAMILLE_COLORS: Record<number, string> = {
|
||||||
|
1: '#a85d3e',
|
||||||
|
2: '#c4a472',
|
||||||
|
3: '#d4a017',
|
||||||
|
4: '#5a7a4a',
|
||||||
|
5: '#3d6a8c',
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAMILLE_LABELS: Record<number, string> = {
|
||||||
|
1: 'Réemploi & filières',
|
||||||
|
2: 'Frugalité & low-tech',
|
||||||
|
3: 'Architecture sociale',
|
||||||
|
4: 'Collectifs & AMO',
|
||||||
|
5: 'Urbanisme de transition',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
structureId: string
|
||||||
|
data: ReseauxBifurcationData | null
|
||||||
|
/** Hashtags actifs dans les filtres (avec leur « # ») : la chip s'allume. */
|
||||||
|
etiquettesActives?: string[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
etiquette: [hashtag: string]
|
||||||
|
voisin: [id: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showFullDesc = ref(false)
|
||||||
|
watch(() => props.structureId, () => { showFullDesc.value = false })
|
||||||
|
|
||||||
|
const structure = computed<StructureV2 | null>(() =>
|
||||||
|
props.data?.structures.find((s) => s.id === props.structureId) ?? null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const familleColor = computed(() => FAMILLE_COLORS[structure.value?.famille_principale ?? 1] ?? '#888')
|
||||||
|
const familleLabel = computed(() => FAMILLE_LABELS[structure.value?.famille_principale ?? 1] ?? '')
|
||||||
|
|
||||||
|
const projetsStructure = computed<ProjetEmblematique[]>(() =>
|
||||||
|
props.data?.projets?.filter((p) => p.structure_parent === props.structureId) ?? [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const structuresVoisines = computed<StructureV2[]>(() => {
|
||||||
|
if (!props.data) return []
|
||||||
|
const edges = props.data.graphe?.edges ?? []
|
||||||
|
return edges
|
||||||
|
.filter((e) => e.source === props.structureId || e.target === props.structureId)
|
||||||
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
||||||
|
.map((e) => (e.source === props.structureId ? e.target : e.source))
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((id) => props.data!.structures.find((s) => s.id === id))
|
||||||
|
.filter(Boolean) as StructureV2[]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.frc { color: var(--text); font-size: var(--fs-sm); }
|
||||||
|
.frc-sr {
|
||||||
|
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
||||||
|
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-famille {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs) var(--space-xs);
|
||||||
|
padding-bottom: var(--space-xs);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
border-bottom: 3px solid;
|
||||||
|
}
|
||||||
|
.frc-famille__pastille { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.frc-famille__libelle { font-weight: var(--fw-semibold); color: color-mix(in srgb, var(--text) 78%, transparent); }
|
||||||
|
.frc-badge {
|
||||||
|
padding: 2px var(--space-xs);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: var(--fs-xs, 0.75rem);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-nom { margin: 0 0 var(--space-2xs); font-size: 1.25rem; font-weight: var(--fw-bold, 700); line-height: 1.25; }
|
||||||
|
.frc-meta { margin: 0 0 var(--space-sm); color: color-mix(in srgb, var(--text) 78%, transparent); }
|
||||||
|
|
||||||
|
.frc-site {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 var(--space-sm);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-tags { margin-bottom: var(--space-sm); }
|
||||||
|
|
||||||
|
.frc-texte { margin: 0 0 var(--space-sm); line-height: var(--lh-base, 1.55); }
|
||||||
|
.frc-long { white-space: pre-wrap; margin: 0; }
|
||||||
|
.frc-long--replie { max-height: 120px; overflow: hidden; }
|
||||||
|
|
||||||
|
.frc-bloc { margin-bottom: var(--space-md); }
|
||||||
|
.frc-titre {
|
||||||
|
margin: 0 0 var(--space-xs);
|
||||||
|
font-size: var(--fs-xs, 0.75rem);
|
||||||
|
font-weight: var(--fw-bold, 700);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-liste-inline { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
|
||||||
|
.frc-pensee {
|
||||||
|
padding: 2px var(--space-xs);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-alt);
|
||||||
|
font-size: var(--fs-xs, 0.75rem);
|
||||||
|
}
|
||||||
|
.frc-pensee--ia { color: color-mix(in srgb, var(--text) 78%, transparent); border: 1px dashed var(--border); }
|
||||||
|
|
||||||
|
.frc-projets { display: flex; flex-direction: column; gap: var(--space-xs); }
|
||||||
|
.frc-projet { padding: var(--space-sm); border-radius: var(--radius-md); background: var(--bg); border: 1px solid var(--border); }
|
||||||
|
.frc-projet__tete { display: flex; justify-content: space-between; gap: var(--space-xs); }
|
||||||
|
.frc-projet__nom { font-weight: var(--fw-medium); }
|
||||||
|
.frc-projet__desc { margin: var(--space-2xs) 0; font-size: var(--fs-xs, 0.75rem); line-height: 1.5; color: color-mix(in srgb, var(--text) 78%, transparent); }
|
||||||
|
.frc-discret { font-size: var(--fs-xs, 0.75rem); color: color-mix(in srgb, var(--text) 78%, transparent); }
|
||||||
|
|
||||||
|
.frc-voisin {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 var(--space-xs);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--fs-xs, 0.75rem);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2xs); }
|
||||||
|
.frc-sources li { display: flex; align-items: baseline; gap: var(--space-xs); min-width: 0; font-size: var(--fs-xs, 0.75rem); overflow-wrap: anywhere; }
|
||||||
|
.frc-source__type { flex-shrink: 0; padding: 1px var(--space-2xs); border-radius: var(--radius-sm); background: var(--bg-alt); }
|
||||||
|
|
||||||
|
.frc-lien {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--fs-xs, 0.75rem);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frc-cta { display: flex; gap: var(--space-sm); padding-top: var(--space-xs); margin: 0; border-top: 1px solid var(--border); }
|
||||||
|
|
||||||
|
.frc-site:focus-visible,
|
||||||
|
.frc-lien:focus-visible,
|
||||||
|
.frc-voisin:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,6 +26,8 @@ const props = defineProps<{
|
|||||||
selectedId?: number | null
|
selectedId?: number | null
|
||||||
/** Fiche survolée dans la liste (B11) : épingle safran, sans recentrer. */
|
/** Fiche survolée dans la liste (B11) : épingle safran, sans recentrer. */
|
||||||
survolId?: number | null
|
survolId?: number | null
|
||||||
|
/** « Voir sur la carte » (B12) : zoome jusqu'à l'épingle, sortie de son cluster ; `n` change à chaque demande. */
|
||||||
|
centrage?: { id: number; n: number } | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -275,4 +277,20 @@ onUnmounted(() => {
|
|||||||
mapInstance = null
|
mapInstance = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// « Voir sur la carte » (B12) : la carte vient peut-être de s'afficher (Fiches → Mixte) ;
|
||||||
|
// on attend sa taille réelle, puis on sort l'épingle de son cluster.
|
||||||
|
watch(
|
||||||
|
() => props.centrage?.n,
|
||||||
|
() => {
|
||||||
|
const c = props.centrage
|
||||||
|
if (!c) return
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!mapInstance || !clusterGroup) return
|
||||||
|
mapInstance.invalidateSize({ pan: false })
|
||||||
|
const m = markers.get(c.id)
|
||||||
|
if (m) clusterGroup.zoomToShowLayer(m, () => mapInstance?.panTo(m.getLatLng(), { animate: true }))
|
||||||
|
}, 260)
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const props = defineProps<{
|
|||||||
selectedId?: string | null
|
selectedId?: string | null
|
||||||
/** Fiche survolée dans la liste (B11) : épingle cerclée de safran, sans recentrer. */
|
/** Fiche survolée dans la liste (B11) : épingle cerclée de safran, sans recentrer. */
|
||||||
survolId?: string | null
|
survolId?: string | null
|
||||||
|
/** « Voir sur la carte » (B12) : zoome jusqu'à l'épingle, sortie de son cluster ; `n` change à chaque demande. */
|
||||||
|
centrage?: { id: string; n: number } | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -272,4 +274,20 @@ onUnmounted(() => {
|
|||||||
mapInstance = null
|
mapInstance = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// « Voir sur la carte » (B12) : la carte vient peut-être de s'afficher (Fiches → Mixte) ;
|
||||||
|
// on attend sa taille réelle, puis on sort l'épingle de son cluster.
|
||||||
|
watch(
|
||||||
|
() => props.centrage?.n,
|
||||||
|
() => {
|
||||||
|
const c = props.centrage
|
||||||
|
if (!c) return
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!mapInstance || !clusterGroup) return
|
||||||
|
mapInstance.invalidateSize({ pan: false })
|
||||||
|
const m = markers.get(c.id)
|
||||||
|
if (m) clusterGroup.zoomToShowLayer(m, () => mapInstance?.panTo(m.getLatLng(), { animate: true }))
|
||||||
|
}, 260)
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+23
-17
@@ -11,7 +11,6 @@
|
|||||||
libelle-recherche="Rechercher une structure…"
|
libelle-recherche="Rechercher une structure…"
|
||||||
:pending="pending"
|
:pending="pending"
|
||||||
:selected-id="selectedId"
|
:selected-id="selectedId"
|
||||||
@select="(f) => onSelectStructure(String(f.id))"
|
|
||||||
>
|
>
|
||||||
<template #carte>
|
<template #carte>
|
||||||
<!-- Métropole -->
|
<!-- Métropole -->
|
||||||
@@ -21,6 +20,7 @@
|
|||||||
:structures="metropoleStructures"
|
:structures="metropoleStructures"
|
||||||
:selectedId="selectedId"
|
:selectedId="selectedId"
|
||||||
:survolId="vf.survolId == null ? null : String(vf.survolId)"
|
:survolId="vf.survolId == null ? null : String(vf.survolId)"
|
||||||
|
:centrage="centrageCarte"
|
||||||
@select-structure="onSelectStructure"
|
@select-structure="onSelectStructure"
|
||||||
@survol="(id) => (vf.survolId = id)"
|
@survol="(id) => (vf.survolId = id)"
|
||||||
/>
|
/>
|
||||||
@@ -63,15 +63,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VueFiches>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════ MODAL FICHE V2 -->
|
<!-- Détail (B12) : le corps de l'ancienne FicheModalV2 ; une structure liée s'ouvre dans le même panneau -->
|
||||||
<FicheModalV2
|
<template #detail="{ fiche, filtrer }">
|
||||||
v-model="ficheModalOpen"
|
<FicheReseauContenu
|
||||||
:structureId="ficheModalId"
|
:structure-id="String(fiche.id)"
|
||||||
:data="bifurcationData"
|
:data="bifurcationData"
|
||||||
@update:structureId="ficheModalId = $event"
|
: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) -->
|
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
||||||
<button
|
<button
|
||||||
@@ -221,18 +224,21 @@ const vf = useVueFiches(fichesMini, config)
|
|||||||
// La carte ne montre que le résultat courant (recherche + filtres)
|
// La carte ne montre que le résultat courant (recherche + filtres)
|
||||||
const metropoleStructures = computed<StructureV2[]>(() => structures.value.filter((s) => vf.idsResultat.has(s.id)))
|
const metropoleStructures = computed<StructureV2[]>(() => structures.value.filter((s) => vf.idsResultat.has(s.id)))
|
||||||
|
|
||||||
// ── Sélection : FicheModalV2 à toutes les largeurs (comportement d'avant B11) ──
|
// ── Sélection (B12) : la fiche ouverte vit dans l'URL (`fiche=`), détail en panneau ──
|
||||||
const selectedId = ref<string | null>(null)
|
const selectedId = computed<string | null>(() => vf.ficheId)
|
||||||
const ficheModalOpen = ref(false)
|
|
||||||
const ficheModalId = ref<string | null>(null)
|
|
||||||
|
|
||||||
function onSelectStructure(id: string) {
|
function onSelectStructure(id: string) {
|
||||||
const ordinateur = typeof window !== 'undefined' && window.innerWidth >= 1024
|
vf.ouvrirFiche(id)
|
||||||
selectedId.value = ordinateur && selectedId.value === id ? null : id
|
|
||||||
ficheModalId.value = id
|
|
||||||
ficheModalOpen.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// « 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 ──
|
// ── Boutons flottants (téléphone) : au-dessus du pied « Voir les N fiches » en état Carte ──
|
||||||
const chatbotOpen = ref(false)
|
const chatbotOpen = ref(false)
|
||||||
const missionOpen = ref(false)
|
const missionOpen = ref(false)
|
||||||
|
|||||||
+47
-29
@@ -17,7 +17,7 @@
|
|||||||
libelle-recherche="Rechercher une structure…"
|
libelle-recherche="Rechercher une structure…"
|
||||||
:pending="pending"
|
:pending="pending"
|
||||||
:selected-id="selectedId"
|
:selected-id="selectedId"
|
||||||
@select="(f) => onSelectOrg(Number(f.id))"
|
:lien-page="(f) => `/fiche/${f.id}`"
|
||||||
>
|
>
|
||||||
<template #carte>
|
<template #carte>
|
||||||
<!-- Métropole -->
|
<!-- Métropole -->
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
:orgs="metropoleOrgs"
|
:orgs="metropoleOrgs"
|
||||||
:selectedId="selectedId"
|
:selectedId="selectedId"
|
||||||
:survolId="survolNum"
|
:survolId="survolNum"
|
||||||
|
:centrage="centrageMetropole"
|
||||||
@select-org="onSelectOrg"
|
@select-org="onSelectOrg"
|
||||||
@survol="(id) => (vf.survolId = id)"
|
@survol="(id) => (vf.survolId = id)"
|
||||||
/>
|
/>
|
||||||
@@ -55,13 +56,22 @@
|
|||||||
<ChatbotPlaceholder @highlightOrgs="onHighlightOrgs" />
|
<ChatbotPlaceholder @highlightOrgs="onHighlightOrgs" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VueFiches>
|
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════ MODAL FICHE (ordinateur) -->
|
<!-- Détail (B12) : la fiche complète vient de la liste (même enregistrement NocoDB que /api/fiche/:id) -->
|
||||||
<FicheModal
|
<template #detail="{ fiche, filtrer }">
|
||||||
v-model="ficheModalOpen"
|
<template v-if="orgParId.get(String(fiche.id))">
|
||||||
:orgId="ficheModalId"
|
<FicheDetail
|
||||||
/>
|
:key="fiche.id"
|
||||||
|
:org="orgParId.get(String(fiche.id))!"
|
||||||
|
compact
|
||||||
|
etiquettes-cliquables
|
||||||
|
@etiquette="(t) => filtrer('fonctions', t)"
|
||||||
|
/>
|
||||||
|
<CommentSection :key="`c-${fiche.id}`" :org-id="Number(fiche.id)" :refresh="commentaires" />
|
||||||
|
<CommentForm :key="`f-${fiche.id}`" :org-id="Number(fiche.id)" @submitted="commentaires++" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</VueFiches>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
<!-- ═══════════════════════════════════════ BOUTON CHATBOT FLOTTANT (mobile) -->
|
||||||
<button
|
<button
|
||||||
@@ -171,41 +181,49 @@ const metropoleOrgs = computed<Org[]>(() => orgsFiltres.value.filter((o) => !o.t
|
|||||||
const outremerOrgs = computed<Org[]>(() => orgsFiltres.value.filter((o) => o.territoire && DOM_TOM.includes(o.territoire)))
|
const outremerOrgs = computed<Org[]>(() => orgsFiltres.value.filter((o) => o.territoire && DOM_TOM.includes(o.territoire)))
|
||||||
const survolNum = computed(() => (vf.survolId == null ? null : Number(vf.survolId)))
|
const survolNum = computed(() => (vf.survolId == null ? null : Number(vf.survolId)))
|
||||||
|
|
||||||
// ── Sélection ─────────────────────────────────────────────────────────────
|
// ── Sélection (B12) : la fiche ouverte vit dans l'URL (`fiche=`), détail en panneau ──
|
||||||
const selectedId = ref<number | null>(null)
|
const orgParId = computed(() => new Map(orgs.value.map((o) => [String(o.Id), o])))
|
||||||
const ficheModalOpen = ref(false)
|
|
||||||
const ficheModalId = ref<number | null>(null)
|
// Épingle et carte-fiche allumées : la fiche ouverte, ou 5 s celle que le chatbot désigne
|
||||||
|
const surligneId = ref<number | null>(null)
|
||||||
|
const selectedId = computed<number | null>(() => surligneId.value ?? (vf.ficheId != null ? Number(vf.ficheId) : null))
|
||||||
|
|
||||||
|
const commentaires = ref(0)
|
||||||
|
|
||||||
// Ordinateur : FicheModal ; téléphone et tablette : page /fiche/:id (comportement d'avant B11)
|
|
||||||
function onSelectOrg(id: number) {
|
function onSelectOrg(id: number) {
|
||||||
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
vf.ouvrirFiche(id)
|
||||||
selectedId.value = selectedId.value === id ? null : id
|
}
|
||||||
ficheModalId.value = id
|
|
||||||
ficheModalOpen.value = true
|
// « Voir sur la carte » : la bonne sous-vue (Métropole ou Outre-mer), puis l'épingle
|
||||||
return
|
const centrageMetropole = ref<{ id: number; n: number } | null>(null)
|
||||||
}
|
watch(() => vf.centrage, (c) => {
|
||||||
selectedId.value = id
|
if (!c) return
|
||||||
try {
|
const o = orgParId.value.get(c.id)
|
||||||
// Le bouton retour de /fiche/:id revient à la même vue, filtres compris
|
const dom = !!o?.territoire && DOM_TOM.includes(o.territoire)
|
||||||
sessionStorage.setItem('nav_back_filters', new URLSearchParams(route.query as Record<string, string>).toString())
|
if (vf.etat.sousVue !== (dom ? 'outremer' : 'metropole')) vf.setSousVue(dom ? 'outremer' : 'metropole')
|
||||||
} catch {}
|
if (!dom) centrageMetropole.value = { id: Number(c.id), n: c.n }
|
||||||
router.push(`/fiche/${id}`)
|
})
|
||||||
|
|
||||||
|
// Le bouton retour de /fiche/:id (« Ouvrir la page de la fiche ») revient à la même vue
|
||||||
|
if (import.meta.client) {
|
||||||
|
watch(() => route.fullPath, () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('nav_back_filters', new URLSearchParams(route.query as Record<string, string>).toString())
|
||||||
|
} catch {}
|
||||||
|
}, { immediate: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Surlignage temporaire (5 s) suite à une réponse chatbot
|
// Surlignage temporaire (5 s) suite à une réponse chatbot
|
||||||
let highlightTimer: ReturnType<typeof setTimeout> | null = null
|
let highlightTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const prevSelectedId = ref<number | null>(null)
|
|
||||||
|
|
||||||
function onHighlightOrgs(ids: (number | string)[]) {
|
function onHighlightOrgs(ids: (number | string)[]) {
|
||||||
if (!ids.length) return
|
if (!ids.length) return
|
||||||
const firstId = typeof ids[0] === 'string' ? parseInt(ids[0], 10) : ids[0]
|
const firstId = typeof ids[0] === 'string' ? parseInt(ids[0], 10) : ids[0]
|
||||||
if (isNaN(firstId)) return
|
if (isNaN(firstId)) return
|
||||||
prevSelectedId.value = selectedId.value
|
surligneId.value = firstId
|
||||||
selectedId.value = firstId
|
|
||||||
if (highlightTimer) clearTimeout(highlightTimer)
|
if (highlightTimer) clearTimeout(highlightTimer)
|
||||||
highlightTimer = setTimeout(() => {
|
highlightTimer = setTimeout(() => {
|
||||||
selectedId.value = prevSelectedId.value
|
surligneId.value = null
|
||||||
prevSelectedId.value = null
|
|
||||||
highlightTimer = null
|
highlightTimer = null
|
||||||
}, 5000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user