refactor: unify recipe routes into [recipeLang] slug with full bilingual support
Consolidate /rezepte and /recipes routes into single [recipeLang] structure to eliminate code duplication. All pages now use conditional API routing and reactive labels based on language parameter. - Merge duplicate route structures into /[recipeLang] with 404 for invalid slugs - Add English API endpoints for search, favorites, tags, and categories - Implement language dropdown in header with localStorage persistence - Convert all pages to use Svelte 5 runes (, , ) - Add German-only redirects (301) for add/edit pages - Make all view pages (list, detail, filters, search, favorites) fully bilingual - Remove floating language switcher in favor of header dropdown
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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 English translations
|
||||
let recipes = await Recipe.find({
|
||||
_id: { $in: userFavorites.favorites },
|
||||
'translations.en': { $exists: true }
|
||||
}).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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
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 English translations
|
||||
const categories = await Recipe.distinct('translations.en.category', {
|
||||
'translations.en': { $exists: true }
|
||||
}).lean();
|
||||
|
||||
return json(JSON.parse(JSON.stringify(categories)));
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
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 English translations
|
||||
const recipes = await Recipe.find({
|
||||
'translations.en': { $exists: true }
|
||||
}, '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)));
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
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');
|
||||
const tag = url.searchParams.get('tag');
|
||||
const icon = url.searchParams.get('icon');
|
||||
const season = url.searchParams.get('season');
|
||||
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||
|
||||
try {
|
||||
// Build base query - only recipes with English translations
|
||||
let dbQuery: any = {
|
||||
'translations.en': { $exists: true }
|
||||
};
|
||||
|
||||
// Apply filters based on context
|
||||
if (category) {
|
||||
dbQuery['translations.en.category'] = category;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
dbQuery['translations.en.tags'] = { $in: [tag] };
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
dbQuery.icon = icon; // Icon is the same for both languages
|
||||
}
|
||||
|
||||
if (season) {
|
||||
const seasonNum = parseInt(season);
|
||||
if (!isNaN(seasonNum)) {
|
||||
dbQuery.season = { $in: [seasonNum] }; // 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 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user