Files
nav-carte/components/NavSidebar.vue
Jules Neny a6fff9a950 feat(aep-v1.1): PA3 bouton Proposer contextuel + CTA sidebar ecosysteme
- app.vue : bouton + Proposer du header pointe vers /proposer-pratique
  si on est sur la carte pratiques regenerative (et sous-pages /pratique/),
  sinon vers /contribuer (ecosysteme AEP par defaut). Idem icone mobile.
- NavSidebar.vue : ajoute le CTA + Proposer une fiche en pied de sidebar
  (style aligne sur PratiqueSidebar.vue)
2026-04-30 02:26:24 +02:00

290 lines
8.5 KiB
Vue

<template>
<aside
class="flex flex-col h-full overflow-hidden"
style="background: var(--nav-surface); border-right: 1px solid var(--nav-bg-alt);"
>
<!-- BARRE DE RECHERCHE (tout en haut) -->
<div
class="shrink-0 px-4 pt-4 pb-3 border-b"
style="border-color: var(--nav-bg-alt);"
>
<label class="sidebar-search-label" aria-label="Rechercher une organisation">
<svg
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"
class="sidebar-search-icon"
>
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
ref="searchInputEl"
:value="search"
type="search"
placeholder="Rechercher une organisation…"
class="sidebar-search-input"
autocomplete="off"
@input="emit('update:search', ($event.target as HTMLInputElement).value)"
/>
<button
v-if="search"
type="button"
class="sidebar-search-clear"
aria-label="Effacer la recherche"
@click.stop="emit('update:search', '')"
>
<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>
<!-- FILTRES (haut, compact) -->
<div
class="shrink-0 px-4 pt-3 pb-3 space-y-4 border-b"
style="border-color: var(--nav-bg-alt);"
>
<!-- Échelle (checkbox compactes) -->
<EchelleFilter
:modelValue="echelle"
:counts="echelleCount"
@update:modelValue="emit('update:echelle', $event)"
/>
<!-- Fonctions (checkbox compactes) -->
<FonctionFilter
:modelValue="fonctions"
:counts="fonctionCount"
@update:modelValue="emit('update:fonctions', $event)"
/>
</div>
<!-- LISTE FICHES (milieu, scrollable) -->
<div class="flex-1 flex flex-col min-h-0">
<!-- Header liste -->
<div
class="shrink-0 flex items-center justify-between px-4 py-2 border-b"
style="border-color: var(--nav-bg-alt);"
>
<span class="text-xs font-bold uppercase tracking-widest" style="color: var(--nav-text-muted);">
{{ resultCount }} résultat{{ resultCount > 1 ? 's' : '' }}
</span>
<button
v-if="hasActiveFilters"
@click="emit('reset-filters')"
class="text-xs underline hover:opacity-70"
style="color: var(--nav-text-muted);"
>
Effacer les filtres
</button>
</div>
<!-- Liste scrollable -->
<div class="flex-1 overflow-y-auto px-3 py-2 space-y-1.5">
<div
v-if="pending"
class="flex items-center justify-center py-8"
style="color: var(--nav-text-muted);"
>
Chargement
</div>
<div v-else-if="orgs.length === 0" class="text-center py-8">
<p class="text-xs" style="color: var(--nav-text-muted);">Aucun résultat</p>
</div>
<!-- Card fiche compacte -->
<div
v-for="org in orgs"
:key="org.Id"
class="rounded-lg px-3 py-2 cursor-pointer transition-all"
:style="selectedId === org.Id
? 'background: var(--nav-bg-alt); border-left: 3px solid var(--nav-accent); padding-left: 9px;'
: 'background: var(--nav-bg); border-left: 3px solid transparent; padding-left: 9px;'"
@click="emit('select-org', org.Id)"
@mouseenter="emit('hover-org', org.Id)"
@mouseleave="emit('hover-org', null)"
>
<div class="flex items-start justify-between gap-1.5">
<span
class="font-semibold text-sm leading-snug"
style="color: var(--nav-text);"
>{{ org.nom }}</span>
<span
v-if="org.echelle"
class="shrink-0 px-1.5 py-0.5 rounded-full text-xs"
style="background: var(--nav-bg-alt); color: var(--nav-text-muted); margin-top: 1px;"
>{{ org.echelle }}</span>
</div>
<div v-if="orgFonctions(org).length" class="mt-1 flex flex-wrap gap-1">
<span
v-for="fn in orgFonctions(org)"
:key="fn"
class="px-1.5 py-0.5 rounded text-xs"
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
>{{ fn }}</span>
</div>
<div v-if="org.localisation_ville" class="mt-0.5 text-xs" style="color: var(--nav-text-muted);">
{{ org.localisation_ville }}
</div>
</div>
</div>
</div>
<!-- CTA PROPOSER -->
<div
class="shrink-0 px-4 py-3 border-t"
style="border-color: var(--nav-bg-alt);"
>
<NuxtLink
to="/contribuer"
class="sidebar-cta-link"
>
+ Proposer une fiche
</NuxtLink>
</div>
</aside>
</template>
<script setup lang="ts">
interface Org {
Id: number
nom: string
echelle?: string
tags_fonction?: string
territoire?: string
localisation_ville?: string
latitude?: number | null
longitude?: number | null
prioritaire?: boolean
}
const props = defineProps<{
search: string
modeValue: string // 'metropole' | 'outremer'
echelle: string[]
fonctions: string[]
territoire: string | null
echelleCount: Record<string, number>
fonctionCount: Record<string, number>
territoireCount: Record<string, number>
resultCount: number
orgs: Org[] // fiches filtrées à afficher dans la liste
selectedId: number | null
hasActiveFilters: boolean
pending?: boolean
}>()
const emit = defineEmits<{
'update:search': [value: string]
'update:mode': [value: string]
'update:echelle': [value: string[]]
'update:fonctions': [value: string[]]
'update:territoire': [value: string | null]
'select-org': [id: number]
'hover-org': [id: number | null]
'reset-filters': []
}>()
const searchInputEl = ref<HTMLInputElement | null>(null)
function orgFonctions(org: Org): string[] {
return (org.tags_fonction ?? '').split(',').map(f => f.trim()).filter(Boolean).slice(0, 2)
}
</script>
<style scoped>
.sidebar-search-label {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--nav-bg-alt);
border-radius: 10px;
background: var(--nav-bg);
padding: 7px 10px;
cursor: text;
width: 100%;
box-sizing: border-box;
transition: border-color 0.2s;
}
.sidebar-search-label:focus-within {
border-color: var(--nav-primary);
background: var(--nav-surface);
}
.sidebar-search-icon {
color: var(--nav-text-muted);
flex-shrink: 0;
transition: color 0.2s;
}
.sidebar-search-label:focus-within .sidebar-search-icon {
color: var(--nav-primary-solid);
}
.sidebar-search-input {
border: none;
outline: none;
background: transparent;
color: var(--nav-text);
font-size: 13px;
width: 100%;
min-width: 0;
font-family: var(--nav-font);
}
.sidebar-search-input::placeholder {
color: var(--nav-text-muted);
}
.sidebar-search-input::-webkit-search-cancel-button {
display: none;
}
.sidebar-search-clear {
display: flex;
align-items: center;
justify-content: center;
color: var(--nav-text-muted);
flex-shrink: 0;
padding: 2px;
border-radius: 50%;
transition: color 0.15s, background 0.15s;
background: transparent;
border: none;
cursor: pointer;
}
.sidebar-search-clear:hover {
color: var(--nav-text);
background: var(--nav-bg-alt);
}
.sidebar-cta-link {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
text-align: center;
font-size: 0.82rem;
font-weight: 600;
color: var(--nav-primary-solid);
background: transparent;
border: 1px solid var(--nav-primary-solid);
border-radius: 6px;
text-decoration: none;
transition: background 0.15s, color 0.15s;
}
.sidebar-cta-link:hover {
background: var(--nav-primary);
color: var(--nav-text-on-primary);
}
</style>