feat(shopping): Rezept-Chips mit Portions-Stepper
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
hsiegeln
2026-04-21 23:45:32 +02:00
parent 3c30d1f35a
commit f4eac4d9c3
2 changed files with 105 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { X, Minus, Plus } from 'lucide-svelte';
import type { ShoppingCartRecipe } from '$lib/server/shopping/repository';
let { recipe, onServingsChange, onRemove }: {
recipe: ShoppingCartRecipe;
onServingsChange: (id: number, servings: number) => void;
onRemove: (id: number) => void;
} = $props();
function dec() {
if (recipe.servings > 1) onServingsChange(recipe.recipe_id, recipe.servings - 1);
}
function inc() {
if (recipe.servings < 50) onServingsChange(recipe.recipe_id, recipe.servings + 1);
}
</script>
<div class="chip">
<a class="title" href={`/recipes/${recipe.recipe_id}`}>{recipe.title}</a>
<div class="controls">
<button aria-label="Portion weniger" onclick={dec} disabled={recipe.servings <= 1}>
<Minus size={16} />
</button>
<span class="val" aria-label="Portionen">{recipe.servings}p</span>
<button aria-label="Portion mehr" onclick={inc} disabled={recipe.servings >= 50}>
<Plus size={16} />
</button>
<button aria-label="Rezept aus Einkaufsliste entfernen" class="rm" onclick={() => onRemove(recipe.recipe_id)}>
<X size={16} />
</button>
</div>
</div>
<style>
.chip {
flex: 0 0 auto;
padding: 0.5rem 0.75rem;
background: white;
border: 1px solid #cfd9d1;
border-radius: 14px;
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 140px;
}
.title {
color: #2b6a3d;
font-weight: 600;
font-size: 0.92rem;
text-decoration: none;
line-height: 1.2;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.controls { display: flex; gap: 0.25rem; align-items: center; }
.controls button {
min-width: 32px;
min-height: 32px;
border-radius: 8px;
border: 1px solid #e4eae7;
background: white;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
color: #444;
}
.controls button:disabled { opacity: 0.4; cursor: not-allowed; }
.controls button.rm { margin-left: auto; }
.controls button.rm:hover { color: #c53030; border-color: #f1b4b4; background: #fdf3f3; }
.val { min-width: 32px; text-align: center; font-weight: 600; color: #444; }
</style>

View File

@@ -3,6 +3,7 @@
import { ShoppingCart } from 'lucide-svelte';
import type { ShoppingListSnapshot } from '$lib/server/shopping/repository';
import ShoppingListRow from '$lib/components/ShoppingListRow.svelte';
import ShoppingCartChip from '$lib/components/ShoppingCartChip.svelte';
import type { ShoppingListRow as Row } from '$lib/server/shopping/repository';
import { shoppingCartStore } from '$lib/client/shopping-cart.svelte';
@@ -30,6 +31,22 @@
void shoppingCartStore.refresh();
}
async function onServingsChange(recipeId: number, servings: number) {
await fetch(`/api/shopping-list/recipe/${recipeId}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ servings })
});
await load();
void shoppingCartStore.refresh();
}
async function onRemoveRecipe(recipeId: number) {
await fetch(`/api/shopping-list/recipe/${recipeId}`, { method: 'DELETE' });
await load();
void shoppingCartStore.refresh();
}
onMount(load);
</script>
@@ -51,6 +68,11 @@
<p class="hint">Lege Rezepte auf der Wunschliste in den Wagen, um sie hier zu sehen.</p>
</section>
{:else}
<div class="chips">
{#each snapshot.recipes as r (r.recipe_id)}
<ShoppingCartChip recipe={r} {onServingsChange} onRemove={onRemoveRecipe} />
{/each}
</div>
<ul class="list">
{#each snapshot.rows as row (row.name_key + '|' + row.unit_key)}
<li>
@@ -76,4 +98,12 @@
flex-direction: column;
gap: 0.5rem;
}
.chips {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 0.5rem 0;
margin: 0.5rem 0;
-webkit-overflow-scrolling: touch;
}
</style>