From 7b9db1c5e6dca8c359a3bee80d482ddb3617087f Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 20 Mar 2026 21:11:54 +0100 Subject: [PATCH] Add meal planner --- index.html | 2 +- js/app.js | 63 ++++++++- js/views/MealPlanner.js | 307 ++++++++++++++++++++++++++++++++++++++++ js/views/RecipeList.js | 7 - 4 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 js/views/MealPlanner.js diff --git a/index.html b/index.html index 8a075ee..544cab0 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/js/app.js b/js/app.js index 0ce03e6..d7a3d52 100644 --- a/js/app.js +++ b/js/app.js @@ -1,18 +1,77 @@ import { getRecipeListHTML } from './views/RecipeList.js'; import { getFilterHTML, setupFilter } from './views/Filter.js'; import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetail.js'; +import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js'; + +function getBottomNavHTML() { + return ` + + `; +} + +function setupTabs() { + const main = document.getElementById('main-view'); + const planner = document.getElementById('planner-view'); + const nav = document.getElementById('app-bottom-nav'); + if (!main || !planner || !nav) return; + + const activeTab = 'nav-tab flex flex-col items-center gap-1 text-black min-w-[3.5rem]'; + const idleTab = 'nav-tab flex flex-col items-center gap-1 text-gray-500 hover:text-gray-700 min-w-[3.5rem]'; + + const apply = (tab) => { + const showRecipes = tab === 'recipes'; + main.classList.toggle('hidden', !showRecipes); + planner.classList.toggle('hidden', showRecipes); + + nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => { + const id = btn.getAttribute('data-tab'); + if (btn.hasAttribute('disabled')) return; + if (id === 'recipes' || id === 'planner') { + btn.className = id === tab ? activeTab : idleTab; + } + }); + }; + + nav.addEventListener('click', (e) => { + const btn = e.target.closest('.nav-tab[data-tab]'); + if (!btn || btn.hasAttribute('disabled')) return; + const tab = btn.getAttribute('data-tab'); + if (tab === 'recipes' || tab === 'planner') apply(tab); + }); + + apply('recipes'); +} document.addEventListener('DOMContentLoaded', () => { const appContainer = document.getElementById('app-container'); - // Inject all views into the main container appContainer.innerHTML = ` ${getRecipeListHTML()} + ${getMealPlannerHTML()} + ${getBottomNavHTML()} ${getRecipeDetailHTML()} ${getFilterHTML()} `; - // Initialize logic for the injected views + setupTabs(); + setupMealPlanner(); setupFilter(); setupRecipeDetail(); }); diff --git a/js/views/MealPlanner.js b/js/views/MealPlanner.js new file mode 100644 index 0000000..96ea896 --- /dev/null +++ b/js/views/MealPlanner.js @@ -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 ` + + `; +} + +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(` + + `); + } + 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(` + + `); + } + 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(); +} diff --git a/js/views/RecipeList.js b/js/views/RecipeList.js index 43c8d16..f1d9428 100644 --- a/js/views/RecipeList.js +++ b/js/views/RecipeList.js @@ -169,13 +169,6 @@ export function getRecipeListHTML() { - -
- - - - -
`; }