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:
clawd
2026-02-18 18:13:03 +00:00
parent 03f3893c2c
commit c5774e8c8d
4 changed files with 41 additions and 16 deletions

View File

@@ -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;
}); });

View File

@@ -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);
} }

View File

@@ -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) {

View File

@@ -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>
)} )}