From 9f7cd7913d5e9edba3870b8d5ae2cf9ce2e6e6fc Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Fri, 22 May 2026 22:37:31 +0000 Subject: [PATCH] Calendar multi-select: selectedDates Set, click-to-toggle, inRangeDates across all selections --- index.html | 75 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index 284cf91..ce7a40b 100644 --- a/index.html +++ b/index.html @@ -796,7 +796,7 @@
Unavailable
-

Click any available date to select it. Weekend rentals automatically highlight both days.

+

Click a date to select it — click again to deselect. Weekend package shows both days automatically.

@@ -885,7 +885,8 @@ let calYear = new Date().getFullYear(); let calMonth = new Date().getMonth() + 1; // 1-based let bookedSet = new Set(); - let selectedDate = null; + let selectedDates = new Set(); // all pinned start dates + let lastSelectedDate = null; // what goes in the booking form let selectedPackage = 'half-day'; const availCache = {}; @@ -964,6 +965,18 @@ fragment.appendChild(el); } + // Pre-compute all in-range dates across every selected start date + const inRangeDates = new Set(); + for (const start of selectedDates) { + const endD = getEndDateStr(start, selectedPackage); + if (endD && endD !== start) { + const dd = new Date(start + 'T12:00:00'); dd.setDate(dd.getDate() + 1); + while (dd.toISOString().split('T')[0] <= endD) { + inRangeDates.add(dd.toISOString().split('T')[0]); dd.setDate(dd.getDate() + 1); + } + } + } + for (let d = 1; d <= daysInMo; d++) { const dateStr = year + '-' + String(month).padStart(2,'0') + '-' + String(d).padStart(2,'0'); const el = document.createElement('div'); @@ -972,13 +985,12 @@ const isPast = dateStr < todayStr; const isBooked = bookedSet.has(dateStr); const isToday = dateStr === todayStr; - const isSel = dateStr === selectedDate; - const endDate = getEndDateStr(selectedDate, selectedPackage); - const isInRange = !isSel && selectedDate && endDate && endDate !== selectedDate - && dateStr > selectedDate && dateStr <= endDate; + const isSel = selectedDates.has(dateStr); + const isInRange = !isSel && inRangeDates.has(dateStr); if (isSel) { el.className = 'cal-day selected'; + el.addEventListener('click', () => selectDate(dateStr)); // click again to deselect } else if (isInRange) { el.className = 'cal-day in-range' + (isBooked ? ' booked' : ''); el.title = isBooked ? 'Conflict — this day is unavailable' : 'In your booking range'; @@ -1000,14 +1012,21 @@ } async function selectDate(dateStr) { - if (await hasRangeConflict(dateStr, selectedPackage)) { - showDateUnavail(); - return; + if (selectedDates.has(dateStr)) { + // Toggle off — deselect this date + selectedDates.delete(dateStr); + lastSelectedDate = selectedDates.size > 0 + ? [...selectedDates].sort().reverse()[0] : null; + } else { + if (await hasRangeConflict(dateStr, selectedPackage)) { + showDateUnavail(); return; + } + selectedDates.add(dateStr); + lastSelectedDate = dateStr; + document.getElementById('bookingForm').scrollIntoView({ behavior: 'smooth', block: 'start' }); } - selectedDate = dateStr; - if (dateInput) dateInput.value = dateStr; + if (dateInput) dateInput.value = lastSelectedDate || ''; renderCalendar(calMonth, calYear); - document.getElementById('bookingForm').scrollIntoView({ behavior: 'smooth', block: 'start' }); } document.getElementById('calPrev').addEventListener('click', () => { @@ -1024,7 +1043,7 @@ // Initial load loadCalendar(calMonth, calYear); - // Keep calendar in sync when user types a date manually; validate against bookedSet + // Keep calendar in sync when user types a date manually; validate against availability if (dateInput) { dateInput.addEventListener('change', async () => { const val = dateInput.value; @@ -1034,14 +1053,14 @@ calMonth = m; calYear = y; await loadCalendar(calMonth, calYear); } - const unavailMsg = document.getElementById('date-unavail-msg'); - if (bookedSet.has(val)) { - dateInput.value = ''; - selectedDate = null; - if (unavailMsg) { unavailMsg.style.display = 'block'; setTimeout(() => unavailMsg.style.display = 'none', 5000); } + if (await hasRangeConflict(val, selectedPackage)) { + dateInput.value = lastSelectedDate || ''; + showDateUnavail(); } else { - selectedDate = val; - if (unavailMsg) unavailMsg.style.display = 'none'; + selectedDates.add(val); + lastSelectedDate = val; + const msg = document.getElementById('date-unavail-msg'); + if (msg) msg.style.display = 'none'; } renderCalendar(calMonth, calYear); }); @@ -1058,11 +1077,15 @@ pkgSelect.addEventListener('change', function() { selectedPackage = this.value; updateLegend(); - if (selectedDate) { - hasRangeConflict(selectedDate, selectedPackage).then(conflict => { - if (conflict) { if (dateInput) dateInput.value = ''; selectedDate = null; showDateUnavail(); } - renderCalendar(calMonth, calYear); - }); + if (selectedDates.size > 0) { + Promise.all([...selectedDates].map(d => hasRangeConflict(d, selectedPackage).then(c => c ? d : null))) + .then(conflicts => { + conflicts.filter(Boolean).forEach(d => selectedDates.delete(d)); + if (!selectedDates.has(lastSelectedDate)) lastSelectedDate = [...selectedDates].sort().reverse()[0] || null; + if (dateInput) dateInput.value = lastSelectedDate || ''; + if (conflicts.some(Boolean)) showDateUnavail(); + renderCalendar(calMonth, calYear); + }); } else { renderCalendar(calMonth, calYear); } const price = PACKAGE_PRICES[this.value]; if (price && balLabel && balAmt) { @@ -1171,7 +1194,7 @@ msg.style.display = 'block'; form.reset(); if (squareCard) { squareCard.destroy(); squareCard = null; } - selectedDate = null; + selectedDates.clear(); lastSelectedDate = null; if (dateInput) dateInput.min = new Date().toISOString().split('T')[0]; btn.textContent = 'Request Sent!'; btn.style.background = '#16a34a';