From f35908095c7bc9546d16008bb48cd18593b3d8d1 Mon Sep 17 00:00:00 2001 From: Enciphered Date: Sat, 30 May 2026 13:40:18 +0500 Subject: [PATCH] feat: implement interactive calendar view grids and confirmation action patterns --- static/js/components.js | 258 +++++++++++++++++++++------- templates/components/date_time.html | 2 +- templates/components/macros.html | 11 +- templates/components/modals.html | 119 +++++++++++++ templates/components/sheets.html | 126 ++++++++++++++ 5 files changed, 449 insertions(+), 67 deletions(-) diff --git a/static/js/components.js b/static/js/components.js index 67b5eb1..421465f 100644 --- a/static/js/components.js +++ b/static/js/components.js @@ -113,10 +113,13 @@ document.addEventListener('DOMContentLoaded', () => { const valueInput = picker.querySelector('.datepicker-value'); const monthYearLabel = picker.querySelector('.datepicker-month-year'); const daysContainer = picker.querySelector('.datepicker-days'); - if (!valueInput || !monthYearLabel || !daysContainer) return; + const weekdaysRow = picker.querySelector('.datepicker-weekdays'); + if (!valueInput || !daysContainer) return; let currentYear = parseInt(picker.dataset.year); let currentMonth = parseInt(picker.dataset.month); // 0-indexed + let view = picker.dataset.view || 'days'; + picker.dataset.view = view; if (isNaN(currentYear) || isNaN(currentMonth)) { const val = valueInput.value; @@ -131,59 +134,116 @@ document.addEventListener('DOMContentLoaded', () => { const today = new Date(); const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; - monthYearLabel.textContent = `${monthNames[currentMonth]} ${currentYear}`; + + // Update header label + if (monthYearLabel) { + if (view === 'days') { + monthYearLabel.textContent = `${monthNames[currentMonth]} ${currentYear}`; + } else if (view === 'months') { + monthYearLabel.textContent = `${currentYear}`; + } else if (view === 'years') { + const startYear = currentYear - (currentYear % 12); + monthYearLabel.textContent = `${startYear} - ${startYear + 11}`; + } + } + + // Toggle weekdays row visibility + if (weekdaysRow) { + if (view === 'days') { + weekdaysRow.classList.remove('hidden'); + } else { + weekdaysRow.classList.add('hidden'); + } + } + + // Reset grid columns class based on active view mode + if (view === 'days') { + daysContainer.classList.remove('grid-cols-3'); + daysContainer.classList.add('grid-cols-7'); + } else { + daysContainer.classList.remove('grid-cols-7'); + daysContainer.classList.add('grid-cols-3'); + } daysContainer.innerHTML = ''; - const firstDayIndex = new Date(currentYear, currentMonth, 1).getDay(); - const totalDays = new Date(currentYear, currentMonth + 1, 0).getDate(); - const prevTotalDays = new Date(currentYear, currentMonth, 0).getDate(); + if (view === 'days') { + const firstDayIndex = new Date(currentYear, currentMonth, 1).getDay(); + const totalDays = new Date(currentYear, currentMonth + 1, 0).getDate(); + const prevTotalDays = new Date(currentYear, currentMonth, 0).getDate(); - // Prev month padding - for (let i = firstDayIndex - 1; i >= 0; i--) { - const dayNum = prevTotalDays - i; - const btn = document.createElement('button'); - btn.type = 'button'; - btn.disabled = true; - btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; - btn.textContent = dayNum; - daysContainer.appendChild(btn); - } + // Prev month padding + for (let i = firstDayIndex - 1; i >= 0; i--) { + const dayNum = prevTotalDays - i; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.disabled = true; + btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; + btn.textContent = dayNum; + daysContainer.appendChild(btn); + } - // Current month days - for (let day = 1; day <= totalDays; day++) { - const btn = document.createElement('button'); - btn.type = 'button'; - const yearStr = currentYear; - const monthStr = String(currentMonth + 1).padStart(2, '0'); - const dayStr = String(day).padStart(2, '0'); - const dateStr = `${yearStr}-${monthStr}-${dayStr}`; + // Current month days + for (let day = 1; day <= totalDays; day++) { + const btn = document.createElement('button'); + btn.type = 'button'; + const yearStr = currentYear; + const monthStr = String(currentMonth + 1).padStart(2, '0'); + const dayStr = String(day).padStart(2, '0'); + const dateStr = `${yearStr}-${monthStr}-${dayStr}`; - const isSelected = selectedDateVal === dateStr; - const isToday = today.getFullYear() === currentYear && today.getMonth() === currentMonth && today.getDate() === day; + const isSelected = selectedDateVal === dateStr; + const isToday = today.getFullYear() === currentYear && today.getMonth() === currentMonth && today.getDate() === day; - btn.className = `py-1 text-xs rounded-lg font-semibold hover:bg-accent hover:text-accent-foreground transition flex items-center justify-center h-7 w-7 mx-auto ${ - isSelected ? 'bg-indigo-600 text-white hover:bg-indigo-650' : - isToday ? 'border border-sky-500/50 text-sky-400' : 'text-slate-350' - }`; - btn.dataset.date = dateStr; - btn.textContent = day; - daysContainer.appendChild(btn); - } + btn.className = `py-1 text-xs rounded-lg font-semibold hover:bg-accent hover:text-accent-foreground transition flex items-center justify-center h-7 w-7 mx-auto ${ + isSelected ? 'bg-indigo-600 text-white hover:bg-indigo-650' : + isToday ? 'border border-sky-500/50 text-sky-400' : 'text-slate-350' + }`; + btn.dataset.date = dateStr; + btn.textContent = day; + daysContainer.appendChild(btn); + } - // Next month padding - const totalGrids = 42; - const currentGrids = firstDayIndex + totalDays; - for (let i = 1; i <= (totalGrids - currentGrids); i++) { - const btn = document.createElement('button'); - btn.type = 'button'; - btn.disabled = true; - btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; - btn.textContent = i; - daysContainer.appendChild(btn); + // Next month padding + const totalGrids = 42; + const currentGrids = firstDayIndex + totalDays; + for (let i = 1; i <= (totalGrids - currentGrids); i++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.disabled = true; + btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; + btn.textContent = i; + daysContainer.appendChild(btn); + } + } else if (view === 'months') { + const shortMonthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + for (let m = 0; m < 12; m++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = `py-3.5 text-xs rounded-xl font-semibold transition text-center hover:bg-accent hover:text-accent-foreground ${ + m === currentMonth ? 'bg-indigo-600 text-white font-bold' : 'text-slate-350' + }`; + btn.dataset.monthVal = m; + btn.textContent = shortMonthNames[m]; + daysContainer.appendChild(btn); + } + } else if (view === 'years') { + const startYear = currentYear - (currentYear % 12); + for (let y = 0; y < 12; y++) { + const yearNum = startYear + y; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = `py-3.5 text-xs rounded-xl font-semibold transition text-center hover:bg-accent hover:text-accent-foreground ${ + yearNum === currentYear ? 'bg-indigo-600 text-white font-bold' : 'text-slate-350' + }`; + btn.dataset.yearVal = yearNum; + btn.textContent = yearNum; + daysContainer.appendChild(btn); + } } } + // Initialize custom date pickers document.querySelectorAll('.custom-datepicker').forEach(picker => { renderCalendar(picker); @@ -205,41 +265,85 @@ document.addEventListener('DOMContentLoaded', () => { if (pop !== popover) pop.classList.add('hidden'); }); - popover.classList.toggle('hidden'); + const isHidden = popover.classList.toggle('hidden'); + if (!isHidden) { + picker.dataset.view = 'days'; + renderCalendar(picker); + } return; } - // Prev Month + // Prev Month / Year / Decade const prevBtn = event.target.closest('.datepicker-prev'); if (prevBtn) { const picker = prevBtn.closest('.custom-datepicker'); - let currentMonth = parseInt(picker.dataset.month); - let currentYear = parseInt(picker.dataset.year); - currentMonth--; - if (currentMonth < 0) { - currentMonth = 11; - currentYear--; + if (picker) { + let currentMonth = parseInt(picker.dataset.month); + let currentYear = parseInt(picker.dataset.year); + const view = picker.dataset.view || 'days'; + + if (view === 'days') { + currentMonth--; + if (currentMonth < 0) { + currentMonth = 11; + currentYear--; + } + picker.dataset.month = currentMonth; + picker.dataset.year = currentYear; + } else if (view === 'months') { + currentYear--; + picker.dataset.year = currentYear; + } else if (view === 'years') { + currentYear -= 12; + picker.dataset.year = currentYear; + } + renderCalendar(picker); } - picker.dataset.month = currentMonth; - picker.dataset.year = currentYear; - renderCalendar(picker); return; } - // Next Month + // Next Month / Year / Decade const nextBtn = event.target.closest('.datepicker-next'); if (nextBtn) { const picker = nextBtn.closest('.custom-datepicker'); - let currentMonth = parseInt(picker.dataset.month); - let currentYear = parseInt(picker.dataset.year); - currentMonth++; - if (currentMonth > 11) { - currentMonth = 0; - currentYear++; + if (picker) { + let currentMonth = parseInt(picker.dataset.month); + let currentYear = parseInt(picker.dataset.year); + const view = picker.dataset.view || 'days'; + + if (view === 'days') { + currentMonth++; + if (currentMonth > 11) { + currentMonth = 0; + currentYear++; + } + picker.dataset.month = currentMonth; + picker.dataset.year = currentYear; + } else if (view === 'months') { + currentYear++; + picker.dataset.year = currentYear; + } else if (view === 'years') { + currentYear += 12; + picker.dataset.year = currentYear; + } + renderCalendar(picker); + } + return; + } + + // Header click (switch view mode) + const headerBtn = event.target.closest('.datepicker-month-year'); + if (headerBtn) { + const picker = headerBtn.closest('.custom-datepicker'); + if (picker) { + const view = picker.dataset.view || 'days'; + if (view === 'days') { + picker.dataset.view = 'months'; + } else if (view === 'months') { + picker.dataset.view = 'years'; + } + renderCalendar(picker); } - picker.dataset.month = currentMonth; - picker.dataset.year = currentYear; - renderCalendar(picker); return; } @@ -260,6 +364,32 @@ document.addEventListener('DOMContentLoaded', () => { return; } + // Month click + const monthBtn = event.target.closest('.datepicker-days button[data-month-val]'); + if (monthBtn) { + const picker = monthBtn.closest('.custom-datepicker'); + if (picker) { + const monthVal = parseInt(monthBtn.dataset.monthVal); + picker.dataset.month = monthVal; + picker.dataset.view = 'days'; + renderCalendar(picker); + } + return; + } + + // Year click + const yearBtn = event.target.closest('.datepicker-days button[data-year-val]'); + if (yearBtn) { + const picker = yearBtn.closest('.custom-datepicker'); + if (picker) { + const yearVal = parseInt(yearBtn.dataset.yearVal); + picker.dataset.year = yearVal; + picker.dataset.view = 'months'; + renderCalendar(picker); + } + return; + } + // Close outside const openPickerPopover = document.querySelector('.datepicker-popover:not(.hidden)'); if (openPickerPopover && !event.target.closest('.custom-datepicker')) { diff --git a/templates/components/date_time.html b/templates/components/date_time.html index 12e42a2..0ee205e 100644 --- a/templates/components/date_time.html +++ b/templates/components/date_time.html @@ -140,7 +140,7 @@ .datepicker-month-year Class - Display title label updated dynamically with current month and year (e.g. "May 2026"). + Clickable header button. Toggles view grid modes between days selection, months selection, and years selection. diff --git a/templates/components/macros.html b/templates/components/macros.html index 5990608..b7ffd42 100644 --- a/templates/components/macros.html +++ b/templates/components/macros.html @@ -97,6 +97,12 @@ {% endmacro %} +{% macro modal_close_only() %} + + +{% endmacro %} + + {% macro sheet_open(id, title, max_width_class="max-w-sm") %} -
+
SuMoTuWeThFrSa
diff --git a/templates/components/modals.html b/templates/components/modals.html index f67af45..a5ed958 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -123,6 +123,107 @@
+ +
+

Confirmation Actions (e.g., Deletes)

+

+ To prevent accidental actions, wrap destructive or state-changing actions (such as deletions) in a confirmation dialog modal. Since modals are populated inside the DOM structure, you can either wrap the action inside a standard HTML form or use HTMX for AJAX deletions. +

+ +
+ +
+ + + +
+ + +
+
+ Demo Project Entity: +
+ Important Draft.docx + {{ ui::modal_trigger(target_id="wiki-confirm-delete-modal", label="Delete Draft", variant="destructive", extra_class="!px-2.5 !py-1 text-[10px]") }} +
+
+
+ + + + + + +
+
+ + @@ -131,6 +232,24 @@

This modal is animated and handled globally by components.js. Pressing escape or clicking outside closes it instantly.

{{ ui::modal_close(close_label="Dismiss Modal") }} + +{{ ui::modal_open(id="wiki-confirm-delete-modal", title="Confirm Deletion") }} +
+

+ Are you sure you want to permanently delete "Important Draft.docx"? This action cannot be undone. +

+
+ + +
+
+{{ ui::modal_close_only() }} + +