/* ==========================================================================
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": `
`,
"gift_certificate_1": `
`,
"gift_certificate_2": `
`,
"event_badge_1": `
`
};
// 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 = `
`;
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 = `
Keine Variablen in diesem Template gefunden.