Rework calendar
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
addDays,
|
||||
addMonths,
|
||||
addWeeks,
|
||||
sameDay,
|
||||
sameMonth,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
@@ -26,12 +25,20 @@ import {
|
||||
newPlanEntryId,
|
||||
savePlans,
|
||||
} from '../services/planStore.js';
|
||||
import {
|
||||
CALENDAR_HANDLE_CLASS,
|
||||
CALENDAR_MONTHS_SHORT,
|
||||
bindCalendarDayClicks,
|
||||
createCalendarTopbarHTML,
|
||||
createCalendarWeekdayHeaderHTML,
|
||||
formatCalendarMonthYear,
|
||||
formatCalendarSelectedDate,
|
||||
isCalendarOnToday,
|
||||
renderCollapsibleCalendar,
|
||||
syncCalendarTodayButton,
|
||||
syncCollapsibleCalendarMode,
|
||||
} from '../ui/mealCalendar.js?v=1';
|
||||
|
||||
const MONTHS_SHORT = [
|
||||
'sty', 'lut', 'mar', 'kwi', 'maj', 'cze',
|
||||
'lip', 'sie', 'wrz', 'paź', 'lis', 'gru',
|
||||
];
|
||||
const WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd'];
|
||||
const WEEKDAYS_LONG = [
|
||||
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
|
||||
];
|
||||
@@ -44,58 +51,34 @@ function recipesForSlot(slotId) {
|
||||
return Object.values(RECIPES).filter((r) => r.allowedSlots.includes(slotId));
|
||||
}
|
||||
|
||||
function isCalendarOnToday(mode, weekStart, monthAnchor, selected) {
|
||||
const today = startOfDay(new Date());
|
||||
if (!sameDay(selected, today)) return false;
|
||||
if (mode === 'week') return weekContains(weekStart, today);
|
||||
return sameMonth(monthAnchor, today);
|
||||
}
|
||||
|
||||
function syncTodayButton(mode, weekStart, monthAnchor, selected) {
|
||||
const btn = document.getElementById('cal-go-today');
|
||||
if (!btn) return;
|
||||
const onToday = isCalendarOnToday(mode, weekStart, monthAnchor, selected);
|
||||
const active = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors';
|
||||
const dim = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-100 bg-gray-50 px-2 text-[10px] font-semibold text-gray-400 shadow-none cursor-default transition-colors';
|
||||
btn.className = onToday ? dim : active;
|
||||
btn.disabled = onToday;
|
||||
syncCalendarTodayButton(
|
||||
document.getElementById('cal-go-today'),
|
||||
isCalendarOnToday(mode, weekStart, monthAnchor, selected),
|
||||
);
|
||||
}
|
||||
|
||||
export function getMealPlannerHTML() {
|
||||
return `
|
||||
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-gray-50 z-10 pb-24">
|
||||
<div class="shrink-0 bg-white border-b border-gray-200 mt-3">
|
||||
<div class="px-3 pt-2 pb-1.5 flex items-center gap-1">
|
||||
<button type="button" id="cal-prev" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors" aria-label="Poprzedni okres">
|
||||
<i class="fas fa-chevron-left text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<p id="cal-period-label" class="flex-1 min-w-0 text-xs font-medium text-gray-900 text-center tabular-nums leading-none px-1 truncate"></p>
|
||||
<button type="button" id="cal-next" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors" aria-label="Następny okres">
|
||||
<i class="fas fa-chevron-right text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-3 pb-2 flex items-center justify-center">
|
||||
<button type="button" id="cal-go-today" title="Dziś" aria-label="Przejdź do dzisiejszego dnia"
|
||||
class="h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors">
|
||||
<i class="fas fa-calendar-day text-[9px] opacity-70" aria-hidden="true"></i>
|
||||
Dziś
|
||||
</button>
|
||||
</div>
|
||||
<div id="calendar-swipe-zone" style="touch-action: pan-x">
|
||||
<div id="calendar-week-wrap" class="px-3 pb-1" style="overflow: hidden; max-height: 10rem; opacity: 1">
|
||||
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
|
||||
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
|
||||
</div>
|
||||
<div id="calendar-week-grid" class="grid grid-cols-7 gap-0.5"></div>
|
||||
${createCalendarTopbarHTML({
|
||||
titleId: 'cal-period-label',
|
||||
prevId: 'cal-prev',
|
||||
todayId: 'cal-go-today',
|
||||
nextId: 'cal-next',
|
||||
})}
|
||||
<div id="calendar-swipe-zone" class="overflow-x-hidden" style="touch-action: none">
|
||||
<div id="calendar-week-wrap" class="px-3 overflow-x-hidden" style="overflow: hidden; max-height: 10rem; opacity: 1; padding-bottom: 0.75rem">
|
||||
${createCalendarWeekdayHeaderHTML()}
|
||||
<div id="calendar-week-grid" class="grid grid-cols-7 gap-1.5 max-w-full overflow-x-hidden"></div>
|
||||
</div>
|
||||
<div id="calendar-month-wrap" class="px-3 pb-1" style="overflow: hidden; max-height: 0; opacity: 0">
|
||||
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
|
||||
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
|
||||
</div>
|
||||
<div id="calendar-month-grid" class="grid grid-cols-7 gap-0.5"></div>
|
||||
<div id="calendar-month-wrap" class="px-3" style="overflow: hidden; max-height: 0; opacity: 0; padding-bottom: 0">
|
||||
${createCalendarWeekdayHeaderHTML()}
|
||||
<div id="calendar-month-grid" class="grid grid-cols-7 gap-1.5"></div>
|
||||
</div>
|
||||
<div id="calendar-drag-handle" class="flex items-center justify-center pb-2 pt-0.5">
|
||||
<i id="calendar-handle-icon" class="fas fa-chevron-down text-[8px] text-gray-300" aria-hidden="true"></i>
|
||||
<span id="calendar-handle-icon" class="${CALENDAR_HANDLE_CLASS}" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,105 +182,23 @@ export function getMealPlannerHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWeekGrid(weekStart, selected, plans) {
|
||||
const grid = document.getElementById('calendar-week-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = addDays(weekStart, i);
|
||||
const isSel = selected && sameDay(day, selected);
|
||||
const isToday = sameDay(day, new Date());
|
||||
const hasMeals = dayHasAnyMeal(plans, day);
|
||||
cells.push(`
|
||||
<button type="button" data-planner-day="${day.getTime()}"
|
||||
class="flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors w-full min-h-10 py-1 gap-0.5 leading-tight
|
||||
${isSel ? 'bg-gray-900 text-white' : 'text-gray-800 hover:bg-gray-100'}
|
||||
${isToday && !isSel ? 'ring-1 ring-inset ring-gray-900' : ''}">
|
||||
<span>${day.getDate()}</span>
|
||||
${hasMeals ? `<span class="w-1 h-1 rounded-full ${isSel ? 'bg-white' : 'bg-gray-900'} opacity-80" aria-hidden="true"></span>` : '<span class="w-1 h-1" aria-hidden="true"></span>'}
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
grid.innerHTML = cells.join('');
|
||||
}
|
||||
|
||||
function renderMonthGrid(monthAnchor, selected, plans) {
|
||||
const grid = document.getElementById('calendar-month-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const first = startOfMonth(monthAnchor);
|
||||
const startGrid = startOfWeekMonday(first);
|
||||
const cells = [];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const day = addDays(startGrid, i);
|
||||
const inMonth = day.getMonth() === first.getMonth();
|
||||
const isSel = selected && sameDay(day, selected);
|
||||
const isToday = sameDay(day, new Date());
|
||||
const hasMeals = inMonth && dayHasAnyMeal(plans, day);
|
||||
cells.push(`
|
||||
<button type="button" data-planner-day="${day.getTime()}"
|
||||
class="flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors w-full min-h-10 py-1 gap-0.5 leading-tight
|
||||
${!inMonth ? 'text-gray-300' : (isSel ? 'bg-gray-900 text-white' : 'text-gray-800 hover:bg-gray-100')}
|
||||
${inMonth && isToday && !isSel ? 'ring-1 ring-inset ring-gray-900' : ''}">
|
||||
<span>${day.getDate()}</span>
|
||||
${inMonth && hasMeals ? `<span class="w-1 h-1 rounded-full ${isSel ? 'bg-white' : 'bg-gray-900'} opacity-80" aria-hidden="true"></span>` : '<span class="w-1 h-1" aria-hidden="true"></span>'}
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
grid.innerHTML = cells.join('');
|
||||
}
|
||||
|
||||
function updatePeriodLabel(mode, weekStart, monthAnchor) {
|
||||
function updatePeriodLabel(mode, weekStart, monthAnchor, selected) {
|
||||
const el = document.getElementById('cal-period-label');
|
||||
if (!el) return;
|
||||
|
||||
if (mode === 'week') {
|
||||
const end = addDays(weekStart, 6);
|
||||
const y = weekStart.getFullYear();
|
||||
if (weekStart.getMonth() === end.getMonth()) {
|
||||
el.textContent = `${weekStart.getDate()}–${end.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} ${y}`;
|
||||
} else {
|
||||
el.textContent = `${weekStart.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} – ${end.getDate()} ${MONTHS_SHORT[end.getMonth()]} ${y}`;
|
||||
}
|
||||
el.textContent = formatCalendarSelectedDate(selected);
|
||||
} else {
|
||||
const m = monthAnchor.getMonth();
|
||||
const y = monthAnchor.getFullYear();
|
||||
const monthLong = [
|
||||
'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',
|
||||
'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień',
|
||||
][m];
|
||||
el.textContent = `${monthLong} ${y}`;
|
||||
el.textContent = formatCalendarMonthYear(monthAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
function syncModeToggle(mode) {
|
||||
const weekWrap = document.getElementById('calendar-week-wrap');
|
||||
const monthWrap = document.getElementById('calendar-month-wrap');
|
||||
const handleIcon = document.getElementById('calendar-handle-icon');
|
||||
|
||||
if (weekWrap) {
|
||||
weekWrap.style.maxHeight = mode === 'week' ? '10rem' : '0';
|
||||
weekWrap.style.opacity = mode === 'week' ? '1' : '0';
|
||||
}
|
||||
if (monthWrap) {
|
||||
monthWrap.style.maxHeight = mode === 'month' ? '25rem' : '0';
|
||||
monthWrap.style.opacity = mode === 'month' ? '1' : '0';
|
||||
}
|
||||
if (handleIcon) {
|
||||
handleIcon.className = mode === 'week'
|
||||
? 'fas fa-chevron-down text-[8px] text-gray-300'
|
||||
: 'fas fa-chevron-up text-[8px] text-gray-300';
|
||||
}
|
||||
}
|
||||
|
||||
function bindDayClicks(container, state, rerender) {
|
||||
container?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-planner-day]');
|
||||
if (!btn) return;
|
||||
const ts = Number(btn.getAttribute('data-planner-day'));
|
||||
state.selected = new Date(ts);
|
||||
rerender();
|
||||
syncCollapsibleCalendarMode({
|
||||
mode,
|
||||
weekWrapEl: document.getElementById('calendar-week-wrap'),
|
||||
monthWrapEl: document.getElementById('calendar-month-wrap'),
|
||||
handleEl: document.getElementById('calendar-handle-icon'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,7 +349,7 @@ function renderDayContent(state) {
|
||||
const heading = document.getElementById('planner-day-heading');
|
||||
if (heading) {
|
||||
const wd = WEEKDAYS_LONG[sel.getDay()];
|
||||
heading.textContent = `${wd}, ${sel.getDate()} ${MONTHS_SHORT[sel.getMonth()]}`;
|
||||
heading.textContent = `${wd}, ${sel.getDate()} ${CALENDAR_MONTHS_SHORT[sel.getMonth()]}`;
|
||||
}
|
||||
|
||||
const dayPlan = getDayPlan(state.plans, sel);
|
||||
@@ -753,7 +654,7 @@ function renderIngredientsSheet(state) {
|
||||
|
||||
if (titleEl) {
|
||||
const wd = WEEKDAYS_LONG[state.selected.getDay()];
|
||||
titleEl.textContent = `${wd}, ${state.selected.getDate()} ${MONTHS_SHORT[state.selected.getMonth()]} — składniki`;
|
||||
titleEl.textContent = `${wd}, ${state.selected.getDate()} ${CALENDAR_MONTHS_SHORT[state.selected.getMonth()]} — składniki`;
|
||||
}
|
||||
if (subEl) subEl.textContent = 'Porównanie potrzeb z zapasami w spiżarni.';
|
||||
|
||||
@@ -845,7 +746,7 @@ function renderIngredientsSheet(state) {
|
||||
<div class="space-y-2">
|
||||
${upcoming.map((day) => {
|
||||
const wd = WEEKDAYS_LONG[day.date.getDay()];
|
||||
const label = `${wd}, ${day.date.getDate()} ${MONTHS_SHORT[day.date.getMonth()]}`;
|
||||
const label = `${wd}, ${day.date.getDate()} ${CALENDAR_MONTHS_SHORT[day.date.getMonth()]}`;
|
||||
const shorts = day.items.filter((it) => !it.enough);
|
||||
return `<div class="rounded-xl border border-amber-200/80 bg-amber-50/50 p-3">
|
||||
<p class="text-[12px] font-semibold text-amber-900">
|
||||
@@ -912,10 +813,21 @@ export function setupMealPlanner() {
|
||||
|
||||
const rerender = () => {
|
||||
syncModeToggle(state.mode);
|
||||
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor);
|
||||
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor, state.selected);
|
||||
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
|
||||
renderWeekGrid(state.weekStart, state.selected, state.plans);
|
||||
renderMonthGrid(state.monthAnchor, state.selected, state.plans);
|
||||
renderCollapsibleCalendar({
|
||||
weekGridEl: weekGrid,
|
||||
monthGridEl: monthGrid,
|
||||
weekAnchorDate: state.weekStart,
|
||||
monthAnchorDate: state.monthAnchor,
|
||||
selectedDate: state.selected,
|
||||
resolveDayState: (day, meta) => ({
|
||||
dimmed: meta.mode === 'month' && !meta.inCurrentMonth,
|
||||
showIndicator: meta.mode === 'month'
|
||||
? meta.inCurrentMonth && dayHasAnyMeal(state.plans, day)
|
||||
: dayHasAnyMeal(state.plans, day),
|
||||
}),
|
||||
});
|
||||
renderDayContent(state);
|
||||
};
|
||||
|
||||
@@ -924,8 +836,14 @@ export function setupMealPlanner() {
|
||||
rerender();
|
||||
};
|
||||
|
||||
bindDayClicks(weekGrid?.parentElement, state, rerender);
|
||||
bindDayClicks(monthGrid?.parentElement, state, rerender);
|
||||
bindCalendarDayClicks(weekGrid, (date) => {
|
||||
state.selected = date;
|
||||
rerender();
|
||||
});
|
||||
bindCalendarDayClicks(monthGrid, (date) => {
|
||||
state.selected = date;
|
||||
rerender();
|
||||
});
|
||||
|
||||
document.getElementById('cal-prev')?.addEventListener('click', () => {
|
||||
if (state.mode === 'week') {
|
||||
@@ -1117,7 +1035,7 @@ export function setupMealPlanner() {
|
||||
}
|
||||
copyList.innerHTML = days.map((d) => {
|
||||
const wd = WEEKDAYS_LONG[d.getDay()];
|
||||
const label = `${wd}, ${d.getDate()} ${MONTHS_SHORT[d.getMonth()]}`;
|
||||
const label = `${wd}, ${d.getDate()} ${CALENDAR_MONTHS_SHORT[d.getMonth()]}`;
|
||||
const hasMeals = dayHasAnyMeal(state.plans, d);
|
||||
const badge = hasMeals ? '<span class="text-[10px] text-amber-600 font-semibold">ma posiłki</span>' : '';
|
||||
return `<button type="button" class="planner-copy-target w-full flex items-center justify-between gap-2 p-3 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white transition-all text-left" data-target-ts="${d.getTime()}">
|
||||
|
||||
Reference in New Issue
Block a user