Manage Schedule UIDs
+Manage Schedule UIDs
-⚠️ Danger Zone
-The actions in this section can cause irreversible changes to the database. Please proceed with caution.
- -diff --git a/db/freesched.db b/db/freesched.db index 4f6a4ae..db96ce2 100644 Binary files a/db/freesched.db and b/db/freesched.db differ diff --git a/pages/index.js b/pages/index.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/pages/index.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/admin.js b/public/admin.js index d4ef2bc..5e60d88 100644 --- a/public/admin.js +++ b/public/admin.js @@ -7,12 +7,6 @@ let currentAdminYear = currentAdminDate.getFullYear(); let selectedAdminDate = null; const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; -// Generate all possible time slots in chronological order (8am to 5pm) -const timeSlotsData = [ - '8:00am', '9:00am', '10:00am', '11:00am', '12:00pm', - '1:00pm', '2:00pm', '3:00pm', '4:00pm', '5:00pm' -]; - // Helper function to convert time string to minutes since midnight function timeToMinutes(timeStr) { const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); @@ -61,6 +55,7 @@ async function renderAdminCalendar(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(); const availability = await fetchAvailability(); // Start of today (midnight) @@ -90,14 +85,37 @@ async function renderAdminCalendar(month, year) { // Check if date is in the past const isPastDate = date < startOfToday; + const isToday = date.toDateString() === today.toDateString(); - if (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 { @@ -116,29 +134,32 @@ async function renderAdminCalendar(month, year) { // 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; - const calendarDates = document.getElementById('adminCalendarDates'); - if (!calendarDates) return; - - // Remove selected class from all dates - const allDates = calendarDates.querySelectorAll('.date-item'); - allDates.forEach(item => item.classList.remove('selected')); - - // Add selected class to the clicked date - const selectedDay = Array.from(allDates).find(item => { - if (!item.textContent) return false; + 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 = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; + selectedDateDisplay.textContent = ''; } - - // Update time slots for the selected date - await updateTimeSlots(date); } // Update time slots based on selected date @@ -152,7 +173,7 @@ async function updateTimeSlots(date) { if (typeof availability[dateStr] === 'string') { availableTimes = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { - availableTimes = availability[dateStr].map(t => t.trim()); + availableTimes = [...availability[dateStr]]; } } @@ -162,51 +183,381 @@ async function updateTimeSlots(date) { const timeSlots = document.getElementById('timeSlots'); if (!timeSlots) return; + // Clear existing time slots timeSlots.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(); + // 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 = ' Add Time Slot'; - // Create a map of all time slots with their status - const allSlots = new Map(); - timeSlotsData.forEach(time => { - const timeInMinutes = timeToMinutes(time); - // Skip times that are in the past on the current day - if (isToday && timeInMinutes <= currentMinutes) { - return; - } - allSlots.set(time, availableTimes.includes(time)); - }); + // 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'); - // Sort all slots chronologically - const sortedSlots = Array.from(allSlots.entries()) - .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); - - // Display all slots in chronological order - sortedSlots.forEach(([time, isAvailable]) => { - const button = document.createElement('button'); - button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); - button.textContent = time; - - if (isAvailable) { - button.classList.add('btn-primary', 'available'); - button.addEventListener('click', () => { - button.classList.remove('btn-primary', 'available'); - button.classList.add('btn-outline-secondary'); + // 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 = ''; + 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); }); - } else { - button.classList.add('btn-outline-secondary'); - button.addEventListener('click', () => { - button.classList.remove('btn-outline-secondary'); - button.classList.add('btn-primary', 'available'); - addSingleTime(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 visual time picker container + const timePicker = document.createElement('div'); + timePicker.id = 'visualTimePicker'; + timePicker.classList.add('card', 'shadow'); + timePicker.style.display = 'none'; + timePicker.style.position = 'fixed'; + timePicker.style.zIndex = '1050'; + timePicker.style.width = '300px'; + timePicker.style.top = '50%'; + timePicker.style.left = '50%'; + timePicker.style.transform = 'translate(-50%, -50%)'; + + // Create the header + const header = document.createElement('div'); + header.classList.add('card-header', 'bg-primary', 'text-white', 'text-center', 'p-0'); + + // Create the time display in the header + const timeDisplay = document.createElement('div'); + timeDisplay.classList.add('display-4', 'py-3', 'mb-0'); + timeDisplay.innerHTML = '12:00 PM'; + header.appendChild(timeDisplay); + + timePicker.appendChild(header); + + // Create the body + const body = document.createElement('div'); + body.classList.add('card-body', 'bg-white'); + + // Create the clock face container + const clockFace = document.createElement('div'); + clockFace.classList.add('time-picker-clock', 'mb-3'); + clockFace.style.position = 'relative'; + clockFace.style.width = '250px'; + clockFace.style.height = '250px'; + clockFace.style.margin = '0 auto'; + clockFace.style.backgroundColor = '#f8f9fa'; + clockFace.style.borderRadius = '50%'; + + // Add clock center dot + const clockCenter = document.createElement('div'); + clockCenter.style.position = 'absolute'; + clockCenter.style.top = '50%'; + clockCenter.style.left = '50%'; + clockCenter.style.transform = 'translate(-50%, -50%)'; + clockCenter.style.width = '8px'; + clockCenter.style.height = '8px'; + clockCenter.style.backgroundColor = '#0d6efd'; + clockCenter.style.borderRadius = '50%'; + clockFace.appendChild(clockCenter); + + // Add hour numbers to the clock face + for (let i = 1; i <= 12; i++) { + const hourNumber = document.createElement('div'); + hourNumber.classList.add('hour-number'); + hourNumber.textContent = i; + hourNumber.style.position = 'absolute'; + hourNumber.style.width = '40px'; + hourNumber.style.height = '40px'; + hourNumber.style.textAlign = 'center'; + hourNumber.style.lineHeight = '40px'; + hourNumber.style.fontWeight = 'bold'; + hourNumber.style.cursor = 'pointer'; + + // Calculate position (in a circle) + const angle = (i * 30 - 90) * (Math.PI / 180); // 30 degrees per hour, starting at 12 o'clock + const radius = 100; // Distance from center + const left = 125 + radius * Math.cos(angle); + const top = 125 + radius * Math.sin(angle); + + hourNumber.style.left = `${left - 20}px`; // Adjust for element width + hourNumber.style.top = `${top - 20}px`; // Adjust for element height + + // Add click event to select hour + hourNumber.addEventListener('click', () => { + document.getElementById('timePickerHour').textContent = i; + + // Highlight selected hour + document.querySelectorAll('.hour-number').forEach(el => { + el.style.backgroundColor = 'transparent'; + el.style.color = '#212529'; }); + hourNumber.style.backgroundColor = '#0d6efd'; + hourNumber.style.color = 'white'; + + // Show minute selection + showMinuteSelection(); + }); + + clockFace.appendChild(hourNumber); + } + + // Function to show minute selection + function showMinuteSelection() { + // Hide hour numbers + document.querySelectorAll('.hour-number').forEach(el => { + el.style.display = 'none'; + }); + + // Show minute numbers + document.querySelectorAll('.minute-number').forEach(el => { + el.style.display = 'block'; + }); + } + + // Function to show hour selection + function showHourSelection() { + // Hide minute numbers + document.querySelectorAll('.minute-number').forEach(el => { + el.style.display = 'none'; + }); + + // Show hour numbers + document.querySelectorAll('.hour-number').forEach(el => { + el.style.display = 'block'; + }); + } + + // Add minute numbers to the clock face (initially hidden) + const minuteValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; + minuteValues.forEach((minute, index) => { + const minuteNumber = document.createElement('div'); + minuteNumber.classList.add('minute-number'); + minuteNumber.textContent = minute.toString().padStart(2, '0'); + minuteNumber.style.position = 'absolute'; + minuteNumber.style.width = '40px'; + minuteNumber.style.height = '40px'; + minuteNumber.style.textAlign = 'center'; + minuteNumber.style.lineHeight = '40px'; + minuteNumber.style.fontWeight = 'bold'; + minuteNumber.style.cursor = 'pointer'; + minuteNumber.style.display = 'none'; // Initially hidden + + // Calculate position (in a circle) + const angle = (index * 30 - 90) * (Math.PI / 180); + const radius = 100; // Distance from center + const left = 125 + radius * Math.cos(angle); + const top = 125 + radius * Math.sin(angle); + + minuteNumber.style.left = `${left - 20}px`; // Adjust for element width + minuteNumber.style.top = `${top - 20}px`; // Adjust for element height + + // Add click event to select minute + minuteNumber.addEventListener('click', () => { + document.getElementById('timePickerMinute').textContent = minute.toString().padStart(2, '0'); + + // Highlight selected minute + document.querySelectorAll('.minute-number').forEach(el => { + el.style.backgroundColor = 'transparent'; + el.style.color = '#212529'; + }); + minuteNumber.style.backgroundColor = '#0d6efd'; + minuteNumber.style.color = 'white'; + + // Show hour selection again + showHourSelection(); + }); + + clockFace.appendChild(minuteNumber); + }); + + body.appendChild(clockFace); + + // Create AM/PM toggle + const ampmToggle = document.createElement('div'); + ampmToggle.classList.add('d-flex', 'justify-content-center', 'mb-3'); + + const amButton = document.createElement('button'); + amButton.classList.add('btn', 'btn-outline-primary', 'me-2'); + amButton.textContent = 'AM'; + amButton.addEventListener('click', () => { + document.getElementById('timePickerAMPM').textContent = 'AM'; + amButton.classList.add('active'); + pmButton.classList.remove('active'); + }); + + const pmButton = document.createElement('button'); + pmButton.classList.add('btn', 'btn-outline-primary'); + pmButton.textContent = 'PM'; + pmButton.addEventListener('click', () => { + document.getElementById('timePickerAMPM').textContent = 'PM'; + pmButton.classList.add('active'); + amButton.classList.remove('active'); + }); + pmButton.classList.add('active'); // Add active class to PM button + + ampmToggle.appendChild(amButton); + ampmToggle.appendChild(pmButton); + + body.appendChild(ampmToggle); + + // Create action buttons + const actionButtons = document.createElement('div'); + actionButtons.classList.add('d-flex', 'justify-content-between'); + + const closeButton = document.createElement('button'); + closeButton.classList.add('btn', 'btn-outline-secondary', 'flex-grow-1', 'me-2'); + closeButton.textContent = 'CANCEL'; + closeButton.addEventListener('click', () => { + timePicker.style.display = 'none'; + }); + + const okButton = document.createElement('button'); + okButton.classList.add('btn', 'btn-primary', 'flex-grow-1'); + okButton.textContent = 'OK'; + okButton.addEventListener('click', () => { + const hour = document.getElementById('timePickerHour').textContent; + const minute = document.getElementById('timePickerMinute').textContent; + const ampm = document.getElementById('timePickerAMPM').textContent.toLowerCase(); + + const timeValue = `${hour}:${minute}${ampm}`; + document.getElementById('customTimeInput').value = timeValue; + + // 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); } - timeSlots.appendChild(button); + // Hide the time picker + timePicker.style.display = 'none'; + }); + + actionButtons.appendChild(closeButton); + actionButtons.appendChild(okButton); + + body.appendChild(actionButtons); + + timePicker.appendChild(body); + + // Add the time picker to the document + document.body.appendChild(timePicker); + + // Show time picker when button is clicked + timeButton.addEventListener('click', (e) => { + // Remove positioning relative to button + timePicker.style.display = 'block'; + + // Reset to default view (hour selection) + showHourSelection(); + + e.stopPropagation(); + }); + + // Close time picker when clicking outside + document.addEventListener('click', (e) => { + if (e.target !== timeButton && !timePicker.contains(e.target)) { + timePicker.style.display = 'none'; + } }); } @@ -238,17 +589,51 @@ async function updateAvailabilityUI() { 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'); + } }); } } @@ -424,148 +809,191 @@ function formatTimeSlot(hour) { return `${displayHour}:00${period}`; } -// 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'); - const selectedDateDisplay = document.getElementById('selectedDateDisplay'); - if (selectedDateDisplay) { - selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; - } -} - -// 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].map(t => t.trim()); - } - } - - // Sort available times chronologically - availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); - - const timeSlots = document.getElementById('timeSlots'); - if (!timeSlots) return; - - timeSlots.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(); - - // Create a map of all time slots with their status - const allSlots = new Map(); - timeSlotsData.forEach(time => { - const timeInMinutes = timeToMinutes(time); - // Skip times that are in the past on the current day - if (isToday && timeInMinutes <= currentMinutes) { - return; - } - allSlots.set(time, availableTimes.includes(time)); - }); - - // Sort all slots chronologically - const sortedSlots = Array.from(allSlots.entries()) - .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); - - // Display all slots in chronological order - sortedSlots.forEach(([time, isAvailable]) => { - const button = document.createElement('button'); - button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); - button.textContent = time; - - if (isAvailable) { - button.classList.add('btn-primary', 'available'); - button.addEventListener('click', () => { - button.classList.remove('btn-primary', 'available'); - button.classList.add('btn-outline-secondary'); - removeTime(dateStr, time); - }); - } else { - button.classList.add('btn-outline-secondary'); - button.addEventListener('click', () => { - button.classList.remove('btn-outline-secondary'); - button.classList.add('btn-primary', 'available'); - addSingleTime(dateStr, time); - }); +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 */ } - timeSlots.appendChild(button); - }); -} - -// 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}`); + /* 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; } - 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'); - 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]; - - if (availability[dateStr]) { - dateElement.classList.add('available'); - } else { - dateElement.classList.remove('available'); + + /* 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 = 'Copy URL'; + + // 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 = ' 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'); + } } -} - -document.addEventListener('DOMContentLoaded', async () => { + // Load existing UIDs await loadUids(); @@ -573,12 +1001,46 @@ document.addEventListener('DOMContentLoaded', async () => { 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'); @@ -596,14 +1058,36 @@ document.addEventListener('DOMContentLoaded', async () => { const url = `${window.location.origin}/${selectedUid}`; uidUrl.textContent = url; uidUrl.href = url; + // Refresh calendar with new UID's data await renderAdminCalendar(currentAdminMonth, currentAdminYear); - // Show availability for current date - if (selectedAdminDate) { - await updateTimeSlots(selectedAdminDate); + + // 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 = 'Select a UID first'; + uidUrl.textContent = ''; uidUrl.href = '#'; // Add placeholder text if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar'; @@ -612,11 +1096,30 @@ document.addEventListener('DOMContentLoaded', async () => { }); // Handle new UID creation - const newUidInput = document.getElementById('newUid'); const createUidBtn = document.getElementById('createUid'); + const modalUidInput = document.getElementById('modalUidInput'); + const confirmCreateUidBtn = document.getElementById('confirmCreateUid'); + const createUidModal = new bootstrap.Modal(document.getElementById('createUidModal')); - createUidBtn.addEventListener('click', async () => { - const uid = newUidInput.value.trim().toLowerCase(); + // 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'); @@ -624,7 +1127,7 @@ document.addEventListener('DOMContentLoaded', async () => { } if (await createUid(uid)) { - newUidInput.value = ''; + createUidModal.hide(); } else { alert('Failed to create UID. Please try again.'); } @@ -657,6 +1160,13 @@ document.addEventListener('DOMContentLoaded', async () => { 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 () => { @@ -664,11 +1174,6 @@ document.addEventListener('DOMContentLoaded', async () => { return; } - // Double confirm for dangerous action - if (!confirm('Please confirm one more time that you want to reset the entire database. All data will be lost.')) { - return; - } - try { const response = await fetch('/api/reset', { method: 'POST' @@ -687,13 +1192,6 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - - - - - - - // Check if a date has availability async function checkAvailabilityForDate(date) { const availability = await fetchAvailability(); @@ -701,10 +1199,6 @@ document.addEventListener('DOMContentLoaded', async () => { return !!availability[dateStr]; } - - - - // Flush (delete) all entries from the database flushDatabaseBtn.addEventListener('click', async () => { const selectedUid = document.getElementById('uidSelect').value; diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/public/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/public.js b/public/public.js index a266ba6..eee58c7 100644 --- a/public/public.js +++ b/public/public.js @@ -26,6 +26,26 @@ function isValidUid(uid) { } document.addEventListener('DOMContentLoaded', async () => { + // Add custom CSS for the current date to make it more distinguishable + const style = document.createElement('style'); + style.textContent = ` + .date-item.current { + border: 2px solid #ffc107 !important; /* Yellow border */ + background-color: rgba(255, 193, 7, 0.1) !important; /* Light yellow background */ + font-weight: bold !important; + color: #212529 !important; /* Ensure text is visible */ + } + + /* Ensure current date styling takes precedence but doesn't interfere with selection */ + .date-item.current.selected { + border: 2px solid #0d6efd !important; /* Blue border for selected */ + background-color: #0d6efd !important; /* Dark blue background when selected */ + color: white !important; /* White text for better contrast */ + font-weight: bold !important; + } + `; + document.head.appendChild(style); + const uid = getUidFromPath(); const mainContent = document.getElementById('mainContent'); const uidError = document.getElementById('uidError'); @@ -94,6 +114,7 @@ document.addEventListener('DOMContentLoaded', async () => { 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++) { @@ -115,13 +136,36 @@ document.addEventListener('DOMContentLoaded', async () => { // Check if date is in the past const isPastDate = date < startOfToday; + const isToday = date.toDateString() === today.toDateString(); - if (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 { diff --git a/public/script.js b/public/script.js index 56fb91d..f93b1e8 100644 --- a/public/script.js +++ b/public/script.js @@ -151,7 +151,7 @@ document.addEventListener('DOMContentLoaded', async () => { } } - // Clear existing time slots or message + // Clear available time slots or message timeSlotsContainer.innerHTML = ''; // Get current time in minutes for comparison diff --git a/public/styles.css b/public/styles.css index 8f7293f..5e623a6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -48,9 +48,10 @@ } .sidebar h3 { - font-size: 1rem; + font-size: 0.9rem; color: #666; margin-bottom: 5px; + font-weight: 400; } .duration { @@ -61,6 +62,8 @@ .main-content { width: 70%; padding: 20px; + position: relative; + min-height: 600px; /* Ensure minimum height for the content area */ } .calendar-header h1 { @@ -174,11 +177,16 @@ font-size: 0.75rem; color: #666; text-align: right; - position: relative; - margin-top: 2rem; + position: absolute; + bottom: 20px; + right: 20px; opacity: 0.8; transition: opacity 0.3s ease; z-index: 1; + background-color: transparent; + padding: 5px; + margin: 0; + pointer-events: auto; } .powered-by a { @@ -231,4 +239,66 @@ #admin .col-md-6 { width: 100%; } + + .powered-by { + position: relative; + bottom: auto; + right: auto; + text-align: center; + margin-top: 30px; + } +} + +/* 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 */ } \ No newline at end of file diff --git a/views/admin.html b/views/admin.html index ef56e66..1db720f 100644 --- a/views/admin.html +++ b/views/admin.html @@ -19,15 +19,41 @@
30 min
+ +The actions in this section can cause irreversible changes to the database. Please proceed with caution.
- -