feat(meals): add optional recipe link to meal cards (#18)
- New optional recipe_url field in the meal modal (below Notes) - Link icon appears on meal cards when a URL is set, opens in new tab - DB migration v6: ALTER TABLE meals ADD COLUMN recipe_url TEXT - API: recipe_url supported in POST /meals and PUT /meals/:id - i18n: new keys recipeUrlLabel, recipeUrlPlaceholder, openRecipe (de, en, sv, it)
This commit is contained in:
+24
-3
@@ -219,6 +219,13 @@ function renderSlot(date, type, mealsForDay) {
|
||||
<span class="meal-card__ingredients-count">${ingLabel}${esc(ingDoneLabel)}</span>
|
||||
</div>` : ''}
|
||||
<div class="meal-card__actions">
|
||||
${meal.recipe_url ? `<a class="meal-card__action-btn meal-card__action-btn--recipe"
|
||||
data-action="open-recipe"
|
||||
href="${esc(meal.recipe_url)}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="${t('meals.openRecipe')}"
|
||||
><i data-lucide="link" style="width:14px;height:14px;" aria-hidden="true"></i></a>` : ''}
|
||||
${canTransfer ? `<button class="meal-card__action-btn meal-card__action-btn--shopping"
|
||||
data-action="transfer-meal"
|
||||
data-meal-id="${meal.id}"
|
||||
@@ -270,6 +277,12 @@ function wireGrid(grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'open-recipe') {
|
||||
// Link öffnet sich nativ - nur Bubbling stoppen damit kein Edit-Modal aufgeht
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'edit-meal') {
|
||||
const mealId = parseInt(btn.dataset.mealId, 10);
|
||||
const meal = state.meals.find((m) => m.id === mealId);
|
||||
@@ -310,7 +323,7 @@ function wireDragDrop(grid) {
|
||||
grid.addEventListener('pointerdown', (e) => {
|
||||
const card = e.target.closest('.meal-card');
|
||||
if (!card) return;
|
||||
if (e.target.closest('[data-action="delete-meal"], [data-action="transfer-meal"]')) return;
|
||||
if (e.target.closest('[data-action="delete-meal"], [data-action="transfer-meal"], [data-action="open-recipe"]')) return;
|
||||
|
||||
const slot = card.closest('.meal-slot');
|
||||
if (!slot) return;
|
||||
@@ -573,6 +586,13 @@ function buildModalContent({ mode, date, mealType, meal }) {
|
||||
placeholder="${t('meals.notesPlaceholder')}">${esc(isEdit && meal.notes ? meal.notes : '')}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-recipe-url">${t('meals.recipeUrlLabel')}</label>
|
||||
<input type="url" class="form-input" id="modal-recipe-url"
|
||||
placeholder="${t('meals.recipeUrlPlaceholder')}"
|
||||
value="${esc(isEdit && meal.recipe_url ? meal.recipe_url : '')}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">${t('meals.ingredientsLabel')}</label>
|
||||
<div class="ingredient-list" id="ingredient-list">${ingRows}</div>
|
||||
@@ -623,6 +643,7 @@ async function saveModal(overlay) {
|
||||
const meal_type = overlay.querySelector('#modal-type').value;
|
||||
const title = overlay.querySelector('#modal-title').value.trim();
|
||||
const notes = overlay.querySelector('#modal-notes').value.trim() || null;
|
||||
const recipe_url = overlay.querySelector('#modal-recipe-url').value.trim() || null;
|
||||
|
||||
if (!title) {
|
||||
window.oikos?.showToast(t('meals.titleRequired'), 'error');
|
||||
@@ -643,11 +664,11 @@ async function saveModal(overlay) {
|
||||
const { mode, meal } = state.modal;
|
||||
|
||||
if (mode === 'create') {
|
||||
const res = await api.post('/meals', { date, meal_type, title, notes, ingredients });
|
||||
const res = await api.post('/meals', { date, meal_type, title, notes, recipe_url, ingredients });
|
||||
state.meals.push(res.data);
|
||||
} else {
|
||||
// Update meal meta
|
||||
await api.put(`/meals/${meal.id}`, { date, meal_type, title, notes });
|
||||
await api.put(`/meals/${meal.id}`, { date, meal_type, title, notes, recipe_url });
|
||||
|
||||
// Sync ingredients
|
||||
const existingIds = new Set((meal.ingredients ?? []).map((i) => i.id));
|
||||
|
||||
Reference in New Issue
Block a user