refactor: merge api/recipes and api/rezepte into unified recipeLang route

Consolidate duplicate recipe API routes into a single
api/[recipeLang=recipeLang]/ structure. Both /api/recipes/ and
/api/rezepte/ URLs continue to work via the param matcher. Shared
read endpoints now serve both languages with caching for both.

Also: remove dead code (5 unused components, cookie.js) and the
redundant cron-execute recurring payment route.
This commit is contained in:
2026-02-11 09:49:44 +01:00
parent 8560077759
commit ac4c00a082
65 changed files with 479 additions and 2344 deletions

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`${apiBase}/items/in_season/` + current_month);

View File

@@ -4,7 +4,7 @@ import { stripHtmlTags } from '$lib/js/stripHtmlTags';
export const load: PageServerLoad = async ({ fetch, params, locals }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const res = await fetch(`${apiBase}/items/${params.name}`);

View File

@@ -5,7 +5,7 @@ import { getAllCategories, isOfflineDataAvailable } from '$lib/offline/db';
export const load: PageLoad = async ({ fetch, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
// Check if we should use offline data
if (browser && isOffline() && canUseOfflineData()) {

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const res = await fetch(`${apiBase}/items/category/${params.category}`);
const items = await res.json();

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const session = await locals.auth();
if (!session?.user?.nickname) {

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const res_season = await fetch(`${apiBase}/items/icon/` + params.icon);
const res_icons = await fetch(`/api/rezepte/items/icon`); // Icons are shared across languages

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from './$types';
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ url, fetch, params, locals }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const query = url.searchParams.get('q') || '';
const category = url.searchParams.get('category');

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`${apiBase}/items/in_season/` + current_month);

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const res_season = await fetch(`${apiBase}/items/in_season/` + params.month);
const item_season = await res_season.json();

View File

@@ -5,7 +5,7 @@ import { getAllTags, isOfflineDataAvailable } from '$lib/offline/db';
export const load: PageLoad = async ({ fetch, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
// Check if we should use offline data
if (browser && isOffline() && canUseOfflineData()) {

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad} from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
const apiBase = `/api/${params.recipeLang}`;
const res_tag = await fetch(`${apiBase}/items/tag/${params.tag}`);
const items_tag = await res_tag.json();

View File

@@ -0,0 +1,73 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '$models/UserFavorites';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type { RecipeModelType } from '$types/types';
import { error } from '@sveltejs/kit';
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params, 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) {
return json([]);
}
const { approvalFilter } = briefQueryConfig(params.recipeLang);
const en = isEnglish(params.recipeLang);
let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites },
...approvalFilter
}).lean() as RecipeModelType[];
if (en) {
const englishRecipes = recipes.map(recipe => ({
_id: recipe._id,
short_name: recipe.translations.en.short_name,
name: recipe.translations.en.name,
category: recipe.translations.en.category,
icon: recipe.icon,
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
images: recipe.images?.map((img, idx) => ({
mediapath: img.mediapath,
alt: recipe.translations.en.images?.[idx]?.alt || img.alt,
caption: recipe.translations.en.images?.[idx]?.caption || img.caption,
})),
description: recipe.translations.en.description,
note: recipe.translations.en.note,
tags: recipe.translations.en.tags || [],
season: recipe.season,
baking: recipe.baking,
preparation: recipe.preparation,
fermentation: recipe.fermentation,
portions: recipe.portions,
cooking: recipe.cooking,
total_time: recipe.total_time,
ingredients: recipe.translations.en.ingredients || [],
instructions: recipe.translations.en.instructions || [],
preamble: recipe.translations.en.preamble,
addendum: recipe.translations.en.addendum,
germanShortName: recipe.short_name,
translationStatus: recipe.translations.en.translationStatus
}));
return json(JSON.parse(JSON.stringify(englishRecipes)));
}
return json(JSON.parse(JSON.stringify(recipes)));
} catch (e) {
throw error(500, 'Failed to fetch favorite recipes');
}
};

View File

@@ -0,0 +1,160 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { error } from '@sveltejs/kit';
import type { RecipeModelType } from '$types/types';
import { isEnglish } from '$lib/server/recipeHelpers';
/** Recursively map populated baseRecipeRef to resolvedRecipe field */
function mapBaseRecipeRefs(items: any[]): any[] {
return items.map((item: any) => {
if (item.type === 'reference' && item.baseRecipeRef) {
const resolvedRecipe = { ...item.baseRecipeRef };
if (resolvedRecipe.ingredients) {
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
}
if (resolvedRecipe.instructions) {
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
}
return { ...item, resolvedRecipe };
}
return item;
});
}
export const GET: RequestHandler = async ({ params }) => {
await dbConnect();
const en = isEnglish(params.recipeLang);
const query = en
? { 'translations.en.short_name': params.name }
: { short_name: params.name };
const populatePaths = en
? [
{
path: 'translations.en.ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations'
}
}
},
{
path: 'translations.en.instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations'
}
}
}
]
: [
{
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations'
}
}
},
{
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations'
}
}
}
];
let dbQuery = Recipe.findOne(query);
for (const p of populatePaths) {
dbQuery = dbQuery.populate(p);
}
const rawRecipe = await dbQuery.lean();
if (!rawRecipe) {
throw error(404, 'Recipe not found');
}
if (en) {
if (!rawRecipe.translations?.en) {
throw error(404, 'English translation not available for this recipe');
}
const t = rawRecipe.translations.en;
let recipe: any = {
_id: rawRecipe._id,
short_name: t.short_name,
name: t.name,
description: t.description,
preamble: t.preamble || '',
addendum: t.addendum || '',
note: t.note || '',
category: t.category,
tags: t.tags || [],
ingredients: t.ingredients || [],
instructions: t.instructions || [],
images: rawRecipe.images || [],
icon: rawRecipe.icon || '',
dateCreated: rawRecipe.dateCreated,
dateModified: rawRecipe.dateModified,
season: rawRecipe.season || [],
baking: t.baking || rawRecipe.baking || { temperature: '', length: '', mode: '' },
preparation: t.preparation || rawRecipe.preparation || '',
fermentation: t.fermentation || rawRecipe.fermentation || { bulk: '', final: '' },
portions: t.portions || rawRecipe.portions || '',
cooking: t.cooking || rawRecipe.cooking || '',
total_time: t.total_time || rawRecipe.total_time || '',
translationStatus: t.translationStatus,
germanShortName: rawRecipe.short_name,
};
if (recipe.ingredients) {
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
}
if (recipe.instructions) {
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
}
// Merge English alt/caption with original image paths
const imagesArray = Array.isArray(rawRecipe.images) ? rawRecipe.images : (rawRecipe.images ? [rawRecipe.images] : []);
if (imagesArray.length > 0) {
const translatedImages = t.images || [];
recipe.images = imagesArray.map((img: any, index: number) => ({
mediapath: img.mediapath,
alt: translatedImages[index]?.alt || img.alt || '',
caption: translatedImages[index]?.caption || img.caption || '',
}));
}
return json(recipe);
}
// German: pass through with base recipe ref mapping
let recipe = JSON.parse(JSON.stringify(rawRecipe));
if (recipe.ingredients) {
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
}
if (recipe.instructions) {
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
}
return json(recipe);
};

View File

@@ -0,0 +1,26 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
const cacheKey = `recipes:${params.recipeLang}:all_brief`;
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
await dbConnect();
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes))));
};

View File

@@ -0,0 +1,14 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, prefix } = briefQueryConfig(params.recipeLang);
await dbConnect();
const field = `${prefix}category`;
const categories = await Recipe.distinct(field, approvalFilter).lean();
return json(JSON.parse(JSON.stringify(categories)));
};

View File

@@ -0,0 +1,18 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
await dbConnect();
const dbRecipes = await Recipe.find(
{ [`${prefix}category`]: params.category, ...approvalFilter },
projection
).lean();
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang)));
return json(JSON.parse(JSON.stringify(recipes)));
};

View File

@@ -0,0 +1,18 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
await dbConnect();
const dbRecipes = await Recipe.find(
{ icon: params.icon, ...approvalFilter },
projection
).lean();
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang)));
return json(JSON.parse(JSON.stringify(recipes)));
};

View File

@@ -0,0 +1,28 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
const cacheKey = `recipes:${params.recipeLang}:in_season:${params.month}`;
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
await dbConnect();
const dbRecipes = await Recipe.find(
{ season: params.month, icon: { $ne: "🍽️" }, ...approvalFilter },
projection
).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes))));
};

View File

@@ -0,0 +1,23 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter } = briefQueryConfig(params.recipeLang);
await dbConnect();
if (isEnglish(params.recipeLang)) {
const recipes = await Recipe.find(approvalFilter, 'translations.en.tags').lean();
const tagsSet = new Set<string>();
recipes.forEach(recipe => {
if (recipe.translations?.en?.tags) {
recipe.translations.en.tags.forEach((tag: string) => tagsSet.add(tag));
}
});
return json(JSON.parse(JSON.stringify(Array.from(tagsSet).sort())));
}
const tags = await Recipe.distinct('tags').lean();
return json(JSON.parse(JSON.stringify(tags)));
};

View File

@@ -0,0 +1,28 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
const cacheKey = `recipes:${params.recipeLang}:tag:${params.tag}`;
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
await dbConnect();
const dbRecipes = await Recipe.find(
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
projection
).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes))));
};

View File

@@ -2,14 +2,17 @@ import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { isEnglish, briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ url, locals }) => {
export const GET: RequestHandler = async ({ url, params, locals }) => {
await dbConnect();
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
const en = isEnglish(params.recipeLang);
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
const category = url.searchParams.get('category');
// Support both single tag (backwards compat) and multiple tags
const singleTag = url.searchParams.get('tag');
const multipleTags = url.searchParams.get('tags');
const tags = multipleTags
@@ -18,7 +21,6 @@ export const GET: RequestHandler = async ({ url, locals }) => {
const icon = url.searchParams.get('icon');
// Support both single season (backwards compat) and multiple seasons
const singleSeason = url.searchParams.get('season');
const multipleSeasons = url.searchParams.get('seasons');
const seasons = multipleSeasons
@@ -28,58 +30,48 @@ export const GET: RequestHandler = async ({ url, locals }) => {
const favoritesOnly = url.searchParams.get('favorites') === 'true';
try {
// Build base query
let dbQuery: any = {};
let dbQuery: any = { ...approvalFilter };
// Apply filters based on context
if (category) {
dbQuery.category = category;
dbQuery[`${prefix}category`] = category;
}
// Multi-tag AND logic: recipe must have ALL selected tags
if (tags.length > 0) {
dbQuery.tags = { $all: tags };
dbQuery[`${prefix}tags`] = { $all: tags };
}
if (icon) {
dbQuery.icon = icon;
}
// Multi-season OR logic: recipe in any selected season
if (seasons.length > 0) {
dbQuery.season = { $in: seasons };
}
// Get all recipes matching base filters
let recipes = await Recipe.find(dbQuery, 'name short_name tags category icon description season dateModified').lean() as BriefRecipeType[];
const dbRecipes = await Recipe.find(dbQuery, projection).lean();
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang));
// Handle favorites filter
if (favoritesOnly && locals.session?.user) {
const { UserFavorites } = await import('../../../../models/UserFavorites');
const { UserFavorites } = await import('$models/UserFavorites');
const userFavorites = await UserFavorites.findOne({ username: locals.session.user.username });
if (userFavorites && userFavorites.favorites) {
if (userFavorites?.favorites) {
const favoriteIds = userFavorites.favorites;
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
} else {
recipes = [];
}
}
// Apply text search if query provided
// Apply text search
if (query) {
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
recipes = recipes.filter(recipe => {
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/&shy;|­/g, '');
return searchTerms.every(term => searchString.includes(term));
});
}
return json(JSON.parse(JSON.stringify(recipes)));
} catch (error) {
} catch (e) {
return json({ error: 'Search failed' }, { status: 500 });
}
};
};

View File

@@ -1,120 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { RecurringPayment } from '$models/RecurringPayment';
import { Payment } from '$models/Payment';
import { PaymentSplit } from '$models/PaymentSplit';
import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit';
import { calculateNextExecutionDate } from '$lib/utils/recurring';
import { invalidateCospendCaches } from '$lib/server/cache';
// This endpoint is designed to be called by a cron job or external scheduler
// It processes all recurring payments that are due for execution
export const POST: RequestHandler = async ({ request }) => {
// Optional: Add basic authentication or API key validation here
const authHeader = request.headers.get('authorization');
const expectedToken = process.env.CRON_API_TOKEN;
if (expectedToken && authHeader !== `Bearer ${expectedToken}`) {
throw error(401, 'Unauthorized');
}
await dbConnect();
try {
const now = new Date();
// Find all active recurring payments that are due
const duePayments = await RecurringPayment.find({
isActive: true,
nextExecutionDate: { $lte: now },
$or: [
{ endDate: { $exists: false } },
{ endDate: null },
{ endDate: { $gte: now } }
]
});
const results = [];
let successCount = 0;
let failureCount = 0;
for (const recurringPayment of duePayments) {
try {
// Create the payment
const payment = await Payment.create({
title: `${recurringPayment.title} (Auto)`,
description: `Automatically generated from recurring payment: ${recurringPayment.description || 'No description'}`,
amount: recurringPayment.amount,
currency: recurringPayment.currency,
paidBy: recurringPayment.paidBy,
date: now,
category: recurringPayment.category,
splitMethod: recurringPayment.splitMethod,
createdBy: recurringPayment.createdBy
});
// Create payment splits
const splitPromises = recurringPayment.splits.map((split) => {
return PaymentSplit.create({
paymentId: payment._id,
username: split.username,
amount: split.amount,
proportion: split.proportion,
personalAmount: split.personalAmount
});
});
await Promise.all(splitPromises);
// Invalidate caches for all affected users
const affectedUsernames = recurringPayment.splits.map((split) => split.username);
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
// Calculate next execution date
const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now);
// Update the recurring payment
await RecurringPayment.findByIdAndUpdate(recurringPayment._id, {
lastExecutionDate: now,
nextExecutionDate: nextExecutionDate
});
successCount++;
results.push({
recurringPaymentId: recurringPayment._id,
paymentId: payment._id,
title: recurringPayment.title,
amount: recurringPayment.amount,
nextExecution: nextExecutionDate,
success: true
});
} catch (paymentError) {
console.error(`[Cron] Error processing recurring payment ${recurringPayment._id}:`, paymentError);
failureCount++;
results.push({
recurringPaymentId: recurringPayment._id,
title: recurringPayment.title,
amount: recurringPayment.amount,
success: false,
error: paymentError instanceof Error ? paymentError.message : 'Unknown error'
});
}
}
return json({
success: true,
timestamp: now.toISOString(),
processed: duePayments.length,
successful: successCount,
failed: failureCount,
results: results
});
} catch (e) {
console.error('[Cron] Error executing recurring payments:', e);
throw error(500, 'Failed to execute recurring payments');
} finally {
// Connection will be reused
}
};

View File

@@ -1,70 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '$models/UserFavorites';
import { Recipe } from '$models/Recipe';
import { dbConnect } 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) {
return json([]);
}
// Get recipes that are favorited AND have approved English translations
let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites },
'translations.en.translationStatus': 'approved'
}).lean();
// Transform to English format
const englishRecipes = recipes.map(recipe => ({
_id: recipe._id,
short_name: recipe.translations.en.short_name,
name: recipe.translations.en.name,
category: recipe.translations.en.category,
icon: recipe.icon,
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
images: recipe.images?.map((img, idx) => ({
mediapath: img.mediapath,
alt: recipe.translations.en.images?.[idx]?.alt || img.alt,
caption: recipe.translations.en.images?.[idx]?.caption || img.caption,
})),
description: recipe.translations.en.description,
note: recipe.translations.en.note,
tags: recipe.translations.en.tags || [],
season: recipe.season,
baking: recipe.baking,
preparation: recipe.preparation,
fermentation: recipe.fermentation,
portions: recipe.portions,
cooking: recipe.cooking,
total_time: recipe.total_time,
ingredients: recipe.translations.en.ingredients || [],
instructions: recipe.translations.en.instructions || [],
preamble: recipe.translations.en.preamble,
addendum: recipe.translations.en.addendum,
germanShortName: recipe.short_name,
translationStatus: recipe.translations.en.translationStatus
}));
const result = JSON.parse(JSON.stringify(englishRecipes));
return json(result);
} catch (e) {
throw error(500, 'Failed to fetch favorite recipes');
}
};

View File

@@ -1,149 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { error } from '@sveltejs/kit';
/**
* GET /api/recipes/items/[name]
* Fetch an English recipe by its English short_name
*/
export const GET: RequestHandler = async ({ params }) => {
await dbConnect();
try {
// Find recipe by English short_name and populate base recipe references
const recipe = await Recipe.findOne({
"translations.en.short_name": params.name
})
.populate({
path: 'translations.en.ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients instructions translations'
}
}
})
.populate({
path: 'translations.en.instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name ingredients instructions translations'
}
}
})
.lean();
if (!recipe) {
throw error(404, 'Recipe not found');
}
if (!recipe.translations?.en) {
throw error(404, 'English translation not available for this recipe');
}
// Return English translation with necessary metadata
let englishRecipe: any = {
_id: recipe._id,
short_name: recipe.translations.en.short_name,
name: recipe.translations.en.name,
description: recipe.translations.en.description,
preamble: recipe.translations.en.preamble || '',
addendum: recipe.translations.en.addendum || '',
note: recipe.translations.en.note || '',
category: recipe.translations.en.category,
tags: recipe.translations.en.tags || [],
ingredients: recipe.translations.en.ingredients || [],
instructions: recipe.translations.en.instructions || [],
images: recipe.images || [], // Use original images with full paths, but English alt/captions
// Use English translations for timing/metadata fields when available, fallback to German version
icon: recipe.icon || '',
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
season: recipe.season || [],
baking: recipe.translations.en.baking || recipe.baking || { temperature: '', length: '', mode: '' },
preparation: recipe.translations.en.preparation || recipe.preparation || '',
fermentation: recipe.translations.en.fermentation || recipe.fermentation || { bulk: '', final: '' },
portions: recipe.translations.en.portions || recipe.portions || '',
cooking: recipe.translations.en.cooking || recipe.cooking || '',
total_time: recipe.translations.en.total_time || recipe.total_time || '',
// Include translation status for display
translationStatus: recipe.translations.en.translationStatus,
// Include German short_name for language switcher
germanShortName: recipe.short_name,
};
// Recursively map populated base recipe refs to resolvedRecipe field
function mapBaseRecipeRefs(items: any[]): any[] {
return items.map((item: any) => {
if (item.type === 'reference' && item.baseRecipeRef) {
const resolvedRecipe = { ...item.baseRecipeRef };
// Recursively map nested baseRecipeRefs
if (resolvedRecipe.ingredients) {
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
}
if (resolvedRecipe.instructions) {
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
}
return { ...item, resolvedRecipe };
}
return item;
});
}
if (englishRecipe.ingredients) {
englishRecipe.ingredients = mapBaseRecipeRefs(englishRecipe.ingredients);
}
if (englishRecipe.instructions) {
englishRecipe.instructions = mapBaseRecipeRefs(englishRecipe.instructions);
}
// Merge English alt/caption with original image paths
// Handle both array and single object (there's a bug in add page that sometimes saves as object)
const imagesArray = Array.isArray(recipe.images) ? recipe.images : (recipe.images ? [recipe.images] : []);
if (imagesArray.length > 0) {
const translatedImages = recipe.translations.en.images || [];
if (translatedImages.length > 0) {
englishRecipe.images = imagesArray.map((img: any, index: number) => ({
mediapath: img.mediapath,
alt: translatedImages[index]?.alt || img.alt || '',
caption: translatedImages[index]?.caption || img.caption || '',
}));
} else {
// No translated image captions, use German ones
englishRecipe.images = imagesArray.map((img: any) => ({
mediapath: img.mediapath,
alt: img.alt || '',
caption: img.caption || '',
}));
}
}
return new Response(JSON.stringify(englishRecipe), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (err: any) {
console.error('Error fetching English recipe:', err);
if (err.status) {
throw err;
}
throw error(500, 'Failed to fetch recipe');
}
};

View File

@@ -1,35 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe'
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Find all recipes that have approved English translations
const recipes = await Recipe.find(
{ 'translations.en.translationStatus': 'approved' },
'_id translations.en short_name season dateModified icon images'
).lean();
// Map to brief format with English data
// Only include first image's alt and mediapath to reduce payload
const found_brief = recipes.map((recipe: any) => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season || [],
dateModified: recipe.dateModified,
germanShortName: recipe.short_name, // For language switcher
images: recipe.images?.[0]
? [{ alt: recipe.images[0].alt, mediapath: recipe.images[0].mediapath }]
: []
})) as BriefRecipeType[];
return json(JSON.parse(JSON.stringify(rand_array(found_brief))));
};

View File

@@ -1,14 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Get distinct categories from approved English translations
const categories = await Recipe.distinct('translations.en.category', {
'translations.en.translationStatus': 'approved'
}).lean();
return json(JSON.parse(JSON.stringify(categories)));
};

View File

@@ -1,35 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Find recipes in this category that have approved English translations
const recipes = await Recipe.find(
{
'translations.en.category': params.category,
'translations.en.translationStatus': 'approved'
},
'_id translations.en short_name images season dateModified icon'
).lean();
// Map to brief format with English data
const englishRecipes = recipes.map((recipe: any) => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
images: recipe.images || [],
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season || [],
dateModified: recipe.dateModified,
germanShortName: recipe.short_name
})) as BriefRecipeType[];
return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
};

View File

@@ -1,35 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Find recipes with this icon that have approved English translations
const recipes = await Recipe.find(
{
icon: params.icon,
'translations.en.translationStatus': 'approved'
},
'_id translations.en short_name images season dateModified icon'
).lean();
// Map to brief format with English data
const englishRecipes = recipes.map((recipe: any) => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
images: recipe.images || [],
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season || [],
dateModified: recipe.dateModified,
germanShortName: recipe.short_name
})) as BriefRecipeType[];
return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
};

View File

@@ -1,35 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe'
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Find recipes in season that have approved English translations
const recipes = await Recipe.find(
{
season: params.month,
icon: {$ne: "🍽️"},
'translations.en.translationStatus': 'approved'
},
'_id translations.en short_name images season dateModified icon'
).lean();
// Map to format with English data
const found_in_season = recipes.map((recipe: any) => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
images: recipe.images || [],
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season || [],
dateModified: recipe.dateModified,
germanShortName: recipe.short_name // For language switcher
}));
return json(JSON.parse(JSON.stringify(rand_array(found_in_season))));
};

View File

@@ -1,24 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Get all recipes with approved English translations
const recipes = await Recipe.find({
'translations.en.translationStatus': 'approved'
}, 'translations.en.tags').lean();
// Extract and flatten all unique tags
const tagsSet = new Set<string>();
recipes.forEach(recipe => {
if (recipe.translations?.en?.tags) {
recipe.translations.en.tags.forEach((tag: string) => tagsSet.add(tag));
}
});
const tags = Array.from(tagsSet).sort();
return json(JSON.parse(JSON.stringify(tags)));
};

View File

@@ -1,35 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
// Find recipes with this tag that have approved English translations
const recipes = await Recipe.find(
{
'translations.en.tags': params.tag,
'translations.en.translationStatus': 'approved'
},
'_id translations.en short_name images season dateModified icon'
).lean();
// Map to brief format with English data
const englishRecipes = recipes.map((recipe: any) => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
images: recipe.images || [],
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season || [],
dateModified: recipe.dateModified,
germanShortName: recipe.short_name
})) as BriefRecipeType[];
return json(JSON.parse(JSON.stringify(rand_array(englishRecipes))));
};

View File

@@ -1,101 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
export const GET: RequestHandler = async ({ url, locals }) => {
await dbConnect();
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
const category = url.searchParams.get('category');
// Support both single tag (backwards compat) and multiple tags
const singleTag = url.searchParams.get('tag');
const multipleTags = url.searchParams.get('tags');
const tags = multipleTags
? multipleTags.split(',').map(t => t.trim()).filter(Boolean)
: (singleTag ? [singleTag] : []);
const icon = url.searchParams.get('icon');
// Support both single season (backwards compat) and multiple seasons
const singleSeason = url.searchParams.get('season');
const multipleSeasons = url.searchParams.get('seasons');
const seasons = multipleSeasons
? multipleSeasons.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
: (singleSeason ? [parseInt(singleSeason)].filter(n => !isNaN(n)) : []);
const favoritesOnly = url.searchParams.get('favorites') === 'true';
try {
// Build base query - only recipes with approved English translations
let dbQuery: any = {
'translations.en.translationStatus': 'approved'
};
// Apply filters based on context
if (category) {
dbQuery['translations.en.category'] = category;
}
// Multi-tag AND logic: recipe must have ALL selected tags
if (tags.length > 0) {
dbQuery['translations.en.tags'] = { $all: tags };
}
if (icon) {
dbQuery.icon = icon; // Icon is the same for both languages
}
// Multi-season OR logic: recipe in any selected season
if (seasons.length > 0) {
dbQuery.season = { $in: seasons }; // Season is the same for both languages
}
// Get all recipes matching base filters
let recipes = await Recipe.find(dbQuery).lean();
// Handle favorites filter
if (favoritesOnly && locals.session?.user) {
const { UserFavorites } = await import('../../../../models/UserFavorites');
const userFavorites = await UserFavorites.findOne({ username: locals.session.user.username });
if (userFavorites && userFavorites.favorites) {
const favoriteIds = userFavorites.favorites;
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
} else {
recipes = [];
}
}
// Transform to English brief format
let briefRecipes: BriefRecipeType[] = recipes.map(recipe => ({
_id: recipe._id,
name: recipe.translations.en.name,
short_name: recipe.translations.en.short_name,
tags: recipe.translations.en.tags || [],
category: recipe.translations.en.category,
icon: recipe.icon,
description: recipe.translations.en.description,
season: recipe.season,
dateModified: recipe.dateModified,
germanShortName: recipe.short_name
}));
// Apply text search if query provided
if (query) {
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
briefRecipes = briefRecipes.filter(recipe => {
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/&shy;|­/g, '');
return searchTerms.every(term => searchString.includes(term));
});
}
return json(JSON.parse(JSON.stringify(briefRecipes)));
} catch (error) {
return json({ error: 'Search failed' }, { status: 500 });
}
};

View File

@@ -1,37 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '$models/UserFavorites';
import { Recipe } from '$models/Recipe';
import { dbConnect } 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) {
return json([]);
}
let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites }
}).lean() as RecipeModelType[];
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
} catch (e) {
throw error(500, 'Failed to fetch favorite recipes');
}
};

View File

@@ -1,70 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {RecipeModelType} from '$types/types';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipe = await Recipe.findOne({ short_name: params.name})
.populate({
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations',
populate: {
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations'
}
}
})
.populate({
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations',
populate: {
path: 'instructions.baseRecipeRef',
select: 'short_name name instructions translations'
}
}
})
.lean() as RecipeModelType[];
recipe = JSON.parse(JSON.stringify(recipe));
if(recipe == null){
throw error(404, "Recipe not found")
}
// Recursively map populated refs to resolvedRecipe field
function mapBaseRecipeRefs(items: any[]): any[] {
return items.map((item: any) => {
if (item.type === 'reference' && item.baseRecipeRef) {
const resolvedRecipe = { ...item.baseRecipeRef };
// Recursively map nested baseRecipeRefs
if (resolvedRecipe.ingredients) {
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
}
if (resolvedRecipe.instructions) {
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
}
return { ...item, resolvedRecipe };
}
return item;
});
}
if (recipe?.ingredients) {
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
}
if (recipe?.instructions) {
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
}
return json(recipe);
};

View File

@@ -1,37 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe'
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
const cacheKey = 'recipes:all_brief';
// Try cache first
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
const dbRecipes = await Recipe.find({}, 'name short_name tags category icon description season dateModified images').lean() as BriefRecipeType[];
// Only include first image's alt and mediapath to reduce payload
recipes = dbRecipes.map(recipe => ({
...recipe,
images: recipe.images?.[0]
? [{ alt: recipe.images[0].alt, mediapath: recipe.images[0].mediapath }]
: []
})) as BriefRecipeType[];
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};

View File

@@ -1,12 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let categories = (await Recipe.distinct('category').lean());
categories= JSON.parse(JSON.stringify(categories));
return json(categories);
};

View File

@@ -1,13 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({category: params.category}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@@ -1,13 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({icon: params.icon}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@@ -1,29 +0,0 @@
import type {rand_array} from '$lib/js/randomize';
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe'
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
const cacheKey = `recipes:in_season:${params.month}`;
// Try cache first
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
recipes = await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean();
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};

View File

@@ -1,12 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let categories = (await Recipe.distinct('tags').lean());
categories= JSON.parse(JSON.stringify(categories));
return json(categories);
};

View File

@@ -1,29 +0,0 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import type {BriefRecipeType} from '$types/types';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
const cacheKey = `recipes:tag:${params.tag}`;
// Try cache first
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
recipes = await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean() as BriefRecipeType[];
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};