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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user