Removes the inline interface from RecipePicker.svelte and replaces any[] in +page.svelte with Suggestion[] — compile-time safety. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
5.8 KiB
Svelte
178 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import type { Recipe, Suggestion } from '$lib/planner/types';
|
|
|
|
let {
|
|
planId,
|
|
date,
|
|
dateLabel,
|
|
suggestions = [],
|
|
allRecipes = [],
|
|
isLoading = false,
|
|
onpick
|
|
}: {
|
|
planId: string;
|
|
date: string;
|
|
dateLabel: string;
|
|
suggestions: Suggestion[];
|
|
allRecipes: Recipe[];
|
|
isLoading?: boolean;
|
|
onpick: (recipeId: string, recipeName: string) => void;
|
|
} = $props();
|
|
|
|
let searchQuery = $state('');
|
|
|
|
let filteredRecipes = $derived(
|
|
searchQuery.trim() === ''
|
|
? allRecipes
|
|
: allRecipes.filter((r) =>
|
|
r.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
);
|
|
|
|
function recipeMetadata(recipe: Recipe): string {
|
|
return [
|
|
recipe.cookTimeMin != null ? `${recipe.cookTimeMin} Min` : null,
|
|
recipe.effort ?? null
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
}
|
|
</script>
|
|
|
|
<div style="background: var(--color-page); font-family: var(--font-sans);">
|
|
<!-- Header -->
|
|
<div style="padding: 10px 12px 6px; border-bottom: 1px solid var(--color-border);">
|
|
<p style="font-family: var(--font-display); font-size: 14px; font-weight: 500; color: var(--color-text); margin: 0;">
|
|
Rezept wählen
|
|
</p>
|
|
<p style="font-size: 11px; color: var(--color-text-muted); margin: 2px 0 0;">
|
|
{dateLabel}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div style="padding: 8px 12px; border-bottom: 1px solid var(--color-border);">
|
|
<input
|
|
type="search"
|
|
bind:value={searchQuery}
|
|
placeholder="Rezept suchen…"
|
|
style="width: 100%; box-sizing: border-box; padding: 5px 8px; font-size: 11px; font-family: var(--font-sans); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface); color: var(--color-text);"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Empfohlen section -->
|
|
{#if isLoading}
|
|
<div data-testid="suggestions-loading">
|
|
{#each [1, 2, 3] as i (i)}
|
|
<div
|
|
style="padding: 7px 12px; border-bottom: 1px solid var(--color-subtle); display: flex; align-items: center; gap: 8px;"
|
|
>
|
|
<div style="flex: 1; min-width: 0;">
|
|
<div
|
|
style="height: 12px; width: 60%; border-radius: 3px; background: var(--color-subtle); animation: pulse 1.5s ease-in-out infinite;"
|
|
></div>
|
|
<div
|
|
style="height: 9px; width: 35%; border-radius: 3px; background: var(--color-subtle); margin-top: 4px; animation: pulse 1.5s ease-in-out infinite;"
|
|
></div>
|
|
</div>
|
|
<div
|
|
style="height: 26px; width: 56px; border-radius: var(--radius-md); background: var(--color-subtle); animation: pulse 1.5s ease-in-out infinite;"
|
|
></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else if suggestions.length > 0}
|
|
<div
|
|
style="font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-muted); padding: 5px 12px 3px; background: var(--color-subtle);"
|
|
>
|
|
Empfohlen · Beste Abwechslung
|
|
</div>
|
|
|
|
{#each suggestions as suggestion (suggestion.recipe.id)}
|
|
{@const meta = recipeMetadata(suggestion.recipe)}
|
|
<div
|
|
style="padding: 7px 12px; border-bottom: 1px solid var(--color-subtle); display: flex; align-items: center; gap: 8px;"
|
|
>
|
|
<div style="flex: 1; min-width: 0;">
|
|
<p
|
|
style="font-family: var(--font-display); font-size: 12px; font-weight: 400; color: var(--color-text); margin: 0;"
|
|
>
|
|
{suggestion.recipe.name}
|
|
</p>
|
|
{#if meta}
|
|
<p style="font-size: 9px; color: var(--color-text-muted); margin: 1px 0 0;">
|
|
{meta}
|
|
</p>
|
|
{/if}
|
|
{#if !suggestion.hasConflict}
|
|
<span
|
|
data-testid="badge-{suggestion.recipe.id}"
|
|
data-type="good"
|
|
style="display: inline-block; margin-top: 3px; font-size: 8px; font-weight: 500; padding: 1px 5px; border-radius: 3px; background: var(--green-tint); color: var(--green-dark);"
|
|
>
|
|
↑ +{(suggestion.scoreDelta ?? 0).toFixed(0)} Punkte
|
|
</span>
|
|
{:else}
|
|
<span
|
|
data-testid="badge-{suggestion.recipe.id}"
|
|
data-type="warning"
|
|
style="display: inline-block; margin-top: 3px; font-size: 8px; font-weight: 500; padding: 1px 5px; border-radius: 3px; background: var(--yellow-tint); color: var(--yellow-text);"
|
|
>
|
|
⚠ Variationskonflikt
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label="Wählen"
|
|
onclick={() => onpick(suggestion.recipe.id, suggestion.recipe.name)}
|
|
style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green); color: #fff; border: none; cursor: pointer;"
|
|
>
|
|
+ Wählen
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
|
|
<!-- Alle Rezepte section -->
|
|
<div
|
|
style="font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-muted); padding: 5px 12px 3px; background: var(--color-subtle);"
|
|
>
|
|
Alle Rezepte
|
|
</div>
|
|
|
|
{#if filteredRecipes.length === 0}
|
|
<p style="padding: 10px 12px; font-size: 11px; color: var(--color-text-muted); margin: 0;">
|
|
Keine Treffer
|
|
</p>
|
|
{:else}
|
|
{#each filteredRecipes as recipe (recipe.id)}
|
|
{@const meta = recipeMetadata(recipe)}
|
|
<div
|
|
style="padding: 7px 12px; border-bottom: 1px solid var(--color-subtle); display: flex; align-items: center; gap: 8px;"
|
|
>
|
|
<div style="flex: 1; min-width: 0;">
|
|
<p
|
|
style="font-family: var(--font-display); font-size: 12px; font-weight: 400; color: var(--color-text); margin: 0;"
|
|
>
|
|
{recipe.name}
|
|
</p>
|
|
{#if meta}
|
|
<p style="font-size: 9px; color: var(--color-text-muted); margin: 1px 0 0;">
|
|
{meta}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label="Wählen"
|
|
onclick={() => onpick(recipe.id, recipe.name)}
|
|
style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green); color: #fff; border: none; cursor: pointer;"
|
|
>
|
|
+ Wählen
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|