242 lines
7.2 KiB
Vue
242 lines
7.2 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 -->
|
|
<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 pratique">
|
|
<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 pratique…"
|
|
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 -->
|
|
<div
|
|
class="shrink-0 px-4 pt-3 pb-3 space-y-4 border-b overflow-y-auto"
|
|
style="border-color: var(--nav-bg-alt); max-height: 280px;"
|
|
>
|
|
<!-- Critères régé -->
|
|
<CritereFilter
|
|
:modelValue="criteres"
|
|
:counts="critereCount"
|
|
@update:modelValue="emit('update:criteres', $event)"
|
|
/>
|
|
|
|
<!-- Type entité -->
|
|
<TypeEntiteFilter
|
|
:modelValue="typesEntite"
|
|
:counts="typeCount"
|
|
@update:modelValue="emit('update:typesEntite', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════ LISTE FICHES -->
|
|
<div class="flex-1 flex flex-col min-h-0">
|
|
<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>
|
|
|
|
<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="pratiques.length === 0" class="text-center py-8">
|
|
<p class="text-xs" style="color: var(--nav-text-muted);">Aucun résultat</p>
|
|
</div>
|
|
|
|
<div
|
|
v-for="pratique in pratiques"
|
|
:key="pratique.id"
|
|
class="rounded-lg px-3 py-2 cursor-pointer transition-all"
|
|
:style="selectedId === pratique.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-pratique', pratique.id)"
|
|
@mouseenter="emit('hover-pratique', pratique.id)"
|
|
@mouseleave="emit('hover-pratique', null)"
|
|
>
|
|
<div class="flex items-start justify-between gap-1.5">
|
|
<span class="font-semibold text-sm leading-snug" style="color: var(--nav-text);">{{ pratique.nom }}</span>
|
|
<span
|
|
v-if="pratique.pays"
|
|
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;"
|
|
>{{ pratique.pays }}</span>
|
|
</div>
|
|
<div v-if="pratique.criteres?.length" class="mt-1 flex flex-wrap gap-1">
|
|
<span
|
|
v-for="cId in pratique.criteres.slice(0, 3)"
|
|
:key="cId"
|
|
class="px-1.5 py-0.5 rounded text-xs"
|
|
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
|
>{{ CRITERES.find(c => c.id === cId)?.label }}</span>
|
|
</div>
|
|
<div v-if="pratique.ville" class="mt-0.5 text-xs" style="color: var(--nav-text-muted);">
|
|
{{ pratique.ville }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { CRITERES } from '~/types/pratique'
|
|
|
|
interface Pratique {
|
|
id: number
|
|
nom: string
|
|
pays?: string
|
|
ville?: string
|
|
type?: string
|
|
criteres?: number[]
|
|
score?: number
|
|
}
|
|
|
|
const props = defineProps<{
|
|
search: string
|
|
criteres: number[]
|
|
typesEntite: string[]
|
|
critereCount: Record<number, number>
|
|
typeCount: Record<string, number>
|
|
resultCount: number
|
|
pratiques: Pratique[]
|
|
selectedId: number | null
|
|
hasActiveFilters: boolean
|
|
pending?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:search': [value: string]
|
|
'update:criteres': [value: number[]]
|
|
'update:typesEntite': [value: string[]]
|
|
'select-pratique': [id: number]
|
|
'hover-pratique': [id: number | null]
|
|
'reset-filters': []
|
|
}>()
|
|
|
|
const searchInputEl = ref<HTMLInputElement | null>(null)
|
|
</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);
|
|
}
|
|
</style>
|