// 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;
}
/* Prevent hover effects on empty calendar cells */
.date-item.empty-cell {
cursor: default !important;
pointer-events: none !important;
}
.date-item.empty-cell:hover {
background-color: transparent !important;
transform: none !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', 'empty-cell');
blank.style.cursor = 'default'; // Remove pointer cursor
blank.style.pointerEvents = 'none'; // Disable hover and click events
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);
}
});