Files
lage-chat-control/frontend/src/routes/outils/+page.svelte
T

141 lines
3.8 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { browser } from '$app/environment'
import { base } from '$app/paths'
import { page } from '$app/state'
import CategoryNav from '$lib/components/CategoryNav.svelte'
import SearchBar from '$lib/components/SearchBar.svelte'
import ToolCard from '$lib/components/ToolCard.svelte'
import EndMarker from '$lib/components/EndMarker.svelte'
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
import { t } from '$lib/i18n/index.svelte'
let { data } = $props()
let query = $state('')
const DIFFICULTIES = ['beginner', 'intermediate', 'advanced'] as const
// Chaque filtre vit dans l'URL (query param) : partageable (mission §5.1).
// Le HTML prérendu montre la liste complète ; le filtrage est une
// amélioration progressive côté client (ADR-001). Les searchParams ne
// sont pas lisibles au prerender, d'où la garde `browser`.
let cat = $derived(browser ? page.url.searchParams.get('cat') : null)
let diff = $derived(browser ? page.url.searchParams.get('diff') : null)
let filtered = $derived(
data.tools.filter(
(tool) =>
(!cat || tool.category === cat) &&
(!diff || tool.difficulty === diff) &&
(!query ||
`${tool.name} ${tool.tags.join(' ')} ${tool.excerpt}`
.toLocaleLowerCase('fr')
.includes(query.toLocaleLowerCase('fr')))
)
)
function diffHref(d: string | null): string {
const params = new URLSearchParams(browser ? page.url.searchParams : undefined)
if (cat) params.set('cat', cat)
if (d) params.set('diff', d)
else params.delete('diff')
const qs = params.toString()
return qs ? `${base}/outils?${qs}` : `${base}/outils`
}
</script>
<svelte:head>
<title>{t('directory.title')}{t('site.name')}</title>
<meta name="description" content={t('directory.intro')} />
</svelte:head>
<header class="stack">
<h1>{t('directory.title')}</h1>
<p>{t('directory.intro')}</p>
</header>
<div class="stack filters">
<SearchBar bind:value={query} />
<CategoryNav categories={data.categories} />
<nav aria-label={t('directory.filterDifficulty')}>
<ul class="cluster">
<li>
<a href={diffHref(null)} aria-current={diff === null ? 'page' : undefined}>
{t('directory.allDifficulties')}
</a>
</li>
{#each DIFFICULTIES as d (d)}
<li>
<a href={diffHref(d)} aria-current={diff === d ? 'page' : undefined}>
<DifficultyBadge difficulty={d} />
</a>
</li>
{/each}
</ul>
</nav>
</div>
<section class="stack" aria-live="polite">
<!-- h2 : préserve l'ordre des titres (h1 → h2 → h3 des cartes) -->
<h2 class="count">
{filtered.length}
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
</h2>
{#if filtered.length === 0}
<p class="empty">{t('directory.empty')}</p>
{:else}
<div class="card-grid">
<ul>
{#each filtered as tool (tool.slug)}
<li><ToolCard {tool} /></li>
{/each}
</ul>
</div>
{/if}
<EndMarker />
</section>
<style>
.filters {
--gap: var(--space-3);
}
nav ul {
list-style: none;
padding: 0;
}
nav a {
display: inline-flex;
align-items: center;
min-height: var(--tap-target);
padding-inline: var(--space-3);
border: 1px solid var(--oki-line);
border-radius: var(--radius-2);
text-decoration: none;
color: var(--oki-ink);
}
nav a[aria-current='page'] {
background: var(--oki-surface-2);
border-color: var(--oki-ink);
font-weight: 700;
}
.count {
color: var(--oki-ink-2);
font-size: var(--text-0);
font-family: var(--font-text);
font-weight: 400;
}
.empty {
padding: var(--space-5);
border: 1px dashed var(--oki-line);
border-radius: var(--radius-2);
text-align: center;
color: var(--oki-ink-2);
}
</style>