558 lines
19 KiB
JavaScript
558 lines
19 KiB
JavaScript
// 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 = `
|
|
<h3>Unable to load opportunities</h3>
|
|
<p>There was a problem loading volunteer opportunities. Please refresh the page or try again later.</p>
|
|
`;
|
|
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 = '<div class="no-shifts">No volunteer opportunities available at this time.</div>';
|
|
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 `
|
|
<div class="shift-card ${isFull ? 'shift-full' : ''}" data-shift-id="${shiftId}">
|
|
<div class="shift-header">
|
|
<h3>${escapeHtml(title)}</h3>
|
|
<span class="shift-status ${isFull ? 'status-full' : 'status-open'}">
|
|
${isFull ? 'FULL' : `${spotsLeft} spots left`}
|
|
</span>
|
|
</div>
|
|
<div class="shift-details">
|
|
<div class="shift-date">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
<line x1="16" y1="2" x2="16" y2="6"></line>
|
|
<line x1="8" y1="2" x2="8" y2="6"></line>
|
|
<line x1="3" y1="10" x2="21" y2="10"></line>
|
|
</svg>
|
|
${shiftDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
|
</div>
|
|
<div class="shift-time">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
</svg>
|
|
${escapeHtml(startTime)}${endTime ? ' - ' + escapeHtml(endTime) : ''}
|
|
</div>
|
|
<div class="shift-location">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
|
<circle cx="12" cy="10" r="3"></circle>
|
|
</svg>
|
|
${escapeHtml(location)}
|
|
</div>
|
|
</div>
|
|
${description ? `<p class="shift-description">${escapeHtml(description)}</p>` : ''}
|
|
<div class="shift-volunteers">
|
|
<div class="volunteer-bar">
|
|
<div class="volunteer-fill" style="width: ${(currentVolunteers / maxVolunteers) * 100}%"></div>
|
|
</div>
|
|
<span class="volunteer-count">${currentVolunteers} / ${maxVolunteers} volunteers</span>
|
|
</div>
|
|
<button class="btn btn-primary signup-btn" data-shift-id="${shiftId}" ${isFull ? 'disabled' : ''}>
|
|
${isFull ? 'Shift Full' : 'Sign Up'}
|
|
</button>
|
|
</div>
|
|
`;
|
|
}).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 = `
|
|
<h4>${escapeHtml(title)}</h4>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="detail-label">📅 Date:</span>
|
|
<span>${shiftDate.toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">⏰ Time:</span>
|
|
<span>${escapeHtml(startTime)}${endTime ? ' - ' + escapeHtml(endTime) : ''}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">📍 Location:</span>
|
|
<span>${escapeHtml(location)}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">👥 Spots Available:</span>
|
|
<span>${spotsLeft} of ${maxVolunteers}</span>
|
|
</div>
|
|
</div>
|
|
${description ? `<p><strong>Description:</strong> ${escapeHtml(description)}</p>` : ''}
|
|
`;
|
|
|
|
// 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 = `
|
|
<span class="btn-text">${currentText}</span>
|
|
<span class="btn-loading" style="display: none;">Signing up...</span>
|
|
`;
|
|
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
|
|
? `<p><strong>Welcome to our volunteer team!</strong></p>
|
|
<p>Thank you for signing up! We've created a temporary account for you and sent login details to your email.</p>
|
|
<p>You'll also receive a confirmation email with all the shift details.</p>`
|
|
: `<p>Thank you for signing up! You'll receive an email confirmation shortly with all the details.</p>`;
|
|
|
|
messageDiv.innerHTML = `
|
|
${message}
|
|
<div class="success-actions">
|
|
<button class="btn btn-primary" id="close-success-btn">Got it!</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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);
|
|
}
|