1// Report Issue functionality for leaderboard 2 3// Setup report issue functionality 4function setupReportIssue() { 5 const reportButton = document.getElementById('reportIssueText'); 6 const modal = document.getElementById('reportIssueModal'); 7 const closeButton = document.getElementById('closeReportModal'); 8 const cancelButton = document.getElementById('cancelReportModal'); 9 const form = document.getElementById('reportIssueForm'); 10 const systemInfoField = document.getElementById('systemInfo'); 11 12 if (!reportButton || !modal || !form) { 13 console.warn('Report issue elements not found'); 14 return; 15 } 16 17 // Populate system info 18 populateSystemInfo(); 19 20 // Event listeners 21 reportButton.addEventListener('click', openReportModal); 22 closeButton?.addEventListener('click', closeReportModal); 23 cancelButton?.addEventListener('click', () => { form.reset(); populateSystemInfo(); closeReportModal(); }); 24 form.addEventListener('submit', handleReportSubmission); 25 26 // Close modal when clicking outside 27 modal.addEventListener('click', (e) => { 28 if (e.target === modal) { 29 closeReportModal(); 30 } 31 }); 32 33 // Close modal on escape key press (with saving) 34 document.addEventListener('keydown', (e) => { 35 if (e.key === 'Escape' && modal.style.display === 'flex') { 36 closeReportModal(); 37 } 38 }); 39} 40 41// Open the report issue modal 42function openReportModal() { 43 const modal = document.getElementById('reportIssueModal'); 44 if (modal) { 45 modal.style.display = 'flex'; 46 document.body.classList.add('modal-open'); 47 48 // Focus on the first input 49 const firstInput = modal.querySelector('select, input, textarea'); 50 if (firstInput) { 51 setTimeout(() => firstInput.focus(), 100); 52 } 53 } 54} 55 56// Close the report issue modal 57function closeReportModal() { 58 const modal = document.getElementById('reportIssueModal'); 59 if (modal) { 60 modal.style.display = 'none'; 61 document.body.classList.remove('modal-open'); 62 } 63} 64 65// Handle form submission 66async function handleReportSubmission(event) { 67 event.preventDefault(); 68 69 const submitBtn = document.getElementById('submitIssueBtn'); 70 const originalBtnContent = submitBtn.innerHTML; 71 72 try { 73 // Show loading state 74 submitBtn.disabled = true; 75 submitBtn.innerHTML = '<span class="icon"><i class="fa-solid fa-spinner fa-spin"></i></span><span>Submitting...</span>'; 76 77 // Collect form data 78 const formData = collectFormData(); 79 80 // Validate required fields 81 if (!validateFormData(formData)) { 82 throw new Error('Please fill in all required fields'); 83 } 84 85 // Submit to backend 86 const response = await submitIssueToBackend(formData); 87 88 if (response.success) { 89 showSuccessMessage(response.issue_url || 'Issue submitted successfully!'); 90 const form = document.getElementById('reportIssueForm'); 91 form.reset(); 92 populateSystemInfo(); 93 closeReportModal(); 94 } else { 95 throw new Error(response.error || 'Failed to submit issue'); 96 } 97 98 } catch (error) { 99 console.error('Error submitting issue:', error); 100 showErrorMessage(error.message || 'Failed to submit issue. Please try again.'); 101 } finally { 102 // Reset button state 103 submitBtn.disabled = false; 104 submitBtn.innerHTML = originalBtnContent; 105 } 106} 107 108// Collect form data 109function collectFormData() { 110 return { 111 issue_type: document.getElementById('issueType').value, 112 title: document.getElementById('issueTitle').value.trim(), 113 description: document.getElementById('issueDescription').value.trim(), 114 system_info: document.getElementById('systemInfo').value, 115 contact_email: document.getElementById('contactEmail').value.trim(), 116 page_url: window.location.href, 117 user_agent: navigator.userAgent, 118 timestamp: new Date().toISOString(), 119 // Include current filter state for context 120 filter_state: getCurrentFilterState() 121 }; 122} 123 124// Validate form data 125function validateFormData(data) { 126 if (!data.issue_type) { 127 showFieldError('issueType', 'Please select an issue type'); 128 return false; 129 } 130 131 if (!data.title || data.title.length < 10) { 132 showFieldError('issueTitle', 'Please provide a descriptive title (at least 10 characters)'); 133 return false;
134 } 135 136 if (!data.description || data.description.length < 20) { 137 showFieldError('issueDescription', 'Please provide a detailed description (at least 20 characters)'); 138 return false; 139 } 140 141 return true; 142} 143 144// Show field-specific error 145function showFieldError(fieldId, message) { 146 const field = document.getElementById(fieldId); 147 if (field) { 148 field.classList.add('is-danger'); 149 150 // Remove existing error message 151 const existingError = field.parentNode.querySelector('.help.is-danger'); 152 if (existingError) { 153 existingError.remove(); 154 } 155 156 // Add error message 157 const errorElement = document.createElement('p'); 158 errorElement.className = 'help is-danger'; 159 errorElement.textContent = message; 160 field.parentNode.appendChild(errorElement); 161 162 // Remove error styling after user starts typing 163 field.addEventListener('input', function clearError() { 164 field.classList.remove('is-danger'); 165 const errorMsg = field.parentNode.querySelector('.help.is-danger'); 166 if (errorMsg) { 167 errorMsg.remove(); 168 } 169 field.removeEventListener('input', clearError); 170 }, { once: true }); 171 } 172} 173 174// Submit issue to backend 175async function submitIssueToBackend(formData) { 176 try { 177 const response = await fetch('/report-issue/', { 178 method: 'POST', 179 headers: { 180 'Content-Type': 'application/json', 181 'X-CSRFToken': getCsrfToken(), 182 }, 183 body: JSON.stringify(formData) 184 }); 185 186 if (!response.ok) { 187 const errorData = await response.json().catch(() => ({})); 188 throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); 189 } 190 191 return await response.json(); 192 } catch (error) { 193 console.error('Backend submission error:', error); 194 throw error; 195 } 196} 197 198// Get CSRF token 199function getCsrfToken() { 200 const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value || 201 document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || 202 getCsrfFromCookie(); 203 204 if (!token) { 205 console.warn('CSRF token not found'); 206 } 207 208 return token || ''; 209} 210 211// Get CSRF token from cookie (Django default) 212function getCsrfFromCookie() { 213 const name = 'csrftoken'; 214 const cookies = document.cookie.split(';'); 215 216 for (let cookie of cookies) { 217 const [key, value] = cookie.trim().split('='); 218 if (key === name) { 219 return decodeURIComponent(value); 220 } 221 } 222 223 return null; 224} 225 226// Get current filter state for context 227function getCurrentFilterState() { 228 try { 229 const activeFilters = window.activeFilters || {}; 230 const searchTerm = document.getElementById('modelSearchInput')?.value || ''; 231 232 return { 233 search_term: searchTerm, 234 active_filters: activeFilters, 235 visible_rows: window.globalGridApi ? getVisibleRowCount() : 'unknown' 236 }; 237 } catch (error) { 238 console.warn('Could not collect filter state:', error); 239 return { error: 'Could not collect filter state' }; 240 } 241} 242 243// Get count of visible rows in grid 244function getVisibleRowCount() { 245 if (!window.globalGridApi) return 'unknown'; 246 247 let count = 0; 248 window.globalGridApi.forEachNodeAfterFilterAndSort(() => count++); 249 return count; 250} 251 252// Populate system information 253function populateSystemInfo() { 254 const systemInfoField = document.getElementById('systemInfo'); 255 if (!systemInfoField) return; 256 257 const info = [ 258 `Browser: ${navigator.userAgent}`, 259 `Screen: ${screen.width}x${screen.height}`, 260 `Viewport: ${window.innerWidth}x${window.innerHeight}`, 261 `Platform: ${navigator.platform}`, 262 `Language: ${navigator.language}`, 263 `Timestamp: ${new Date().toISOString()}` 264 ].join(' | '); 265 266 systemInfoField.value = info; 267} 268 269// Show success message 270function showSuccessMessage(message) { 271 // Create success notification 272 const notification = document.createElement('div'); 273 notification.className = 'notification is-success is-light'; 274 notification.style.cssText = ` 275 position: fixed; 276 top: 20px; 277 right: 20px; 278 z-index: 9999; 279 max-width: 400px; 280 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 281 `; 282 283 notification.innerHTML = ` 284 <button class="delete"></button> 285 <strong>Success!</strong><br> 286 ${typeof message === 'string' && message.startsWith('http') ? 287 `<a href="${message}" target="_blank" class="has-text-link">View issue on GitHub</a>` : 288 message} 289 `; 290 291 document.body.appendChild(notification); 292 293 // Auto-remove after 5 seconds 294 setTimeout(() => { 295 if (notification.parentNode) { 296 notification.parentNode.removeChild(notification); 297 } 298 }, 5000); 299 300 // Manual close 301 const deleteBtn = notification.querySelector('.delete'); 302 if (deleteBtn) { 303 deleteBtn.addEventListener('click', () => { 304 if (notification.parentNode) { 305 notification.parentNode.removeChild(notification); 306 } 307 }); 308 } 309} 310 311// Show error message 312function showErrorMessage(message) { 313 // Create error notification 314 const notification = document.createElement('div'); 315 notification.className = 'notification is-danger is-light';
316 notification.style.cssText = ` 317 position: fixed; 318 top: 20px; 319 right: 20px; 320 z-index: 9999; 321 max-width: 400px; 322 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 323 `; 324 325 notification.innerHTML = ` 326 <button class="delete"></button> 327 <strong>Error!</strong><br> 328 ${message} 329 `; 330 331 document.body.appendChild(notification); 332 333 // Auto-remove after 8 seconds (longer for errors) 334 setTimeout(() => { 335 if (notification.parentNode) { 336 notification.parentNode.removeChild(notification); 337 } 338 }, 8000); 339 340 // Manual close 341 const deleteBtn = notification.querySelector('.delete'); 342 if (deleteBtn) { 343 deleteBtn.addEventListener('click', () => { 344 if (notification.parentNode) { 345 notification.parentNode.removeChild(notification); 346 } 347 }); 348 } 349} 350 351 352 353// Export functions for use by other modules 354window.LeaderboardReportIssue = { 355 setupReportIssue, 356 openReportModal, 357 closeReportModal, 358 handleReportSubmission, 359 collectFormData, 360 validateFormData, 361 submitIssueToBackend, 362 showSuccessMessage, 363 showErrorMessage 364};
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.