- 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)
290 lines
8.5 KiB
Vue
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>
|