Compare commits
3 Commits
1d78b5439e
...
b534cd1ddc
Author | SHA1 | Date | |
---|---|---|---|
b534cd1ddc
|
|||
6a64a7ddd6
|
|||
fe46ab194e
|
@@ -25,6 +25,6 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
|
|||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
},
|
||||||
trustHost: true, // needed for reverse proxy setups
|
trustHost: true // needed for reverse proxy setups
|
||||||
})
|
})
|
||||||
|
33
src/lib/assets/icons/Heart.svelte
Normal file
33
src/lib/assets/icons/Heart.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
@keyframes shake{
|
||||||
|
0%{
|
||||||
|
transform: rotate(0)
|
||||||
|
scale(1,1);
|
||||||
|
}
|
||||||
|
25%{
|
||||||
|
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
|
||||||
|
transform: rotate(30deg)
|
||||||
|
scale(1.2,1.2)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
50%{
|
||||||
|
|
||||||
|
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
|
||||||
|
transform: rotate(-30deg)
|
||||||
|
scale(1.2,1.2);
|
||||||
|
}
|
||||||
|
74%{
|
||||||
|
|
||||||
|
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
|
||||||
|
transform: rotate(30deg)
|
||||||
|
scale(1.2, 1.2);
|
||||||
|
}
|
||||||
|
100%{
|
||||||
|
transform: rotate(0)
|
||||||
|
scale(1,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" {...$$restProps}><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="m47.6 300.4 180.7 168.7c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg>
|
@@ -7,6 +7,8 @@ import "$lib/css/nordtheme.css";
|
|||||||
import "$lib/css/shake.css";
|
import "$lib/css/shake.css";
|
||||||
import "$lib/css/icon.css";
|
import "$lib/css/icon.css";
|
||||||
export let do_margin_right = false;
|
export let do_margin_right = false;
|
||||||
|
export let isFavorite = false;
|
||||||
|
export let showFavoriteIndicator = false;
|
||||||
// to manually override lazy loading for top cards
|
// to manually override lazy loading for top cards
|
||||||
export let loading_strat : "lazy" | "eager" | undefined;
|
export let loading_strat : "lazy" | "eager" | undefined;
|
||||||
if(loading_strat === undefined){
|
if(loading_strat === undefined){
|
||||||
@@ -192,6 +194,14 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
|||||||
scale: 0.9 0.9;
|
scale: 0.9 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favorite-indicator{
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2rem;
|
||||||
|
top: -0.5em;
|
||||||
|
left: -0.5em;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
.icon:hover,
|
.icon:hover,
|
||||||
.icon:focus-visible
|
.icon:focus-visible
|
||||||
{
|
{
|
||||||
@@ -224,6 +234,9 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
|||||||
<img class:blur={!isloaded} id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
|
<img class:blur={!isloaded} id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#if showFavoriteIndicator && isFavorite}
|
||||||
|
<div class="favorite-indicator">❤️</div>
|
||||||
|
{/if}
|
||||||
{#if icon_override || recipe.season.includes(current_month)}
|
{#if icon_override || recipe.season.includes(current_month)}
|
||||||
<button class=icon on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/icon/${recipe.icon}`}}>{recipe.icon}</button>
|
<button class=icon on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/icon/${recipe.icon}`}}>{recipe.icon}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
66
src/lib/components/FavoriteButton.svelte
Normal file
66
src/lib/components/FavoriteButton.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let recipeId: string;
|
||||||
|
export let isFavorite: boolean = false;
|
||||||
|
export let isLoggedIn: boolean = false;
|
||||||
|
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
async function toggleFavorite() {
|
||||||
|
if (!isLoggedIn || isLoading) return;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const method = isFavorite ? 'DELETE' : 'POST';
|
||||||
|
const response = await fetch('/api/rezepte/favorites', {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ recipeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
isFavorite = !isFavorite;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle favorite:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.favorite-button {
|
||||||
|
all: unset;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 100ms;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-button:hover,
|
||||||
|
.favorite-button:focus-visible {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if isLoggedIn}
|
||||||
|
<button
|
||||||
|
class="favorite-button"
|
||||||
|
disabled={isLoading}
|
||||||
|
on:click={toggleFavorite}
|
||||||
|
title={isFavorite ? 'Favorit entfernen' : 'Als Favorit speichern'}
|
||||||
|
>
|
||||||
|
{isFavorite ? '❤️' : '🖤'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
@@ -12,6 +12,6 @@ export function rand_array(array){
|
|||||||
let time = new Date()
|
let time = new Date()
|
||||||
const seed = Math.floor(time.getTime()/MS_PER_DAY)
|
const seed = Math.floor(time.getTime()/MS_PER_DAY)
|
||||||
let rand = mulberry32(seed)
|
let rand = mulberry32(seed)
|
||||||
array.sort((a,b) => 0.5 - rand())
|
// Create a copy to avoid mutating the original array
|
||||||
return array
|
return [...array].sort((a,b) => 0.5 - rand())
|
||||||
}
|
}
|
||||||
|
48
src/lib/server/favorites.ts
Normal file
48
src/lib/server/favorites.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for handling user favorites on the server side
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getUserFavorites(fetch: any, locals: any): Promise<string[]> {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.nickname) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const favRes = await fetch('/api/rezepte/favorites');
|
||||||
|
if (favRes.ok) {
|
||||||
|
const favData = await favRes.json();
|
||||||
|
return favData.favorites || [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail if favorites can't be loaded
|
||||||
|
console.error('Error loading user favorites:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string[]): any[] {
|
||||||
|
return recipes.map(recipe => ({
|
||||||
|
...recipe,
|
||||||
|
isFavorite: userFavorites.some(favId => favId.toString() === recipe._id.toString())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRecipesWithFavorites(
|
||||||
|
fetch: any,
|
||||||
|
locals: any,
|
||||||
|
recipeLoader: () => Promise<any>
|
||||||
|
): Promise<{ recipes: any[], session: any }> {
|
||||||
|
const [recipes, userFavorites, session] = await Promise.all([
|
||||||
|
recipeLoader(),
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recipes: addFavoriteStatusToRecipes(recipes, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
}
|
11
src/models/UserFavorites.ts
Normal file
11
src/models/UserFavorites.ts
Normal file
@@ -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);
|
112
src/routes/api/rezepte/favorites/+server.ts
Normal file
112
src/routes/api/rezepte/favorites/+server.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
40
src/routes/api/rezepte/favorites/recipes/+server.ts
Normal file
40
src/routes/api/rezepte/favorites/recipes/+server.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
@@ -11,6 +11,9 @@ if(data.session){
|
|||||||
<Header>
|
<Header>
|
||||||
<ul class=site_header slot=links>
|
<ul class=site_header slot=links>
|
||||||
<li><a href="/rezepte">Alle Rezepte</a></li>
|
<li><a href="/rezepte">Alle Rezepte</a></li>
|
||||||
|
{#if user}
|
||||||
|
<li><a href="/rezepte/favorites">Favoriten</a></li>
|
||||||
|
{/if}
|
||||||
<li><a href="/rezepte/season">In Saison</a></li>
|
<li><a href="/rezepte/season">In Saison</a></li>
|
||||||
<li><a href="/rezepte/category">Kategorie</a></li>
|
<li><a href="/rezepte/category">Kategorie</a></li>
|
||||||
<li><a href="/rezepte/icon">Icon</a></li>
|
<li><a href="/rezepte/icon">Icon</a></li>
|
||||||
|
@@ -1,13 +1,22 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch, locals }) {
|
||||||
let current_month = new Date().getMonth() + 1
|
let current_month = new Date().getMonth() + 1
|
||||||
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
|
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
|
||||||
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
|
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
|
||||||
const item_season = await res_season.json();
|
const item_season = await res_season.json();
|
||||||
const item_all_brief = await res_all_brief.json();
|
const item_all_brief = await res_all_brief.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
season: item_season,
|
season: addFavoriteStatusToRecipes(item_season, userFavorites),
|
||||||
all_brief: item_all_brief,
|
all_brief: addFavoriteStatusToRecipes(item_all_brief, userFavorites),
|
||||||
|
session
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -36,14 +36,14 @@ h1{
|
|||||||
|
|
||||||
<MediaScroller title="In Saison">
|
<MediaScroller title="In Saison">
|
||||||
{#each data.season as recipe}
|
{#each data.season as recipe}
|
||||||
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true}></Card>
|
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</MediaScroller>
|
</MediaScroller>
|
||||||
|
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<MediaScroller title={category}>
|
<MediaScroller title={category}>
|
||||||
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
|
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
|
||||||
<Card {recipe} {current_month} do_margin_right={true}></Card>
|
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</MediaScroller>
|
</MediaScroller>
|
||||||
{/each}
|
{/each}
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
import {season} from '$lib/js/season_store';
|
import {season} from '$lib/js/season_store';
|
||||||
import RecipeNote from '$lib/components/RecipeNote.svelte';
|
import RecipeNote from '$lib/components/RecipeNote.svelte';
|
||||||
import {stripHtmlTags} from '$lib/js/stripHtmlTags';
|
import {stripHtmlTags} from '$lib/js/stripHtmlTags';
|
||||||
|
import FavoriteButton from '$lib/components/FavoriteButton.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
@@ -308,6 +309,13 @@ h4{
|
|||||||
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
|
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FavoriteButton
|
||||||
|
recipeId={data.short_name}
|
||||||
|
isFavorite={data.isFavorite || false}
|
||||||
|
isLoggedIn={!!data.session?.user}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if data.note}
|
{#if data.note}
|
||||||
<RecipeNote note={data.note}></RecipeNote>
|
<RecipeNote note={data.note}></RecipeNote>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -6,5 +6,21 @@ export async function load({ fetch, params}) {
|
|||||||
if(!res.ok){
|
if(!res.ok){
|
||||||
throw error(res.status, item.message)
|
throw error(res.status, item.message)
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
|
// Check if this recipe is favorited by the user
|
||||||
|
let isFavorite = false;
|
||||||
|
try {
|
||||||
|
const favRes = await fetch(`/api/rezepte/favorites/check/${params.name}`);
|
||||||
|
if (favRes.ok) {
|
||||||
|
const favData = await favRes.json();
|
||||||
|
isFavorite = favData.isFavorite;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail if not authenticated or other error
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isFavorite
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
19
src/routes/rezepte/category/[category]/+page.server.ts
Normal file
19
src/routes/rezepte/category/[category]/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
||||||
|
const res = await fetch(`/api/rezepte/items/category/${params.category}`);
|
||||||
|
const items = await res.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: params.category,
|
||||||
|
recipes: addFavoriteStatusToRecipes(items, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
@@ -18,7 +18,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(data.recipes) as recipe}
|
||||||
<Card {recipe} {current_month}></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export async function load({ fetch, params }) {
|
|
||||||
const res = await fetch(`/api/rezepte/items/category/${params.category}`);
|
|
||||||
const items = await res.json();
|
|
||||||
return {
|
|
||||||
category: params.category,
|
|
||||||
recipes: items
|
|
||||||
}
|
|
||||||
};
|
|
32
src/routes/rezepte/favorites/+page.server.ts
Normal file
32
src/routes/rezepte/favorites/+page.server.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.nickname) {
|
||||||
|
throw redirect(302, '/rezepte');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/rezepte/favorites/recipes');
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
favorites: [],
|
||||||
|
error: 'Failed to load favorites'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const favorites = await res.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
favorites,
|
||||||
|
session
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
favorites: [],
|
||||||
|
error: 'Failed to load favorites'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
58
src/routes/rezepte/favorites/+page.svelte
Normal file
58
src/routes/rezepte/favorites/+page.svelte
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import '$lib/css/nordtheme.css';
|
||||||
|
import Recipes from '$lib/components/Recipes.svelte';
|
||||||
|
import Card from '$lib/components/Card.svelte';
|
||||||
|
import Search from '$lib/components/Search.svelte';
|
||||||
|
export let data: PageData;
|
||||||
|
export let current_month = new Date().getMonth() + 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1{
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
.subheading{
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.empty-state{
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 3rem;
|
||||||
|
color: var(--nord3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Meine Favoriten - Bocken Rezepte</title>
|
||||||
|
<meta name="description" content="Meine favorisierten Rezepte aus der Bockenschen Küche." />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1>Favoriten</h1>
|
||||||
|
<p class=subheading>
|
||||||
|
{#if data.favorites.length > 0}
|
||||||
|
{data.favorites.length} favorisierte Rezepte
|
||||||
|
{:else}
|
||||||
|
Noch keine Favoriten gespeichert
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Search></Search>
|
||||||
|
|
||||||
|
{#if data.error}
|
||||||
|
<p class="empty-state">Fehler beim Laden der Favoriten: {data.error}</p>
|
||||||
|
{:else if data.favorites.length > 0}
|
||||||
|
<Recipes>
|
||||||
|
{#each data.favorites as recipe}
|
||||||
|
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true}></Card>
|
||||||
|
{/each}
|
||||||
|
</Recipes>
|
||||||
|
{:else}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>Du hast noch keine Rezepte als Favoriten gespeichert.</p>
|
||||||
|
<p>Besuche ein <a href="/rezepte">Rezept</a> und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
22
src/routes/rezepte/icon/[icon]/+page.server.ts
Normal file
22
src/routes/rezepte/icon/[icon]/+page.server.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
||||||
|
const res_season = await fetch(`/api/rezepte/items/icon/` + params.icon);
|
||||||
|
const res_icons = await fetch(`/api/rezepte/items/icon`);
|
||||||
|
const icons = await res_icons.json();
|
||||||
|
const item_season = await res_season.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
icons: icons,
|
||||||
|
icon: params.icon,
|
||||||
|
season: addFavoriteStatusToRecipes(item_season, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
@@ -11,7 +11,7 @@
|
|||||||
<IconLayout icons={data.icons} active_icon={data.icon} >
|
<IconLayout icons={data.icons} active_icon={data.icon} >
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(data.season) as recipe}
|
||||||
<Card {recipe} icon_override=true></Card>
|
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
</IconLayout>
|
</IconLayout>
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export async function load({ fetch, params }) {
|
|
||||||
const res_season = await fetch(`/api/rezepte/items/icon/` + params.icon);
|
|
||||||
const res_icons = await fetch(`/api/rezepte/items/icon`);
|
|
||||||
const icons = await res_icons.json();
|
|
||||||
const item_season = await res_season.json();
|
|
||||||
return {
|
|
||||||
icons: icons,
|
|
||||||
icon: params.icon,
|
|
||||||
season: item_season,
|
|
||||||
};
|
|
||||||
};
|
|
19
src/routes/rezepte/season/+page.server.ts
Normal file
19
src/routes/rezepte/season/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
|
let current_month = new Date().getMonth() + 1
|
||||||
|
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
|
||||||
|
const item_season = await res_season.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
season: addFavoriteStatusToRecipes(item_season, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
@@ -14,7 +14,7 @@
|
|||||||
<SeasonLayout active_index={current_month-1}>
|
<SeasonLayout active_index={current_month-1}>
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(data.season) as recipe}
|
||||||
<Card {recipe} {current_month}></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
</SeasonLayout>
|
</SeasonLayout>
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
|
||||||
let current_month = new Date().getMonth() + 1
|
|
||||||
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
|
|
||||||
const item_season = await res_season.json();
|
|
||||||
return {
|
|
||||||
season: item_season,
|
|
||||||
};
|
|
||||||
};
|
|
19
src/routes/rezepte/season/[month]/+page.server.ts
Normal file
19
src/routes/rezepte/season/[month]/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
||||||
|
const res_season = await fetch(`/api/rezepte/items/in_season/` + params.month);
|
||||||
|
const item_season = await res_season.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
month: params.month,
|
||||||
|
season: addFavoriteStatusToRecipes(item_season, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
@@ -12,7 +12,7 @@
|
|||||||
<SeasonLayout active_index={data.month -1}>
|
<SeasonLayout active_index={data.month -1}>
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(data.season) as recipe}
|
||||||
<Card {recipe} icon_override=true></Card>
|
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
</SeasonLayout>
|
</SeasonLayout>
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export async function load({ fetch, params }) {
|
|
||||||
const res_season = await fetch(`/api/rezepte/items/in_season/` + params.month);
|
|
||||||
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
|
|
||||||
const item_season = await res_season.json();
|
|
||||||
const item_all_brief = await res_all_brief.json();
|
|
||||||
return {
|
|
||||||
month: params.month,
|
|
||||||
season: item_season,
|
|
||||||
};
|
|
||||||
};
|
|
19
src/routes/rezepte/tag/[tag]/+page.server.ts
Normal file
19
src/routes/rezepte/tag/[tag]/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
||||||
|
const res_tag = await fetch(`/api/rezepte/items/tag/${params.tag}`);
|
||||||
|
const items_tag = await res_tag.json();
|
||||||
|
|
||||||
|
// Get user favorites and session
|
||||||
|
const [userFavorites, session] = await Promise.all([
|
||||||
|
getUserFavorites(fetch, locals),
|
||||||
|
locals.auth()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: params.tag,
|
||||||
|
recipes: addFavoriteStatusToRecipes(items_tag, userFavorites),
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
@@ -18,7 +18,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(data.recipes) as recipe}
|
||||||
<Card {recipe} {current_month}></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export async function load({ fetch, params }) {
|
|
||||||
const res_tag = await fetch(`/api/rezepte/items/tag/${params.tag}`);
|
|
||||||
const items_tag = await res_tag.json();
|
|
||||||
return {
|
|
||||||
tag: params.tag,
|
|
||||||
recipes: items_tag
|
|
||||||
}
|
|
||||||
};
|
|
Reference in New Issue
Block a user