/** * Project Editor Modal * Handles creation, editing, viewing, and variable management for projects * * @class ProjectEditor */ class ProjectEditor { constructor() { this.mode = 'create'; // 'create', 'edit', 'view', 'variables' this.projectId = null; this.originalProject = null; this.variables = []; } /** * Open editor in create mode */ openCreate() { this.mode = 'create'; this.projectId = null; this.originalProject = null; this.render(); this.attachEventListeners(); } /** * Open editor in edit mode */ async openEdit(projectId) { this.mode = 'edit'; this.projectId = projectId; try { const response = await apiRequest(`/api/admin/projects/${projectId}`); if (!response.success || !response.project) { throw new Error('Failed to load project'); } this.originalProject = response.project; this.variables = response.variables || []; this.render(); this.populateForm(response.project); this.attachEventListeners(); } catch (error) { console.error('Failed to load project:', error); showToast('Failed to load project for editing', 'error'); } } /** * Open editor in view mode (read-only) */ async openView(projectId) { this.mode = 'view'; this.projectId = projectId; try { const response = await apiRequest(`/api/admin/projects/${projectId}`); if (!response.success || !response.project) { throw new Error('Failed to load project'); } this.originalProject = response.project; this.variables = response.variables || []; this.renderViewMode(response.project); } catch (error) { console.error('Failed to load project:', error); showToast('Failed to load project', 'error'); } } /** * Open variables management mode */ async openVariables(projectId) { this.mode = 'variables'; this.projectId = projectId; try { const [projectResponse, variablesResponse] = await Promise.all([ apiRequest(`/api/admin/projects/${projectId}`), apiRequest(`/api/admin/projects/${projectId}/variables`) ]); if (!projectResponse.success || !projectResponse.project) { throw new Error('Failed to load project'); } this.originalProject = projectResponse.project; this.variables = variablesResponse.variables || []; this.renderVariablesMode(); } catch (error) { console.error('Failed to load project variables:', error); showToast('Failed to load variables', 'error'); } } /** * Render the editor modal */ render() { const container = document.getElementById('modal-container'); const title = this.mode === 'create' ? 'Create New Project' : 'Edit Project'; container.innerHTML = `

${title}

Lowercase slug format (letters, numbers, hyphens only)

(Inactive projects are hidden from rule rendering)

`; } /** * Render view mode (read-only) */ renderViewMode(project) { const container = document.getElementById('modal-container'); const techStack = project.techStack || {}; const metadata = project.metadata || {}; container.innerHTML = `

${escapeHtml(project.name)}

${escapeHtml(project.id)}

${project.active ? 'Active' : 'Inactive' }
${project.description ? `

Description

${escapeHtml(project.description)}

` : ''} ${Object.keys(techStack).length > 0 ? `

Tech Stack

${techStack.framework ? `
Framework: ${escapeHtml(techStack.framework)}
` : ''} ${techStack.database ? `
Database: ${escapeHtml(techStack.database)}
` : ''} ${techStack.frontend ? `
Frontend: ${escapeHtml(techStack.frontend)}
` : ''} ${techStack.css ? `
CSS: ${escapeHtml(techStack.css)}
` : ''}
` : ''} ${project.repositoryUrl ? `

Repository

${escapeHtml(project.repositoryUrl)}
` : ''}

Variables (${this.variables.length})

${this.variables.length > 0 ? `
${this.variables.slice(0, 5).map(v => ` `).join('')}
Name Value Category
${escapeHtml(v.variableName)} ${escapeHtml(v.value)} ${escapeHtml(v.category || 'other')}
${this.variables.length > 5 ? `
Showing 5 of ${this.variables.length} variables
` : ''}
` : '

No variables defined

'}

Created: ${new Date(project.createdAt).toLocaleString()}

Updated: ${new Date(project.updatedAt).toLocaleString()}

`; // Attach close handlers document.getElementById('close-modal').addEventListener('click', () => this.close()); } /** * Render variables management mode */ renderVariablesMode() { const container = document.getElementById('modal-container'); container.innerHTML = `

Manage Variables

${escapeHtml(this.originalProject.name)} (${escapeHtml(this.originalProject.id)})

${this.variables.length} variable${this.variables.length !== 1 ? 's' : ''} defined

${this.variables.length > 0 ? this.variables.map(v => this.renderVariableCard(v)).join('') : `

No variables defined for this project.

Click "Add Variable" to create one.

`}
`; // Attach event listeners document.getElementById('close-modal').addEventListener('click', () => { this.close(); // Refresh project list if (window.loadProjects) window.loadProjects(); if (window.loadStatistics) window.loadStatistics(); }); document.getElementById('add-variable-btn').addEventListener('click', () => { this.showVariableForm(); }); } /** * Render a single variable card */ renderVariableCard(variable) { return `
${escapeHtml(variable.variableName)}

${escapeHtml(variable.value)}

${variable.description ? `

${escapeHtml(variable.description)}

` : ''}
${escapeHtml(variable.category || 'other')} ${escapeHtml(variable.dataType || 'string')}
`; } /** * Show variable form (add/edit) */ showVariableForm(variableName = null) { const existingVariable = variableName ? this.variables.find(v => v.variableName === variableName) : null; const isEdit = !!existingVariable; const formHtml = `

${isEdit ? 'Edit' : 'Add'} Variable

UPPER_SNAKE_CASE format

`; // Insert form const container = document.querySelector('#variables-list'); const formContainer = document.createElement('div'); formContainer.id = 'variable-form-container'; formContainer.innerHTML = formHtml; container.insertBefore(formContainer, container.firstChild); // Attach event listeners document.getElementById('variable-form').addEventListener('submit', async (e) => { e.preventDefault(); await this.saveVariable(isEdit); }); document.getElementById('cancel-var-btn').addEventListener('click', () => { document.getElementById('variable-form-container').remove(); }); } /** * Save variable (create or update) */ async saveVariable(isEdit = false) { const variableName = document.getElementById('var-name').value.trim(); const value = document.getElementById('var-value').value.trim(); const description = document.getElementById('var-description').value.trim(); const category = document.getElementById('var-category').value; const dataType = document.getElementById('var-datatype').value; if (!variableName || !value) { showToast('Variable name and value are required', 'error'); return; } // Validate UPPER_SNAKE_CASE if (!/^[A-Z][A-Z0-9_]*$/.test(variableName)) { showToast('Variable name must be UPPER_SNAKE_CASE (e.g., DB_NAME)', 'error'); return; } try { const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables`, { method: 'POST', body: JSON.stringify({ variableName, value, description, category, dataType }) }); if (response.success) { showToast(`Variable ${isEdit ? 'updated' : 'created'} successfully`, 'success'); // Reload variables const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`); this.variables = variablesResponse.variables || []; // Re-render this.renderVariablesMode(); } else { showToast(response.message || 'Failed to save variable', 'error'); } } catch (error) { console.error('Failed to save variable:', error); showToast('Failed to save variable', 'error'); } } /** * Edit variable */ editVariable(variableName) { // Remove any existing form first const existingForm = document.getElementById('variable-form-container'); if (existingForm) existingForm.remove(); this.showVariableForm(variableName); } /** * Delete variable */ async deleteVariable(variableName) { if (!confirm(`Delete variable "${variableName}"?`)) { return; } try { const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables/${variableName}`, { method: 'DELETE' }); if (response.success) { showToast('Variable deleted successfully', 'success'); // Reload variables const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`); this.variables = variablesResponse.variables || []; // Re-render this.renderVariablesMode(); } else { showToast(response.message || 'Failed to delete variable', 'error'); } } catch (error) { console.error('Failed to delete variable:', error); showToast('Failed to delete variable', 'error'); } } /** * Populate form with project data (edit mode) */ populateForm(project) { document.getElementById('project-id').value = project.id || ''; document.getElementById('project-name').value = project.name || ''; document.getElementById('project-description').value = project.description || ''; document.getElementById('project-active').checked = project.active !== false; document.getElementById('repo-url').value = project.repositoryUrl || ''; if (project.techStack) { document.getElementById('tech-framework').value = project.techStack.framework || ''; document.getElementById('tech-database').value = project.techStack.database || ''; document.getElementById('tech-frontend').value = project.techStack.frontend || ''; } } /** * Attach event listeners */ attachEventListeners() { document.getElementById('close-modal').addEventListener('click', () => this.close()); document.getElementById('cancel-btn').addEventListener('click', () => this.close()); document.getElementById('save-btn').addEventListener('click', () => this.submit()); } /** * Submit form */ async submit() { const form = document.getElementById('project-form'); if (!form.checkValidity()) { form.reportValidity(); return; } const projectData = { id: document.getElementById('project-id').value.trim(), name: document.getElementById('project-name').value.trim(), description: document.getElementById('project-description').value.trim(), active: document.getElementById('project-active').checked, repositoryUrl: document.getElementById('repo-url').value.trim() || null, techStack: { framework: document.getElementById('tech-framework').value.trim() || undefined, database: document.getElementById('tech-database').value.trim() || undefined, frontend: document.getElementById('tech-frontend').value.trim() || undefined } }; try { let response; if (this.mode === 'create') { response = await apiRequest('/api/admin/projects', { method: 'POST', body: JSON.stringify(projectData) }); } else { response = await apiRequest(`/api/admin/projects/${this.projectId}`, { method: 'PUT', body: JSON.stringify(projectData) }); } if (response.success) { showToast(`Project ${this.mode === 'create' ? 'created' : 'updated'} successfully`, 'success'); this.close(); // Refresh project list if (window.loadProjects) window.loadProjects(); if (window.loadStatistics) window.loadStatistics(); } else { showToast(response.message || 'Failed to save project', 'error'); } } catch (error) { console.error('Failed to save project:', error); showToast('Failed to save project', 'error'); } } /** * Close modal */ close() { const container = document.getElementById('modal-container'); container.innerHTML = ''; } } // Utility function function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Create global instance window.projectEditor = new ProjectEditor();