PageSourceSearch

https://grassgigs.com/jam-form.js?v=20260401b

js grassgigs.com collected 2026-10-02 00:30:05 UTC 41,961 bytes, 1,086 lines download raw bytes

1// jam-form.js - Community Jam Submission Form
2// Handles jam submission, venue search, validation, and form interactions
3
4
5
6// Use API_BASE_URL from app.js (already declared globally)
7
8// Global state (scoped to avoid conflicts with dashboard.js)
9let selectedVenue = null;
10let jamVenueSearchTimeout = null;
11
12// Wait for DOM to be ready, then override the function
13if (document.readyState === 'loading') {
14    document.addEventListener('DOMContentLoaded', initJamForm);
15} else {
16    // DOM already loaded
17    initJamForm();
18}
19
20function initJamForm() {
21    // Store the original function from app.js
22    const originalShowJamSubmissionForm = window.showJamSubmissionForm;
23    
24    if (!originalShowJamSubmissionForm) {
25        console.error('[JAM-FORM] Original showJamSubmissionForm not found!');
26        return;
27    }
28    
29    // Override with our enhanced version
30    window.showJamSubmissionForm = function() {
31        // Check authentication
32        const idToken = localStorage.getItem('cognitoIdToken');
33        if (!idToken) {
34            document.getElementById('bandLoginModal').style.display = 'block';
35            showLoginForm();
36            return;
37        }
38        
39        // If user is logged in, they must have verified their email (Cognito requirement)
40        
41        // Reset form and show modal
42        resetJamForm();
43        closeModal('jamDashboardModal');
44        document.getElementById('jamSubmissionModal').style.display = 'block';
45        
46        // Set minimum date to today
47        const today = new Date().toISOString().split('T')[0];
48        const startDateInput = document.getElementById('startDate');
49        if (startDateInput) {
50            startDateInput.setAttribute('min', today);
51        }
52        
53        // Initialize venue search when modal opens
54        setTimeout(() => {
55            initializeVenueSearch();
56        }, 100);
57    };
58    
59    // Set up form event listeners
60    const descriptionTextarea = document.getElementById('jamDescription');
61    if (descriptionTextarea) {
62        descriptionTextarea.addEventListener('input', updateDescriptionCounter);
63    }
64    
65    const jamForm = document.getElementById('jamSubmissionForm');
66    if (jamForm) {
67        jamForm.addEventListener('submit', submitJamForm);
68    }
69    
70    // Close venue results when clicking outside
71    document.addEventListener('click', function(e) {
72        const venueSearch = document.getElementById('venueSearch');
73        const venueResults = document.getElementById('venueSearchResults');
74        
75        if (venueSearch && venueResults && 
76            !venueSearch.contains(e.target) && 
77            !venueResults.contains(e.target)) {
78            venueResults.style.display = 'none';
79        }
80    });
81}
82
83// Reset jam form
84function resetJamForm() {
85    const form = document.getElementById('jamSubmissionForm');
86    if (form) {
87        form.reset();
88        // Clear editing state
89        delete form.dataset.editingJamId;
90    }
91    
92    // Reset submit button text
93    const submitBtn = form?.querySelector('button[type="submit"]');
94    if (submitBtn) {
95        submitBtn.textContent = 'Submit Jam';
96    }
97    
98    // Reset modal title
99    const modalTitle = document.querySelector('#jamSubmissionModal h2');
100    if (modalTitle) {
101        modalTitle.textContent = 'Submit a Community Jam';
102    }
103    
104    // Clear selected venue
105    selectedVenue = null;
106    const selectedDisplay = document.getElementById('selectedVenueDisplay');
107    const venueSearch = document.getElementById('venueSearch');
108    const venueResults = document.getElementById('venueSearchResults');
109    
110    if (selectedDisplay) selectedDisplay.style.display = 'none';
111    if (venueSearch) venueSearch.style.display = 'block';
112    if (venueResults) venueResults.innerHTML = '';
113    
114    // Hide new venue section
115    const newVenueSection = document.getElementById('newVenueSection');
116    if (newVenueSection) newVenueSection.style.display = 'none';
117    
118    // Reset character counter
119    updateDescriptionCounter();
120    
121    // Clear error messages
122    clearFormErrors();
123}
124
125// Clear selected venue
126function clearSelectedVenue() {
127    selectedVenue = null;
128    document.getElementById('selectedVenueDisplay').style.display = 'none';
129    document.getElementById('venueSearch').style.display = 'block';
130    document.getElementById('venueSearch').value = '';
131    document.getElementById('venueSearchResults').innerHTML = '';
132    
133    // Re-initialize venue search to ensure event listeners are attached
134    initializeVenueSearch();
135    
136    // Remove required from new venue fields since they're hidden
137    const newVenueAddress = document.getElementById('newVenueAddress');
138    if (newVenueAddress) {
139        newVenueAddress.removeAttribute('required');
140    }
141}
142
143// Hide new venue section
144function hideNewVenueSection() {
145    document.getElementById('newVenueSection').style.display = 'none';
146    document.getElementById('newVenueName').value = '';
147    document.getElementById('newVenueAddress').value = '';
148    document.getElementById('newVenueCity').value = '';
149    document.getElementById('newVenueState').value = '';
150    document.getElementById('newVenueZip').value = '';
151    
152    // Remove required from new venue fields since they're hidden
153    const newVenueAddress = document.getElementById('newVenueAddress');
154    if (newVenueAddress) {
155        newVenueAddress.removeAttribute('required');
156    }
157}
158
159// Show new venue section
160function showNewVenueSection() {
161    document.getElementById('newVenueSection').style.display = 'block';
162    document.getElementById('venueSearchResults').innerHTML = '';
163    document.getElementById('venueSearch').value = '';
164    
165    // Add required to new venue address field when creating new venue
166    const newVenueAddress = document.getElementById('newVenueAddress');
167    if (newVenueAddress) {
168        newVenueAddress.setAttribute('required', 'required');
169    }
170}
171
172// Initialize venue search (called when modal opens)
173function initializeVenueSearch() {
174    const venueSearchInput = document.getElementById('venueSearch');
175    
176    if (!venueSearchInput) {
177        console.warn('[JAM-FORM] Venue search input not found');
178        return;
179    }
180    
181    console.log('[JAM-FORM] Initializing venue search - element found:', venueSearchInput);
182    
183    // Remove existing listeners by cloning
184    const newInput = venueSearchInput.cloneNode(true);
185    venueSearchInput.parentNode.replaceChild(newInput, venueSearchInput);
186    
187    console.log('[JAM-FORM] Event listeners attached to new input element');
188    
189    // Attach input event for autocomplete
190    newInput.addEventListener('input', function(e) {
191        const query = e.target.value.trim();
192        console.log('[JAM-FORM] Venue search input event fired! Query:', query);
193        
194        // Clear any existing timeout
195        if (jamVenueSearchTimeout) {
196            clearTimeout(jamVenueSearchTimeout);
197        }
198        
199        // If query is too short, clear results and return
200        if (!query || query.length < 2) {
201            console.log('[JAM-FORM] Query too short, clearing results');
202            const resultsEl = document.getElementById('venueSearchResults');
203            if (resultsEl) {
204                resultsEl.innerHTML = '';
205                resultsEl.style.display = 'none';
206            }
207            return;
208        }
209        
210        // Debounce the search
211        jamVenueSearchTimeout = setTimeout(() => {
212            console.log('[JAM-FORM] Debounce complete, searching venues for:', query);
213            searchVenuesForJam(query);
214        }, 300);
215    });
216    
217    newInput.addEventListener('focus', function() {
218        console.log('[JAM-FORM] Venue search focused');
219        this.setAttribute('aria-expanded', 'true');
220    });
221    
222    newInput.addEventListener('blur', function() {
223        console.log('[JAM-FORM] Venue search blurred');
224        setTimeout(() => {
225            this.setAttribute('aria-expanded', 'false');
226        }, 200);
227    });
228    
229    console.log('[JAM-FORM] Venue search initialization complete');
230}
231
232// Venue search with autocomplete
233async function searchVenuesForJam(query) {
234    if (!query || query.length < 2) {
235        document.getElementById('venueSearchResults').innerHTML = '';
236        return;
237    }
238    
239    try {
240        console.log('[JAM-FORM] Fetching venues from API...');
241        const response = await fetch(`${API_BASE_URL}/jams/venues/search?q=${encodeURIComponent(query)}&limit=10`);
242        const data = await response.json();
243        
244        console.log('[JAM-FORM] API response:', data);
245        
246        if (data.success && data.venues) {
247            displayVenueResultsForJam(data.venues);
248        } else {
249            displayVenueResultsForJam([]);
250        }
251    } catch (error) {
252        console.error('[JAM-FORM] Error searching venues:', error);
253        displayVenueResultsForJam([]);
254    }
255}
256
257// Display venue search results
258function displayVenueResultsForJam(venues) {
259    const resultsContainer = document.getElementById('venueSearchResults');
260    
261    // Filter out venues without required information (venue_name and city)
262    const validVenues = venues.filter(venue => {
263        return venue.venue_name && venue.venue_name.trim() !== '' &&
264               venue.city && venue.city.trim() !== '';
265    });
266    
267    if (validVenues.length === 0) {
268        resultsContainer.innerHTML = `
269            <div class="venue-result" onclick="showNewVenueSection()" style="cursor: pointer; padding: 1rem; border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(14, 165, 233, 0.1);">
270                <strong style="color: #0ea5e9;">➕ Create New Venue</strong>
271                <div style="font-size: 0.9rem; color: #94a3b8;">Venue not found? Add a new one</div>
272            </div>
273        `;
274        resultsContainer.style.display = 'block';
275        return;
276    }
277    
278    let html = '';
279    validVenues.forEach(venue => {
280        html += `
281            <div class="venue-result" onclick='selectVenueForJam(${JSON.stringify(venue)})' style="cursor: pointer; padding: 1rem; border-bottom: 1px solid rgba(255,255,255,0.1);">
282                <strong>${escapeHtml(venue.venue_name)}</strong>
283                <div style="font-size: 0.9rem; color: #94a3b8;">${escapeHtml(venue.city)}${venue.state ? ', ' + escapeHtml(venue.state) : ''}</div>
284            </div>
285        `;
286    });
287    
288    html += `
289        <div class="venue-result" onclick="showNewVenueSection()" style="cursor: pointer; padding: 1rem; background: rgba(14, 165, 233, 0.1);">
290            <strong style="color: #0ea5e9;">➕ Create New Venue</strong>
291        </div>
292    `;
293    
294    resultsContainer.innerHTML = html;
295    resultsContainer.style.display = 'block';
296}
297
298// Select venue from search results
299function selectVenueForJam(venue) {
300    selectedVenue = venue;
301    document.getElementById('selectedVenueName').textContent = venue.venue_name;
302    document.getElementById('selectedVenueLocation').textContent = venue.state ? `${venue.city}, ${venue.state}` : venue.city;
303    document.getElementById('selectedVenueDisplay').style.display = 'flex';
304    document.getElementById('venueSearch').style.display = 'none';
305    document.getElementById('venueSearchResults').innerHTML = '';
306    document.getElementById('venueSearchResults').style.display = 'none';
307    
308    // Hide new venue section and remove required attribute
309    hideNewVenueSection();
310}
311
312// Update description character counter
313function updateDescriptionCounter() {
314    const textarea = document.getElementById('jamDescription');
315    const counter = document.getElementById('jamDescriptionCounter');
316    if (textarea && counter) {
317        const length = textarea.value.length;
318        counter.textContent = `${length}/500`;
319    }
320}
321
322// Escape HTML to prevent XSS
323function escapeHtml(text) {
324    const div = document.createElement('div');
325    div.textContent = text;
326    return div.innerHTML;
327}
328
329// Validate form fields
330function validateJamForm() {
331    const errors = [];
332    
333    const jamName = document.getElementById('jamName').value.trim();
334    if (jamName.length < 3 || jamName.length > 100) {
335        errors.push('Jam name must be between 3 and 100 characters');
336        setFieldError('jamName', true);
337    } else {
338        setFieldError('jamName', false);
339    }
340    
341    const newVenueSection = document.getElementById('newVenueSection');
342    if (!selectedVenue && newVenueSection.style.display === 'none') {
343        errors.push('Please select a venue or create a new one');
344        setFieldError('venueSearch', true);
345    } else if (newVenueSection.style.display !== 'none') {
346        // Clear venue search error when creating new venue
347        setFieldError('venueSearch', false);
348        
349        const newVenueName = document.getElementById('newVenueName').value.trim();
350        const newVenueAddress = document.getElementById('newVenueAddress').value.trim();
351        const newVenueCity = document.getElementById('newVenueCity').value.trim();
352        const newVenueState = document.getElementById('newVenueState').value;
353        
354        if (!newVenueName) {
355            errors.push('Venue name is required');
356            setFieldError('newVenueName', true);
357        } else {
358            setFieldError('newVenueName', false);
359        }
360        
361        if (!newVenueAddress) {
362            errors.push('Venue address is required');
363            setFieldError('newVenueAddress', true);
364        } else {
365            setFieldError('newVenueAddress', false);
366        }
367        
368        if (!newVenueCity) {
369            errors.push('Venue city is required');
370            setFieldError('newVenueCity', true);
371        } else {
372            setFieldError('newVenueCity', false);
373        }
374        
375        // State is now optional for international venues
376        setFieldError('newVenueState', false);
377    } else {
378        setFieldError('venueSearch', false);
379    }
380    
381    const recurringPattern = document.getElementById('recurringPattern').value;
382    if (!recurringPattern) {
383        errors.push('Please select a recurring pattern');
384        setFieldError('recurringPattern', true);
385    } else {
386        setFieldError('recurringPattern', false);
387    }
388    
389    const dayOfWeek = document.getElementById('dayOfWeek').value;
390    if (!dayOfWeek) {
391        errors.push('Please select a day of week');
392        setFieldError('dayOfWeek', true);
393    } else {
394        setFieldError('dayOfWeek', false);
395    }
396    
397    const startTime = document.getElementById('startTime').value;
398    const endTime = document.getElementById('endTime').value;
399    
400    if (!startTime) {
401        errors.push('Start time is required');
402        setFieldError('startTime', true);
403    } else {
404        setFieldError('startTime', false);
405    }
406    
407    // End time is optional, but if provided must be after start time
408    if (endTime) {
409        setFieldError('endTime', false);
410        
411        if (startTime && endTime && startTime >= endTime) {
412            errors.push('End time must be after start time');
413            setFieldError('endTime', true);
414        }
415    } else {
416        setFieldError('endTime', false);
417    }
418    
419    const startDate = document.getElementById('startDate').value;
420    if (!startDate) {
421        errors.push('Start date is required');
422        setFieldError('startDate', true);
423    } else {
424        // Check if we're editing an existing jam
425        const form = document.getElementById('jamSubmissionForm');
426        const isEditing = form?.dataset.editingJamId;
427        
428        // Only validate future dates for new jams, not when editing
429        if (!isEditing) {
430            const today = new Date();
431            today.setHours(0, 0, 0, 0);
432            const selectedDate = new Date(startDate + 'T00:00:00');
433            
434            if (selectedDate < today) {
435                errors.push('Start date must be today or in the future');
436                setFieldError('startDate', true);
437            } else {
438                setFieldError('startDate', false);
439            }
440        } else {
441            // When editing, always clear the error
442            setFieldError('startDate', false);
443        }
444    }
445    
446    const description = document.getElementById('jamDescription').value;
447    if (description.length > 500) {
448        errors.push('Description must be 500 characters or less');
449        setFieldError('jamDescription', true);
450    } else {
451        setFieldError('jamDescription', false);
452    }
453    
454    const link = document.getElementById('jamLink').value.trim();
455    if (!link) {
456        errors.push('Link is required');
457        setFieldError('jamLink', true);
458    } else {
459        const urlPattern = /^https?:\/\/.+/i;
460        const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
461        
462        if (!urlPattern.test(link) && !emailPattern.test(link)) {
463            errors.push('Link must be a valid URL (starting with http:// or https://) or email address');
464            setFieldError('jamLink', true);
465        } else {
466            setFieldError('jamLink', false);
467        }
468    }
469    
470    return errors;
471}
472
473// Set field error state
474function setFieldError(fieldId, hasError) {
475    const field = document.getElementById(fieldId);
476    if (field) {
477        if (hasError) {
478            field.style.borderColor = '#ef4444';
479            field.setAttribute('aria-invalid', 'true');
480        } else {
481            field.style.borderColor = '';
482            field.setAttribute('aria-invalid', 'false');
483        }
484    }
485}
486
487// Clear form errors
488function clearFormErrors() {
489    const fields = ['jamName', 'venueSearch', 'newVenueName', 'newVenueAddress', 'newVenueCity', 
490                    'newVenueState', 'recurringPattern', 'dayOfWeek', 'startTime', 'endTime', 
491                    'startDate', 'jamDescription', 'jamLink'];
492    
493    fields.forEach(fieldId => {
494        setFieldError(fieldId, false);
495    });
496    
497    const errorsContainer = document.getElementById('jamFormErrors');
498    if (errorsContainer) {
499        errorsContainer.style.display = 'none';
500        errorsContainer.innerHTML = '';
501    }
502}
503
504// Display form errors
505function displayFormErrors(errors) {
506    const errorsContainer = document.getElementById('jamFormErrors');
507    if (errorsContainer && errors.length > 0) {
508        // Make field names more user-friendly
509        const friendlyErrors = errors.map(error => {
510            return error
511                .replace(/venue_name:/i, 'Venue Name:')
512                .replace(/venue_address:/i, 'Venue Address:')
513                .replace(/venue_city:/i, 'City:')
514                .replace(/venue_state:/i, 'State:')
515                .replace(/venue_zip:/i, 'ZIP Code:')
516                .replace(/start_time:/i, 'Start Time:')
517                .replace(/end_time:/i, 'End Time:')
518                .replace(/start_date:/i, 'Start Date:')
519                .replace(/recurrence_pattern:/i, 'Recurrence Pattern:')
520                .replace(/day_of_week:/i, 'Day of Week:')
521                .replace(/frequency:/i, 'Frequency:')
522                .replace(/link:/i, 'Link:')
523                .replace(/description:/i, 'Description:')
524                .replace(/venue_id:/i, 'Venue:');
525        });
526        
527        errorsContainer.innerHTML = `
528            <div style="background: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
529                <strong style="color: #ef4444;">Please fix the following errors:</strong>
530                <ul style="margin: 0.5rem 0 0 1.5rem; color: #ef4444;">
531                    ${friendlyErrors.map(error => `<li>${escapeHtml(error)}</li>`).join('')}
532                </ul>
533            </div>
534        `;
535        errorsContainer.style.display = 'block';
536        errorsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
537    }
538}
539
540// Submit jam form
541async function submitJamForm(event) {
542    event.preventDefault();
543    
544    // Check if we're in edit mode
545    const form = document.getElementById('jamSubmissionForm');
546    const editingJamId = form?.dataset.editingJamId;
547    
548    if (editingJamId) {
549        // This is an update - call the update function
550        console.log('[JAM-FORM] Edit mode detected, calling submitJamUpdate');
551        await submitJamUpdate(editingJamId);
552        return;
553    }
554    
555    // Otherwise, proceed with normal submission
556    console.log('[DEBUG] Submitting jam form');
557    
558    const errors = validateJamForm();
559    console.log('[DEBUG] Validation errors:', errors);
560    
561    if (errors.length > 0) {
562        console.log('[DEBUG] Validation failed, displaying errors');
563        displayFormErrors(errors);
564        return;
565    }
566    
567    console.log('[DEBUG] Validation passed');
568    clearFormErrors();
569    
570    const jamName = document.getElementById('jamName').value.trim();
571    const recurringPattern = document.getElementById('recurringPattern').value;
572    const dayOfWeek = document.getElementById('dayOfWeek').value;
573    const startTime = document.getElementById('startTime').value;
574    const endTime = document.getElementById('endTime').value;
575    const startDate = document.getElementById('startDate').value;
576    const description = document.getElementById('jamDescription').value.trim();
577    const link = document.getElementById('jamLink').value.trim();
578    
579    // Convert day name to number (0=Sunday, 1=Monday, ..., 6=Saturday)
580    const dayNameToNumber = {
581        'Sunday': 0,
582        'Monday': 1,
583        'Tuesday': 2,
584        'Wednesday': 3,
585        'Thursday': 4,
586        'Friday': 5,
587        'Saturday': 6
588    };
589    const dayOfWeekNumber = dayNameToNumber[dayOfWeek];
590    
591    // Build submission data object
592    const submissionData = {
593        jam_name: jamName,
594        start_time: startTime,
595        end_time: endTime,
596        start_date: startDate, // Required at top level
597        description: description || null,
598        link: link
599    };
600    
601    // Add optional end date if provided
602    const endDate = document.getElementById('endDate').value;
603    if (endDate) {
604        submissionData.end_date = endDate;
605    }
606    
607    // Add venue data (either venue_id or new_venue at top level)
608    const newVenueSection = document.getElementById('newVenueSection');
609    
610    console.log('[DEBUG] selectedVenue:', selectedVenue);
611    console.log('[DEBUG] newVenueSection display:', newVenueSection?.style.display);
612    
613    if (selectedVenue) {
614        // Existing venue - send venue_id (which is the venue_name)
615        console.log('[DEBUG] Using selected venue:', selectedVenue.venue_id);
616        submissionData.venue_id = selectedVenue.venue_id;
617    } else if (newVenueSection.style.display !== 'none') {
618        // New venue - send new_venue object
619        const stateValue = document.getElementById('newVenueState').value;
620        submissionData.new_venue = {
621            venue_name: document.getElementById('newVenueName').value.trim(),
622            address: document.getElementById('newVenueAddress').value.trim(),
623            city: document.getElementById('newVenueCity').value.trim(),
624            state: stateValue === 'OTHER' ? '' : stateValue,
625            zip: document.getElementById('newVenueZip').value.trim()
626        };
627    } else {
628        displayFormErrors(['Please select a venue or create a new one']);
629        return;
630    }
631    
632    // Build recurrence_pattern with required fields
633    const recurrencePattern = {
634        start_date: startDate,
635        day_of_week: dayOfWeekNumber // Must be a number 0-6
636    };
637    
638    // Add optional end date if provided
639    if (endDate) {
640        recurrencePattern.end_date = endDate;
641    }
642    
643    // Handle different pattern types
644    if (recurringPattern === 'weekly') {
645        recurrencePattern.frequency = 'weekly';
646        recurrencePattern.interval = 1;
647    } else if (recurringPattern === 'biweekly') {
648        recurrencePattern.frequency = 'weekly';
649        recurrencePattern.interval = 2;
650    } else if (recurringPattern === 'monthly') {
651        recurrencePattern.frequency = 'monthly';
652    } else if (recurringPattern === 'nth_1st_3rd') {
653        recurrencePattern.frequency = 'monthly';
654        recurrencePattern.nth_occurrence = [1, 3];
655    } else if (recurringPattern === 'nth_2nd_4th') {
656        recurrencePattern.frequency = 'monthly';
657        recurrencePattern.nth_occurrence = [2, 4];
658    } else if (recurringPattern.startsWith('nth_')) {
659        // Handle nth-day patterns (e.g., 'nth_2nd' -> 2nd Monday of month)
660        recurrencePattern.frequency = 'monthly';
661        const nthMap = {
662            'nth_1st': 1,
663            'nth_2nd': 2,
664            'nth_3rd': 3,
665            'nth_4th': 4
666        };
667        recurrencePattern.nth_occurrence = nthMap[recurringPattern];
668    }
669    
670    submissionData.recurrence_pattern = recurrencePattern;
671    
672    console.log('[DEBUG] Submission data:', submissionData);
673    
674    const submitBtn = document.getElementById('jamSubmitBtn');
675    const originalText = submitBtn.textContent;
676    submitBtn.disabled = true;
677    submitBtn.textContent = 'Submitting...';
678    
679    try {
680        const accessToken = localStorage.getItem('cognitoAccessToken');
681        
682        if (!accessToken) {
683            displayFormErrors(['You must be logged in to submit a jam.']);
684            return;
685        }
686        
687        const response = await fetch(`${API_BASE_URL}/jams/submit`, {
688            method: 'POST',
689            headers: {
690                'Content-Type': 'application/json',
691                'Authorization': 'Bearer ' + accessToken
692            },
693            body: JSON.stringify(submissionData)
694        });
695        
696        const data = await response.json();
697        
698        if (response.ok) {
699            showToast('Jam submitted successfully! Our team will review your submission.', 'success');
700            hideJamSubmissionForm();
701            
702            if (typeof loadDashboard === 'function') {
703                loadDashboard();
704            }
705        } else {
706            // Handle validation errors from backend
707            if (data.validation_errors) {
708                // Convert validation errors object to array with field names
709                const errorMessages = Object.entries(data.validation_errors).map(
710                    ([field, message]) => `${field}: ${message}`
711                );
712                displayFormErrors(errorMessages);
713            } else if (data.error) {
714                if (data.error.includes('rate limit')) {
715                    displayFormErrors([data.error]);
716                } else if (data.error.includes('duplicate')) {
717                    displayFormErrors([data.error]);
718                } else if (data.error.includes('geocoding')) {
719                    displayFormErrors(['Unable to geocode the venue address. Please check the address and try again.']);
720                } else {
721                    displayFormErrors([data.error]);
722                }
723            } else {
724                displayFormErrors(['Failed to submit jam. Please try again.']);
725            }
726        }
727    } catch (error) {
728        console.error('Error submitting jam:', error);
729        displayFormErrors(['Network error. Please check your connection and try again.']);
730    } finally {
731        submitBtn.disabled = false;
732        submitBtn.textContent = originalText;
733    }
734}
735
736
737// Edit existing jam
738window.editJam = async function(jamId) {
739    console.log('[JAM-FORM] Edit jam called for:', jamId);
740    
741    try {
742        // Fetch the jam data
743        const jams = window.dashboardData?.jams || [];
744        const jam = jams.find(j => j.jam_id === jamId);
745        
746        if (!jam) {
747            alert('Jam not found');
748            return;
749        }
750        
751        // Close jam dashboard
752        closeModal('jamDashboardModal');
753        
754        // Open jam submission modal
755        document.getElementById('jamSubmissionModal').style.display = 'block';
756        
757        // Store jam_id for update
758        document.getElementById('jamSubmissionForm').dataset.editingJamId = jamId;
759        
760        // Pre-populate form fields
761        // Note: jam_name is now hidden and always "Open Jam"
762        
763        // Venue - if we have venue_id, select it
764        if (jam.venue_id) {
765            selectedVenue = {
766                venue_id: jam.venue_id,
767                venue_name: jam.venue_name,
768                city: jam.venue_city,
769                state: jam.venue_state,
770                address: jam.venue_address,
771                zip_code: jam.venue_zip
772            };
773            
774            // Show selected venue display
775            document.getElementById('selectedVenueName').textContent = jam.venue_name;
776            document.getElementById('selectedVenueLocation').textContent = jam.venue_state ? `${jam.venue_city}, ${jam.venue_state}` : jam.venue_city;
777            document.getElementById('selectedVenueDisplay').style.display = 'flex';
778            document.getElementById('venueSearch').style.display = 'none';
779            hideNewVenueSection();
780        }
781        
782        // Recurrence pattern
783        const pattern = jam.recurrence_pattern || {};
784        if (pattern.frequency) {
785            // Map backend frequency to frontend dropdown value
786            let dropdownValue = pattern.frequency;
787            
788            // If it's monthly with nth_occurrence, map to appropriate format
789            if (pattern.frequency === 'monthly' && pattern.nth_occurrence) {
790                const nthOcc = pattern.nth_occurrence;
791                // Handle array (multi-occurrence: 1st & 3rd, 2nd & 4th)
792                if (Array.isArray(nthOcc)) {
793                    if (nthOcc.includes(1) && nthOcc.includes(3)) {
794                        dropdownValue = 'nth_1st_3rd';
795                    } else if (nthOcc.includes(2) && nthOcc.includes(4)) {
796                        dropdownValue = 'nth_2nd_4th';
797                    } else {
798                        dropdownValue = 'monthly';
799                    }
800                } else {
801                    const nthMap = {
802                        1: 'nth_1st',
803                        2: 'nth_2nd',
804                        3: 'nth_3rd',
805                        4: 'nth_4th'
806                    };
807                    dropdownValue = nthMap[nthOcc] || 'monthly';
808                }
809            } else if (pattern.frequency === 'monthly' && pattern.week_of_month) {
810                // Also handle week_of_month field (alternate name)
811                const wom = pattern.week_of_month;
812                if (Array.isArray(wom)) {
813                    if (wom.includes(1) && wom.includes(3)) {
814                        dropdownValue = 'nth_1st_3rd';
815                    } else if (wom.includes(2) && wom.includes(4)) {
816                        dropdownValue = 'nth_2nd_4th';
817                    } else {
818                        dropdownValue = 'monthly';
819                    }
820                } else {
821                    const nthMap = {
822                        1: 'nth_1st',
823                        2: 'nth_2nd',
824                        3: 'nth_3rd',
825                        4: 'nth_4th'
826                    };
827                    dropdownValue = nthMap[parseInt(wom)] || 'monthly';
828                }
829            } else if (pattern.frequency === 'weekly' && pattern.interval === 2) {
830                // Biweekly is stored as weekly with interval 2
831                dropdownValue = 'biweekly';
832            }
833            
834            document.getElementById('recurringPattern').value = dropdownValue;
835        }
836        
837        // Day of week - convert number to day name
838        if (pattern.day_of_week !== undefined) {
839            const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
840            const dayName = dayNames[pattern.day_of_week];
841            if (dayName) {
842                document.getElementById('dayOfWeek').value = dayName;
843            }
844        }
845        
846        // Times
847        if (jam.start_time) {
848            document.getElementById('startTime').value = jam.start_time;
849        }
850        if (jam.end_time) {
851            document.getElementById('endTime').value = jam.end_time;
852        }
853        
854        // Start date
855        if (jam.start_date || pattern.start_date) {
856            document.getElementById('startDate').value = jam.start_date || pattern.start_date;
857        }
858        
859        // End date (optional)
860        if (jam.end_date || pattern.end_date) {
861            document.getElementById('endDate').value = jam.end_date || pattern.end_date;
862        }
863        
864        // Link
865        if (jam.link) {
866            document.getElementById('jamLink').value = jam.link;
867        }
868        
869        // Description (not additional_info)
870        if (jam.description) {
871            document.getElementById('jamDescription').value = jam.description;
872            // Update character counter
873            updateDescriptionCounter();
874        }
875        
876        // Update submit button text
877        const submitBtn = document.querySelector('#jamSubmissionForm button[type="submit"]');
878        if (submitBtn) {
879            submitBtn.textContent = 'Update Jam';
880        }
881        
882        // Update modal title
883        const modalTitle = document.querySelector('#jamSubmissionModal h2');
884        if (modalTitle) {
885            modalTitle.textContent = 'Edit Community Jam';
886        }
887        
888    } catch (error) {
889        console.error('[JAM-FORM] Error editing jam:', error);
890        alert('Failed to load jam data for editing');
891    }
892};
893
894
895
896// Submit jam update
897async function submitJamUpdate(jamId) {
898    console.log('[JAM-FORM] Submitting jam update for:', jamId);
899    
900    const submitBtn = document.querySelector('#jamSubmissionForm button[type="submit"]');
901    const originalText = submitBtn.textContent;
902    
903    try {
904        submitBtn.disabled = true;
905        submitBtn.textContent = 'Updating...';
906        
907        console.log('[JAM-FORM] Button disabled, clearing errors...');
908        
909        // Clear previous errors
910        clearFormErrors();
911        
912        console.log('[JAM-FORM] Validating form...');
913        
914        // Validate form
915        const errors = validateJamForm();
916        if (errors.length > 0) {
917            console.log('[JAM-FORM] Validation errors:', errors);
918            displayFormErrors(errors);
919            return;
920        }
921        
922        console.log('[JAM-FORM] Validation passed, collecting form data...');
923        
924        // Convert day name to number (0=Sunday, 1=Monday, ..., 6=Saturday)
925        const dayNameToNumber = {
926            'Sunday': 0,
927            'Monday': 1,
928            'Tuesday': 2,
929            'Wednesday': 3,
930            'Thursday': 4,
931            'Friday': 5,
932            'Saturday': 6
933        };
934        const dayOfWeek = document.getElementById('dayOfWeek').value;
935        const dayOfWeekNumber = dayNameToNumber[dayOfWeek];
936        
937        const recurringPattern = document.getElementById('recurringPattern').value;
938        const startDate = document.getElementById('startDate').value;
939        
940        // Build recurrence_pattern
941        const recurrencePattern = {
942            start_date: startDate,
943            day_of_week: dayOfWeekNumber
944        };
945        
946        // Handle different pattern types
947        if (recurringPattern === 'weekly') {
948            recurrencePattern.frequency = 'weekly';
949            recurrencePattern.interval = 1;
950        } else if (recurringPattern === 'biweekly') {
951            recurrencePattern.frequency = 'weekly';
952            recurrencePattern.interval = 2;
953        } else if (recurringPattern === 'monthly') {
954            recurrencePattern.frequency = 'monthly';
955        } else if (recurringPattern === 'nth_1st_3rd') {
956            recurrencePattern.frequency = 'monthly';
957            recurrencePattern.nth_occurrence = [1, 3];
958        } else if (recurringPattern === 'nth_2nd_4th') {
959            recurrencePattern.frequency = 'monthly';
960            recurrencePattern.nth_occurrence = [2, 4];
961        } else if (recurringPattern.startsWith('nth_')) {
962            // Handle nth-day patterns (e.g., 'nth_2nd' -> 2nd Monday of month)
963            recurrencePattern.frequency = 'monthly';
964            const nthMap = {
965                'nth_1st': 1,
966                'nth_2nd': 2,
967                'nth_3rd': 3,
968                'nth_4th': 4
969            };
970            recurrencePattern.nth_occurrence = nthMap[recurringPattern];
971        }
972        
973        // Collect form data
974        const formData = {
975            jam_id: jamId,
976            recurrence_pattern: recurrencePattern,
977            start_time: document.getElementById('startTime').value,
978            end_time: document.getElementById('endTime').value,
979            start_date: startDate,
980            link: document.getElementById('jamLink').value,
981            description: document.getElementById('jamDescription').value.trim() || null
982        };
983        
984        // Add optional end date if provided
985        const endDate = document.getElementById('endDate').value;
986        if (endDate) {
987            formData.end_date = endDate;
988            recurrencePattern.end_date = endDate;
989        }
990        
991        // Add venue data (either venue_id or new_venue)
992        const newVenueSection = document.getElementById('newVenueSection');
993        
994        if (selectedVenue) {
995            // Existing venue - send venue_id
996            formData.venue_id = selectedVenue.venue_id;
997        } else if (newVenueSection.style.display !== 'none') {
998            // New venue - send new_venue object
999            const stateValue = document.getElementById('newVenueState').value;
1000            formData.new_venue = {
1001                venue_name: document.getElementById('newVenueName').value.trim(),
1002                address: document.getElementById('newVenueAddress').value.trim(),
1003                city: document.getElementById('newVenueCity').value.trim(),
1004                state: stateValue === 'OTHER' ? '' : stateValue,
1005                zip: document.getElementById('newVenueZip').value.trim()
1006            };
1007        } else {
1008            displayFormErrors(['Please select a venue or create a new one']);
1009            return;
1010        }
1011        
1012        console.log('[JAM-FORM] Update data:', formData);
1013        
1014        console.log('[JAM-FORM] Getting access token...');
1015        
1016        // Submit to API
1017        const accessToken = localStorage.getItem('cognitoAccessToken');
1018        
1019        if (!accessToken) {
1020            console.log('[JAM-FORM] No access token found');
1021            displayFormErrors(['You must be logged in to update a jam.']);
1022            return;
1023        }
1024        
1025        console.log('[JAM-FORM] Sending update request to API...');
1026        
1027        const response = await fetch(`${API_BASE_URL}/jams/update`, {
1028            method: 'POST',
1029            headers: {
1030                'Content-Type': 'application/json',
1031                'Authorization': 'Bearer ' + accessToken
1032            },
1033            body: JSON.stringify(formData)
1034        });
1035        
1036        console.log('[JAM-FORM] Response received:', response.status);
1037        
1038        const data = await response.json();
1039        
1040        console.log('[JAM-FORM] Response data:', data);
1041        
1042        if (response.ok && data.success) {
1043            console.log('[JAM-FORM] Update successful!');
1044            showToast('Jam updated successfully! Your changes will be reviewed.', 'success');
1045            
1046            // Close modal and reset form
1047            closeModal('jamSubmissionModal');
1048            resetJamForm();
1049            delete document.getElementById('jamSubmissionForm').dataset.editingJamId;
1050            
1051            // Reload jams in dashboard
1052            if (typeof loadUserJams === 'function') {
1053                await loadUserJams();
1054            }
1055            
1056            // Show jam dashboard
1057            if (typeof showJamDashboard === 'function') {
1058                showJamDashboard();
1059            }
1060        } else {
1061            // Handle validation errors from backend
1062            if (data.validation_errors) {
1063                // Convert validation errors object to array with field names
1064                const errorMessages = Object.entries(data.validation_errors).map(
1065                    ([field, message]) => `${field}: ${message}`
1066                );
1067                displayFormErrors(errorMessages);
1068            } else if (data.details) {
1069                // Handle validation details from backend
1070                const errorMessages = Object.entries(data.details).map(
1071                    ([field, message]) => `${field}: ${message}`
1072                );
1073                displayFormErrors(errorMessages);
1074            } else {
1075                displayFormErrors([data.error || 'Failed to update jam. Please try again.']);
1076            }
1077        }
1078        
1079    } catch (error) {
1080        console.error('[JAM-FORM] Error updating jam:', error);
1081        displayFormErrors(['Network error. Please check your connection and try again.']);
1082    } finally {
1083        submitBtn.disabled = false;
1084        submitBtn.textContent = originalText;
1085    }
1086}

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.