diff --git a/public/admin.js b/public/admin.js deleted file mode 100644 index ae15582..0000000 --- a/public/admin.js +++ /dev/null @@ -1,1549 +0,0 @@ -// public/admin.js (for admin.html) - -// Global variables -let currentAdminDate = new Date(); -let currentAdminMonth = currentAdminDate.getMonth(); -let currentAdminYear = currentAdminDate.getFullYear(); -let selectedAdminDate = null; -const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - -// Helper function to convert time string to minutes since midnight -function timeToMinutes(timeStr) { - const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); - if (!match) return 0; - - let hours = parseInt(match[1]); - const minutes = parseInt(match[2] || '0'); - const meridian = match[3].toLowerCase(); - - if (meridian === 'pm' && hours !== 12) { - hours += 12; - } else if (meridian === 'am' && hours === 12) { - hours = 0; - } - - return hours * 60 + minutes; -} - -// Helper function to ensure consistent time format -function formatTimeSlot(hour) { - const meridian = hour >= 12 ? 'pm' : 'am'; - const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); - return `${displayHour}:00${meridian}`; -} - -// Render the admin calendar with available dates highlighted -async function renderAdminCalendar(month, year) { - const adminCalendarDates = document.getElementById('adminCalendarDates'); - const adminMonthYear = document.getElementById('adminMonthYear'); - if (!adminCalendarDates || !adminMonthYear) { - console.error('Required calendar elements not found'); - return; - } - - // Clear existing calendar - adminCalendarDates.innerHTML = ''; - adminMonthYear.textContent = `${months[month]} ${year}`; - - // Get current UID and availability - const selectedUid = document.getElementById('uidSelect').value; - if (!selectedUid) { - // If no UID selected, just show empty calendar - return; - } - - 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(); - const availability = await fetchAvailability(); - - // Start of today (midnight) - const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - - // 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 - adminCalendarDates.appendChild(blank); - } - - // Clear any existing selected date - const existingSelected = adminCalendarDates.querySelector('.selected'); - if (existingSelected) { - existingSelected.classList.remove('selected'); - } - - // Populate the calendar with days - let todayElement = null; - 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]; - - // Check if date is in the past - const isPastDate = date < startOfToday; - const isToday = date.toDateString() === today.toDateString(); - - if (isToday) { - day.classList.add('current'); - todayElement = day; - } - - // 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', () => selectAdminDate(date, firstDay)); - } - adminCalendarDates.appendChild(day); - } - - // If a date was previously selected, re-select it - if (selectedAdminDate && - selectedAdminDate.getMonth() === month && - selectedAdminDate.getFullYear() === year) { - selectAdminDate(selectedAdminDate, firstDay); - } -} - -// Handle admin date selection -async function selectAdminDate(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; - } - - // Update selected date and time slots - selectedAdminDate = date; - await updateTimeSlots(date); - const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); - dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected - // Find the exact date item for the clicked date - const selectedDay = Array.from(dateItems).find(item => { - const dayText = item.textContent; - const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText)); - return itemDate.toDateString() === date.toDateString(); - }); - if (selectedDay) selectedDay.classList.add('selected'); - - // Remove the selected date display text - const selectedDateDisplay = document.getElementById('selectedDateDisplay'); - if (selectedDateDisplay) { - selectedDateDisplay.textContent = ''; - } -} - -// Update time slots based on selected date -async function updateTimeSlots(date) { - const availability = await fetchAvailability(); - const dateStr = date.toISOString().split('T')[0]; - let availableTimes = []; - - // Handle case where availability might be undefined or null - if (availability && availability[dateStr]) { - if (typeof availability[dateStr] === 'string') { - availableTimes = availability[dateStr].split(',').map(t => t.trim()); - } else if (Array.isArray(availability[dateStr])) { - availableTimes = [...availability[dateStr]]; - } - } - - // Sort available times chronologically - availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); - - const timeSlots = document.getElementById('timeSlots'); - if (!timeSlots) return; - - // Clear existing time slots - timeSlots.innerHTML = ''; - - // Create time button directly without the container and heading - const timeButton = document.createElement('button'); - timeButton.type = 'button'; - timeButton.id = 'customTimeInput'; - timeButton.classList.add('btn', 'btn-success', 'btn-lg', 'w-100', 'mb-4'); - timeButton.style.backgroundColor = '#2e8b57'; // Money green color - timeButton.style.borderColor = '#2e8b57'; - timeButton.style.fontSize = '1.25rem'; - timeButton.style.padding = '0.75rem 1.25rem'; - timeButton.innerHTML = '<i class="bi bi-clock"></i> Add Time Slot'; - - // Add button directly to time slots - timeSlots.appendChild(timeButton); - - // Initialize visual time picker - initializeVisualTimePicker(dateStr, availableTimes); - - // Add divider - const divider = document.createElement('hr'); - timeSlots.appendChild(divider); - - // Display existing time slots - if (availableTimes.length === 0) { - const noTimesMessage = document.createElement('p'); - noTimesMessage.classList.add('text-muted', 'text-center'); - noTimesMessage.textContent = 'No time slots available for this date.'; - timeSlots.appendChild(noTimesMessage); - } else { - // Create container for time slot list - const timeSlotsContainer = document.createElement('div'); - timeSlotsContainer.classList.add('d-flex', 'flex-column', 'gap-2', 'mt-3'); - - // Display all slots in chronological order - availableTimes.forEach(time => { - // Create a container div for the time slot - const timeSlotContainer = document.createElement('div'); - timeSlotContainer.classList.add('position-relative', 'w-100', 'd-flex', 'align-items-center', 'mb-2'); - - // Create the time slot pill - const timeSlotItem = document.createElement('div'); - timeSlotItem.classList.add('btn', 'btn-light', 'rounded-pill', 'px-4', 'py-3', 'flex-grow-1'); - timeSlotItem.style.backgroundColor = '#e9f2ff'; - timeSlotItem.style.color = '#0d6efd'; - timeSlotItem.style.border = '1px solid #0d6efd'; - timeSlotItem.style.textAlign = 'center'; - timeSlotItem.style.fontSize = '1.1rem'; - timeSlotItem.style.fontWeight = '500'; - timeSlotItem.style.transition = 'all 0.2s ease'; - - // Add time text directly to the pill - timeSlotItem.textContent = time; - - // Add delete button/icon as a separate element - const deleteIcon = document.createElement('button'); - deleteIcon.innerHTML = '<i class="bi bi-trash"></i>'; - deleteIcon.classList.add('btn', 'btn-sm', 'text-danger', 'ms-3'); - deleteIcon.style.backgroundColor = 'transparent'; - deleteIcon.style.border = 'none'; - deleteIcon.style.fontSize = '1.2rem'; - deleteIcon.style.padding = '8px'; - deleteIcon.style.cursor = 'pointer'; - deleteIcon.style.transition = 'all 0.2s ease'; - deleteIcon.style.opacity = '0.8'; - deleteIcon.style.borderRadius = '50%'; - deleteIcon.style.display = 'flex'; - deleteIcon.style.alignItems = 'center'; - deleteIcon.style.justifyContent = 'center'; - - // Add hover effects - timeSlotItem.addEventListener('mouseenter', () => { - timeSlotItem.style.backgroundColor = '#0d6efd'; - timeSlotItem.style.color = 'white'; - timeSlotItem.style.transform = 'translateY(-2px)'; - timeSlotItem.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; - }); - - timeSlotItem.addEventListener('mouseleave', () => { - timeSlotItem.style.backgroundColor = '#e9f2ff'; - timeSlotItem.style.color = '#0d6efd'; - timeSlotItem.style.transform = 'translateY(0)'; - timeSlotItem.style.boxShadow = 'none'; - }); - - // Add hover effects for delete icon - deleteIcon.addEventListener('mouseenter', () => { - deleteIcon.style.opacity = '1'; - deleteIcon.style.transform = 'scale(1.2)'; - deleteIcon.style.backgroundColor = 'rgba(220, 53, 69, 0.1)'; - }); - - deleteIcon.addEventListener('mouseleave', () => { - deleteIcon.style.opacity = '0.8'; - deleteIcon.style.transform = 'scale(1)'; - deleteIcon.style.backgroundColor = 'transparent'; - }); - - deleteIcon.addEventListener('click', (e) => { - e.stopPropagation(); - removeTime(dateStr, time); - }); - - timeSlotContainer.appendChild(timeSlotItem); - timeSlotContainer.appendChild(deleteIcon); - timeSlotsContainer.appendChild(timeSlotContainer); - }); - - timeSlots.appendChild(timeSlotsContainer); - } -} - -// Initialize visual time picker function -function initializeVisualTimePicker(dateStr, availableTimes) { - const timeButton = document.getElementById('customTimeInput'); - if (!timeButton) return; - - // Remove any existing time picker - const existingTimePicker = document.getElementById('visualTimePicker'); - if (existingTimePicker) { - existingTimePicker.remove(); - } - - // Create the timepicker modal from the template - moved to a separate function for clarity - document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate()); - - // Initialize timepicker functionality and store the reference to the controller - const timepickerController = initializeTimepickerFunctionality(dateStr, availableTimes); - - // Show time picker when button is clicked - timeButton.addEventListener('click', (e) => { - showTimepicker(timepickerController); - e.stopPropagation(); - }); - - // Close time picker when clicking outside - document.addEventListener('click', (e) => { - const timepicker = document.getElementById('visualTimePicker'); - const timepickerContainer = document.querySelector('.timepicker-container'); - - // Only close if we're not in the middle of a drag operation - if (timepicker && - timepicker.style.display === 'block' && - e.target !== timeButton && - (!timepickerContainer || !timepickerContainer.contains(e.target)) && - !window.timepickerDragging) { // Check the global dragging flag - timepickerController.closeTimepicker(); - } - }); -} - -// Helper function to show the timepicker -function showTimepicker(timepickerController) { - const timepicker = document.getElementById('visualTimePicker'); - if (!timepicker) return; - - // First make the timepicker visible but with opacity 0 - timepicker.style.display = 'block'; - timepicker.style.opacity = '0'; - - // Force a reflow to ensure the clock dimensions are calculated - setTimeout(() => { - // Always start with hours view when opening the timepicker - timepickerController.switchToHoursView(); - - // Make the timepicker visible - timepicker.style.opacity = '1'; - document.body.style.overflow = 'hidden'; - document.body.style.paddingRight = '0px'; - }, 10); -} - -// Create the timepicker HTML template -function createTimepickerTemplate() { - return ` - <div class="timepicker-modal" role="dialog" tabindex="-1" id="visualTimePicker" style="display: none;"> - <div id="timepickerWrapper" class="timepicker-wrapper h-100 d-flex align-items-center justify-content-center flex-column position-fixed animation fade-in" style="animation-duration: 300ms;"> - <div class="d-flex align-items-center justify-content-center flex-column timepicker-container"> - <div class="d-flex flex-column timepicker-elements justify-content-around"> - - <div id="timepickerHead" class="timepicker-head d-flex flex-row align-items-center justify-content-center" style="padding-right:0px"> - <div class="timepicker-head-content d-flex w-100 justify-content-evenly"> - <div class="timepicker-current-wrapper"> - <span class="position-relative h-100"> - <button type="button" class="timepicker-current timepicker-hour active" tabindex="0" style="pointer-events: none;">12</button> - </span> - <button type="button" class="timepicker-dot" disabled="">:</button> - <span class="position-relative h-100"> - <button type="button" class="timepicker-current timepicker-minute" tabindex="0">00</button> - </span> - </div> - - <div class="d-flex flex-column justify-content-center timepicker-mode-wrapper"> - <button type="button" class="timepicker-hour-mode timepicker-am" tabindex="0">AM</button> - <button type="button" class="timepicker-hour-mode timepicker-pm active" tabindex="0">PM</button> - </div> - </div> - </div> - - <div id="timepickerClockWrapper" class="timepicker-clock-wrapper d-flex justify-content-center flex-column align-items-center"> - <div class="timepicker-clock timepicker-clock-animation"> - <span class="timepicker-middle-dot position-absolute"></span> - <div class="timepicker-hand-pointer position-absolute" style="transform: rotateZ(360deg);"> - <div class="timepicker-circle position-absolute active"></div> - </div> - <!-- Clock face will be dynamically generated by JS --> - </div> - </div> - </div> - - <div id="timepickerFooter" class="timepicker-footer"> - <div class="w-100 d-flex justify-content-between"> - <button type="button" class="timepicker-button timepicker-clear" tabindex="0">Clear</button> - <button type="button" class="timepicker-button timepicker-cancel" tabindex="0">Cancel</button> - <button type="button" class="timepicker-button timepicker-submit" tabindex="0">Ok</button> - </div> - </div> - </div> - </div> - </div>`; -} - -// Initialize the timepicker functionality -function initializeTimepickerFunctionality(dateStr, availableTimes) { - // DOM elements - group related elements together - const timeElements = { - hour: document.querySelector('.timepicker-hour'), - minute: document.querySelector('.timepicker-minute'), - am: document.querySelector('.timepicker-am'), - pm: document.querySelector('.timepicker-pm'), - dot: document.querySelector('.timepicker-dot') - }; - - const clockElements = { - wrapper: document.querySelector('.timepicker-clock-wrapper'), - clock: document.querySelector('.timepicker-clock'), - hand: document.querySelector('.timepicker-hand-pointer') - }; - - const actionButtons = { - clear: document.querySelector('.timepicker-clear'), - cancel: document.querySelector('.timepicker-cancel'), - submit: document.querySelector('.timepicker-submit') - }; - - // State variables - const state = { - currentView: 'hours', - selectedHour: 12, - selectedMinute: 0, - isDragging: false, - isPM: timeElements.pm.classList.contains('active') - }; - - // Create a global variable to track if we're dragging from inside the timepicker - window.timepickerDragging = false; - - // Initialize - setupEventListeners(); - renderClockFace(state.currentView); - - function setupEventListeners() { - // Header hour/minute buttons - timeElements.hour.addEventListener('click', () => switchView('hours')); - timeElements.minute.addEventListener('click', () => switchView('minutes')); - - // Clock face events - clockElements.clock.addEventListener('mousedown', handleClockMouseDown); - document.addEventListener('mousemove', handleClockMouseMove); - document.addEventListener('mouseup', handleClockMouseUp); - - // Prevent text selection on the clock - clockElements.clock.style.userSelect = 'none'; - clockElements.clock.style.webkitUserSelect = 'none'; - clockElements.clock.style.msUserSelect = 'none'; - - // AM/PM buttons - timeElements.am.addEventListener('click', () => setAmPm('AM')); - timeElements.pm.addEventListener('click', () => setAmPm('PM')); - - // Action buttons - actionButtons.clear.addEventListener('click', clearTime); - actionButtons.cancel.addEventListener('click', closeTimepicker); - actionButtons.submit.addEventListener('click', submitTime); - } - - function switchView(view) { - state.currentView = view; - - // Update active state in header - if (view === 'hours') { - timeElements.hour.classList.add('active'); - timeElements.minute.classList.remove('active'); - timeElements.hour.style.pointerEvents = 'none'; - timeElements.minute.style.pointerEvents = 'auto'; - } else { - timeElements.hour.classList.remove('active'); - timeElements.minute.classList.add('active'); - timeElements.hour.style.pointerEvents = 'auto'; - timeElements.minute.style.pointerEvents = 'none'; - } - - renderClockFace(view); - } - - function renderClockFace(view) { - // Clear existing time tips - const existingTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes'); - existingTips.forEach(tip => tip.remove()); - - if (view === 'hours') { - renderHoursFace(); - updateHandPosition(state.selectedHour, 'hours'); - } else { - renderMinutesFace(); - updateHandPosition(state.selectedMinute, 'minutes'); - } - } - - function renderHoursFace() { - const clockRadius = clockElements.clock.offsetWidth / 2; - const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) - - for (let hour = 1; hour <= 12; hour++) { - const tip = createClockTip(hour, clockRadius, tipRadius, 'hours'); - - if (hour === state.selectedHour) { - tip.classList.add('active'); - } - - clockElements.clock.appendChild(tip); - } - } - - function renderMinutesFace() { - const clockRadius = clockElements.clock.offsetWidth / 2; - const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) - - for (let minute = 0; minute < 60; minute += 5) { - const tip = createClockTip(minute, clockRadius, tipRadius, 'minutes'); - - if (minute === state.selectedMinute) { - tip.classList.add('active'); - } - - clockElements.clock.appendChild(tip); - } - } - - // Helper function to create clock face tips (numbers) - function createClockTip(value, clockRadius, tipRadius, type) { - // Calculate angle and position - let angle, displayValue; - - if (type === 'hours') { - angle = ((value * 30) - 90) * (Math.PI / 180); - displayValue = value; - } else { - angle = ((value * 6) - 90) * (Math.PI / 180); - displayValue = value.toString().padStart(2, '0'); - } - - // Calculate position - const left = clockRadius + tipRadius * Math.cos(angle); - const top = clockRadius + tipRadius * Math.sin(angle); - - // Create the tip element - const tip = document.createElement('span'); - tip.className = `timepicker-time-tips-${type}`; - tip.style.left = `${left}px`; - tip.style.top = `${top}px`; - tip.style.position = 'absolute'; - tip.style.transform = 'translate(-50%, -50%)'; - tip.style.userSelect = 'none'; - tip.style.webkitUserSelect = 'none'; - tip.style.msUserSelect = 'none'; - - const tipElement = document.createElement('span'); - tipElement.className = 'timepicker-tips-element'; - tipElement.textContent = displayValue; - - tip.appendChild(tipElement); - return tip; - } - - function updateHandPosition(value, view) { - let angle; - - if (view === 'hours') { - // For hours, convert 12 to 0 for calculation purposes - const hour = value === 12 ? 0 : value; - angle = (hour / 12) * 360; - } else { - angle = (value / 60) * 360; - } - - clockElements.hand.style.transform = `rotateZ(${angle}deg)`; - - // Update the active circle on the hand - const circle = clockElements.hand.querySelector('.timepicker-circle'); - if (circle) { - circle.classList.add('active'); - } - } - - function handleClockMouseDown(event) { - state.isDragging = true; - // Set the global flag to indicate we're dragging from inside the timepicker - window.timepickerDragging = true; - updateTimeFromClockPosition(event); - } - - function handleClockMouseMove(event) { - if (state.isDragging) { - updateTimeFromClockPosition(event); - } - } - - function handleClockMouseUp() { - if (state.isDragging) { - state.isDragging = false; - - // Reset the global dragging flag after a short delay - // This allows the click event to process first - setTimeout(() => { - window.timepickerDragging = false; - }, 10); - - // If we just finished selecting an hour, switch to minutes - if (state.currentView === 'hours') { - switchView('minutes'); - } - } - } - - function updateTimeFromClockPosition(event) { - const clockRect = clockElements.clock.getBoundingClientRect(); - const centerX = clockRect.left + clockRect.width / 2; - const centerY = clockRect.top + clockRect.height / 2; - - // Calculate angle from center to mouse position - const x = event.clientX - centerX; - const y = event.clientY - centerY; - - // Calculate angle in degrees, starting from 12 o'clock position - let angle = Math.atan2(y, x) * (180 / Math.PI) + 90; - - // Normalize angle to 0-360 - if (angle < 0) { - angle += 360; - } - - if (state.currentView === 'hours') { - updateHourFromAngle(angle); - } else { - updateMinuteFromAngle(angle); - } - } - - function updateHourFromAngle(angle) { - // Convert angle to hour (1-12) - let hour = Math.round(angle / 30); - - // Handle edge cases for full circle - if (hour === 0 || hour > 12) hour = 12; - - state.selectedHour = hour; - timeElements.hour.textContent = hour; - updateHandPosition(hour, 'hours'); - - // Update active class on hour tips - const hourTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours'); - hourTips.forEach(tip => { - const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); - if (hourValue === state.selectedHour) { - tip.classList.add('active'); - } else { - tip.classList.remove('active'); - } - }); - } - - function updateMinuteFromAngle(angle) { - // Convert angle to minute (0-55, step 5) - let minute = Math.floor(angle / 6); - - // Round to nearest 5 - minute = Math.round(minute / 5) * 5; - - // Handle edge case for full circle - if (minute >= 60) minute = 0; - - state.selectedMinute = minute; - timeElements.minute.textContent = minute.toString().padStart(2, '0'); - updateHandPosition(minute, 'minutes'); - - // Update active class on minute tips - const minuteTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-minutes'); - minuteTips.forEach(tip => { - const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); - if (minuteValue === state.selectedMinute) { - tip.classList.add('active'); - } else { - tip.classList.remove('active'); - } - }); - } - - function setAmPm(period) { - if (period === 'AM') { - timeElements.am.classList.add('active'); - timeElements.pm.classList.remove('active'); - state.isPM = false; - } else { - timeElements.am.classList.remove('active'); - timeElements.pm.classList.add('active'); - state.isPM = true; - } - } - - function clearTime() { - state.selectedHour = 12; - state.selectedMinute = 0; - timeElements.hour.textContent = '12'; - timeElements.minute.textContent = '00'; - setAmPm('PM'); - switchView('hours'); - } - - function closeTimepicker() { - const timepicker = document.getElementById('visualTimePicker'); - if (timepicker) { - timepicker.style.display = 'none'; - document.body.style.overflow = ''; - document.body.style.paddingRight = ''; - } - } - - function submitTime() { - const formattedHour = state.selectedHour; - const formattedMinute = state.selectedMinute.toString().padStart(2, '0'); - const period = state.isPM ? 'PM' : 'AM'; - - const timeValue = `${formattedHour}:${formattedMinute}${period.toLowerCase()}`; - - // Check if time already exists in available times - if (availableTimes.includes(timeValue)) { - // Silently handle duplicate time slots - no error message - console.log('Time slot already exists, not adding duplicate'); - } else { - // Automatically add the time slot - addSingleTime(dateStr, timeValue); - } - - closeTimepicker(); - } - - // Return controller object with public methods - return { - renderClockFace, - closeTimepicker, - switchToHoursView: () => switchView('hours') - }; -} - -// Fetch available dates from the API -async function fetchAvailability() { - try { - const selectedUid = document.getElementById('uidSelect').value; - if (!selectedUid) return {}; - - const response = await fetch(`/api/availability/${selectedUid}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const availability = await response.json(); - return availability; - } catch (error) { - console.error('Error fetching availability:', error); - return {}; - } -} - -// Helper function to update calendar UI after availability changes -async function updateAvailabilityUI() { - const availability = await fetchAvailability(); - if (selectedAdminDate) { - await updateTimeSlots(selectedAdminDate); - } - - const calendarDates = document.getElementById('adminCalendarDates'); - if (calendarDates) { - const allDateElements = calendarDates.querySelectorAll('.date-item'); - const today = new Date(); - const currentMinutes = today.getHours() * 60 + today.getMinutes(); - - allDateElements.forEach(dateElement => { - if (!dateElement.textContent) return; // Skip empty cells - const day = parseInt(dateElement.textContent); - const date = new Date(currentAdminYear, currentAdminMonth, day); - const dateStr = date.toISOString().split('T')[0]; - - // Check if date is in the past - const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - const isPastDate = date < startOfToday; - const isToday = date.toDateString() === today.toDateString(); - - // 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) { - dateElement.classList.add('available'); - } else { - dateElement.classList.remove('available'); - } - - // Always ensure past dates have the disabled class - if (isPastDate) { - dateElement.classList.add('disabled'); - dateElement.classList.remove('available'); - } - }); - } -} - -// UID management functions -async function loadUids() { - try { - const response = await fetch('/api/uids'); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to fetch UIDs'); - } - const uids = await response.json(); - - const select = document.getElementById('uidSelect'); - select.innerHTML = '<option value="">Select a Schedule UID...</option>'; - - uids.forEach(uidObj => { - const option = document.createElement('option'); - option.value = uidObj.uid; - option.textContent = uidObj.uid; - select.appendChild(option); - }); - } catch (error) { - console.error('Error loading UIDs:', error); - } -} - -async function createUid(uid) { - try { - const response = await fetch('/api/uids', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ uid }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create UID'); - } - - // Reload UIDs and select the new one - await loadUids(); - - // Select the new UID and trigger change event - const select = document.getElementById('uidSelect'); - select.value = uid; - select.dispatchEvent(new Event('change')); - - - return true; - } catch (error) { - console.error('Error creating UID:', error); - return false; - } -} - -async function deleteUid(uid) { - try { - const response = await fetch(`/api/uids/${uid}`, { - method: 'DELETE' - }); - - if (!response.ok) throw new Error('Failed to delete UID'); - await loadUids(); - return true; - } catch (error) { - console.error('Error deleting UID:', error); - return false; - } -} - - - -// Add a single time for the selected date -async function addSingleTime(dateStr, time) { - const selectedUid = document.getElementById('uidSelect').value; - if (!selectedUid) { - alert('Please select a UID first'); - return; - } - - try { - const availability = await fetchAvailability(); - let times = []; - if (availability && availability[dateStr]) { - if (typeof availability[dateStr] === 'string') { - times = availability[dateStr].split(',').map(t => t.trim()); - } else if (Array.isArray(availability[dateStr])) { - times = [...availability[dateStr]]; - } - } - times.push(time); - - const response = await fetch(`/api/availability/${selectedUid}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: dateStr, times: [...new Set(times)] }) // Remove duplicates - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - console.log(result.message); - await updateAvailabilityUI(); - } catch (error) { - console.error('Error adding time slot:', error); - alert('Failed to add time slot. Please try again.'); - } -} - -// Remove a single time for the selected date -async function removeTime(dateStr, time) { - const selectedUid = document.getElementById('uidSelect').value; - if (!selectedUid) { - alert('Please select a UID first'); - return; - } - - try { - const availability = await fetchAvailability(); - let times = []; - if (availability && availability[dateStr]) { - if (typeof availability[dateStr] === 'string') { - times = availability[dateStr].split(',').filter(t => t.trim() !== time); - } else if (Array.isArray(availability[dateStr])) { - times = availability[dateStr].filter(t => t.trim() !== time); - } - } - - const response = await fetch(`/api/availability/${selectedUid}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: dateStr, times }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `HTTP error! status: ${response.status}`); - } - await updateAvailabilityUI(); - } catch (error) { - console.error('Error removing time slot:', error); - alert('Failed to remove time slot. Please try again.'); - } -} - -// Helper function to convert time string to minutes since midnight -function timeToMinutes(timeStr) { - const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); - if (!match) return 0; - - let [_, hours, minutes, period] = match; - hours = parseInt(hours); - minutes = minutes ? parseInt(minutes) : 0; - period = period.toLowerCase(); - - if (period === 'pm' && hours !== 12) hours += 12; - if (period === 'am' && hours === 12) hours = 0; - - return hours * 60 + minutes; -} - -// Helper function to ensure consistent time format -function formatTimeSlot(hour) { - const period = hour < 12 ? 'am' : 'pm'; - const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour); - return `${displayHour}:00${period}`; -} - -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; - } - - /* Time picker styling */ - #visualTimePicker { - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; - border-radius: 8px; - overflow: hidden; - } - - #visualTimePicker .card-header { - background-color: #0d6efd; - } - - #visualTimePicker .display-4 { - font-size: 3rem; - font-weight: 300; - color: white; - } - - .time-picker-clock { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - } - - .hour-number, .minute-number { - border-radius: 50%; - transition: all 0.2s ease; - } - - .hour-number:hover, .minute-number:hover { - background-color: rgba(13, 110, 253, 0.1); - } - - /* Time slot pills styling */ - .time-slot { - transition: all 0.2s ease; - } - - .time-slot:hover { - transform: translateY(-2px); - } - - /* Make all UID buttons consistent */ - #createUid, #deleteUid, #flushDatabase, #copyUrlBtn { - border-radius: 30px; - padding: 0.75rem 1.5rem; - font-size: 1.25rem; - font-weight: 500; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - transition: all 0.3s ease; - } - - #createUid { - background-color: #2e8b57; /* Money green color */ - border-color: #2e8b57; - } - - #createUid:hover { - background-color: #267349; - border-color: #267349; - } - - /* URL container styling */ - .url-container { - display: flex; - align-items: center; - gap: 10px; - } - - .url-display { - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - } - `; - document.head.appendChild(style); - - // Create and add the Copy URL button - const urlContainer = document.querySelector('.form-control-lg.bg-light.p-3'); - if (urlContainer) { - // Create a wrapper div for the URL and button - const wrapper = document.createElement('div'); - wrapper.classList.add('url-container', 'mt-2'); - wrapper.style.display = 'flex'; - wrapper.style.alignItems = 'center'; - wrapper.style.gap = '10px'; - wrapper.style.flexWrap = 'nowrap'; // Prevent wrapping - - // Move the existing elements to the wrapper - const uidPlaceholder = document.getElementById('uidPlaceholder'); - const uidUrl = document.getElementById('uidUrl'); - - // Create a div to contain the URL display - const urlDisplay = document.createElement('div'); - urlDisplay.classList.add('url-display'); - urlDisplay.style.overflow = 'hidden'; - urlDisplay.style.textOverflow = 'ellipsis'; - urlDisplay.style.whiteSpace = 'nowrap'; // Keep URL on one line - urlDisplay.style.minWidth = '0'; // Allow flex item to shrink below content size - urlDisplay.style.flexGrow = '1'; // Take available space - urlDisplay.style.flexShrink = '1'; // Allow shrinking - - // Move the existing elements to the URL display div - if (uidPlaceholder) urlDisplay.appendChild(uidPlaceholder); - if (uidUrl) urlDisplay.appendChild(uidUrl); - - // Add the URL display to the wrapper - wrapper.appendChild(urlDisplay); - - // Create the Copy URL button - const copyUrlBtn = document.createElement('button'); - copyUrlBtn.id = 'copyUrlBtn'; - copyUrlBtn.classList.add('btn', 'btn-primary', 'rounded-pill', 'd-none'); - copyUrlBtn.style.height = '40px'; - copyUrlBtn.style.padding = '0 15px'; - copyUrlBtn.style.fontSize = '0.9rem'; - copyUrlBtn.style.whiteSpace = 'nowrap'; - copyUrlBtn.style.width = 'auto'; // Only as wide as content - copyUrlBtn.style.minWidth = 'fit-content'; // Ensure it fits content - copyUrlBtn.style.flexShrink = '0'; // Prevent button from shrinking - - copyUrlBtn.innerHTML = '<i class="bi bi-clipboard"></i><span style="margin-left: 8px;">Copy URL</span>'; - - // Add click event to copy URL to clipboard - copyUrlBtn.addEventListener('click', () => { - const url = document.getElementById('uidUrl').href; - if (url && url !== '#') { - navigator.clipboard.writeText(url) - .then(() => { - // Change button text temporarily to indicate success - const originalText = copyUrlBtn.innerHTML; - copyUrlBtn.innerHTML = '<i class="bi bi-check"></i> Copied!'; - copyUrlBtn.classList.remove('btn-primary'); - copyUrlBtn.classList.add('btn-success'); - - // Reset button after 2 seconds - setTimeout(() => { - copyUrlBtn.innerHTML = originalText; - copyUrlBtn.classList.remove('btn-success'); - copyUrlBtn.classList.add('btn-primary'); - }, 2000); - }) - .catch(err => { - console.error('Failed to copy URL: ', err); - alert('Failed to copy URL to clipboard'); - }); - } - }); - - // Add the button to the wrapper - wrapper.appendChild(copyUrlBtn); - - // Clear the container and add the wrapper - urlContainer.innerHTML = ''; - urlContainer.appendChild(document.createElement('small')).classList.add('d-block', 'text-muted', 'mb-1'); - urlContainer.querySelector('small').textContent = 'Public Access URL:'; - urlContainer.appendChild(wrapper); - - // Show the button if a UID is already selected - const selectedUid = document.getElementById('uidSelect').value; - if (selectedUid) { - copyUrlBtn.classList.remove('d-none'); - } - } - - // Load existing UIDs - await loadUids(); - - // Handle UID selection - const uidSelect = document.getElementById('uidSelect'); - const deleteUidBtn = document.getElementById('deleteUid'); - const uidUrl = document.getElementById('uidUrl'); - const uidPlaceholder = document.getElementById('uidPlaceholder'); - - uidSelect.addEventListener('change', async () => { - const selectedUid = uidSelect.value; - deleteUidBtn.disabled = !selectedUid; - flushDatabaseBtn.disabled = !selectedUid; - - // Show or hide calendar and availability sections based on UID selection - const calendarSection = document.getElementById('calendarSection'); - const calendarDatesSection = document.getElementById('calendarDatesSection'); - const availabilitySection = document.getElementById('availabilitySection'); - - if (selectedUid) { - calendarSection.classList.remove('d-none'); - calendarDatesSection.classList.remove('d-none'); - availabilitySection.classList.remove('d-none'); - - // Show URL link and hide placeholder - uidPlaceholder.classList.add('d-none'); - uidUrl.classList.remove('d-none'); - - // Show copy button when URL is displayed - if (document.getElementById('copyUrlBtn')) { - document.getElementById('copyUrlBtn').classList.remove('d-none'); - } - } else { - calendarSection.classList.add('d-none'); - calendarDatesSection.classList.add('d-none'); - availabilitySection.classList.add('d-none'); - - // Hide URL link and show placeholder - uidPlaceholder.classList.remove('d-none'); - uidUrl.classList.add('d-none'); - - // Hide copy button when no URL is displayed - if (document.getElementById('copyUrlBtn')) { - document.getElementById('copyUrlBtn').classList.add('d-none'); - } - } - - // Clear current selections - selectedAdminDate = null; - const selectedDateElem = document.getElementById('selectedDateDisplay'); - const timeSlotsElem = document.getElementById('timeSlots'); - const calendarDatesElem = document.getElementById('adminCalendarDates'); - const adminMonthYear = document.getElementById('adminMonthYear'); - - // Clear all UI elements - if (selectedDateElem) selectedDateElem.textContent = ''; - if (timeSlotsElem) timeSlotsElem.innerHTML = ''; - if (calendarDatesElem) calendarDatesElem.innerHTML = ''; - if (adminMonthYear) adminMonthYear.textContent = ''; - - if (selectedUid) { - const url = `${window.location.origin}/${selectedUid}`; - uidUrl.textContent = url; - uidUrl.href = url; - - // Refresh calendar with new UID's data - await renderAdminCalendar(currentAdminMonth, currentAdminYear); - - // Automatically select today's date and show time slots - const today = new Date(); - if (today.getMonth() === currentAdminMonth && today.getFullYear() === currentAdminYear) { - // Get the first day of the month to pass to selectAdminDate - const firstDay = new Date(currentAdminYear, currentAdminMonth, 1).getDay(); - - // Select today's date - selectedAdminDate = today; - await updateTimeSlots(today); - - // Highlight today in the calendar - const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); - dateItems.forEach(item => item.classList.remove('selected')); - - // Find today's element in the calendar - const todayDay = today.getDate(); - const todayElement = Array.from(dateItems).find(item => { - return item.textContent && parseInt(item.textContent) === todayDay; - }); - - if (todayElement) { - todayElement.classList.add('selected'); - } - } - } else { - uidUrl.textContent = ''; - uidUrl.href = '#'; - // Add placeholder text - if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar'; - await renderAdminCalendar(currentAdminMonth, currentAdminYear); - } - }); - - // Handle new UID creation - const createUidBtn = document.getElementById('createUid'); - const modalUidInput = document.getElementById('modalUidInput'); - const confirmCreateUidBtn = document.getElementById('confirmCreateUid'); - const createUidModal = new bootstrap.Modal(document.getElementById('createUidModal')); - - // Add event listener for modal shown event to focus the input field - document.getElementById('createUidModal').addEventListener('shown.bs.modal', function () { - modalUidInput.focus(); - }); - - createUidBtn.addEventListener('click', () => { - modalUidInput.value = ''; - createUidModal.show(); - }); - - // Add event listener for Enter key on the input field - modalUidInput.addEventListener('keyup', (event) => { - if (event.key === 'Enter') { - confirmCreateUidBtn.click(); - } - }); - - confirmCreateUidBtn.addEventListener('click', async () => { - const uid = modalUidInput.value.trim().toLowerCase(); - - if (!/^[a-z0-9-]+$/.test(uid)) { - alert('UID can only contain lowercase letters, numbers, and hyphens'); - return; - } - - if (await createUid(uid)) { - createUidModal.hide(); - } else { - alert('Failed to create UID. Please try again.'); - } - }); - - // Handle UID deletion - deleteUidBtn.addEventListener('click', async () => { - const uid = uidSelect.value; - if (!uid) return; - - if (!confirm(`Are you sure you want to delete UID: ${uid}?\nThis will remove all associated availability data.`)) { - return; - } - - if (await deleteUid(uid)) { - // Clear selection and trigger change event - uidSelect.value = ''; - uidSelect.dispatchEvent(new Event('change')); - } else { - alert('Failed to delete UID. Please try again.'); - } - }); - - // Initialize UI elements - const adminCalendarDates = document.getElementById('adminCalendarDates'); - const adminMonthYear = document.getElementById('adminMonthYear'); - const adminPrevMonthBtn = document.getElementById('adminPrevMonth'); - const adminNextMonthBtn = document.getElementById('adminNextMonth'); - const selectedDateDisplay = document.getElementById('selectedDateDisplay'); - const timeSlots = document.getElementById('timeSlots'); - const flushDatabaseBtn = document.getElementById('flushDatabase'); - const resetDatabaseBtn = document.getElementById('resetDatabase'); - const devToolsBtn = document.getElementById('devToolsBtn'); - const devToolsModal = new bootstrap.Modal(document.getElementById('devToolsModal')); - - // Show Dev Tools modal when button is clicked - devToolsBtn.addEventListener('click', () => { - devToolsModal.show(); - }); - - // Handle database reset - resetDatabaseBtn.addEventListener('click', async () => { - if (!confirm('⚠️ WARNING: This will permanently delete ALL UIDs and their associated availability data. This action cannot be undone.\n\nAre you absolutely sure you want to proceed?')) { - return; - } - - try { - const response = await fetch('/api/reset', { - method: 'POST' - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to reset database'); - } - - // Reload the page to reset all state - window.location.reload(); - } catch (error) { - console.error('Error resetting database:', error); - alert('Failed to reset database. Please try again.'); - } - }); - - // Check if a date has availability - async function checkAvailabilityForDate(date) { - const availability = await fetchAvailability(); - const dateStr = date.toISOString().split('T')[0]; - return !!availability[dateStr]; - } - - // Flush (delete) all entries from the database - flushDatabaseBtn.addEventListener('click', async () => { - const selectedUid = document.getElementById('uidSelect').value; - if (!selectedUid) { - alert('Please select a UID first'); - return; - } - - if (!confirm(`Are you sure you want to delete ALL availability entries for UID: ${selectedUid}?`)) { - return; - } - - try { - console.log(`Attempting to delete all availability for UID: ${selectedUid}...`); - const response = await fetch(`/api/availability/${selectedUid}/flush`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' } - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - console.log('Delete availability response:', result); - - // Refresh data - await fetchAvailability(); // Refresh availability data - - // Reset UI - selectedAdminDate = null; // Clear selected date - selectedDateDisplay.textContent = ''; - timeSlots.innerHTML = ''; // Clear time slots - - // Re-render the calendar - await renderAdminCalendar(currentAdminMonth, currentAdminYear); - - // If a date was previously selected in the calendar, update the time slots UI - const selectedDateElement = document.querySelector('#adminCalendarDates .date-item.selected'); - if (selectedDateElement) { - const day = parseInt(selectedDateElement.textContent); - selectedAdminDate = new Date(currentAdminYear, currentAdminMonth, day); - await updateTimeSlots(selectedAdminDate); - } else { - // If no date was selected, select today's date if it's in the current month - const today = new Date(); - if (today.getMonth() === currentAdminMonth && today.getFullYear() === currentAdminYear) { - selectedAdminDate = today; - await updateTimeSlots(today); - - // Find and highlight today in the calendar - const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); - const todayDay = today.getDate(); - const todayElement = Array.from(dateItems).find(item => { - return item.textContent && parseInt(item.textContent) === todayDay; - }); - - if (todayElement) { - todayElement.classList.add('selected'); - } - } - } - } catch (error) { - console.error('Error deleting availability:', error); - alert('Failed to delete availability. Please try again.'); - } - }); - - // Navigate to previous month (admin) - adminPrevMonthBtn.addEventListener('click', () => { - currentAdminMonth--; - if (currentAdminMonth < 0) { - currentAdminMonth = 11; - currentAdminYear--; - } - renderAdminCalendar(currentAdminMonth, currentAdminYear); - }); - - // Navigate to next month (admin) - adminNextMonthBtn.addEventListener('click', () => { - currentAdminMonth++; - if (currentAdminMonth > 11) { - currentAdminMonth = 0; - currentAdminYear++; - } - renderAdminCalendar(currentAdminMonth, currentAdminYear); - }); - - // Handle global database flush - const flushGlobalDatabaseBtn = document.getElementById('flushGlobalDatabase'); - if (flushGlobalDatabaseBtn) { - flushGlobalDatabaseBtn.addEventListener('click', async () => { - if (!confirm('WARNING: This will delete ALL UIDs and their availability data. This action cannot be undone. Are you sure you want to proceed?')) { - return; - } - - try { - const response = await fetch('/api/flush-global', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' } - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - console.log('Global database flush response:', result); - alert(result.message || 'Database flushed successfully!'); - - // Reset everything - selectedAdminDate = null; - const selectedDateElem = document.getElementById('selectedDateDisplay'); - const timeSlotsElem = document.getElementById('timeSlots'); - const calendarDatesElem = document.getElementById('adminCalendarDates'); - if (selectedDateElem) selectedDateElem.textContent = ''; - if (timeSlotsElem) timeSlotsElem.innerHTML = ''; - if (calendarDatesElem) calendarDatesElem.innerHTML = ''; - - // Reload UIDs - await loadUids(); - - // Update UID URL display - const uidUrl = document.getElementById('uidUrl'); - if (uidUrl) uidUrl.textContent = 'Select a UID first'; - - // Disable delete button - const deleteUidBtn = document.getElementById('deleteUid'); - if (deleteUidBtn) deleteUidBtn.disabled = true; - } catch (error) { - console.error('Error flushing database:', error); - alert('Failed to flush database. Please try again.'); - } - }); - } - - // Initial render with current date - renderAdminCalendar(currentAdminMonth, currentAdminYear); - if (currentAdminDate.getDate() <= new Date(currentAdminYear, currentAdminMonth, 0).getDate()) { - const hasAvailability = await checkAvailabilityForDate(currentAdminDate); - if (hasAvailability) { - selectAdminDate(currentAdminDate, new Date(currentAdminYear, currentAdminMonth, 1).getDay()); - } - } -}); \ No newline at end of file diff --git a/public/avatar.jpg b/public/images/avatar.jpg similarity index 100% rename from public/avatar.jpg rename to public/images/avatar.jpg diff --git a/public/script.js b/public/script.js deleted file mode 100644 index f93b1e8..0000000 --- a/public/script.js +++ /dev/null @@ -1,245 +0,0 @@ -// public/public.js (for index.html) -document.addEventListener('DOMContentLoaded', async () => { - 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'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const availability = await response.json(); - 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) { - // Fetch availability first to ensure data is ready - const availability = await fetchAvailability(); - - 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(); - - // Start of today (midnight) - const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - - // 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]; - - // Check if date is in the past - const isPastDate = date < startOfToday; - - if (date.toDateString() === today.toDateString()) { - day.classList.add('current'); - } - if (availability[dateStr]) { - day.classList.add('available'); - } - if (isPastDate) { - day.classList.add('text-muted'); - day.style.cursor = 'not-allowed'; - } else { - day.addEventListener('click', () => selectDate(date, firstDay)); - day.style.cursor = 'pointer'; - } - calendarDates.appendChild(day); - } - - // Only try to select today if we're in the current month and year - if (today.getMonth() === month && today.getFullYear() === year) { - await selectDate(today, firstDay); // Use await to ensure selection and display complete - } - } - - // 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 - } - - // Helper function to convert time string to minutes for sorting - function timeToMinutes(timeStr) { - console.log('Converting time:', timeStr); - const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); - if (!match) { - console.warn('No match for time:', timeStr); - return 0; - } - - let [_, hours, minutes, period] = match; - hours = parseInt(hours); - minutes = minutes ? parseInt(minutes) : 0; - period = period.toLowerCase(); - - if (period === 'pm' && hours !== 12) hours += 12; - if (period === 'am' && hours === 12) hours = 0; - - const totalMinutes = hours * 60 + minutes; - console.log(`${timeStr} -> ${totalMinutes} minutes`); - return totalMinutes; - } - - // Update time slots based on selected date - async function updateTimeSlots(date) { - const availability = await fetchAvailability(); - const dateStr = date.toISOString().split('T')[0]; - let times = []; - if (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]); - } - } - - // Clear available time slots or message - timeSlotsContainer.innerHTML = ''; - - // Get current time in minutes for comparison - const now = new Date(); - const currentMinutes = now.getHours() * 60 + now.getMinutes(); - const isToday = date.toDateString() === now.toDateString(); - - // Filter out past times if it's today - if (isToday) { - times = times.filter(time => timeToMinutes(time) > currentMinutes); - } - - // Sort remaining times chronologically - times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); - - if (times.length === 0) { - // Display appropriate message based on context - const noAvailabilityMsg = document.createElement('p'); - noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); - - let message; - if (isToday) { - if (currentMinutes > timeToMinutes('5:00pm')) { - message = 'No more availability today.'; - } else { - message = 'No available time slots for today.'; - } - } else { - message = 'No availability on selected date.'; - } - - noAvailabilityMsg.textContent = message; - timeSlotsContainer.appendChild(noAvailabilityMsg); - } else { - // Populate time slots dynamically - 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', () => { - if (button.classList.contains('available') && selectedDate) { - const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot'); - allTimeSlots.forEach(s => s.classList.remove('selected')); - button.classList.add('selected'); - } - }); - 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 - - // Initial render (set to current date, automatically selecting and displaying it) - await renderCalendar(currentMonth, currentYear); // Use await to ensure the initial render completes -}); \ No newline at end of file diff --git a/public/styles.css b/public/styles.css deleted file mode 100644 index 4476b18..0000000 --- a/public/styles.css +++ /dev/null @@ -1,380 +0,0 @@ -/* Custom styles to modernize and align with Calendly-inspired design */ -:root { - --primary-color: #007bff; /* Blue from Calendly */ - --secondary-color: #6c757d; /* Gray for muted elements */ - --light-bg: #f8f9fa; /* Light background for modern look */ - --shadow-color: rgba(0, 0, 0, 0.1); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: 'Arial', sans-serif; /* Matches Calendly font */ -} - -.container-fluid { - max-width: 1000px; - margin: 20px auto; - background-color: #fff; - border-radius: 6px; - box-shadow: 0 2px 4px var(--shadow-color); - overflow: hidden; -} - -.sidebar { - width: 30%; - padding: 20px; - border-right: 1px solid #e1e8ed; - text-align: center; - background-color: #fff; -} - -.profile-pic { - width: 80px; - height: 80px; - object-fit: cover; - border-radius: 50%; - margin-bottom: 10px; - border: 2px solid #fff; - box-shadow: 0 2px 4px var(--shadow-color); -} - -.sidebar h2 { - font-size: 1.25rem; - color: #333; - margin-bottom: 5px; - font-weight: 600; -} - -.sidebar h3 { - font-size: 0.9rem; - color: #666; - margin-bottom: 5px; - font-weight: 400; -} - -.duration { - font-size: 0.875rem; - color: #666; -} - -.main-content { - width: 70%; - padding: 20px; - position: relative; -} - -.calendar-header h1 { - font-size: 1.5rem; - color: #333; - font-weight: 700; - margin-bottom: 10px; -} - -.month-nav { - display: flex; - justify-content: space-between; - font-size: 1rem; - color: #333; - padding: 0.5rem 0; - border-bottom: 1px solid #e1e8ed; -} - -.month-nav button { - color: var(--secondary-color); - transition: color 0.3s ease; -} - -.month-nav button:hover { - color: var(--primary-color); -} - -.days-of-week, .dates { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 5px; - text-align: center; -} - -.days-of-week span { - font-weight: bold; - color: #666; - padding: 5px; -} - -.dates .date-item { - padding: 10px; - border-radius: 6px; - cursor: pointer; - transition: background-color 0.3s ease, transform 0.2s ease; -} - -.dates .date-item.disabled { - cursor: not-allowed; - color: var(--secondary-color); - opacity: 0.5; -} - -.dates .date-item:hover { - background-color: #f0f8ff; - transform: scale(1.05); -} - -.dates .date-item.available { - background-color: #e9f2ff; - border: 2px solid var(--primary-color); - font-weight: 600; -} - -.dates .date-item.selected { - background-color: #0d6efd !important; - color: #fff !important; - border-radius: 6px; - font-weight: 600; - border: none !important; -} - -.dates .date-item.current { - background-color: #e9f2ff; - border: 2px solid var(--primary-color); - font-weight: 600; -} - -.time-slot { - font-size: 1rem; - padding: 0.75rem 1.5rem; - transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.2s ease; -} - -.time-slot.available { - background-color: #e9f2ff; - border-color: var(--primary-color); - color: var(--primary-color); -} - -.time-slots .text-muted { - color: #6c757d; /* Muted text color, matching Calendly */ - font-size: rem; /* Slightly larger font, e.g., 20px instead of 16px */ - font-weight: bold; /* Bold text */ - padding: 2.2rem 0; - text-align: center; -} - -.time-slot.available:hover, .time-slot:hover { - background-color: var(--primary-color); - color: #fff; - transform: scale(1.05); -} - -.timezone { - font-size: 0.875rem; - color: #666; - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -#timezone { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.powered-by { - font-size: 0.75rem; - color: #666; - opacity: 0.8; - transition: opacity 0.3s ease; - z-index: 10; - background-color: transparent; - padding: 5px; - margin-left: auto; /* Push to the right */ - pointer-events: auto; -} - -.powered-by.text-end { - text-align: right; -} - -.powered-by a { - color: inherit; - text-decoration: none; - cursor: pointer; -} - -.powered-by a.github-link { - display: inline-flex; - align-items: center; - background-color: #e9e9e9; - color: #333; - padding: 8px 12px; - border-radius: 20px; - font-size: 14px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - white-space: nowrap; -} - -.octocat-icon { - margin-right: 8px; - display: flex; - align-items: center; -} - -.octocat-icon svg { - fill: #333; -} - -.powered-by:hover { - opacity: 1; -} - -/* Admin-specific powered-by styling */ -.admin-powered-by { - position: absolute; - bottom: 20px; - right: 20px; - margin-top: 0; -} - -/* Index-specific powered-by styling */ -.index-powered-by { - text-align: right; - margin-top: 0; - margin-left: auto; - display: inline-flex; - justify-content: flex-end; - align-items: center; - flex-shrink: 0; -} - -/* Add clearance for elements with flex classes */ -.d-flex.flex-column.gap-2.mt-3 { - margin-bottom: 50px; /* Increased margin to ensure these elements don't overlap with the powered-by button */ -} - -/* Admin-specific styles */ -#admin .calendar { - margin-bottom: 20px; -} - -#admin .form-control, #admin .form-select { - border-radius: 6px; -} - -#admin .btn-primary, #admin .btn-danger { - border-radius: 4px; -} - -#availabilityList .list-group-item { - border-radius: 4px; - margin-bottom: 5px; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .container-fluid { - margin: 10px; - border-radius: 6px; - } - - .sidebar, .main-content { - width: 100%; - } - - .sidebar { - border-right: none; - border-bottom: 1px solid #e1e8ed; - } - - #admin .row { - flex-direction: column; - } - #admin .col-md-6 { - width: 100%; - } - - .powered-by { - position: relative; - bottom: auto; - right: auto; - text-align: center; - margin-top: 30px; - } - - .admin-powered-by { - position: relative; - bottom: auto; - right: auto; - text-align: right; - margin-top: 20px; - } - - .index-powered-by { - position: relative; - text-align: right; - margin-left: auto; - display: inline-flex; - justify-content: flex-end; - } -} - -/* Sidebar buttons styling */ -.sidebar .btn { - padding: 12px 20px; - font-size: 1.1rem; - font-weight: 500; - border: none; - transition: all 0.3s ease; -} - -.sidebar .btn-success { - background-color: #2e8b57; /* Money green color */ - border-color: #2e8b57; -} - -.sidebar .btn-success:hover { - background-color: #267349; - border-color: #267349; -} - -.sidebar .btn-warning { - background-color: #ffc107; - border-color: #ffc107; - color: #212529; -} - -.sidebar .btn-warning:hover { - background-color: #e0a800; - border-color: #e0a800; -} - -.sidebar .btn-danger { - background-color: #dc3545; - border-color: #dc3545; -} - -.sidebar .btn-danger:hover { - background-color: #c82333; - border-color: #c82333; -} - -/* Dev Tools button styling */ -.sidebar .btn-info { - background-color: #00bfff; /* Neon blue color */ - border-color: #00bfff; - color: white; - box-shadow: 0 0 10px rgba(0, 191, 255, 0.5); /* Neon glow effect */ -} - -.sidebar .btn-info:hover { - background-color: #0099cc; - border-color: #0099cc; - box-shadow: 0 0 15px rgba(0, 191, 255, 0.7); /* Enhanced glow on hover */ -} - -.time-slots { - margin-bottom: 50px; /* Increased margin to ensure time slots don't overlap with the powered-by button */ -} \ No newline at end of file diff --git a/public/timepicker.css b/public/timepicker.css deleted file mode 100644 index 1e9af76..0000000 --- a/public/timepicker.css +++ /dev/null @@ -1,413 +0,0 @@ -/** - * Timepicker styling - */ -/* Core timepicker styling */ -.timepicker-wrapper { - --fs-timepicker-wrapper-bg: rgba(0, 0, 0, 0.4); - --fs-timepicker-elements-min-width: 310px; - --fs-timepicker-elements-min-height: 325px; - --fs-timepicker-elements-background: #fff; - --fs-timepicker-elements-border-top-right-radius: 0.6rem; - --fs-timepicker-elements-border-top-left-radius: 0.6rem; - --fs-timepicker-head-bg: #3b71ca; - --fs-timepicker-head-height: 100px; - --fs-timepicker-head-border-top-right-radius: 0.5rem; - --fs-timepicker-head-border-top-left-radius: 0.5rem; - --fs-timepicker-head-padding-y: 10px; - --fs-timepicker-head-padding-right: 24px; - --fs-timepicker-head-padding-left: 50px; - --fs-timepicker-button-font-size: 0.8rem; - --fs-timepicker-button-min-width: 64px; - --fs-timepicker-button-font-weight: 500; - --fs-timepicker-button-line-height: 40px; - --fs-timepicker-button-border-radius: 10px; - --fs-timepicker-button-color: #4f4f4f; - --fs-timepicker-button-hover-bg: rgba(0, 0, 0, 0.08); - --fs-timepicker-button-focus-bg: rgba(0, 0, 0, 0.08); - --fs-timepicker-button-padding-x: 10px; - --fs-timepicker-button-height: 40px; - --fs-timepicker-button-margin-bottom: 10px; - --fs-timepicker-current-font-size: 3.75rem; - --fs-timepicker-current-font-weight: 300; - --fs-timepicker-current-line-height: 1.2; - --fs-timepicker-current-color: #fff; - --fs-timepicker-current-opacity: 0.54; - --fs-timepicker-clock-wrapper-min-width: 310px; - --fs-timepicker-clock-wrapper-max-width: 325px; - --fs-timepicker-clock-wrapper-min-height: 305px; - --fs-timepicker-clock-wrapper-text-color: #4f4f4f; - --fs-timepicker-mode-wrapper-font-size: 18px; - --fs-timepicker-mode-wrapper-color: rgba(255, 255, 255, 0.54); - --fs-timepicker-clock-width: 260px; - --fs-timepicker-clock-height: 260px; - --fs-timepicker-clock-face-bg: #f0f0f0; - --fs-timepicker-time-tips-inner-active-color: #fff; - --fs-timepicker-time-tips-inner-active-bg: #3b71ca; - --fs-timepicker-time-tips-inner-active-font-weight: 400; - --fs-timepicker-dot-font-weight: 300; - --fs-timepicker-dot-line-height: 1.2; - --fs-timepicker-dot-color: #fff; - --fs-timepicker-dot-font-size: 3.75rem; - --fs-timepicker-dot-opacity: 0.54; - --fs-timepicker-item-middle-dot-width: 6px; - --fs-timepicker-item-middle-dot-height: 6px; - --fs-timepicker-item-middle-dot-border-radius: 50%; - --fs-timepicker-item-middle-dot-bg: #3b71ca; - --fs-timepicker-hand-pointer-bg: #3b71ca; - --fs-timepicker-hand-pointer-bottom: 50%; - --fs-timepicker-hand-pointer-height: 40%; - --fs-timepicker-hand-pointer-left: calc(50% - 1px); - --fs-timepicker-hand-pointer-width: 2px; - --fs-timepicker-circle-top: -21px; - --fs-timepicker-circle-left: -15px; - --fs-timepicker-circle-width: 4px; - --fs-timepicker-circle-border-width: 14px; - --fs-timepicker-circle-border-color: #3b71ca; - --fs-timepicker-circle-height: 4px; - --fs-timepicker-circle-active-background-color: #fff; - --fs-timepicker-hour-mode-color: #fff; - --fs-timepicker-hour-mode-opacity: 0.54; - --fs-timepicker-hour-mode-hover-bg: rgba(0, 0, 0, 0.15); - --fs-timepicker-hour-mode-active-color: #fff; - --fs-timepicker-footer-border-bottom-left-radius: 0.5rem; - --fs-timepicker-footer-border-bottom-right-radius: 0.5rem; - --fs-timepicker-footer-height: 56px; - --fs-timepicker-footer-padding-x: 12px; - --fs-timepicker-footer-bg: #fff; - --fs-timepicker-clock-animation: show-up-clock 350ms linear; - --fs-timepicker-zindex: 1065; - - touch-action: none; - z-index: var(--fs-timepicker-zindex); - opacity: 1; - right: 0; - bottom: 0; - top: 0; - left: 0; - background-color: var(--fs-timepicker-wrapper-bg); -} - -/* Animation */ -.animation { - animation-fill-mode: both; -} - -.fade-in { - animation-name: fadeIn; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes show-up-clock { - 0% { - opacity: 0; - transform: scale(0.7); - } - to { - opacity: 1; - transform: scale(1); - } -} - -/* Timepicker components */ -.timepicker-modal { - margin: 0; - font-family: "Roboto", sans-serif; - font-size: 1rem; - font-weight: 400; - line-height: 1.6; - color: #4f4f4f; - text-align: left; - background-color: #fff; - z-index: var(--fs-timepicker-zindex); - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} - -/* Ensure all elements inside the timepicker use border-box */ -.timepicker-modal *, -.timepicker-modal *::before, -.timepicker-modal *::after { - box-sizing: border-box; -} - -.timepicker-container { - max-height: calc(100% - 64px); - overflow-y: auto; - box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04); -} - -.timepicker-elements { - min-width: var(--fs-timepicker-elements-min-width); - min-height: var(--fs-timepicker-elements-min-height); - background: var(--fs-timepicker-elements-background); - border-top-right-radius: var(--fs-timepicker-elements-border-top-right-radius); - border-top-left-radius: var(--fs-timepicker-elements-border-top-left-radius); -} - -.timepicker-head { - background-color: var(--fs-timepicker-head-bg); - height: var(--fs-timepicker-head-height); - border-top-right-radius: var(--fs-timepicker-head-border-top-right-radius); - border-top-left-radius: var(--fs-timepicker-head-border-top-left-radius); - padding: var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-right) var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-left); -} - -.timepicker-current { - font-size: var(--fs-timepicker-current-font-size); - font-weight: var(--fs-timepicker-current-font-weight); - line-height: var(--fs-timepicker-current-line-height); - color: var(--fs-timepicker-current-color); - opacity: var(--fs-timepicker-current-opacity); - border: none; - background: transparent; - padding: 0; - position: relative; - vertical-align: unset; -} - -.timepicker-current.active { - opacity: 1; -} - -.timepicker-dot { - font-size: var(--fs-timepicker-dot-font-size); - font-weight: var(--fs-timepicker-dot-font-weight); - line-height: var(--fs-timepicker-dot-line-height); - color: var(--fs-timepicker-dot-color); - opacity: var(--fs-timepicker-dot-opacity); - border: none; - background: transparent; - padding: 0; -} - -.timepicker-mode-wrapper { - font-size: var(--fs-timepicker-mode-wrapper-font-size); - color: var(--fs-timepicker-mode-wrapper-color); -} - -.timepicker-hour-mode { - padding: 0; - background-color: transparent; - border: none; - color: var(--fs-timepicker-hour-mode-color); - opacity: var(--fs-timepicker-hour-mode-opacity); - cursor: pointer; -} - -/* These focus styles are overridden later in the file with !important */ -.timepicker-hour-mode:hover, -.timepicker-hour-mode:focus, -.timepicker-hour:hover, -.timepicker-hour:focus, -.timepicker-minute:hover, -.timepicker-minute:focus { - background-color: var(--fs-timepicker-hour-mode-hover-bg); - outline: none; - cursor: pointer; -} - -.timepicker-hour-mode.active, -.timepicker-hour.active, -.timepicker-minute.active { - color: #fff; - opacity: 1; - outline: none; -} - -.timepicker-clock-wrapper { - min-width: var(--fs-timepicker-clock-wrapper-min-width); - max-width: var(--fs-timepicker-clock-wrapper-max-width); - min-height: var(--fs-timepicker-clock-wrapper-min-height); - overflow-x: hidden; - height: 100%; - color: var(--fs-timepicker-clock-wrapper-text-color); -} - -.timepicker-clock { - position: relative; - border-radius: 100%; - width: var(--fs-timepicker-clock-width); - height: var(--fs-timepicker-clock-height); - cursor: default; - margin: 0 auto; - background-color: var(--fs-timepicker-clock-face-bg); - user-select: none; -} - -.timepicker-clock-animation { - animation: var(--fs-timepicker-clock-animation); -} - -.timepicker-middle-dot { - top: 50%; - left: 50%; - width: var(--fs-timepicker-item-middle-dot-width); - height: var(--fs-timepicker-item-middle-dot-height); - transform: translate(-50%, -50%); - border-radius: var(--fs-timepicker-item-middle-dot-border-radius); - background-color: var(--fs-timepicker-item-middle-dot-bg); -} - -.timepicker-hand-pointer { - background-color: var(--fs-timepicker-hand-pointer-bg); - bottom: var(--fs-timepicker-hand-pointer-bottom); - height: var(--fs-timepicker-hand-pointer-height); - left: var(--fs-timepicker-hand-pointer-left); - transform-origin: center bottom 0; - width: var(--fs-timepicker-hand-pointer-width); -} - -.timepicker-circle { - top: var(--fs-timepicker-circle-top); - left: var(--fs-timepicker-circle-left); - width: var(--fs-timepicker-circle-width); - border: var(--fs-timepicker-circle-border-width) solid var(--fs-timepicker-circle-border-color); - height: var(--fs-timepicker-circle-height); - box-sizing: content-box; - border-radius: 100%; - background-color: transparent; -} - -.timepicker-circle.active { - background-color: var(--fs-timepicker-circle-active-background-color); -} - -.timepicker-time-tips-minutes, -.timepicker-time-tips-hours { - position: absolute; - border-radius: 100%; - width: 32px; - height: 32px; - text-align: center; - cursor: pointer; - font-size: 1.1rem; - display: flex; - justify-content: center; - align-items: center; - font-weight: 300; - user-select: none; -} - -.timepicker-time-tips-minutes.active, -.timepicker-time-tips-hours.active { - color: var(--fs-timepicker-time-tips-inner-active-color); - background-color: var(--fs-timepicker-time-tips-inner-active-bg); - font-weight: var(--fs-timepicker-time-tips-inner-active-font-weight); -} - -.timepicker-footer { - border-bottom-left-radius: var(--fs-timepicker-footer-border-bottom-left-radius); - border-bottom-right-radius: var(--fs-timepicker-footer-border-bottom-right-radius); - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: var(--fs-timepicker-footer-height); - padding-left: var(--fs-timepicker-footer-padding-x); - padding-right: var(--fs-timepicker-footer-padding-x); - background-color: var(--fs-timepicker-footer-bg); -} - -.timepicker-button { - font-size: var(--fs-timepicker-button-font-size); - min-width: var(--fs-timepicker-button-min-width); - box-sizing: border-box; - font-weight: var(--fs-timepicker-button-font-weight); - line-height: var(--fs-timepicker-button-line-height); - border-radius: var(--fs-timepicker-button-border-radius); - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--fs-timepicker-button-color); - border: none; - background-color: transparent; - transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - outline: none; - padding: 0 var(--fs-timepicker-button-padding-x); - height: var(--fs-timepicker-button-height); - margin-bottom: var(--fs-timepicker-button-margin-bottom); - cursor: pointer; -} - -.timepicker-button:hover { - background-color: var(--fs-timepicker-button-hover-bg); -} - -.timepicker-button:focus { - outline: none; - background-color: var(--fs-timepicker-button-focus-bg); -} - -/* Add missing button styling */ -button { - margin: 0; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -/** Remove focus from buttons after clicked **/ -/* Prevent selection/focus styling on all buttons in the timepicker */ -.timepicker-modal button { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -webkit-tap-highlight-color: transparent; - touch-action: manipulation; -} - -/* Override focus styles completely */ -.timepicker-modal button:focus { - outline: none !important; - box-shadow: none !important; - -webkit-box-shadow: none !important; -} - -/* Prevent text selection on all elements */ -.timepicker-modal * { - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -/* Add this to the body when the timepicker is active */ -body.timepicker-active { - -webkit-tap-highlight-color: transparent; -} - -/* Add !important to ensure these styles take precedence */ -.timepicker-current:focus, -.timepicker-dot:focus, -.timepicker-hour-mode:focus, -.timepicker-button:focus { - outline: none !important; - background-color: transparent !important; -} - -/* Only apply background color on hover, not on focus */ -.timepicker-hour-mode:hover, -.timepicker-hour:hover, -.timepicker-minute:hover, -.timepicker-button:hover { - background-color: var(--fs-timepicker-hour-mode-hover-bg); -} - -/* Ensure active states are properly styled */ -.timepicker-hour-mode.active, -.timepicker-hour.active, -.timepicker-minute.active { - color: #fff !important; - opacity: 1 !important; -} \ No newline at end of file diff --git a/views/admin.html b/views/admin.html index 71ade52..0cd961f 100644 --- a/views/admin.html +++ b/views/admin.html @@ -16,7 +16,7 @@ <div class="row g-0"> <div class="col-md-4 sidebar bg-light border-end border-gray-300"> <div class="d-flex flex-column align-items-center p-4"> - <img src="/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm"> + <img src="/images/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm"> <h2 class="fs-5 text-dark fw-bold mb-2">Doug Masiero</h2> <h3 class="fs-6 text-muted mb-2">30 Minute Meeting</h3> <p class="text-muted fs-6">30 min</p> diff --git a/views/index.html b/views/index.html index 3a8dafb..81b143b 100644 --- a/views/index.html +++ b/views/index.html @@ -6,7 +6,7 @@ <title>FreeSched Meeting Scheduler</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> - <link rel="stylesheet" href="/styles.css"> + <link rel="stylesheet" href="/css/styles.css"> <!-- Bootstrap Icons --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"> </head> @@ -21,7 +21,7 @@ <div class="row g-0"> <div class="col-md-4 sidebar bg-light border-end border-gray-300"> <div class="d-flex flex-column align-items-center p-4"> - <img src="/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm"> + <img src="/images/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm"> <h2 class="fs-5 text-dark fw-bold mb-2">Doug Masiero</h2> <h3 class="fs-6 text-muted mb-2">30 Minute Meeting</h3> <p class="text-muted fs-6">30 min</p>