@@ -593,7 +603,7 @@ function showShiftPopup(shift, targetElement) {
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
- const shiftDate = new Date(shift.Date);
+ const shiftDate = createLocalDate(shift.Date);
popup.innerHTML = `
${escapeHtml(shift.Title)}
@@ -990,10 +1000,12 @@ function createCalendarDay(dayNumber, isOtherMonth, date) {
const shiftsContainer = document.createElement('div');
shiftsContainer.className = 'calendar-shifts';
- const dateString = date.toISOString().split('T')[0];
+ // Create a consistent date string for comparison without timezone conversion
+ const calendarDateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+
+ // Debug logging to verify date matching
const dayShifts = allShifts.filter(shift => {
- const shiftDate = new Date(shift.Date);
- return shiftDate.toISOString().split('T')[0] === dateString;
+ return shift.Date === calendarDateString;
});
dayShifts.forEach(shift => {
@@ -1052,7 +1064,7 @@ function showShiftPopup(shift, targetElement) {
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
- const shiftDate = new Date(shift.Date);
+ const shiftDate = createLocalDate(shift.Date);
popup.innerHTML = `
${escapeHtml(shift.Title)}
diff --git a/map/app/public/js/shifts.js.backup b/map/app/public/js/shifts.js.backup
new file mode 100644
index 0000000..14225bd
--- /dev/null
+++ b/map/app/public/js/shifts.js.backup
@@ -0,0 +1,1147 @@
+let currentUser = null;
+let allShifts = [];
+let mySignups = [];
+let currentView = 'grid'; // 'grid' or 'calendar'
+let currentCalendarDate = new Date(); // For calendar navigation
+
+// Utility function to create a local date from YYYY-MM-DD string
+// This prevents timezone issues when displaying dates
+function createLocalDate(dateString) {
+ if (!dateString) return null;
+ const parts = dateString.split('-');
+ if (parts.length !== 3) return new Date(dateString); // fallback to original behavior
+ // Create date using local timezone (year, month-1, day)
+ return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
+}
+
+// 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
+ ? `Cancel Signup
+ ${generateCalendarDropdown(shift)}`
+ : isFull
+ ? 'Shift Full '
+ : `Sign Up `
+ }
+
+
+ `;
+ }).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)}
+ Cancel Signup
+
+
+ `;
+ }).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 `
+
+
+ 📅 Add to Calendar ▼
+
+
+
+ `;
+}
+
+// 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
+ ? `Cancel Signup
+ ${generateCalendarDropdown(shift)}`
+ : isFull
+ ? 'Shift Full '
+ : `Sign Up `
+ }
+
+
+ `;
+ }).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)}
+ Cancel Signup
+
+
+ `;
+ }).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
+ ? `Cancel Signup `
+ : isFull
+ ? 'Shift Full '
+ : `Sign Up `
+ }
+
+ `;
+
+ // 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}
+
+ Cancel
+ Confirm
+
+
+
+ `;
+
+ 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
+ ? `Cancel Signup `
+ : isFull
+ ? 'Shift Full '
+ : `Sign Up `
+ }
+
+ `;
+
+ // 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';
+ });
+});
\ No newline at end of file