let currentUser = null; let allShifts = []; let mySignups = []; let currentView = 'grid'; // 'grid' or 'calendar' let currentCalendarDate = new Date(); // For calendar navigation // Function to set viewport dimensions for shifts page function setShiftsViewportDimensions() { const doc = document.documentElement; doc.style.setProperty('--app-height', `${window.innerHeight}px`); doc.style.setProperty('--app-width', `${window.innerWidth}px`); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', async () => { // Set initial viewport dimensions and listen for resize events setShiftsViewportDimensions(); window.addEventListener('resize', setShiftsViewportDimensions); window.addEventListener('orientationchange', () => { setTimeout(setShiftsViewportDimensions, 100); }); await checkAuth(); await loadShifts(); await loadMySignups(); setupEventListeners(); initializeViewToggle(); // Add clear filters button handler const clearBtn = document.getElementById('clear-filters-btn'); if (clearBtn) { clearBtn.addEventListener('click', clearFilters); } }); async function checkAuth() { try { const response = await fetch('/api/auth/check'); const data = await response.json(); if (!data.authenticated) { window.location.href = '/login.html'; return; } currentUser = data.user; document.getElementById('user-email').textContent = currentUser.email; // Add admin link if user is admin if (currentUser.isAdmin) { const headerActions = document.querySelector('.header-actions'); const adminLink = document.createElement('a'); adminLink.href = '/admin.html#shifts'; adminLink.className = 'btn btn-secondary'; adminLink.textContent = '⚙️ Manage Shifts'; headerActions.insertBefore(adminLink, headerActions.firstChild); } } catch (error) { console.error('Auth check failed:', error); window.location.href = '/login.html'; } } async function loadShifts() { try { const response = await fetch('/api/shifts'); const data = await response.json(); if (data.success) { allShifts = data.shifts; displayShifts(allShifts); } } catch (error) { showStatus('Failed to load shifts', 'error'); } } async function loadMySignups() { try { const response = await fetch('/api/shifts/my-signups'); const data = await response.json(); if (data.success) { mySignups = data.signups; displayMySignups(); } else { // Still display empty signups if the endpoint fails mySignups = []; displayMySignups(); } } catch (error) { console.error('Failed to load signups:', error); // Don't show error to user, just display empty signups mySignups = []; displayMySignups(); } } function displayShifts(shifts) { const grid = document.getElementById('shifts-grid'); if (shifts.length === 0) { grid.innerHTML = '
No shifts available for the selected criteria.
'; return; } grid.innerHTML = shifts.map(shift => { const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; const shiftDate = new Date(shift.Date); return `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()}

⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers

${shift.Description ? `

${escapeHtml(shift.Description)}

` : ''}
${isSignedUp ? ` ${generateCalendarDropdown(shift)}` : isFull ? '' : `` }
`; }).join(''); // Set up event listeners using delegation setupShiftCardListeners(); // Update calendar view if it's currently active if (currentView === 'calendar') { renderCalendar(); } } function displayMySignups() { const list = document.getElementById('my-signups-list'); if (mySignups.length === 0) { list.innerHTML = '

You haven\'t signed up for any shifts yet.

'; return; } // Need to match signups with shift details for date/time info const signupsWithDetails = mySignups.map(signup => { const shift = allShifts.find(s => s.ID === signup.shift_id); return { ...signup, shift }; }).filter(s => s.shift); // Only show signups where we can find the shift details list.innerHTML = signupsWithDetails.map(signup => { const shiftDate = new Date(signup.shift.Date); return `

${escapeHtml(signup.shift.Title)}

📅 ${shiftDate.toLocaleDateString()} ⏰ ${signup.shift['Start Time']} - ${signup.shift['End Time']}

📍 ${escapeHtml(signup.shift.Location || 'TBD')}

${generateCalendarDropdown(signup.shift)}
`; }).join(''); // Set up event listeners using delegation setupMySignupsListeners(); // Update calendar view if it's currently active if (currentView === 'calendar') { renderCalendar(); } } // New function to setup listeners for shift cards function setupShiftCardListeners() { const grid = document.getElementById('shifts-grid'); if (!grid) return; // Use event delegation on the grid itself, not cloning grid.removeEventListener('click', handleShiftCardClick); // Remove if exists grid.addEventListener('click', handleShiftCardClick); } // Create a separate handler function function handleShiftCardClick(e) { const target = e.target; // Handle signup button if (target.classList.contains('signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) signupForShift(shiftId); return; } // Handle cancel button if (target.classList.contains('cancel-signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) cancelSignup(shiftId); return; } // Handle calendar toggle if (target.classList.contains('calendar-toggle')) { e.preventDefault(); e.stopPropagation(); const dropdown = target.nextElementSibling; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { if (opt !== dropdown) opt.style.display = 'none'; }); // Toggle this dropdown dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; return; } // Handle calendar option clicks if (target.classList.contains('calendar-option')) { e.stopPropagation(); // Let the link work naturally return; } } // New function to setup listeners for my signups function setupMySignupsListeners() { const list = document.getElementById('my-signups-list'); if (!list) return; // Use event delegation list.removeEventListener('click', handleMySignupsClick); // Remove if exists list.addEventListener('click', handleMySignupsClick); } // Create a separate handler for my signups function handleMySignupsClick(e) { const target = e.target; // Handle cancel button if (target.classList.contains('cancel-signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) cancelSignup(shiftId); return; } // Handle calendar toggle if (target.classList.contains('calendar-toggle')) { e.preventDefault(); e.stopPropagation(); const dropdown = target.nextElementSibling; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { if (opt !== dropdown) opt.style.display = 'none'; }); // Toggle this dropdown dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; return; } } // New function to generate calendar URLs function generateCalendarUrls(shift) { const shiftDate = new Date(shift.Date); // Parse start and end times const [startHour, startMinute] = shift['Start Time'].split(':').map(n => parseInt(n)); const [endHour, endMinute] = shift['End Time'].split(':').map(n => parseInt(n)); // Create start and end datetime objects const startDate = new Date(shiftDate); startDate.setHours(startHour, startMinute, 0, 0); const endDate = new Date(shiftDate); endDate.setHours(endHour, endMinute, 0, 0); // Format dates for different calendar formats const formatGoogleDate = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}${month}${day}T${hours}${minutes}00`; }; const formatISODate = (date) => { return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); }; // Event details const title = shift.Title; const description = shift.Description || 'Volunteer shift'; const location = shift.Location || ''; // Google Calendar URL const googleStartStr = formatGoogleDate(startDate); const googleEndStr = formatGoogleDate(endDate); const googleParams = new URLSearchParams({ action: 'TEMPLATE', text: title, dates: `${googleStartStr}/${googleEndStr}`, details: description, location: location }); const googleUrl = `https://calendar.google.com/calendar/render?${googleParams.toString()}`; // Outlook Web Calendar URL const outlookStartStr = startDate.toISOString(); const outlookEndStr = endDate.toISOString(); const outlookParams = new URLSearchParams({ path: '/calendar/action/compose', rru: 'addevent', subject: title, startdt: outlookStartStr, enddt: outlookEndStr, body: description, location: location }); const outlookUrl = `https://outlook.live.com/calendar/0/deeplink/compose?${outlookParams.toString()}`; // Apple Calendar (.ics file) - we'll generate this dynamically const icsContent = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//BNKops//Volunteer Shifts//EN', 'BEGIN:VEVENT', `UID:${shift.ID}-${Date.now()}@bnkops.com`, `DTSTART:${formatISODate(startDate)}`, `DTEND:${formatISODate(endDate)}`, `SUMMARY:${title}`, `DESCRIPTION:${description.replace(/\n/g, '\\n')}`, `LOCATION:${location}`, 'STATUS:CONFIRMED', 'END:VEVENT', 'END:VCALENDAR' ].join('\r\n'); // Create a data URL for the .ics file const icsDataUrl = 'data:text/calendar;charset=utf-8,' + encodeURIComponent(icsContent); return { google: googleUrl, outlook: outlookUrl, apple: icsDataUrl, icsFilename: `${title.replace(/[^a-z0-9]/gi, '_')}_${shift.ID}.ics` }; } // Update calendar dropdown HTML generator (remove onclick handlers) function generateCalendarDropdown(shift) { const urls = generateCalendarUrls(shift); return `
`; } // Update setupShiftCardListeners to handle calendar dropdowns function setupShiftCardListeners() { const grid = document.getElementById('shifts-grid'); if (!grid) return; // Use event delegation on the grid itself, not cloning grid.removeEventListener('click', handleShiftCardClick); // Remove if exists grid.addEventListener('click', handleShiftCardClick); } // Create a separate handler function function handleShiftCardClick(e) { const target = e.target; // Handle signup button if (target.classList.contains('signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) signupForShift(shiftId); return; } // Handle cancel button if (target.classList.contains('cancel-signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) cancelSignup(shiftId); return; } // Handle calendar toggle if (target.classList.contains('calendar-toggle')) { e.preventDefault(); e.stopPropagation(); const dropdown = target.nextElementSibling; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { if (opt !== dropdown) opt.style.display = 'none'; }); // Toggle this dropdown dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; return; } // Handle calendar option clicks if (target.classList.contains('calendar-option')) { e.stopPropagation(); // Let the link work naturally return; } } // Fix the setupMySignupsListeners function similarly function setupMySignupsListeners() { const list = document.getElementById('my-signups-list'); if (!list) return; // Use event delegation list.removeEventListener('click', handleMySignupsClick); // Remove if exists list.addEventListener('click', handleMySignupsClick); } // Create a separate handler for my signups function handleMySignupsClick(e) { const target = e.target; // Handle cancel button if (target.classList.contains('cancel-signup-btn')) { e.preventDefault(); e.stopPropagation(); const shiftId = target.getAttribute('data-shift-id'); if (shiftId) cancelSignup(shiftId); return; } // Handle calendar toggle if (target.classList.contains('calendar-toggle')) { e.preventDefault(); e.stopPropagation(); const dropdown = target.nextElementSibling; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { if (opt !== dropdown) opt.style.display = 'none'; }); // Toggle this dropdown dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; return; } } // Update the displayShifts function to properly show calendar dropdowns function displayShifts(shifts) { const grid = document.getElementById('shifts-grid'); if (shifts.length === 0) { grid.innerHTML = '
No shifts available for the selected criteria.
'; return; } grid.innerHTML = shifts.map(shift => { const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; const shiftDate = new Date(shift.Date); return `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()}

⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers

${shift.Description ? `

${escapeHtml(shift.Description)}

` : ''}
${isSignedUp ? ` ${generateCalendarDropdown(shift)}` : isFull ? '' : `` }
`; }).join(''); // Set up event listeners using delegation setupShiftCardListeners(); // Update calendar view if it's currently active if (currentView === 'calendar') { renderCalendar(); } } // Update the displayMySignups function to always show calendar dropdowns function displayMySignups() { const list = document.getElementById('my-signups-list'); if (mySignups.length === 0) { list.innerHTML = '

You haven\'t signed up for any shifts yet.

'; return; } // Need to match signups with shift details for date/time info const signupsWithDetails = mySignups.map(signup => { const shift = allShifts.find(s => s.ID === signup.shift_id); return { ...signup, shift }; }).filter(s => s.shift); // Only show signups where we can find the shift details list.innerHTML = signupsWithDetails.map(signup => { const shiftDate = new Date(signup.shift.Date); return `

${escapeHtml(signup.shift.Title)}

📅 ${shiftDate.toLocaleDateString()} ⏰ ${signup.shift['Start Time']} - ${signup.shift['End Time']}

📍 ${escapeHtml(signup.shift.Location || 'TBD')}

${generateCalendarDropdown(signup.shift)}
`; }).join(''); // Set up event listeners using delegation setupMySignupsListeners(); // Update calendar view if it's currently active if (currentView === 'calendar') { renderCalendar(); } } // Add a global variable to track popup cleanup let currentPopup = null; // Update the showShiftPopup function to handle z-index and cleanup function showShiftPopup(shift, targetElement) { // Remove any existing popup if (currentPopup) { currentPopup.remove(); currentPopup = null; } const existingPopup = document.querySelector('.shift-popup'); if (existingPopup) { existingPopup.remove(); } const popup = document.createElement('div'); popup.className = 'shift-popup'; const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; const shiftDate = new Date(shift.Date); popup.innerHTML = `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()}

⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers

${shift.Description ? `

${escapeHtml(shift.Description)}

` : ''}
${isSignedUp ? `` : isFull ? '' : `` }
`; // Position popup document.body.appendChild(popup); currentPopup = popup; // Track the current popup const rect = targetElement.getBoundingClientRect(); const popupRect = popup.getBoundingClientRect(); let left = rect.left + (rect.width / 2) - (popupRect.width / 2); let top = rect.bottom + 10; // Adjust if popup goes off screen if (left < 10) left = 10; if (left + popupRect.width > window.innerWidth - 10) { left = window.innerWidth - popupRect.width - 10; } if (top + popupRect.height > window.innerHeight - 10) { top = rect.top - popupRect.height - 10; } popup.style.left = `${left}px`; popup.style.top = `${top}px`; // Add event listeners for buttons in popup const signupBtn = popup.querySelector('.signup-btn'); const cancelBtn = popup.querySelector('.cancel-signup-btn'); if (signupBtn) { signupBtn.addEventListener('click', async (e) => { e.stopPropagation(); await signupForShift(shift.ID); popup.remove(); currentPopup = null; }); } if (cancelBtn) { cancelBtn.addEventListener('click', async (e) => { e.stopPropagation(); await cancelSignup(shift.ID); popup.remove(); currentPopup = null; }); } // Close popup when clicking outside const closePopup = (e) => { if (!popup.contains(e.target) && e.target !== targetElement) { popup.remove(); currentPopup = null; document.removeEventListener('click', closePopup); } }; setTimeout(() => { document.addEventListener('click', closePopup); }, 100); } // Close calendar dropdowns when clicking outside document.addEventListener('click', function(e) { // Don't close if clicking on a toggle or option if (!e.target.classList.contains('calendar-toggle') && !e.target.classList.contains('calendar-option') && !e.target.closest('.calendar-dropdown')) { document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); } }); async function signupForShift(shiftId) { try { const response = await fetch(`/api/shifts/${shiftId}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showStatus('Successfully signed up for shift!', 'success'); await loadShifts(); await loadMySignups(); } else { showStatus(data.error || 'Failed to sign up', 'error'); } } catch (error) { console.error('Error signing up:', error); showStatus('Failed to sign up for shift', 'error'); } } // Add a custom confirmation modal function function showConfirmModal(message, onConfirm, onCancel = null) { // Remove any existing modal const existingModal = document.querySelector('.confirm-modal'); if (existingModal) { existingModal.remove(); } // Create modal const modal = document.createElement('div'); modal.className = 'confirm-modal'; modal.innerHTML = `

Confirm Action

${message}

`; document.body.appendChild(modal); // Add event listeners const cancelBtn = modal.querySelector('.confirm-cancel'); const confirmBtn = modal.querySelector('.confirm-ok'); const backdrop = modal.querySelector('.confirm-modal-backdrop'); const cleanup = () => { modal.remove(); }; cancelBtn.addEventListener('click', () => { cleanup(); if (onCancel) onCancel(); }); confirmBtn.addEventListener('click', () => { cleanup(); onConfirm(); }); // Close on backdrop click backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { cleanup(); if (onCancel) onCancel(); } }); // Close on escape key const handleEscape = (e) => { if (e.key === 'Escape') { cleanup(); document.removeEventListener('keydown', handleEscape); if (onCancel) onCancel(); } }; document.addEventListener('keydown', handleEscape); // Focus the confirm button for keyboard navigation setTimeout(() => { confirmBtn.focus(); }, 100); } // Update the cancelSignup function to use the custom modal async function cancelSignup(shiftId) { showConfirmModal( 'Are you sure you want to cancel your signup for this shift?', async () => { try { const response = await fetch(`/api/shifts/${shiftId}/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showStatus('Signup cancelled', 'success'); await loadShifts(); await loadMySignups(); } else { showStatus(data.error || 'Failed to cancel signup', 'error'); } } catch (error) { console.error('Error cancelling signup:', error); showStatus('Failed to cancel signup', 'error'); } } ); } function setupEventListeners() { const dateFilter = document.getElementById('date-filter'); if (dateFilter) { dateFilter.addEventListener('change', filterShifts); } } function filterShifts() { const dateFilter = document.getElementById('date-filter').value; if (!dateFilter) { displayShifts(allShifts); return; } const filtered = allShifts.filter(shift => { return shift.Date === dateFilter; // Changed from shift.date to shift.Date }); displayShifts(filtered); } function clearFilters() { document.getElementById('date-filter').value = ''; loadShifts(); // Reload shifts without filters } function showStatus(message, type = 'info') { const container = document.getElementById('status-container'); if (!container) return; const messageDiv = document.createElement('div'); messageDiv.className = `status-message ${type}`; messageDiv.textContent = message; container.appendChild(messageDiv); setTimeout(() => { messageDiv.remove(); }, 5000); } function escapeHtml(text) { if (text === null || text === undefined) { return ''; } const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Calendar View Functions function initializeViewToggle() { const gridBtn = document.getElementById('grid-view-btn'); const calendarBtn = document.getElementById('calendar-view-btn'); const prevBtn = document.getElementById('prev-month'); const nextBtn = document.getElementById('next-month'); if (gridBtn && calendarBtn) { gridBtn.addEventListener('click', () => switchView('grid')); calendarBtn.addEventListener('click', () => switchView('calendar')); // Set initial active state gridBtn.classList.add('active'); } if (prevBtn && nextBtn) { prevBtn.addEventListener('click', () => navigateCalendar(-1)); nextBtn.addEventListener('click', () => navigateCalendar(1)); } } function switchView(view) { const gridView = document.getElementById('shifts-grid'); const calendarView = document.getElementById('calendar-view'); const gridBtn = document.getElementById('grid-view-btn'); const calendarBtn = document.getElementById('calendar-view-btn'); currentView = view; if (view === 'calendar') { gridView.style.display = 'none'; calendarView.style.display = 'block'; gridBtn.classList.remove('active'); calendarBtn.classList.add('active'); renderCalendar(); } else { gridView.style.display = 'grid'; calendarView.style.display = 'none'; gridBtn.classList.add('active'); calendarBtn.classList.remove('active'); } } function navigateCalendar(direction) { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + direction); renderCalendar(); } function renderCalendar() { const year = currentCalendarDate.getFullYear(); const month = currentCalendarDate.getMonth(); // Update header const monthYearHeader = document.getElementById('calendar-month-year'); if (monthYearHeader) { const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; monthYearHeader.textContent = `${monthNames[month]} ${year}`; } // Get calendar grid const calendarGrid = document.getElementById('calendar-grid'); if (!calendarGrid) return; calendarGrid.innerHTML = ''; // Add day headers const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; dayHeaders.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendarGrid.appendChild(dayHeader); }); // Get first day of month and number of days const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); const startingDayOfWeek = firstDay.getDay(); // Get previous month's last days const prevMonth = new Date(year, month, 0); const daysInPrevMonth = prevMonth.getDate(); // Add previous month's trailing days for (let i = startingDayOfWeek - 1; i >= 0; i--) { const dayNumber = daysInPrevMonth - i; const dayElement = createCalendarDay(dayNumber, true, new Date(year, month - 1, dayNumber)); calendarGrid.appendChild(dayElement); } // Add current month's days for (let day = 1; day <= daysInMonth; day++) { const currentDate = new Date(year, month, day); const dayElement = createCalendarDay(day, false, currentDate); calendarGrid.appendChild(dayElement); } // Add next month's leading days to fill the grid const totalCells = calendarGrid.children.length; const remainingCells = 42 - totalCells; // 6 rows × 7 days for (let day = 1; day <= remainingCells; day++) { const dayElement = createCalendarDay(day, true, new Date(year, month + 1, day)); calendarGrid.appendChild(dayElement); } } function createCalendarDay(dayNumber, isOtherMonth, date) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; if (isOtherMonth) { dayElement.classList.add('other-month'); } // Check if it's today const today = new Date(); if (date.toDateString() === today.toDateString()) { dayElement.classList.add('today'); } // Add day number const dayNumberElement = document.createElement('div'); dayNumberElement.className = 'calendar-day-number'; dayNumberElement.textContent = dayNumber; dayElement.appendChild(dayNumberElement); // Add shifts for this day const shiftsContainer = document.createElement('div'); shiftsContainer.className = 'calendar-shifts'; const dateString = date.toISOString().split('T')[0]; const dayShifts = allShifts.filter(shift => { const shiftDate = new Date(shift.Date); return shiftDate.toISOString().split('T')[0] === dateString; }); dayShifts.forEach(shift => { const shiftElement = createCalendarShift(shift); shiftsContainer.appendChild(shiftElement); }); dayElement.appendChild(shiftsContainer); return dayElement; } function createCalendarShift(shift) { const shiftElement = document.createElement('div'); shiftElement.className = 'calendar-shift'; // Determine shift type and color const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; if (isSignedUp) { shiftElement.classList.add('my-shift'); } else if (isFull) { shiftElement.classList.add('full-shift'); } else { shiftElement.classList.add('available-shift'); } // Set shift text (time and title) const timeText = `${shift['Start Time']} ${shift.Title}`; shiftElement.textContent = timeText; shiftElement.title = `${shift.Title}\n${shift['Start Time']} - ${shift['End Time']}\n${shift.Location || 'TBD'}`; // Add click handler shiftElement.addEventListener('click', (e) => { e.stopPropagation(); showShiftPopup(shift, e.target); }); return shiftElement; } function showShiftPopup(shift, targetElement) { // Remove any existing popup if (currentPopup) { currentPopup.remove(); currentPopup = null; } const existingPopup = document.querySelector('.shift-popup'); if (existingPopup) { existingPopup.remove(); } const popup = document.createElement('div'); popup.className = 'shift-popup'; const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; const shiftDate = new Date(shift.Date); popup.innerHTML = `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()}

⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers

${shift.Description ? `

${escapeHtml(shift.Description)}

` : ''}
${isSignedUp ? `` : isFull ? '' : `` }
`; // Position popup document.body.appendChild(popup); currentPopup = popup; // Track the current popup const rect = targetElement.getBoundingClientRect(); const popupRect = popup.getBoundingClientRect(); let left = rect.left + (rect.width / 2) - (popupRect.width / 2); let top = rect.bottom + 10; // Adjust if popup goes off screen if (left < 10) left = 10; if (left + popupRect.width > window.innerWidth - 10) { left = window.innerWidth - popupRect.width - 10; } if (top + popupRect.height > window.innerHeight - 10) { top = rect.top - popupRect.height - 10; } popup.style.left = `${left}px`; popup.style.top = `${top}px`; // Add event listeners for buttons in popup const signupBtn = popup.querySelector('.signup-btn'); const cancelBtn = popup.querySelector('.cancel-signup-btn'); if (signupBtn) { signupBtn.addEventListener('click', async (e) => { e.stopPropagation(); await signupForShift(shift.ID); popup.remove(); currentPopup = null; }); } if (cancelBtn) { cancelBtn.addEventListener('click', async (e) => { e.stopPropagation(); await cancelSignup(shift.ID); popup.remove(); currentPopup = null; }); } // Close popup when clicking outside const closePopup = (e) => { if (!popup.contains(e.target) && e.target !== targetElement) { popup.remove(); currentPopup = null; document.removeEventListener('click', closePopup); } }; setTimeout(() => { document.addEventListener('click', closePopup); }, 100); } // Keep the document click handler to close dropdowns when clicking outside document.addEventListener('click', function() { document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); });