3 Commits

Author SHA1 Message Date
hsiegeln
0a97ea2fea fix(wishlist): Card stacked auf Mobile, Titel-Overflow behoben
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m15s
Auf schmalen Viewports (~390px) ueberlagerten die drei Action-Buttons
den Titel: .text reservierte 170px padding-right, aber nach 96px Bild
+ Gaps blieb kaum Platz fuer den Titel — lange Woerter wie
"Spaetzle-Pfanne" liefen hinter die Buttons.

Fix: @media (max-width: 600px) — Card wird flex-direction:column,
Actions-Row rutscht aus position:absolute in eine statische Reihe mit
border-top unter dem Body, full-width. Zusaetzlich overflow-wrap +
word-break als Safety-Net gegen bindestrich-gefuellte Monstertitel.

Desktop-Layout unveraendert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:39:12 +02:00
hsiegeln
12f499cb98 fix(home): $effect-Loop bei sort=viewed via untrack
Der Profile-Switch-Refetch-Effect las allLoading in der sync tracking-
Phase. Der await fetch beendete die Sync-Phase, das finale
allLoading = false im finally lief ausserhalb → wurde als externer
Write interpretiert → Effect rerun → naechster Fetch → Endlosschleife.

2136 GETs auf /api/recipes/all?sort=viewed in 8s beobachtet.

Fix: nur profileStore.active bleibt tracked (der tatsaechliche
Trigger). allSort/allLoading werden in untrack() gelesen — die Writes
auf allLoading im finally triggern damit keinen Effect-Rerun mehr.

Verifiziert lokal: 1 Request statt 2000+ bei mount mit allSort=viewed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:39:02 +02:00
hsiegeln
829850aa88 chore: bump package.json + package-lock auf 1.4.0
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 3m21s
Tag v1.4.0 ist schon gesetzt (auf 2b0bd4d), der synchrone Version-Bump
in package.json und package-lock.json wurde dabei vergessen. Mit dem
Pattern aus vorigen Releases (v1.2.0/v1.3.0) wieder konsistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:58:45 +02:00
4 changed files with 51 additions and 24 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "kochwas", "name": "kochwas",
"version": "1.3.0", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kochwas", "name": "kochwas",
"version": "1.3.0", "version": "1.4.0",
"dependencies": { "dependencies": {
"@google/generative-ai": "^0.24.1", "@google/generative-ai": "^0.24.1",
"@types/archiver": "^7.0.0", "@types/archiver": "^7.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "kochwas", "name": "kochwas",
"version": "1.3.0", "version": "1.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from 'svelte'; import { onMount, tick, untrack } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { CookingPot, X, ChevronDown } from 'lucide-svelte'; import { CookingPot, X, ChevronDown } from 'lucide-svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
@@ -235,27 +235,31 @@
// profiles, refetch with the new profile_id so the list reflects what // profiles, refetch with the new profile_id so the list reflects what
// the *current* profile has viewed. Other sorts are profile-agnostic // the *current* profile has viewed. Other sorts are profile-agnostic
// and don't need this. // and don't need this.
//
// Only `profileStore.active` must be a tracked dep. `allSort` /
// `allLoading` are read inside untrack: otherwise the `allLoading = false`
// write in the fetch-finally would re-trigger the effect and start the
// next fetch → endless loop.
$effect(() => { $effect(() => {
const active = profileStore.active; // eslint-disable-next-line @typescript-eslint/no-unused-expressions
if (allSort !== 'viewed') return; profileStore.active;
if (allLoading) return; untrack(() => {
// Re-fetch the first page; rehydrate would re-load the previous if (allSort !== 'viewed') return;
// depth, but a sort-context change should reset to page 1 anyway. if (allLoading) return;
void (async () => { void (async () => {
allLoading = true; allLoading = true;
try { try {
const res = await fetch(buildAllUrl('viewed', ALL_PAGE, 0)); const res = await fetch(buildAllUrl('viewed', ALL_PAGE, 0));
if (!res.ok) return; if (!res.ok) return;
const body = await res.json(); const body = await res.json();
const hits = body.hits as SearchHit[]; const hits = body.hits as SearchHit[];
allRecipes = hits; allRecipes = hits;
allExhausted = hits.length < ALL_PAGE; allExhausted = hits.length < ALL_PAGE;
} finally { } finally {
allLoading = false; allLoading = false;
} }
// 'active' is referenced so $effect tracks it as a dep: })();
void active; });
})();
}); });
// Sync current query back into the URL as ?q=... via replaceState, // Sync current query back into the URL as ?q=... via replaceState,

View File

@@ -284,6 +284,8 @@
font-weight: 600; font-weight: 600;
font-size: 1rem; font-size: 1rem;
line-height: 1.3; line-height: 1.3;
overflow-wrap: break-word;
word-break: break-word;
} }
.meta { .meta {
display: flex; display: flex;
@@ -340,4 +342,25 @@
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
} }
/* Handy: Card stacked — Bild+Titel oben, Actions als eigene Reihe
darunter full-width. Vermeidet Titel-Overflow hinter den Buttons auf
schmalen Viewports (≤~414px), gibt Tap-Targets mehr Platz. */
@media (max-width: 600px) {
.card {
flex-direction: column;
}
.text {
padding: 0.7rem 0.75rem;
}
.actions-top {
position: static;
display: flex;
gap: 0.4rem;
padding: 0.5rem 0.75rem;
border-top: 1px solid #e4eae7;
justify-content: flex-end;
background: #fafbfa;
}
}
</style> </style>