/** * Project Selector Component * Reusable dropdown for selecting active project context in admin pages * * Features: * - Loads active projects from API * - Persists selection to localStorage * - Emits change events * - Supports callback functions * - Responsive design with icons */ class ProjectSelector { constructor(containerId, options = {}) { this.containerId = containerId; this.projects = []; this.selectedProjectId = null; // Options this.options = { showAllOption: options.showAllOption !== undefined ? options.showAllOption : true, allOptionText: options.allOptionText || 'All Projects (Template View)', onChange: options.onChange || null, storageKey: options.storageKey || 'selected_project_id', placeholder: options.placeholder || 'Select a project...', label: options.label || 'Active Project Context', showLabel: options.showLabel !== undefined ? options.showLabel : true, compact: options.compact || false, // Compact mode for navbar autoLoad: options.autoLoad !== undefined ? options.autoLoad : true }; // Auth token this.token = localStorage.getItem('admin_token'); if (this.options.autoLoad) { this.init(); } } /** * Initialize the component */ async init() { try { // Load saved project from localStorage const savedProjectId = localStorage.getItem(this.options.storageKey); if (savedProjectId) { this.selectedProjectId = savedProjectId; } // Load projects from API await this.loadProjects(); // Render the selector this.render(); // Attach event listeners this.attachEventListeners(); // Trigger initial change event if project was pre-selected if (this.selectedProjectId && this.options.onChange) { this.options.onChange(this.selectedProjectId, this.getSelectedProject()); } } catch (error) { console.error('Failed to initialize project selector:', error); this.renderError(); } } /** * Load projects from API */ async loadProjects() { const response = await fetch('/api/admin/projects?active=true', { headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' } }); if (response.status === 401) { localStorage.removeItem('admin_token'); window.location.href = '/admin/login.html'; return; } const data = await response.json(); if (data.success) { this.projects = data.projects || []; // Sort by name this.projects.sort((a, b) => a.name.localeCompare(b.name)); } else { throw new Error(data.message || 'Failed to load projects'); } } /** * Render the selector component */ render() { const container = document.getElementById(this.containerId); if (!container) { console.error(`Container #${this.containerId} not found`); return; } // Determine selected project const selectedProject = this.getSelectedProject(); // Build HTML based on compact or full mode if (this.options.compact) { container.innerHTML = this.renderCompact(selectedProject); } else { container.innerHTML = this.renderFull(selectedProject); } } /** * Render compact mode (for navbar) */ renderCompact(selectedProject) { const displayText = selectedProject ? selectedProject.name : this.options.placeholder; const displayColor = selectedProject ? 'text-indigo-700' : 'text-gray-500'; return `
`; } /** * Render full mode (for content area) */ renderFull(selectedProject) { return `
${this.options.showLabel ? ` ` : ''} ${selectedProject ? `

${escapeHtml(selectedProject.name)}

${selectedProject.description ? `

${escapeHtml(selectedProject.description)}

` : ''}
${selectedProject.variableCount || 0} variable${(selectedProject.variableCount || 0) !== 1 ? 's' : ''} available for substitution
` : `

Viewing template text with variable placeholders. Select a project to see rendered values.

`}
`; } /** * Render error state */ renderError() { const container = document.getElementById(this.containerId); if (!container) return; container.innerHTML = `

Failed to load projects

Please refresh the page to try again.

`; } /** * Attach event listeners */ attachEventListeners() { const selectElement = document.getElementById(`${this.containerId}-select`); if (!selectElement) return; selectElement.addEventListener('change', (e) => { const newProjectId = e.target.value || null; this.handleChange(newProjectId); }); } /** * Handle project selection change */ handleChange(projectId) { const previousProjectId = this.selectedProjectId; this.selectedProjectId = projectId; // Save to localStorage if (projectId) { localStorage.setItem(this.options.storageKey, projectId); } else { localStorage.removeItem(this.options.storageKey); } // Re-render to update info panel this.render(); this.attachEventListeners(); // Re-attach after re-render // Trigger callback if (this.options.onChange) { const selectedProject = this.getSelectedProject(); this.options.onChange(projectId, selectedProject, previousProjectId); } // Dispatch custom event for other listeners const event = new CustomEvent('projectChanged', { detail: { projectId, project: this.getSelectedProject(), previousProjectId } }); document.dispatchEvent(event); } /** * Get currently selected project object */ getSelectedProject() { if (!this.selectedProjectId) return null; return this.projects.find(p => p.id === this.selectedProjectId) || null; } /** * Get all loaded projects */ getProjects() { return this.projects; } /** * Programmatically set the selected project */ setSelectedProject(projectId) { this.handleChange(projectId); } /** * Reload projects from API */ async reload() { try { await this.loadProjects(); this.render(); this.attachEventListeners(); } catch (error) { console.error('Failed to reload projects:', error); this.renderError(); } } /** * Get current selection */ getSelection() { return { projectId: this.selectedProjectId, project: this.getSelectedProject() }; } } /** * Utility: Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Export for use in other scripts window.ProjectSelector = ProjectSelector;