From 7cac02de5af30c55d2ed2b98a28e7ff924933265 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:57:17 +0200 Subject: [PATCH] feat(ui): Favoriten-Liste, Dismiss-from-Recent, Inline-Rename, Lucide-Icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homepage: - Neue Sektion "Deine Favoriten" über "Zuletzt hinzugefügt" (alphabetisch sortiert, lädt wenn Profil aktiv ist; versteckt sonst) - Jede Karte in "Zuletzt hinzugefügt" hat jetzt oben-rechts ein X-Icon zum Ausblenden. Das Rezept selbst bleibt in der DB — nur die Anzeige in der Recent-Liste wird per recipe.hidden_from_recent = 1 unterdrückt. Section versteckt sich, wenn die Liste leer wird. DB: - Neue Migration 004_recipe_hidden_from_recent.sql (+Index) - listFavoritesForProfile in search-local.ts (ORDER BY title NOCASE) - setRecipeHiddenFromRecent in actions.ts API: - GET /api/recipes/favorites?profile_id=X - PATCH /api/recipes/[id] akzeptiert jetzt title und/oder hidden_from_recent (Zod-Schema mit refine) Rezept-Detail: - Titel ist jetzt inline editierbar: kleines Stift-Icon rechts neben H1. Click öffnet Input, Enter speichert (PATCH), Escape bricht ab. Kein location.reload() mehr. - RecipeView bekommt neuen Snippet-Prop titleSlot für Title-Override. - Neue Aktionsreihenfolge: Zeile 1: Favorit | Wunschliste | Drucken Zeile 2: Heute gekocht | Löschen (Umbenennen ist jetzt am Titel statt in der Leiste.) Icons (lucide-svelte, neues Dep): - Emoji-Icons durch Lucide-SVGs ersetzt auf Startseite, Header, Rezept-Detail, Wunschliste, Header-Dropdown: 🍽️→Heart/Utensils, ⚙️→Settings, 🥘→CookingPot, 🌐→Globe, ♥/♡→Heart(filled), 🖨→Printer, ✎→Pencil, 🗑→Trash2, ✓→Check, 🍳→ChefHat, X→X - Header-Brand-Badge auf Mobile behält sein 🍳 (ist im ::after-Pseudo, Lucide käme da nicht sauber rein). - SearchLoader-Emojis bleiben — die sind Teil der Animations-Charme. Tests: 99/99 grün (bestehend), Typecheck 0 Fehler. --- package-lock.json | 30 +-- package.json | 1 + src/lib/components/RecipeView.svelte | 9 +- .../004_recipe_hidden_from_recent.sql | 6 + src/lib/server/recipes/actions.ts | 11 ++ src/lib/server/recipes/search-local.ts | 22 +++ src/routes/+layout.svelte | 22 ++- src/routes/+page.svelte | 172 +++++++++++++--- src/routes/api/recipes/[id]/+server.ts | 22 ++- src/routes/api/recipes/favorites/+server.ts | 14 ++ src/routes/recipes/[id]/+page.svelte | 187 ++++++++++++++++-- src/routes/wishlist/+page.svelte | 11 +- 12 files changed, 420 insertions(+), 87 deletions(-) create mode 100644 src/lib/server/db/migrations/004_recipe_hidden_from_recent.sql create mode 100644 src/routes/api/recipes/favorites/+server.ts diff --git a/package-lock.json b/package-lock.json index a020d72..7d35e99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "archiver": "^7.0.1", "better-sqlite3": "^11.5.0", "linkedom": "^0.18.5", + "lucide-svelte": "^1.0.1", "yauzl": "^3.3.0", "zod": "^3.23.8" }, @@ -443,7 +444,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -454,7 +454,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -465,7 +464,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -475,14 +473,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -963,7 +959,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1097,7 +1092,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1129,7 +1123,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT" }, "node_modules/@types/yauzl": { @@ -1280,7 +1273,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1445,7 +1437,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1471,7 +1462,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1750,7 +1740,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2041,7 +2030,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", - "dev": true, "license": "MIT" }, "node_modules/dom-serializer": { @@ -2192,14 +2180,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -2619,7 +2605,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/lodash": { @@ -2641,11 +2626,19 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/lucide-svelte": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-1.0.1.tgz", + "integrity": "sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -3434,7 +3427,6 @@ "version": "5.55.4", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz", "integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -3486,7 +3478,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -3960,7 +3951,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, "license": "MIT" }, "node_modules/zip-stream": { diff --git a/package.json b/package.json index 1e28cdc..5c2dd7f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "archiver": "^7.0.1", "better-sqlite3": "^11.5.0", "linkedom": "^0.18.5", + "lucide-svelte": "^1.0.1", "yauzl": "^3.3.0", "zod": "^3.23.8" } diff --git a/src/lib/components/RecipeView.svelte b/src/lib/components/RecipeView.svelte index 70de7fd..56384c5 100644 --- a/src/lib/components/RecipeView.svelte +++ b/src/lib/components/RecipeView.svelte @@ -6,9 +6,10 @@ recipe: Recipe; showActions?: import('svelte').Snippet; banner?: import('svelte').Snippet; + titleSlot?: import('svelte').Snippet; }; - let { recipe, showActions, banner }: Props = $props(); + let { recipe, showActions, banner, titleSlot }: Props = $props(); const defaultServings = $derived(recipe.servings_default ?? 4); let servingsOverride = $state(null); @@ -61,7 +62,11 @@ {/if}
-

{recipe.title}

+ {#if titleSlot} + {@render titleSlot()} + {:else} +

{recipe.title}

+ {/if} {#if recipe.description}

{recipe.description}

{/if} diff --git a/src/lib/server/db/migrations/004_recipe_hidden_from_recent.sql b/src/lib/server/db/migrations/004_recipe_hidden_from_recent.sql new file mode 100644 index 0000000..7d96cb1 --- /dev/null +++ b/src/lib/server/db/migrations/004_recipe_hidden_from_recent.sql @@ -0,0 +1,6 @@ +-- Let the user dismiss individual recipes from the "Zuletzt hinzugefügt" +-- list on the homepage. The recipe itself stays searchable and fully +-- functional — only its appearance in the "recent" list is suppressed. +ALTER TABLE recipe ADD COLUMN hidden_from_recent INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX idx_recipe_hidden_from_recent ON recipe(hidden_from_recent, created_at); diff --git a/src/lib/server/recipes/actions.ts b/src/lib/server/recipes/actions.ts index 6fcaf3c..7767f17 100644 --- a/src/lib/server/recipes/actions.ts +++ b/src/lib/server/recipes/actions.ts @@ -150,3 +150,14 @@ export function renameRecipe( recipeId ); } + +export function setRecipeHiddenFromRecent( + db: Database.Database, + recipeId: number, + hidden: boolean +): void { + db.prepare('UPDATE recipe SET hidden_from_recent = ? WHERE id = ?').run( + hidden ? 1 : 0, + recipeId + ); +} diff --git a/src/lib/server/recipes/search-local.ts b/src/lib/server/recipes/search-local.ts index 0868966..b158554 100644 --- a/src/lib/server/recipes/search-local.ts +++ b/src/lib/server/recipes/search-local.ts @@ -67,8 +67,30 @@ export function listRecentRecipes( (SELECT AVG(stars) FROM rating WHERE recipe_id = r.id) AS avg_stars, (SELECT MAX(cooked_at) FROM cooking_log WHERE recipe_id = r.id) AS last_cooked_at FROM recipe r + WHERE r.hidden_from_recent = 0 ORDER BY r.created_at DESC LIMIT ?` ) .all(limit) as SearchHit[]; } + +export function listFavoritesForProfile( + db: Database.Database, + profileId: number +): SearchHit[] { + return db + .prepare( + `SELECT r.id, + r.title, + r.description, + r.image_path, + r.source_domain, + (SELECT AVG(stars) FROM rating WHERE recipe_id = r.id) AS avg_stars, + (SELECT MAX(cooked_at) FROM cooking_log WHERE recipe_id = r.id) AS last_cooked_at + FROM recipe r + JOIN favorite f ON f.recipe_id = r.id + WHERE f.profile_id = ? + ORDER BY r.title COLLATE NOCASE` + ) + .all(profileId) as SearchHit[]; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 00665aa..e38e525 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { page } from '$app/stores'; import { goto, afterNavigate } from '$app/navigation'; + import { Heart, Settings, CookingPot, Globe, Utensils } from 'lucide-svelte'; import { profileStore } from '$lib/client/profile.svelte'; import ProfileSwitcher from '$lib/components/ProfileSwitcher.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; @@ -151,7 +152,7 @@ {#if r.image_path} {:else} -
🥘
+
{/if}
{r.title}
@@ -168,7 +169,8 @@ href={`/search/web?q=${encodeURIComponent(navQuery.trim())}`} onclick={pickHit} > - 🌐 Im Internet weitersuchen + + Im Internet weitersuchen {:else}

Keine lokalen Rezepte – aus dem Internet:

@@ -190,7 +192,7 @@ {#if w.thumbnail} {:else} -
🍽️
+
{/if}
{w.title}
@@ -209,8 +211,12 @@
{/if}
@@ -363,9 +369,11 @@ letter-spacing: 0.03em; } .dd-web { - display: block; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; padding: 0.75rem 0.85rem; - text-align: center; border-top: 1px solid #e4eae7; text-decoration: none; color: #2b6a3d; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 71d8553..3f55ce2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,13 +1,16 @@ @@ -112,7 +149,7 @@ {#if r.image_path} {:else} -
🥘
+
{/if}
{r.title}
@@ -125,10 +162,12 @@ {/each} - 🌐 Im Internet weitersuchen + Im Internet weitersuchen {:else if searchedFor === query.trim()} -

Keine lokalen Rezepte für „{searchedFor}" — Ergebnisse aus dem Internet:

+

+ Keine lokalen Rezepte für „{searchedFor}" — Ergebnisse aus dem Internet: +

{#if webSearching} {:else if webError} @@ -141,7 +180,7 @@ {#if w.thumbnail} {:else} -
🍽️
+
{/if}
{w.title}
@@ -156,29 +195,62 @@ {/if} {/if} -{:else if recent.length > 0} -
-

Zuletzt hinzugefügt

- -
+
+
{r.title}
+ {#if r.source_domain} +
{r.source_domain}
+ {/if} +
+ + + {/each} + + + {/if} + {#if recent.length > 0} +
+

Zuletzt hinzugefügt

+ +
+ {/if} {/if}