1956 lines
77 KiB
JavaScript
1956 lines
77 KiB
JavaScript
/* ==========================================================================
|
|
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": `
|
|
<svg viewBox="0 0 600 350" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#0f172a;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#1e1b4b;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
<!-- Background -->
|
|
<rect width="100%" height="100%" fill="url(#grad)" rx="15" />
|
|
<!-- Design accents -->
|
|
<circle cx="500" cy="80" r="120" fill="#6366f1" opacity="0.15" />
|
|
<circle cx="550" cy="120" r="80" fill="#a855f7" opacity="0.15" />
|
|
<path d="M 0,350 L 250,350 L 150,220 L 0,260 Z" fill="#6366f1" opacity="0.1" />
|
|
|
|
<!-- Logo area -->
|
|
<g transform="translate(45, 45)">
|
|
<polygon points="12 2 2 7 12 12 22 7 12 2" fill="none" stroke="#a855f7" stroke-width="2" stroke-linejoin="round"/>
|
|
<polyline points="2 17 12 22 22 17" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/>
|
|
<polyline points="2 12 12 17 22 12" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/>
|
|
<text x="32" y="16" fill="#ffffff" font-family="'Outfit', sans-serif" font-weight="800" font-size="18">{{ Company }}</text>
|
|
</g>
|
|
|
|
<!-- Name and Title -->
|
|
<text x="45" y="160" fill="#ffffff" font-family="'Outfit', sans-serif" font-weight="700" font-size="28" letter-spacing="-0.5">{{ Name }}</text>
|
|
<text x="45" y="190" fill="#a855f7" font-family="'Inter', sans-serif" font-weight="600" font-size="14" letter-spacing="1.5" text-transform="uppercase">{{ Title }}</text>
|
|
|
|
<!-- Divider line -->
|
|
<line x1="45" y1="215" x2="550" y2="215" stroke="#334155" stroke-width="1" />
|
|
|
|
<!-- Contact details -->
|
|
<g transform="translate(45, 255)" font-family="'Inter', sans-serif" font-size="12" fill="#94a3b8">
|
|
<!-- Email -->
|
|
<text x="0" y="0" font-weight="600" fill="#6366f1">E-MAIL:</text>
|
|
<text x="60" y="0" fill="#e2e8f0">{{ Email }}</text>
|
|
|
|
<!-- Phone -->
|
|
<text x="0" y="25" font-weight="600" fill="#6366f1">TELEFON:</text>
|
|
<text x="60" y="25" fill="#e2e8f0">{{ Phone }}</text>
|
|
|
|
<!-- Website -->
|
|
<text x="300" y="0" font-weight="600" fill="#6366f1">WEB:</text>
|
|
<text x="340" y="0" fill="#e2e8f0">{{ Website }}</text>
|
|
</g>
|
|
</svg>
|
|
`,
|
|
"gift_certificate_1": `
|
|
<svg viewBox="0 0 600 350" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="gold-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#d97706;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="#111827" rx="15" />
|
|
<rect x="15" y="15" width="570" height="320" fill="none" stroke="url(#gold-grad)" stroke-width="2" rx="10" opacity="0.6"/>
|
|
|
|
<text x="300" y="65" text-anchor="middle" fill="url(#gold-grad)" font-family="'Outfit', sans-serif" font-weight="800" font-size="26" letter-spacing="3" text-transform="uppercase">Gutschein</text>
|
|
<text x="300" y="90" text-anchor="middle" fill="#94a3b8" font-family="'Inter', sans-serif" font-weight="500" font-size="12" text-transform="uppercase" letter-spacing="1">Für das Ereignis: {{ Occasion }}</text>
|
|
|
|
<text x="300" y="150" text-anchor="middle" fill="#ffffff" font-family="'Outfit', sans-serif" font-weight="700" font-size="36" fill="url(#gold-grad)">{{ Amount }} €</text>
|
|
|
|
<g transform="translate(100, 210)" font-family="'Inter', sans-serif" font-size="13" fill="#e2e8f0">
|
|
<text x="0" y="0" fill="#94a3b8">Empfänger:</text>
|
|
<text x="100" y="0" font-weight="600">{{ Recipient }}</text>
|
|
|
|
<text x="0" y="30" fill="#94a3b8">Schenker:</text>
|
|
<text x="100" y="30" font-weight="600">{{ Sender }}</text>
|
|
</g>
|
|
|
|
<text x="300" y="300" text-anchor="middle" fill="#6b7280" font-family="'Inter', sans-serif" font-size="11">Ausgestellt am: {{ Date }}</text>
|
|
</svg>
|
|
`,
|
|
"gift_certificate_2": `
|
|
<svg viewBox="0 0 600 350" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="100%" height="100%" fill="#111827" rx="15" />
|
|
<rect x="15" y="15" width="570" height="320" fill="none" stroke="#f59e0b" stroke-width="1" rx="10" opacity="0.3"/>
|
|
<text x="300" y="80" text-anchor="middle" fill="#f59e0b" font-family="'Outfit', sans-serif" font-weight="700" font-size="18" letter-spacing="1">GUTSCHEINBEDINGUNGEN</text>
|
|
<g transform="translate(60, 130)" font-family="'Inter', sans-serif" font-size="11" fill="#94a3b8">
|
|
<text x="0" y="0">1. Dieser Gutschein ist ab Ausstellungsdatum 3 Jahre gültig.</text>
|
|
<text x="0" y="25">2. Eine Barauszahlung des Gutscheinwerts ist ausgeschlossen.</text>
|
|
<text x="0" y="50">3. Der Gutschein kann für alle Dienstleistungen eingelöst werden.</text>
|
|
<text x="0" y="75">4. Bei Verlust oder Beschädigung erfolgt kein Ersatz.</text>
|
|
</g>
|
|
<text x="300" y="290" text-anchor="middle" fill="#6b7280" font-family="'Inter', sans-serif" font-size="10">Ausgestellt von: {{ Sender }} am {{ Date }}</text>
|
|
</svg>
|
|
`,
|
|
"event_badge_1": `
|
|
<svg viewBox="0 0 400 600" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="purple-fade" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#1e1b4b;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="url(#purple-fade)" rx="20"/>
|
|
<rect x="15" y="15" width="370" height="570" fill="none" stroke="#f3f4f6" stroke-width="1" rx="15" opacity="0.1"/>
|
|
|
|
<!-- Badge Notch -->
|
|
<path d="M 150,0 L 250,0 L 230,30 L 170,30 Z" fill="#0f172a" />
|
|
<circle cx="200" cy="15" r="5" fill="#334155" />
|
|
|
|
<text x="200" y="90" text-anchor="middle" fill="#a78bfa" font-family="'Outfit', sans-serif" font-weight="800" font-size="16" letter-spacing="3" text-transform="uppercase">{{ EventName }}</text>
|
|
|
|
<!-- Attendee Name -->
|
|
<text x="200" y="240" text-anchor="middle" fill="#ffffff" font-family="'Outfit', sans-serif" font-weight="700" font-size="28" letter-spacing="-0.5">{{ AttendeeName }}</text>
|
|
|
|
<rect x="100" y="280" width="200" height="35" fill="#0f172a" rx="8" />
|
|
<text x="200" y="303" text-anchor="middle" fill="#10b981" font-family="'Inter', sans-serif" font-weight="700" font-size="13" letter-spacing="1">ACCESS: {{ AccessLevel }}</text>
|
|
|
|
<text x="200" y="380" text-anchor="middle" fill="#94a3b8" font-family="'Inter', sans-serif" font-size="13">Sitzplatz: <tspan font-weight="600" fill="#ffffff">{{ SeatNumber }}</tspan></text>
|
|
|
|
<!-- Mock Barcode -->
|
|
<g transform="translate(100, 460)">
|
|
<rect x="0" y="0" width="6" height="50" fill="#ffffff" />
|
|
<rect x="10" y="0" width="3" height="50" fill="#ffffff" />
|
|
<rect x="17" y="0" width="9" height="50" fill="#ffffff" />
|
|
<rect x="30" y="0" width="3" height="50" fill="#ffffff" />
|
|
<rect x="37" y="0" width="6" height="50" fill="#ffffff" />
|
|
<rect x="47" y="0" width="12" height="50" fill="#ffffff" />
|
|
<rect x="63" y="0" width="3" height="50" fill="#ffffff" />
|
|
<rect x="70" y="0" width="9" height="50" fill="#ffffff" />
|
|
<rect x="83" y="0" width="6" height="50" fill="#ffffff" />
|
|
<rect x="93" y="0" width="15" height="50" fill="#ffffff" />
|
|
<rect x="112" y="0" width="3" height="50" fill="#ffffff" />
|
|
<rect x="120" y="0" width="9" height="50" fill="#ffffff" />
|
|
<rect x="133" y="0" width="6" height="50" fill="#ffffff" />
|
|
<rect x="143" y="0" width="12" height="50" fill="#ffffff" />
|
|
<rect x="160" y="0" width="3" height="50" fill="#ffffff" />
|
|
<rect x="167" y="0" width="9" height="50" fill="#ffffff" />
|
|
<rect x="180" y="0" width="6" height="50" fill="#ffffff" />
|
|
<rect x="190" y="0" width="10" height="50" fill="#ffffff" />
|
|
</g>
|
|
<text x="200" y="535" text-anchor="middle" fill="#6b7280" font-family="'Inter', sans-serif" font-size="10" letter-spacing="4">#0094-1184-7492</text>
|
|
</svg>
|
|
`
|
|
};
|
|
|
|
// 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 = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
} else if (type === 'error') {
|
|
iconSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>';
|
|
} else if (type === 'warning') {
|
|
iconSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
|
|
} else {
|
|
iconSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
|
|
}
|
|
|
|
toast.innerHTML = `
|
|
<span class="toast-icon">${iconSvg}</span>
|
|
<span class="toast-message">${message}</span>
|
|
`;
|
|
|
|
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 = `<span class="template-id-sub" style="font-size: 10px; color: var(--text-muted); margin-top: 2px; display: block;">ID: <code style="font-size: 9px; color: #ff79c6 !important; background: #08090d; border: 1px solid var(--border-color); padding: 1px 3px; border-radius: 2px;">${tpl.Id}</code></span>`;
|
|
|
|
card.innerHTML = `
|
|
<div class="template-meta">
|
|
<div class="template-icon-wrapper">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
|
|
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
|
|
<polyline points="2 17 12 22 22 17"></polyline>
|
|
<polyline points="2 12 12 17 22 12"></polyline>
|
|
</svg>
|
|
</div>
|
|
<h3 class="template-name" title="${tpl.Id}">${displayName}</h3>
|
|
${subTitleHtml}
|
|
<span class="template-variable-count" style="margin-top: 6px; display: inline-flex; gap: 8px; flex-wrap: wrap;">
|
|
<span>${varsCount} ${varsCount === 1 ? 'Variable' : 'Variablen'}</span>
|
|
<span style="opacity: 0.5;">•</span>
|
|
<span>${pageCount} ${pageCount === 1 ? 'Seite' : 'Seiten'}</span>
|
|
</span>
|
|
</div>
|
|
<div class="template-actions">
|
|
<button class="template-btn-use" id="btn-use-${tpl.Id}">
|
|
<span>Auswählen</span>
|
|
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none">
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
<polyline points="12 5 19 12 12 19"></polyline>
|
|
</svg>
|
|
</button>
|
|
<button class="template-btn-rename" id="btn-rename-${tpl.Id}" title="Name zuweisen/ändern">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
<path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
</svg>
|
|
</button>
|
|
<button class="template-btn-delete" id="btn-delete-${tpl.Id}" title="Vorlage löschen">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
|
<polyline points="3 6 5 6 21 6"></polyline>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="empty-state" style="padding: 20px;">
|
|
<p style="font-size: 13px; color: var(--text-secondary);">Keine Variablen in diesem Template gefunden.</p>
|
|
</div>
|
|
`;
|
|
} 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 = `
|
|
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
<polyline points="14 2 14 8 20 8"></polyline>
|
|
</svg>
|
|
`;
|
|
|
|
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 = `
|
|
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none">
|
|
<polyline points="3 6 5 6 21 6"></polyline>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
</svg>
|
|
`;
|
|
|
|
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 = `
|
|
<span class="font-item-icon">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none">
|
|
<polyline points="4 7 4 4 20 4 20 7"></polyline>
|
|
<line x1="9" y1="20" x2="15" y2="20"></line>
|
|
<line x1="12" y1="4" x2="12" y2="20"></line>
|
|
</svg>
|
|
</span>
|
|
<span>${font}</span>
|
|
`;
|
|
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);
|