From b90a42b1aa03361130472abe4a0d67bd61249e5d Mon Sep 17 00:00:00 2001
From: Alexander Bocken
Date: Wed, 18 Feb 2026 21:01:16 +0100
Subject: [PATCH] recipes: add shared "to try" list for external recipes
Household-shared list of external recipes to try, with name, multiple
links, and optional notes. Includes add/edit/delete with confirmation.
Linked from the favorites page via a styled pill button.
---
src/lib/components/recipes/ToTryCard.svelte | 162 +++++++++
src/models/ToTryRecipe.ts | 18 +
.../favorites/+page.svelte | 27 +-
.../to-try/+page.server.ts | 27 ++
.../to-try/+page.svelte | 327 ++++++++++++++++++
.../[recipeLang=recipeLang]/to-try/+server.ts | 120 +++++++
6 files changed, 680 insertions(+), 1 deletion(-)
create mode 100644 src/lib/components/recipes/ToTryCard.svelte
create mode 100644 src/models/ToTryRecipe.ts
create mode 100644 src/routes/[recipeLang=recipeLang]/to-try/+page.server.ts
create mode 100644 src/routes/[recipeLang=recipeLang]/to-try/+page.svelte
create mode 100644 src/routes/api/[recipeLang=recipeLang]/to-try/+server.ts
diff --git a/src/lib/components/recipes/ToTryCard.svelte b/src/lib/components/recipes/ToTryCard.svelte
new file mode 100644
index 0000000..19be491
--- /dev/null
+++ b/src/lib/components/recipes/ToTryCard.svelte
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
{item.name}
+ {#if item.links?.length}
+
+ {/if}
+ {#if item.notes}
+
{item.notes}
+ {/if}
+
+
+
diff --git a/src/models/ToTryRecipe.ts b/src/models/ToTryRecipe.ts
new file mode 100644
index 0000000..aabca8f
--- /dev/null
+++ b/src/models/ToTryRecipe.ts
@@ -0,0 +1,18 @@
+import mongoose from 'mongoose';
+
+const ToTryRecipeSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, trim: true },
+ links: [
+ {
+ url: { type: String, required: true },
+ label: { type: String, default: '' }
+ }
+ ],
+ notes: { type: String, default: '' },
+ addedBy: { type: String, required: true }
+ },
+ { timestamps: true }
+);
+
+export const ToTryRecipe = mongoose.model('ToTryRecipe', ToTryRecipeSchema);
diff --git a/src/routes/[recipeLang=recipeLang]/favorites/+page.svelte b/src/routes/[recipeLang=recipeLang]/favorites/+page.svelte
index ae3285d..5ee13a6 100644
--- a/src/routes/[recipeLang=recipeLang]/favorites/+page.svelte
+++ b/src/routes/[recipeLang=recipeLang]/favorites/+page.svelte
@@ -25,7 +25,8 @@
emptyState2: isEnglish
? 'Visit a recipe and click the heart icon to add it to your favorites.'
: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.',
- recipesLink: isEnglish ? 'recipe' : 'Rezept'
+ recipesLink: isEnglish ? 'recipe' : 'Rezept',
+ toTry: isEnglish ? 'Recipes to try' : 'Zum Ausprobieren'
});
const { filtered: filteredFavorites, handleSearchResults } = createSearchFilter(() => data.favorites);
@@ -47,6 +48,28 @@ h1{
margin-top: 3rem;
color: var(--nord3);
}
+.to-try-link{
+ text-align: center;
+ margin-bottom: 1.5em;
+}
+.to-try-link a{
+ display: inline-block;
+ padding: 0.4em 1.2em;
+ border-radius: var(--radius-pill);
+ background: var(--nord10);
+ color: var(--nord6);
+ text-decoration: none;
+ font-size: 0.95rem;
+ font-weight: 500;
+ box-shadow: var(--shadow-sm);
+ transition: transform var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast);
+}
+.to-try-link a:hover,
+.to-try-link a:focus-visible{
+ transform: scale(1.05);
+ background: var(--nord9);
+ box-shadow: var(--shadow-hover);
+}
@@ -63,6 +86,8 @@ h1{
{/if}
+{labels.toTry} →
+
{#if data.error}
diff --git a/src/routes/[recipeLang=recipeLang]/to-try/+page.server.ts b/src/routes/[recipeLang=recipeLang]/to-try/+page.server.ts
new file mode 100644
index 0000000..c27ee46
--- /dev/null
+++ b/src/routes/[recipeLang=recipeLang]/to-try/+page.server.ts
@@ -0,0 +1,27 @@
+import type { PageServerLoad } from "./$types";
+import { redirect } from '@sveltejs/kit';
+import { ToTryRecipe } from '$models/ToTryRecipe';
+import { dbConnect } from '$utils/db';
+
+export const load: PageServerLoad = async ({ locals, params }) => {
+ const session = await locals.auth();
+
+ if (!session?.user?.nickname) {
+ throw redirect(302, `/${params.recipeLang}`);
+ }
+
+ await dbConnect();
+
+ try {
+ const items = await ToTryRecipe.find().sort({ createdAt: -1 }).lean();
+ return {
+ items: JSON.parse(JSON.stringify(items)),
+ session
+ };
+ } catch (e) {
+ return {
+ items: [],
+ error: 'Failed to load to-try recipes'
+ };
+ }
+};
diff --git a/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte b/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte
new file mode 100644
index 0000000..58cf97c
--- /dev/null
+++ b/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte
@@ -0,0 +1,327 @@
+
+
+
+
+
+ {labels.pageTitle}
+
+
+
+{labels.title}
+
+ {#if items.length > 0}
+ {labels.count}
+ {:else}
+ {labels.noItems}
+ {/if}
+
+
+
+ {#if !showForm}
+
+ {/if}
+
+
+{#if showForm}
+
+{/if}
+
+{#if items.length > 0}
+
+ {#each items as item (item._id)}
+
+ {/each}
+
+{:else if !showForm}
+
+{/if}
diff --git a/src/routes/api/[recipeLang=recipeLang]/to-try/+server.ts b/src/routes/api/[recipeLang=recipeLang]/to-try/+server.ts
new file mode 100644
index 0000000..f55cc1c
--- /dev/null
+++ b/src/routes/api/[recipeLang=recipeLang]/to-try/+server.ts
@@ -0,0 +1,120 @@
+import { json, error, type RequestHandler } from '@sveltejs/kit';
+import { ToTryRecipe } from '$models/ToTryRecipe';
+import { dbConnect } from '$utils/db';
+
+export const GET: RequestHandler = async ({ locals }) => {
+ const session = await locals.auth();
+
+ if (!session?.user?.nickname) {
+ throw error(401, 'Authentication required');
+ }
+
+ await dbConnect();
+
+ try {
+ const items = await ToTryRecipe.find().sort({ createdAt: -1 }).lean();
+ return json(items);
+ } catch (e) {
+ throw error(500, 'Failed to fetch to-try recipes');
+ }
+};
+
+export const POST: RequestHandler = async ({ request, locals }) => {
+ const session = await locals.auth();
+
+ if (!session?.user?.nickname) {
+ throw error(401, 'Authentication required');
+ }
+
+ const { name, links, notes } = await request.json();
+
+ if (!name?.trim()) {
+ throw error(400, 'Name is required');
+ }
+
+ if (!Array.isArray(links) || links.length === 0 || !links.some((l: any) => l.url?.trim())) {
+ throw error(400, 'At least one link is required');
+ }
+
+ await dbConnect();
+
+ try {
+ const item = await ToTryRecipe.create({
+ name: name.trim(),
+ links: links.filter((l: any) => l.url?.trim()),
+ notes: notes?.trim() || '',
+ addedBy: session.user.nickname
+ });
+ return json(item, { status: 201 });
+ } catch (e) {
+ throw error(500, 'Failed to create to-try recipe');
+ }
+};
+
+export const PATCH: RequestHandler = async ({ request, locals }) => {
+ const session = await locals.auth();
+
+ if (!session?.user?.nickname) {
+ throw error(401, 'Authentication required');
+ }
+
+ const { id, name, links, notes } = await request.json();
+
+ if (!id) {
+ throw error(400, 'ID is required');
+ }
+
+ if (!name?.trim()) {
+ throw error(400, 'Name is required');
+ }
+
+ if (!Array.isArray(links) || links.length === 0 || !links.some((l: any) => l.url?.trim())) {
+ throw error(400, 'At least one link is required');
+ }
+
+ await dbConnect();
+
+ try {
+ const item = await ToTryRecipe.findByIdAndUpdate(
+ id,
+ {
+ name: name.trim(),
+ links: links.filter((l: any) => l.url?.trim()),
+ notes: notes?.trim() || ''
+ },
+ { new: true }
+ ).lean();
+
+ if (!item) {
+ throw error(404, 'Item not found');
+ }
+
+ return json(item);
+ } catch (e) {
+ if (e instanceof Error && 'status' in e) throw e;
+ throw error(500, 'Failed to update to-try recipe');
+ }
+};
+
+export const DELETE: RequestHandler = async ({ request, locals }) => {
+ const session = await locals.auth();
+
+ if (!session?.user?.nickname) {
+ throw error(401, 'Authentication required');
+ }
+
+ const { id } = await request.json();
+
+ if (!id) {
+ throw error(400, 'ID is required');
+ }
+
+ await dbConnect();
+
+ try {
+ await ToTryRecipe.findByIdAndDelete(id);
+ return json({ success: true });
+ } catch (e) {
+ throw error(500, 'Failed to delete to-try recipe');
+ }
+};