/**
* Calendar Component
* Airbnb-style availability calendar with iCal synchronization
*/
class AvailabilityCalendar {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
if (!this.container) return;
this.options = {
apiUrl: 'api/calendar.php',
months: 2,
locale: 'fr-FR',
weekStart: 1, // Monday
refreshInterval: 30 * 60 * 1000, // 30 minutes
onDateSelect: null,
...options
};
this.bookedDates = new Set();
this.currentMonth = new Date();
this.currentMonth.setDate(1);
this.selectedStart = null;
this.selectedEnd = null;
this.isLoading = true;
this.lastUpdated = null;
this.weekDays = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
this.init();
}
async init() {
this.render();
await this.fetchBookedDates();
this.render();
this.startAutoRefresh();
}
async fetchBookedDates() {
this.isLoading = true;
this.render();
try {
const response = await fetch(this.options.apiUrl);
const data = await response.json();
if (data.success) {
this.bookedDates = new Set(data.booked_dates || []);
this.lastUpdated = data.last_updated;
this.isLoading = false;
} else {
throw new Error(data.error || 'Failed to load calendar');
}
} catch (error) {
console.error('Calendar fetch error:', error);
this.isLoading = false;
this.renderError();
return;
}
}
startAutoRefresh() {
if (this.refreshTimer) clearInterval(this.refreshTimer);
this.refreshTimer = setInterval(() => {
this.fetchBookedDates().then(() => this.render());
}, this.options.refreshInterval);
}
render() {
if (!this.container) return;
if (this.isLoading) {
this.container.innerHTML = `
Chargement du calendrier...
`;
return;
}
const html = `
${this.renderMonth(this.currentMonth)}
${this.renderMonth(this.getNextMonth(this.currentMonth))}
${this.lastUpdated ? `Mis à jour : ${this.lastUpdated}
` : ''}
`;
this.container.innerHTML = html;
this.attachEvents();
}
renderMonth(date) {
const year = date.getFullYear();
const month = date.getMonth();
const monthName = date.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDayOfWeek = this.getAdjustedDay(new Date(year, month, 1).getDay());
let daysHtml = '';
// Weekday headers
for (const day of this.weekDays) {
daysHtml += `${day}
`;
}
// Empty cells before first day
for (let i = 0; i < firstDayOfWeek; i++) {
daysHtml += '';
}
// Day cells
const today = new Date();
today.setHours(0, 0, 0, 0);
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const dateObj = new Date(year, month, day);
const isPast = dateObj < today;
const isToday = dateObj.getTime() === today.getTime();
const isBooked = this.bookedDates.has(dateStr);
const isSelected = this.isDateSelected(dateStr);
const isInRange = this.isDateInRange(dateStr);
let classes = ['calendar-day'];
if (isPast) classes.push('calendar-day--past');
if (isBooked && !isPast) classes.push('calendar-day--booked');
if (isToday) classes.push('calendar-day--today');
if (isSelected) classes.push('calendar-day--selected');
if (isInRange) classes.push('calendar-day--in-range');
if (isPast || isBooked) classes.push('calendar-day--disabled');
daysHtml += `${day}
`;
}
return `
`;
}
renderError() {
if (!this.container) return;
this.container.innerHTML = `
Impossible de charger le calendrier de disponibilité.
`;
}
attachEvents() {
const days = this.container.querySelectorAll('.calendar-day:not(.calendar-day--disabled):not(.calendar-day--empty)');
days.forEach(day => {
day.addEventListener('click', (e) => {
const dateStr = e.target.dataset.date;
if (dateStr) this.selectDate(dateStr);
});
});
}
selectDate(dateStr) {
if (!this.selectedStart || (this.selectedStart && this.selectedEnd)) {
// Start new selection
this.selectedStart = dateStr;
this.selectedEnd = null;
} else {
// Complete the range
if (dateStr < this.selectedStart) {
this.selectedEnd = this.selectedStart;
this.selectedStart = dateStr;
} else if (dateStr === this.selectedStart) {
this.selectedStart = null;
} else {
// Check if any booked date is in the range
if (this.hasBookedDateInRange(this.selectedStart, dateStr)) {
// Reset and start new selection
this.selectedStart = dateStr;
this.selectedEnd = null;
} else {
this.selectedEnd = dateStr;
}
}
}
this.render();
// Sync with booking widget
if (this.options.onDateSelect) {
this.options.onDateSelect(this.selectedStart, this.selectedEnd);
}
// Update booking widget inputs
this.updateBookingWidget();
}
updateBookingWidget() {
const arrivalInput = document.getElementById('booking-arrival');
const departureInput = document.getElementById('booking-departure');
if (arrivalInput && this.selectedStart) {
arrivalInput.value = this.formatDateFr(this.selectedStart);
}
if (departureInput && this.selectedEnd) {
departureInput.value = this.formatDateFr(this.selectedEnd);
}
}
formatDateFr(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
}
hasBookedDateInRange(start, end) {
const startDate = new Date(start);
const endDate = new Date(end);
const current = new Date(startDate);
while (current <= endDate) {
const dateStr = current.toISOString().split('T')[0];
if (this.bookedDates.has(dateStr)) return true;
current.setDate(current.getDate() + 1);
}
return false;
}
isDateSelected(dateStr) {
return dateStr === this.selectedStart || dateStr === this.selectedEnd;
}
isDateInRange(dateStr) {
if (!this.selectedStart || !this.selectedEnd) return false;
return dateStr > this.selectedStart && dateStr < this.selectedEnd;
}
getAdjustedDay(day) {
// Convert Sunday=0 to Monday=0 system
return (day + 6) % 7;
}
getNextMonth(date) {
const next = new Date(date);
next.setMonth(next.getMonth() + 1);
return next;
}
isFirstMonth(date) {
return date.getMonth() === this.currentMonth.getMonth() &&
date.getFullYear() === this.currentMonth.getFullYear();
}
isSecondMonth(date) {
const next = this.getNextMonth(this.currentMonth);
return date.getMonth() === next.getMonth() &&
date.getFullYear() === next.getFullYear();
}
prevMonth() {
const today = new Date();
today.setDate(1);
today.setHours(0, 0, 0, 0);
const prev = new Date(this.currentMonth);
prev.setMonth(prev.getMonth() - 1);
if (prev >= today) {
this.currentMonth = prev;
} else {
this.currentMonth = today;
}
this.render();
}
nextMonth() {
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
this.render();
}
async retry() {
await this.fetchBookedDates();
this.render();
}
getSelectedDates() {
return {
start: this.selectedStart,
end: this.selectedEnd
};
}
destroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
}
}
// Global calendar instance
let calendar;
document.addEventListener('DOMContentLoaded', function() {
calendar = new AvailabilityCalendar('availability-calendar', {
apiUrl: 'api/calendar.php',
onDateSelect: function(start, end) {
// Update booking widget when dates are selected
const arrivalInput = document.getElementById('booking-arrival');
const departureInput = document.getElementById('booking-departure');
if (arrivalInput) {
arrivalInput.value = start ? formatDateShort(start) : '';
arrivalInput.placeholder = start ? '' : 'Ajouter un...';
}
if (departureInput) {
departureInput.value = end ? formatDateShort(end) : '';
departureInput.placeholder = end ? '' : 'Ajouter un...';
}
}
});
});
function formatDateShort(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
}