Spaces:
Paused
Paused
| /** | |
| * ICRA26 Workshop - Main JavaScript | |
| * Shared utilities for all pages | |
| */ | |
| // Common utility functions | |
| const Utils = { | |
| /** | |
| * Format bytes to human readable string | |
| */ | |
| formatBytes(bytes) { | |
| if (bytes < 1024) return bytes + ' B'; | |
| if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; | |
| return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; | |
| }, | |
| /** | |
| * Format date to local string | |
| */ | |
| formatDate(dateString) { | |
| return new Date(dateString).toLocaleString(); | |
| }, | |
| /** | |
| * Escape HTML to prevent XSS | |
| */ | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| }, | |
| /** | |
| * Show notification message | |
| */ | |
| showNotification(message, type = 'info', duration = 5000) { | |
| const container = document.getElementById('notification-container'); | |
| if (!container) return; | |
| const notification = document.createElement('div'); | |
| notification.className = `notification notification-${type}`; | |
| notification.innerHTML = ` | |
| <span class="notification-message">${message}</span> | |
| <button class="notification-close">×</button> | |
| `; | |
| container.appendChild(notification); | |
| // Close button handler | |
| notification.querySelector('.notification-close').addEventListener('click', () => { | |
| notification.remove(); | |
| }); | |
| // Auto-remove after duration | |
| if (duration > 0) { | |
| setTimeout(() => notification.remove(), duration); | |
| } | |
| }, | |
| /** | |
| * Debounce function | |
| */ | |
| debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| }, | |
| /** | |
| * Check if element is in viewport | |
| */ | |
| isInViewport(element) { | |
| const rect = element.getBoundingClientRect(); | |
| return ( | |
| rect.top >= 0 && | |
| rect.left >= 0 && | |
| rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && | |
| rect.right <= (window.innerWidth || document.documentElement.clientWidth) | |
| ); | |
| } | |
| }; | |
| // API helper | |
| const API = { | |
| async get(url) { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return response.json(); | |
| }, | |
| async post(url, data) { | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(data), | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json().catch(() => ({ detail: 'Request failed' })); | |
| throw new Error(error.detail || 'Request failed'); | |
| } | |
| return response.json(); | |
| }, | |
| async upload(url, file, onProgress) { | |
| return new Promise((resolve, reject) => { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open('POST', url); | |
| xhr.upload.addEventListener('progress', (e) => { | |
| if (e.lengthComputable && onProgress) { | |
| onProgress((e.loaded / e.total) * 100); | |
| } | |
| }); | |
| xhr.addEventListener('load', () => { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| resolve(JSON.parse(xhr.responseText)); | |
| } else { | |
| try { | |
| const error = JSON.parse(xhr.responseText); | |
| reject(new Error(error.detail || 'Upload failed')); | |
| } catch { | |
| reject(new Error('Upload failed')); | |
| } | |
| } | |
| }); | |
| xhr.addEventListener('error', () => reject(new Error('Network error'))); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| xhr.send(formData); | |
| }); | |
| } | |
| }; | |
| // Auto-refresh helper for leaderboard | |
| class AutoRefresh { | |
| constructor(fetchFn, interval = 30000) { | |
| this.fetchFn = fetchFn; | |
| this.interval = interval; | |
| this.isActive = true; | |
| } | |
| start() { | |
| this.isActive = true; | |
| this.fetchFn(); | |
| this.timer = setInterval(() => { | |
| if (this.isActive) { | |
| this.fetchFn(); | |
| } | |
| }, this.interval); | |
| } | |
| stop() { | |
| this.isActive = false; | |
| if (this.timer) { | |
| clearInterval(this.timer); | |
| } | |
| } | |
| refresh() { | |
| if (this.isActive) { | |
| this.fetchFn(); | |
| } | |
| } | |
| } | |
| // Status polling helper | |
| class StatusPoller { | |
| constructor(submissionId, onUpdate, onComplete, interval = 3000) { | |
| this.submissionId = submissionId; | |
| this.onUpdate = onUpdate; | |
| this.onComplete = onComplete; | |
| this.interval = interval; | |
| this.timer = null; | |
| } | |
| start() { | |
| const poll = async () => { | |
| try { | |
| const response = await fetch(`/api/status/${this.submissionId}`); | |
| const data = await response.json(); | |
| this.onUpdate(data); | |
| if (data.status === 'completed' || data.status === 'error') { | |
| this.stop(); | |
| if (this.onComplete) { | |
| this.onComplete(data); | |
| } | |
| } else { | |
| this.timer = setTimeout(poll, this.interval); | |
| } | |
| } catch (error) { | |
| console.error('Polling error:', error); | |
| this.timer = setTimeout(poll, this.interval); | |
| } | |
| }; | |
| poll(); | |
| } | |
| stop() { | |
| if (this.timer) { | |
| clearTimeout(this.timer); | |
| this.timer = null; | |
| } | |
| } | |
| } | |
| // Export for use in inline scripts | |
| window.Utils = Utils; | |
| window.API = API; | |
| window.AutoRefresh = AutoRefresh; | |
| window.StatusPoller = StatusPoller; | |