Files
nav-carte/app.vue
Jules Neny 46f57ae5fe fix(media): quick fixes post-visuel Phase 8.F + secu deploy.sh
- Retrait blur Voronoi (.voronoi-bg filter:blur 10px supprime) : retour aux
  cellules colorees non-blurrees, plus lisible visuellement
- Onglet "MEDIA" renomme "recherche-média" (app.vue desktop nav + sheet mobile)
- deploy.sh sed redact etendu : couvre desormais TOKEN, API_KEY, PASSWORD,
  SECRET (avant : TOKEN uniquement). Fix incident leak MISTRAL_API_KEY +
  RESEND_API_KEY dans transcript Phase 8 deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:46:09 +02:00

397 lines
20 KiB
Vue
Raw 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="/media"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/media' }"
>
recherche-média
</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 — popover 3 choix -->
<div class="hidden sm:block relative" ref="proposerAnchor" data-proposer-popover>
<button
@click="proposerOpen = !proposerOpen"
class="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all hover:opacity-80 inline-flex items-center gap-1"
style="background: var(--nav-accent); color: var(--nav-text);"
aria-label="Proposer une contribution"
>
+ Proposer
</button>
<div
v-if="proposerOpen"
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[240px] py-1"
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
>
<NuxtLink
to="/contribuer"
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
style="color: var(--nav-text);"
@click="proposerOpen = false"
>
<span>Fiche Entraide <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 1 — Écosystème archi</span></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
<NuxtLink
to="/contribuer-reseau"
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
style="color: var(--nav-text);"
@click="proposerOpen = false"
>
<span>Réseau / collectif <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 2 — Réseaux AEP</span></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
<NuxtLink
to="/contribuer-job"
class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70"
style="color: var(--nav-text);"
@click="proposerOpen = false"
>
<span>Plateforme jobs <span style="color: var(--nav-text-muted); font-weight: 400; font-size: 0.7rem; display: block;">Carte 3 — Jobs archi</span></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
</div>
</div>
<!-- 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 → popover -->
<div class="sm:hidden relative" data-proposer-popover>
<button
@click="proposerOpen = !proposerOpen"
class="p-2 rounded-lg"
style="background: var(--nav-accent); color: var(--nav-text);"
title="Contribuer"
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>
</button>
<div
v-if="proposerOpen"
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[220px] py-1"
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
>
<NuxtLink to="/contribuer" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
<span>Fiche Entraide</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
<NuxtLink to="/contribuer-reseau" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
<span>Réseau / collectif</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
<div style="height: 1px; background: var(--nav-bg-alt); margin: 2px 0;"></div>
<NuxtLink to="/contribuer-job" class="flex items-center justify-between px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" style="color: var(--nav-text);" @click="proposerOpen = false">
<span>Plateforme jobs</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; color: var(--nav-text-muted);"><polyline points="9 18 15 12 9 6"/></svg>
</NuxtLink>
</div>
</div>
<!-- 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="/media" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/media' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">recherche-média</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 })
// ── Popover "+ Proposer" ─────────────────────────────────────────────────
const proposerOpen = ref(false)
const proposerAnchor = ref<HTMLElement | null>(null)
function onClickOutsideProposer(e: MouseEvent) {
// Ferme si le clic est hors de tout élément portant data-proposer-popover
const target = e.target as HTMLElement
if (!target.closest('[data-proposer-popover]')) {
proposerOpen.value = false
}
}
watch(proposerOpen, (open) => {
if (open) {
// Délai court pour ne pas attraper le clic d'ouverture lui-même
setTimeout(() => document.addEventListener('click', onClickOutsideProposer, true), 10)
} else {
document.removeEventListener('click', onClickOutsideProposer, true)
}
})
onUnmounted(() => {
document.removeEventListener('click', onClickOutsideProposer, true)
})
// ── 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>