Files
nav-carte/components/FicheReseauContenu.vue
T

292 lines
11 KiB
Vue

<!--
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>