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}

+ + {#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 editingId} +

{labels.editHeading}

+ {/if} +
+ + +
+ +
+ + + {#each links as link, i (i)} + + {/each} + +
+ +
+ + +
+ +
+ + +
+
+{/if} + +{#if items.length > 0} +
+ {#each items as item (item._id)} + + {/each} +
+{:else if !showForm} +
+

{labels.emptyState}

+
+{/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'); + } +};