@@ -492,18 +528,16 @@ function renderDayContent(state, onMealRemoved = null) {
: `${escapeHtml(recipe.thumbLabel)}`}
-
${escapeHtml(recipe.title)}
${customDot}
-
+
${escapeHtml(recipe.title)}
${customDot}
+
${recipe.minutes} min
·
${entryN.kcal} kcal${servLabel}
-
@@ -545,11 +579,126 @@ function renderDayContent(state, onMealRemoved = null) {
}).join('');
bindMealEntrySwipe(state, onMealRemoved);
+ syncPendingMealRemovalTicker(state);
}
-function removeMealEntry(state, slotId, entryId) {
- const key = dateKey(state.selected);
- const day = state.plans[key];
+function getPendingMealRemovalKey(dayKey, slotId, entryId) {
+ return `${dayKey}::${slotId}::${entryId}`;
+}
+
+function getPendingMealRemoval(state, dayKey, slotId, entryId) {
+ if (!(state.pendingMealRemovals instanceof Map)) return null;
+ return state.pendingMealRemovals.get(getPendingMealRemovalKey(dayKey, slotId, entryId)) || null;
+}
+
+function getPendingMealRemovalProgress(state, dayKey, slotId, entryId) {
+ const pending = getPendingMealRemoval(state, dayKey, slotId, entryId);
+ if (!pending) return 0;
+ return Math.max(0, Math.min(1, (pending.expiresAt - Date.now()) / PLANNER_MEAL_PENDING_DELETE_MS));
+}
+
+function getPendingMealRemovalButtonStyle(progress) {
+ const clamped = Math.max(0, Math.min(1, progress));
+ const angle = Math.round(clamped * 360);
+ return `conic-gradient(from -90deg, rgba(243,198,191,0.96) 0deg, rgba(243,198,191,0.96) ${angle}deg, rgba(139,90,84,0.22) ${angle}deg, rgba(139,90,84,0.22) 360deg)`;
+}
+
+function stopPendingMealRemovalTicker(state) {
+ if (state.pendingMealRemovalTickerId) {
+ clearInterval(state.pendingMealRemovalTickerId);
+ state.pendingMealRemovalTickerId = null;
+ }
+}
+
+function refreshPendingMealRemovalButtons(state) {
+ const buttons = document.querySelectorAll('[data-pending-delete-progress]');
+ buttons.forEach((button) => {
+ const dayKey = button.getAttribute('data-day-key');
+ const slotId = button.getAttribute('data-slot-id');
+ const entryId = button.getAttribute('data-entry-id');
+ if (!dayKey || !slotId || !entryId) return;
+ const progress = getPendingMealRemovalProgress(state, dayKey, slotId, entryId);
+ button.style.background = getPendingMealRemovalButtonStyle(progress);
+ });
+}
+
+function syncPendingMealRemovalTicker(state) {
+ const hasPending = state.pendingMealRemovals instanceof Map && state.pendingMealRemovals.size > 0;
+ if (!hasPending) {
+ stopPendingMealRemovalTicker(state);
+ return;
+ }
+
+ refreshPendingMealRemovalButtons(state);
+ if (state.pendingMealRemovalTickerId) return;
+
+ state.pendingMealRemovalTickerId = window.setInterval(() => {
+ syncPendingMealRemovals(state);
+ refreshPendingMealRemovalButtons(state);
+ if (!(state.pendingMealRemovals instanceof Map) || state.pendingMealRemovals.size === 0) {
+ stopPendingMealRemovalTicker(state);
+ }
+ }, PLANNER_MEAL_PENDING_DELETE_TICK_MS);
+}
+
+function clearPendingMealRemoval(state, pendingKey) {
+ if (!(state.pendingMealRemovals instanceof Map)) return false;
+ const pending = state.pendingMealRemovals.get(pendingKey);
+ if (!pending) return false;
+ clearTimeout(pending.timeoutId);
+ state.pendingMealRemovals.delete(pendingKey);
+ if (state.pendingMealRemovals.size === 0) stopPendingMealRemovalTicker(state);
+ return true;
+}
+
+function syncPendingMealRemovals(state) {
+ if (!(state.pendingMealRemovals instanceof Map) || state.pendingMealRemovals.size === 0) return;
+
+ for (const [pendingKey, pending] of state.pendingMealRemovals.entries()) {
+ const arr = state.plans[pending.dayKey]?.[pending.slotId];
+ const stillExists = Array.isArray(arr) && arr.some((entry) => entry && entry.id === pending.entryId);
+ if (!stillExists) clearPendingMealRemoval(state, pendingKey);
+ }
+}
+
+function queueMealEntryRemoval(state, dayKey, slotId, entryId, onMealRemoved = null) {
+ if (!dayKey || !slotId || !entryId) return false;
+
+ if (!(state.pendingMealRemovals instanceof Map)) state.pendingMealRemovals = new Map();
+ const pendingKey = getPendingMealRemovalKey(dayKey, slotId, entryId);
+ if (state.pendingMealRemovals.has(pendingKey)) return false;
+
+ const timeoutId = window.setTimeout(() => {
+ state.pendingMealRemovals.delete(pendingKey);
+ const removed = removeMealEntry(state, dayKey, slotId, entryId);
+ if (typeof onMealRemoved === 'function') onMealRemoved();
+ else {
+ if (removed) savePlans(state.plans);
+ renderDayContent(state);
+ }
+ if (!(state.pendingMealRemovals instanceof Map) || state.pendingMealRemovals.size === 0) {
+ stopPendingMealRemovalTicker(state);
+ }
+ }, PLANNER_MEAL_PENDING_DELETE_MS);
+
+ state.pendingMealRemovals.set(pendingKey, {
+ dayKey,
+ slotId,
+ entryId,
+ expiresAt: Date.now() + PLANNER_MEAL_PENDING_DELETE_MS,
+ timeoutId,
+ });
+
+ return true;
+}
+
+function cancelQueuedMealEntryRemoval(state, dayKey, slotId, entryId) {
+ if (!dayKey || !slotId || !entryId) return false;
+ return clearPendingMealRemoval(state, getPendingMealRemovalKey(dayKey, slotId, entryId));
+}
+
+function removeMealEntry(state, dayKey, slotId, entryId) {
+ const day = state.plans[dayKey];
const arr = day?.[slotId];
if (!Array.isArray(arr) || !entryId) return false;
@@ -559,7 +708,7 @@ function removeMealEntry(state, slotId, entryId) {
if (next.length === 0) delete day[slotId];
else day[slotId] = next;
- if (Object.keys(day).length === 0) delete state.plans[key];
+ if (Object.keys(day).length === 0) delete state.plans[dayKey];
return true;
}
@@ -628,24 +777,13 @@ function bindMealEntrySwipe(state, onMealRemoved = null) {
stopTracking();
if (shouldDelete) {
+ const dayKey = dateKey(state.selected);
const slotId = card.getAttribute('data-slot-id');
const entryId = card.getAttribute('data-entry-id');
- row.style.pointerEvents = 'none';
- row.style.setProperty('--planner-swipe-progress', '1');
- card.style.willChange = '';
- card.style.transition = 'transform 160ms cubic-bezier(0.32, 0.72, 0, 1), opacity 160ms ease';
- card.style.transform = 'translateX(-120%)';
- card.style.opacity = '0';
- window.setTimeout(() => {
- if (removeMealEntry(state, slotId, entryId)) {
- if (typeof onMealRemoved === 'function') onMealRemoved();
- else {
- savePlans(state.plans);
- renderDayContent(state);
- }
- showPlannerToast('Usunięto posiłek z planu dnia');
- }
- }, 160);
+ resetVisual();
+ if (queueMealEntryRemoval(state, dayKey, slotId, entryId, onMealRemoved)) {
+ renderDayContent(state, onMealRemoved);
+ }
return;
}
@@ -672,6 +810,7 @@ function bindMealEntrySwipe(state, onMealRemoved = null) {
card.addEventListener('pointerdown', (e) => {
if (pointerId !== null) return;
if (e.pointerType === 'mouse' && e.button !== 0) return;
+ if (row.getAttribute('data-pending-delete') === 'true') return;
pointerId = e.pointerId;
startX = e.clientX;
@@ -975,6 +1114,8 @@ export function setupMealPlanner() {
monthAnchor: startOfDay(new Date()),
selected: startOfDay(new Date()),
plans,
+ pendingMealRemovals: new Map(),
+ pendingMealRemovalTickerId: null,
pickerSlot: null,
};
@@ -1102,6 +1243,17 @@ export function setupMealPlanner() {
persist();
return;
}
+ const cancelPendingRemoveBtn = e.target.closest('.planner-cancel-pending-remove');
+ if (cancelPendingRemoveBtn) {
+ const dayKey = cancelPendingRemoveBtn.getAttribute('data-day-key');
+ const slotId = cancelPendingRemoveBtn.getAttribute('data-slot-id');
+ const entryId = cancelPendingRemoveBtn.getAttribute('data-entry-id');
+ if (!dayKey || !slotId || !entryId) return;
+ if (cancelQueuedMealEntryRemoval(state, dayKey, slotId, entryId)) {
+ renderDayContent(state, persist);
+ }
+ return;
+ }
const openRecipe = e.target.closest('.planner-open-recipe');
if (openRecipe) {
const recipeId = openRecipe.getAttribute('data-recipe-id');