feat: split random into 'Was koche ich?' + 'Was backe ich?'
- Two dice buttons on homepage - Cook: mittag, abend, fruehstueck categories - Bake: backen, torten, desserts categories - Backend: /api/recipes/random?categories=slug1,slug2 filter - Luna's feature request
This commit is contained in:
@@ -4,7 +4,9 @@ import * as svc from '../services/recipe.service.js';
|
|||||||
|
|
||||||
export async function recipeRoutes(app: FastifyInstance) {
|
export async function recipeRoutes(app: FastifyInstance) {
|
||||||
app.get('/api/recipes/random', async (request, reply) => {
|
app.get('/api/recipes/random', async (request, reply) => {
|
||||||
const recipe = await svc.getRandomRecipe();
|
const { categories } = request.query as { categories?: string };
|
||||||
|
const categorySlugs = categories ? categories.split(',') : undefined;
|
||||||
|
const recipe = await svc.getRandomRecipe(categorySlugs);
|
||||||
if (!recipe) return reply.status(404).send({ error: 'No recipes found' });
|
if (!recipe) return reply.status(404).send({ error: 'No recipes found' });
|
||||||
return recipe;
|
return recipe;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -261,8 +261,18 @@ export async function toggleFavorite(id: string, userId?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRandomRecipe() {
|
export async function getRandomRecipe(categorySlugs?: string[]) {
|
||||||
const { rows } = await query('SELECT slug FROM recipes ORDER BY RANDOM() LIMIT 1');
|
let sql = 'SELECT r.slug FROM recipes r';
|
||||||
|
const params: string[] = [];
|
||||||
|
|
||||||
|
if (categorySlugs && categorySlugs.length > 0) {
|
||||||
|
sql += ' JOIN categories c ON r.category_id = c.id WHERE c.slug = ANY($1)';
|
||||||
|
params.push(categorySlugs as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY RANDOM() LIMIT 1';
|
||||||
|
|
||||||
|
const { rows } = await query(sql, params);
|
||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
return getRecipeBySlug(rows[0].slug);
|
return getRecipeBySlug(rows[0].slug);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ export function fetchRecipe(slug: string) {
|
|||||||
return apiFetch<Recipe>(`/recipes/${slug}`)
|
return apiFetch<Recipe>(`/recipes/${slug}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchRandomRecipe() {
|
export function fetchRandomRecipe(categories?: string[]) {
|
||||||
return apiFetch<Recipe>('/recipes/random')
|
const qs = categories ? `?categories=${categories.join(',')}` : ''
|
||||||
|
return apiFetch<Recipe>(`/recipes/random${qs}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchRecipes(q: string) {
|
export function searchRecipes(q: string) {
|
||||||
|
|||||||
@@ -48,13 +48,18 @@ export function HomePage() {
|
|||||||
? recipes.filter(r => r.tags?.includes(activeTag))
|
? recipes.filter(r => r.tags?.includes(activeTag))
|
||||||
: recipes
|
: recipes
|
||||||
|
|
||||||
const handleRandomRecipe = async () => {
|
const handleRandomCook = async () => {
|
||||||
try {
|
try {
|
||||||
const recipe = await fetchRandomRecipe()
|
const recipe = await fetchRandomRecipe(['mittag', 'abend', 'fruehstueck'])
|
||||||
if (recipe?.slug) navigate(`/recipe/${recipe.slug}?from=random`)
|
if (recipe?.slug) navigate(`/recipe/${recipe.slug}?from=random`)
|
||||||
} catch {
|
} catch { /* no recipes */ }
|
||||||
// no recipes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRandomBake = async () => {
|
||||||
|
try {
|
||||||
|
const recipe = await fetchRandomRecipe(['backen', 'torten', 'desserts'])
|
||||||
|
if (recipe?.slug) navigate(`/recipe/${recipe.slug}?from=random`)
|
||||||
|
} catch { /* no recipes */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTags = (tags || []).filter(t => t.recipe_count > 0)
|
const activeTags = (tags || []).filter(t => t.recipe_count > 0)
|
||||||
@@ -80,15 +85,22 @@ export function HomePage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Random Recipe Button */}
|
{/* Random Recipe Buttons */}
|
||||||
{totalCount > 1 && (
|
{totalCount > 1 && (
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4 pb-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleRandomRecipe}
|
onClick={handleRandomCook}
|
||||||
className="w-full bg-gradient-to-r from-primary to-secondary text-white px-4 py-3 rounded-2xl font-medium text-sm flex items-center justify-center gap-2 shadow-sm active:scale-[0.98] transition-transform min-h-[44px]"
|
className="flex-1 bg-gradient-to-r from-primary to-secondary text-white px-3 py-3 rounded-2xl font-medium text-sm flex items-center justify-center gap-2 shadow-sm active:scale-[0.98] transition-transform min-h-[44px]"
|
||||||
>
|
>
|
||||||
<Dices size={18} />
|
<Dices size={16} />
|
||||||
Was koche ich heute? 🎲
|
Was koche ich? 🥘
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRandomBake}
|
||||||
|
className="flex-1 bg-gradient-to-r from-secondary to-primary text-white px-3 py-3 rounded-2xl font-medium text-sm flex items-center justify-center gap-2 shadow-sm active:scale-[0.98] transition-transform min-h-[44px]"
|
||||||
|
>
|
||||||
|
<Dices size={16} />
|
||||||
|
Was backe ich? 🎂
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user