feat(home): Collapsible Sections fuer Favoriten + Zuletzt hinzugefuegt
Header als <button> mit Chevron + Count-Pill, slide-Transition (180ms).
State in localStorage unter kochwas.collapsed.sections — JSON-Map
{favorites, recent}, default beide offen, corrupt-JSON faellt auf
Default zurueck.
Alle Rezepte bleibt absichtlich nicht-collapsibel — Hauptliste, immer
sichtbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { CookingPot, X } from 'lucide-svelte';
|
||||
import { CookingPot, X, ChevronDown } from 'lucide-svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { Snapshot } from './$types';
|
||||
import type { SearchHit } from '$lib/server/recipes/search-local';
|
||||
import { randomQuote } from '$lib/quotes';
|
||||
@@ -49,6 +50,20 @@
|
||||
let allChips: HTMLElement | undefined = $state();
|
||||
let allObserver: IntersectionObserver | null = null;
|
||||
|
||||
type CollapseKey = 'favorites' | 'recent';
|
||||
const COLLAPSE_STORAGE_KEY = 'kochwas.collapsed.sections';
|
||||
let collapsed = $state<Record<CollapseKey, boolean>>({
|
||||
favorites: false,
|
||||
recent: false
|
||||
});
|
||||
|
||||
function toggleCollapsed(key: CollapseKey) {
|
||||
collapsed[key] = !collapsed[key];
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot persists across history navigation. We capture not only the
|
||||
// search store, but also the pagination depth ("user had loaded 60
|
||||
// recipes via infinite scroll") so on back-nav we can re-hydrate the
|
||||
@@ -175,6 +190,16 @@
|
||||
// this; if loadAllMore lands first, rehydrateAll's larger result
|
||||
// simply overwrites allRecipes once it resolves.
|
||||
void loadAllMore();
|
||||
const rawCollapsed = localStorage.getItem(COLLAPSE_STORAGE_KEY);
|
||||
if (rawCollapsed) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawCollapsed) as Partial<Record<CollapseKey, boolean>>;
|
||||
if (typeof parsed.favorites === 'boolean') collapsed.favorites = parsed.favorites;
|
||||
if (typeof parsed.recent === 'boolean') collapsed.recent = parsed.recent;
|
||||
} catch {
|
||||
// Corrupt JSON — keep defaults (both open).
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// IntersectionObserver an den Sentinel hängen — wenn sichtbar, nachladen.
|
||||
@@ -376,7 +401,22 @@
|
||||
{:else}
|
||||
{#if profileStore.active && favorites.length > 0}
|
||||
<section class="listing">
|
||||
<button
|
||||
type="button"
|
||||
class="section-head"
|
||||
onclick={() => toggleCollapsed('favorites')}
|
||||
aria-expanded={!collapsed.favorites}
|
||||
>
|
||||
<ChevronDown
|
||||
size={18}
|
||||
strokeWidth={2.2}
|
||||
class={collapsed.favorites ? 'chev rotated' : 'chev'}
|
||||
/>
|
||||
<h2>Deine Favoriten</h2>
|
||||
<span class="count">{favorites.length}</span>
|
||||
</button>
|
||||
{#if !collapsed.favorites}
|
||||
<div transition:slide={{ duration: 180 }}>
|
||||
<ul class="cards">
|
||||
{#each favorites as r (r.id)}
|
||||
<li class="card-wrap">
|
||||
@@ -396,11 +436,28 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
{#if recent.length > 0}
|
||||
<section class="listing">
|
||||
<button
|
||||
type="button"
|
||||
class="section-head"
|
||||
onclick={() => toggleCollapsed('recent')}
|
||||
aria-expanded={!collapsed.recent}
|
||||
>
|
||||
<ChevronDown
|
||||
size={18}
|
||||
strokeWidth={2.2}
|
||||
class={collapsed.recent ? 'chev rotated' : 'chev'}
|
||||
/>
|
||||
<h2>Zuletzt hinzugefügt</h2>
|
||||
<span class="count">{recent.length}</span>
|
||||
</button>
|
||||
{#if !collapsed.recent}
|
||||
<div transition:slide={{ duration: 180 }}>
|
||||
<ul class="cards">
|
||||
{#each recent as r (r.id)}
|
||||
<li class="card-wrap">
|
||||
@@ -427,6 +484,8 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
<section class="listing">
|
||||
@@ -546,6 +605,45 @@
|
||||
color: #444;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.25rem;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
min-height: 44px;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.section-head:hover {
|
||||
background: #f4f8f5;
|
||||
}
|
||||
.section-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
color: #444;
|
||||
font-weight: 600;
|
||||
}
|
||||
.section-head .count {
|
||||
margin-left: auto;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
:global(.chev) {
|
||||
color: #2b6a3d;
|
||||
flex-shrink: 0;
|
||||
transition: transform 180ms;
|
||||
}
|
||||
:global(.chev.rotated) {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.listing-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user