diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a20905f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7d1267c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/recipe-mockup.iml b/.idea/recipe-mockup.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/recipe-mockup.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..3278371 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1775222853874 + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index 3b5f11c..6b18458 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + @@ -11,173 +11,334 @@ + + + - +
@@ -190,4 +351,4 @@ - \ No newline at end of file + diff --git a/js/app.js b/js/app.js index f94ac44..4af1188 100644 --- a/js/app.js +++ b/js/app.js @@ -1,9 +1,9 @@ import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js?v=2'; import { getFilterHTML, setupFilter } from './views/Filter.js?v=2'; import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2'; -import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=2'; +import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=4'; import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2'; -import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=3'; +import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=5'; function getAppToastHTML() { return ` @@ -52,7 +52,7 @@ function setupThemeToggle() { if (label) label.textContent = isDark ? 'Jasny' : 'Ciemny'; const meta = document.querySelector('meta[name="theme-color"]'); - if (meta) meta.setAttribute('content', isDark ? '#0d0d0d' : '#ffffff'); + if (meta) meta.setAttribute('content', isDark ? '#161513' : '#f3efe9'); }); } diff --git a/js/ui/mealCalendar.js b/js/ui/mealCalendar.js new file mode 100644 index 0000000..eacd75e --- /dev/null +++ b/js/ui/mealCalendar.js @@ -0,0 +1,241 @@ +import { + addDays, + sameDay, + startOfDay, + startOfMonth, + startOfWeekMonday, + weekContains, +} from '../services/dateUtils.js'; + +export const CALENDAR_MONTHS_LONG = [ + 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', + 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień', +]; + +export const CALENDAR_MONTHS_SHORT = [ + 'sty', 'lut', 'mar', 'kwi', 'maj', 'cze', + 'lip', 'sie', 'wrz', 'paź', 'lis', 'gru', +]; + +export const CALENDAR_WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd']; +export const CALENDAR_DAY_ATTR = 'data-calendar-day'; +export const CALENDAR_HANDLE_CLASS = 'block h-1 w-10 rounded-full bg-[#6d6c67]/75'; + +function getCalendarDayHTML(day, meta, dayState, dayAttr) { + const { mode, selectedDate, inCurrentMonth } = meta; + const isSelected = selectedDate && sameDay(day, selectedDate); + const showIndicator = !!dayState.showIndicator; + const isDisabled = !!dayState.disabled; + const isDimmed = !!dayState.dimmed && !isSelected; + const bg = isSelected ? '#23221e' : '#2f2f2d'; + const border = isSelected ? '#787876' : '#444442'; + const text = isSelected ? '#f2efe8' : (isDimmed ? '#7d7a74' : '#d7d2c8'); + const dot = isSelected ? '#f2efe8' : '#a59f92'; + const opacity = isDimmed ? '0.72' : '1'; + const outerClass = `${mode === 'month' ? 'mx-auto ' : ''}flex h-[2.3rem] w-full min-w-0 max-w-full items-center justify-center rounded-full border text-xs font-medium transition-colors leading-tight overflow-hidden`; + const innerClass = mode === 'month' + ? 'relative flex h-full w-full flex-col items-center justify-center' + : 'relative flex h-full w-full items-center justify-center'; + const dotBottom = mode === 'month' ? '0.18rem' : '0.15rem'; + const tagName = isDisabled ? 'div' : 'button'; + const buttonAttrs = isDisabled ? '' : ` type="button" ${dayAttr}="${day.getTime()}"`; + + return ` + <${tagName}${buttonAttrs} + class="${outerClass}" + style="background:${bg};border-color:${border};color:${text};opacity:${opacity};"> + + ${day.getDate()} + ${showIndicator + ? `` + : ''} + + + `; +} + +function getMonthCells(monthAnchor) { + const first = startOfMonth(monthAnchor); + const startGrid = startOfWeekMonday(first); + const cells = []; + for (let i = 0; i < 42; i++) cells.push(addDays(startGrid, i)); + while (cells.length > 35 && cells.slice(-7).every((day) => day.getMonth() !== first.getMonth())) { + cells.splice(-7); + } + return { cells, month: first.getMonth() }; +} + +function getDayState(day, meta, resolveDayState) { + if (typeof resolveDayState !== 'function') { + return { + disabled: false, + dimmed: meta.mode === 'month' && !meta.inCurrentMonth, + showIndicator: false, + }; + } + + const resolved = resolveDayState(day, meta) || {}; + return { + disabled: !!resolved.disabled, + dimmed: !!resolved.dimmed, + showIndicator: !!resolved.showIndicator, + }; +} + +export function createCalendarWeekdayHeaderHTML(labels = CALENDAR_WEEKDAYS_SHORT) { + return ` +
+ ${labels.map((label) => `
${label}
`).join('')} +
+ `; +} + +export function createCalendarTopbarHTML({ + titleId, + prevId, + todayId, + nextId, + wrapperClass = 'px-4 pt-4 pb-3 flex items-center gap-3', + titleClass = 'text-[18px] font-semibold text-gray-900 leading-none tracking-[-0.03em]', +}) { + return ` +
+
+

+
+
+ + + +
+
+ `; +} + +export function formatCalendarMonthYear(date) { + return `${CALENDAR_MONTHS_LONG[date.getMonth()]} ${date.getFullYear()}`; +} + +export function formatCalendarSelectedDate(date) { + return `${date.getDate()} ${CALENDAR_MONTHS_SHORT[date.getMonth()]} ${date.getFullYear()}`; +} + +export function isCalendarOnToday(mode, weekStart, monthAnchor, selectedDate) { + const today = startOfDay(new Date()); + if (!sameDay(selectedDate, today)) return false; + if (mode === 'week') return weekContains(weekStart, today); + return startOfMonth(monthAnchor).getTime() === startOfMonth(today).getTime(); +} + +export function syncCalendarTodayButton(buttonEl, isOnToday) { + if (!buttonEl) return; + const base = 'h-full shrink-0 inline-flex items-center justify-center rounded-full px-2.5 text-[11px] font-semibold leading-none transition-colors'; + const active = `${base} text-[#d7d2c8] hover:bg-[#3a3a37]`; + const dim = `${base} text-[#7d7a74] cursor-default`; + buttonEl.className = isOnToday ? dim : active; + buttonEl.disabled = isOnToday; +} + +export function renderCalendarGrid({ + gridEl, + mode, + anchorDate, + selectedDate, + resolveDayState, + dayAttr = CALENDAR_DAY_ATTR, +}) { + if (!gridEl) return; + + if (mode === 'week') { + const weekStart = startOfWeekMonday(anchorDate); + const cells = []; + for (let i = 0; i < 7; i++) { + const day = addDays(weekStart, i); + const meta = { + mode, + selectedDate, + inCurrentMonth: true, + }; + cells.push(getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr)); + } + gridEl.innerHTML = cells.join(''); + return; + } + + const { cells, month } = getMonthCells(anchorDate); + gridEl.innerHTML = cells.map((day) => { + const meta = { + mode, + selectedDate, + inCurrentMonth: day.getMonth() === month, + }; + return getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr); + }).join(''); +} + +export function renderCollapsibleCalendar({ + weekGridEl, + monthGridEl, + weekAnchorDate, + monthAnchorDate, + selectedDate, + resolveDayState, + dayAttr = CALENDAR_DAY_ATTR, +}) { + renderCalendarGrid({ + gridEl: weekGridEl, + mode: 'week', + anchorDate: weekAnchorDate, + selectedDate, + resolveDayState, + dayAttr, + }); + renderCalendarGrid({ + gridEl: monthGridEl, + mode: 'month', + anchorDate: monthAnchorDate, + selectedDate, + resolveDayState, + dayAttr, + }); +} + +export function syncCollapsibleCalendarMode({ + mode, + weekWrapEl, + monthWrapEl, + handleEl, + activePaddingBottom = '0.75rem', + weekMaxHeight = '10rem', + monthMaxHeight = '17.25rem', +}) { + if (weekWrapEl) { + weekWrapEl.style.maxHeight = mode === 'week' ? weekMaxHeight : '0'; + weekWrapEl.style.opacity = mode === 'week' ? '1' : '0'; + weekWrapEl.style.paddingBottom = mode === 'week' ? activePaddingBottom : '0'; + } + if (monthWrapEl) { + monthWrapEl.style.maxHeight = mode === 'month' ? monthMaxHeight : '0'; + monthWrapEl.style.opacity = mode === 'month' ? '1' : '0'; + monthWrapEl.style.paddingBottom = mode === 'month' ? activePaddingBottom : '0'; + } + if (handleEl) handleEl.className = CALENDAR_HANDLE_CLASS; +} + +export function bindCalendarDayClicks(containerEl, onSelect, dayAttr = CALENDAR_DAY_ATTR) { + if (!containerEl || typeof onSelect !== 'function') return; + containerEl.addEventListener('click', (event) => { + const button = event.target.closest(`[${dayAttr}]`); + if (!button || !containerEl.contains(button)) return; + const ts = Number(button.getAttribute(dayAttr)); + if (!Number.isFinite(ts)) return; + onSelect(new Date(ts), button, event); + }); +} diff --git a/js/ui/mealPlanEditor.js b/js/ui/mealPlanEditor.js index 84d021a..5bb309a 100644 --- a/js/ui/mealPlanEditor.js +++ b/js/ui/mealPlanEditor.js @@ -6,7 +6,6 @@ import { sameDay, sameMonth, startOfDay, - startOfMonth, startOfWeekMonday, } from '../services/dateUtils.js'; import { @@ -15,16 +14,23 @@ import { newPlanEntryId, savePlans, } from '../services/planStore.js'; +import { dayHasAnyMeal } from '../services/planIngredients.js'; import { showAppToast } from './toast.js'; +import { + bindCalendarDayClicks, + createCalendarTopbarHTML, + createCalendarWeekdayHeaderHTML, + formatCalendarMonthYear, + formatCalendarSelectedDate, + isCalendarOnToday, + renderCalendarGrid, + syncCalendarTodayButton, +} from './mealCalendar.js?v=1'; function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } -const MONTHS_LONG = ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień']; -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']; const slotLabel = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label])); /* ── HTML template ──────────────────────────────────── */ @@ -47,16 +53,15 @@ export function getMealPlanEditorHTML() {