Spaces:
Runtime error
Runtime error
| /** | |
| * UI Manager Module | |
| * Handles user interface updates, messages, and tree list display | |
| */ | |
| export class UIManager { | |
| constructor(authManager) { | |
| this.authManager = authManager; | |
| } | |
| initialize() { | |
| this.displayUserInfo(); | |
| this.loadSelectedLocation(); | |
| this.setupDemoUserUI(); | |
| } | |
| displayUserInfo() { | |
| if (!this.authManager.currentUser) return; | |
| const userNameEl = document.getElementById('userName'); | |
| const userRoleEl = document.getElementById('userRole'); | |
| const userAvatarEl = document.getElementById('userAvatar'); | |
| if (userNameEl) { | |
| userNameEl.textContent = this.authManager.currentUser.full_name; | |
| } | |
| if (userRoleEl) { | |
| userRoleEl.textContent = this.authManager.currentUser.role; | |
| } | |
| if (userAvatarEl) { | |
| userAvatarEl.textContent = this.authManager.currentUser.full_name.charAt(0).toUpperCase(); | |
| } | |
| } | |
| loadSelectedLocation() { | |
| const selectedLocation = localStorage.getItem('selectedLocation'); | |
| if (selectedLocation) { | |
| try { | |
| const location = JSON.parse(selectedLocation); | |
| const latElement = document.getElementById('latitude'); | |
| const lngElement = document.getElementById('longitude'); | |
| if (latElement && lngElement) { | |
| latElement.value = location.lat.toFixed(6); | |
| lngElement.value = location.lng.toFixed(6); | |
| // Clear the stored location | |
| localStorage.removeItem('selectedLocation'); | |
| this.showMessage('Location loaded from map!', 'success'); | |
| } | |
| } catch (error) { | |
| console.error('Error loading selected location:', error); | |
| } | |
| } | |
| } | |
| showMessage(message, type) { | |
| const messageDiv = document.getElementById('message'); | |
| if (!messageDiv) return; | |
| messageDiv.className = `message ${type === 'error' ? 'error' : 'success'}`; | |
| messageDiv.textContent = message; | |
| // Auto-hide after 5 seconds | |
| setTimeout(() => { | |
| messageDiv.textContent = ''; | |
| messageDiv.className = ''; | |
| }, 5000); | |
| } | |
| renderTreeList(trees) { | |
| const treeList = document.getElementById('treeList'); | |
| if (!treeList) return; | |
| if (trees.length === 0) { | |
| treeList.innerHTML = '<div class="loading">No trees recorded yet</div>'; | |
| return; | |
| } | |
| // Compute display numbers for Ishita's trees created today | |
| const now = new Date(); | |
| const y = now.getFullYear(), m = now.getMonth(), d = now.getDate(); | |
| const isToday = (ts) => { try { const t=new Date(ts); return t.getFullYear()===y && t.getMonth()===m && t.getDate()===d; } catch(_) { return false; } }; | |
| const ishitaToday = trees.filter(t => (t.created_by||'').toLowerCase()==='ishita' && isToday(t.created_at)); | |
| // Sort by created_at ascending to assign small to early ones | |
| ishitaToday.sort((a,b) => new Date(a.created_at) - new Date(b.created_at)); | |
| const ishitaIndex = new Map(); | |
| ishitaToday.forEach((t, idx) => ishitaIndex.set(t.id, idx+1)); | |
| treeList.innerHTML = trees.map(tree => { | |
| const canEdit = this.authManager.canEditTree(tree.created_by); | |
| const canDelete = this.authManager.canDeleteTree(tree.created_by); | |
| return ` | |
| <div class="tree-item" data-tree-id="${tree.id}"> | |
| <div class="tree-header"> | |
| <div class="tree-id">Tree #${tree.id}${ishitaIndex.has(tree.id) ? ` (Ishita No. ${ishitaIndex.get(tree.id)})` : ''}</div> | |
| <div class="tree-actions"> | |
| ${canEdit ? `<button class="btn-icon edit-tree" data-tree-id="${tree.id}" title="Edit Tree">Edit</button>` : ''} | |
| ${canDelete ? `<button class="btn-icon delete-tree" data-tree-id="${tree.id}" title="Delete Tree">Delete</button>` : ''} | |
| </div> | |
| </div> | |
| <div class="tree-info"> | |
| ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'} | |
| ${tree.location_name ? `<br><strong>Location:</strong> ${this.escapeHtml(tree.location_name)}` : ''} | |
| <br>${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)} | |
| ${tree.tree_code ? `<br>Code: ${tree.tree_code}` : ''} | |
| <br>${new Date(tree.created_at).toLocaleDateString()} | |
| <br>By: ${tree.created_by || 'Unknown'} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| escapeHtml(text) { | |
| if (typeof text !== 'string') return text; | |
| return text | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| showLoadingState(containerId, message = 'Loading...') { | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| container.innerHTML = ` | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| ${message} | |
| </div> | |
| `; | |
| } | |
| showErrorState(containerId, message = 'Error loading content') { | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| container.innerHTML = `<div class="loading">${message}</div>`; | |
| } | |
| updateLocationButtonState(isGetting) { | |
| const locationBtn = document.getElementById('getLocation'); | |
| if (!locationBtn) return; | |
| locationBtn.textContent = isGetting ? 'Getting...' : 'Get GPS Location'; | |
| locationBtn.disabled = isGetting; | |
| } | |
| highlightAutoFilledField(fieldId) { | |
| const input = document.getElementById(fieldId); | |
| if (input && !input.value.trim()) { | |
| input.style.backgroundColor = '#f0f9ff'; | |
| setTimeout(() => { | |
| input.style.backgroundColor = ''; | |
| }, 2000); | |
| } | |
| } | |
| createFileInput(accept, capture = false) { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = accept; | |
| if (capture) { | |
| input.capture = 'environment'; | |
| } | |
| return input; | |
| } | |
| confirmDeletion(treeId) { | |
| return confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`); | |
| } | |
| scrollToTop() { | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| focusFirstError() { | |
| const firstError = document.querySelector('.form-input.error, .message.error'); | |
| if (firstError) { | |
| firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| if (firstError.focus) { | |
| firstError.focus(); | |
| } | |
| } | |
| } | |
| addFieldError(fieldId, message) { | |
| const field = document.getElementById(fieldId); | |
| if (!field) return; | |
| field.classList.add('error'); | |
| // Remove existing error message | |
| const existingError = field.parentNode.querySelector('.field-error'); | |
| if (existingError) { | |
| existingError.remove(); | |
| } | |
| // Add error message | |
| const errorEl = document.createElement('div'); | |
| errorEl.className = 'field-error'; | |
| errorEl.textContent = message; | |
| field.parentNode.appendChild(errorEl); | |
| } | |
| clearFieldErrors() { | |
| document.querySelectorAll('.form-input.error').forEach(field => { | |
| field.classList.remove('error'); | |
| }); | |
| document.querySelectorAll('.field-error').forEach(error => { | |
| error.remove(); | |
| }); | |
| } | |
| showUploadProgress(filename, progress) { | |
| // This could be expanded for actual progress tracking | |
| console.log(`Uploading ${filename}: ${progress}%`); | |
| } | |
| setupDemoUserUI() { | |
| if (!this.authManager.isDemoUser()) { | |
| return; // Not a demo user, no changes needed | |
| } | |
| // Add demo notice at the top of the form | |
| this.addDemoNotice(); | |
| // Disable submit button and update its appearance | |
| this.disableSubmitButtonForDemo(); | |
| // Show welcome button for demo users | |
| this.showWelcomeButton(); | |
| } | |
| addDemoNotice() { | |
| const formCard = document.querySelector('.tt-card .tt-card-content'); | |
| if (!formCard) return; | |
| const demoNotice = document.createElement('div'); | |
| demoNotice.className = 'demo-notice'; | |
| demoNotice.innerHTML = ` | |
| <div class="demo-notice-icon">i</div> | |
| <div> | |
| <strong>Demo Mode</strong> - You're exploring TreeTrack! | |
| Feel free to test all features and fill out the form. | |
| <em>Note: Data won't be permanently saved in this demo environment.</em> | |
| </div> | |
| `; | |
| // Insert before the form | |
| const form = document.getElementById('treeForm'); | |
| if (form) { | |
| formCard.insertBefore(demoNotice, form); | |
| } | |
| } | |
| disableSubmitButtonForDemo() { | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| if (!submitBtn) return; | |
| // Update button text and styling | |
| submitBtn.textContent = 'Try TreeTrack - Demo Mode'; | |
| submitBtn.classList.add('tt-btn-demo-disabled'); | |
| submitBtn.disabled = true; | |
| // Add tooltip on hover | |
| submitBtn.title = 'This is a demo environment - data won\'t be permanently saved'; | |
| // Override the form submission to show demo message | |
| submitBtn.setAttribute('data-demo-disabled', 'true'); | |
| } | |
| isDemoButtonDisabled() { | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| return submitBtn && submitBtn.getAttribute('data-demo-disabled') === 'true'; | |
| } | |
| showWelcomeButton() { | |
| const welcomeBtn = document.getElementById('welcomeBtn'); | |
| if (welcomeBtn) { | |
| welcomeBtn.style.display = 'inline-flex'; | |
| } | |
| } | |
| } | |