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();
|
} = $props();
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
|
let year = $state<string>("");
|
||||||
const normalizedQuery = $derived(query.trim().toLowerCase());
|
const normalizedQuery = $derived(query.trim().toLowerCase());
|
||||||
const isSearching = $derived(normalizedQuery.length >= 2);
|
const isFiltering = $derived(normalizedQuery.length >= 2 || year !== "");
|
||||||
|
|
||||||
const searchResults = $derived.by<SearchIndexEntry[]>(() => {
|
function yearOf(entry: SearchIndexEntry): string {
|
||||||
if (!isSearching || !searchIndex) return [];
|
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;
|
const q = normalizedQuery;
|
||||||
return searchIndex
|
return searchIndex
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
|
if (year && yearOf(p) !== year) return false;
|
||||||
|
if (q) {
|
||||||
const hay = `${p.headline} ${p.excerpt} ${p.subheadline} ${p.tags.join(" ")}`.toLowerCase();
|
const hay = `${p.headline} ${p.excerpt} ${p.subheadline} ${p.tags.join(" ")}`.toLowerCase();
|
||||||
return hay.includes(q);
|
if (!hay.includes(q)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
})
|
})
|
||||||
.slice(0, 40);
|
.slice(0, 80);
|
||||||
});
|
});
|
||||||
|
|
||||||
function searchResultHref(slug: string): string {
|
function searchResultHref(slug: string): string {
|
||||||
@@ -95,7 +117,7 @@
|
|||||||
|
|
||||||
<div class="blog-overview">
|
<div class="blog-overview">
|
||||||
{#if searchIndex}
|
{#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">
|
<label class="relative block w-full sm:max-w-sm">
|
||||||
<span class="sr-only">Suche</span>
|
<span class="sr-only">Suche</span>
|
||||||
<span class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-zinc-500">
|
<span class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-zinc-500">
|
||||||
@@ -118,10 +140,30 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</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>
|
</div>
|
||||||
{/if}
|
{/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">
|
<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">
|
<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" />
|
<Ico icon="mdi:calendar-clock" class="h-4 w-4" />
|
||||||
@@ -142,13 +184,23 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isSearching}
|
{#if isFiltering}
|
||||||
<div class="mb-3 text-xs text-zinc-600">
|
<div class="mb-3 flex flex-wrap items-center gap-2 text-xs text-zinc-600">
|
||||||
{searchResults.length} Treffer für „{query}"
|
<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>
|
</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">
|
<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">
|
<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}
|
{#if hit.image}
|
||||||
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">
|
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">
|
||||||
|
|||||||
Reference in New Issue
Block a user