308 lines
13 KiB
JavaScript
308 lines
13 KiB
JavaScript
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'];
|
||
|
||
function startOfDay(d) {
|
||
const x = new Date(d);
|
||
x.setHours(0, 0, 0, 0);
|
||
return x;
|
||
}
|
||
|
||
function sameDay(a, b) {
|
||
return a.getFullYear() === b.getFullYear()
|
||
&& a.getMonth() === b.getMonth()
|
||
&& a.getDate() === b.getDate();
|
||
}
|
||
|
||
function addDays(d, n) {
|
||
const x = new Date(d);
|
||
x.setDate(x.getDate() + n);
|
||
return startOfDay(x);
|
||
}
|
||
|
||
/** Poniedziałek jako pierwszy dzień tygodnia (PL) */
|
||
function startOfWeekMonday(d) {
|
||
const date = startOfDay(d);
|
||
const day = date.getDay();
|
||
const diff = day === 0 ? -6 : 1 - day;
|
||
return addDays(date, diff);
|
||
}
|
||
|
||
function startOfMonth(d) {
|
||
const x = new Date(d.getFullYear(), d.getMonth(), 1);
|
||
return startOfDay(x);
|
||
}
|
||
|
||
function addMonths(d, n) {
|
||
const x = new Date(d);
|
||
x.setMonth(x.getMonth() + n);
|
||
return startOfDay(x);
|
||
}
|
||
|
||
function addWeeks(d, n) {
|
||
return addDays(d, n * 7);
|
||
}
|
||
|
||
function weekContains(weekStart, d) {
|
||
const t = startOfDay(d).getTime();
|
||
const ws = weekStart.getTime();
|
||
const we = addDays(weekStart, 6).getTime();
|
||
return t >= ws && t <= we;
|
||
}
|
||
|
||
function sameMonth(a, b) {
|
||
return a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
export function getMealPlannerHTML() {
|
||
return `
|
||
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 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-between gap-3">
|
||
<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 id="planner-cal-mode" class="inline-flex items-center shrink-0 rounded-md border border-gray-200 bg-gray-50 p-px gap-px shadow-sm" role="tablist" aria-label="Skala kalendarza">
|
||
<button type="button" data-cal-mode="week" id="planner-mode-week" title="Tydzień" aria-label="Widok tygodnia"
|
||
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors bg-white text-gray-900 shadow-sm">
|
||
<i class="fas fa-calendar-week text-[10px]" aria-hidden="true"></i>
|
||
</button>
|
||
<button type="button" data-cal-mode="month" id="planner-mode-month" title="Miesiąc" aria-label="Widok miesiąca"
|
||
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors text-gray-400 hover:text-gray-600">
|
||
<i class="fas fa-calendar-days text-[10px]" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="calendar-week-wrap" class="px-3 pb-2.5">
|
||
<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>
|
||
</div>
|
||
<div id="calendar-month-wrap" class="hidden px-3 pb-2.5">
|
||
<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>
|
||
</div>
|
||
<div class="flex-1 overflow-y-auto px-4 pt-3">
|
||
<p class="text-sm text-gray-500 leading-relaxed">Dotknij dnia w kalendarzu, aby później przypisać posiłki. Ta sekcja na razie jest zapowiedzią rozbudowy planera.</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderWeekGrid(weekStart, selected) {
|
||
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());
|
||
cells.push(`
|
||
<button type="button" data-planner-day="${day.getTime()}"
|
||
class="aspect-square flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors min-h-0
|
||
${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>
|
||
</button>
|
||
`);
|
||
}
|
||
grid.innerHTML = cells.join('');
|
||
}
|
||
|
||
function renderMonthGrid(monthAnchor, selected) {
|
||
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());
|
||
cells.push(`
|
||
<button type="button" data-planner-day="${day.getTime()}"
|
||
class="aspect-square flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors min-h-0
|
||
${!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>
|
||
</button>
|
||
`);
|
||
}
|
||
grid.innerHTML = cells.join('');
|
||
}
|
||
|
||
function updatePeriodLabel(mode, weekStart, monthAnchor) {
|
||
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}`;
|
||
}
|
||
} 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}`;
|
||
}
|
||
}
|
||
|
||
function syncModeToggle(mode) {
|
||
const w = document.getElementById('planner-mode-week');
|
||
const m = document.getElementById('planner-mode-month');
|
||
const weekWrap = document.getElementById('calendar-week-wrap');
|
||
const monthWrap = document.getElementById('calendar-month-wrap');
|
||
|
||
const base = 'planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors';
|
||
const active = `${base} bg-white text-gray-900 shadow-sm`;
|
||
const idle = `${base} text-gray-400 hover:text-gray-600`;
|
||
|
||
if (w && m) {
|
||
if (mode === 'week') {
|
||
w.className = active;
|
||
m.className = idle;
|
||
} else {
|
||
w.className = idle;
|
||
m.className = active;
|
||
}
|
||
}
|
||
if (weekWrap && monthWrap) {
|
||
weekWrap.classList.toggle('hidden', mode !== 'week');
|
||
monthWrap.classList.toggle('hidden', mode !== 'month');
|
||
}
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
|
||
export function setupMealPlanner() {
|
||
const state = {
|
||
mode: 'week',
|
||
weekStart: startOfWeekMonday(new Date()),
|
||
monthAnchor: startOfDay(new Date()),
|
||
selected: startOfDay(new Date()),
|
||
};
|
||
|
||
const weekGrid = document.getElementById('calendar-week-grid');
|
||
const monthGrid = document.getElementById('calendar-month-grid');
|
||
|
||
const rerender = () => {
|
||
syncModeToggle(state.mode);
|
||
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor);
|
||
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
|
||
if (state.mode === 'week') {
|
||
renderWeekGrid(state.weekStart, state.selected);
|
||
} else {
|
||
renderMonthGrid(state.monthAnchor, state.selected);
|
||
}
|
||
};
|
||
|
||
bindDayClicks(weekGrid?.parentElement, state, rerender);
|
||
bindDayClicks(monthGrid?.parentElement, state, rerender);
|
||
|
||
document.getElementById('planner-cal-mode')?.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('[data-cal-mode]');
|
||
if (!btn) return;
|
||
const mode = btn.getAttribute('data-cal-mode');
|
||
if (mode !== 'week' && mode !== 'month') return;
|
||
state.mode = mode;
|
||
if (mode === 'week') {
|
||
state.weekStart = startOfWeekMonday(state.selected);
|
||
} else {
|
||
state.monthAnchor = startOfMonth(state.selected);
|
||
}
|
||
rerender();
|
||
});
|
||
|
||
document.getElementById('cal-prev')?.addEventListener('click', () => {
|
||
if (state.mode === 'week') {
|
||
state.weekStart = addWeeks(state.weekStart, -1);
|
||
if (!weekContains(state.weekStart, state.selected)) {
|
||
state.selected = new Date(state.weekStart);
|
||
}
|
||
} else {
|
||
state.monthAnchor = addMonths(state.monthAnchor, -1);
|
||
if (!sameMonth(state.monthAnchor, state.selected)) {
|
||
state.selected = startOfMonth(state.monthAnchor);
|
||
}
|
||
}
|
||
rerender();
|
||
});
|
||
|
||
document.getElementById('cal-next')?.addEventListener('click', () => {
|
||
if (state.mode === 'week') {
|
||
state.weekStart = addWeeks(state.weekStart, 1);
|
||
if (!weekContains(state.weekStart, state.selected)) {
|
||
state.selected = new Date(state.weekStart);
|
||
}
|
||
} else {
|
||
state.monthAnchor = addMonths(state.monthAnchor, 1);
|
||
if (!sameMonth(state.monthAnchor, state.selected)) {
|
||
state.selected = startOfMonth(state.monthAnchor);
|
||
}
|
||
}
|
||
rerender();
|
||
});
|
||
|
||
document.getElementById('cal-go-today')?.addEventListener('click', () => {
|
||
const today = startOfDay(new Date());
|
||
state.selected = today;
|
||
state.weekStart = startOfWeekMonday(today);
|
||
state.monthAnchor = startOfMonth(today);
|
||
rerender();
|
||
});
|
||
|
||
rerender();
|
||
}
|