From fe46ab194ef20ceba7246c593c620ec8a489fc70 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 1 Sep 2025 20:18:57 +0200 Subject: [PATCH] Implement user favorites feature for recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add UserFavorites MongoDB model with ObjectId references - Create authenticated API endpoints for favorites management - Add Heart icon and FavoriteButton components with toggle functionality - Display favorite button below recipe tags for logged-in users - Add Favoriten navigation link (visible only when authenticated) - Create favorites page with grid layout and search functionality - Store favorites by MongoDB ObjectId for data integrity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lib/assets/icons/Heart.svelte | 33 ++++++ src/lib/components/FavoriteButton.svelte | 102 ++++++++++++++++ src/models/UserFavorites.ts | 11 ++ src/routes/api/rezepte/favorites/+server.ts | 112 ++++++++++++++++++ .../favorites/check/[shortName]/+server.ts | 42 +++++++ .../api/rezepte/favorites/recipes/+server.ts | 40 +++++++ src/routes/rezepte/+layout.svelte | 3 + src/routes/rezepte/[name]/+page.svelte | 10 ++ src/routes/rezepte/[name]/+page.ts | 18 ++- src/routes/rezepte/favorites/+page.server.ts | 32 +++++ src/routes/rezepte/favorites/+page.svelte | 58 +++++++++ 11 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 src/lib/assets/icons/Heart.svelte create mode 100644 src/lib/components/FavoriteButton.svelte create mode 100644 src/models/UserFavorites.ts create mode 100644 src/routes/api/rezepte/favorites/+server.ts create mode 100644 src/routes/api/rezepte/favorites/check/[shortName]/+server.ts create mode 100644 src/routes/api/rezepte/favorites/recipes/+server.ts create mode 100644 src/routes/rezepte/favorites/+page.server.ts create mode 100644 src/routes/rezepte/favorites/+page.svelte diff --git a/src/lib/assets/icons/Heart.svelte b/src/lib/assets/icons/Heart.svelte new file mode 100644 index 0000000..dfd4c1c --- /dev/null +++ b/src/lib/assets/icons/Heart.svelte @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/src/lib/components/FavoriteButton.svelte b/src/lib/components/FavoriteButton.svelte new file mode 100644 index 0000000..9ed997d --- /dev/null +++ b/src/lib/components/FavoriteButton.svelte @@ -0,0 +1,102 @@ + + + + +{#if isLoggedIn} + +{/if} \ No newline at end of file diff --git a/src/models/UserFavorites.ts b/src/models/UserFavorites.ts new file mode 100644 index 0000000..88bd612 --- /dev/null +++ b/src/models/UserFavorites.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const UserFavoritesSchema = new mongoose.Schema( + { + username: { type: String, required: true, unique: true }, + favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' }] // Recipe MongoDB ObjectIds + }, + { timestamps: true } +); + +export const UserFavorites = mongoose.model("UserFavorites", UserFavoritesSchema); \ No newline at end of file diff --git a/src/routes/api/rezepte/favorites/+server.ts b/src/routes/api/rezepte/favorites/+server.ts new file mode 100644 index 0000000..5d62cd2 --- /dev/null +++ b/src/routes/api/rezepte/favorites/+server.ts @@ -0,0 +1,112 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { UserFavorites } from '../../../../models/UserFavorites'; +import { Recipe } from '../../../../models/Recipe'; +import { dbConnect, dbDisconnect } from '../../../../utils/db'; +import { error } from '@sveltejs/kit'; +import mongoose from 'mongoose'; + +export const GET: RequestHandler = async ({ locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + await dbConnect(); + + try { + const userFavorites = await UserFavorites.findOne({ + username: session.user.nickname + }).lean(); + + await dbDisconnect(); + + return json({ + favorites: userFavorites?.favorites || [] + }); + } catch (e) { + await dbDisconnect(); + throw error(500, 'Failed to fetch favorites'); + } +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + const { recipeId } = await request.json(); + + if (!recipeId) { + throw error(400, 'Recipe ID required'); + } + + await dbConnect(); + + try { + // Validate that the recipe exists and get its ObjectId + const recipe = await Recipe.findOne({ short_name: recipeId }); + if (!recipe) { + await dbDisconnect(); + throw error(404, 'Recipe not found'); + } + + await UserFavorites.findOneAndUpdate( + { username: session.user.nickname }, + { $addToSet: { favorites: recipe._id } }, + { upsert: true, new: true } + ); + + await dbDisconnect(); + + return json({ success: true }); + } catch (e) { + await dbDisconnect(); + if (e instanceof Error && e.message.includes('404')) { + throw e; + } + throw error(500, 'Failed to add favorite'); + } +}; + +export const DELETE: RequestHandler = async ({ request, locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + const { recipeId } = await request.json(); + + if (!recipeId) { + throw error(400, 'Recipe ID required'); + } + + await dbConnect(); + + try { + // Find the recipe's ObjectId + const recipe = await Recipe.findOne({ short_name: recipeId }); + if (!recipe) { + await dbDisconnect(); + throw error(404, 'Recipe not found'); + } + + await UserFavorites.findOneAndUpdate( + { username: session.user.nickname }, + { $pull: { favorites: recipe._id } } + ); + + await dbDisconnect(); + + return json({ success: true }); + } catch (e) { + await dbDisconnect(); + if (e instanceof Error && e.message.includes('404')) { + throw e; + } + throw error(500, 'Failed to remove favorite'); + } +}; \ No newline at end of file diff --git a/src/routes/api/rezepte/favorites/check/[shortName]/+server.ts b/src/routes/api/rezepte/favorites/check/[shortName]/+server.ts new file mode 100644 index 0000000..06afc8d --- /dev/null +++ b/src/routes/api/rezepte/favorites/check/[shortName]/+server.ts @@ -0,0 +1,42 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { UserFavorites } from '../../../../../../models/UserFavorites'; +import { Recipe } from '../../../../../../models/Recipe'; +import { dbConnect, dbDisconnect } from '../../../../../../utils/db'; +import { error } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ locals, params }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + return json({ isFavorite: false }); + } + + await dbConnect(); + + try { + // Find the recipe by short_name to get its ObjectId + const recipe = await Recipe.findOne({ short_name: params.shortName }); + if (!recipe) { + await dbDisconnect(); + throw error(404, 'Recipe not found'); + } + + // Check if this recipe is in the user's favorites + const userFavorites = await UserFavorites.findOne({ + username: session.user.nickname, + favorites: recipe._id + }).lean(); + + await dbDisconnect(); + + return json({ + isFavorite: !!userFavorites + }); + } catch (e) { + await dbDisconnect(); + if (e instanceof Error && e.message.includes('404')) { + throw e; + } + throw error(500, 'Failed to check favorite status'); + } +}; \ No newline at end of file diff --git a/src/routes/api/rezepte/favorites/recipes/+server.ts b/src/routes/api/rezepte/favorites/recipes/+server.ts new file mode 100644 index 0000000..8e5f0c7 --- /dev/null +++ b/src/routes/api/rezepte/favorites/recipes/+server.ts @@ -0,0 +1,40 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { UserFavorites } from '../../../../../models/UserFavorites'; +import { Recipe } from '../../../../../models/Recipe'; +import { dbConnect, dbDisconnect } from '../../../../../utils/db'; +import type { RecipeModelType } from '../../../../../types/types'; +import { error } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + await dbConnect(); + + try { + const userFavorites = await UserFavorites.findOne({ + username: session.user.nickname + }).lean(); + + if (!userFavorites?.favorites?.length) { + await dbDisconnect(); + return json([]); + } + + let recipes = await Recipe.find({ + _id: { $in: userFavorites.favorites } + }).lean() as RecipeModelType[]; + + await dbDisconnect(); + + recipes = JSON.parse(JSON.stringify(recipes)); + + return json(recipes); + } catch (e) { + await dbDisconnect(); + throw error(500, 'Failed to fetch favorite recipes'); + } +}; \ No newline at end of file diff --git a/src/routes/rezepte/+layout.svelte b/src/routes/rezepte/+layout.svelte index 4c154bc..482c8bf 100644 --- a/src/routes/rezepte/+layout.svelte +++ b/src/routes/rezepte/+layout.svelte @@ -11,6 +11,9 @@ if(data.session){