Calendar multi-select: selectedDates Set, click-to-toggle, inRangeDates across all selections

This commit is contained in:
2026-05-22 22:37:31 +00:00
parent 2a7911184c
commit 9f7cd7913d
+49 -26
View File
@@ -796,7 +796,7 @@
<div class="cal-legend-item" id="legend-range" style="display:none"><div class="cal-legend-dot" style="background:rgba(249,115,22,0.35)"></div> In Range</div>
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(239,68,68,0.35)"></div> Unavailable</div>
</div>
<p id="calHint" style="font-size:0.72rem;color:rgba(255,255,255,0.35);margin-top:0.5rem">Click any available date to select it. Weekend rentals automatically highlight both days.</p>
<p id="calHint" style="font-size:0.72rem;color:rgba(255,255,255,0.35);margin-top:0.5rem">Click a date to select it — click again to deselect. Weekend package shows both days automatically.</p>
</div>
<form class="contact-form" id="bookingForm" novalidate>
@@ -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';