Files
nav-carte/app.vue
Jules Neny f5732bf336 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>
2026-05-08 18:58:42 +02:00

313 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex flex-col h-screen overflow-hidden" :class="{ dark: isDark }" style="background: var(--nav-bg);">
<!-- TOP NAV GLOBAL -->
<header
class="flex items-center justify-between px-4 py-2.5 shrink-0 relative z-[9999] shadow-sm gap-3"
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);"
>
<!-- Logo -->
<a href="/" class="logo-link flex items-center gap-2 hover:opacity-90 transition-opacity shrink-0" title="Architecture d'Écologie Politique">
<div
class="h-8 px-2 rounded-lg flex items-center justify-center shrink-0"
style="background: var(--nav-primary-solid);"
>
<span class="font-bold text-xs tracking-tight" style="color: var(--nav-text-on-primary);">AEP</span>
</div>
<div class="logo-text flex flex-col leading-tight">
<span class="logo-line-1 font-bold tracking-tight" style="color: var(--nav-text);">Architecture</span>
<span class="logo-line-2 font-bold tracking-tight" style="color: var(--nav-text);">d'Écologie Politique</span>
</div>
</a>
<!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── -->
<nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets">
<NuxtLink
to="/"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/' }"
>
Écosystème Entraide Architecture
</NuxtLink>
<NuxtLink
to="/agences"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/agences' }"
>
Réseaux AEP
</NuxtLink>
<NuxtLink
to="/trouver-du-taf"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/trouver-du-taf' }"
>
Jobs
</NuxtLink>
<NuxtLink
to="/codev"
class="nav-tab"
:class="{ 'nav-tab--active': route.path.startsWith('/codev') }"
>
Codev
</NuxtLink>
<NuxtLink
to="/rag"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/rag' }"
>
RAG
<span class="nav-tab-badge">en construction</span>
</NuxtLink>
</nav>
<!-- ── Barre recherche mobile (640px1024px) — masquée < 640px car accessible dans la sheet -->
<div class="hidden sm:flex lg:hidden flex-1 mx-2">
<label class="flex items-center gap-2 w-full px-3 py-1.5 rounded-xl border" style="background: var(--nav-bg); border-color: var(--nav-bg-alt);">
<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="headerSearch"
type="search"
placeholder="Rechercher…"
class="flex-1 bg-transparent border-0 outline-none text-sm"
style="color: var(--nav-text); font-family: var(--nav-font);"
autocomplete="off"
@input="onHeaderSearch"
@keydown.enter="onHeaderSearch"
/>
<button
v-if="headerSearch"
type="button"
@click.stop="clearHeaderSearch"
class="flex-shrink-0"
style="color: var(--nav-text-muted);"
aria-label="Effacer la recherche"
>
<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>
<!-- Actions droite -->
<div class="flex items-center gap-2 shrink-0">
<NuxtLink
to="/a-propos"
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-all hover:opacity-80 hidden md:inline-flex items-center gap-1"
style="color: var(--nav-text-muted);"
>
À propos
</NuxtLink>
<NuxtLink
to="/signaler"
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-all hover:opacity-80 hidden lg:inline-flex items-center gap-1"
style="color: var(--nav-text-muted);"
>
Signaler
</NuxtLink>
<!-- Proposer une ressource -->
<NuxtLink
to="/contribuer"
class="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all hover:opacity-80 hidden sm:inline-flex items-center gap-1"
style="background: var(--nav-accent); color: var(--nav-text);"
>
+ Proposer
</NuxtLink>
<!-- Toggle dark mode -->
<button
@click="toggleDark"
class="p-2 rounded-lg transition-all hover:opacity-80"
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
:title="isDark ? 'Passer en mode clair' : 'Passer en mode sombre'"
:aria-label="isDark ? 'Mode clair' : 'Mode sombre'"
>
<svg v-if="!isDark" 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">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<svg v-else 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">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
</button>
<!-- Mobile : contribuer icône -->
<NuxtLink
to="/contribuer"
class="sm:hidden p-2 rounded-lg"
style="background: var(--nav-accent); color: var(--nav-text);"
title="Contribuer une fiche"
aria-label="Contribuer"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</NuxtLink>
<!-- Hamburger mobile (lg:hidden) — toujours en dernier à droite -->
<div class="lg:hidden relative">
<button
@click="hamburgerOpen = !hamburgerOpen"
class="p-2 rounded-lg transition-all hover:opacity-80"
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
:aria-label="hamburgerOpen ? 'Fermer le menu' : 'Menu'"
:aria-expanded="hamburgerOpen"
>
<svg v-if="!hamburgerOpen" 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="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<svg v-else 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
v-if="hamburgerOpen"
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[210px] py-1"
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
@click="hamburgerOpen = false"
>
<NuxtLink to="/" class="block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Écosystème Entraide Architecture</NuxtLink>
<NuxtLink to="/agences" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/agences' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Réseaux AEP</NuxtLink>
<NuxtLink to="/trouver-du-taf" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/trouver-du-taf' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Jobs</NuxtLink>
<NuxtLink to="/rag" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text);">RAG</NuxtLink>
<NuxtLink to="/codev" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path.startsWith('/codev') ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Codev</NuxtLink>
<div style="height: 1px; background: var(--nav-bg-alt); margin: 4px 0;"></div>
<NuxtLink to="/manifeste" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/manifeste' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text-muted);'">Manifeste</NuxtLink>
<NuxtLink to="/a-propos" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text-muted);">À propos</NuxtLink>
<a href="https://liberapay.com/trans-former.fr/donate" target="_blank" rel="noopener noreferrer" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text-muted);">Soutenir →</a>
<NuxtLink to="/signaler" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text-muted);">Signaler</NuxtLink>
</div>
</div>
</div>
</header>
<!-- Contenu page (flex-1 pour remplir l'espace) -->
<div class="flex-1" :class="route.path === '/' ? 'overflow-hidden' : 'overflow-y-auto'">
<NuxtPage />
</div>
<!-- Bandeau bas transparence IA + soutien + compteurs semaine -->
<BandeauBas />
</div>
</template>
<script setup lang="ts">
const router = useRouter()
const route = useRoute()
const hamburgerOpen = ref(false)
watch(() => route.path, () => { hamburgerOpen.value = false })
// ── Dark mode ─────────────────────────────────────────────────────────────
const isDark = ref(false)
onMounted(() => {
const stored = localStorage.getItem('aep_theme')
if (stored === 'dark') {
isDark.value = true
document.documentElement.classList.add('dark')
}
})
function toggleDark() {
isDark.value = !isDark.value
if (isDark.value) {
document.documentElement.classList.add('dark')
localStorage.setItem('aep_theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('aep_theme', 'light')
}
}
// ── Barre de recherche header mobile ─────────────────────────────────────
const headerSearch = ref((route.query.q as string) ?? '')
// Sync depuis URL quand la route change
watch(() => route.query.q, (v) => {
headerSearch.value = (v as string) ?? ''
})
function onHeaderSearch() {
const q = headerSearch.value.trim()
if (route.path === '/') {
router.replace({ query: q ? { ...route.query, q } : { ...route.query, q: undefined } })
} else if (q) {
router.push({ path: '/', query: { q } })
}
}
function clearHeaderSearch() {
headerSearch.value = ''
if (route.path === '/') {
const q = { ...route.query }
delete q.q
router.replace({ query: Object.keys(q).length ? q : undefined })
}
}
// ── Fiche aléatoire ───────────────────────────────────────────────────────
function goRandom() {
router.push({ path: '/', query: { random: '1' } })
}
</script>
<style>
/* ── Logo header (texte 2 lignes) ─────────────────────────────────────── */
.logo-text {
line-height: 1.05;
}
.logo-line-1, .logo-line-2 {
font-size: 0.7rem;
letter-spacing: -0.01em;
}
@media (min-width: 640px) {
.logo-line-1, .logo-line-2 { font-size: 0.78rem; }
}
@media (min-width: 1024px) {
.logo-line-1, .logo-line-2 { font-size: 0.85rem; }
}
/* ── Onglets header desktop ───────────────────────────────────────────── */
.nav-tab {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 6px 16px 8px;
font-size: 0.8rem;
font-weight: 500;
color: var(--nav-text-muted);
text-decoration: none;
transition: color 0.15s;
border-bottom: 2px solid transparent;
gap: 2px;
}
.nav-tab:hover {
color: var(--nav-text);
}
.nav-tab--active {
color: var(--nav-text);
border-bottom-color: var(--nav-primary-solid);
font-weight: 600;
}
.nav-tab-badge {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--nav-text-muted);
opacity: 0.65;
}
</style>