feat(zitare): add search and sort to category detail page

Add inline search field and sort-by-author option to category pages.
Also extract hardcoded German strings and respect display settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 20:47:35 +01:00
parent 0c479b3e88
commit 40b55eb65f
3 changed files with 88 additions and 11 deletions

View file

@ -38,7 +38,12 @@
"courage": "Mut",
"hope": "Hoffnung",
"nature": "Natur",
"quotes": "{count} Zitate"
"quotes": "{count} Zitate",
"notFound": "Kategorie nicht gefunden",
"backToCategories": "Zurück zu Kategorien",
"searchInCategory": "In dieser Kategorie suchen...",
"sortByAuthor": "Nach Autor",
"sortByDefault": "Standard"
},
"favorites": {
"title": "Favoriten",

View file

@ -38,7 +38,12 @@
"courage": "Courage",
"hope": "Hope",
"nature": "Nature",
"quotes": "{count} quotes"
"quotes": "{count} quotes",
"notFound": "Category not found",
"backToCategories": "Back to categories",
"searchInCategory": "Search in this category...",
"sortByAuthor": "By author",
"sortByDefault": "Default"
},
"favorites": {
"title": "Favorites",

View file

@ -2,7 +2,9 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { getQuotesByCategory, CATEGORIES, type Category } from '@zitare/content';
import { getQuotesByCategory, CATEGORIES, type Category, type Quote } from '@zitare/content';
import { quotesStore } from '$lib/stores/quotes.svelte';
import { zitareSettings } from '$lib/stores/settings.svelte';
import QuoteCard from '$lib/components/QuoteCard.svelte';
// Get category from URL
@ -14,6 +16,31 @@
// Get quotes for this category
let quotes = $derived(isValidCategory ? getQuotesByCategory(category) : []);
// Search & sort state
let searchTerm = $state('');
let sortBy = $state<'default' | 'author'>('default');
// Filtered and sorted quotes
let displayedQuotes = $derived<Quote[]>(() => {
let filtered = quotes;
// Filter by search
if (searchTerm.length >= 2) {
const lower = searchTerm.toLowerCase();
filtered = filtered.filter(
(q) =>
quotesStore.getText(q).toLowerCase().includes(lower) ||
q.author.toLowerCase().includes(lower)
);
}
// Sort
if (sortBy === 'author') {
return [...filtered].sort((a, b) => a.author.localeCompare(b.author));
}
return filtered;
});
// Category labels
const categoryLabels: Record<Category, string> = {
weisheit: 'categories.wisdom',
@ -30,7 +57,9 @@
</script>
<svelte:head>
<title>Zitare - {isValidCategory ? $_(categoryLabels[category]) : 'Kategorie'}</title>
<title
>Zitare - {isValidCategory ? $_(categoryLabels[category]) : $_('categories.notFound')}</title
>
</svelte:head>
<div class="max-w-3xl mx-auto">
@ -47,20 +76,58 @@
{#if isValidCategory}
<h1 class="text-3xl font-bold text-foreground mb-2">{$_(categoryLabels[category])}</h1>
<p class="text-foreground-secondary mb-8">
<p class="text-foreground-secondary mb-6">
{$_('categories.quotes', { values: { count: quotes.length } })}
</p>
<div class="space-y-6">
{#each quotes as quote (quote.id)}
<QuoteCard {quote} showSource />
{/each}
<!-- Search & Sort Bar -->
<div class="flex gap-3 mb-8">
<div class="relative flex-1">
<svg
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
placeholder={$_('categories.searchInCategory')}
bind:value={searchTerm}
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-surface-elevated border border-border text-foreground text-sm focus:outline-none focus:border-primary transition-colors"
/>
</div>
<select
bind:value={sortBy}
class="px-3 py-2.5 rounded-xl bg-surface-elevated border border-border text-foreground text-sm"
>
<option value="default">{$_('categories.sortByDefault')}</option>
<option value="author">{$_('categories.sortByAuthor')}</option>
</select>
</div>
{#if displayedQuotes.length === 0 && searchTerm.length >= 2}
<div class="text-center py-12">
<p class="text-foreground-secondary">{$_('search.noResults')}</p>
</div>
{:else}
<div class="space-y-6">
{#each displayedQuotes as quote (quote.id)}
<QuoteCard {quote} showSource={zitareSettings.showSource} />
{/each}
</div>
{/if}
{:else}
<div class="text-center py-12">
<p class="text-foreground-secondary">Kategorie nicht gefunden</p>
<p class="text-foreground-secondary">{$_('categories.notFound')}</p>
<button onclick={() => goto('/categories')} class="mt-4 text-primary hover:underline">
Zurück zu Kategorien
{$_('categories.backToCategories')}
</button>
</div>
{/if}