feat(shopping): build main +page.svelte with responsive layout and empty states

Mobile/desktop responsive shopping list page with:
- Three empty states (no plan, no list, all checked)
- Unchecked/checked item sections with divider
- Add custom item form
- Desktop right panel with recipe references
- Filtered staples info

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 18:56:42 +02:00
parent 6cc79836d5
commit 741141168b

View File

@@ -1 +1,230 @@
<h1 class="text-2xl font-medium p-6">Einkaufsliste</h1>
<script lang="ts">
import ShoppingHeader from '$lib/shopping/ShoppingHeader.svelte';
import ChecklistItem from '$lib/shopping/ChecklistItem.svelte';
import AddCustomItem from '$lib/shopping/AddCustomItem.svelte';
import RecipeReferencePanel from '$lib/shopping/RecipeReferencePanel.svelte';
let { data } = $props();
let shoppingList = $derived(data.shoppingList);
let weekPlan = $derived(data.weekPlan);
let isPlanner = $derived(data.benutzer?.rolle === 'planer');
let items = $derived(shoppingList?.items ?? []);
let uncheckedItems = $derived(items.filter((i) => !i.isChecked));
let checkedItems = $derived(items.filter((i) => i.isChecked));
let totalItems = $derived(items.length);
let checkedCount = $derived(checkedItems.length);
let slots = $derived(weekPlan?.slots ?? []);
let filteredStaplesCount = $derived(shoppingList?.filteredStaplesCount ?? 0);
let listId = $derived(shoppingList?.id ?? '');
</script>
<!-- Mobile layout -->
<div class="flex h-full flex-col lg:hidden">
<header class="sticky top-0 z-10 border-b border-[var(--color-border)] bg-[var(--color-page)] px-4 py-3">
<ShoppingHeader
{totalItems}
{checkedCount}
generatedAt={shoppingList?.generatedAt ?? null}
weekPlanId={weekPlan?.id ?? null}
{isPlanner}
hasShoppingList={!!shoppingList}
/>
</header>
<main class="flex-1 overflow-y-auto px-4 py-3">
{#if !weekPlan}
<!-- Empty state: no week plan -->
<div class="flex flex-col items-center justify-center py-16 text-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Noch kein Wochenplan für diese Woche.
</p>
<a
href="/planner"
class="mt-3 font-[var(--font-sans)] text-[13px] font-medium text-[var(--green-dark)] hover:underline"
>
Zum Wochenplaner
</a>
</div>
{:else if !shoppingList}
<!-- Empty state: plan exists, no shopping list -->
<div class="flex flex-col items-center justify-center py-16 text-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Einkaufsliste noch nicht erstellt.
</p>
{#if isPlanner}
<p class="mt-1 font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
Generiere die Liste aus dem Wochenplan.
</p>
{:else}
<p class="mt-1 font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
Der Planer muss die Liste zuerst erstellen.
</p>
{/if}
</div>
{:else}
<!-- Unchecked items -->
{#if uncheckedItems.length > 0}
<div class="divide-y divide-[var(--color-border)]">
{#each uncheckedItems as item (item.id)}
<ChecklistItem
{listId}
itemId={item.id ?? ''}
name={item.name ?? ''}
quantity={item.quantity ?? null}
unit={item.unit ?? null}
isChecked={false}
sourceRecipes={item.sourceRecipes ?? []}
/>
{/each}
</div>
{:else if totalItems > 0}
<p class="py-4 text-center font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Alles erledigt!
</p>
{/if}
<!-- Add custom item -->
<div class="mt-3">
<AddCustomItem {listId} />
</div>
<!-- Filtered staples info (mobile) -->
{#if filteredStaplesCount > 0}
<div class="mt-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2">
<p class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
{filteredStaplesCount} Grundzutaten ausgeblendet ·
<a href="/pantry" class="font-medium text-[var(--green-dark)] hover:underline">Vorrat bearbeiten</a>
</p>
</div>
{/if}
<!-- Checked items -->
{#if checkedItems.length > 0}
<div class="mt-4">
<p class="mb-1 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Abgehakt ({checkedCount})
</p>
<div class="divide-y divide-[var(--color-border)]">
{#each checkedItems as item (item.id)}
<ChecklistItem
{listId}
itemId={item.id ?? ''}
name={item.name ?? ''}
quantity={item.quantity ?? null}
unit={item.unit ?? null}
isChecked={true}
sourceRecipes={item.sourceRecipes ?? []}
/>
{/each}
</div>
</div>
{/if}
{/if}
</main>
</div>
<!-- Desktop layout -->
<div class="hidden h-screen lg:flex lg:flex-col">
<header class="border-b border-[var(--color-border)] bg-[var(--color-page)] px-6 py-4">
<ShoppingHeader
{totalItems}
{checkedCount}
generatedAt={shoppingList?.generatedAt ?? null}
weekPlanId={weekPlan?.id ?? null}
{isPlanner}
hasShoppingList={!!shoppingList}
/>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Left panel: checklist -->
<main class="flex-1 overflow-y-auto px-6 py-5">
{#if !weekPlan}
<div class="flex h-full flex-col items-center justify-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Noch kein Wochenplan für diese Woche.
</p>
<a
href="/planner"
class="mt-3 font-[var(--font-sans)] text-[13px] font-medium text-[var(--green-dark)] hover:underline"
>
Zum Wochenplaner
</a>
</div>
{:else if !shoppingList}
<div class="flex h-full flex-col items-center justify-center">
<p class="font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Einkaufsliste noch nicht erstellt.
</p>
{#if isPlanner}
<p class="mt-1 font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
Generiere die Liste aus dem Wochenplan.
</p>
{:else}
<p class="mt-1 font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
Der Planer muss die Liste zuerst erstellen.
</p>
{/if}
</div>
{:else}
<!-- Unchecked items -->
{#if uncheckedItems.length > 0}
<div class="divide-y divide-[var(--color-border)]">
{#each uncheckedItems as item (item.id)}
<ChecklistItem
{listId}
itemId={item.id ?? ''}
name={item.name ?? ''}
quantity={item.quantity ?? null}
unit={item.unit ?? null}
isChecked={false}
sourceRecipes={item.sourceRecipes ?? []}
/>
{/each}
</div>
{:else if totalItems > 0}
<p class="py-4 text-center font-[var(--font-sans)] text-[14px] text-[var(--color-text-muted)]">
Alles erledigt!
</p>
{/if}
<!-- Add custom item -->
<div class="mt-4">
<AddCustomItem {listId} />
</div>
<!-- Checked items -->
{#if checkedItems.length > 0}
<div class="mt-6 border-t border-[var(--color-border)] pt-4">
<p class="mb-1 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Abgehakt ({checkedCount})
</p>
<div class="divide-y divide-[var(--color-border)]">
{#each checkedItems as item (item.id)}
<ChecklistItem
{listId}
itemId={item.id ?? ''}
name={item.name ?? ''}
quantity={item.quantity ?? null}
unit={item.unit ?? null}
isChecked={true}
sourceRecipes={item.sourceRecipes ?? []}
/>
{/each}
</div>
</div>
{/if}
{/if}
</main>
<!-- Right panel: recipe reference (desktop only) -->
{#if weekPlan}
<aside class="w-[280px] flex-shrink-0 overflow-y-auto border-l border-[var(--color-border)] bg-[var(--color-surface)] p-5">
<RecipeReferencePanel {slots} {filteredStaplesCount} />
</aside>
{/if}
</div>
</div>