feat(mobile+UX): refonte hamburger, pop-ups Mission, Manifeste, fixes mobile

Hamburger:
- Ajout Jobs, Manifeste, Soutenir
- Ré-ordonnancement (cartes/RAG/Codev en haut, ressources en bas)

Pop-ups Mission:
- MissionPopup générique (slot, props title/ctaLabel/storageKey)
- Auto-show 1ère visite Carte 1 (Entraide) et Carte 2 (Réseaux AEP)
- Bouton (i) flottant pour rouvrir

Pages:
- /manifeste : nouvelle page (texte version page-carto-V1)
- /a-propos : section 1 retirée (devient pop-up Carte 1) + scroll latéral fixé
- /agences : 3e onglet "Graphe" sur mobile + labels structures sur GraphView
- /trouver-du-taf : intro pédagogique repliable (onglets / tags / 5 axes),
  filtres mobile repliables, "Plateformes B2C" → "Pour archi indépendants"

Mobile UX:
- FAB coeur jaune Soutenir retiré (BandeauBas) — accessible via hamburger
- FicheModal/V2 : décalage top:76px sur mobile pour ne plus mordre header
- Logo header : "Architecture d'Écologie / Politique" en clair (2 lignes)

Cause racine résolue:
- /api/chatbot-reseaux n'avait jamais été déployé → 404 en prod avant ce build

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-08 18:58:42 +02:00
parent 5967a5af57
commit f5732bf336
11 changed files with 889 additions and 137 deletions

View File

@@ -139,72 +139,7 @@
</footer>
<!-- FAB MOBILE (< 1024px) -->
<div v-else>
<!-- FAB soutenir (à gauche du chatbot) -->
<button
class="fab-soutenir"
type="button"
@click="fabSheetOpen = true"
aria-label="Soutenir le projet AEP"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<!-- Bottom sheet FAB -->
<Teleport to="body">
<Transition name="backdrop">
<div
v-if="fabSheetOpen"
class="fixed inset-0 z-[1020]"
style="background: rgba(26,34,56,0.5);"
@click="fabSheetOpen = false"
aria-hidden="true"
/>
</Transition>
<Transition name="sheet">
<div
v-if="fabSheetOpen"
class="fab-sheet"
role="dialog"
aria-modal="true"
aria-label="Soutenir AEP"
>
<!-- Poignée -->
<div class="flex justify-center pt-3 pb-1">
<div class="rounded-full" style="width: 36px; height: 4px; background: var(--nav-bg-alt);" />
</div>
<div class="px-5 pb-6">
<h2 class="text-base font-bold mb-2" style="color: var(--nav-text);">Soutenir AEP</h2>
<template v-if="stats">
<p class="text-sm mb-1" style="color: var(--nav-text-muted);">
Coût IA ce mois : <strong>{{ stats.cout_mois_eur.toFixed(2) }} </strong>
· Tokens : {{ stats.tokens_mois.toLocaleString('fr-FR') }}
</p>
<p class="text-sm mb-3" style="color: var(--nav-text-muted);">
{{ stats.fiches_semaine }} fiche{{ stats.fiches_semaine !== 1 ? 's' : '' }} ajoutée{{ stats.fiches_semaine !== 1 ? 's' : '' }} cette semaine
</p>
</template>
<p class="text-sm mb-4" style="color: var(--nav-text-muted); line-height: 1.5;">
1 = 30 fiches mises en ligne. AEP est libre, sans pub, financé par les dons.
</p>
<a
href="https://liberapay.com/trans-former.fr/donate"
target="_blank"
rel="noopener noreferrer"
class="block w-full text-center py-3 rounded-xl font-semibold text-sm"
style="background: var(--nav-primary); color: var(--nav-text-on-primary); text-decoration: none;"
@click="fabSheetOpen = false"
>
Soutenir sur Liberapay
</a>
</div>
</div>
</Transition>
</Teleport>
</div>
<!-- Mobile (< 1024px) : pas de FAB Soutenir est dans le menu hamburger -->
</template>
<script setup lang="ts">
@@ -221,7 +156,6 @@ interface Stats {
const stats = ref<Stats | null>(null)
const loading = ref(true)
const modalOpen = ref(false)
const fabSheetOpen = ref(false)
const tooltipVisible = ref(false)
// Desktop — replié par défaut, déploie au hover, replie immédiatement à la sortie
@@ -460,39 +394,6 @@ const jaugePct = computed(() => {
border-top-color: var(--nav-primary-solid, #1a2238);
}
/* ── FAB mobile soutenir ─────────────────────────────────────────────────── */
.fab-soutenir {
position: fixed;
bottom: 68px; /* au-dessus du FAB chatbot à 24px du bas + 48px de hauteur */
left: 16px;
z-index: 1000;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: var(--nav-accent);
color: var(--nav-text);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(26,34,56,0.25);
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
.fab-soutenir:hover { opacity: 0.88; transform: translateY(-1px); }
/* ── Bottom sheet FAB ────────────────────────────────────────────────────── */
.fab-sheet {
position: fixed;
inset-x: 0;
bottom: 0;
z-index: 1021;
background: var(--nav-surface);
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 32px rgba(26,34,56,0.18);
}
/* ── Modal ───────────────────────────────────────────────────────────────── */
.modal-backdrop {
position: fixed;

View File

@@ -15,10 +15,9 @@
<Transition name="modal">
<div
v-if="modelValue && orgId != null"
class="fixed z-[1501] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
class="fiche-modal fixed z-[1501] left-1/2 -translate-x-1/2 flex flex-col"
style="
width: min(768px, 92vw);
max-height: 90vh;
background: var(--nav-bg);
border-radius: 16px;
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
@@ -144,6 +143,21 @@ function onCommentSubmitted() {
</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; }
@@ -156,6 +170,11 @@ function onCommentSubmitted() {
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; }

View File

@@ -15,10 +15,9 @@
<Transition name="modal">
<div
v-if="modelValue && structureId != null && structure"
class="fixed z-[1501] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
class="fiche-modal-v2 fixed z-[1501] left-1/2 -translate-x-1/2 flex flex-col"
style="
width: min(780px, 94vw);
max-height: 90vh;
background: var(--nav-bg);
border-radius: 16px;
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
@@ -325,6 +324,21 @@ const structuresVoisines = computed<StructureV2[]>(() => {
.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;
@@ -333,6 +347,11 @@ const structuresVoisines = computed<StructureV2[]>(() => {
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; }

View File

@@ -587,6 +587,20 @@ async function initGraph() {
.attr('fill', '#2a2a2a')
.style('pointer-events', 'none')
// Labels structures : nom au-dessus du cercle, halo pour lisibilite
d3NodeSelection.filter((d: any) => d.type === 'structure')
.append('text')
.attr('class', 'graph-struct-label')
.text((d: any) => {
const raw = d.label as string
return raw.length > 22 ? raw.slice(0, 20) + '…' : raw
})
.attr('text-anchor', 'middle')
.attr('dy', (d: any) => -(d.r + 5))
.attr('font-size', '9.5px')
.attr('font-weight', '500')
.style('pointer-events', 'none')
// Tooltip hover pour structures
d3NodeSelection.filter((d: any) => d.type === 'structure')
.on('mouseenter', (_event: any, d: any) => {
@@ -858,3 +872,15 @@ onUnmounted(() => {
if (simulation) simulation.stop()
})
</script>
<style>
/* Labels des structures dans le graphe (D3 injecte les <text>, donc style global) */
.graph-view .graph-struct-label {
fill: var(--nav-text);
paint-order: stroke;
stroke: var(--nav-bg);
stroke-width: 3px;
stroke-linejoin: round;
user-select: none;
}
</style>

181
components/MissionPopup.vue Normal file
View File

@@ -0,0 +1,181 @@
<template>
<Teleport to="body">
<Transition name="backdrop">
<div
v-if="modelValue"
class="fixed inset-0 z-[1500]"
style="background: rgba(26,34,56,0.55);"
@click="close"
aria-hidden="true"
/>
</Transition>
<Transition name="modal">
<div
v-if="modelValue"
class="mission-modal"
role="dialog"
aria-modal="true"
aria-labelledby="mission-title"
@keydown.esc="close"
>
<button
class="mission-close"
type="button"
@click="close"
aria-label="Fermer"
>
<svg width="16" height="16" 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 class="mission-body">
<h2 id="mission-title" class="mission-title">{{ title }}</h2>
<slot>
<p class="mission-text">
L'architecture est l'une des professions les plus complexes qui soit ; elle croise droit, technique, esthétique, économie, social, écologie tout à la fois, tout simultanément, souvent sans filet. Paradoxalement, c'est aussi l'une des moins structurées sur le plan de l'entraide&nbsp;: peu de transmission horizontale, beaucoup d'isolement, une culture du chacun-pour-soi héritée d'une formation qui prépare à la compétition plus qu'à la coopération. On sort de l'école seul·e. On s'installe seul·e. On réinvente ce que d'autres ont déjà traversé.
</p>
<p class="mission-text">
Cette carte est née de cette frustration — et de cette conviction&nbsp;: les ressources existent, les gens qui ont réussi à sortir la tête de l'eau aussi. L'enjeu, c'est de les documenter, de les rendre accessibles, de les ajuster en temps réel grâce aux retours de la communauté. Pas un catalogue figé&nbsp;; un commun vivant, au service de ceux et celles qui cherchent à faire évoluer leur pratique vers quelque chose de plus épanouissant, mieux rémunéré, au service de la société et qui prend soin de la santé, la nôtre et celle des gens pour qui nous construisons.
</p>
</slot>
<div class="mission-cta-wrap">
<button class="btn-explorer" type="button" @click="close">{{ ctaLabel }}</button>
<NuxtLink to="/manifeste" class="link-manifeste" @click="close">Lire le manifeste </NuxtLink>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: boolean
title?: string
ctaLabel?: string
storageKey?: string
}>(), {
title: "L'écosystème d'entraide architecte",
ctaLabel: 'Explorer la carte',
storageKey: 'aep_mission_seen',
})
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
function close() {
emit('update:modelValue', false)
if (typeof window !== 'undefined') {
try { localStorage.setItem(props.storageKey, '1') } catch {}
}
}
</script>
<style scoped>
.mission-modal {
position: fixed;
z-index: 1501;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(560px, 92vw);
max-height: calc(100dvh - 80px);
background: var(--nav-bg);
border-radius: 16px;
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
display: flex;
flex-direction: column;
overflow: hidden;
}
.mission-close {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: var(--nav-bg-alt);
color: var(--nav-text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
z-index: 1;
}
.mission-close:hover { background: var(--nav-surface); }
.mission-body {
padding: 1.75rem 1.5rem 1.5rem;
overflow-y: auto;
}
.mission-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--nav-text);
margin: 0 0 1rem;
line-height: 1.25;
padding-right: 2rem;
}
.mission-text,
:slotted(.mission-text) {
font-size: 0.95rem;
line-height: 1.65;
color: var(--nav-text);
margin: 0 0 1rem;
}
:slotted(.mission-text strong) { font-weight: 700; }
:slotted(.mission-text a) { color: var(--nav-primary-solid); text-decoration: underline; }
.mission-cta-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
}
.btn-explorer {
padding: 0.65rem 1.25rem;
background: var(--nav-primary);
color: var(--nav-text-on-primary);
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-explorer:hover { opacity: 0.88; }
.link-manifeste {
font-size: 0.875rem;
color: var(--nav-primary-solid);
text-decoration: underline;
text-underline-offset: 2px;
}
.link-manifeste:hover { opacity: 0.75; }
.backdrop-enter-active, .backdrop-leave-active { transition: opacity 0.2s ease; }
.backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
.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%, -48%); }
@media (max-width: 480px) {
.mission-body { padding: 1.5rem 1.1rem 1.25rem; }
.mission-title { font-size: 1.1rem; }
.mission-text { font-size: 0.9rem; }
}
@media (prefers-reduced-motion: reduce) {
.modal-enter-active, .modal-leave-active { transition: none; }
.backdrop-enter-active, .backdrop-leave-active { transition: none; }
}
</style>