Calendar: range highlight, cross-month conflict check, dynamic legend + instructions

This commit is contained in:
2026-05-22 22:25:02 +00:00
parent 9029ce2d12
commit 2a7911184c
+71 -11
View File
@@ -792,9 +792,11 @@
</div> </div>
<div class="cal-legend"> <div class="cal-legend">
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(255,255,255,0.12)"></div> Available</div> <div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(255,255,255,0.12)"></div> Available</div>
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(239,68,68,0.35)"></div> Booked</div>
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:var(--orange)"></div> Selected</div> <div class="cal-legend-item"><div class="cal-legend-dot" style="background:var(--orange)"></div> Selected</div>
<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> </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>
</div> </div>
<form class="contact-form" id="bookingForm" novalidate> <form class="contact-form" id="bookingForm" novalidate>
@@ -884,14 +886,57 @@
let calMonth = new Date().getMonth() + 1; // 1-based let calMonth = new Date().getMonth() + 1; // 1-based
let bookedSet = new Set(); let bookedSet = new Set();
let selectedDate = null; let selectedDate = null;
let selectedPackage = 'half-day';
const availCache = {};
function getEndDateStr(startDate, pkg) {
if (!startDate || pkg !== 'weekend') return startDate;
const d = new Date(startDate + 'T12:00:00');
d.setDate(d.getDate() + 1);
return d.toISOString().split('T')[0];
}
async function fetchAvail(month, year) {
const key = year + '-' + String(month).padStart(2, '0');
if (!availCache[key]) {
const res = await fetch('/availability.php?month=' + month + '&year=' + year);
const data = await res.json();
availCache[key] = new Set(data.booked_dates || []);
}
return availCache[key];
}
async function hasRangeConflict(startDate, pkg) {
const endDate = getEndDateStr(startDate, pkg);
const dates = (endDate && endDate !== startDate) ? [startDate, endDate] : [startDate];
for (const date of dates) {
const [y, m] = date.split('-').map(Number);
const set = await fetchAvail(m, y);
if (set.has(date)) return true;
}
return false;
}
function showDateUnavail() {
const msg = document.getElementById('date-unavail-msg');
if (msg) { msg.style.display = 'block'; setTimeout(() => msg.style.display = 'none', 5000); }
}
function updateLegend() {
const rangeItem = document.getElementById('legend-range');
const hint = document.getElementById('calHint');
const isWknd = selectedPackage === 'weekend';
if (rangeItem) rangeItem.style.display = isWknd ? 'flex' : 'none';
if (hint) hint.textContent = isWknd
? 'Click your start date — both weekend days highlight automatically.'
: 'Click any available date to select it.';
}
async function loadCalendar(month, year) { async function loadCalendar(month, year) {
calTitle.textContent = MONTH_NAMES[month - 1] + ' ' + year; calTitle.textContent = MONTH_NAMES[month - 1] + ' ' + year;
calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1">Checking availability&hellip;</div>'; calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1">Checking availability&hellip;</div>';
try { try {
const res = await fetch('/availability.php?month=' + month + '&year=' + year); bookedSet = await fetchAvail(month, year);
const data = await res.json();
bookedSet = new Set(data.booked_dates || []);
renderCalendar(month, year); renderCalendar(month, year);
} catch { } catch {
calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1;color:rgba(239,68,68,0.6)">Could not load availability.</div>'; calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1;color:rgba(239,68,68,0.6)">Could not load availability.</div>';
@@ -928,14 +973,20 @@
const isBooked = bookedSet.has(dateStr); const isBooked = bookedSet.has(dateStr);
const isToday = dateStr === todayStr; const isToday = dateStr === todayStr;
const isSel = dateStr === selectedDate; const isSel = dateStr === selectedDate;
const endDate = getEndDateStr(selectedDate, selectedPackage);
const isInRange = !isSel && selectedDate && endDate && endDate !== selectedDate
&& dateStr > selectedDate && dateStr <= endDate;
if (isSel) { if (isSel) {
el.className = 'cal-day selected'; el.className = 'cal-day selected';
} else if (isInRange) {
el.className = 'cal-day in-range' + (isBooked ? ' booked' : '');
el.title = isBooked ? 'Conflict — this day is unavailable' : 'In your booking range';
} else if (isPast) { } else if (isPast) {
el.className = 'cal-day past'; el.className = 'cal-day past';
} else if (isBooked) { } else if (isBooked) {
el.className = 'cal-day booked'; el.className = 'cal-day booked';
el.title = 'Already booked'; el.title = 'Unavailable';
} else { } else {
el.className = 'cal-day available' + (isToday ? ' today' : ''); el.className = 'cal-day available' + (isToday ? ' today' : '');
el.addEventListener('click', () => selectDate(dateStr)); el.addEventListener('click', () => selectDate(dateStr));
@@ -948,14 +999,14 @@
calGrid.appendChild(fragment); calGrid.appendChild(fragment);
} }
function selectDate(dateStr) { async function selectDate(dateStr) {
selectedDate = dateStr; if (await hasRangeConflict(dateStr, selectedPackage)) {
if (dateInput) { showDateUnavail();
dateInput.value = dateStr; return;
dateInput.dispatchEvent(new Event('change'));
} }
selectedDate = dateStr;
if (dateInput) dateInput.value = dateStr;
renderCalendar(calMonth, calYear); renderCalendar(calMonth, calYear);
// Smooth-scroll form into view
document.getElementById('bookingForm').scrollIntoView({ behavior: 'smooth', block: 'start' }); document.getElementById('bookingForm').scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
@@ -1002,8 +1053,17 @@
const pkgSelect = document.querySelector('select[name="package"]'); const pkgSelect = document.querySelector('select[name="package"]');
const balLabel = document.getElementById('balance-due-label'); const balLabel = document.getElementById('balance-due-label');
const balAmt = document.getElementById('balance-due-amount'); const balAmt = document.getElementById('balance-due-amount');
if (pkgSelect) { selectedPackage = pkgSelect.value || 'half-day'; }
if (pkgSelect) { if (pkgSelect) {
pkgSelect.addEventListener('change', function() { 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);
});
} else { renderCalendar(calMonth, calYear); }
const price = PACKAGE_PRICES[this.value]; const price = PACKAGE_PRICES[this.value];
if (price && balLabel && balAmt) { if (price && balLabel && balAmt) {
balAmt.textContent = '$' + (price - DEPOSIT).toFixed(2).replace(/\.00$/, ''); balAmt.textContent = '$' + (price - DEPOSIT).toFixed(2).replace(/\.00$/, '');