feat(posts): filter by year next to search
Deploy to Firebase Hosting / deploy (push) Has been cancelled
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user