PageSourceSearch

https://projectconnectgroup.netlify.app/js/script.js?v=44

js projectconnectgroup.netlify.app collected 2026-10-03 11:17:58 UTC 119,645 bytes, 3,040 lines download raw bytes

1// ==========================================
2// MOBILE MENU TOGGLE
3// ==========================================
4const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
5const navLinks = document.querySelector('.nav-links');
6
7if (mobileMenuToggle) {
8    const toggleMenu = () => {
9        navLinks.classList.toggle('active');
10        mobileMenuToggle.classList.toggle('active');
11
12        // Animate hamburger icon
13        const spans = mobileMenuToggle.querySelectorAll('span');
14        if (navLinks.classList.contains('active')) {
15            spans[0].style.transform = 'rotate(45deg) translateY(8px)';
16            spans[1].style.opacity = '0';
17            spans[2].style.transform = 'rotate(-45deg) translateY(-8px)';
18        } else {
19            spans[0].style.transform = '';
20            spans[1].style.opacity = '1';
21            spans[2].style.transform = '';
22        }
23    };
24
25    // Add both click and touch events (touchend is more reliable than touchstart)
26    mobileMenuToggle.addEventListener('click', toggleMenu);
27    mobileMenuToggle.addEventListener('touchend', (e) => {
28        e.preventDefault();
29        toggleMenu();
30    }, { passive: false });
31
32    // Close menu when clicking on a link
33    navLinks.querySelectorAll('a').forEach(link => {
34        link.addEventListener('click', () => {
35            navLinks.classList.remove('active');
36            mobileMenuToggle.classList.remove('active');
37            const spans = mobileMenuToggle.querySelectorAll('span');
38            spans[0].style.transform = '';
39            spans[1].style.opacity = '1';
40            spans[2].style.transform = '';
41        });
42    });
43}
44
45// ==========================================
46// PASSWORD VISIBILITY TOGGLE
47// ==========================================
48function togglePasswordVisibility(inputId) {
49    const input = document.getElementById(inputId);
50    const toggleButton = input.nextElementSibling;
51    const icon = toggleButton.querySelector('.password-toggle-icon');
52
53    if (input.type === 'password') {
54        input.type = 'text';
55        icon.textContent = '🙈'; // Eye with slash
56        toggleButton.setAttribute('aria-label', 'Hide password');
57    } else {
58        input.type = 'password';
59        icon.textContent = '👁️'; // Eye
60        toggleButton.setAttribute('aria-label', 'Show password');
61    }
62}
63
64// ==========================================
65// BUTTON LOADING STATE HELPERS
66// ==========================================
67function setButtonLoading(button, isLoading = true) {
68    if (isLoading) {
69        button.dataset.originalText = button.textContent;
70        button.classList.add('loading');
71        button.disabled = true;
72    } else {
73        button.classList.remove('loading');
74        button.disabled = false;
75        if (button.dataset.originalText) {
76            button.textContent = button.dataset.originalText;
77            delete button.dataset.originalText;
78        }
79    }
80}
81
82// ==========================================
83// FORM VALIDATION HELPERS
84// ==========================================
85function showFieldError(input, message) {
86    const formGroup = input.closest('.form-group');
87    if (formGroup) {
88        formGroup.classList.add('error');
89        formGroup.classList.remove('success');
90
91        // Remove existing error message
92        const existingError = formGroup.querySelector('.error-message');
93        if (existingError) existingError.remove();
94
95        // Add new error message
96        const errorDiv = document.createElement('small');
97        errorDiv.className = 'error-message';
98        errorDiv.textContent = message;
99        input.parentNode.appendChild(errorDiv);
100    } else {
101        input.classList.add('error');
102    }
103}
104
105function clearFieldError(input) {
106    const formGroup = input.closest('.form-group');
107    if (formGroup) {
108        formGroup.classList.remove('error');
109        const errorMessage = formGroup.querySelector('.error-message');
110        if (errorMessage) errorMessage.remove();
111    } else {
112        input.classList.remove('error');
113    }
114}
115
116function showFieldSuccess(input) {
117    const formGroup = input.closest('.form-group');
118    if (formGroup) {
119        formGroup.classList.add('success');
120        formGroup.classList.remove('error');
121        const errorMessage = formGroup.querySelector('.error-message');
122        if (errorMessage) errorMessage.remove();
123    }
124}
125
126function clearAllFieldErrors(form) {
127    const errorGroups = form.querySelectorAll('.form-group.error');
128    errorGroups.forEach(group => {
129        group.classList.remove('error');
130        const errorMessage = group.querySelector('.error-message');
131        if (errorMessage) errorMessage.remove();
132    });
133
134    const errorInputs = form.querySelectorAll('input.error, select.error, textarea.error');
135    errorInputs.forEach(input => input.classList.remove('error'));
136}
137
138// ==========================================
139// SMOOTH SCROLLING
140// ==========================================
141document.querySelectorAll('a[href^="#"]').forEach(anchor => {
142    anchor.addEventListener('click', function (e) {
143        const href = this.getAttribute('href');
144
145        // Only prevent default for anchor links that aren't just "#"
146        if (href && href !== '#') {
147            const target = document.querySelector(href);
148            if (target) {
149                e.preventDefault();
150                const offsetTop = target.offsetTop - 80; // Account for fixed navbar
151                window.scrollTo({
152                    top: offsetTop,
153                    behavior: 'smooth'
154                });
155            }
156        }
157    });
158});
159
160// ==========================================
161// NAVBAR SCROLL EFFECT
162// ==========================================
163const navbar = document.querySelector('.navbar');
164let lastScroll = 0;
165
166window.addEventListener('scroll', () => {
167    const currentScroll = window.pageYOffset;
168
169    // Add scrolled class for styling
170    if (currentScroll > 50) {
171        navbar.classList.add('scrolled');
172    } else {
173        navbar.classList.remove('scrolled');
174    }
175
176    lastScroll = currentScroll;
177});
178
179// ==========================================
180// INTERSECTION OBSERVER FOR ANIMATIONS
181// ==========================================
182const observerOptions = {
183    threshold: 0.1,
184    rootMargin: '0px 0px -50px 0px'
185};
186
187const observer = new IntersectionObserver((entries) => {
188    entries.forEach(entry => {
189        if (entry.isIntersecting) {
190            entry.target.classList.add('fade-in-up');
191            observer.unobserve(entry.target);
192        }
193    });
194}, observerOptions);
195
196// Observe elements
197document.addEventListener('DOMContentLoaded', () => {
198    const animateElements = document.querySelectorAll(
199        '.value-card, .timeline-item, .event-card, .testimonial-card, .contact-card, .topic-badge'
200    );
201
202    animateElements.forEach(el => {
203        observer.observe(el);
204    });
205});
206
207// ==========================================
208// STATISTICS COUNTER ANIMATION
209// ==========================================
210const animateCounter = (element, target, duration = 2000) => {
211    const start = 0;
212    const increment = target / (duration / 16); // 60 FPS
213    let current = start;
214
215    const updateCounter = () => {
216        current += increment;
217        if (current < target) {
218            element.textContent = Math.floor(current).toLocaleString() + '+';
219            requestAnimationFrame(updateCounter);
220        } else {
221            element.textContent = target.toLocaleString() + '+';
222        }
223    };
224
225    updateCounter();
226};
227
228// Observe stats section
229const statsObserver = new IntersectionObserver((entries) => {
230    entries.forEach(entry => {
231        if (entry.isIntersecting) {
232            const statNumbers = document.querySelectorAll('.stat-number');
233            statNumbers.forEach(stat => {
234                const text = stat.textContent;
235                const number = parseInt(text.replace(/[^0-9]/g, ''));
236                animateCounter(stat, number);
237            });
238            statsObserver.unobserve(entry.target);
239        }
240    });
241}, { threshold: 0.5 });
242
243const heroStats = document.querySelector('.hero-stats');
244if (heroStats) {
245    statsObserver.observe(heroStats);
246}
247
248// ==========================================
249// ACTIVE NAVIGATION LINK
250// ==========================================
251const sections = document.querySelectorAll('section[id]');
252
253const highlightNavigation = () => {
254    const scrollPosition = window.pageYOffset + 100;
255
256    sections.forEach(section => {
257        const sectionTop = section.offsetTop;
258        const sectionHeight = section.offsetHeight;
259        const sectionId = section.getAttribute('id');
260        const navLink = document.querySelector(`.nav-links a[href="#${sectionId}"]`);
261
262        if (navLink) {
263            if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
264                navLink.style.color = 'var(--primary-color)';
265            } else {
266                navLink.style.color = '';
267            }
268        }
269    });
270};
271
272window.addEventListener('scroll', highlightNavigation);
273
274// ==========================================
275// PARALLAX EFFECT
276// ==========================================
277window.addEventListener('scroll', () => {
278    const scrolled = window.pageYOffset;
279    const heroLogo = document.querySelector('.hero-logo');
280
281    if (heroLogo && scrolled < window.innerHeight) {
282        heroLogo.style.transform = `translateY(${scrolled * 0.5}px)`;
283        heroLogo.style.opacity = 1 - (scrolled / window.innerHeight);
284    }
285});
286
287// ==========================================
288// READING PROGRESS BAR
289// ==========================================
290const scrollProgressBar = document.getElementById('scroll-progress');
291if (scrollProgressBar) {
292    let progressTicking = false;
293    const updateScrollProgress = () => {
294        const doc = document.documentElement;
295        const max = doc.scrollHeight - window.innerHeight;
296        const ratio = max > 0 ? Math.min(window.pageYOffset / max, 1) : 0;
297        scrollProgressBar.style.transform = `scaleX(${ratio})`;
298        progressTicking = false;
299    };
300    window.addEventListener('scroll', () => {
301        if (!progressTicking) {
302            progressTicking = true;
303            requestAnimationFrame(updateScrollProgress);
304        }
305    }, { passive: true });
306    window.addEventListener('resize', updateScrollProgress, { passive: true });
307    updateScrollProgress();
308}
309
310// ==========================================
311// HERO VIDEO: REDUCED MOTION + PAUSE CONTROL
312// ==========================================
313const heroVideo = document.getElementById('hero-video');
314const heroVideoToggle = document.getElementById('hero-video-toggle');
315if (heroVideo && heroVideoToggle) {
316    const iconPause = heroVideoToggle.querySelector('.icon-pause');
317    const iconPlay = heroVideoToggle.querySelector('.icon-play');
318    let heroVideoUserPaused = false;
319
320    const setVideoToggleState = (paused) => {
321        if (iconPause) iconPause.style.display = paused ? 'none' : '';
322        if (iconPlay) iconPlay.style.display = paused ? '' : 'none';
323        heroVideoToggle.setAttribute('aria-pressed', String(paused));
324        heroVideoToggle.setAttribute('aria-label', paused ? 'Play background video' : 'Pause background video');
325    };
326
327    // Users who prefer reduced motion get a still frame, not an autoplaying loop
328    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
329        heroVideo.removeAttribute('autoplay');
330        heroVideo.pause();
331        heroVideoUserPaused = true;
332        setVideoToggleState(true);
333    } else {
334        setVideoToggleState(false);
335    }
336
337    heroVideoToggle.addEventListener('click', () => {
338        if (heroVideo.paused) {
339            heroVideoUserPaused = false;
340            heroVideo.play().catch(() => {});
341            setVideoToggleState(false);
342        } else {
343            heroVideoUserPaused = true;
344            heroVideo.pause();
345            setVideoToggleState(true);
346        }
347    });
348
349    // Save bandwidth/battery: pause playback while the hero is off screen
350    const heroSection = document.getElementById('home');
351    if (heroSection && 'IntersectionObserver' in window) {
352        const heroVisibility = new IntersectionObserver((entries) => {
353            entries.forEach(entry => {
354                if (!entry.isIntersecting) {
355                    heroVideo.pause();
356                } else if (!heroVideoUserPaused) {
357                    heroVideo.play().catch(() => {});
358                }
359            });
360        }, { threshold: 0.05 });
361        heroVisibility.observe(heroSection);
362    }
363}
364
365// Email buttons now use mailto: links directly
366// No need for copy functionality
367
368// ==========================================
369// RESPONSIVE VIDEO EMBEDS (if needed in future)
370// ==========================================
371const setVideoAspectRatio = () => {
372    const videos = document.querySelectorAll('iframe[src*="youtube"], iframe[src*="vimeo"]');
373    videos.forEach(video => {
374        const width = video.offsetWidth;
375        video.style.height = (width * 9 / 16) + 'px';
376    });
377};
378
379window.addEventListener('resize', setVideoAspectRatio);
380setVideoAspectRatio();
381
382// ==========================================
383// FETCH AND DISPLAY EVENTS
384// ==========================================
385async function fetchAndDisplayEvents() {
386    try {
387        console.log('Fetching events from API...');
388        const response = await fetch('/.netlify/functions/get-events');
389
390        console.log('Response status:', response.status);
391
392        if (!response.ok) {
393            const errorText = await response.text();
394            console.error('API error response:', errorText);
395            throw new Error(`Failed to fetch events: ${response.status}`);
396        }
397
398        const data = await response.json();
399        console.log('Events data received:', data);
400
401        // Store the master list, build the search/filter toolbar, then render
402        allUpcomingEvents = data.events || [];
403        buildEventsToolbar(allUpcomingEvents);
404        renderEvents();
405
406        // Populate the registration form dropdown
407        populateEventDropdown(data.events);
408
409    } catch (error) {
410        console.error('Error fetching events:', error);
411        console.error('Error details:', error.message);
412
413        const container = document.getElementById('events-container');
414        if (container) {
415            container.setAttribute('aria-busy', 'false');
416            container.innerHTML = '<div class="no-events">Unable to load events. Please try again later.</div>';
417        }
418
419        const eventSelect = document.getElementById('event-select');
420        if (eventSelect) {
421            eventSelect.innerHTML = '<option value="">Unable to load events</option>';
422        }
423    }
424}
425
426// ==========================================
427// UTILITY FUNCTIONS FOR EVENT DISPLAY
428// ==========================================
429
430// Format time from 24-hour format (HH:MM:SS) to 12-hour format (h:MM AM/PM)
431function formatTime(timeString) {
432    if (!timeString) return '6:00 PM';
433
434    // Handle both HH:MM:SS and HH:MM formats
435    const timeParts = timeString.split(':');
436    let hours = parseInt(timeParts[0], 10);
437    const minutes = timeParts[1];
438
439    const ampm = hours >= 12 ? 'PM' : 'AM';
440    hours = hours % 12;
441    hours = hours ? hours : 12; // Convert 0 to 12
442
443    return `${hours}:${minutes} ${ampm}`;
444}
445
446// ==========================================
447// EVENT SEARCH & LOCATION FILTERS
448// ==========================================
449let allUpcomingEvents = [];
450// Event IDs the signed-in member is already booked on (see loadMemberRegistrations)
451const registeredEventIds = new Set();
452let showingAllEvents = false;
453let eventSearchQuery = '';
454let eventLocationFilter = 'all';
455
456// Locations arrive as full venue addresses ("NQ64, 14-16 Baldwin St, Bristol,
457// BS1 1SE, England"). For filter chips we want just the city: drop postcodes,
458// countries and placeholders, then take the last remaining part.
459const LOCATION_NOISE_RE = /^(uk|united kingdom|england|scotland|wales|ireland|northern ireland|australia|tbc|tbd)$/i;
460const POSTCODE_RE = /^([A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}|\d{4,5})$/i;
461
462function getEventCity(event) {
463    const parts = String(event.location || '').split(',')
464        .map(part => part.trim())
465        .filter(part => part && !LOCATION_NOISE_RE.test(part) && !POSTCODE_RE.test(part));
466    return parts.length ? parts[parts.length - 1] : '';
467}
468
469function getFilteredEvents() {
470    const query = eventSearchQuery.trim().toLowerCase();
471    return allUpcomingEvents.filter(event => {
472        if (eventLocationFilter !== 'all' && getEventCity(event) !== eventLocationFilter) {
473            return false;
474        }
475        if (!query) return true;
476        return [event.eventName, event.location, event.country, event.description]
477            .some(field => (field || '').toLowerCase().includes(query));
478    });
479}
480
481function eventFiltersActive() {
482    return eventSearchQuery.trim() !== '' || eventLocationFilter !== 'all';
483}
484
485function renderEvents() {
486    displayEventsGrid(getFilteredEvents(), eventFiltersActive());
487}
488
489function clearEventFilters() {
490    eventSearchQuery = '';
491    eventLocationFilter = 'all';
492    const searchInput = document.getElementById('events-search');
493    if (searchInput) searchInput.value = '';
494    const chips = document.querySelectorAll('#events-filter-chips .filter-chip');
495    chips.forEach(chip => {
496        const isAll = chip.dataset.location === 'all';
497        chip.classList.toggle('active', isAll);
498        chip.setAttribute('aria-pressed', String(isAll));
499    });
500    renderEvents();
501}
502
503function buildEventsToolbar(events) {
504    const toolbar = document.getElementById('events-toolbar');
505    if (!toolbar) return;
506
507    // A toolbar is only useful when there is something to narrow down
508    if (!events || events.length < 2) {
509        toolbar.hidden = true;
510        return;
511    }
512    toolbar.hidden = false;
513
514    const chipsWrap = document.getElementById('events-filter-chips');
515    if (chipsWrap) {
516        const locations = [...new Set(
517            events.map(getEventCity).filter(Boolean)
518        )].sort((a, b) => a.localeCompare(b));
519
520        if (locations.length > 1) {
521            chipsWrap.innerHTML =
522                `<button type="button" class="filter-chip active" data-location="all" aria-pressed="true">All locations</button>` +
523                locations.map(loc =>
524                    `<button type="button" class="filter-chip" data-location="${escapeHtmlAttr(loc)}" aria-pressed="false">${escapeHtml(loc)}</button>`
525                ).join('');
526        } else {
527            chipsWrap.innerHTML = '';
528        }
529    }
530
531    if (toolbar.dataset.wired === 'true') return;
532    toolbar.dataset.wired = 'true';
533
534    // Chip selection (delegated so rebuilt chips keep working)
535    if (chipsWrap) {
536        chipsWrap.addEventListener('click', (e) => {
537            const chip = e.target.closest('.filter-chip');
538            if (!chip) return;
539            eventLocationFilter = chip.dataset.location || 'all';
540            chipsWrap.querySelectorAll('.filter-chip').forEach(c => {
541                const active = c === chip;
542                c.classList.toggle('active', active);
543                c.setAttribute('aria-pressed', String(active));
544            });
545            renderEvents();
546        });
547    }
548
549    // Debounced text search
550    const searchInput = document.getElementById('events-search');
551    if (searchInput) {
552        let searchTimer = null;
553        searchInput.addEventListener('input', () => {
554            clearTimeout(searchTimer);
555            searchTimer = setTimeout(() => {
556                eventSearchQuery = searchInput.value;
557                renderEvents();
558            }, 150);
559        });
560
561        // Press "/" anywhere to jump to the event search box
562        document.addEventListener('keydown', (e) => {
563            if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey) return;
564            const target = e.target;
565            const tag = target.tagName;
566            if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable) return;
567            if (document.querySelector('.modal.active')) return;
568            e.preventDefault();
569            searchInput.focus();
570            searchInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
571        });
572    }
573}
574
575// ==========================================
576// DISPLAY EVENTS IN GRID
577// ==========================================
578
579function displayEventsGrid(events, isFiltered = false) {
580    const container = document.getElementById('events-container');
581
582    if (!container) {
583        console.log('Events container not found on this page');
584        return;
585    }
586
587    container.setAttribute('aria-busy', 'false');
588    const showMoreWrap = document.getElementById('show-more-events');
589
590    if (!events || events.length === 0) {
591        if (isFiltered) {
592            container.innerHTML = `
593                <div class="no-events no-events--filtered">
594                    <p>No events match your search.</p>
595                    <button type="button" class="clear-filters-btn" onclick="clearEventFilters()">Clear filters</button>
596                </div>
597            `;
598        } else {
599            container.innerHTML = '<div class="no-events">No upcoming events at the moment. Check back soon!</div>';
600        }
601        if (showMoreWrap) showMoreWrap.style.display = 'none';
602        return;
603    }
604
605    // Unfiltered view shows the first 3 with a "Show More" toggle;
606    // an active search/filter always shows every match
607    const eventsToShow = (showingAllEvents || isFiltered) ? events : events.slice(0, 3);
608
609    if (showMoreWrap) {
610        showMoreWrap.style.display = (!isFiltered && events.length > 3) ? 'block' : 'none';
611        const toggleBtn = document.getElementById('toggle-events-btn');
612        if (toggleBtn) {
613            toggleBtn.textContent = showingAllEvents ? 'Show Less Events' : `Show More Events (${events.length - 3} more)`;
614        }
615    }
616
617    container.innerHTML = eventsToShow.map(event => {
618        const eventDate = new Date(event.eventDate);
619        const dateFormatted = eventDate.toLocaleDateString('en-GB', {
620            weekday: 'long',
621            day: 'numeric',
622            month: 'long',
623            year: 'numeric'
624        });
625
626        // Check if registration is available
627        const hasValidDate = event.eventDate && event.eventDate.trim() !== '';
628        const hasValidLocation = event.location &&
629                                  event.location.trim() !== '' &&
630                                  event.location.toUpperCase() !== 'TBC';
631        const canRegister = hasValidDate && hasValidLocation;
632
633        // Sanitize all user-controlled data to prevent XSS
634        const safeName = escapeHtml(event.eventName);
635        const safeLocation = escapeHtml(event.location);
636        const safeCountry = escapeHtml(event.country);
637        const safeNotes = escapeHtml(event.notes);
638        const safeDescription = escapeHtml(event.description);
639        const safeId = escapeHtmlAttr(event.id);
640        const safeNameAttr = escapeHtmlAttr(event.eventName);
641        const safeDateAttr = escapeHtmlAttr(dateFormatted);
642        const safeLocationAttr = escapeHtmlAttr(event.location + ', ' + event.country);
643        const safeFeaturedImage = event.featuredImage ? sanitizeUrl(event.featuredImage) : null;
644
645        // Donation amount shown in the event's own currency (£10, A$15, ...)
646        const donationLabel = escapeHtml(window.PCGCurrency
647            ? window.PCGCurrency.getCurrencyForCountry(event.country).label
648            : '£10');
649
650        // Display affiliates/sponsors if available
651        let partnersInfo = '';
652        if (event.partners) {
653            const partnersList = Array.isArray(event.partners) ? event.partners.join(', ') : event.partners;
654            const safePartners = escapeHtml(partnersList);
655            partnersInfo = `
656                <div class="event-detail">
657                    <strong>🤝 Affiliates:</strong>
658                    <span>${safePartners}</span>
659                </div>
660            `;
661        }
662
663        // Partner Event info - shown at bottom as collapsible
664        const hasPartnerEvent = event.partnerEvent && event.partnerEventLocation;
665        let partnerEventSection = '';
666        if (hasPartnerEvent) {
667            const eventId = event.id.replace(/[^a-zA-Z0-9]/g, '');
668            const safePartnerName = escapeHtml(event.partnerEventName);
669            const safePartnerLocation = escapeHtml(event.partnerEventLocation);
670            const safePartnerLink = sanitizeUrl(event.partnerEventLink);
671            const hasValidPartnerLocation = event.partnerEventLocation &&
672                                           event.partnerEventLocation.trim() !== '' &&
673                                           event.partnerEventLocation.toUpperCase() !== 'TBC';
674
675            partnerEventSection = `
676                <button class="partner-event-toggle" onclick="togglePartnerEvent('${eventId}')">
677                    🤝 Partner Event Details
678                    <span class="toggle-icon" id="toggle-${eventId}">▼</span>
679                </button>
680                <div class="partner-event-info" id="partner-${eventId}" style="display: none;">
681                    ${event.partnerEventName ? `
682                    <div class="event-detail">
683                        <strong>Event Name:</strong>
684                        <span>${safePartnerName}</span>
685                    </div>
686                    ` : ''}
687                    <div class="event-detail">
688                        <div style="display: flex; flex-direction: column; gap: 0.35rem;">
689                            <strong>📍 Location:</strong>
690                            ${hasValidPartnerLocation ? `
691                                <a href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(safePartnerLocation)}"
692                                   target="_blank"
693                                   rel="noopener noreferrer"
694                                   class="map-link"
695                                   title="View on Google Maps"
696                                   style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.3rem 0.6rem; background: var(--primary-color); color: white; border-radius: 4px; text-decoration: none; font-size: 0.75rem; font-weight: 600; transition: all 0.3s ease; white-space: nowrap; width: fit-content;">
697                                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
698                                        <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
699                                        <circle cx="12" cy="10" r="3"></circle>
700                                    </svg>
701                                    View Map
702                                </a>
703                            ` : ''}
704                        </div>
705                        <span>${safePartnerLocation}</span>
706                    </div>
707                    ${safePartnerLink ? `
708                        <div class="event-detail">
709                            <a href="${safePartnerLink}" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: underline;">
710                                View Partner Event Details →
711                            </a>
712                        </div>
713                    ` : ''}
714                </div>
715            `;
716        }
717
718        return `
719            <div class="event-card${safeFeaturedImage ? ' event-card--has-image' : ''}">
720                ${safeFeaturedImage ? `
721                    <div class="event-card-image">
722                        <img src="${safeFeaturedImage}" alt="${safeName}" loading="lazy">
723                    </div>
724                ` : ''}
725                <h3>${safeName}</h3>
726
727                ${canRegister && !event.isSpecialEvent && registeredEventIds.has(String(event.id)) ? `
728                    <a href="dashboard.html" class="register-btn register-btn--done" aria-label="You're registered for ${safeNameAttr}. View it in your dashboard">
729                        ✓ You're registered
730                    </a>
731                ` : canRegister && event.isSpecialEvent ? `
732                    <a href="special-event.html?id=${safeId}" class="register-btn" style="display: inline-block; text-decoration: none; text-align: center;">
733                        ✨ Register — Special Event
734                    </a>
735                ` : canRegister ? `
736                    <button class="register-btn js-register-btn"
737                            data-event-id="${safeId}"
738                            data-event-name="${safeNameAttr}"
739                            data-event-date="${safeDateAttr}"
740                            data-event-location="${safeLocationAttr}"
741                            data-event-country="${escapeHtmlAttr(event.country || '')}">
742                        Register
743                    </button>
744                ` : `
745                    <div style="padding: 0.75rem 2rem; border-radius: 50px; background: rgba(255, 255, 255, 0.1); color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1.5rem; text-align: center;">
746                        Registration Coming Soon
747                    </div>
748                `}
749
750                <div class="event-detail">
751                    ${hasValidDate ? `
752                    <div style="display: flex; flex-direction: column; gap: 0.35rem;">
753                        <strong>📅 Date:</strong>
754                        <button type="button" class="event-calendar-btn js-calendar-btn" data-event-id="${safeId}" aria-label="Add ${safeNameAttr} to your calendar">
755                            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
756                                <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
757                                <line x1="16" y1="2" x2="16" y2="6"></line>
758                                <line x1="8" y1="2" x2="8" y2="6"></line>
759                                <line x1="3" y1="10" x2="21" y2="10"></line>
760                                <line x1="12" y1="14" x2="12" y2="18"></line>
761                                <line x1="10" y1="16" x2="14" y2="16"></line>
762                            </svg>
763                            Add to Calendar
764                        </button>
765                    </div>
766                    ` : `
767                    <strong>📅 Date:</strong>
768                    `}
769                    <span>${dateFormatted}</span>
770                </div>
771
772                <div class="event-detail">
773                    <strong>🕐 Time:</strong>
774                    <span>${event.startTime ? formatTime(event.startTime) : '6:00 PM'}</span>
775                </div>
776
777                <div class="event-detail">
778                    <div style="display: flex; flex-direction: column; gap: 0.35rem;">
779                        <strong>📍 Location:</strong>
780                        ${hasValidLocation ? `
781                            <a href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(safeLocation + ', ' + safeCountry)}"
782                               target="_blank"
783                               rel="noopener noreferrer"
784                               class="map-link"
785                               title="View on Google Maps"
786                               style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.3rem 0.6rem; background: var(--primary-color); color: white; border-radius: 4px; text-decoration: none; font-size: 0.75rem; font-weight: 600; transition: all 0.3s ease; white-space: nowrap; width: fit-content;">
787                                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
788                                    <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
789                                    <circle cx="12" cy="10" r="3"></circle>
790                                </svg>
791                                View Map
792                            </a>
793                        ` : ''}
794                    </div>
795                    <span>${safeLocation}, ${safeCountry}</span>
796                </div>
797
798                ${partnersInfo}
799
800                ${event.description ? `
801                    <div class="event-detail event-description">
802                        <p style="margin-top: 1rem; line-height: 1.6; white-space: pre-line;">${safeDescription}</p>
803                    </div>
804                ` : ''}
805
806                ${event.notes ? `
807                    <div class="event-detail">
808                        <p style="margin-top: 1rem; line-height: 1.6;">${safeNotes}</p>
809                    </div>
810                ` : ''}
811
812                ${partnerEventSection}
813
814                <div class="event-support">
815                    <p class="event-support-text">Attending? Help keep PCG free for the next person.</p>
816                    <button type="button" class="event-donate-btn js-donate-btn"
817                            data-event-id="${safeId}"
818                            data-event-name="${safeNameAttr}"
819                            data-event-country="${escapeHtmlAttr(event.country || '')}">
820                        💙 Donate ${donationLabel}
821                    </button>
822                </div>
823            </div>
824        `;
825    }).join('');
826}
827
828// Toggle partner event details
829function togglePartnerEvent(eventId) {
830    const detailsDiv = document.getElementById(`partner-${eventId}`);
831    const toggleIcon = document.getElementById(`toggle-${eventId}`);
832
833    if (detailsDiv.style.display === 'none') {
834        detailsDiv.style.display = 'block';
835        toggleIcon.textContent = '▲';
836    } else {
837        detailsDiv.style.display = 'none';
838        toggleIcon.textContent = '▼';
839    }
840}
841
842// Toggle showing all events vs first 3
843function toggleAllEvents() {
844    showingAllEvents = !showingAllEvents;
845    renderEvents();
846
847    // Scroll to events section if expanding
848    if (showingAllEvents) {
849        document.getElementById('upcoming-events').scrollIntoView({ behavior: 'smooth', block: 'start' });
850    }
851}
852
853// ==========================================
854// ADD TO CALENDAR (.ics download)
855// ==========================================
856function escapeIcsText(value) {
857    return String(value || '')
858        .replace(/\\/g, '\\\\')
859        .replace(/;/g, '\\;')
860        .replace(/,/g, '\\,')
861        .replace(/\r?\n/g, '\\n');
862}
863
864function formatIcsLocalDate(date) {
865    const pad = (n) => String(n).padStart(2, '0');
866    return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}T${pad(date.getHours())}${pad(date.getMinutes())}00`;
867}
868
869function downloadEventIcs(event) {
870    const datePart = String(event.eventDate || '').slice(0, 10);
871    const timePart = (event.startTime || '18:00').slice(0, 5);
872    const start = new Date(`${datePart}T${timePart}:00`);
873    if (isNaN(start.getTime())) {
874        showNotification('❌ This event has no confirmed date yet', 'error');
875        return;
876    }
877    const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
878
879    const location = [event.location, event.country]
880        .filter(part => part && String(part).trim() && String(part).trim().toUpperCase() !== 'TBC')
881        .join(', ');
882    const description = (event.description || 'A free Project Connect Group networking event.')
883        + '\n\nDetails & registration: https://projectconnect.group/#upcoming-events';
884
885    const ics = [
886        'BEGIN:VCALENDAR',
887        'VERSION:2.0',
888        'PRODID:-//Project Connect Group//Events//EN',
889        'BEGIN:VEVENT',
890        `UID:pcg-${escapeIcsText(event.id)}@projectconnect.group`,
891        `DTSTAMP:${formatIcsLocalDate(new Date())}`,
892        `DTSTART:${formatIcsLocalDate(start)}`,
893        `DTEND:${formatIcsLocalDate(end)}`,
894        `SUMMARY:${escapeIcsText(event.eventName || 'Project Connect Group Event')}`,
895        location ? `LOCATION:${escapeIcsText(location)}` : null,
896        `DESCRIPTION:${escapeIcsText(description)}`,
897        'END:VEVENT',
898        'END:VCALENDAR'
899    ].filter(Boolean).join('\r\n');
900
901    const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' });
902    const url = URL.createObjectURL(blob);
903    const link = document.createElement('a');
904    const slug = String(event.eventName || 'pcg-event').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'pcg-event';
905    link.href = url;
906    link.download = `${slug}.ics`;
907    document.body.appendChild(link);
908    link.click();
909    document.body.removeChild(link);
910    URL.revokeObjectURL(url);
911}
912
913// Delegated listeners so card buttons keep working across re-renders
914document.addEventListener('DOMContentLoaded', () => {
915    const eventsContainer = document.getElementById('events-container');
916    if (!eventsContainer) return;
917    eventsContainer.addEventListener('click', (e) => {
918        const calendarBtn = e.target.closest('.js-calendar-btn');
919        if (calendarBtn) {
920            const event = allUpcomingEvents.find(ev => String(ev.id) === calendarBtn.dataset.eventId);
921            if (event) downloadEventIcs(event);
922            return;
923        }
924
925        const donateBtn = e.target.closest('.js-donate-btn');
926        if (donateBtn) {
927            openSupportDonation(
928                donateBtn.dataset.eventId,
929                donateBtn.dataset.eventName,
930                donateBtn.dataset.eventCountry
931            );
932        }
933    });
934});
935
936// ==========================================
937// POPULATE EVENT DROPDOWN
938// ==========================================
939function populateEventDropdown(events) {
940    const eventSelect = document.getElementById('event-select');
941
942    if (!eventSelect) {
943        console.log('Event select dropdown not found on this page');
944        return;
945    }
946
947    if (!events || events.length === 0) {
948        eventSelect.innerHTML = '<option value="">No upcoming events</option>';
949        return;
950    }
951
952    eventSelect.innerHTML = '<option value="">Choose an event...</option>' +
953        events.map(event => {
954            const eventDate = new Date(event.eventDate);
955            const dateFormatted = eventDate.toLocaleDateString('en-GB', {
956                day: 'numeric',
957                month: 'short',
958                year: 'numeric'
959            });
960
961            const displayText = `${event.eventName} - ${event.location} (${dateFormatted})`;
962            return `<option value="${escapeHtmlAttr(event.id)}">${escapeHtml(displayText)}</option>`;
963        }).join('');
964}
965
966// ==========================================
967// SCROLL TO REGISTRATION
968// ==========================================
969function scrollToRegistration(eventId) {
970    const registrationSection = document.getElementById('contact');
971    const eventSelect = document.getElementById('event-select');
972
973    if (eventSelect) {
974        eventSelect.value = eventId;
975    }
976
977    if (registrationSection) {
978        registrationSection.scrollIntoView({ behavior: 'smooth' });
979    }
980}
981
982// ==========================================
983// FETCH GALLERY IMAGES FROM AIRTABLE
984// ==========================================
985async function fetchGalleryFromAirtable() {
986    try {
987        const response = await fetch(
988            `https://api.airtable.com/v0/${AIRTABLE_CONFIG.baseId}/${AIRTABLE_CONFIG.galleryTable}?sort[0][field]=Date&sort[0][direction]=desc`,
989            {
990                headers: {
991                    'Authorization': `Bearer ${AIRTABLE_CONFIG.apiKey}`,
992                    'Content-Type': 'application/json'
993                }
994            }
995        );
996
997        if (!response.ok) {
998            throw new Error('Failed to fetch gallery from Airtable');
999        }
1000
1001        const data = await response.json();
1002        displayGallery(data.records);
1003    } catch (error) {
1004        console.error('Error fetching gallery:', error);
1005    }
1006}
1007
1008// ==========================================
1009// DISPLAY GALLERY IMAGES
1010// ==========================================
1011function displayGallery(records) {
1012    const galleryContainer = document.getElementById('gallery-container');
1013    if (!galleryContainer || records.length === 0) return;
1014
1015    // Clear existing placeholder images
1016    galleryContainer.innerHTML = '';
1017
1018    records.forEach(record => {
1019        const { Title, Date, Image } = record.fields;
1020
1021        if (Image && Image[0]) {
1022            const galleryItem = document.createElement('div');
1023            galleryItem.className = 'gallery-item';
1024
1025            galleryItem.innerHTML = `
1026                <img src="${Image[0].url}" alt="${Title || 'Event photo'}">
1027                <div class="gallery-overlay">
1028                    <h4>${Title || 'Event'}</h4>
1029                    <p>${Date || ''}</p>
1030                </div>
1031            `;
1032
1033            galleryContainer.appendChild(galleryItem);
1034        }
1035    });
1036}
1037
1038// ==========================================
1039// SUBMIT REGISTRATION VIA NETLIFY FUNCTION
1040// ==========================================
1041async function submitRegistration(formData) {
1042    try {
1043        const response = await fetch('/.netlify/functions/submit-registration', {
1044            method: 'POST',
1045            headers: {
1046                'Content-Type': 'application/json'
1047            },
1048            body: JSON.stringify({
1049                attendeeName: formData.Name,
1050                email: formData.Email,
1051                phone: formData.Phone,
1052                companyOrganisation: formData.Company,
1053                jobTitle: formData['Job Title'] || '',
1054                notes: formData.Notes || '',
1055                eventId: formData.Event
1056            })
1057        });
1058
1059        const data = await response.json();
1060
1061        if (!response.ok) {
1062            throw new Error(data.error || 'Failed to submit registration');
1063        }
1064
1065        return data;
1066    } catch (error) {
1067        console.error('Error submitting registration:', error);
1068        throw error;
1069    }
1070}
1071
1072// ==========================================
1073// HANDLE REGISTRATION FORM SUBMISSION
1074// ==========================================
1075function initializeRegistrationForm() {
1076    const registrationForm = document.getElementById('registration-form');
1077
1078    if (registrationForm) {
1079        registrationForm.addEventListener('submit', async (e) => {
1080            e.preventDefault();
1081
1082            const submitButton = registrationForm.querySelector('button[type="submit"]');
1083            const originalButtonText = submitButton.textContent;
1084            submitButton.disabled = true;
1085            submitButton.textContent = 'Submitting...';
1086
1087            const formData = {
1088                attendeeName: document.getElementById('attendee-name').value,
1089                email: document.getElementById('email').value,
1090                phone: document.getElementById('phone').value || '',
1091                companyOrganisation: document.getElementById('company').value || '',
1092                jobTitle: document.getElementById('job-title').value || '',
1093                notes: document.getElementById('notes').value || '',
1094                eventId: document.getElementById('event-select').value,
1095            };
1096
1097            try {
1098                const response = await fetch('/.netlify/functions/submit-registration', {
1099                    method: 'POST',
1100                    headers: { 'Content-Type': 'application/json' },
1101                    body: JSON.stringify(formData),
1102                });
1103
1104                const data = await response.json();
1105
1106                if (response.status === 200) {
1107                    showNotification('✅ Registration successful! We look forward to seeing you at the event.', 'success');
1108                    registrationForm.reset();
1109                } else if (response.status === 409) {
1110                    showNotification('ℹ️ You are already registered for this event.', 'error');
1111                } else {
1112                    showNotification('❌ Registration failed: ' + (data.error || 'Unknown error'), 'error');
1113                }
1114            } catch (error) {
1115                console.error('Error:', error);
1116                showNotification('❌ An error occurred. Please try again or c
1116ontact us directly.', 'error');
1117            } finally {
1118                submitButton.disabled = false;
1119                submitButton.textContent = originalButtonText;
1120            }
1121        });
1122    }
1123}
1124
1125// ==========================================
1126// SHOW NOTIFICATION
1127// ==========================================
1128function showNotification(message, type = 'success') {
1129    const notification = document.createElement('div');
1130    notification.textContent = message;
1131    notification.style.cssText = `
1132        position: fixed;
1133        bottom: 20px;
1134        left: 50%;
1135        transform: translateX(-50%);
1136        background: ${type === 'success' ? 'var(--primary-color)' : '#f44336'};
1137        color: white;
1138        padding: 1rem 2rem;
1139        border-radius: 50px;
1140        font-weight: 600;
1141        z-index: 10000;
1142        animation: fadeInUp 0.3s ease;
1143        max-width: 90%;
1144        text-align: center;
1145    `;
1146    document.body.appendChild(notification);
1147
1148    setTimeout(() => {
1149        notification.style.animation = 'fadeInUp 0.3s ease reverse';
1150        setTimeout(() => notification.remove(), 300);
1151    }, 4000);
1152}
1153
1154// ==========================================
1155// INITIALIZE FEATURES
1156// ==========================================
1157document.addEventListener('DOMContentLoaded', () => {
1158    // Fetch and display events from Netlify function
1159    fetchAndDisplayEvents();
1160
1161    // Delegated click handler for Register buttons. Reading event data from
1162    // data-* attributes avoids breaking the handler when fields contain
1163    // characters like apostrophes (e.g. "O'Connell Street").
1164    const eventsContainer = document.getElementById('events-container');
1165    if (eventsContainer) {
1166        eventsContainer.addEventListener('click', async (e) => {
1167            const btn = e.target.closest('.js-register-btn');
1168            if (!btn || btn.disabled) return;
1169
1170            // Checking the session takes a moment on mobile data, so show
1171            // that the tap registered instead of appearing to do nothing.
1172            const label = btn.textContent;
1173            btn.disabled = true;
1174            btn.setAttribute('aria-busy', 'true');
1175            btn.textContent = 'Opening…';
1176            try {
1177                await openRegistrationModal(
1178                    btn.dataset.eventId,
1179                    btn.dataset.eventName,
1180                    btn.dataset.eventDate,
1181                    btn.dataset.eventLocation,
1182                    btn.dataset.eventCountry || ''
1183                );
1184            } finally {
1185                btn.disabled = false;
1186                btn.removeAttribute('aria-busy');
1187                btn.textContent = label;
1188            }
1189        });
1190    }
1191
1192    // Initialize registration form
1193    initializeRegistrationForm();
1194
1195    const params = new URLSearchParams(window.location.search);
1196    const isOn = (value) => value === '1' || value === 'true';
1197
1198    // If we just came back from an OAuth/email-confirm flow with a pending
1199    // event registration, reopen that modal automatically.
1200    if (params.get('resume_registration') === '1') {
1201        // Strip the param so a refresh doesn't keep retriggering.
1202        const cleanUrl = window.location.pathname + window.location.hash;
1203        window.history.replaceState({}, document.title, cleanUrl);
1204        // Wait for auth state to settle, then resume.
1205        setTimeout(() => resumePendingRegistration(), 500);
1206    } else if (isOn(params.get('login')) || isOn(params.get('signup'))) {
1207        // Deep links such as /?login=1 (used by other pages and emails)
1208        const wantsSignup = isOn(params.get('signup'));
1209        const cleanUrl = window.location.pathname + window.location.hash;
1210        window.history.replaceState({}, document.title, cleanUrl);
1211        getCurrentSession().then(({ session, member }) => {
1212            if (session && member) return; // Already signed in, nothing to do
1213            if (wantsSignup) openSignupModal(); else openLoginModal();
1214        });
1215    }
1216
1217    // Note: Gallery still uses placeholder images
1218    // To load from Airtable, create a separate Netlify function
1219});
1220
1221// ==========================================
1222// MODAL REGISTRATION FUNCTIONS
1223// ==========================================
1224
1225// The event shown in the registration modal, kept so it can be restored if
1226// the member has to log in again part-way through.
1227let currentRegistrationEvent = null;
1228
1229async function openRegistrationModal(eventId, eventName, eventDate, eventLocation, eventCountry) {
1230    // Check if user is logged in FIRST
1231    let { session, member } = await getCurrentSession();
1232
1233    // Signed in but no profile row yet (e.g. straight back from an email
1234    // confirmation link): build it from the sign-up details rather than
1235    // treating the member as logged out.
1236    if (session && !member) {
1237        member = await ensureMemberProfile(session);
1238    }
1239
1240    if (!session || !member) {
1241        // Stash the event the user was trying to register for. After they
1242        // log in or create an account we'll auto-reopen this modal — see
1243        // resumePendingRegistration() below.
1244        setPendingRegistration({
1245            eventId: eventId,
1246            eventName: eventName,
1247            eventDate: eventDate,
1248            eventLocation: eventLocation,
1249            eventCountry: eventCountry || '',
1250        });
1251
1252        // Most people tapping Register are new, so lead with sign-up unless
1253        // this browser has signed in before.
1254        if (isReturningMember()) {
1255            openLoginModal();
1256        } else {
1257            openSignupModal();
1258        }
1259        return;
1260    }
1261
1262    markReturningMember();
1263    currentRegistrationEvent = { eventId, eventName, eventDate, eventLocation, eventCountry: eventCountry || '' };
1264
1265    const modal = document.getElementById('registration-modal');
1266    const form = document.getElementById('modal-registration-form');
1267    if (form) {
1268        form.reset();
1269        clearAllFieldErrors(form);
1270    }
1271    setFormStatus('reg-status');
1272
1273    // Populate event details, including the start time when we know it
1274    const eventRecord = allUpcomingEvents.find(ev => String(ev.id) === String(eventId));
1275    const timeText = eventRecord && eventRecord.startTime ? ` · ${formatTime(eventRecord.startTime)}` : '';
1276    document.getElementById('modal-event-name').textContent = eventName;
1277    document.getElementById('modal-event-date').textContent = `📅 ${eventDate}${timeText}`;
1278    document.getElementById('modal-event-location').textContent = `📍 ${eventLocation}`;
1279
1280    // Set Google Maps link
1281    const mapLink = document.getElementById('modal-map-link');
1282    if (mapLink) {
1283        mapLink.href = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(eventLocation)}`;
1284    }
1285
1286    // Set hidden event ID field
1287    document.getElementById('modal-event-id').value = eventId;
1288    const countryEl = document.getElementById('modal-event-country');
1289    if (countryEl) countryEl.value = eventCountry || '';
1290
1291    // Auto-fill user details from member profile
1292    const nameField = document.getElementById('modal-attendee-name');
1293    const emailField = document.getElementById('modal-email');
1294    const phoneField = document.getElementById('modal-phone');
1295    const companyField = document.getElementById('modal-company');
1296    const jobTitleField = document.getElementById('modal-job-title');
1297
1298    const memberEmail = member.email || (session.user && session.user.email) || '';
1299    if (nameField) nameField.value = member.member_name || '';
1300    if (emailField) emailField.value = memberEmail;
1301    if (phoneField) phoneField.value = member.phone || '';
1302    if (companyField) companyField.value = member.company_organisation || '';
1303    if (jobTitleField) jobTitleField.value = member.job_title || '';
1304
1305    // Members with a complete profile get a one-tap confirmation; anyone
1306    // missing a required detail sees the form fields instead.
1307    const profileComplete = [member.member_name, memberEmail, member.company_organisation, member.job_title]
1308        .every(value => value && String(value).trim() !== '');
1309
1310    document.getElementById('reg-summary-name').textContent = member.member_name || memberEmail;
1311    document.getElementById('reg-summary-email').textContent = memberEmail;
1312    const roleEl = document.getElementById('reg-summary-role');
1313    const roleLine = [member.job_title, member.company_organisation].filter(
1313Boolean).join(' · ');
1314    roleEl.textContent = roleLine;
1315    roleEl.hidden = !roleLine;
1316
1317    // Google/LinkedIn sign-ups have no company or job title yet: ask only
1318    // for what's missing rather than the whole form.
1319    setRegistrationMode(profileComplete ? 'summary' : 'partial');
1320
1321    // Show modal
1322    modal.classList.add('active');
1323
1324    // Prevent body scroll when modal is open
1325    document.body.style.overflow = 'hidden';
1326}
1327
1328// 'summary': the "Registering as" card only. 'partial': the card plus just the
1329// details still missing. 'fields': the full editable form.
1330function setRegistrationMode(mode) {
1331    const summary = document.getElementById('reg-summary');
1332    const fields = document.getElementById('reg-fields');
1333    if (summary) summary.hidden = mode === 'fields';
1334    if (!fields) return;
1335    fields.hidden = mode === 'summary';
1336    fields.querySelectorAll('.form-group').forEach(group => {
1337        const input = group.querySelector('input');
1338        group.hidden = mode === 'partial' && !!input && (input.type === 'tel' || input.value.trim() !== '');
1339    });
1340}
1341
1342function showRegistrationFields() {
1343    setRegistrationMode('fields');
1344    const nameField = document.getElementById('modal-attendee-name');
1345    if (nameField) nameField.focus();
1346}
1347
1348function closeRegistrationModal() {
1349    const modal = document.getElementById('registration-modal');
1350    const form = document.getElementById('modal-registration-form');
1351
1352    // Hide modal
1353    modal.classList.remove('active');
1354
1355    // Re-enable body scroll
1356    document.body.style.overflow = '';
1357
1358    // Reset form
1359    form.reset();
1360    clearAllFieldErrors(form);
1361    setFormStatus('reg-status');
1362}
1363
1364// Close modal when clicking overlay
1365document.addEventListener('DOMContentLoaded', () => {
1366    const modal = document.getElementById('registration-modal');
1367    const overlay = modal?.querySelector('.modal-overlay');
1368
1369    if (overlay) {
1370        overlay.addEventListener('click', closeRegistrationModal);
1371    }
1372
1373    // Global ESC key handler - closes any open modal
1374    document.addEventListener('keydown', (e) => {
1375        if (e.key === 'Escape') {
1376            // Find any active modal
1377            const activeModal = document.querySelector('.modal.active');
1378            if (activeModal) {
1379                // Find the close button and click it to trigger the proper close function
1380                const closeButton = activeModal.querySelector('.modal-close');
1381                if (closeButton) {
1382                    closeButton.click();
1383                }
1384            }
1385        }
1386    });
1387
1388    // Handle modal form submission
1389    initializeModalRegistrationForm();
1390});
1391
1392// ==========================================
1393// HANDLE MODAL REGISTRATION FORM SUBMISSION
1394// ==========================================
1395function initializeModalRegistrationForm() {
1396    const modalForm = document.getElementById('modal-registration-form');
1397
1398    if (!modalForm) return;
1399
1400    modalForm.addEventListener('submit', async (e) => {
1401        e.preventDefault();
1402
1403        clearAllFieldErrors(modalForm);
1404        setFormStatus('reg-status');
1405
1406        const nameInput = document.getElementById('modal-attendee-name');
1407        const emailInput = document.getElementById('modal-email');
1408        const companyInput = document.getElementById('modal-company');
1409        const jobTitleInput = document.getElementById('modal-job-title');
1410        const termsCheckbox = document.getElementById('modal-terms');
1411
1412        // Validate required details; reveal the fields if one is missing
1413        const problems = [];
1414        if (!nameInput.value.trim()) problems.push([nameInput, 'Please enter your name']);
1415        if (!isValidEmail(emailInput.value.trim())) problems.push([emailInput, 'Please enter a valid email address']);
1416        if (!companyInput.value.trim()) problems.push([companyInput, 'Please enter your company or organisation']);
1417        if (!jobTitleInput.value.trim()) problems.push([jobTitleInput, 'Please enter your job title']);
1418
1419        if (problems.length > 0) {
1420            setRegistrationMode('fields');
1421            problems.forEach(([input, message]) => showFieldError(input, message));
1422            problems[0][0].focus();
1423            return;
1424        }
1425
1426        if (termsCheckbox && !termsCheckbox.checked) {
1427            showFieldError(termsCheckbox, 'Please tick the box to accept the Terms & Conditions and Privacy Policy');
1428            termsCheckbox.focus();
1429            return;
1430        }
1431
1432        const submitButton = modalForm.querySelector('button[type="submit"]');
1433        setButtonLoading(submitButton, true);
1434
1435        const formData = {
1436            attendeeName: nameInput.value.trim(),
1437            email: emailInput.value.trim(),
1438            phone: document.getElementById('modal-phone').value.trim() || '',
1439            companyOrganisation: companyInput.value.trim(),
1440            jobTitle: jobTitleInput.value.trim(),
1441            notes: document.getElementById('modal-notes')?.value || '',
1442            eventId: document.getElementById('modal-event-id').value,
1443            registrationDate: new Date().toISOString(),
1444        };
1445
1446        const eventName = document.getElementById('modal-event-name').textContent;
1447        const eventCountry = document.getElementById('modal-event-country')?.value || '';
1448
1449        // Session expired while the modal was open: keep their place and
1450        // ask them to log in again rather than failing.
1451        const askToLogInAgain = () => {
1452            if (currentRegistrationEvent) setPendingRegistration(currentRegistrationEvent);
1453            closeRegistrationModal();
1454            openLoginModal();
1455            setFormStatus('login-status', 'info', 'Your session has expired. Log in again and we\'ll take you straight back to your registration.');
1456        };
1457
1458        try {
1459            // Get Supabase session for authentication
1460            const { session } = await getCurrentSession();
1461            if (!session) {
1462                askToLogInAgain();
1463                return;
1464            }
1465
1466            const response = await fetch('/.netlify/functions/submit-registration', {
1467                method: 'POST',
1468                headers: {
1469                    'Content-Type': 'application/json',
1470                    'Authorization': `Bearer ${session.access_token}`
1471                },
1472                body: JSON.stringify(formData),
1473            });
1474
1475            const data = await response.json().catch(() => ({}));
1476
1477            if (response.status === 200) {
1478                markEventRegistered(formData.eventId);
1479
1480                // Close registration modal
1481                closeRegistrationModal();
1482
1483                // Show donation modal (doubles as the "you're in" confirmation)
1484                showDonationModal(data.registrationId, eventName, eventCountry);
1485            } else if (response.status === 409) {
1486                // Already booked is good news, not an error
1487                markEventRegistered(formData.eventId);
1488                closeRegistrationModal();
1489                showNotification('✅ You\'re already registered for this event. It\'s in your dashboard.', 'success');
1490            } else if (response.status === 401) {
1491                askToLogInAgain();
1492            } else {
1493                const reason = response.status >= 500
1494                    ? 'something went wrong on our side'
1495                    : (data.message || data.error || 'something went wrong');
1496                setFormStatus('reg-status', 'error', `We couldn't complete your registration (${reason}). Please try again, or email [email protected] and we'll add you.`);
1497                console.error('Registration error details:', data);
1498            }
1499        } catch (error) {
1500            console.error('Error:', error);
1501            setFormStatus('reg-status', 'error', 'We couldn\'t reach the server. Check your connection and try again.');
1502        } finally {
1503            setButtonLoading(submitButton, false);
1504        }
1505    });
1506}
1507
1508// ==========================================
1509// DONATION MODAL FUNCTIONS
1510// ==========================================
1511
1512// Tracks how the donation modal was opened, so closing it doesn't announce a
1513// registration that never happened.
1514let donationModalMode = 'post-registration';
1515
1516function showDonationModal(registrationId, eventName, eventCountry, options = {}) {
1517    const mode = options.mode || 'post-registration';
1518    const eventId = options.eventId || '';
1519    donationModalMode = mode;
1520
1521    const modal = document.getElementById('donation-modal');
1522    // The subtitle is rewritten below, which can remove this span, so it may
1523    // legitimately be absent when reopening the modal in a different mode.
1524    const nameSpan = document.getElementById('donation-event-name');
1525    if (nameSpan) nameSpan.textContent = eventName || '';
1526    document.getElementById('donation-event-name-hidden').value = eventName || '';
1527    document.getElementById('donation-registration-id').value = registrationId || '';
1528
1529    const eventIdInput = document.getElementById('donation-event-id');
1530    if (eventIdInput) eventIdInput.value = eventId;
1531
1532    const countryInput = document.getElementById('donation-country-hidden');
1533    if (countryInput) countryInput.value = eventCountry || '';
1534
1535    // The modal doubles as a post-registration prompt and a standalone
1536    // "support us" prompt, so swap the header copy to match.
1537    const steps = document.getElementById('donation-steps');
1538    const title = document.getElementById('donation-title');
1539    const subtitle = document.getElementById('donation-subtitle');
1540    const isSupport = mode === 'support';
1541
1542    // Set 'flex' explicitly: clearing the property would drop the inline
1543    // display:flex and stack the steps vertically.
1544    if (steps) steps.style.display = isSupport ? 'none' : 'flex';
1545    if (title) {
1546        title.textContent = isSupport
1547            ? 'Support Project Connect Group'
1548            : "You're in — and it's FREE";
1549    }
1550    if (subtitle) {
1551        if (isSupport && eventName) {
1552            subtitle.innerHTML = 'Chipping in towards <span style="font-weight: 700;"></span> 💙';
1553            subtitle.querySelector('span').textContent = eventName;
1554        } else if (isSupport) {
1555            subtitle.textContent = 'Help us keep every event free 💙';
1556        } else {
1557            subtitle.innerHTML = 'See you at <span id="donation-event-name" style="font-weight: 700;"></span> 🎉';
1558            subtitle.querySelector('#donation-event-name').textContent = eventName || '';
1559        }
1560    }
1561
1562    // Update displayed currency to match the event's country (e.g. £ for UK,
1563    // A$ for Australia). Falls back to GBP — see js/currency.js.
1564    if (window.PCGCurrency) {
1565        const currency = window.PCGCurrency.getCurrencyForCountry(eventCountry);
1566        document.querySelectorAll('#donation-modal [data-currency-amount]').forEach(node => {
1567            node.textContent = currency.label;
1568        });
1569    }
1570
1571    setDonationStatus('', '');
1572    modal.classList.add('active');
1573    document.body.style.overflow = 'hidden';
1574}
1575
1576// Open the donation prompt on its own - used by the "Donate" button on event
1577// cards and the support band, so members who registered days ago (or guests who
1578// never registered) can still chip in.
1579function openSupportDonation(eventId, eventName, eventCountry) {
1580    showDonationModal('', eventName || '', eventCountry || '', { mode: 'support', eventId: eventId || '' });
1581}
1582
1583function closeDonationModal() {
1584    const modal = document.getElementById('donation-modal');
1585    modal.classList.remove('active');
1586    document.body.style.overflow = '';
1587    setDonationStatus('', '');
1588
1589    // Only confirm a registration when the modal actually followed one.
1590    const isHomepage = window.location.pathname.includes('index.html') || window.location.pathname === '/';
1591    if (donationModalMode === 'post-registration' && isHomepage) {
1592        showNotification('✅ Registration successful! We look forward to seeing you at the event.', 'success');
1593    }
1594}
1595
1596// Persistent status message inside the donation modal, so failures can't be
1597// missed the way a transient toast can (type: info | success | error)
1598function setDonationStatus(type, message) {
1599    const el = document.getElementById('donation-status');
1600    if (!el) return;
1601    if (!message) {
1602        el.className = 'upload-status';
1603        el.textContent = '';
1604        el.style.display = 'none';
1605        return;
1606    }
1607    el.className = 'upload-status upload-status--' + type;
1608    el.textContent = message;
1609    el.style.display = 'block';
1610}
1611
1612async function processDonation() {
1613    const registrationId = document.getElementById('donation-registration-id').value;
1614    const eventName = document.getElementById('donation-event-name-hidden').value;
1615    const eventCountry = document.getElementById('donation-country-hidden')?.value || '';
1616    const donateBtn = document.getElementById('donate-btn');
1617
1618    // Disable button and show loading state
1619    donateBtn.disabled = true;
1620    const originalText = donateBtn.textContent;
1621    donateBtn.textContent = 'Processing...';
1622
1623    const failDonation = (message) => {
1624        setDonationStatus('error', message);
1625        showNotification('❌ ' + message, 'error');
1626        donateBtn.disabled = false;
1627        donateBtn.textContent = originalText;
1628    };
1629
1630    try {
1631        setDonationStatus('info', 'Preparing your secure checkout…');
1632
1633        // A login isn't required to donate. When the donor IS signed in we send
1634        // their token so the donation links to their registration and earns the
1635        // donor badge; otherwise Stripe Checkout collects their email.
1636        const eventId = document.getElementById('donation-event-id')?.value || '';
1637        let session = null;
1638        try {
1639            ({ session } = await getCurrentSession());
1640        } catch (sessionError) {
1641            console.warn('No active session for donation, continuing as guest:', sessionError);
1642        }
1643
1644        const response = await fetch('/.netlify/functions/create-donation-checkout', {
1645            method: 'POST',
1646            headers: {
1647                'Content-Type': 'application/json',
1648                ...(session ? { 'Authorization': `Bearer ${session.access_token}` } : {})
1649            },
1650            body: JSON.stringify({
1651                registrationId: registrationId,
1652                eventId: eventId,
1653                eventCountry: eventCountry,
1654            })
1655        });
1656
1657        const data = await response.json().catch(() => ({}));
1658
1659        if (response.ok && data.url) {
1660            setDonationStatus('success', 'Taking you to the secure Stripe checkout…');
1661            donateBtn.textContent = 'Redirecting...';
1662            // Redirect to Stripe Checkout
1663            window.location.href = data.url;
1664        } else if (response.status === 401) {
1665            failDonation('Your session has expired - please log in again, then donate from your dashboard.');
1666        } else if (response.status === 403 || response.status === 404) {
1667            failDonation('We could not match this donation to your event registration. Please email [email protected] and we will sort it out.');
1668        } else if (response.status === 429) {
1669            failDonation('Too many attempts - please wait a minute and try again.');
1670        } else {
1671            console.error('Donation checkout error response:', response.status, data);
1672            failDonation('Donations are temporarily unavailable - please try again later or email [email protected].');
1673        }
1674    } catch (error) {
1675        console.error('Error creating donation checkout:', error);
1676        failDonation(error instanceof TypeError
1677            ? 'Could not reach the server - please check your connection and try again.'
1678            : 'Unable to process the donation right now - please try again later.');
1679    }
1680}
1681
1682function skipDonation() {
1683    closeDonationModal();
1684}
1685
1686// ==========================================
1687// MEMBER AUTHENTICATION
1688// ==========================================
1689
1690// Session management
1691function getSessionToken() {
1692    return localStorage.getItem('pcg_session_token');
1693}
1694
1695function setSessionToken(token) {
1696    localStorage.setItem('pcg_session_token', token);
1697}
1698
1699function clearSession() {
1700    localStorage.removeItem('pcg_session_token');
1701    localStorage.removeItem('pcg_member_data');
1702}
1703
1704function getMemberData() {
1705    const data = localStorage.getItem('pcg_member_data');
1706    return data ? JSON.parse(data) : null;
1707}
1708
1709function setMemberData(memberData) {
1710    localStorage.setItem('pcg_member_data', JSON.stringify(memberData));
1711}
1712
1713// Check authentication state on page load
1714async function checkAuthState() {
1715    const { session, member } = await getCurrentSession();
1716
1717    const authNavItem = document.getElementById('auth-nav-item');
1718    const joinUsNavItem = document.getElementById('join-us-nav-item');
1719    const memberNavItem = document.getElementById('member-nav-item');
1720    const hostNavItem = document.getElementById('host-nav-item');
1721    const adminNavItem = document.getElementById('admin-nav-item');
1722    const mobileAuthBtn = document.getElementById('mobile-auth-btn');
1723
1724    if (session && member) {
1725        // User is logged in
1726        markReturningMember();
1727
1728        if (authNavItem) authNavItem.style.display = 'none';
1729        if (joinUsNavItem) joinUsNavItem.style.display = 'none';
1730
1731        if (memberNavItem) {
1732            memberNavItem.style.display = 'block';
1733
1734            // Everyone goes to member dashboard first
1735            // Admin/Host buttons are shown within the dashboard
1736            const dashboardLink = memberNavItem.querySelector('a');
1737            if (dashboardLink) {
1738                dashboardLink.href = 'dashboard.html';
1739                dashboardLink.textContent = 'My Dashboard';
1740            }
1741        }
1742
1743        if (mobileAuthBtn) {
1744            mobileAuthBtn.href = 'dashboard.html';
1745            mobileAuthBtn.textContent = 'My Dashboard';
1746            mobileAuthBtn.dataset.mode = 'dashboard';
1747        }
1748
1749        // Show host/admin links based on role
1750        const role = member.member_role || 'member';
1751        if (role === 'admin' && adminNavItem) {
1752            adminNavItem.style.display = 'block';
1753            // Admins don't need host link since admin console has all host features
1754            if (hostNavItem) hostNavItem.style.display = 'none';
1755        } else if (role === 'host' && hostNavItem) {
1756            hostNavItem.style.display = 'block';
1757            if (adminNavItem) adminNavItem.style.display = 'none';
1758        } else {
1759            // Regular members see neither
1760            if (hostNavItem) hostNavItem.style.display = 'none';
1761            if (adminNavItem) adminNavItem.style.display = 'none';
1762        }
1763
1764        loadMemberRegistrations(session);
1765    } else {
1766        // User is not logged in
1767        if (authNavItem) authNavItem.style.display = 'block';
1768        if (joinUsNavItem) joinUsNavItem.style.display = 'block';
1769        if (memberNavItem) memberNavItem.style.display = 'none';
1770        if (hostNavItem) hostNavItem.style.display = 'none';
1771        if (adminNavItem) adminNavItem.style.display = 'none';
1772
1773        if (mobileAuthBtn) {
1774            mobileAuthBtn.href = '/?login=1';
1775            mobileAuthBtn.textContent = 'Log in';
1776            mobileAuthBtn.dataset.mode = 'login';
1777        }
1778
1779        if (registeredEventIds.size > 0) {
1780            registeredEventIds.clear();
1781            if (allUpcomingEvents.length) renderEvents();
1782        }
1783    }
1784}
1785
1786// Call on page load
1787document.addEventListener('DOMContentLoaded', checkAuthState);
1788
1789// Mobile header shortcut: open the login modal in place when there is one
1790document.addEventListener('DOMContentLoaded', () => {
1791    const mobileAuthBtn = document.getElementById('mobile-auth-btn');
1792    if (!mobileAuthBtn) return;
1793    mobileAuthBtn.addEventListener('click', (e) => {
1794        if (mobileAuthBtn.dataset.mode === 'login' && document.getElementById('login-modal')) {
1795            e.preventDefault();
1796            openLoginModal();
1797        }
1798    });
1799});
1800
1801/**
1802 * Fetch the signed-in member's upcoming registrations so event cards can say
1803 * "You're registered" instead of offering a duplicate booking.
1804 */
1805let memberRegistrationsLoaded = false;
1806async function loadMemberRegistrations(session) {
1807    if (memberRegistrationsLoaded || !session || !document.getElementById('events-container')) return;
1808    memberRegistrationsLoaded = true;
1809    try {
1810        const response = await fetch('/.netlify/functions/get-member-registrations', {
1811            method: 'GET',
1812            headers: { 'Authorization': `Bearer ${session.access_token}` },
1813        });
1814        if (!response.ok) return;
1815        const data = await response.json();
1816        (data.upcomingEvents || []).forEach(reg => {
1817            if (reg && reg.eventId) registeredEventIds.add(String(reg.eventId));
1818        });
1819        if (registeredEventIds.size > 0 && allUpcomingEvents.length) renderEvents();
1820    } catch (error) {
1821        // Non-essential: cards simply keep their Register buttons
1822        console.warn('Could not load member registrations:', error);
1823    }
1824}
1825
1826function markEventRegistered(eventId) {
1827    if (!eventId) return;
1828    registeredEventIds.add(String(eventId));
1829    if (allUpcomingEvents.length) renderEvents();
1830}
1831
1832// ==========================================
1833// SCROLL TO TOP BUTTON
1834// ==========================================
1835
1836const scrollToTopBtn = document.getElementById('scroll-to-top');
1837
1838// Show/hide button based on scroll position
1839window.addEventListener('scroll', () => {
1840    if (scrollToTopBtn) {
1841        if (window.pageYOffset > 300) {
1842            scrollToTopBtn.classList.add('visible');
1843        } else {
1844            scrollToTopBtn.classList.remove('visible');
1845        }
1846    }
1847});
1848
1849// Scroll to top when button is clicked
1850if (scrollToTopBtn) {
1851    scrollToTopBtn.addEventListener('click', () => {
1852        window.scrollTo({
1853            top: 0,
1854            behavior: 'smooth'
1855        });
1856    });
1857}
1858
1859// ==========================================
1860// PENDING REGISTRATION & RETURNING MEMBERS
1861// ==========================================
1862const PENDING_REGISTRATION_KEY = 'pcg_pending_registration';
1863const PENDING_REGISTRATION_TTL_MS = 24 * 60 * 60 * 1000; // allows time to confirm an email
1864const RETURNING_MEMBER_KEY = 'pcg_returning_member';
1865
1866function setPendingRegistration(pending) {
1867    try {
1868        localStorage.setItem(PENDING_REGISTRATION_KEY, JSON.stringify({ ...pending, savedAt: Date.now() }));
1869    } catch (_) {
1870        // localStorage may be unavailable (private mode) — proceed anyway.
1871    }
1872}
1873
1874function getPendingRegistration() {
1875    try {
1876        const raw = localStorage.getItem(PENDING_REGISTRATION_KEY);
1877        if (!raw) return null;
1878        const pending = JSON.parse(raw);
1879        if (!pending || !pending.eventId) return null;
1880        if (pending.savedAt && Date.now() - pending.savedAt > PENDING_REGISTRATION_TTL_MS) {
1881            localStorage.removeItem(PENDING_REGISTRATION_KEY);
1882            return null;
1883        }
1884        return pending;
1885    } catch (_) {
1886        return null;
1887    }
1888}
1889
1890function clearPendingRegistration() {
1891    try {
1892        localStorage.removeItem(PENDING_REGISTRATION_KEY);
1893    } catch (_) { /* ignore */ }
1894}
1895
1896// Remember that this browser has signed in before, so the next Register tap
1897// opens "Log in" rather than "Create account".
1898function markReturningMember() {
1899    try {
1900        localStorage.setItem(RETURNING_MEMBER_KEY, '1');
1901    } catch (_) { /* ignore */ }
1902}
1903
1904function isReturningMember() {
1905    try {
1906        return localStorage.getItem(RETURNING_MEMBER_KEY) === '1';
1907    } catch (_) {
1908        return false;
1909    }
1910}
1911
1912/**
1913 * If the user clicked "Register" on an event before authenticating, re-open
1914 * the registration modal automatically once they're signed in. Returns true
1915 * if a pending registration was resumed, false otherwise.
1916 */
1917function resumePendingRegistration(statusMessage) {
1918    const pending = getPendingRegistration();
1919    if (!pending) return false;
1920    clearPendingRegistration();
1921
1922    // Give the auth state a tick to settle, then open the modal.
1923    setTimeout(() => {
1924        openRegistrationModal(
1925            pending.eventId,
1926            pending.eventName,
1927            pending.eventDate,
1928            pending.eventLocation,
1929            pending.eventCountry
1930        ).then(() => {
1931            // Confirm inside the modal: a toast here would cover the button
1932            const modal = document.getElementById('registration-modal');
1933            const status = document.getElementById('reg-status');
1934            if (statusMessage && modal && modal.classList.contains('active') && status && status.hidden) {
1935                setFormStatus('reg-status', 'success', statusMessage);
1936            }
1937        }).catch(err => console.error('Failed to resume pending registration:', err));
1938    }, 200);
1939
1940    return true;
1941}
1942
1943// ==========================================
1944// FORM HELPERS (auth + registration modals)
1945// ==========================================
1946function isValidEmail(email) {
1947    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || '').trim());
1948}
1949
1950/**
1951 * Show (or clear, when called with no message) an inline status message.
1952 * Text is always set with textContent. An optional action renders as a
1953 * button after the message, e.g. "Resend email".
1954 */
1955function setFormStatus(elementId, type, message, action) {
1956    const el = document.getElementById(elementId);
1957    if (!el) return;
1958    el.replaceChildren();
1959    if (!message) {
1960        el.hidden = true;
1961        el.className = 'form-status';
1962        return;
1963    }
1964    el.className = `form-status form-status--${type || 'info'}`;
1965    const text = document.createElement('span');
1966    text.textContent = message;
1967    el.appendChild(text);
1968    if (action && action.label && typeof action.onClick === 'function') {
1969        const btn = document.createElement('button');
1970        btn.type = 'button';
1971        btn.className = 'link-btn';
1972        btn.textContent = action.label;
1973        btn.addEventListener('click', action.onClick);
1974        el.appendChild(document.createTextNode(' '));
1975        el.appendChild(btn);
1976    }
1977    el.hidden = false;
1978}
1979
1980// Show which event the member is signing in for, so the detour to create an
1981// account doesn't feel like losing their place.
1982function renderAuthEventContext() {
1983    const pending = getPendingRegistration();
1984    document.querySelectorAll('.auth-event-context').forEach(el => {
1985        el.replaceChildren();
1986        if (!pending || !pending.eventName) {
1987            el.hidden = true;
1988            return;
1989        }
1990        const label = document.createElement('span');
1991        label.className = 'auth-event-context__label';
1992        label.textContent = 'Almost there! Your place at';
1993        const name = document.createElement('strong');
1994        name.className = 'auth-event-context__name';
1995        name.textContent = pending.eventName;
1996        // Keep it to one short line: date, time and city rather than the
1997        // full venue address
1998        const record = allUpcomingEvents.find(ev => String(ev.id) === String(pending.eventId));
1999        const time = record && record.startTime ? formatTime(record.startTime) : '';
2000        const place = (record && getEventCity(record)) || pending.eventLocation;
2001        const meta = document.createElement('span');
2002        meta.className = 'auth-event-context__meta';
2003        meta.textContent = [pending.eventDate, time, place].filter(Boolean).join(' · ');
2004        el.append(label, name, meta);
2005        el.hidden = false;
2006    });
2007
2008    const signupSubmit = document.getElementById('signup-submit');
2009    if (signupSubmit && !signupSubmit.classList.contains('loading')) {
2010        signupSubmit.textContent = pending ? 'Create account & continue' : 'Create account';
2011    }
2012    const signupTitle = document.getElementById('signup-title');
2013    if (signupTitle) {
2014        signupTitle.textContent = pending ? 'Create a free account to book' : 'Create your free account';
2015    }
2016    const loginTitle = document.getElementById('login-title');
2017    if (loginTitle) {
2018        loginTitle.textContent = pending ? 'Log in to book your place' : 'Welcome back';
2019    }
2020}
2021
2022// Focus the first field on desktop only: on phones it would pop the keyboard
2023// over the event summary before the member has read it.
2024function focusFirstField(modal) {
2025    if (!window.matchMedia || !window.matchMedia('(pointer: fine)').matches) return;
2026    const field = modal.querySelector('input:not([type="hidden"]):not([type="checkbox"])');
2027    if (field) setTimeout(() => field.focus(), 50);
2028}
2029
2030// ==========================================
2031// LOGIN / SIGNUP MODALS
2032// ==========================================
2033function openLoginModal() {
2034    const modal = document.getElementById('login-modal');
2035    if (!modal) {
2036        // Pages without the modal (e.g. thank-you.html) send people home to log in
2037        window.location.href = '/?login=1';
2038        return;
2039    }
2040    renderAuthEventContext();
2041    modal.classList.add('active');
2042    document.body.style.overflow = 'hidden';
2043    focusFirstField(modal);
2044}
2045
2046function closeLoginModal() {
2047    const modal = document.getElementById('login-modal');
2048    if (!modal) return;
2049    modal.classList.remove('active');
2050    document.body.style.overflow = '';
2051    const form = document.getElementById('login-form');
2052    if (form) {
2053        form.reset();
2054        clearAllFieldErrors(form);
2055    }
2056    setFormStatus('login-status');
2057}
2058
2059// Signup modal functions
2060function openSignupModal() {
2061    const modal = document.getElementById('signup-modal');
2062    if (!modal) {
2063        window.location.href = '/?signup=1';
2064        return;
2065    }
2066    renderAuthEventContext();
2067    modal.classList.add('active');
2068    document.body.style.overflow = 'hidden';
2069    focusFirstField(modal);
2070}
2071
2072function closeSignupModal() {
2073    const modal = document.getElementById('signup-modal');
2074    if (!modal) return;
2075    modal.classList.remove('active');
2076    document.body.style.overflow = '';
2077    resetSignupForm();
2078}
2079
2080// Back to a blank sign-up form (also used by "Use a different email")
2081function resetSignupForm() {
2082    const form = document.getElementById('signup-form');
2083    if (form) {
2084        form.reset();
2085        clearAllFieldErrors(form);
2086    }
2087    setFormStatus('signup-status');
2088    setFormStatus('signup-check-status');
2089    const main = document.getElementById('signup-main');
2090    const checkEmail = document.getElementById('signup-check-email');
2091    if (main) main.hidden = false;
2092    if (checkEmail) checkEmail.hidden = true;
2093}
2094
2095// The X button / backdrop: the member is abandoning the booking, so forget it
2096function dismissAuthModal(which) {
2097    clearPendingRegistration();
2098    if (which === 'signup') {
2099        closeSignupModal();
2100    } else {
2101        closeLoginModal();
2102    }
2103}
2104
2105// Move between "Log in" and "Create account", carrying the email across
2106function switchAuthModal(target) {
2107    const loginEmail = document.getElementById('login-email');
2108    const signupEmail = document.getElementById('signup-email');
2109    if (target === 'signup') {
2110        const email = loginEmail ? loginEmail.value.trim() : '';
2111        closeLoginModal();
2112        openSignupModal();
2113        if (signupEmail && email) signupEmail.value = email;
2114    } else {
2115        const email = signupEmail ? signupEmail.value.trim() : '';
2116        closeSignupModal();
2117        openLoginModal();
2118        if (loginEmail && email) loginEmail.value = email;
2119    }
2120}
2121
2122// Kept for older links: sign-up is now a single step
2123function goToSignupStep() {
2124    return true;
2125}
2126
2127// OAuth button handlers
2128async function loginWithGoogle() {
2129    try {
2130        const result = await signInWithGoogle();
2131        if (!result.success && result.error) {
2132            showNotification('❌ ' + result.error, 'error');
2133        }
2134        // Success will trigger OAuth redirect, no notification needed
2135    } catch (error) {
2136        console.error('Error:', error);
2137        showNotification('❌ An error occurred. Please try again.', 'error');
2138    }
2139}
2140
2141async function loginWithLinkedIn() {
2142    try {
2143        const result = await signInWithLinkedIn();
2144        if (!result.success && result.error) {
2145            showNotification('❌ ' + result.error, 'error');
2146        }
2147        // Success will trigger OAuth redirect, no notification needed
2148    } catch (error) {
2149        console.error('Error:', error);
2150        showNotification('❌ An error occurred. Please try again.', 'error');
2151    }
2152}
2153
2154async function signupWithGoogle() {
2155    return loginWithGoogle();
2156}
2157
2158async function signupWithLinkedIn() {
2159    return loginWithLinkedIn();
2160}
2161
2162// After any successful sign-in: continue the booking, or go to the dashboard
2163function finishSignIn(successMessage) {
2164    if (resumePendingRegistration(`${successMessage} Just confirm your place below.`)) {
2165        return;
2166    } else {
2167        showNotification(`✅ ${successMessage} Taking you to your dashboard…`, 'success');
2168        setTimeout(() => {
2169            window.location.href = 'dashboard.html';
2170        }, 1000);
2171    }
2172}
2173
2174// "Email me a sign-in link" — passwordless login for existing members
2175async function handleSendLoginLink() {
2176    const emailInput = document.getElementById('login-email');
2177    const linkBtn = document.getElementById('login-link-btn');
2178    const loginForm = document.getElementById('login-form');
2179    if (!emailInput) return;
2180
2181    clearAllFieldErrors(loginForm);
2182    setFormStatus('login-status');
2183
2184    const email = emailInput.value.trim();
2185    if (!isValidEmail(email)) {
2186        showFieldError(emailInput, 'Enter your email address and we\'ll send you a link');
2187        emailInput.focus();
2188        return;
2189    }
2190
2191    if (linkBtn) linkBtn.disabled = true;
2192    try {
2193        const result = await sendLoginLink(email);
2194        if (result.success) {
2195            setFormStatus('login-status', 'success', `Check your inbox: we've emailed a sign-in link to ${email}. Open it on this device to continue.`);
2196        } else if (result.code === 'no_account') {
2197            setFormStatus('login-status', 'error', result.error, {
2198                label: 'Create a free account',
2199                onClick: () => switchAuthModal('signup')
2200            });
2201        } else {
2202            setFormStatus('login-status', 'error', result.error);
2203        }
2204    } finally {
2205        if (linkBtn) linkBtn.disabled = false;
2206    }
2207}
2208
2209async function handleResendConfirmation(emailOverride) {
2210    const email = emailOverride
2211        || document.getElementById('signup-check-email-address')?.textContent
2212        || '';
2213    if (!email) return;
2214    const statusId = emailOverride ? 'login-status' : 'signup-check-status';
2215    setFormStatus(statusId, 'info', 'Sending…');
2216    const result = await resendConfirmationEmail(email);
2217    if (result.success) {
2218        setFormStatus(statusId, 'success', `Sent! Check ${email} for the confirmation link.`);
2219    } else {
2220        setFormStatus(statusId, 'error', result.error);
2221    }
2222}
2223
2224function showSignupCheckEmail(email) {
2225    const main = document.getElementById('signup-main');
2226    const checkEmail = document.getElementById('signup-check-email');
2227    const address = document.getElementById('signup-check-email-address');
2228    const next = document.getElementById('signup-check-email-next');
2229    if (address) address.textContent = email;
2230    if (next) {
2231        next.textContent = getPendingRegistration()
2232            ? 'Tap the link to activate your account and we\'ll bring you straight back to finish your booking.'
2233            : 'Tap the link to activate your account and you\'re all set.';
2234    }
2235    if (main) main.hidden = true;
2236    if (checkEmail) checkEmail.hidden = false;
2237    const content = document.querySelector('#signup-modal .modal-content');
2238    if (content) content.scrollTop = 0;
2239}
2240
2241// Handle login form submission
2242document.addEventListener('DOMContentLoaded', () => {
2243    const loginForm = document.getElementById('login-form');
2244
2245    if (loginForm) {
2246        loginForm.addEventListener('submit', async (e) => {
2247            e.preventDefault();
2248
2249            clearAllFieldErrors(loginForm);
2250            setFormStatus('login-status');
2251
2252            const emailInput = document.getElementById('login-email');
2253            const passwordInput = document.getElementById('login-password');
2254            const email = emailInput.value.trim();
2255            const password = passwordInput.value;
2256
2257            if (!isValidEmail(email)) {
2258                showFieldError(emailInput, 'Please enter a valid email address');
2259                emailInput.focus();
2260                return;
2261            }
2262            if (!password) {
2263                showFieldError(passwordInput, 'Please enter your password');
2264                passwordInput.focus();
2265                return;
2266            }
2267
2268            const submitButton = loginForm.querySelector('button[type="submit"]');
2269            setButtonLoading(submitButton, true);
2270
2271            try {
2272                const result = await signInWithEmail(email, password);
2273
2274                if (result.success) {
2275                    markReturningMember();
2276                    closeLoginModal();
2277                    finishSignIn('Logged in!');
2278                } else if (result.code === 'email_not_confirmed') {
2279                    setFormStatus('login-status', 'error', result.error, {
2280                        label: 'Resend confirmation email',
2281                        onClick: () => handleResendConfirmation(email)
2282                    });
2283                } else if (result.code === 'invalid_credentials') {
2284                    setFormStatus('login-status', 'error', `${result.error} Check for typos, reset your password, or use "Email me a sign-in link" below.`, {
2285                        label: 'New here? Create an account',
2286                        onClick: () => switchAuthModal('signup')
2287                    });
2288                } else {
2289                    setFormStatus('login-status', 'error', result.error || 'Could not sign you in. Please try again.');
2290                }
2291            } catch (error) {
2292                console.error('Error:', error);
2293                setFormStatus('login-status', 'error', 'Something went wrong. Please try again.');
2294            } finally {
2295                setButtonLoading(submitButton, false);
2296            }
2297        });
2298    }
2299});
2300
2301// Handle signup form submission
2302document.addEventListener('DOMContentLoaded', () => {
2303    const signupForm = document.getElementById('signup-form');
2304
2305    if (signupForm) {
2306        signupForm.addEventListener('submit', async (e) => {
2307            e.preventDefault();
2308
2309            clearAllFieldErrors(signupForm);
2310            setFormStatus('signup-status');
2311
2312            const nameInput = document.getElementById('signup-name');
2313            const emailInput = document.getElementById('signup-email');
2314            const passwordInput = document.getElementById('signup-password');
2315            const companyInput = document.getElementById('signup-company');
2316            const jobTitleInput = document.getElementById('signup-job-title');
2317            const termsCheckbox = document.getElementById('signup-terms');
2318
2319            const memberName = nameInput.value.trim();
2320            const email = emailInput.value.trim();
2321            const password = passwordInput.value;
2322            const companyOrganisation = companyInput.value.trim();
2323            const jobTitle = jobTitleInput.value.trim();
2324
2325            const problems = [];
2326            if (!memberName) problems.push([nameInput, 'Please enter your name']);
2327            if (!isValidEmail(email)) problems.push([emailInput, 'Please enter a valid email address']);
2328            if (password.length < 6) problems.push([passwordInput, 'Use at least 6 characters']);
2329            if (!companyOrganisation) problems.push([companyInput, 'Please enter your company or organisation']);
2330            if (!jobTitle) problems.push([jobTitleInput, 'Please enter your job title']);
2331            if (termsCheckbox && !termsCheckbox.checked) problems.push([termsCheckbox, 'Please accept the Terms & Conditions and Privacy Policy']);
2332
2333            if (problems.length > 0) {
2334                problems.forEach(([input, message]) => showFieldError(input, message));
2335                problems[0][0].focus();
2336                return;
2337            }
2338
2339            const submitButton = document.getElementById('signup-submit') || signupForm.querySelector('button[type="submit"]');
2340            setButtonLoading(submitButton, true);
2341
2342            try {
2343                const result = await signUpWithEmail(email, password, {
2344                    memberName,
2345                    companyOrganisation,
2346                    jobTitle,
2347                    termsAccepted: true
2348                });
2349
2350                if (!result.success) {
2351                    if (result.alreadyRegistered) {
2352                        // Send them to log in with the email already filled in
2353                        switchAuthModal('login');
2354                        setFormStatus('login-status', 'info', 'You already have an account with that email. Log in below, or get a sign-in link by email if you\'ve forgotten your password.');
2355                    } else {
2356                        setFormStatus('signup-status', 'error', result.error || 'Could not create your account. Please try again.');
2357                    }
2358                    return;
2359                }
2360
2361                markReturningMember();
2362
2363                // Email confirmation required: no session yet. The profile is
2364                // built from these details when they follow the link.
2365                if (result.needsConfirmation) {
2366                    showSignupCheckEmail(email);
2367                    return;
2368                }
2369
2370                const member = await ensureMemberProfile(result.session);
2371                closeSignupModal();
2372
2373                if (!member) {
2374                    showNotification('⚠️ Your account is ready, but we couldn\'t save your details. You can add them from your dashboard.', 'error');
2375                }
2376                finishSignIn('Account created!');
2377            } catch (error) {
2378                console.error('Error:', error);
2379                setFormStatus('signup-status', 'error', 'Something went wrong. Please try again.');
2380            } finally {
2381                setButtonLoading(submitButton, false);
2382                renderAuthEventContext();
2383            }
2384        });
2385    }
2386});
2387
2388
2389// Handle password reset form submission
2390document.addEventListener('DOMContentLoaded', () => {
2391    const passwordResetForm = document.getElementById('password-reset-form');
2392
2393    if (passwordResetForm) {
2394        passwordResetForm.addEventListener('submit', async (e) => {
2395            e.preventDefault();
2396
2397            clearAllFieldErrors(passwordResetForm);
2398            setFormStatus('reset-status');
2399
2400            const emailInput = document.getElementById('reset-email');
2401            const email = emailInput.value.trim();
2402            if (!isValidEmail(email)) {
2403                showFieldError(emailInput, 'Please enter a valid email address');
2404                emailInput.focus();
2405                return;
2406            }
2407
2408            const submitButton = passwordResetForm.querySelector('button[type="submit"]');
2409            setButtonLoading(submitButton, true);
2410
2411            try {
2412                const result = await resetPassword(email);
2413
2414                if (result.success) {
2415                    setFormStatus('reset-status', 'success', `If there's an account for ${email}, a reset link is on its way. Check your inbox.`);
2416                } else {
2417                    setFormStatus('reset-status', 'error', result.error || 'Failed to send reset link');
2418                }
2419            } catch (error) {
2420                console.error('Error:', error);
2421                setFormStatus('reset-status', 'error', 'Something went wrong. Please try again.');
2422            } finally {
2423                setButtonLoading(submitButton, false);
2424            }
2425        });
2426    }
2427});
2428
2429// ==========================================
2430// MEMBER PROFILE CREATION
2431// ==========================================
2432
2433// Shared so the auth listener and the registration flow don't race each other
2434// into creating the profile (and sending the welcome email) twice.
2435let memberProfilePromise = null;
2436
2437/**
2438 * Make sure a signed-in user has a row in `members`, building it from the
2439 * details captured at sign-up (email) or supplied by Google/LinkedIn.
2440 * Returns the member, or null if it couldn't be created.
2441 */
2442async function ensureMemberProfile(session) {
2443    if (!session || !session.user) return null;
2444    if (memberProfilePromise) return memberProfilePromise;
2445
2446    memberProfilePromise = (async () => {
2447        const { member } = await getCurrentSession();
2448        if (member) return member;
2449
2450        const metadata = session.user.user_metadata || {};
2451
2452        // Profile photo - LinkedIn OIDC uses 'picture', Google uses 'avatar_url' or 'picture'
2453        const profilePhoto = metadata.picture ||
2454                           metadata.avatar_url ||
2455                           metadata.profile_image_url ||
2456                           null;
2457
2458        // Full name - LinkedIn OIDC uses 'name', can also construct from given_name + family_name
2459        const memberName = metadata.full_name ||
2460                         metadata.name ||
2461                         (metadata.given_name && metadata.family_name ?
2462                          `${metadata.given_name} ${metadata.family_name}` : null) ||
2463                         session.user.email?.split('@')[0] ||
2464                         'User';
2465
2466        // Company and job title come from our own sign-up form. LinkedIn OIDC
2467        // doesn't provide them, so OAuth members add them when they register.
2468        const profileResult = await saveMemberProfile({
2469            memberName: memberName,
2470            phone: null,
2471            companyOrganisation: metadata.company || null,
2472            jobTitle: metadata.job_title || null,
2473            linkedinProfile: null,
2474            profilePhoto: profilePhoto
2475        });
2476
2477        if (!profileResult.success) {
2478            console.error('Failed to create member profile:', profileResult.error);
2479            return null;
2480        }
2481
2482        // Send welcome email (don't fail the sign-up if it doesn't send)
2483        // keepalive lets the request finish if we redirect straight away.
2484        // Recipient and name are taken server-side from the verified session
2485        // and member row.
2486        fetch('/.netlify/functions/send-welcome-email', {
2487            method: 'POST',
2488            keepalive: true,
2489            headers: {
2490                'Authorization': `Bearer ${session.access_token}`,
2491            },
2492        }).catch(emailError => console.error('Error sending welcome email:', emailError));
2493
2494        return profileResult.member;
2495    })();
2496
2497    try {
2498        return await memberProfilePromise;
2499    } finally {
2500        memberProfilePromise = null;
2501    }
2502}
2503
2504// Handle Supabase auth state changes
2505document.addEventListener('DOMContentLoaded', () => {
2506    onAuthStateChange(async (event, session) => {
2507        if (event === 'SIGNED_IN' && session) {
2508            // User logged in (password, email link or OAuth). Create their
2509            // profile if this is their first sign-in.
2510            await ensureMemberProfile(session);
2511
2512            // Update UI to show dashboard links
2513            checkAuthState();
2514        } else if (event === 'SIGNED_OUT') {
2515            // User signed out
2516            checkAuthState(); // Update UI
2517        }
2518    });
2519});
2520
2521// ==========================================
2522// GALLERY - LOAD AND DISPLAY EVENT ALBUMS
2523// ==========================================
2524
2525// Global variable to store events data
2526let eventsData = [];
2527
2528async function loadGallery() {
2529    const galleryLoading = document.getElementById('gallery-loading');
2530    const galleryAlbumsContainer = document.getElementById('gallery-albums-container');
2531    const galleryEmpty = document.getElementById('gallery-empty');
2532    const galleryUploadSection = document.getElementById('gallery-upload-section');
2533
2534    if (!galleryAlbumsContainer) return;
2535
2536    try {
2537        // Show upload button if logged in
2538        const sessionToken = getSessionToken();
2539        if (sessionToken) {
2540            if (galleryUploadSection) galleryUploadSection.style.display = 'block';
2541        }
2542
2543        // Fetch past events with photos
2544        const response = await fetch('/.netlify/functions/get-past-events');
2545        const data = await response.json();
2546
2547        if (galleryLoading) galleryLoading.style.display = 'none';
2548
2549        if (data.success && data.events.length > 0) {
2550            eventsData = data.events; // Store for album viewer
2551            displayEventAlbums(data.events);
2552            galleryAlbumsContainer.style.display = 'grid';
2553            if (galleryEmpty) galleryEmpty.style.display = 'none';
2554        } else {
2555            if (galleryEmpty) galleryEmpty.style.display = 'block';
2556        }
2557    } catch (error) {
2558        console.error('Error loading gallery:', error);
2559        if (galleryLoading) galleryLoading.style.display = 'none';
2560        if (galleryEmpty) {
2561            galleryEmpty.innerHTML = '<p>Failed to load gallery. Please try again later.</p>';
2562            galleryEmpty.style.display = 'block';
2563        }
2564    }
2565}
2566
2567function displayEventAlbums(events) {
2568    const galleryAlbumsContainer = document.getElementById('gallery-albums-container');
2569    if (!galleryAlbumsContainer) return;
2570
2571    // Filter to only show events that have photos
2572    const eventsWithPhotos = events.filter(event => event.photos && event.photos.length > 0);
2573
2574    if (eventsWithPhotos.length === 0) {
2575        // Show empty state if no events have photos
2576        const galleryEmpty = document.getElementById('gallery-empty');
2577        if (galleryEmpty) galleryEmpty.style.display = 'block';
2578        galleryAlbumsContainer.style.display = 'none';
2579        return;
2580    }
2581
2582    let albumsHTML = '';
2583
2584    eventsWithPhotos.forEach(event => {
2585        const eventDate = new Date(event.eventDate);
2586        const dateFormatted = eventDate.toLocaleDateString('en-US', {
2587            day: 'numeric',
2588            month: 'short',
2589            year: 'numeric'
2590        });
2591
2592        const photoCount = event.photos.length;
2593        const coverPhoto = event.photos[0]; // First photo as album cover
2594
2595        albumsHTML += `
2596            <div class="gallery-item album-cover" onclick="openAlbumViewer('${event.id}', '${event.eventName.replace(/'/g, "\\'")}', '${event.location}', '${event.country}', '${dateFormatted}')">
2597                <img src="${coverPhoto.url}" alt="${event.eventName}" loading="lazy">
2598                <div class="gallery-overlay">
2599                    <h4>${event.location}</h4>
2600                    <p>${dateFormatted}</p>
2601                </div>
2602            </div>
2603        `;
2604    });
2605
2606    galleryAlbumsContainer.innerHTML = albumsHTML;
2607}
2608
2609// ==========================================
2610// ALBUM VIEWER MODAL
2611// ==========================================
2612
2613async function openAlbumViewer(eventId, eventName, location, country, date) {
2614    const modal = document.getElementById('album-viewer-modal');
2615    if (!modal) return;
2616
2617    // Set event details
2618    document.getElementById('album-event-name').textContent = eventName;
2619    document.getElementById('album-event-details').textContent = `${location}, ${country} • ${date}`;
2620
2621    // Find event data
2622    const event = eventsData.find(e => e.id === eventId);
2623    if (!event) return;
2624
2625    // Display photos with comment sections
2626    await displayAlbumPhotos(event);
2627
2628    modal.classList.add('active');
2629    document.body.style.overflow = 'hidden';
2630}
2631
2632function closeAlbumViewer() {
2633    const modal = document.getElementById('album-viewer-modal');
2634    if (!modal) return;
2635
2636    modal.classList.remove('active');
2637    document.body.style.overflow = '';
2638}
2639
2640async function displayAlbumPhotos(event) {
2641    const container = document.getElementById('album-photos-container');
2642    if (!container) return;
2643
2644    // Check if user is logged in using Supabase
2645    const { data: { session } } = await supabaseClient.auth.getSession();
2646    const isLoggedIn = !!session;
2647
2648    let photosHTML = '';
2649
2650    event.photos.forEach(photo => {
2651        photosHTML += `
2652            <div class="album-photo-item" data-photo-url="${photo.url}">
2653                <img src="${photo.url}" alt="${event.eventName}" loading="lazy">
2654
2655                <div class="photo-comments-section">
2656                    <h4>Comments</h4>
2657                    <div class="comments-list" id="comments-${photo.id}">
2658                        <!-- Comments will load here -->
2659                    </div>
2660                    ${isLoggedIn ? `
2661                        <div class="add-comment-form">
2662                            <textarea class="comment-input" placeholder="Add a comment..." rows="2"></textarea>
2663                            <button class="btn-primary btn-sm" onclick="addPhotoComment('${event.id}', '${photo.url}', this)">Post Comment</button>
2664                        </div>
2665                    ` : `
2666                        <p style="color: var(--text-secondary); font-size: 14px; margin-top: 1rem;">
2667                            <a href="javascript:void(0)" onclick="closeAlbumViewer(); openLoginModal();">Login</a> to comment on photos
2668                        </p>
2669                    `}
2670                </div>
2671            </div>
2672        `;
2673    });
2674
2675    container.innerHTML = photosHTML;
2676
2677    // Load comments for each photo
2678    event.photos.forEach(photo => {
2679        loadPhotoComments(event.id, photo);
2680    });
2681}
2682
2683async function loadPhotoComments(eventId, photo) {
2684    const commentsList = document.getElementById(`comments-${photo.id}`);
2685    if (!commentsList) return;
2686
2687    try {
2688        const response = await fetch(`/.netlify/functions/get-photo-comments?eventId=${eventId}`);
2689        const data = await response.json();
2690
2691        if (data.success) {
2692            // Filter comments for this specific photo
2693            const photoComments = data.comments.filter(c => c.photoUrl === photo.url);
2694
2695            if (photoComments.length === 0) {
2696                commentsList.innerHTML = '<p style="color: var(--text-secondary); font-size: 14px;">No comments yet. Be the first to comment!</p>';
2697            } else {
2698                let commentsHTML = '';
2699                photoComments.forEach(comment => {
2700                    const commentDate = new Date(comment.createdDate);
2701                    const dateStr = commentDate.toLocaleDateString('en-US', {
2702                        month: 'short',
2703                        day: 'numeric',
2704                        year: 'numeric',
2705                        hour: '2-digit',
2706                        minute: '2-digit'
2707                    });
2708
2709                    commentsHTML += `
2710                        <div class="comment-item">
2711                            <strong>${escapeHtml(comment.memberName)}</strong>
2712                            <span class="comment-date">${dateStr}</span>
2713                            <p>${escapeHtml(comment.commentText)}</p>
2714                        </div>
2715                    `;
2716                });
2717                commentsList.innerHTML = commentsHTML;
2718            }
2719        }
2720    } catch (error) {
2721        console.error('Error loading comments:', error);
2722        commentsList.innerHTML = '<p style="color: red; font-size: 14px;">Failed to load comments</p>';
2723    }
2724}
2725
2726async function addPhotoComment(eventId, photoUrl, button) {
2727    const commentInput = button.previousElementSibling;
2728    const commentText = commentInput.value.trim();
2729
2730    if (!commentText) {
2731        showNotification('❌ Please enter a comment', 'error');
2732        return;
2733    }
2734
2735    const sessionToken = getSessionToken();
2736    if (!sessionToken) {
2737        showNotification('❌ Please log in to comment', 'error');
2738        return;
2739    }
2740
2741    const originalButtonText = button.textContent;
2742    button.disabled = true;
2743    button.textContent = 'Posting...';
2744
2745    try {
2746        const response = await fetch('/.netlify/functions/add-photo-comment', {
2747            method: 'POST',
2748            headers: {
2749                'Authorization': `Bearer ${sessionToken}`,
2750                'Content-Type': 'application/json',
2751            },
2752            body: JSON.stringify({
2753                eventId: eventId,
2754                photoUrl: photoUrl,
2755                commentText: commentText,
2756            }),
2757        });
2758
2759        const data = await response.json();
2760
2761        if (response.status === 200) {
2762            showNotification('✅ Comment added!', 'success');
2763            commentInput.value = '';
2764
2765            // Reload comments for this photo
2766            const event = eventsData.find(e => e.id === eventId);
2767            const photo = event.photos.find(p => p.url === photoUrl);
2768            if (photo) {
2769                await loadPhotoComments(eventId, photo);
2770            }
2771        } else {
2772            showNotification('❌ ' + (data.message || data.error || 'Failed to add comment'), 'error');
2773        }
2774    } catch (error) {
2775        console.error('Error:', error);
2776        showNotification('❌ An error occurred. Please try again.', 'error');
2777    } finally {
2778        button.disabled = false;
2779        button.textContent = originalButtonText;
2780    }
2781}
2782
2783// ==========================================
2784// PHOTO UPLOAD MODAL
2785// ==========================================
2786
2787function openPhotoUploadModal() {
2788    const modal = document.getElementById('photo-upload-modal');
2789    if (!modal) return;
2790
2791    modal.classList.add('active');
2792    document.body.style.overflow = 'hidden';
2793
2794    // Load past events for selection
2795    loadPastEventsForUpload();
2796}
2797
2798function closePhotoUploadModal() {
2799    const modal = document.getElementById('photo-upload-modal');
2800    if (!modal) return;
2801
2802    modal.classList.remove('active');
2803    document.body.style.overflow = '';
2804
2805    // Reset form
2806    const form = document.getElementById('photo-upload-form');
2807    if (form) form.reset();
2808
2809    // Hide preview
2810    const previewContainer = document.getElementById('photo-preview-container');
2811    if (previewContainer) previewContainer.style.display = 'none';
2812}
2813
2814async function loadPastEventsForUpload() {
2815    const select = document.getElementById('photo-event-select');
2816    if (!select) return;
2817
2818    try {
2819        // Include events without photos for upload dropdown
2820        const response = await fetch('/.netlify/functions/get-past-events?includeEmpty=true');
2821        const data = await response.json();
2822
2823        if (data.success && data.events.length > 0) {
2824            select.innerHTML = '<option value="">Select an event...</option>' +
2825                data.events.map(event => {
2826                    const eventDate = new Date(event.eventDate);
2827                    const dateFormatted = eventDate.toLocaleDateString('en-US', {
2828                        day: 'numeric',
2829                        month: 'short',
2830                        year: 'numeric'
2831                    });
2832                    return `<option value="${event.id}">${event.eventName} - ${dateFormatted}</option>`;
2833                }).join('');
2834        } else {
2835            select.innerHTML = '<option value="">No past events available</option>';
2836        }
2837    } catch (error) {
2838        console.error('Error loading events:', error);
2839        select.innerHTML = '<option value="">Failed to load events</option>';
2840    }
2841}
2842
2843// Handle photo file selection and preview
2844document.addEventListener('DOMContentLoaded', () => {
2845    const photoFileInput = document.getElementById('photo-file');
2846    const photoPreview = document.getElementById('photo-preview');
2847    const photoPreviewContainer = document.getElementById('photo-preview-container');
2848
2849    if (photoFileInput) {
2850        photoFileInput.addEventListener('change', (e) => {
2851            const file = e.target.files[0];
2852            if (file && photoPreview && photoPreviewContainer) {
2853                const reader = new FileReader();
2854                reader.onload = (event) => {
2855                    photoPreview.src = event.target.result;
2856                    photoPreviewContainer.style.display = 'block';
2857                };
2858                reader.readAsDataURL(file);
2859            }
2860        });
2861    }
2862
2863    // Handle photo upload form submission
2864    const photoUploadForm = document.getElementById('photo-upload-form');
2865    if (photoUploadForm) {
2866        photoUploadForm.addEventListener('submit', async (e) => {
2867            e.preventDefault();
2868
2869            const submitButton = photoUploadForm.querySelector('button[type="submit"]');
2870            const originalButtonText = submitButton.textContent;
2871            submitButton.disabled = true;
2872            submitButton.textContent = 'Uploading...';
2873
2874            const eventId = document.getElementById('photo-event-select').value;
2875            const photoFile = document.getElementById('photo-file').files[0];
2876
2877            if (!eventId || !photoFile) {
2878                showNotification('❌ Please select an event and a photo', 'error');
2879                submitButton.disabled = false;
2880                submitButton.textContent = originalButtonText;
2881                return;
2882            }
2883
2884            // Check file size (max 5MB)
2885            if (photoFile.size > 5 * 1024 * 1024) {
2886                showNotification('❌ Photo must be less than 5MB', 'error');
2887                submitButton.disabled = false;
2888                submitButton.textContent = originalButtonText;
2889                return;
2890            }
2891
2892            // Convert to base64
2893            const reader = new FileReader();
2894            reader.readAsDataURL(photoFile);
2895
2896            reader.onload = async () => {
2897                const photoBase64 = reader.result;
2898                const sessionToken = getSessionToken();
2899
2900                try {
2901                    const response = await fetch('/.netlify/functions/upload-event-photo', {
2902                        method: 'POST',
2903                        headers: {
2904                            'Authorization': `Bearer ${sessionToken}`,
2905                            'Content-Type': 'application/json',
2906                        },
2907                        body: JSON.stringify({
2908                            eventId: eventId,
2909                            photoBase64: photoBase64,
2910                        }),
2911                    });
2912
2913                    const data = await response.json();
2914
2915                    if (response.status === 200) {
2916                        showNotification('✅ Photo uploaded successfully! Your name has been added to the photo.', 'success');
2917                        closePhotoUploadModal();
2918
2919                        // Reload gallery to show new photo
2920                        setTimeout(() => {
2921                            loadGallery();
2922                        }, 1000);
2923                    } else if (response.status === 500 && data.error === 'Image hosting not configured') {
2924                        showNotification('❌ Image hosting not configured. Please contact the administrator.', 'error');
2925                        console.error('IMGBB_API_KEY not set in environment variables');
2926                    } else {
2927                        showNotification('❌ ' + (data.message || data.error || 'Failed to upload photo'), 'error');
2928                    }
2929                } catch (error) {
2930                    console.error('Error:', error);
2931                    showNotification('❌ An error occurred. Please try again.', 'error');
2932                } finally {
2933                    submitButton.disabled = false;
2934                    submitButton.textContent = originalButtonText;
2935                }
2936            };
2937
2938            reader.onerror = () => {
2939                showNotification('❌ Failed to read photo file', 'error');
2940                submitButton.disabled = false;
2941                submitButton.textContent = originalButtonText;
2942            };
2943        });
2944    }
2945
2946    // Load gallery on page load
2947    loadGallery();
2948});
2949
2950// ==========================================
2951// MEMBER SEARCH & MESSAGING
2952// ==========================================
2953// Search and the send-message form are handled in dashboard.js (via the
2954// search-members and send-member-message Netlify functions). Don't bind
2955// them here as well: dashboard.html loads both scripts.
2956
2957function closeSendMessageModal() {
2958    const modal = document.getElementById('send-message-modal');
2959    if (!modal) return;
2960
2961    modal.classList.remove('active');
2962    document.body.style.overflow = '';
2963
2964    // Reset form
2965    const form = document.getElementById('send-message-form');
2966    if (form) form.reset();
2967}
2968
2969// ==========================================
2970// TERMS & CONDITIONS AND PRIVACY MODALS
2971// ==========================================
2972function openTermsModal() {
2973    const modal = document.getElementById('terms-modal');
2974    modal.classList.add('active');
2975    document.body.style.overflow = 'hidden';
2976}
2977
2978function closeTermsModal() {
2979    const modal = document.getElementById('terms-modal');
2980    modal.classList.remove('active');
2981    document.body.style.overflow = '';
2982}
2983
2984function openPrivacyModal() {
2985    const modal = document.getElementById('privacy-modal');
2986    modal.classList.add('active');
2987    document.body.style.overflow = 'hidden';
2988}
2989
2990function closePrivacyModal() {
2991    const modal = document.getElementById('privacy-modal');
2992    modal.classList.remove('active');
2993    document.body.style.overflow = '';
2994}
2995
2996function openPasswordResetModal() {
2997    const modal = document.getElementById('password-reset-modal');
2998    modal.classList.add('active');
2999    document.body.style.overflow = 'hidden';
3000    // Close login modal if it's open
3001    closeLoginModal();
3002}
3003
3004function closePasswordResetModal() {
3005    const modal = document.getElementById('password-reset-modal');
3006    modal.classList.remove('active');
3007    document.body.style.overflow = '';
3008    document.getElementById('password-reset-form').reset();
3009}
3010
3011// ==========================================
3012// TIMELINE COLLAPSE/EXPAND
3013// ==========================================
3014const timelineToggle = document.getElementById('timeline-toggle');
3015const milestonesContainer = document.getElementById('milestones-container');
3016
3017if (timelineToggle && milestonesContainer) {
3018    timelineToggle.addEventListener('click', () => {
3019        const isExpanded = timelineToggle.getAttribute('aria-expanded') === 'true';
3020
3021        if (isExpanded) {
3022            // Collapse
3023            milestonesContainer.classList.add('collapsed');
3024            timelineToggle.setAttribute('aria-expanded', 'false');
3025            timelineToggle.querySelector('.toggle-text').textContent = 'Show Full Timeline';
3026        } else {
3027            // Expand
3028            milestonesContainer.classList.remove('collapsed');
3029            timelineToggle.setAttribute('aria-expanded', 'true');
3030            timelineToggle.querySelector('.toggle-text').textContent = 'Hide Timeline';
3031        }
3032    });
3033}
3034
3035// ==========================================
3036// CONSOLE MESSAGE
3037// ==========================================
3038console.log('%c🚀 Project Connect Group', 'font-size: 24px; font-weight: bold; color: #00bcd4;');
3039console.log('%c#PayItForward #FindAMentor #BreakingPaywalls', 'font-size: 14px; color: #00bcd4;');
3040console.log('Interested in how this site was built? Get in touch with us!');

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.