Files
nav-carte/pages/fiche/[id].vue
Jules Neny 682d5d337e fix(aep-v1.1): bugs E2E M1 M2 M3 L1 L2 L3
M1 - Chips a11y : converti les <span> chips mobile (criteres, types,
echelle, fonctions) en <button type=button> avec aria-pressed pour
support clavier et lecteurs d'ecran (sidebar desktop deja en boutons).

M2 - Effacer les filtres ne vide pas la search : resetFilters() reset
maintenant aussi mobileSearch dans pratiques-regeneratives.vue et
index.vue.

M3 - FAB Soutenir overlap chip Agence : repositionne le FAB Soutenir
en stack vertical avec le FAB Chatbot (right: 16px, bottom: 84px) au
lieu de left: 16px, bottom: 68px. Evite l'overlap avec les chips de
la bottom-sheet sur viewport intermediaire.

L1 - /fiche/[id] introuvable pour pratiques : ajoute un fallback dans
pages/fiche/[id].vue qui detecte si l'id correspond a une pratique
regenerative et redirige vers /pratique/[id] (navigateTo replace).

L2 - Label retour incorrect sur /proposer-pratique : harmonise en
'Retour aux pratiques regeneratives'.

L3 - Map ne fitBounds pas apres filtre : EuropeMap et NavMap appellent
maintenant fitBounds(bounds, padding 40px, maxZoom 10) quand la liste
filtree contient 1 a 15 markers. Saute le tout premier rendu pour
preserver la vue initiale.
2026-04-30 02:31:31 +02:00

139 lines
4.9 KiB
Vue

<template>
<div class="min-h-screen" style="background: var(--nav-bg);">
<div class="max-w-4xl mx-auto px-4 py-6">
<!-- Bouton retour carte (préserve filtres URL) -->
<NuxtLink
:to="retourUrl"
class="inline-flex items-center gap-1.5 text-sm mb-6 rounded-lg px-3 py-1.5 transition-colors"
style="color: var(--nav-text); background: var(--nav-bg-alt);"
aria-label="Retour à la carte"
>
<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">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Retour à la carte
</NuxtLink>
<!-- Chargement -->
<div v-if="pending" class="py-16 text-center text-sm" style="color: var(--nav-text-muted);">
Chargement de la fiche
</div>
<!-- Erreur -->
<div v-else-if="error" class="py-16 text-center">
<p class="text-lg 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 -->
<FicheDetail :org="org" />
<!-- Séparateur -->
<div class="mb-6" style="height: 1px; background: var(--nav-bg-alt);"></div>
<!-- CommentSection -->
<CommentSection :org-id="org.Id" :refresh="commentRefreshTick" />
<!-- CommentForm -->
<CommentForm :org-id="org.Id" @submitted="onCommentSubmitted" />
</template>
</div>
</div>
</template>
<script setup lang="ts">
import type { Org } from '~/types/org'
// ── Params & route ────────────────────────────────────────────────────
const route = useRoute()
const orgId = route.params.id as string
// ── Retour carte — préserve les filtres via sessionStorage ────────────
const retourUrl = ref('/')
onMounted(() => {
if (typeof window !== 'undefined') {
const stored = sessionStorage.getItem('nav_back_filters')
if (stored) {
retourUrl.value = `/?${stored}`
}
}
})
// ── Fetch fiche SSR ───────────────────────────────────────────────────
const { data: org, pending, error } = await useFetch<Org>(`/api/fiche/${orgId}`, {
key: `fiche-${orgId}`,
})
// ── Fallback Pratiques regeneratives (bug E2E L1) ─────────────────────
// Si /api/fiche/:id echoue, on regarde si l'id correspond a une pratique
// regenerative et on redirige automatiquement vers /pratique/:id.
if (error.value) {
try {
const pratiquesRes = await $fetch<{ list: { id: number }[] }>('/api/pratiques')
const numericId = Number(orgId)
if (!isNaN(numericId) && pratiquesRes.list?.some((p) => p.id === numericId)) {
await navigateTo(`/pratique/${numericId}`, { replace: true })
}
} catch {
// pas de fallback dispo, on garde l'erreur
}
}
// ── Commentaires — tick de rafraîchissement ───────────────────────────
const commentRefreshTick = ref(0)
function onCommentSubmitted() {
// Incrémenter pour que CommentSection se recharge
commentRefreshTick.value++
}
// ── SEO dynamiques ────────────────────────────────────────────────────
const description = computed(() => {
if (!org.value) return 'Fiche organisation — AEP'
const desc =
org.value.description_enrichie ||
org.value.description_user ||
org.value.description ||
''
return desc.substring(0, 160).trim()
})
useHead({
title: computed(() =>
org.value ? `${org.value.nom} — AEP` : 'Fiche organisation — AEP'
),
meta: [
{
name: 'description',
content: description,
},
{
property: 'og:title',
content: computed(() =>
org.value ? `${org.value.nom} — AEP` : 'AEP'
),
},
{
property: 'og:description',
content: description,
},
{
property: 'og:image',
content: '/og-default.png', // logo par défaut dans public/
},
{
property: 'og:type',
content: 'article',
},
],
})
</script>