feat: add recipes module with CRUD functionality and integrate with meals

- Implemented new recipes page with UI for managing recipes.
- Added REST API routes for recipes including create, read, update, and delete operations.
- Introduced database schema for recipes and recipe ingredients.
- Updated meals to link with recipes, allowing meals to reference specific recipes.
- Enhanced validation for recipe-related fields in meals.
- Added styles for the recipes page and components.
This commit is contained in:
Serhiy Bobrov
2026-04-21 13:43:42 +03:00
committed by Ulas Kalayci
parent 41467a84b6
commit 0b54fe255b
25 changed files with 1421 additions and 48 deletions
+20 -6
View File
@@ -7,7 +7,7 @@
import { createLogger } from '../logger.js';
import express from 'express';
import * as db from '../db.js';
import { str, oneOf, date, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT, DATE_RE } from '../middleware/validate.js';
import { str, oneOf, date, num, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT, DATE_RE } from '../middleware/validate.js';
const log = createLogger('Meals');
@@ -156,15 +156,21 @@ router.post('/', (req, res) => {
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false });
const vRecipeUrl = str(req.body.recipe_url, 'Rezept-URL', { max: MAX_TEXT, required: false });
const errors = collectErrors([vDate, vType, vTitle, vNotes, vRecipeUrl]);
const vRecipeId = num(req.body.recipe_id, 'Rezept-ID', { required: false });
const errors = collectErrors([vDate, vType, vTitle, vNotes, vRecipeUrl, vRecipeId]);
if (!req.body.meal_type) errors.push('Mahlzeit-Typ ist erforderlich.');
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
if (vRecipeId.value !== null) {
const recipeExists = db.get().prepare('SELECT id FROM recipes WHERE id = ?').get(vRecipeId.value);
if (!recipeExists) return res.status(400).json({ error: 'Rezept nicht gefunden.', code: 400 });
}
const meal = db.transaction(() => {
const result = db.get().prepare(`
INSERT INTO meals (date, meal_type, title, notes, recipe_url, created_by)
VALUES (?, ?, ?, ?, ?, ?)
`).run(vDate.value, vType.value, vTitle.value, vNotes.value, vRecipeUrl.value, req.session.userId);
INSERT INTO meals (date, meal_type, title, notes, recipe_url, recipe_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(vDate.value, vType.value, vTitle.value, vNotes.value, vRecipeUrl.value, vRecipeId.value, req.session.userId);
const mealId = result.lastInsertRowid;
@@ -217,16 +223,23 @@ router.put('/:id', (req, res) => {
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
if (req.body.notes !== undefined) checks.push(str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false }));
if (req.body.recipe_url !== undefined) checks.push(str(req.body.recipe_url, 'Rezept-URL', { max: MAX_TEXT, required: false }));
if (req.body.recipe_id !== undefined) checks.push(num(req.body.recipe_id, 'Rezept-ID', { required: false }));
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
if (req.body.recipe_id !== undefined && req.body.recipe_id !== null && req.body.recipe_id !== '') {
const recipeExists = db.get().prepare('SELECT id FROM recipes WHERE id = ?').get(req.body.recipe_id);
if (!recipeExists) return res.status(400).json({ error: 'Rezept nicht gefunden.', code: 400 });
}
db.get().prepare(`
UPDATE meals
SET date = COALESCE(?, date),
meal_type = COALESCE(?, meal_type),
title = COALESCE(?, title),
notes = ?,
recipe_url = ?
recipe_url = ?,
recipe_id = ?
WHERE id = ?
`).run(
req.body.date ?? null,
@@ -234,6 +247,7 @@ router.put('/:id', (req, res) => {
req.body.title?.trim() ?? null,
req.body.notes !== undefined ? (req.body.notes || null) : meal.notes,
req.body.recipe_url !== undefined ? (req.body.recipe_url || null) : meal.recipe_url,
req.body.recipe_id !== undefined ? (req.body.recipe_id || null) : meal.recipe_id,
id
);