chore: release v0.25.3

This commit is contained in:
Ulas Kalayci
2026-04-26 18:24:27 +02:00
parent 1cc1b63745
commit 9fba1d7ae4
26 changed files with 242 additions and 120 deletions
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "التقويم",
+2 -1
View File
@@ -264,7 +264,8 @@
"savedRecipeLabel": "Gespeichertes Rezept",
"savedRecipePlaceholder": "Rezept auswählen",
"saveAsRecipe": "Als Rezept speichern",
"recipeScaleLabel": "Zutaten skalieren"
"recipeScaleLabel": "Zutaten skalieren",
"deletedToast": "Mahlzeit gelöscht"
},
"calendar": {
"title": "Kalender",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Ημερολόγιο",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Calendar",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Calendario",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Calendrier",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "कैलेंडर",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Calendario",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "カレンダー",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Calendário",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Календарь",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Sparat recept",
"savedRecipePlaceholder": "Välj recept",
"saveAsRecipe": "Spara som recept",
"recipeScaleLabel": "Skala ingredienser"
"recipeScaleLabel": "Skala ingredienser",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Kalender",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Takvim",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "Календар",
+2 -1
View File
@@ -258,7 +258,8 @@
"savedRecipeLabel": "Saved recipe",
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients"
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
},
"calendar": {
"title": "日历",
+30 -13
View File
@@ -6,7 +6,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate, getLocale } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -660,18 +660,35 @@ function openBudgetModal({ mode, entry = null }) {
// --------------------------------------------------------
async function deleteEntry(id) {
if (!await confirmModal(t('budget.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/budget/${id}`);
state.entries = state.entries.filter((e) => e.id !== id);
const sumRes = await api.get(`/budget/summary?month=${state.month}`);
state.summary = sumRes.data;
renderBody();
vibrate([30, 50, 30]);
window.oikos?.showToast(t('budget.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
}
const entry = state.entries.find((e) => e.id === id);
state.entries = state.entries.filter((e) => e.id !== id);
renderBody();
vibrate([30, 50, 30]);
let undone = false;
window.oikos?.showToast(t('budget.deletedToast'), 'default', 5000, () => {
undone = true;
if (entry) {
state.entries = [...state.entries, entry].sort((a, b) => new Date(b.date) - new Date(a.date));
renderBody();
}
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/budget/${id}`);
const sumRes = await api.get(`/budget/summary?month=${state.month}`);
state.summary = sumRes.data;
renderBody();
} catch (err) {
if (entry) {
state.entries = [...state.entries, entry].sort((a, b) => new Date(b.date) - new Date(a.date));
renderBody();
}
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
// --------------------------------------------------------
+28 -13
View File
@@ -6,7 +6,7 @@
import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { stagger } from '/utils/ux.js';
import { t, formatTime } from '/i18n.js';
import { esc, fmtLocation } from '/utils/html.js';
@@ -764,7 +764,6 @@ function showEventPopup(ev, anchor) {
});
popup.querySelector('#popup-delete').addEventListener('click', async () => {
if (!await confirmModal(t('calendar.deleteConfirm', { title: ev.title }), { danger: true, confirmLabel: t('common.delete') })) return;
popup.remove();
await deleteEvent(ev.id);
});
@@ -868,7 +867,6 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
if (!await confirmModal(t('calendar.deleteConfirm', { title: event.title }), { danger: true, confirmLabel: t('common.delete') })) return;
closeModal();
await deleteEvent(event.id);
});
@@ -1073,15 +1071,32 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null) {
}
async function deleteEvent(id) {
try {
await api.delete(`/calendar/${id}`);
api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {});
refreshReminders();
state.events = state.events.filter((e) => e.id !== id);
renderView();
window.oikos?.showToast(t('calendar.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'error');
}
const event = state.events.find((e) => e.id === id);
state.events = state.events.filter((e) => e.id !== id);
renderView();
let undone = false;
window.oikos?.showToast(t('calendar.deletedToast'), 'default', 5000, () => {
undone = true;
if (event) {
state.events = [...state.events, event];
renderView();
}
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/calendar/${id}`);
api.delete(`/reminders?entity_type=event&entity_id=${id}`).catch(() => {});
refreshReminders();
} catch (err) {
if (event) {
state.events = [...state.events, event];
renderView();
}
window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'danger');
}
}, 5000);
}
+27 -11
View File
@@ -5,7 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -350,16 +350,32 @@ function openContactModal({ mode, contact = null }) {
}
async function deleteContact(id) {
if (!await confirmModal(t('contacts.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/contacts/${id}`);
state.contacts = state.contacts.filter((c) => c.id !== id);
renderList();
vibrate([30, 50, 30]);
window.oikos?.showToast(t('contacts.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
}
const contact = state.contacts.find((c) => c.id === id);
state.contacts = state.contacts.filter((c) => c.id !== id);
renderList();
vibrate([30, 50, 30]);
let undone = false;
window.oikos?.showToast(t('contacts.deletedToast'), 'default', 5000, () => {
undone = true;
if (contact) {
state.contacts = [...state.contacts, contact].sort((a, b) => a.name.localeCompare(b.name));
renderList();
}
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/contacts/${id}`);
} catch (err) {
if (contact) {
state.contacts = [...state.contacts, contact].sort((a, b) => a.name.localeCompare(b.name));
renderList();
}
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
+22 -10
View File
@@ -5,7 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal as closeSharedModal, selectModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal as closeSharedModal, selectModal } from '/components/modal.js';
import { stagger } from '/utils/ux.js';
import { t, formatDate } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -920,15 +920,27 @@ function collectModalIngredients(overlay) {
// --------------------------------------------------------
async function deleteMeal(mealId) {
if (!await confirmModal(t('meals.deleteMeal') + '?', { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/meals/${mealId}`);
state.meals = state.meals.filter((m) => m.id !== mealId);
renderWeekGrid();
window.oikos?.showToast(t('meals.deleteMeal'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
}
const meal = state.meals.find((m) => m.id === mealId);
const itemEl = _container.querySelector(`.meal-slot--has-meal[data-meal-id="${mealId}"]`);
if (itemEl) itemEl.style.display = 'none';
let undone = false;
window.oikos?.showToast(t('meals.deletedToast'), 'default', 5000, () => {
undone = true;
if (itemEl) itemEl.style.display = '';
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/meals/${mealId}`);
state.meals = state.meals.filter((m) => m.id !== mealId);
renderWeekGrid();
} catch (err) {
if (itemEl) itemEl.style.display = '';
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
// --------------------------------------------------------
+28 -11
View File
@@ -5,7 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal, btnError, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal, btnError } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -476,16 +476,33 @@ async function togglePin(id) {
}
async function deleteNote(id) {
if (!await confirmModal(t('notes.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/notes/${id}`);
state.notes = state.notes.filter((n) => n.id !== id);
renderGrid();
vibrate([30, 50, 30]);
window.oikos?.showToast(t('notes.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
}
closeModal();
const note = state.notes.find((n) => n.id === id);
state.notes = state.notes.filter((n) => n.id !== id);
renderGrid();
vibrate([30, 50, 30]);
let undone = false;
window.oikos?.showToast(t('notes.deletedToast'), 'default', 5000, () => {
undone = true;
if (note) {
state.notes = [...state.notes, note].sort((a, b) => b.pinned - a.pinned);
renderGrid();
}
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/notes/${id}`);
} catch (err) {
if (note) {
state.notes = [...state.notes, note].sort((a, b) => b.pinned - a.pinned);
renderGrid();
}
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
// --------------------------------------------------------
+20 -14
View File
@@ -5,7 +5,7 @@
import { api } from '/api.js';
import { t } from '/i18n.js';
import { openModal as openSharedModal, closeModal as closeSharedModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
let _container = null;
@@ -134,6 +134,7 @@ function renderRecipeList() {
for (const recipe of state.recipes) {
const card = document.createElement('article');
card.className = 'recipe-card';
card.dataset.id = String(recipe.id);
const h = document.createElement('h2');
h.className = 'recipe-card__title';
@@ -370,21 +371,26 @@ async function saveRecipe(panel, mode, recipe) {
}
async function removeRecipe(recipe) {
const ok = await confirmModal(t('recipes.deleteConfirm', { title: recipe.title }), {
danger: true,
confirmLabel: t('common.delete'),
const itemEl = _container.querySelector(`.recipe-card[data-id="${recipe.id}"]`);
if (itemEl) itemEl.style.display = 'none';
let undone = false;
window.oikos?.showToast(t('recipes.deleted'), 'default', 5000, () => {
undone = true;
if (itemEl) itemEl.style.display = '';
});
if (!ok) return;
try {
await api.delete(`/recipes/${recipe.id}`);
state.recipes = state.recipes.filter((r) => r.id !== recipe.id);
renderRecipeList();
window.oikos?.showToast(t('recipes.deleted'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
}
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/recipes/${recipe.id}`);
state.recipes = state.recipes.filter((r) => r.id !== recipe.id);
renderRecipeList();
} catch (err) {
if (itemEl) itemEl.style.display = '';
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
async function duplicateRecipe(recipe) {
+25 -17
View File
@@ -8,7 +8,7 @@ import { api } from '/api.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
import { esc } from '/utils/html.js';
import { promptModal, confirmModal } from '/components/modal.js';
import { promptModal } from '/components/modal.js';
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
// --------------------------------------------------------
@@ -781,23 +781,31 @@ function wireListContentEvents(container) {
// ---- Liste löschen ----
if (action === 'delete-list') {
if (!await confirmModal(t('shopping.deleteListConfirm', { name: state.activeList?.name }), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/shopping/${state.activeListId}`);
state.lists = state.lists.filter((l) => l.id !== state.activeListId);
state.activeListId = state.lists[0]?.id ?? null;
if (state.activeListId) {
await switchList(state.activeListId, container);
} else {
state.items = [];
state.activeList = null;
renderTabs(container);
renderListContent(container);
const deletedListId = state.activeListId;
let undone = false;
window.oikos.showToast(t('shopping.deletedListToast'), 'default', 5000, () => {
undone = true;
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/shopping/${deletedListId}`);
state.lists = state.lists.filter((l) => l.id !== deletedListId);
state.activeListId = state.lists[0]?.id ?? null;
if (state.activeListId) {
await switchList(state.activeListId, container);
} else {
state.items = [];
state.activeList = null;
renderTabs(container);
renderListContent(container);
}
} catch (err) {
window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger');
}
window.oikos.showToast(t('shopping.deletedListToast'));
} catch (err) {
window.oikos.showToast(err.message, 'danger');
}
}, 5000);
}
});
+24 -13
View File
@@ -6,7 +6,7 @@
import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal, confirmModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate, formatTime } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -582,18 +582,29 @@ async function handleFormSubmit(e, container) {
}
async function handleDeleteTask(id, container) {
if (!await confirmModal(t('tasks.deleteConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/tasks/${id}`);
// Erinnerungen für diese Aufgabe ebenfalls entfernen
api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {});
refreshReminders();
closeModal();
window.oikos.showToast(t('tasks.deletedToast'), 'default');
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
}
closeModal();
const itemEl = container.querySelector(`[data-task-id="${id}"]`);
if (itemEl) itemEl.style.display = 'none';
let undone = false;
window.oikos.showToast(t('tasks.deletedToast'), 'default', 5000, () => {
undone = true;
if (itemEl) itemEl.style.display = '';
});
setTimeout(async () => {
if (undone) return;
try {
await api.delete(`/tasks/${id}`);
// Erinnerungen für diese Aufgabe ebenfalls entfernen
api.delete(`/reminders?entity_type=task&entity_id=${id}`).catch(() => {});
refreshReminders();
await loadTasks(container);
} catch (err) {
if (itemEl) itemEl.style.display = '';
window.oikos.showToast(err.message ?? t('common.unknownError'), 'danger');
}
}, 5000);
}
async function handleAddSubtask(parentId, container) {