feat(posts): filter by year next to search
Deploy to Firebase Hosting / deploy (push) Has been cancelled

- Year dropdown derived from post.created, sorted desc
- Combines with search (AND); "Filter zurücksetzen" clears both
- Renamed isSearching→isFiltering to reflect combined state
This commit is contained in:
Peter Meier
2026-04-14 15:47:53 +02:00
parent c6170acdfa
commit b2296a79db
+65 -13
View File
@@ -54,18 +54,40 @@
} = $props();
let query = $state("");
let year = $state<string>("");
const normalizedQuery = $derived(query.trim().toLowerCase());
const isSearching = $derived(normalizedQuery.length >= 2);
const isFiltering = $derived(normalizedQuery.length >= 2 || year !== "");
const searchResults = $derived.by<SearchIndexEntry[]>(() => {
if (!isSearching || !searchIndex) return [];
function yearOf(entry: SearchIndexEntry): string {
const iso = entry.created ?? "";
if (!iso) return "";
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? "" : String(d.getFullYear());
}
const availableYears = $derived.by<string[]>(() => {
if (!searchIndex) return [];
const set = new Set<string>();
for (const p of searchIndex) {
const y = yearOf(p);
if (y) set.add(y);
}
return Array.from(set).sort((a, b) => b.localeCompare(a));
});
const filteredResults = $derived.by<SearchIndexEntry[]>(() => {
if (!isFiltering || !searchIndex) return [];
const q = normalizedQuery;
return searchIndex
.filter((p) => {
const hay = `${p.headline} ${p.excerpt} ${p.subheadline} ${p.tags.join(" ")}`.toLowerCase();
return hay.includes(q);
if (year && yearOf(p) !== year) return false;
if (q) {
const hay = `${p.headline} ${p.excerpt} ${p.subheadline} ${p.tags.join(" ")}`.toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
})
.slice(0, 40);
.slice(0, 80);
});
function searchResultHref(slug: string): string {
@@ -95,7 +117,7 @@
<div class="blog-overview">
{#if searchIndex}
<div class="mb-4">
<div class="mb-4 flex flex-wrap items-center gap-2">
<label class="relative block w-full sm:max-w-sm">
<span class="sr-only">Suche</span>
<span class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-zinc-500">
@@ -118,10 +140,30 @@
</button>
{/if}
</label>
{#if availableYears.length > 0}
<label class="relative inline-flex items-center">
<span class="sr-only">Jahr</span>
<span class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-zinc-500">
<Ico icon="mdi:calendar" class="h-4 w-4" />
</span>
<select
bind:value={year}
class="appearance-none rounded-sm border border-zinc-300 bg-white py-2 pr-7 pl-8 text-sm focus:border-zinc-500 focus:outline-none"
>
<option value="">Alle Jahre</option>
{#each availableYears as y}
<option value={y}>{y}</option>
{/each}
</select>
<span class="pointer-events-none absolute top-1/2 right-1.5 -translate-y-1/2 text-zinc-500">
<Ico icon="mdi:chevron-down" class="h-4 w-4" />
</span>
</label>
{/if}
</div>
{/if}
{#if !isSearching && upcomingEvents.length > 0}
{#if !isFiltering && upcomingEvents.length > 0}
<section class="mb-6 rounded-sm border border-zinc-200 bg-zinc-50 p-4">
<h2 class="mb-3 flex items-center gap-1.5 text-sm font-semibold tracking-wide text-zinc-700 uppercase">
<Ico icon="mdi:calendar-clock" class="h-4 w-4" />
@@ -142,13 +184,23 @@
</section>
{/if}
{#if isSearching}
<div class="mb-3 text-xs text-zinc-600">
{searchResults.length} Treffer für „{query}"
{#if isFiltering}
<div class="mb-3 flex flex-wrap items-center gap-2 text-xs text-zinc-600">
<span>
{filteredResults.length}
{filteredResults.length === 1 ? "Treffer" : "Treffer"}
{#if query}für „{query}"{/if}
{#if year}im Jahr {year}{/if}
</span>
<button
type="button"
onclick={() => { query = ""; year = ""; }}
class="text-zinc-500 underline hover:text-zinc-900"
>Filter zurücksetzen</button>
</div>
{#if searchResults.length > 0}
{#if filteredResults.length > 0}
<div class="py-2 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each searchResults as hit}
{#each filteredResults as hit}
<a href={searchResultHref(hit.slug)} class="flex flex-col overflow-hidden border border-gray-200 bg-white text-slate-950 no-underline transition-all">
{#if hit.image}
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">