fix(recipes): address review feedback — shared type, design system tokens, test coverage
- Extract RecipeSummary type to $lib/recipes/types.ts (was duplicated in 3 files) - Fix +page.svelte header link: replace Skeleton UI classes with design system tokens - Fix h1: use font-[var(--font-display)] and correct size - Fix FilterChipRow: text-[11px] → text-[13px] + tracking-[0.04em] per design system - Fix RecipeCard metadata: text-[11px] → text-[12px] for readability - Remove unused imports (vi, beforeEach, afterEach) from page.test.ts - Add combined search + effort filter test - Add reset-to-Alle filter test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
aria-pressed={activeFilter === label}
|
aria-pressed={activeFilter === label}
|
||||||
onclick={() => onFilter(label)}
|
onclick={() => onFilter(label)}
|
||||||
class="font-sans text-[11px] font-medium px-[14px] py-[5px] rounded-[12px] border cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--green-light)] {activeFilter === label
|
class="font-sans text-[13px] font-medium tracking-[0.04em] px-[14px] py-[5px] rounded-[12px] border cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--green-light)] {activeFilter === label
|
||||||
? 'bg-[var(--green-tint)] text-[var(--green-dark)] border-[var(--green-light)]'
|
? 'bg-[var(--green-tint)] text-[var(--green-dark)] border-[var(--green-light)]'
|
||||||
: 'bg-[var(--color-surface)] text-[var(--color-text-muted)] border-[var(--color-border)]'}"
|
: 'bg-[var(--color-surface)] text-[var(--color-text-muted)] border-[var(--color-border)]'}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type RecipeSummary = {
|
import type { RecipeSummary } from './types';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
cookTimeMin?: number;
|
|
||||||
effort?: string;
|
|
||||||
heroImageUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let { recipe, compact = false }: { recipe: RecipeSummary; compact?: boolean } = $props();
|
let { recipe, compact = false }: { recipe: RecipeSummary; compact?: boolean } = $props();
|
||||||
|
|
||||||
@@ -60,7 +54,7 @@
|
|||||||
<div class="px-2 py-1.5">
|
<div class="px-2 py-1.5">
|
||||||
<p class="font-medium text-[13px] text-[var(--color-text)] truncate">{recipe.name}</p>
|
<p class="font-medium text-[13px] text-[var(--color-text)] truncate">{recipe.name}</p>
|
||||||
{#if metadata}
|
{#if metadata}
|
||||||
<p class="text-[11px] text-[var(--color-text-muted)]">{metadata}</p>
|
<p class="text-[12px] text-[var(--color-text-muted)]">{metadata}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RecipeCard from './RecipeCard.svelte';
|
import RecipeCard from './RecipeCard.svelte';
|
||||||
|
import type { RecipeSummary } from './types';
|
||||||
type RecipeSummary = { id: string; name: string; cookTimeMin?: number; effort?: string; heroImageUrl?: string };
|
|
||||||
|
|
||||||
let { recipes }: { recipes: RecipeSummary[] } = $props();
|
let { recipes }: { recipes: RecipeSummary[] } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
7
frontend/src/lib/recipes/types.ts
Normal file
7
frontend/src/lib/recipes/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type RecipeSummary = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
cookTimeMin?: number;
|
||||||
|
effort?: string;
|
||||||
|
heroImageUrl?: string;
|
||||||
|
};
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FilterChipRow from '$lib/recipes/FilterChipRow.svelte';
|
import FilterChipRow from '$lib/recipes/FilterChipRow.svelte';
|
||||||
import RecipeGrid from '$lib/recipes/RecipeGrid.svelte';
|
import RecipeGrid from '$lib/recipes/RecipeGrid.svelte';
|
||||||
|
import type { RecipeSummary } from '$lib/recipes/types';
|
||||||
type RecipeSummary = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
cookTimeMin?: number;
|
|
||||||
effort?: string;
|
|
||||||
heroImageUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let { data }: { data: { recipes: RecipeSummary[] } } = $props();
|
let { data }: { data: { recipes: RecipeSummary[] } } = $props();
|
||||||
|
|
||||||
@@ -37,8 +30,8 @@
|
|||||||
|
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-2xl font-medium">Rezepte</h1>
|
<h1 class="font-[var(--font-display)] text-[28px] font-medium text-[var(--color-text)]">Rezepte</h1>
|
||||||
<a href="/recipes/new" class="btn variant-filled-primary">Rezept hinzufügen</a>
|
<a href="/recipes/new" class="font-sans text-[13px] font-medium tracking-[0.04em] rounded-[var(--radius-md)] bg-[var(--green-dark)] px-[24px] py-[12px] text-white">Rezept hinzufügen</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/svelte';
|
import { render, screen } from '@testing-library/svelte';
|
||||||
import { userEvent } from '@testing-library/user-event';
|
import { userEvent } from '@testing-library/user-event';
|
||||||
import Page from './+page.svelte';
|
import Page from './+page.svelte';
|
||||||
@@ -71,4 +71,29 @@ describe('recipe library page', () => {
|
|||||||
render(Page, { props: { data: { recipes: [] } } });
|
render(Page, { props: { data: { recipes: [] } } });
|
||||||
expect(screen.getByText(/keine rezepte/i)).toBeInTheDocument();
|
expect(screen.getByText(/keine rezepte/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies search and effort filter together', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(Page, { props: { data: mockData } });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Leicht' }));
|
||||||
|
const searchInput = screen.getByPlaceholderText(/suchen/i);
|
||||||
|
await user.type(searchInput, 'Gemüse');
|
||||||
|
|
||||||
|
expect(screen.getByText('Gemüsesuppe')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Spaghetti Bolognese')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Chicken Curry')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets to all recipes when Alle chip is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(Page, { props: { data: mockData } });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Mittel' }));
|
||||||
|
expect(screen.queryByText('Spaghetti Bolognese')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Alle' }));
|
||||||
|
expect(screen.getByText('Spaghetti Bolognese')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Chicken Curry')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user