/* ========================================================================== SVG TEMPLATER JAVASCRIPT ENGINE Statically configured API endpoints, state management, and Demo Mode simulation. ========================================================================== */ window.ENV = { API_KEY: "[[.APIKey]]", API_URL: "[[.APIUrl]]" }; // 1. STATIC CONFIGURATION (User: Edit these values to configure backend communication) const API_BASE_URL = window.ENV.API_URL; const API_TOKEN = window.ENV.API_KEY; // Set your Bearer Token here if authentication is enabled // 2. STATE MANAGER const state = { templates: [], activeTemplate: null, zoom: 100, isDemoMode: false, isConnected: false, // true only after a confirmed successful API response format: 'svg', // 'svg' or 'png' fonts: [], lastRenderedUrl: null, renderedUrls: [], activePageIndex: 0, suppressRenderToast: false }; // 3. MOCK DATA FOR DEMO MODE // 3. MOCK DATA FOR DEMO MODE const MOCK_TEMPLATES = [ { Id: "business_card_v1", Name: "Visitenkarte VIP", Pages: [ { TemplateId: "business_card_v1", Page: 1, TemplateKeys: ["Name", "Title", "Company", "Email", "Phone", "Website"] } ] }, { Id: "gift_certificate", Name: "Geschenkgutschein Gold", Pages: [ { TemplateId: "gift_certificate", Page: 1, TemplateKeys: ["Recipient", "Amount", "Occasion", "Sender", "Date"] }, { TemplateId: "gift_certificate", Page: 2, TemplateKeys: ["Sender", "Date"] } ] }, { Id: "event_badge", Name: "Event Badge VIP", Pages: [ { TemplateId: "event_badge", Page: 1, TemplateKeys: ["AttendeeName", "AccessLevel", "EventName", "SeatNumber"] } ] } ]; const MOCK_SVG_TEMPLATES = { "business_card_v1_1": ` {{ Company }} {{ Name }} {{ Title }} E-MAIL: {{ Email }} TELEFON: {{ Phone }} WEB: {{ Website }} `, "gift_certificate_1": ` Gutschein Für das Ereignis: {{ Occasion }} {{ Amount }} € Empfänger: {{ Recipient }} Schenker: {{ Sender }} Ausgestellt am: {{ Date }} `, "gift_certificate_2": ` GUTSCHEINBEDINGUNGEN 1. Dieser Gutschein ist ab Ausstellungsdatum 3 Jahre gültig. 2. Eine Barauszahlung des Gutscheinwerts ist ausgeschlossen. 3. Der Gutschein kann für alle Dienstleistungen eingelöst werden. 4. Bei Verlust oder Beschädigung erfolgt kein Ersatz. Ausgestellt von: {{ Sender }} am {{ Date }} `, "event_badge_1": ` {{ EventName }} {{ AttendeeName }} ACCESS: {{ AccessLevel }} Sitzplatz: {{ SeatNumber }} #0094-1184-7492 ` }; // 4. UI ELEMENT CACHE const elements = { viewHome: document.getElementById('view-home'), viewEditor: document.getElementById('view-editor'), dropZone: document.getElementById('svg-drop-zone'), fileInput: document.getElementById('svg-file-input'), templatesGrid: document.getElementById('templates-grid'), templatesEmpty: document.getElementById('templates-empty'), templatesLoading: document.getElementById('templates-loading'), statsTotalCount: document.getElementById('stats-total-count'), statsApiHost: document.getElementById('stats-api-host'), statsAppMode: document.getElementById('stats-app-mode'), appModeBadge: document.getElementById('app-mode-badge'), modeText: document.getElementById('mode-text'), btnToggleDemo: document.getElementById('btn-toggle-demo'), btnBackHome: document.getElementById('btn-back-home'), editorTemplateName: document.getElementById('editor-template-name'), dynamicInputsArea: document.getElementById('dynamic-inputs-area'), formatSvg: document.getElementById('format-svg'), formatPng: document.getElementById('format-png'), resolutionSettings: document.getElementById('resolution-settings'), renderWidth: document.getElementById('render-width'), renderHeight: document.getElementById('render-height'), btnSubmitRender: document.getElementById('btn-submit-render'), renderSpinner: document.getElementById('render-spinner'), renderBtnIcon: document.getElementById('render-btn-icon'), renderBtnText: document.getElementById('render-btn-text'), btnZoomIn: document.getElementById('btn-zoom-in'), btnZoomOut: document.getElementById('btn-zoom-out'), btnFullscreen: document.getElementById('btn-fullscreen'), btnZoomReset: document.getElementById('btn-zoom-reset'), toastContainer: document.getElementById('toast-container'), variablesForm: document.getElementById('dynamic-variables-form'), pulseDot: document.querySelector('#app-mode-badge .pulse-dot'), fontsList: document.getElementById('fonts-list'), fontsEmpty: document.getElementById('fonts-empty'), fontsLoading: document.getElementById('fonts-loading'), fontFileInput: document.getElementById('font-file-input'), btnUploadFont: document.getElementById('btn-upload-font'), btnDownloadPreview: document.getElementById('btn-download-preview'), btnDownloadAll: document.getElementById('btn-download-all'), previewCard: document.querySelector('.preview-card'), previewViewport: document.getElementById('preview-viewport'), previewCanvasContainer: document.getElementById('preview-canvas-container'), previewLoadingOverlay: document.getElementById('preview-loading-overlay'), previewPlaceholder: document.getElementById('preview-placeholder'), previewImage: document.getElementById('preview-image'), zoomBadge: document.getElementById('zoom-badge'), btnRenameActive: document.getElementById('btn-rename-active'), editorTemplateId: document.getElementById('editor-template-id'), pageNavigator: document.getElementById('page-navigator'), pageIndicator: document.getElementById('page-indicator'), btnPagePrev: document.getElementById('btn-page-prev'), btnPageNext: document.getElementById('btn-page-next'), editorPagesList: document.getElementById('editor-pages-list'), pageFileInput: document.getElementById('page-file-input'), btnAddPage: document.getElementById('btn-add-page') }; // 4.5 CUSTOM PROMPT WINDOW UTILITY (Hacker/Hackathon neobrutalist style) function showCustomPrompt(title, defaultValue, placeholder = "") { return new Promise((resolve) => { // Create modal overlay const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100vw'; overlay.style.height = '100vh'; overlay.style.backgroundColor = 'rgba(10, 11, 16, 0.85)'; overlay.style.backdropFilter = 'blur(6px)'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.zIndex = '99999'; overlay.style.animation = 'fadeIn 0.2s ease-out'; // Create dialog card const dialog = document.createElement('div'); dialog.className = 'card'; dialog.style.width = '400px'; dialog.style.border = '1px solid rgba(255, 255, 255, 0.08)'; dialog.style.boxShadow = '0 20px 40px rgba(0, 0, 0, 0.4)'; dialog.style.padding = '0'; dialog.style.overflow = 'hidden'; dialog.style.borderRadius = '8px'; // Header const header = document.createElement('div'); header.className = 'card-header'; header.style.padding = '18px 24px'; header.style.borderBottom = '1px solid rgba(255, 255, 255, 0.06)'; header.style.background = '#141724'; const headerTitle = document.createElement('h2'); headerTitle.style.fontSize = '14px'; headerTitle.style.margin = '0'; headerTitle.style.color = '#ffffff'; headerTitle.textContent = title; header.appendChild(headerTitle); // Body const body = document.createElement('div'); body.className = 'card-body'; body.style.padding = '24px'; body.style.background = '#141724'; body.style.display = 'flex'; body.style.flexDirection = 'column'; body.style.gap = '16px'; // Input const input = document.createElement('input'); input.type = 'text'; input.className = 'custom-input'; input.value = defaultValue || ''; input.placeholder = placeholder || 'Name eingeben...'; input.style.width = '100%'; input.style.border = '1px solid rgba(255, 255, 255, 0.12)'; input.style.borderRadius = '6px'; input.style.padding = '10px 14px'; input.style.background = '#0a0b10'; input.style.color = '#f3f4f6'; input.style.fontFamily = 'inherit'; // Actions container const actions = document.createElement('div'); actions.style.display = 'flex'; actions.style.justifyContent = 'flex-end'; actions.style.gap = '12px'; actions.style.marginTop = '8px'; // Cancel button const cancelBtn = document.createElement('button'); cancelBtn.className = 'back-btn'; cancelBtn.textContent = 'Abbrechen'; cancelBtn.style.padding = '8px 14px'; cancelBtn.style.fontSize = '12px'; // Submit button const submitBtn = document.createElement('button'); submitBtn.className = 'submit-btn'; submitBtn.textContent = 'Bestätigen'; submitBtn.style.padding = '8px 16px'; submitBtn.style.width = 'auto'; submitBtn.style.fontSize = '12px'; submitBtn.style.background = '#6366f1'; submitBtn.style.color = '#ffffff'; submitBtn.style.border = 'none'; submitBtn.style.boxShadow = 'none'; submitBtn.style.borderRadius = '6px'; actions.appendChild(cancelBtn); actions.appendChild(submitBtn); body.appendChild(input); body.appendChild(actions); dialog.appendChild(header); dialog.appendChild(body); overlay.appendChild(dialog); document.body.appendChild(overlay); // Focus input setTimeout(() => input.focus(), 50); // Helper to close and resolve const closeWith = (val) => { overlay.style.animation = 'fadeOut 0.15s ease-in'; setTimeout(() => { overlay.remove(); resolve(val); }, 100); }; // Listeners cancelBtn.addEventListener('click', () => closeWith(null)); submitBtn.addEventListener('click', () => closeWith(input.value)); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); closeWith(input.value); } else if (e.key === 'Escape') { e.preventDefault(); closeWith(null); } }); }); } // 5. TOAST NOTIFICATION UTILITY function showToast(message, type = 'info', duration = 4000) { // Limit active toasts to max 3 to prevent screen flooding const activeToasts = elements.toastContainer.querySelectorAll('.toast:not(.toast-leave)'); if (activeToasts.length >= 3) { const oldest = activeToasts[0]; oldest.classList.add('toast-leave'); oldest.remove(); } const toast = document.createElement('div'); toast.className = `toast ${type}`; let iconSvg = ''; if (type === 'success') { iconSvg = ''; } else if (type === 'error') { iconSvg = ''; } else if (type === 'warning') { iconSvg = ''; } else { iconSvg = ''; } toast.innerHTML = ` ${iconSvg} ${message} `; elements.toastContainer.appendChild(toast); // Auto-remove setTimeout(() => { toast.classList.add('toast-leave'); toast.addEventListener('transitionend', () => { toast.remove(); }); }, duration); } // 6. API CLIENT IMPLEMENTATION const API = { getHeaders() { const headers = {}; if (API_TOKEN) { headers['Authorization'] = `Bearer ${API_TOKEN}`; } return headers; }, async fetchAllTemplates() { if (state.isDemoMode) { return MOCK_TEMPLATES; } const res = await fetch(`${API_BASE_URL}/svg/`, { headers: this.getHeaders() }); if (!res.ok) { if (res.status === 401) { throw new Error("Nicht autorisiert. Bitte prüfen Sie den statischen Token."); } throw new Error(`Server-Fehler: ${res.status} ${res.statusText}`); } // The backend returns a standard JSON array now return await res.json(); }, async uploadSVG(file, filename, customName) { if (state.isDemoMode) { // Read file as text for mock parser const reader = new FileReader(); const text = await new Promise((resolve) => { reader.onload = (e) => resolve(e.target.result); reader.readAsText(file); }); const templateKeys = []; const regex = /\{\{\s*(.*?)\s*\}\}/g; let match; while ((match = regex.exec(text)) !== null) { const varname = match[1].trim(); if (!templateKeys.includes(varname)) { templateKeys.push(varname); } } const newId = `custom_template_${Math.random().toString(36).substring(2, 9)}`; // Register template inside mock storage MOCK_SVG_TEMPLATES[newId + "_1"] = text; const newTemplate = { Id: newId, Name: customName || filename.replace('.svg', ''), Pages: [ { TemplateId: newId, Page: 1, TemplateKeys: templateKeys } ] }; MOCK_TEMPLATES.unshift(newTemplate); return newTemplate; } const formData = new FormData(); formData.append('files', file, filename); const urlParams = new URLSearchParams(); if (customName) { urlParams.set('name', customName); } const res = await fetch(`${API_BASE_URL}/svg/?${urlParams.toString()}`, { method: 'POST', headers: this.getHeaders(), body: formData }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Upload failed: ${res.statusText}`); } return await res.json(); }, async deleteTemplate(id) { if (state.isDemoMode) { const index = MOCK_TEMPLATES.findIndex(t => t.Id === id); if (index !== -1) { MOCK_TEMPLATES.splice(index, 1); delete MOCK_SVG_TEMPLATES[id]; return true; } return false; } const res = await fetch(`${API_BASE_URL}/svg/${id}`, { method: 'DELETE', headers: this.getHeaders() }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Löschen fehlgeschlagen: ${res.statusText}`); } return true; }, async renameTemplate(id, name) { if (state.isDemoMode) { const template = MOCK_TEMPLATES.find(t => t.Id === id); if (template) { template.Name = name; return true; } return false; } const headers = this.getHeaders(); headers['Content-Type'] = 'application/json'; const res = await fetch(`${API_BASE_URL}/svg/${id}`, { method: 'PATCH', headers: headers, body: JSON.stringify({ Name: name }) }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Namen zuweisen fehlgeschlagen: ${res.statusText}`); } return true; }, async addPage(templateId, files) { if (state.isDemoMode) { // Demo mode loop for (let i = 0; i < files.length; i++) { await this.addSinglePageDemo(templateId, files[i], files[i].name); } const template = MOCK_TEMPLATES.find(t => t.Id === templateId); return template.Pages; } const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i], files[i].name); } const res = await fetch(`${API_BASE_URL}/svg/${templateId}/page/`, { method: 'POST', headers: this.getHeaders(), body: formData }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Seiten hinzufügen fehlgeschlagen: ${res.statusText}`); } return await res.json(); }, async addSinglePageDemo(templateId, file, filename) { // Read file as text for mock parser const reader = new FileReader(); const text = await new Promise((resolve) => { reader.onload = (e) => resolve(e.target.result); reader.readAsText(file); }); const template = MOCK_TEMPLATES.find(t => t.Id === templateId); if (!template) return; const templateKeys = []; const regex = /\{\{\s*(.*?)\s*\}\}/g; let match; while ((match = regex.exec(text)) !== null) { const varname = match[1].trim(); if (!templateKeys.includes(varname)) { templateKeys.push(varname); } } const newPageNumber = template.Pages.length + 1; MOCK_SVG_TEMPLATES[`${templateId}_${newPageNumber}`] = text; const newPage = { TemplateId: templateId, Page: newPageNumber, TemplateKeys: templateKeys }; template.Pages.push(newPage); }, async deletePage(templateId, pageNumber) { if (state.isDemoMode) { const template = MOCK_TEMPLATES.find(t => t.Id === templateId); if (!template) throw new Error("Template not found in mock store."); const pageIndex = template.Pages.findIndex(p => p.Page === pageNumber); if (pageIndex !== -1) { template.Pages.splice(pageIndex, 1); delete MOCK_SVG_TEMPLATES[`${templateId}_${pageNumber}`]; // Re-index remaining pages template.Pages.forEach((p, idx) => { const oldNum = p.Page; const newNum = idx + 1; p.Page = newNum; if (MOCK_SVG_TEMPLATES[`${templateId}_${oldNum}`]) { MOCK_SVG_TEMPLATES[`${templateId}_${newNum}`] = MOCK_SVG_TEMPLATES[`${templateId}_${oldNum}`]; if (oldNum !== newNum) { delete MOCK_SVG_TEMPLATES[`${templateId}_${oldNum}`]; } } }); return true; } return false; } const res = await fetch(`${API_BASE_URL}/svg/${templateId}/page/${pageNumber}`, { method: 'DELETE', headers: this.getHeaders() }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Seite löschen fehlgeschlagen: ${res.statusText}`); } return true; }, async renderTemplate(id, templateKeys, format, w, h) { if (state.isDemoMode) { // Simulation logic await new Promise(resolve => setTimeout(resolve, 800)); // Simulate networking const template = MOCK_TEMPLATES.find(t => t.Id === id); if (!template) { throw new Error("Template not found in mock store."); } const urls = []; // Render each page for (let i = 0; i < template.Pages.length; i++) { const pageNum = template.Pages[i].Page; const rawSvg = MOCK_SVG_TEMPLATES[`${id}_${pageNum}`]; if (!rawSvg) continue; // Replace variables with actual values let rendered = rawSvg; Object.keys(templateKeys).forEach(key => { const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'); rendered = rendered.replace(regex, templateKeys[key]); }); // Fallback for missing keys to empty string const missingRegex = /\{\{\s*(.*?)\s*\}\}/g; rendered = rendered.replace(missingRegex, ''); // Convert to Blob URL const blob = new Blob([rendered], { type: 'image/svg+xml' }); urls.push(URL.createObjectURL(blob)); } return { Urls: urls }; } // Prepare request parameters const queryParams = new URLSearchParams(); queryParams.set('format', format); if (w) queryParams.set('w', w); if (h) queryParams.set('h', h); const headers = this.getHeaders(); headers['Content-Type'] = 'application/json'; // POST request with a body const res = await fetch(`${API_BASE_URL}/svg/${id}?${queryParams.toString()}`, { method: 'POST', headers: headers, body: JSON.stringify({ TemplateKeys: templateKeys }) }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Rendering fehlgeschlagen: ${res.statusText}`); } return await res.json(); }, async fetchAllFonts() { if (state.isDemoMode) { return ["Roboto-Regular.ttf", "Outfit-Bold.ttf", "Inter-Medium.ttf"]; } const res = await fetch(`${API_BASE_URL}/font/`, { headers: this.getHeaders() }); if (!res.ok) { throw new Error(`Schriften laden fehlgeschlagen: ${res.statusText}`); } return await res.json(); }, async uploadFont(fileBlob, filename) { if (state.isDemoMode) { // Mock upload await new Promise(resolve => setTimeout(resolve, 800)); // Add custom font to list const ext = filename.split('.').pop().toUpperCase(); if (!MOCK_FONTS.includes(filename)) { MOCK_FONTS.unshift(filename); } return filename; } const headers = this.getHeaders(); const extension = filename.split('.').pop().toLowerCase(); headers['Content-Type'] = `font/${extension}`; const res = await fetch(`${API_BASE_URL}/font/`, { method: 'POST', headers: headers, body: fileBlob }); if (!res.ok) { const errMsg = await res.text(); throw new Error(errMsg || `Schrift-Upload fehlgeschlagen: ${res.statusText}`); } return filename; } }; // Global mock fonts store const MOCK_FONTS = ["Roboto-Regular.ttf", "Outfit-Bold.ttf", "Inter-Medium.ttf"]; // 7. PRESENTATION & ROUTING ENGINE function navigateTo(viewName) { if (viewName === 'home') { elements.viewEditor.classList.remove('active'); setTimeout(() => { elements.viewEditor.classList.add('hidden'); elements.viewHome.classList.remove('hidden'); elements.viewHome.classList.add('active'); }, 150); // Refresh list loadTemplatesList(); } else if (viewName === 'editor') { elements.viewHome.classList.remove('active'); setTimeout(() => { elements.viewHome.classList.add('hidden'); elements.viewEditor.classList.remove('hidden'); elements.viewEditor.classList.add('active'); }, 150); } } // Update the active UI Mode dashboard displays function updateModeUI() { // API Host display try { const parsedUrl = new URL(API_BASE_URL); elements.statsApiHost.textContent = parsedUrl.host; } catch { elements.statsApiHost.textContent = API_BASE_URL || 'Unknown'; } if (state.isDemoMode) { elements.statsAppMode.textContent = 'Demo Modus'; elements.modeText.textContent = 'Demo Modus'; elements.appModeBadge.className = 'status-badge demo'; elements.pulseDot.className = 'pulse-dot demo'; elements.btnToggleDemo.classList.add('active'); } else if (state.isConnected) { elements.statsAppMode.textContent = 'Live API'; elements.modeText.textContent = 'Live Verbunden'; elements.appModeBadge.className = 'status-badge live'; elements.pulseDot.className = 'pulse-dot live'; elements.btnToggleDemo.classList.remove('active'); } else { elements.statsAppMode.textContent = 'Nicht verbunden'; elements.modeText.textContent = 'Nicht verbunden'; elements.appModeBadge.className = 'status-badge disconnected'; elements.pulseDot.className = 'pulse-dot disconnected'; elements.btnToggleDemo.classList.remove('active'); } } function enableDemoMode(enable) { state.isDemoMode = enable; updateModeUI(); loadTemplatesList(); loadFontsList(); } // 8. TEMPLATE LIST VIEW RENDERER async function loadTemplatesList() { elements.templatesEmpty.classList.add('hidden'); elements.templatesGrid.classList.add('hidden'); elements.templatesLoading.classList.remove('hidden'); try { state.templates = await API.fetchAllTemplates(); // Mark connection as confirmed (only if not in demo mode) if (!state.isDemoMode) { state.isConnected = true; updateModeUI(); } elements.templatesLoading.classList.add('hidden'); elements.statsTotalCount.textContent = state.templates.length; if (state.templates.length === 0) { elements.templatesEmpty.classList.remove('hidden'); return; } // Render grids elements.templatesGrid.innerHTML = ''; state.templates.forEach(tpl => { const card = document.createElement('div'); card.className = 'template-item'; card.id = `template-card-${tpl.Id}`; // Calculate unique keys and page count across all pages const uniqueKeys = []; if (tpl.Pages) { tpl.Pages.forEach(p => { if (p.TemplateKeys) { p.TemplateKeys.forEach(k => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); } }); } const varsCount = uniqueKeys.length; const pageCount = tpl.Pages ? tpl.Pages.length : 0; const displayName = tpl.Name || tpl.Id; const subTitleHtml = `ID: ${tpl.Id}`; card.innerHTML = `

${displayName}

${subTitleHtml} ${varsCount} ${varsCount === 1 ? 'Variable' : 'Variablen'} ${pageCount} ${pageCount === 1 ? 'Seite' : 'Seiten'}
`; elements.templatesGrid.appendChild(card); // Wire listeners document.getElementById(`btn-use-${tpl.Id}`).addEventListener('click', () => { openTemplateEditor(tpl); }); document.getElementById(`btn-rename-${tpl.Id}`).addEventListener('click', async (e) => { e.stopPropagation(); const oldName = tpl.Name || ""; const newName = await showCustomPrompt(`Name für Vorlage "${tpl.Id}"`, oldName, "Neuer Name..."); if (newName !== null) { const trimmedName = newName.trim(); if (trimmedName === "") { showToast("Der Name darf nicht leer sein!", "error"); return; } try { await API.renameTemplate(tpl.Id, trimmedName); showToast("Name erfolgreich zugewiesen!", "success"); loadTemplatesList(); // Update active editor name if it was the renamed one if (state.activeTemplate && state.activeTemplate.Id === tpl.Id) { state.activeTemplate.Name = trimmedName; elements.editorTemplateName.textContent = trimmedName || tpl.Id; } } catch (err) { showToast(`Fehler beim Zuweisen: ${err.message}`, "error"); } } }); document.getElementById(`btn-delete-${tpl.Id}`).addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Möchten Sie die Vorlage "${tpl.Id}" wirklich unwiderruflich löschen?`)) { try { const success = await API.deleteTemplate(tpl.Id); if (success) { showToast("Vorlage erfolgreich gelöscht", "success"); loadTemplatesList(); } } catch (err) { showToast(`Fehler beim Löschen: ${err.message}`, "error"); } } }); }); elements.templatesGrid.classList.remove('hidden'); } catch (err) { // Mark as disconnected if this was a live-mode fetch if (!state.isDemoMode) { state.isConnected = false; updateModeUI(); } elements.templatesLoading.classList.add('hidden'); elements.templatesEmpty.classList.remove('hidden'); throw err; // Re-throw so init() or callers can handle it } } // 9. DYNAMIC EDITOR ENGINE function openTemplateEditor(template) { state.activeTemplate = template; state.zoom = 100; // UI resets elements.editorTemplateName.textContent = template.Name || template.Id; if (elements.editorTemplateId) { elements.editorTemplateId.textContent = template.Id; } elements.previewImage.classList.add('hidden'); elements.previewPlaceholder.classList.remove('hidden'); elements.previewLoadingOverlay.classList.add('hidden'); elements.btnDownloadPreview.classList.add('hidden'); if (elements.btnDownloadAll) elements.btnDownloadAll.classList.add('hidden'); elements.previewCard.classList.remove('fullscreen-active'); elements.btnFullscreen.classList.remove('active'); state.lastRenderedUrl = null; state.renderedUrls = []; state.activePageIndex = 0; if (elements.pageNavigator) elements.pageNavigator.style.display = 'none'; applyZoom(); // Reset settings state.format = 'svg'; elements.formatSvg.classList.add('active'); elements.formatPng.classList.remove('active'); elements.resolutionSettings.classList.add('hidden'); elements.renderWidth.value = ''; elements.renderHeight.value = ''; // Extract unique keys across all pages const uniqueKeys = []; if (template.Pages && template.Pages.length > 0) { template.Pages.forEach(p => { if (p.TemplateKeys) { p.TemplateKeys.forEach(k => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); } }); } else if (template.TemplateKeys) { template.TemplateKeys.forEach(k => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); } // Generate dynamic input elements elements.dynamicInputsArea.innerHTML = ''; if (uniqueKeys.length === 0) { elements.dynamicInputsArea.innerHTML = `

Keine Variablen in diesem Template gefunden.

`; } else { uniqueKeys.forEach(key => { const group = document.createElement('div'); group.className = 'input-group'; // Label const label = document.createElement('label'); label.setAttribute('for', `input-var-${key}`); label.textContent = key.replace(/_/g, ' '); // Clean look // Input field const input = document.createElement('input'); input.type = 'text'; input.id = `input-var-${key}`; input.name = key; input.className = 'custom-input'; input.placeholder = `Wert für ${key}...`; input.required = false; // Allow empty replacement // Setup autocomplete mocks for demo templates if (state.isDemoMode) { if (key === 'Company') input.value = 'TomatenTum Dev'; if (key === 'Name') input.value = 'Tim Müller'; if (key === 'Title') input.value = 'System Architect'; if (key === 'Email') input.value = 'tueem@tomatentum.net'; if (key === 'Phone') input.value = '+49 123 4567890'; if (key === 'Website') input.value = 'git.tomatentum.net'; if (key === 'Recipient') input.value = 'Joshua'; if (key === 'Amount') input.value = '50'; if (key === 'Occasion') input.value = 'Geburtstag'; if (key === 'Sender') input.value = 'Antigravity AI'; if (key === 'Date') input.value = new Date().toLocaleDateString('de-DE'); if (key === 'EventName') input.value = 'TomatenCon 2026'; if (key === 'AttendeeName') input.value = 'Tim Mueller'; if (key === 'AccessLevel') input.value = 'VIP GOLD'; if (key === 'SeatNumber') input.value = 'Reihe 4, Platz 12'; } group.appendChild(label); group.appendChild(input); elements.dynamicInputsArea.appendChild(group); }); } // Refresh pages manager UI renderPagesManager(); navigateTo('editor'); } function renderPagesManager() { if (!state.activeTemplate) return; const pages = state.activeTemplate.Pages || []; elements.editorPagesList.innerHTML = ''; if (pages.length === 0) { const li = document.createElement('li'); li.className = 'font-list-empty'; li.style.padding = '12px'; li.style.fontSize = '12px'; li.style.textAlign = 'center'; li.style.color = 'var(--text-muted)'; li.textContent = 'Keine Seiten vorhanden.'; elements.editorPagesList.appendChild(li); return; } pages.forEach(page => { const li = document.createElement('li'); li.className = 'font-item'; li.style.display = 'flex'; li.style.justifyContent = 'space-between'; li.style.alignItems = 'center'; li.style.gap = '12px'; li.style.padding = '10px 14px'; // Left part: Page Indicator const leftDiv = document.createElement('div'); leftDiv.style.display = 'flex'; leftDiv.style.alignItems = 'center'; leftDiv.style.gap = '8px'; const iconSpan = document.createElement('span'); iconSpan.className = 'font-item-icon'; iconSpan.style.color = '#bd93f9'; iconSpan.innerHTML = ` `; const labelSpan = document.createElement('span'); labelSpan.style.fontWeight = '700'; labelSpan.textContent = `Seite ${page.Page}`; leftDiv.appendChild(iconSpan); leftDiv.appendChild(labelSpan); // Middle part: Keys List const middleDiv = document.createElement('div'); middleDiv.style.display = 'flex'; middleDiv.style.flexWrap = 'wrap'; middleDiv.style.gap = '4px'; middleDiv.style.maxWidth = '180px'; middleDiv.style.justifyContent = 'flex-start'; if (page.TemplateKeys && page.TemplateKeys.length > 0) { page.TemplateKeys.forEach(k => { const keyBadge = document.createElement('span'); keyBadge.style.fontSize = '9px'; keyBadge.style.background = 'rgba(0, 255, 204, 0.08)'; keyBadge.style.color = '#00ffcc'; keyBadge.style.border = '1px solid rgba(0, 255, 204, 0.2)'; keyBadge.style.padding = '1px 4px'; keyBadge.style.borderRadius = '2px'; keyBadge.style.fontFamily = 'monospace'; keyBadge.textContent = k; middleDiv.appendChild(keyBadge); }); } else { const noKeysBadge = document.createElement('span'); noKeysBadge.style.fontSize = '9px'; noKeysBadge.style.color = 'var(--text-muted)'; noKeysBadge.textContent = 'keine Variablen'; middleDiv.appendChild(noKeysBadge); } // Right part: Actions (Delete Button) const rightDiv = document.createElement('div'); // Only allow deleting if more than 1 page exists if (pages.length > 1) { const delBtn = document.createElement('button'); delBtn.className = 'template-btn-delete'; delBtn.title = 'Seite löschen'; delBtn.style.width = '28px'; delBtn.style.height = '28px'; delBtn.style.padding = '0'; delBtn.style.display = 'flex'; delBtn.style.alignItems = 'center'; delBtn.style.justifyContent = 'center'; delBtn.style.border = '2px solid var(--border-color) !important'; delBtn.innerHTML = ` `; delBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Möchten Sie Seite ${page.Page} wirklich aus diesem Template löschen?`)) { try { showToast(`Lösche Seite ${page.Page}...`, "info"); const success = await API.deletePage(state.activeTemplate.Id, page.Page); if (success) { showToast("Seite erfolgreich gelöscht!", "success"); // Re-fetch template to get updated pages list if (!state.isDemoMode) { const freshTemplates = await API.fetchAllTemplates(); const freshActive = freshTemplates.find(t => t.Id === state.activeTemplate.Id); if (freshActive) { state.activeTemplate = freshActive; } } // Reset pagination if needed state.activePageIndex = 0; state.renderedUrls = []; elements.previewImage.classList.add('hidden'); elements.previewPlaceholder.classList.remove('hidden'); elements.btnDownloadPreview.classList.add('hidden'); if (elements.pageNavigator) elements.pageNavigator.style.display = 'none'; // Re-render editor pages manager renderPagesManager(); // Refresh listing grid in background loadTemplatesList(); } } catch (err) { showToast(`Fehler beim Löschen der Seite: ${err.message}`, "error"); } } }); rightDiv.appendChild(delBtn); } li.appendChild(leftDiv); li.appendChild(middleDiv); li.appendChild(rightDiv); elements.editorPagesList.appendChild(li); }); } function updatePagePreview() { if (!state.renderedUrls || state.renderedUrls.length === 0) { if (elements.pageNavigator) elements.pageNavigator.style.display = 'none'; if (elements.btnDownloadAll) elements.btnDownloadAll.classList.add('hidden'); return; } const totalPages = state.renderedUrls.length; if (state.activePageIndex >= totalPages) state.activePageIndex = totalPages - 1; if (state.activePageIndex < 0) state.activePageIndex = 0; const activeUrl = state.renderedUrls[state.activePageIndex]; // Update image src elements.previewLoadingOverlay.classList.remove('hidden'); elements.previewImage.src = activeUrl; // Update page navigator UI if (totalPages > 1) { if (elements.pageNavigator) { elements.pageNavigator.style.display = 'flex'; elements.pageIndicator.textContent = `Seite ${state.activePageIndex + 1} / ${totalPages}`; // Styled disabled state for tactile buttons elements.btnPagePrev.disabled = (state.activePageIndex === 0); elements.btnPagePrev.style.opacity = (state.activePageIndex === 0) ? '0.4' : '1'; elements.btnPagePrev.style.pointerEvents = (state.activePageIndex === 0) ? 'none' : 'auto'; elements.btnPageNext.disabled = (state.activePageIndex === totalPages - 1); elements.btnPageNext.style.opacity = (state.activePageIndex === totalPages - 1) ? '0.4' : '1'; elements.btnPageNext.style.pointerEvents = (state.activePageIndex === totalPages - 1) ? 'none' : 'auto'; } if (elements.btnDownloadAll) elements.btnDownloadAll.classList.remove('hidden'); } else { if (elements.pageNavigator) elements.pageNavigator.style.display = 'none'; if (elements.btnDownloadAll) elements.btnDownloadAll.classList.add('hidden'); } } async function handleRenderSubmit(e) { if (e && typeof e.preventDefault === 'function') { e.preventDefault(); } if (!state.activeTemplate) return; // Toggle button and overlay loading indicators elements.btnSubmitRender.disabled = true; elements.renderSpinner.classList.remove('hidden'); elements.renderBtnIcon.classList.add('hidden'); elements.renderBtnText.textContent = 'Rendere...'; elements.previewLoadingOverlay.classList.remove('hidden'); try { // Extract unique keys across all pages const uniqueKeys = []; if (state.activeTemplate.Pages && state.activeTemplate.Pages.length > 0) { state.activeTemplate.Pages.forEach(p => { if (p.TemplateKeys) { p.TemplateKeys.forEach(k => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); } }); } else if (state.activeTemplate.TemplateKeys) { state.activeTemplate.TemplateKeys.forEach(k => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); } const formValues = {}; uniqueKeys.forEach(key => { const inputEl = document.getElementById(`input-var-${key}`); if (inputEl) { formValues[key] = inputEl.value; } }); const widthVal = elements.renderWidth.value ? parseInt(elements.renderWidth.value) : 0; const heightVal = elements.renderHeight.value ? parseInt(elements.renderHeight.value) : 0; const response = await API.renderTemplate( state.activeTemplate.Id, formValues, state.format, widthVal, heightVal ); state.renderedUrls = response.Urls || []; state.activePageIndex = 0; state.suppressRenderToast = false; // Enable success toast for initial submit // Register preview display handlers elements.previewImage.onload = () => { elements.previewPlaceholder.classList.add('hidden'); elements.previewImage.classList.remove('hidden'); elements.previewLoadingOverlay.classList.add('hidden'); elements.btnDownloadPreview.classList.remove('hidden'); if (!state.suppressRenderToast) { showToast("Grafik erfolgreich gerendert!", "success"); } state.suppressRenderToast = false; // Reset }; elements.previewImage.onerror = () => { elements.previewPlaceholder.classList.remove('hidden'); elements.previewImage.classList.add('hidden'); elements.previewLoadingOverlay.classList.add('hidden'); elements.btnDownloadPreview.classList.add('hidden'); if (elements.btnDownloadAll) elements.btnDownloadAll.classList.add('hidden'); showToast("Fehler beim Laden des gerenderten Bildes.", "error"); }; // Trigger the image load in the UI updatePagePreview(); } catch (err) { console.error("Render submit failed:", err); showToast(`Rendering-Fehler: ${err.message}`, "error"); elements.previewLoadingOverlay.classList.add('hidden'); } finally { elements.btnSubmitRender.disabled = false; elements.renderSpinner.classList.add('hidden'); elements.renderBtnIcon.classList.remove('hidden'); elements.renderBtnText.textContent = 'Rendering erstellen'; } } // Dedicated download function using Blob fetching to force download instead of opening a new tab async function downloadActiveRender() { const activeUrl = state.renderedUrls[state.activePageIndex]; if (!activeUrl) return; try { showToast("Bereite Download vor...", "info"); // Fetch as blob to force browser download dialog (CORS is resolved because /public/ is open) const fileRes = await fetch(activeUrl); if (!fileRes.ok) throw new Error("Grafik konnte nicht vom Server abgerufen werden."); const blob = await fileRes.blob(); const blobUrl = URL.createObjectURL(blob); const downloadLink = document.createElement('a'); downloadLink.href = blobUrl; // Generate nice filename: templateId_page-X_timestamp.format const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const pageNum = state.activePageIndex + 1; downloadLink.download = `${state.activeTemplate.Id}_page-${pageNum}_${timestamp}.${state.format}`; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); // Clean up blob URL after a short delay setTimeout(() => URL.revokeObjectURL(blobUrl), 100); showToast("Grafik erfolgreich heruntergeladen!", "success"); } catch (err) { console.error("Blob download failed, using fallback:", err); // Fallback: direct navigation download if blob fails const downloadLink = document.createElement('a'); downloadLink.href = activeUrl; const pageNum = state.activePageIndex + 1; downloadLink.download = `${state.activeTemplate.Id}_page-${pageNum}.${state.format}`; downloadLink.target = '_blank'; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); showToast("Download über Ausweichmethode gestartet.", "warning"); } } async function downloadAllPages() { if (!state.renderedUrls || state.renderedUrls.length === 0) return; if (typeof JSZip === 'undefined') { showToast("ZIP-Bibliothek wird noch geladen. Bitte kurz warten...", "warning"); return; } showToast("Erstelle ZIP-Datei aller Seiten...", "info"); const zip = new JSZip(); const folder = zip.folder("seiten"); try { for (let i = 0; i < state.renderedUrls.length; i++) { const activeUrl = state.renderedUrls[i]; const fileRes = await fetch(activeUrl); if (!fileRes.ok) throw new Error(`Seite ${i+1} konnte nicht geladen werden.`); const blob = await fileRes.blob(); const pageNum = i + 1; const filename = `${state.activeTemplate.Id}_page-${pageNum}.${state.format}`; folder.file(filename, blob); } const zipBlob = await zip.generateAsync({ type: "blob" }); const blobUrl = URL.createObjectURL(zipBlob); const downloadLink = document.createElement('a'); downloadLink.href = blobUrl; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); downloadLink.download = `${state.activeTemplate.Id}_all-pages_${timestamp}.zip`; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); showToast("ZIP-Datei erfolgreich heruntergeladen!", "success"); } catch (err) { console.error("ZIP packaging failed:", err); showToast(`Fehler beim ZIP-Erstellen: ${err.message}`, "error"); } } // 9.5 FONTS MANAGEMENT ENGINE async function loadFontsList() { elements.fontsEmpty.classList.add('hidden'); elements.fontsList.classList.add('hidden'); elements.fontsLoading.classList.remove('hidden'); try { const fontItems = state.isDemoMode ? MOCK_FONTS : await API.fetchAllFonts(); state.fonts = fontItems || []; elements.fontsLoading.classList.add('hidden'); if (state.fonts.length === 0) { elements.fontsEmpty.classList.remove('hidden'); return; } elements.fontsList.innerHTML = ''; state.fonts.forEach(font => { const li = document.createElement('li'); li.className = 'font-item'; li.innerHTML = ` ${font} `; elements.fontsList.appendChild(li); }); elements.fontsList.classList.remove('hidden'); } catch (err) { console.warn("Failed to load fonts:", err); elements.fontsLoading.classList.add('hidden'); elements.fontsEmpty.classList.remove('hidden'); } } async function handleFontUpload(files) { if (files.length === 0) return; const file = files[0]; const extension = file.name.split('.').pop().toLowerCase(); if (extension !== 'ttf' && extension !== 'otf') { showToast("Es werden nur .ttf und .otf Schriftdateien unterstützt.", "error"); return; } showToast("Lade Schriftart hoch...", "info"); try { // Read file as ArrayBuffer/Blob const reader = new FileReader(); reader.onload = async (e) => { try { const arrayBuffer = e.target.result; const blob = new Blob([arrayBuffer], { type: `font/${extension}` }); await API.uploadFont(blob, file.name); showToast(`Schriftart "${file.name}" erfolgreich installiert!`, "success"); loadFontsList(); } catch (err) { showToast(`Schrift-Upload fehlgeschlagen: ${err.message}`, "error"); } }; reader.readAsArrayBuffer(file); } catch (err) { showToast(`Fehler beim Lesen der Datei: ${err.message}`, "error"); } } // 10. PREVIEW CANVAS ZOOM LOGIC function applyZoom() { elements.previewCanvasContainer.style.setProperty('--zoom', state.zoom / 100); if (elements.zoomBadge) { elements.zoomBadge.textContent = `${state.zoom}%`; } } elements.btnZoomIn.addEventListener('click', () => { if (state.zoom < 1500) { // Dynamic step sizes for comfortable vector scaling if (state.zoom >= 400) { state.zoom += 100; } else if (state.zoom >= 150) { state.zoom += 50; } else { state.zoom += 25; } applyZoom(); } }); elements.btnZoomOut.addEventListener('click', () => { if (state.zoom > 25) { // Dynamic step sizes for comfortable vector scaling if (state.zoom > 400) { state.zoom -= 100; } else if (state.zoom > 150) { state.zoom -= 50; } else { state.zoom -= 25; } applyZoom(); } }); // Toggle Fullscreen Mode in same tab elements.btnFullscreen.addEventListener('click', () => { const isFullscreen = elements.previewCard.classList.toggle('fullscreen-active'); elements.btnFullscreen.classList.toggle('active', isFullscreen); if (isFullscreen) { showToast("Vollbildmodus aktiviert", "info"); } else { showToast("Vollbildmodus beendet", "info"); } // Reset zoom when toggling fullscreen to fit content nicely state.zoom = 100; applyZoom(); }); // Reset Zoom function elements.btnZoomReset.addEventListener('click', () => { state.zoom = 100; applyZoom(); // Center the viewport back to center elements.previewViewport.scrollLeft = (elements.previewViewport.scrollWidth - elements.previewViewport.clientWidth) / 2; elements.previewViewport.scrollTop = (elements.previewViewport.scrollHeight - elements.previewViewport.clientHeight) / 2; showToast("Zoom zurückgesetzt", "info"); }); // 10.5 PREVIEW DRAG & PAN ENGINE function setupPreviewPanning() { const viewport = elements.previewViewport; let isDown = false; let startX; let startY; let scrollLeft; let scrollTop; // Apply grab cursor styling dynamically viewport.style.cursor = 'grab'; viewport.addEventListener('mousedown', (e) => { // Only pan on left mouse button click if (e.button !== 0) return; // Prevent pan if clicking interactive elements inside viewport if (e.target.closest('button') || e.target.closest('a')) return; isDown = true; viewport.style.cursor = 'grabbing'; startX = e.pageX - viewport.offsetLeft; startY = e.pageY - viewport.offsetTop; scrollLeft = viewport.scrollLeft; scrollTop = viewport.scrollTop; }); viewport.addEventListener('mouseleave', () => { isDown = false; viewport.style.cursor = 'grab'; }); viewport.addEventListener('mouseup', () => { isDown = false; viewport.style.cursor = 'grab'; }); viewport.addEventListener('mousemove', (e) => { if (!isDown) return; e.preventDefault(); // Prevent text selection while dragging const x = e.pageX - viewport.offsetLeft; const y = e.pageY - viewport.offsetTop; const walkX = (x - startX) * 1.5; const walkY = (y - startY) * 1.5; viewport.scrollLeft = scrollLeft - walkX; viewport.scrollTop = scrollTop - walkY; }); } // 11. UPLOAD DRAG AND DROP FLOW function setupDragAndDrop() { const dropZone = elements.dropZone; const fileInput = elements.fileInput; ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('dragover'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('dragover'); }, false); }); dropZone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; handleFiles(files); }); fileInput.addEventListener('change', (e) => { handleFiles(fileInput.files); }); } function handleFiles(files) { if (files.length === 0) return; const file = files[0]; if (file.type !== 'image/svg+xml' && !file.name.endsWith('.svg')) { showToast("Es werden nur gültige .svg Dateien unterstützt.", "error"); return; } (async () => { // Prompt for template name immediately, block nameless templates let templateName = ''; while (true) { const input = await showCustomPrompt( "Name für neues Template festlegen", file.name.replace('.svg', ''), "z.B. Visitenkarte..." ); if (input === null) { showToast("Upload abgebrochen.", "warning"); return; // User cancelled } if (input.trim() !== '') { templateName = input.trim(); break; } showToast("Der Vorlagen-Name darf nicht leer sein! Bitte geben Sie einen Namen ein.", "error"); } showToast("Lade SVG Template hoch...", "info"); try { // Pass custom name to uploadSVG so that it is assigned directly in Demo Mode or Live Mode via ?name= query await API.uploadSVG(file, file.name, templateName); showToast(`Template "${templateName}" erfolgreich registriert!`, "success"); loadTemplatesList(); } catch (err) { showToast(`Upload fehlgeschlagen: ${err.message}`, "error"); } })(); } // 12. GENERAL INTERFACES WIRING function wireControls() { // Navigation back home button elements.btnBackHome.addEventListener('click', () => { navigateTo('home'); }); // Toggle demo mode statically elements.btnToggleDemo.addEventListener('click', async () => { if (!state.isDemoMode) { // Currently LIVE → switch to Demo enableDemoMode(true); showToast("Demo-Modus aktiv!", "success"); } else { // Currently DEMO → try to switch to Live showToast("Verbinde mit Live-API...", "info"); try { const res = await fetch(`${API_BASE_URL}/svg/`, { headers: API.getHeaders() }); if (!res.ok) { if (res.status === 401) { throw new Error("Nicht autorisiert. Bitte prüfen Sie den statischen Token."); } throw new Error(`Server-Fehler: ${res.status}`); } // Connection successful state.isDemoMode = false; state.isConnected = true; updateModeUI(); await loadTemplatesList(); await loadFontsList(); showToast("Erfolgreich mit Live-API verbunden!", "success"); } catch (err) { console.warn("Live API connection failed:", err); showToast(`Backend nicht erreichbar: ${err.message}`, "error"); // Stay in Demo Mode, mark not connected state.isConnected = false; updateModeUI(); } } }); // Output Formats selection toggler elements.formatSvg.addEventListener('click', () => { state.format = 'svg'; elements.formatSvg.classList.add('active'); elements.formatPng.classList.remove('active'); elements.resolutionSettings.classList.add('hidden'); }); elements.formatPng.addEventListener('click', () => { state.format = 'png'; elements.formatPng.classList.add('active'); elements.formatSvg.classList.remove('active'); elements.resolutionSettings.classList.remove('hidden'); }); // Render Click trigger elements.btnSubmitRender.addEventListener('click', handleRenderSubmit); // Font upload triggers elements.btnUploadFont.addEventListener('click', () => { elements.fontFileInput.click(); }); elements.fontFileInput.addEventListener('change', () => { handleFontUpload(elements.fontFileInput.files); }); // Preview download button trigger elements.btnDownloadPreview.addEventListener('click', downloadActiveRender); // Preview download all pages button trigger if (elements.btnDownloadAll) { elements.btnDownloadAll.addEventListener('click', downloadAllPages); } // Active template rename button trigger elements.btnRenameActive.addEventListener('click', async () => { if (!state.activeTemplate) return; const tpl = state.activeTemplate; const oldName = tpl.Name || ""; const newName = await showCustomPrompt("Name der Vorlage ändern", oldName, "Neuer Name..."); if (newName !== null) { const trimmedName = newName.trim(); if (trimmedName === "") { showToast("Der Name darf nicht leer sein!", "error"); return; } try { await API.renameTemplate(tpl.Id, trimmedName); showToast("Name erfolgreich zugewiesen!", "success"); // Update local state state.activeTemplate.Name = trimmedName; elements.editorTemplateName.textContent = trimmedName || tpl.Id; // Refresh home list in background so it's fresh when navigating back loadTemplatesList(); } catch (err) { showToast(`Fehler beim Zuweisen: ${err.message}`, "error"); } } }); // Pagination controls elements.btnPagePrev.addEventListener('click', () => { if (state.activePageIndex > 0) { state.activePageIndex--; state.suppressRenderToast = true; updatePagePreview(); } }); elements.btnPageNext.addEventListener('click', () => { if (state.activePageIndex < state.renderedUrls.length - 1) { state.activePageIndex++; state.suppressRenderToast = true; updatePagePreview(); } }); // Add page file input triggers elements.btnAddPage.addEventListener('click', () => { elements.pageFileInput.click(); }); elements.pageFileInput.addEventListener('change', async () => { const files = elements.pageFileInput.files; if (!files || files.length === 0) return; if (!state.activeTemplate) return; showToast("Füge Seite(n) zu Template hinzu...", "info"); try { const pages = await API.addPage(state.activeTemplate.Id, files); showToast("Seite(n) erfolgreich hinzugefügt!", "success"); // Re-fetch active template to get fresh data if (!state.isDemoMode) { const freshTemplates = await API.fetchAllTemplates(); const freshActive = freshTemplates.find(t => t.Id === state.activeTemplate.Id); if (freshActive) { state.activeTemplate = freshActive; } } // Update pages list renderPagesManager(); // Refresh variables form in case new pages introduced new variables openTemplateEditor(state.activeTemplate); // Refresh home grid listing in background loadTemplatesList(); } catch (err) { showToast(`Fehler beim Hinzufügen: ${err.message}`, "error"); } finally { elements.pageFileInput.value = ''; } }); } // 13. INITIALIZATION ON BOOT async function init() { setupDragAndDrop(); setupPreviewPanning(); wireControls(); updateModeUI(); // Try to connect to live API on start; if it fails, stay in live mode // (user can manually switch to Demo Mode via the button) try { await Promise.all([ loadTemplatesList(), loadFontsList() ]); } catch (err) { console.warn("Initial API connection failed:", err); elements.templatesLoading.classList.add('hidden'); elements.templatesEmpty.classList.remove('hidden'); showToast("Backend nicht erreichbar. Drücken Sie 'Demo Modus' um Beispieldaten zu laden.", "warning"); } } document.addEventListener('DOMContentLoaded', init);