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:
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
160
src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts
Normal file
160
src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts
Normal 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);
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
23
src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts
Normal file
23
src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts
Normal 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)));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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(/­|/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 });
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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))));
|
||||
};
|
||||
@@ -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(/­|/g, '');
|
||||
|
||||
return searchTerms.every(term => searchString.includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
return json(JSON.parse(JSON.stringify(briefRecipes)));
|
||||
|
||||
} catch (error) {
|
||||
return json({ error: 'Search failed' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
Reference in New Issue
Block a user