// Public shifts JavaScript let allShifts = []; let filteredShifts = []; // Utility function to create a local date from YYYY-MM-DD string function createLocalDate(dateString) { if (!dateString) return new Date(); const parts = dateString.split('-'); if (parts.length !== 3) return new Date(dateString); return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', () => { loadPublicShifts(); setupEventListeners(); // Check if there's a specific shift in the URL hash const hash = window.location.hash; if (hash.startsWith('#shift-')) { const shiftId = hash.replace('#shift-', ''); setTimeout(() => highlightShift(shiftId), 1000); // Wait for shifts to load } }); // Load public shifts from API async function loadPublicShifts() { const loadingEl = document.getElementById('shifts-loading'); const gridEl = document.getElementById('shifts-grid'); const noShiftsEl = document.getElementById('no-shifts'); try { showLoadingState(true); const response = await fetch('/api/public/shifts'); const data = await response.json(); if (data.success) { allShifts = data.shifts || []; filteredShifts = [...allShifts]; displayShifts(filteredShifts); } else { throw new Error(data.error || 'Failed to load shifts'); } } catch (error) { console.error('Error loading shifts:', error); showStatus('Failed to load volunteer opportunities. Please try again later.', 'error'); if (noShiftsEl) { noShiftsEl.innerHTML = `

Unable to load opportunities

There was a problem loading volunteer opportunities. Please refresh the page or try again later.

`; noShiftsEl.style.display = 'block'; } } finally { showLoadingState(false); } } // Show/hide loading state function showLoadingState(show) { const loadingEl = document.getElementById('shifts-loading'); const gridEl = document.getElementById('shifts-grid'); const noShiftsEl = document.getElementById('no-shifts'); if (loadingEl) loadingEl.style.display = show ? 'flex' : 'none'; if (gridEl) gridEl.style.display = show ? 'none' : 'grid'; if (noShiftsEl && show) noShiftsEl.style.display = 'none'; } // Utility function to escape HTML function escapeHtml(text) { // Handle null/undefined values if (text === null || text === undefined) { return ''; } // Convert to string if not already text = String(text); const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } // Display shifts in the grid function displayShifts(shifts) { const grid = document.getElementById('shifts-grid'); if (!shifts || shifts.length === 0) { grid.innerHTML = '
No volunteer opportunities available at this time.
'; return; } grid.innerHTML = shifts.map(shift => { // NocoDB may use 'Id', 'ID', or 'id' - check all variations const shiftId = shift.id || shift.Id || shift.ID || shift.ncRecordId; // Debug log to see what fields are available console.log('Shift object keys:', Object.keys(shift)); console.log('Shift ID found:', shiftId); const shiftDate = createLocalDate(shift.Date); const currentVolunteers = shift['Current Volunteers'] || 0; const maxVolunteers = shift['Max Volunteers'] || 0; const spotsLeft = Math.max(0, maxVolunteers - currentVolunteers); const isFull = spotsLeft === 0; // Use empty string as fallback for any missing fields const title = shift.Title || 'Untitled Shift'; const location = shift.Location || 'Location TBD'; const startTime = shift['Start Time'] || ''; const endTime = shift['End Time'] || ''; const description = shift.Description || ''; return `

${escapeHtml(title)}

${isFull ? 'FULL' : `${spotsLeft} spots left`}
${shiftDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
${escapeHtml(startTime)}${endTime ? ' - ' + escapeHtml(endTime) : ''}
${escapeHtml(location)}
${description ? `

${escapeHtml(description)}

` : ''}
${currentVolunteers} / ${maxVolunteers} volunteers
`; }).join(''); // Attach event listeners to all signup buttons after rendering setTimeout(() => { const signupButtons = grid.querySelectorAll('.signup-btn:not([disabled])'); signupButtons.forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const shiftId = e.currentTarget.getAttribute('data-shift-id'); console.log('Signup button clicked for shift:', shiftId); showSignupModal(shiftId); }); }); console.log(`Attached listeners to ${signupButtons.length} signup buttons`); }, 0); } // Setup event listeners function setupEventListeners() { // Date filter const dateFilter = document.getElementById('date-filter'); if (dateFilter) { dateFilter.addEventListener('change', applyFilters); } // Clear filters button const clearFiltersBtn = document.getElementById('clear-filters-btn'); if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', clearFilters); } // Modal close buttons - Fixed to properly close modals const closeModalBtn = document.getElementById('close-modal'); const cancelSignupBtn = document.getElementById('cancel-signup'); if (closeModalBtn) { closeModalBtn.addEventListener('click', (e) => { e.preventDefault(); closeSignupModal(); }); } if (cancelSignupBtn) { cancelSignupBtn.addEventListener('click', (e) => { e.preventDefault(); closeSignupModal(); }); } // Success modal close const closeSuccessBtn = document.getElementById('close-success-btn'); const closeSuccessModal = document.getElementById('close-success-modal'); if (closeSuccessBtn) { closeSuccessBtn.addEventListener('click', (e) => { e.preventDefault(); closeSuccessModals(); }); } if (closeSuccessModal) { closeSuccessModal.addEventListener('click', (e) => { e.preventDefault(); closeSuccessModals(); }); } // Signup form submission const signupForm = document.getElementById('signup-form'); if (signupForm) { signupForm.addEventListener('submit', handleSignupSubmit); } // Close modals when clicking outside document.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { closeAllModals(); } }); } // Close signup modal function closeSignupModal() { const modal = document.getElementById('signup-modal'); if (modal) { modal.classList.add('hidden'); modal.style.display = 'none'; } } // Close success modal function closeSuccessModals() { const modal = document.getElementById('success-modal'); if (modal) { modal.classList.add('hidden'); modal.style.display = 'none'; } } // Close all modals function closeAllModals() { closeSignupModal(); closeSuccessModals(); } // Apply filters function applyFilters() { const dateFilter = document.getElementById('date-filter'); const filterDate = dateFilter ? dateFilter.value : null; filteredShifts = allShifts.filter(shift => { if (filterDate) { const shiftDate = shift.date; if (shiftDate !== filterDate) { return false; } } return true; }); displayShifts(filteredShifts); } // Clear all filters function clearFilters() { const dateFilter = document.getElementById('date-filter'); if (dateFilter) dateFilter.value = ''; filteredShifts = [...allShifts]; displayShifts(filteredShifts); } // Show signup modal for specific shift function showSignupModal(shiftId) { console.log('showSignupModal called with shiftId:', shiftId); // Find shift using the same ID field variations const shift = allShifts.find(s => { const sId = s.id || s.Id || s.ID || s.ncRecordId; return sId == shiftId; }); if (!shift) { console.error('Shift not found:', shiftId); console.log('Available shifts:', allShifts.map(s => ({ id: s.id, Id: s.Id, ID: s.ID, ncRecordId: s.ncRecordId, Title: s.Title }))); return; } const modal = document.getElementById('signup-modal'); const shiftDetails = document.getElementById('shift-details'); if (!modal || !shiftDetails) { console.error('Modal elements not found'); return; } const shiftDate = createLocalDate(shift.Date); // Use the actual field names from the shift object const title = shift.Title || 'Untitled Shift'; const startTime = shift['Start Time'] || ''; const endTime = shift['End Time'] || ''; const location = shift.Location || 'Location TBD'; const description = shift.Description || ''; const maxVolunteers = shift['Max Volunteers'] || 0; const currentVolunteers = shift['Current Volunteers'] || 0; const spotsLeft = Math.max(0, maxVolunteers - currentVolunteers); shiftDetails.innerHTML = `

${escapeHtml(title)}

📅 Date: ${shiftDate.toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
⏰ Time: ${escapeHtml(startTime)}${endTime ? ' - ' + escapeHtml(endTime) : ''}
📍 Location: ${escapeHtml(location)}
👥 Spots Available: ${spotsLeft} of ${maxVolunteers}
${description ? `

Description: ${escapeHtml(description)}

` : ''} `; // Store shift ID in form for submission const form = document.getElementById('signup-form'); if (form) { form.setAttribute('data-shift-id', shiftId); } // Clear previous form data const emailField = document.getElementById('signup-email'); const nameField = document.getElementById('signup-name'); const phoneField = document.getElementById('signup-phone'); if (emailField) emailField.value = ''; if (nameField) nameField.value = ''; if (phoneField) phoneField.value = ''; // Show the modal with proper display modal.classList.remove('hidden'); modal.style.display = 'flex'; // Changed from 'block' to 'flex' for centering // Ensure modal content is scrolled to top const modalContent = modal.querySelector('.modal-content'); if (modalContent) { modalContent.scrollTop = 0; } console.log('Modal should now be visible and centered'); } // Handle signup form submission async function handleSignupSubmit(e) { e.preventDefault(); const form = e.target; const shiftId = form.getAttribute('data-shift-id'); const submitBtn = document.getElementById('submit-signup'); if (!submitBtn) { console.error('Submit button not found'); return; } // Get or create the button text elements let btnText = submitBtn.querySelector('.btn-text'); let btnLoading = submitBtn.querySelector('.btn-loading'); // If elements don't exist, the button might have plain text - restructure it if (!btnText && !btnLoading) { const currentText = submitBtn.textContent || 'Sign Up'; submitBtn.innerHTML = ` ${currentText} `; btnText = submitBtn.querySelector('.btn-text'); btnLoading = submitBtn.querySelector('.btn-loading'); } if (!shiftId) { showStatus('No shift selected', 'error'); return; } const formData = { email: document.getElementById('signup-email')?.value?.trim() || '', name: document.getElementById('signup-name')?.value?.trim() || '', phone: document.getElementById('signup-phone')?.value?.trim() || '' }; // Basic validation if (!formData.email || !formData.name) { showStatus('Please fill in all required fields', 'error'); return; } // Email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(formData.email)) { showStatus('Please enter a valid email address', 'error'); return; } try { // Show loading state submitBtn.disabled = true; if (btnText) btnText.style.display = 'none'; if (btnLoading) btnLoading.style.display = 'inline'; const response = await fetch(`/api/public/shifts/${shiftId}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const data = await response.json(); if (data.success) { // Close signup modal closeSignupModal(); // Show success modal showSuccessModal(data.isNewUser); // Refresh shifts to update counts loadPublicShifts(); } else { showStatus(data.error || 'Signup failed. Please try again.', 'error'); } } catch (error) { console.error('Signup error:', error); showStatus('Network error. Please try again.', 'error'); } finally { // Reset button state submitBtn.disabled = false; if (btnText) btnText.style.display = 'inline'; if (btnLoading) btnLoading.style.display = 'none'; } } // Show success modal function showSuccessModal(isNewUser) { const modal = document.getElementById('success-modal'); const messageDiv = document.getElementById('success-message'); if (!modal || !messageDiv) return; const message = isNewUser ? `

Welcome to our volunteer team!

Thank you for signing up! We've created a temporary account for you and sent login details to your email.

You'll also receive a confirmation email with all the shift details.

` : `

Thank you for signing up! You'll receive an email confirmation shortly with all the details.

`; messageDiv.innerHTML = ` ${message}
`; // Re-attach event listener for the new button const closeBtn = messageDiv.querySelector('#close-success-btn'); if (closeBtn) { closeBtn.addEventListener('click', closeModals); } modal.classList.remove('hidden'); // Auto-close after 10 seconds setTimeout(() => { if (!modal.classList.contains('hidden')) { closeModals(); } }, 10000); } // Close all modals function closeModals() { const modals = document.querySelectorAll('.modal'); modals.forEach(modal => { modal.classList.add('hidden'); }); } // Highlight specific shift (for direct links) function highlightShift(shiftId) { const shiftCard = document.querySelector(`[data-shift-id="${shiftId}"]`); if (shiftCard) { shiftCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); shiftCard.style.border = '3px solid var(--primary-color)'; setTimeout(() => { shiftCard.style.border = '1px solid var(--border-color)'; }, 3000); } } // Show status message function showStatus(message, type = 'info') { const container = document.getElementById('status-container'); if (!container) return; const statusEl = document.createElement('div'); statusEl.className = `status-message status-${type}`; statusEl.textContent = message; container.appendChild(statusEl); // Auto-remove after 5 seconds setTimeout(() => { if (statusEl.parentNode) { statusEl.parentNode.removeChild(statusEl); } }, 5000); }