// public/public.js (for index.html) // Convert time string (e.g., '9:30am' or '2:00pm') to minutes since midnight function timeToMinutes(timeStr) { const match = timeStr.match(/([0-9]{1,2}):([0-9]{2})(am|pm)/i); if (!match) { console.error('Invalid time format:', timeStr); return 0; } const [_, hours, minutes, period] = match; let totalHours = parseInt(hours); if (period.toLowerCase() === 'pm' && totalHours !== 12) totalHours += 12; if (period.toLowerCase() === 'am' && totalHours === 12) totalHours = 0; return totalHours * 60 + parseInt(minutes); } // Get UID from URL path function getUidFromPath() { const path = window.location.pathname.substring(1); // Remove leading slash return path || null; } // Validate UID format function isValidUid(uid) { return /^[a-z0-9-]+$/.test(uid); } document.addEventListener('DOMContentLoaded', async () => { // Add custom CSS for the current date to make it more distinguishable const style = document.createElement('style'); style.textContent = ` .date-item.current { border: 2px solid #ffc107 !important; /* Yellow border */ background-color: rgba(255, 193, 7, 0.1) !important; /* Light yellow background */ font-weight: bold !important; color: #212529 !important; /* Ensure text is visible */ } /* Ensure current date styling takes precedence but doesn't interfere with selection */ .date-item.current.selected { border: 2px solid #0d6efd !important; /* Blue border for selected */ background-color: #0d6efd !important; /* Dark blue background when selected */ color: white !important; /* White text for better contrast */ font-weight: bold !important; } `; document.head.appendChild(style); const uid = getUidFromPath(); const mainContent = document.getElementById('mainContent'); const uidError = document.getElementById('uidError'); // Check if we have a valid UID if (!uid || !isValidUid(uid)) { mainContent.style.display = 'none'; uidError.style.display = 'block'; return; } // Hide error, show main content uidError.style.display = 'none'; mainContent.style.display = 'block'; const calendarDates = document.getElementById('calendarDates'); const monthYear = document.getElementById('monthYear'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); const currentTimeElement = document.getElementById('currentTime'); const timeSlotsContainer = document.getElementById('timeSlots'); // Check if navigation buttons exist before adding event listeners if (!prevMonthBtn || !nextMonthBtn) { console.error('Navigation buttons (prevMonth or nextMonth) not found in the DOM'); return; } let currentDate = new Date(); let currentMonth = currentDate.getMonth(); let currentYear = currentDate.getFullYear(); let selectedDate = null; // Track selected date const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; // Fetch available dates from the API async function fetchAvailability() { try { const response = await fetch(`/api/availability/${uid}`); if (response.status === 404) { // UID doesn't exist, show error page mainContent.style.display = 'none'; uidError.style.display = 'block'; return null; } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const availability = await response.json(); console.log('Fetched availability for UID:', uid, availability); return availability; } catch (error) { console.error('Error fetching availability:', error); return {}; } } // Render the calendar with available dates highlighted and automatically select/display the current date async function renderCalendar(month, year) { const availability = await fetchAvailability(); if (availability === null) return; // UID doesn't exist calendarDates.innerHTML = ''; monthYear.textContent = `${months[month]} ${year}`; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const today = new Date(); const currentMinutes = today.getHours() * 60 + today.getMinutes(); // Create blank cells for days before the first day of the month for (let i = 0; i < firstDay; i++) { const blank = document.createElement('div'); blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted'); calendarDates.appendChild(blank); } // Populate the calendar with days for (let i = 1; i <= daysInMonth; i++) { const day = document.createElement('div'); day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center'); day.textContent = i; const date = new Date(year, month, i); const dateStr = date.toISOString().split('T')[0]; // Start of today (midnight) const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Check if date is in the past const isPastDate = date < startOfToday; const isToday = date.toDateString() === today.toDateString(); if (isToday) { day.classList.add('current'); } // Check if the date has available time slots let hasAvailableTimes = false; if (availability[dateStr]) { let times = []; if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { times = availability[dateStr].map(t => t.trim()); } // For today, filter out past times if (isToday) { times = times.filter(time => timeToMinutes(time) > currentMinutes); } // Only mark as available if there are valid times hasAvailableTimes = times.length > 0; } // Only add 'available' class if the date is not in the past AND has available times if (hasAvailableTimes && !isPastDate) { day.classList.add('available'); } if (isPastDate) { day.classList.add('disabled'); } else { day.addEventListener('click', () => selectDate(date, firstDay)); } calendarDates.appendChild(day); } // Auto-select today if we're in the current month/year if (today.getMonth() === month && today.getFullYear() === year) { await selectDate(today, firstDay); // Also update time slots directly since we're on today's date await updateTimeSlots(today); } } // Handle date selection async function selectDate(date, firstDay) { // Check if the date is in the past const today = new Date(); const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); if (date < startOfToday) { console.warn('Attempted to select a past date'); return; } selectedDate = date; const dateItems = document.querySelectorAll('#calendarDates .date-item'); dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected // Find the exact date item for the clicked or auto-selected date const selectedDay = Array.from(dateItems).find(item => { const dayText = item.textContent; const itemDate = new Date(currentYear, currentMonth, parseInt(dayText)); return itemDate.toDateString() === date.toDateString(); }); if (selectedDay) selectedDay.classList.add('selected'); await updateTimeSlots(date); // Update time slots based on selected date, ensuring async completion } // Update time slots based on selected date async function updateTimeSlots(date) { const availability = await fetchAvailability(); const dateStr = date.toISOString().split('T')[0]; console.log('Looking for date:', dateStr); console.log('Available dates:', Object.keys(availability)); let times = []; if (availability[dateStr]) { console.log('Found times for date:', availability[dateStr]); // Ensure availability[dateStr] is a string or array before processing if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { times = availability[dateStr].map(t => t.trim()); } else { console.warn(`Unexpected data format for date ${dateStr}:`, availability[dateStr]); } // Filter out past times if it's today const today = new Date(); const isToday = date.toDateString() === today.toDateString(); console.log('Time filtering - Today:', today.toDateString()); console.log('Time filtering - Selected:', date.toDateString()); console.log('Is today?', isToday); if (isToday) { const currentMinutes = today.getHours() * 60 + today.getMinutes(); console.log('Current minutes:', currentMinutes); console.log('Times before filtering:', times); times = times.filter(time => { const timeMin = timeToMinutes(time); console.log(`Time ${time} = ${timeMin} minutes`); return timeMin > currentMinutes; }); console.log('Times after filtering:', times); } // Sort times chronologically times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); } // Clear existing time slots or message timeSlotsContainer.innerHTML = ''; if (times.length === 0) { // Display "No availability on selected date." if no times are available const noAvailabilityMsg = document.createElement('p'); noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); noAvailabilityMsg.textContent = 'No availability on selected date.'; timeSlotsContainer.appendChild(noAvailabilityMsg); } else { // Populate time slots dynamically if times are available times.forEach(time => { const button = document.createElement('button'); button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2'); button.classList.add('btn-outline-primary', 'available'); button.textContent = time; button.addEventListener('click', () => { const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot'); allTimeSlots.forEach(s => s.classList.remove('selected')); button.classList.add('selected'); // Format date as MM/DD/YY const [year, month, day] = dateStr.split('-'); const formattedDate = `${month}/${day}/${year.slice(2)}`; // Show booking modal document.getElementById('selectedTime').value = time; document.getElementById('selectedDate').value = dateStr; document.getElementById('bookingModalLabel').textContent = `Confirm Appointment for ${formattedDate} at ${time} EST`; // Clear previous form data document.getElementById('bookingForm').reset(); // Show modal const bookingModal = new bootstrap.Modal(document.getElementById('bookingModal')); bookingModal.show(); }); timeSlotsContainer.appendChild(button); }); } } // Navigate to previous month prevMonthBtn.addEventListener('click', () => { currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } renderCalendar(currentMonth, currentYear); }); // Navigate to next month nextMonthBtn.addEventListener('click', () => { currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } renderCalendar(currentMonth, currentYear); }); // Update current time in Eastern Time (US & Canada) function updateCurrentTime() { const options = { timeZone: 'America/New_York', hour: '2-digit', minute: '2-digit', hour12: true }; const time = new Date().toLocaleTimeString('en-US', options); currentTimeElement.textContent = time; } // Update time every second setInterval(updateCurrentTime, 1000); updateCurrentTime(); // Initial call // Form validation and submission const form = document.getElementById('bookingForm'); const submitBtn = document.getElementById('submitBooking'); // Email validation function function isValidEmail(email) { const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return re.test(email); } // Phone validation function (optional field) function isValidPhone(phone) { if (!phone) return true; // Optional field const re = /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/; return re.test(phone); } // Format phone number as user types document.getElementById('phone').addEventListener('input', (e) => { let value = e.target.value.replace(/\D/g, ''); // Remove non-digits if (value.length >= 3) { value = value.slice(0, 3) + '-' + value.slice(3); if (value.length >= 7) { value = value.slice(0, 7) + '-' + value.slice(7); } } e.target.value = value.slice(0, 12); // Limit to XXX-XXX-XXXX }); // Validate fields on blur document.getElementById('name').addEventListener('blur', (e) => { if (!e.target.value.trim()) { e.target.classList.add('is-invalid'); } else { e.target.classList.remove('is-invalid'); } }); document.getElementById('email').addEventListener('blur', (e) => { const email = e.target.value.trim(); if (!email || !isValidEmail(email)) { e.target.classList.add('is-invalid'); } else { e.target.classList.remove('is-invalid'); } }); document.getElementById('phone').addEventListener('blur', (e) => { const phone = e.target.value.trim(); if (phone && !isValidPhone(phone)) { e.target.classList.add('is-invalid'); } else { e.target.classList.remove('is-invalid'); } }); submitBtn.addEventListener('click', async (e) => { e.preventDefault(); const name = document.getElementById('name').value.trim(); const email = document.getElementById('email').value.trim(); const phone = document.getElementById('phone').value.trim(); const notes = document.getElementById('notes').value.trim(); const selectedTime = document.getElementById('selectedTime').value; const selectedDate = document.getElementById('selectedDate').value; // Clear previous validation state form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); // Validate required fields let hasErrors = false; if (!name) { document.getElementById('name').classList.add('is-invalid'); hasErrors = true; } if (!email || !isValidEmail(email)) { document.getElementById('email').classList.add('is-invalid'); hasErrors = true; } // Validate phone if provided if (phone && !isValidPhone(phone)) { document.getElementById('phone').classList.add('is-invalid'); hasErrors = true; } if (hasErrors) { return; } // Prepare booking data const bookingData = { name, email, phone: phone || null, notes: notes || null, date: selectedDate, time: selectedTime }; try { const response = await fetch('/api/book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bookingData) }); if (!response.ok) { throw new Error('Booking failed'); } // Close modal and show success message const modal = bootstrap.Modal.getInstance(document.getElementById('bookingModal')); modal.hide(); alert('Appointment scheduled successfully!'); // Refresh the calendar await renderCalendar(currentMonth, currentYear); } catch (error) { console.error('Error booking appointment:', error); alert('Failed to schedule appointment. Please try again.'); } }); // Initial render and setup await renderCalendar(currentMonth, currentYear); // Force an immediate update of time slots for today's date const today = new Date(); selectedDate = today; // Ensure selectedDate is set await updateTimeSlots(today); // Double-check that time slots are updated if (!timeSlotsContainer.hasChildNodes()) { const noAvailabilityMsg = document.createElement('p'); noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); noAvailabilityMsg.textContent = 'No availability on selected date.'; timeSlotsContainer.appendChild(noAvailabilityMsg); } });