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.