Add recipe mockup

This commit is contained in:
2026-03-20 21:29:38 +01:00
parent 6e0d650f54
commit c4d2e74703
9 changed files with 1062 additions and 0 deletions

View File

@@ -0,0 +1,307 @@
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();
}