Unify calendar code
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m16s

This commit is contained in:
2026-04-20 23:44:18 +02:00
parent c43b3766cd
commit 08a275093c
7 changed files with 783 additions and 486 deletions

View File

@@ -1,10 +1,7 @@
import { INGREDIENTS, RECIPES, PRODUCTS, getProductsForIngredient } from '../data/catalog.js?v=9';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
addDays,
addMonths,
sameDay,
sameMonth,
startOfDay,
startOfWeekMonday,
} from '../services/dateUtils.js';
@@ -17,14 +14,17 @@ import {
import { dayHasAnyMeal, autoSelectProducts, saveLastProductSelection } from '../services/planIngredients.js?v=4';
import { loadPantry } from '../services/pantryShopping.js?v=2';
import {
bindCollapsibleCalendarSwipeGesture,
bindCalendarDayClicks,
bindCalendarHorizontalSwipe,
createCollapsibleCalendarHTML,
createCalendarTopbarHTML,
createCalendarWeekdayHeaderHTML,
formatCalendarPeriodLabel,
isCalendarOnToday,
renderCalendarGrid,
renderCollapsibleCalendar,
syncCalendarTodayButton,
} from './mealCalendar.js?v=14';
syncCollapsibleCalendarMode,
syncCollapsibleCalendarToggleIcon,
} from './mealCalendar.js?v=15';
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260417-116';
function esc(s) {
@@ -52,25 +52,38 @@ export function getMealPlanEditorHTML() {
</button>
</div>
</div>
<div id="mpe-cal-wrap" class="hidden relative z-[1] shrink-0 px-5 pt-3 pb-3 bg-[rgb(var(--app-bg-rgb))]" style="background:rgb(var(--app-bg-rgb)) !important; background-image:none !important;">
<div id="mpe-cal-wrap" class="hidden relative z-[2] shrink-0 px-5 pt-3 pb-3 bg-[rgb(var(--app-bg-rgb))]" style="background:rgb(var(--app-bg-rgb)) !important; background-image:none !important;">
<div id="mpe-top-shadow" class="pointer-events-none absolute inset-x-0 -bottom-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(var(--overlay-rgb),0.12), rgba(var(--overlay-rgb),0.03), rgba(var(--overlay-rgb),0));"></div>
<div id="mpe-cal-section" class="hidden">
${createCalendarTopbarHTML({
todayId: 'mpe-cal-today',
wrapperClass: 'mb-2 flex items-center justify-end gap-3',
})}
${createCalendarWeekdayHeaderHTML()}
<div id="mpe-cal-grid" class="grid grid-cols-7 gap-1.5"></div>
<button id="mpe-cal-toggle" type="button" class="w-full flex items-center justify-center py-1 mt-1 text-gray-400 hover:text-gray-600 transition-colors"><i id="mpe-cal-toggle-icon" class="fas fa-chevron-down text-[10px]"></i></button>
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mt-3 mb-2">Pora posiłku</p>
<div id="mpe-slot-chips" class="flex flex-wrap gap-1.5"></div>
${createCollapsibleCalendarHTML({
idPrefix: 'mpe-cal',
swipeZoneId: 'mpe-cal-swipe-zone',
weekWrapId: 'mpe-cal-week-wrap',
weekGridId: 'mpe-cal-week-grid',
monthWrapId: 'mpe-cal-month-wrap',
monthGridId: 'mpe-cal-month-grid',
toggleId: 'mpe-cal-toggle',
iconId: 'mpe-cal-toggle-icon',
weekWrapClass: 'overflow-x-hidden bg-[rgb(var(--app-bg-rgb))]',
monthWrapClass: 'bg-[rgb(var(--app-bg-rgb))]',
weekWrapStyle: 'overflow: hidden; max-height: 10rem; opacity: 1; padding-bottom: 0.25rem',
toggleClass: 'w-full flex items-center justify-center py-1 mt-1 text-gray-400 hover:text-gray-600 transition-colors',
})}
</div>
</div>
<div id="mpe-summary-wrap" class="relative z-[1] shrink-0 px-5 pb-3 bg-[rgb(var(--app-bg-rgb))]" style="background:rgb(var(--app-bg-rgb)) !important; background-image:none !important;">
<div id="mpe-nutrition-section"></div>
<div id="mpe-servings-row" class="mt-3"></div>
<div id="mpe-top-shadow" class="pointer-events-none absolute inset-x-0 -bottom-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(var(--overlay-rgb),0.12), rgba(var(--overlay-rgb),0.03), rgba(var(--overlay-rgb),0));"></div>
</div>
<div id="mpe-ing-scroll" class="flex-1 min-h-0 overflow-y-auto no-scrollbar px-5 bg-[rgb(var(--app-bg-rgb))]" style="background:rgb(var(--app-bg-rgb)) !important; background-image:none !important; padding-bottom:calc(1.5rem + env(safe-area-inset-bottom));">
<div id="mpe-slot-section" class="pt-3 pb-1 hidden">
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Pora posiłku</p>
<div id="mpe-slot-chips" class="flex flex-wrap gap-1.5"></div>
</div>
<div id="mpe-summary-wrap" class="relative z-[1] pt-3 pb-3 bg-[rgb(var(--app-bg-rgb))]" style="background:rgb(var(--app-bg-rgb)) !important; background-image:none !important;">
<div id="mpe-nutrition-section"></div>
<div id="mpe-servings-row" class="mt-3"></div>
</div>
<div id="mpe-ing-section" class="mb-4">
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Składniki</p>
<div id="mpe-ing-list" class="space-y-1.5"></div>
@@ -155,6 +168,18 @@ export function setupMealPlanEditor() {
/* ── Calendar ──────────────────────────────────── */
function resolveCalendarDayState(day, meta, plans = loadPlans(), today = startOfDay(new Date())) {
const isSelected = S.date && sameDay(day, S.date);
const isPast = day.getTime() < today.getTime();
return {
disabled: isPast && !isSelected,
dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
showIndicator: meta.mode === 'month'
? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
: dayHasAnyMeal(plans, day),
};
}
function renderCal() {
const wrap = document.getElementById('mpe-cal-wrap');
const sec = document.getElementById('mpe-cal-section');
@@ -169,45 +194,46 @@ export function setupMealPlanEditor() {
}
wrap.classList.remove('hidden');
sec.classList.remove('hidden');
const grid = document.getElementById('mpe-cal-grid');
const weekGrid = document.getElementById('mpe-cal-week-grid');
const monthGrid = document.getElementById('mpe-cal-month-grid');
const todayBtn = document.getElementById('mpe-cal-today');
const icon = document.getElementById('mpe-cal-toggle-icon');
if (!grid) return;
if (!weekGrid || !monthGrid) return;
const today = startOfDay(new Date());
const plans = loadPlans();
const mode = S.calExpanded ? 'month' : 'week';
if (icon) {
icon.className = S.calExpanded ? 'fas fa-chevron-up text-[10px]' : 'fas fa-chevron-down text-[10px]';
}
syncCollapsibleCalendarMode({
mode,
weekWrapEl: document.getElementById('mpe-cal-week-wrap'),
monthWrapEl: document.getElementById('mpe-cal-month-wrap'),
activePaddingBottom: '0.25rem',
});
syncCollapsibleCalendarToggleIcon(icon, mode);
syncCalendarTodayButton(
todayBtn,
isCalendarOnToday(mode, startOfWeekMonday(S.calDate), S.calDate, S.date),
S.date,
{
labelText: formatCalendarPeriodLabel(mode, S.calDate, S.calDate),
},
);
renderCalendarGrid({
gridEl: grid,
mode,
anchorDate: S.calDate,
renderCollapsibleCalendar({
weekGridEl: weekGrid,
monthGridEl: monthGrid,
weekAnchorDate: S.calDate,
monthAnchorDate: S.calDate,
selectedDate: S.date,
resolveDayState: (day, meta) => {
const isSelected = S.date && sameDay(day, S.date);
const isPast = day.getTime() < today.getTime();
return {
disabled: isPast && !isSelected,
dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
showIndicator: meta.mode === 'month'
? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
: dayHasAnyMeal(plans, day),
};
},
resolveDayState: (day, meta) => resolveCalendarDayState(day, meta, plans, today),
});
syncScrollShadows();
}
function renderSlots() {
const el = document.getElementById('mpe-slot-chips');
const sec = document.getElementById('mpe-slot-section');
if (sec) sec.classList.toggle('hidden', !S.showCal);
if (!el || !S.showCal) return;
const r = RECIPES[S.recipeId];
if (!r) return;
@@ -611,50 +637,39 @@ export function setupMealPlanEditor() {
});
}
document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm);
bindCalendarDayClicks(document.getElementById('mpe-cal-grid'), (date) => {
const selectCalendarDate = (date) => {
S.date = date;
S.calDate = new Date(date);
renderCal();
});
};
bindCalendarHorizontalSwipe(document.getElementById('mpe-cal-grid'), {
onPrev: () => {
if (!S.showCal) return;
S.calDate = S.calExpanded ? addMonths(S.calDate, -1) : addDays(S.calDate, -7);
renderCal();
bindCalendarDayClicks(document.getElementById('mpe-cal-week-grid'), selectCalendarDate);
bindCalendarDayClicks(document.getElementById('mpe-cal-month-grid'), selectCalendarDate);
bindCollapsibleCalendarSwipeGesture({
zoneEl: document.getElementById('mpe-cal-swipe-zone'),
weekWrapEl: document.getElementById('mpe-cal-week-wrap'),
monthWrapEl: document.getElementById('mpe-cal-month-wrap'),
getMode: () => (S.calExpanded ? 'month' : 'week'),
setMode: (mode) => {
S.calExpanded = mode === 'month';
},
onNext: () => {
if (!S.showCal) return;
S.calDate = S.calExpanded ? addMonths(S.calDate, 1) : addDays(S.calDate, 7);
renderCal();
getWeekAnchor: () => S.calDate,
setWeekAnchor: (date) => {
S.calDate = startOfDay(date);
},
renderGhost: (ghostGrid, direction) => {
if (!S.showCal) return false;
const sign = direction === 'prev' ? -1 : 1;
const mode = S.calExpanded ? 'month' : 'week';
const anchor = S.calExpanded
? addMonths(S.calDate, sign)
: addDays(S.calDate, 7 * sign);
const today = startOfDay(new Date());
const plans = loadPlans();
renderCalendarGrid({
gridEl: ghostGrid,
mode,
anchorDate: anchor,
selectedDate: S.date,
resolveDayState: (day, meta) => {
const isSelected = S.date && sameDay(day, S.date);
const isPast = day.getTime() < today.getTime();
return {
disabled: isPast && !isSelected,
dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
showIndicator: meta.mode === 'month'
? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
: dayHasAnyMeal(plans, day),
};
},
});
getMonthAnchor: () => S.calDate,
setMonthAnchor: (date) => {
S.calDate = startOfDay(date);
},
getSelectedDate: () => S.date,
setSelectedDate: (date) => {
S.date = startOfDay(date);
},
rerender: renderCal,
resolveDayState: (day, meta) => resolveCalendarDayState(day, meta),
selectOnNavigateOutside: false,
enableVerticalModeSwipe: false,
});
document.getElementById('mpe-cal-today')?.addEventListener('click', () => {
const today = startOfDay(new Date());
@@ -824,7 +839,10 @@ export function setupMealPlanEditor() {
if (e.target.closest('#mpe-add-btn')) {
S.addOpen = true; S.addQuery = '';
renderAddArea();
document.getElementById('mpe-add-search')?.focus();
requestAnimationFrame(() => {
document.getElementById('mpe-add-area')?.scrollIntoView({ behavior: 'smooth', block: 'end' });
document.getElementById('mpe-add-search')?.focus({ preventScroll: true });
});
return;
}