feat(staples): add staples page with onboarding and settings layouts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,44 @@
|
||||
<title>Vorräte einrichten — Mealplan</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-[var(--color-page)]">
|
||||
<p class="text-[var(--color-text-muted)]">A3 — Vorräte einrichten (coming soon)</p>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import ProgressSidebar from '$lib/components/ProgressSidebar.svelte';
|
||||
import StaplesManager from '$lib/onboarding/StaplesManager.svelte';
|
||||
|
||||
type Category = { id: string; name: string; ingredients: { id: string; name: string; isStaple: boolean }[] };
|
||||
|
||||
let { data, ctx }: { data: { categories: Category[] }; ctx?: string } = $props();
|
||||
|
||||
const isOnboarding = $derived(ctx === 'onboarding');
|
||||
</script>
|
||||
|
||||
{#if isOnboarding}
|
||||
<div class="flex min-h-screen bg-[var(--color-page)]">
|
||||
<!-- Desktop sidebar -->
|
||||
<aside class="hidden md:flex w-[300px] flex-shrink-0 flex-col bg-[var(--color-surface)] border-r border-[var(--color-border)] p-[40px_28px]">
|
||||
<ProgressSidebar currentStep={2} />
|
||||
</aside>
|
||||
|
||||
<!-- Main area -->
|
||||
<main class="flex flex-1 flex-col">
|
||||
<!-- Mobile step indicator -->
|
||||
<p class="md:hidden px-6 pt-6 text-sm text-[var(--color-text-muted)]">Schritt 2 von 3</p>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 p-6">
|
||||
<StaplesManager categories={data.categories} context="onboarding" />
|
||||
</div>
|
||||
|
||||
<!-- Footer navigation -->
|
||||
<div class="flex justify-between p-6">
|
||||
<a href="/planner">Überspringen</a>
|
||||
<a href="/household/invite">Weiter</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen flex-col bg-[var(--color-page)]">
|
||||
<h1>Vorräte</h1>
|
||||
<StaplesManager categories={data.categories} context="settings" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
86
frontend/src/routes/household/staples/page.test.ts
Normal file
86
frontend/src/routes/household/staples/page.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import Page from './+page.svelte';
|
||||
|
||||
vi.mock('$app/state', () => ({
|
||||
page: { url: { searchParams: { get: vi.fn() } } }
|
||||
}));
|
||||
|
||||
const mockCategories = [
|
||||
{
|
||||
id: 'cat-1',
|
||||
name: 'Öle & Fette',
|
||||
ingredients: [
|
||||
{ id: 'ing-1', name: 'Olivenöl', isStaple: true },
|
||||
{ id: 'ing-2', name: 'Butter', isStaple: false }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
describe('staples page — onboarding context (?ctx=onboarding)', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('renders ProgressSidebar with step 2 active', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step');
|
||||
});
|
||||
|
||||
it('renders Continue button linking to /household/invite', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
const continueLink = screen.getByRole('link', { name: /weiter/i });
|
||||
expect(continueLink).toHaveAttribute('href', '/household/invite');
|
||||
});
|
||||
|
||||
it('renders Skip button linking to /planner', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
const skipLink = screen.getByRole('link', { name: /überspringen/i });
|
||||
expect(skipLink).toHaveAttribute('href', '/planner');
|
||||
});
|
||||
|
||||
it('renders the StaplesManager with categories', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sets the page title', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
expect(document.title).toBe('Vorräte einrichten — Mealplan');
|
||||
});
|
||||
|
||||
it('renders mobile step indicator Schritt 2 von 3', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
||||
expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('staples page — settings context (no ctx)', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('does not render ProgressSidebar', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories } } });
|
||||
expect(screen.queryByTestId('step-1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Continue or Skip buttons', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories } } });
|
||||
expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a settings heading', () => {
|
||||
render(Page, { props: { data: { categories: mockCategories } } });
|
||||
expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user