320 lines
22 KiB
Plaintext
320 lines
22 KiB
Plaintext
---
|
||
import '@fontsource-variable/public-sans';
|
||
import '@fontsource-variable/fraunces';
|
||
import '../../../../styles/admin.css';
|
||
import { getPageById, listFeatured, listProducts, listActiveSlides, getSettings, formatPrice } from '../../../../lib/store.js';
|
||
import { BLOCK_TYPES } from '../../../../lib/blocks.js';
|
||
import { adminBase } from '../../../../lib/auth.js';
|
||
|
||
const base = adminBase();
|
||
const { id } = Astro.params;
|
||
const page = getPageById(id);
|
||
if (!page) return Astro.redirect(base + '/inhalte?tab=pages');
|
||
|
||
const settings = getSettings();
|
||
const accent = settings.brand_accent || '#b8566a';
|
||
const accentDark = settings.brand_accent_dark || '#8d3f50';
|
||
|
||
// Daten für die Client-Vorschau
|
||
const products = listProducts().map(p => ({ slug: p.slug, name: p.name, shortName: p.shortName, category: p.category, cardImage: p.cardImage, badge: p.badge, price: formatPrice(p.priceCents) }));
|
||
const featuredSlugs = listFeatured().map(p => p.slug);
|
||
const slides = listActiveSlides().map(s => ({ image: s.image, headline: s.headline, subline: s.subline, link: s.link }));
|
||
const categories = [...new Set(products.map(p => p.category).filter(Boolean))];
|
||
|
||
const data = {
|
||
pageId: page.id, slug: page.slug, title: page.title,
|
||
blocks: (Array.isArray(page.blocks) && page.blocks.length) ? page.blocks : (page.body && String(page.body).trim() ? [{ type: 'richtext', html: page.body }] : []), blockTypes: BLOCK_TYPES,
|
||
products, featuredSlugs, slides, categories,
|
||
saveUrl: '/api/admin-page-blocks', base,
|
||
shopOrigin: '',
|
||
};
|
||
const dataJson = JSON.stringify(data);
|
||
---
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="robots" content="noindex" />
|
||
<title>Editor · {page.title}</title>
|
||
<style is:inline set:html={`:root{--accent:${accent};--accent-dark:${accentDark};}`}></style>
|
||
<style is:inline>
|
||
html,body{height:100%;margin:0}
|
||
.ed-shell{display:grid;grid-template-rows:auto 1fr;height:100vh;background:var(--s-bg);font-family:var(--s-font)}
|
||
.ed-top{display:flex;align-items:center;gap:14px;padding:11px 18px;background:color-mix(in srgb,var(--s-bg) 86%, transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--s-border);position:sticky;top:0;z-index:20}
|
||
.ed-top .ed-title{font-family:var(--s-display);font-size:17px;font-weight:560;color:var(--s-ink);letter-spacing:-.01em}
|
||
.ed-top .ed-slug{font-size:12px;color:var(--s-faint)}
|
||
.ed-top .spacer{flex:1}
|
||
.ed-cols{display:grid;grid-template-columns:262px 1fr 304px;min-height:0;overflow:hidden}
|
||
.ed-pane{overflow:auto;height:100%}
|
||
.ed-left{border-right:1px solid var(--s-border);background:var(--s-bg);padding:14px}
|
||
.ed-right{border-left:1px solid var(--s-border);background:var(--s-surface);padding:18px}
|
||
.ed-center{background:var(--s-sunken);display:flex;flex-direction:column;align-items:center;padding:20px;gap:14px}
|
||
.ed-sec{font-size:10px;text-transform:uppercase;letter-spacing:.09em;color:var(--s-faint);font-weight:700;margin:6px 4px 8px}
|
||
.ed-add{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:16px}
|
||
.ed-add button{display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px 6px;border:1px solid var(--s-border);border-radius:10px;background:var(--s-surface);cursor:pointer;font-size:11px;font-weight:600;color:var(--s-text);transition:.13s;font-family:inherit}
|
||
.ed-add button:hover{border-color:var(--accent);color:var(--accent-dark);transform:translateY(-1px);box-shadow:var(--s-shadow)}
|
||
.ed-add svg{width:18px;height:18px;color:var(--s-subtle)}
|
||
.ed-add button:hover svg{color:var(--accent)}
|
||
.ed-list{display:flex;flex-direction:column;gap:7px}
|
||
.ed-item{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--s-border);border-radius:10px;background:var(--s-surface);cursor:pointer;transition:.12s}
|
||
.ed-item:hover{border-color:var(--s-border-2)}
|
||
.ed-item.active{border-color:var(--accent);box-shadow:0 0 0 2px var(--s-acc-ring)}
|
||
.ed-item .grip{color:var(--s-faint);cursor:grab;font-size:14px;line-height:1}
|
||
.ed-item .lbl{flex:1;font-size:13px;font-weight:600;color:var(--s-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.ed-item .acts{display:flex;gap:3px}
|
||
.ed-item .acts button{width:24px;height:24px;border:none;background:transparent;color:var(--s-subtle);cursor:pointer;border-radius:6px;font-size:13px;display:grid;place-items:center}
|
||
.ed-item .acts button:hover{background:var(--s-bg);color:var(--s-ink)}
|
||
.ed-item.dragging{opacity:.4}
|
||
.ed-empty{padding:30px 14px;text-align:center;color:var(--s-faint);font-size:12.5px}
|
||
.ed-frame-wrap{width:100%;flex:1;display:flex;justify-content:center}
|
||
.ed-frame{width:100%;max-width:1180px;height:100%;border:1px solid var(--s-border);border-radius:12px;background:#fff;box-shadow:var(--s-shadow);transition:max-width .25s var(--s-ease)}
|
||
.ed-frame.mobile{max-width:400px}
|
||
.ed-device{display:inline-flex;background:var(--s-sunken);border:1px solid var(--s-border);border-radius:9px;padding:3px;gap:3px}
|
||
.ed-device button{border:none;background:transparent;padding:5px 12px;border-radius:7px;font-size:12px;font-weight:600;color:var(--s-subtle);cursor:pointer;font-family:inherit}
|
||
.ed-device button.active{background:var(--s-surface);color:var(--s-ink);box-shadow:var(--s-shadow)}
|
||
.ed-field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
|
||
.ed-field label{font-size:12.5px;font-weight:600;color:var(--s-text)}
|
||
.ed-right .s-input,.ed-right .s-textarea,.ed-right .s-select{font-size:13px}
|
||
.ed-imgrow{display:flex;gap:7px}
|
||
.ed-imgrow .s-input{flex:1}
|
||
.ed-nosel{padding:36px 10px;text-align:center;color:var(--s-faint);font-size:13px}
|
||
.ed-mediabtn{font-size:11px}
|
||
</style>
|
||
</head>
|
||
<body class="admin-body">
|
||
<div class="ed-shell">
|
||
<header class="ed-top">
|
||
<a class="s-btn s-btn-sm" href={base + '/inhalte?tab=pages'}>‹ Schließen</a>
|
||
<div><div class="ed-title">{page.title}</div><div class="ed-slug">/seite/{page.slug}</div></div>
|
||
<div class="spacer"></div>
|
||
<div class="ed-device" id="edDevice">
|
||
<button data-dev="desktop" class="active">Desktop</button>
|
||
<button data-dev="mobile">Mobil</button>
|
||
</div>
|
||
<a class="s-btn s-btn-sm" href={'/seite/' + page.slug} target="_blank">Vorschau im Shop ↗</a>
|
||
<button class="s-btn s-btn-primary s-btn-sm" id="edSave">Speichern</button>
|
||
</header>
|
||
<div class="ed-cols">
|
||
<aside class="ed-pane ed-left">
|
||
<div class="ed-sec">Block hinzufügen</div>
|
||
<div class="ed-add" id="edAdd"></div>
|
||
<div class="ed-sec">Blöcke</div>
|
||
<div class="ed-list" id="edList"></div>
|
||
</aside>
|
||
<main class="ed-pane ed-center">
|
||
<div class="ed-frame-wrap"><iframe class="ed-frame" id="edFrame" title="Vorschau"></iframe></div>
|
||
</main>
|
||
<aside class="ed-pane ed-right" id="edSettings">
|
||
<div class="ed-nosel">Wähle links einen Block, um ihn zu bearbeiten.</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="s-toasts" id="toasts" aria-live="polite"></div>
|
||
|
||
<script is:inline define:vars={{ dataJson }}>
|
||
(function () {
|
||
var D = JSON.parse(dataJson);
|
||
var blocks = Array.isArray(D.blocks) ? D.blocks : [];
|
||
var selected = blocks.length ? 0 : -1;
|
||
var device = 'desktop';
|
||
var uid = 1;
|
||
blocks.forEach(function (b) { if (!b._id) b._id = 'b' + (uid++); });
|
||
|
||
function toast(msg, kind) {
|
||
var c = document.getElementById('toasts'); if (!c) return;
|
||
var t = document.createElement('div'); t.className = 's-toast ' + (kind || 'ok'); t.textContent = msg;
|
||
c.appendChild(t); requestAnimationFrame(function () { t.classList.add('show'); });
|
||
setTimeout(function () { t.classList.remove('show'); setTimeout(function () { t.remove(); }, 250); }, 2600);
|
||
}
|
||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
||
function meta(type) { for (var i = 0; i < D.blockTypes.length; i++) if (D.blockTypes[i].key === type) return D.blockTypes[i]; return null; }
|
||
function defaults(type) { var m = meta(type); return m ? JSON.parse(JSON.stringify(m.defaults)) : {}; }
|
||
|
||
// ---- Block -> HTML (Vorschau, spiegelt BlockRenderer) ----
|
||
function productsFor(b) {
|
||
var limit = Math.max(1, Math.min(12, Number(b.limit) || 4));
|
||
var items;
|
||
if (b.source === 'all') items = D.products.slice();
|
||
else if (b.source === 'category' && b.category) items = D.products.filter(function (p) { return p.category === b.category; });
|
||
else items = D.products.filter(function (p) { return D.featuredSlugs.indexOf(p.slug) > -1; });
|
||
if (!items.length) items = D.products.filter(function (p) { return D.featuredSlugs.indexOf(p.slug) > -1; });
|
||
if (!items.length) items = D.products.slice();
|
||
return items.slice(0, limit);
|
||
}
|
||
function prodCard(p) {
|
||
return '<a class="prod-card" href="#" onclick="return false">' +
|
||
'<div class="prod-media">' + (p.cardImage ? '<img src="' + esc(p.cardImage) + '" alt="">' : '') + (p.badge ? '<span class="prod-badge">' + esc(p.badge) + '</span>' : '') + '</div>' +
|
||
'<div class="prod-info"><span class="prod-cat">' + esc(p.category) + '</span><span class="prod-name">' + esc(p.shortName || p.name) + '</span><span class="prod-price">' + esc(p.price) + '</span></div></a>';
|
||
}
|
||
function blockHtml(b) {
|
||
var spacer = { small: 28, medium: 56, large: 96 };
|
||
switch (b.type) {
|
||
case 'hero':
|
||
return '<section class="blk blk-hero ' + (b.image ? 'has-img' : '') + ' align-' + (b.align || 'center') + '"' + (b.image ? ' style="--hero-img:url(\'' + esc(b.image) + '\')"' : '') + '><div class="wrap blk-hero-inner">' +
|
||
(b.headline ? '<h1>' + esc(b.headline) + '</h1>' : '') + (b.subline ? '<p class="blk-hero-sub">' + esc(b.subline) + '</p>' : '') +
|
||
(b.cta_text ? '<a class="btn btn-primary btn-lg" href="#" onclick="return false">' + esc(b.cta_text) + '</a>' : '') + '</div></section>';
|
||
case 'richtext':
|
||
return '<section class="blk blk-rich"><div class="wrap prose">' + (b.html || '') + '</div></section>';
|
||
case 'image':
|
||
return '<section class="blk blk-image"><div class="wrap img-' + (b.width || 'wide') + '">' + (b.image ? '<img src="' + esc(b.image) + '" alt="">' : '<div style="aspect-ratio:16/7;background:#eee;border-radius:14px"></div>') + (b.caption ? '<p class="blk-cap">' + esc(b.caption) + '</p>' : '') + '</div></section>';
|
||
case 'gallery':
|
||
var cols = Math.max(2, Math.min(4, Number(b.columns) || 3));
|
||
var imgs = (b.images || []).map(function (s) { return '<img src="' + esc(s) + '" alt="">'; }).join('');
|
||
return '<section class="blk blk-gallery"><div class="wrap"><div class="blk-gal-grid" style="grid-template-columns:repeat(' + cols + ',1fr)">' + (imgs || '<div style="grid-column:1/-1;padding:30px;text-align:center;color:#999">Noch keine Bilder</div>') + '</div></div></section>';
|
||
case 'slider':
|
||
if (!D.slides.length) return '<section class="blk"><div class="wrap" style="padding:30px;text-align:center;color:#999">Slider (keine aktiven Slides)</div></section>';
|
||
var s0 = D.slides[0];
|
||
return '<section class="blk blk-sliderref"><div class="wrap"><div class="slider"><div class="slides"><div class="slide">' + (s0.image ? '<img src="' + esc(s0.image) + '" alt="">' : '') + '<div class="slide-cap"><h2>' + esc(s0.headline) + '</h2>' + (s0.subline ? '<p>' + esc(s0.subline) + '</p>' : '') + '</div></div></div></div></div></section>';
|
||
case 'features':
|
||
var fi = (b.items || []).map(function (it) { return '<div class="blk-feat"><h3>' + esc(it.title) + '</h3><p>' + esc(it.text) + '</p></div>'; }).join('');
|
||
return '<section class="blk blk-features"><div class="wrap">' + (b.headline ? '<h2 class="blk-h2">' + esc(b.headline) + '</h2>' : '') + '<div class="blk-feat-grid">' + fi + '</div></div></section>';
|
||
case 'productgrid':
|
||
var pc = productsFor(b).map(prodCard).join('');
|
||
return '<section class="blk blk-products"><div class="wrap">' + (b.headline ? '<h2 class="blk-h2">' + esc(b.headline) + '</h2>' : '') + '<div class="prod-grid">' + (pc || '<div style="padding:30px;color:#999">Keine Produkte</div>') + '</div></div></section>';
|
||
case 'cta':
|
||
return '<section class="blk blk-cta"><div class="wrap"><div class="blk-cta-box">' + (b.headline ? '<h2>' + esc(b.headline) + '</h2>' : '') + (b.text ? '<p>' + esc(b.text) + '</p>' : '') + (b.cta_text ? '<a class="btn btn-primary btn-lg" href="#" onclick="return false">' + esc(b.cta_text) + '</a>' : '') + '</div></div></section>';
|
||
case 'spacer':
|
||
return '<div class="blk-spacer" style="height:' + (spacer[b.size] || 56) + 'px"></div>';
|
||
case 'html':
|
||
return '<section class="blk blk-html">' + (b.code || '') + '</section>';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
var frame = document.getElementById('edFrame');
|
||
function renderPreview() {
|
||
var body = blocks.map(blockHtml).join('\n') || '<div style="padding:80px 24px;text-align:center;color:#aaa;font-family:sans-serif">Diese Seite ist noch leer.<br>Füge links einen Block hinzu.</div>';
|
||
var doc = '<!doctype html><html lang="de"><head><meta charset="utf-8"><link rel="stylesheet" href="/styles/global.css">' +
|
||
'<style>:root{--accent:' + getComputedStyle(document.documentElement).getPropertyValue('--accent') + ';--accent-dark:' + getComputedStyle(document.documentElement).getPropertyValue('--accent-dark') + ';}body{margin:0}</style>' +
|
||
'</head><body>' + body + '</body></html>';
|
||
// CSS via @fontsource ist gebundlet; wir referenzieren global.css statisch — fällt aus, aber Klassen reichen für Layout.
|
||
var d = frame.contentDocument || frame.contentWindow.document;
|
||
d.open(); d.write(doc); d.close();
|
||
}
|
||
|
||
// ---- Linke Liste ----
|
||
var listEl = document.getElementById('edList');
|
||
function renderList() {
|
||
listEl.innerHTML = '';
|
||
if (!blocks.length) { listEl.innerHTML = '<div class="ed-empty">Noch keine Blöcke.<br>Oben einen Typ wählen.</div>'; return; }
|
||
blocks.forEach(function (b, i) {
|
||
var m = meta(b.type);
|
||
var row = document.createElement('div');
|
||
row.className = 'ed-item' + (i === selected ? ' active' : '');
|
||
row.draggable = true; row.dataset.idx = i;
|
||
row.innerHTML = '<span class="grip">⋮⋮</span><span class="lbl">' + esc(m ? m.label : b.type) + '</span>' +
|
||
'<span class="acts"><button title="Hoch" data-act="up">▲</button><button title="Runter" data-act="down">▼</button><button title="Duplizieren" data-act="dup">⧉</button><button title="Löschen" data-act="del">✕</button></span>';
|
||
row.addEventListener('click', function (e) {
|
||
var act = e.target.getAttribute('data-act');
|
||
if (act) { e.stopPropagation(); doAct(act, i); return; }
|
||
selected = i; renderList(); renderSettings();
|
||
});
|
||
// Drag
|
||
row.addEventListener('dragstart', function (e) { row.classList.add('dragging'); e.dataTransfer.setData('text/plain', i); });
|
||
row.addEventListener('dragend', function () { row.classList.remove('dragging'); });
|
||
row.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
row.addEventListener('drop', function (e) {
|
||
e.preventDefault(); var from = Number(e.dataTransfer.getData('text/plain')); var to = i;
|
||
if (from === to) return; var moved = blocks.splice(from, 1)[0]; blocks.splice(to, 0, moved);
|
||
selected = to; renderAll();
|
||
});
|
||
listEl.appendChild(row);
|
||
});
|
||
}
|
||
function doAct(act, i) {
|
||
if (act === 'up' && i > 0) { var t = blocks[i - 1]; blocks[i - 1] = blocks[i]; blocks[i] = t; selected = i - 1; }
|
||
else if (act === 'down' && i < blocks.length - 1) { var t2 = blocks[i + 1]; blocks[i + 1] = blocks[i]; blocks[i] = t2; selected = i + 1; }
|
||
else if (act === 'dup') { var copy = JSON.parse(JSON.stringify(blocks[i])); copy._id = 'b' + (uid++); blocks.splice(i + 1, 0, copy); selected = i + 1; }
|
||
else if (act === 'del') { blocks.splice(i, 1); selected = Math.min(selected, blocks.length - 1); }
|
||
renderAll();
|
||
}
|
||
|
||
// ---- Add-Buttons ----
|
||
var addEl = document.getElementById('edAdd');
|
||
D.blockTypes.forEach(function (m) {
|
||
var b = document.createElement('button');
|
||
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="' + m.icon + '"/></svg><span>' + esc(m.label) + '</span>';
|
||
b.addEventListener('click', function () {
|
||
var nb = defaults(m.key); nb.type = m.key; nb._id = 'b' + (uid++);
|
||
var at = selected > -1 ? selected + 1 : blocks.length;
|
||
blocks.splice(at, 0, nb); selected = at; renderAll();
|
||
});
|
||
addEl.appendChild(b);
|
||
});
|
||
|
||
// ---- Settings-Panel ----
|
||
var setEl = document.getElementById('edSettings');
|
||
function renderSettings() {
|
||
if (selected < 0 || !blocks[selected]) { setEl.innerHTML = '<div class="ed-nosel">Wähle links einen Block, um ihn zu bearbeiten.</div>'; return; }
|
||
var b = blocks[selected], m = meta(b.type);
|
||
var h = '<div class="ed-sec" style="margin-bottom:14px">' + esc(m ? m.label : b.type) + ' bearbeiten</div>';
|
||
if (!m || !m.fields.length) h += '<p class="s-help">Dieser Block hat keine Einstellungen.</p>';
|
||
m && m.fields.forEach(function (f) {
|
||
h += '<div class="ed-field"><label>' + esc(f.label) + '</label>';
|
||
var val = b[f.name];
|
||
if (f.type === 'textarea') h += '<textarea class="s-textarea" data-f="' + f.name + '" style="min-height:90px">' + esc(val) + '</textarea>';
|
||
else if (f.type === 'select') h += '<select class="s-select" data-f="' + f.name + '">' + f.options.map(function (o) { return '<option value="' + esc(o) + '"' + (String(val) === String(o) ? ' selected' : '') + '>' + esc(o) + '</option>'; }).join('') + '</select>';
|
||
else if (f.type === 'number') h += '<input class="s-input" type="number" data-f="' + f.name + '" value="' + esc(val) + '">';
|
||
else if (f.type === 'image') h += '<div class="ed-imgrow"><input class="s-input" data-f="' + f.name + '" value="' + esc(val) + '" placeholder="Bild-URL"><button class="s-btn s-btn-sm ed-mediabtn" data-pick="' + f.name + '" type="button">📷</button></div>';
|
||
else if (f.type === 'imagelist') h += '<textarea class="s-textarea" data-fl="' + f.name + '" placeholder="Eine Bild-URL pro Zeile" style="min-height:90px">' + esc((val || []).join('\n')) + '</textarea>';
|
||
else if (f.type === 'features') {
|
||
(val || []).forEach(function (it, fi) {
|
||
h += '<input class="s-input" data-feat="' + fi + '" data-featk="title" value="' + esc(it.title) + '" placeholder="Titel" style="margin-bottom:5px"><input class="s-input" data-feat="' + fi + '" data-featk="text" value="' + esc(it.text) + '" placeholder="Text" style="margin-bottom:10px">';
|
||
});
|
||
}
|
||
else h += '<input class="s-input" data-f="' + f.name + '" value="' + esc(val) + '">';
|
||
h += '</div>';
|
||
});
|
||
setEl.innerHTML = h;
|
||
// Bindings
|
||
setEl.querySelectorAll('[data-f]').forEach(function (el) {
|
||
el.addEventListener('input', function () { blocks[selected][el.getAttribute('data-f')] = el.value; renderPreview(); });
|
||
});
|
||
setEl.querySelectorAll('[data-fl]').forEach(function (el) {
|
||
el.addEventListener('input', function () { blocks[selected][el.getAttribute('data-fl')] = el.value.split('\n').map(function (s) { return s.trim(); }).filter(Boolean); renderPreview(); });
|
||
});
|
||
setEl.querySelectorAll('[data-feat]').forEach(function (el) {
|
||
el.addEventListener('input', function () {
|
||
var fi = Number(el.getAttribute('data-feat')), k = el.getAttribute('data-featk');
|
||
if (!blocks[selected].items) blocks[selected].items = [];
|
||
if (!blocks[selected].items[fi]) blocks[selected].items[fi] = { title: '', text: '' };
|
||
blocks[selected].items[fi][k] = el.value; renderPreview();
|
||
});
|
||
});
|
||
setEl.querySelectorAll('[data-pick]').forEach(function (el) {
|
||
el.addEventListener('click', function () {
|
||
var url = prompt('Bild-URL eingeben (oder aus der Medien-Bibliothek kopieren):', blocks[selected][el.getAttribute('data-pick')] || '');
|
||
if (url != null) { blocks[selected][el.getAttribute('data-pick')] = url; renderSettings(); renderPreview(); }
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderAll() { renderList(); renderSettings(); renderPreview(); }
|
||
|
||
// Device toggle
|
||
document.getElementById('edDevice').addEventListener('click', function (e) {
|
||
var dev = e.target.getAttribute('data-dev'); if (!dev) return;
|
||
device = dev;
|
||
Array.prototype.forEach.call(this.children, function (c) { c.classList.toggle('active', c.getAttribute('data-dev') === dev); });
|
||
frame.classList.toggle('mobile', dev === 'mobile');
|
||
});
|
||
|
||
// Save
|
||
document.getElementById('edSave').addEventListener('click', function () {
|
||
var btn = this; btn.disabled = true; var old = btn.textContent; btn.textContent = 'Speichert …';
|
||
var clean = blocks.map(function (b) { var c = JSON.parse(JSON.stringify(b)); delete c._id; return c; });
|
||
fetch(D.saveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: D.pageId, blocks: clean }) })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (d) { if (d.ok) toast('Gespeichert (' + d.count + ' Blöcke).', 'ok'); else toast('Fehler: ' + (d.error || '?'), 'err'); })
|
||
.catch(function () { toast('Speichern fehlgeschlagen.', 'err'); })
|
||
.then(function () { btn.disabled = false; btn.textContent = old; });
|
||
});
|
||
|
||
renderAll();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|