import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TemplateEditorService } from './template-editor.service'; import { ZipExportService } from './zip-export.service'; import { HbsRenderService } from './hbs-render.service'; interface Template { name: string; path: string; type: 'template' | 'partial'; } interface Session { sessionId: string; success: boolean; message: string; } interface CompoDocConfig { hideGenerator?: boolean; disableSourceCode?: boolean; disableGraph?: boolean; disableCoverage?: boolean; disablePrivate?: boolean; disableProtected?: boolean; disableInternal?: boolean; disableLifeCycleHooks?: boolean; disableConstructors?: boolean; disableRoutesGraph?: boolean; disableSearch?: boolean; disableDependencies?: boolean; disableProperties?: boolean; disableDomTree?: boolean; disableTemplateTab?: boolean; disableStyleTab?: boolean; disableMainGraph?: boolean; disableFilePath?: boolean; disableOverview?: boolean; hideDarkModeToggle?: boolean; minimal?: boolean; customFavicon?: string; includes?: string; includesName?: string; } @Component({ selector: 'template-playground-root', template: `

Template Playground

Session: {{sessionId.substring(0, 8)}}... Saving... Last saved: {{lastSaved | date:'short'}}

CompoDoc Configuration

Templates

  • {{template.name}} {{template.type}}
Loading templates...

{{selectedFile.path}}

{{selectedFile.type}}

Live Preview

`, styles: [` .template-playground { display: flex; flex-direction: column; height: 100vh; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .template-playground-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 2rem; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } .template-playground-status { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; } .session-info { color: #6c757d; font-family: monospace; } .saving-indicator { color: #ffc107; font-weight: bold; } .last-saved { color: #28a745; } .template-playground-actions { display: flex; gap: 0.5rem; } .config-panel { background: #e9ecef; padding: 1rem 2rem; border-bottom: 1px solid #dee2e6; transition: all 0.3s ease; max-height: 200px; overflow: hidden; } .config-panel.collapsed { max-height: 0; padding: 0 2rem; } .config-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem; margin-top: 0.5rem; } .config-options label { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; } .template-playground-body { display: flex; flex: 1; overflow: hidden; } .template-playground-sidebar { width: 250px; background: #f8f9fa; border-right: 1px solid #dee2e6; overflow-y: auto; } .template-file-list { padding: 1rem; } .template-file-list h3 { margin: 0 0 0.5rem 0; font-size: 0.875rem; font-weight: 600; color: #495057; text-transform: uppercase; letter-spacing: 0.5px; } .file-list { list-style: none; padding: 0; margin: 0 0 1.5rem 0; } .file-list li { display: flex; align-items: center; padding: 0.5rem; cursor: pointer; border-radius: 4px; font-size: 0.875rem; transition: background-color 0.15s ease; } .file-list li:hover { background: #e9ecef; } .file-list li.active { background: #007bff; color: white; } .file-icon { margin-right: 0.5rem; opacity: 0.7; } .file-type { margin-left: auto; font-size: 0.75rem; opacity: 0.7; text-transform: uppercase; } .loading-templates { text-align: center; color: #6c757d; font-style: italic; padding: 2rem; } .template-playground-main { flex: 1; display: flex; overflow: hidden; } .template-playground-editor { width: 50%; display: flex; flex-direction: column; border-right: 1px solid #dee2e6; } .editor-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } .editor-header h4 { margin: 0; font-size: 0.875rem; font-weight: 600; } .file-type-badge { background: #6c757d; color: white; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; text-transform: uppercase; } .editor-container { flex: 1; position: relative; } .template-playground-preview { width: 50%; display: flex; flex-direction: column; } .preview-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } .preview-header h4 { margin: 0; font-size: 0.875rem; font-weight: 600; } .preview-frame { flex: 1; border: none; background: white; } .btn { padding: 0.375rem 0.75rem; border: 1px solid transparent; border-radius: 0.25rem; font-size: 0.875rem; font-weight: 500; text-decoration: none; cursor: pointer; transition: all 0.15s ease; } .btn-primary { background: #007bff; border-color: #007bff; color: white; } .btn-primary:hover { background: #0056b3; border-color: #004085; } .btn-secondary { background: #6c757d; border-color: #6c757d; color: white; } .btn-secondary:hover { background: #545b62; border-color: #4e555b; } .btn-success { background: #28a745; border-color: #28a745; color: white; } .btn-success:hover { background: #1e7e34; border-color: #1c7430; } .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; } `] }) export class TemplatePlaygroundComponent implements OnInit, OnDestroy { @ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef; @ViewChild('previewFrame', { static: true }) previewFrame!: ElementRef; sessionId: string = ''; templates: Template[] = []; selectedFile: Template | null = null; config: CompoDocConfig = {}; showConfigPanel: boolean = false; saving: boolean = false; lastSaved: Date | null = null; private saveTimeout?: number; private readonly SAVE_DELAY = 300; // 300ms debounce get previewUrl(): string { return this.sessionId ? `/api/session/${this.sessionId}/docs/` : ''; } constructor( private http: HttpClient, private editorService: TemplateEditorService, private zipService: ZipExportService, private hbsService: HbsRenderService ) {} async ngOnInit() { try { await this.createSession(); await this.loadSessionTemplates(); await this.loadSessionConfig(); this.initializeEditor(); } catch (error) { console.error('Error initializing template playground:', error); } } ngOnDestroy() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } } private async createSession(): Promise { const response = await this.http.post('/api/session/create', {}).toPromise(); if (response && response.success) { this.sessionId = response.sessionId; console.log('Session created:', this.sessionId); } else { throw new Error('Failed to create session'); } } private async loadSessionTemplates(): Promise { if (!this.sessionId) return; const response = await this.http.get<{templates: Template[], success: boolean}>(`/api/session/${this.sessionId}/templates`).toPromise(); if (response && response.success) { this.templates = response.templates; // Auto-select the first template if (this.templates.length > 0 && !this.selectedFile) { this.selectFile(this.templates[0]); } } } private async loadSessionConfig(): Promise { if (!this.sessionId) return; const response = await this.http.get<{config: CompoDocConfig, success: boolean}>(`/api/session/${this.sessionId}/config`).toPromise(); if (response && response.success) { this.config = response.config; } } initializeEditor() { this.editorService.initializeEditor(this.editorContainer.nativeElement); // Set up debounced save on content change this.editorService.setOnChangeCallback((content: string) => { this.scheduleAutoSave(content); }); } async selectFile(template: Template) { this.selectedFile = template; if (!this.sessionId) return; try { const response = await this.http.get<{content: string, success: boolean}>(`/api/session/${this.sessionId}/template/${template.path}`).toPromise(); if (response && response.success) { this.editorService.setEditorContent(response.content, template.type === 'template' ? 'handlebars' : 'handlebars'); } } catch (error) { console.error('Error loading template:', error); } } private scheduleAutoSave(content: string): void { if (!this.selectedFile || !this.sessionId) return; // Clear existing timeout if (this.saveTimeout) { clearTimeout(this.saveTimeout); } // Set saving indicator this.saving = true; // Schedule new save this.saveTimeout = window.setTimeout(async () => { try { await this.saveTemplate(content); this.saving = false; this.lastSaved = new Date(); } catch (error) { console.error('Error saving template:', error); this.saving = false; } }, this.SAVE_DELAY); } private async saveTemplate(content: string): Promise { if (!this.selectedFile || !this.sessionId) return; const response = await this.http.post<{success: boolean}>(`/api/session/${this.sessionId}/template/${this.selectedFile.path}`, { content }).toPromise(); if (!response || !response.success) { throw new Error('Failed to save template'); } } async updateConfig(): Promise { if (!this.sessionId) return; try { const response = await this.http.post<{success: boolean}>(`/api/session/${this.sessionId}/config`, { config: this.config }).toPromise(); if (response && response.success) { // Config updated, documentation will be regenerated automatically } } catch (error) { console.error('Error updating config:', error); } } toggleConfigPanel(): void { this.showConfigPanel = !this.showConfigPanel; } refreshPreview(): void { if (this.previewFrame?.nativeElement) { this.previewFrame.nativeElement.src = this.previewFrame.nativeElement.src; } } resetToDefault(): void { // Implementation for resetting to default templates if (confirm('Are you sure you want to reset all templates to their default values? This action cannot be undone.')) { // TODO: Implement reset functionality console.log('Reset to default templates'); } } async exportZip(): Promise { try { if (!this.sessionId) { console.error('No active session. Please refresh the page and try again.'); return; } console.log('Creating template package...'); // Call server-side ZIP creation endpoint for all templates const response = await this.http.post(`/api/session/${this.sessionId}/download-all-templates`, {}, { responseType: 'blob', observe: 'response' }).toPromise(); if (!response || !response.body) { throw new Error('Failed to create template package'); } // Get the ZIP file as a blob const zipBlob = response.body; // Get filename from response headers or construct it const contentDisposition = response.headers.get('Content-Disposition'); let filename = `compodoc-templates-${this.sessionId}.zip`; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="([^"]+)"/); if (filenameMatch) { filename = filenameMatch[1]; } } // Create download link and trigger download const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('Template package downloaded successfully!'); } catch (error) { console.error('Error downloading template package:', error); } } trackByName(index: number, item: Template): string { return item.name; } }