v1.8.7: Pages — visual block builder with heading/paragraph/list/divider/hero/contact blocks, HTML editor mode, all pages (privacy/terms/about/home)
This commit is contained in:
Binary file not shown.
@@ -52309,3 +52309,35 @@ $3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
5ee2e2965acf11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779998512185
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
9a7f43085acf11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779998612196
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
|
||||
+310
-60
@@ -244,45 +244,83 @@ a[role=button]:active {
|
||||
<p id="login-err" class="text-error text-xs mt-3 hidden">Invalid credentials</p>
|
||||
|
||||
<section id="page-pages" class="page hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Pages</h2>
|
||||
<p class="text-sm text-on-surface-variant mt-1">Edit site pages content</p>
|
||||
<p class="text-sm text-on-surface-variant mt-1">Frontend builder — edit any page visually</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Tabs -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<button id="pages-tab-privacy" onclick="switchPageTab('privacy')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all">
|
||||
سياسة الخصوصية
|
||||
</button>
|
||||
<button id="pages-tab-terms" onclick="switchPageTab('terms')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all">
|
||||
شروط الخدمة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="glass rounded-2xl p-5 space-y-4" style="border:1px solid rgba(78,222,163,0.1)">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-on-surface-variant">تعديل محتوى HTML للصفحة</p>
|
||||
<div class="flex gap-2">
|
||||
<a id="pages-preview-btn" href="/privacy" target="_blank"
|
||||
class="glass px-3 py-1.5 rounded-lg text-xs text-on-surface-variant hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span class="msym text-sm">open_in_new</span> معاينة
|
||||
<a id="pb-preview-btn" href="/privacy" target="_blank"
|
||||
class="glass glass-hover px-4 py-2 rounded-xl text-sm flex items-center gap-2">
|
||||
<span class="msym text-base">open_in_new</span> Preview
|
||||
</a>
|
||||
<button onclick="savePageContent()" id="pages-save-btn"
|
||||
class="bg-primary text-surface font-bold px-4 py-1.5 rounded-lg text-xs flex items-center gap-1 hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-sm">save</span> حفظ
|
||||
<button onclick="pbSave()" id="pb-save-btn"
|
||||
class="bg-primary text-surface font-bold px-5 py-2 rounded-xl text-sm flex items-center gap-2 hover:shadow-[0_0_20px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-base">save</span> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="pages-editor"
|
||||
class="w-full bg-surface/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-on-surface focus:outline-none focus:border-primary/50 transition-colors resize-none"
|
||||
style="height:420px;direction:ltr"></textarea>
|
||||
<p id="pages-save-status" class="text-xs text-primary hidden">✅ تم الحفظ بنجاح</p>
|
||||
|
||||
<!-- Page selector -->
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<button onclick="pbSwitch('privacy')" id="pb-tab-privacy" class="pb-tab px-4 py-2 rounded-xl text-xs font-bold bg-primary text-surface transition-all">Privacy</button>
|
||||
<button onclick="pbSwitch('terms')" id="pb-tab-terms" class="pb-tab px-4 py-2 rounded-xl text-xs font-bold glass text-on-surface-variant hover:text-primary transition-all">Terms</button>
|
||||
<button onclick="pbSwitch('about')" id="pb-tab-about" class="pb-tab px-4 py-2 rounded-xl text-xs font-bold glass text-on-surface-variant hover:text-primary transition-all">About</button>
|
||||
<button onclick="pbSwitch('index')" id="pb-tab-index" class="pb-tab px-4 py-2 rounded-xl text-xs font-bold glass text-on-surface-variant hover:text-primary transition-all">Home</button>
|
||||
</div>
|
||||
|
||||
<!-- Mode toggle -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button onclick="pbSetMode('visual')" id="pb-mode-visual"
|
||||
class="px-3 py-1.5 rounded-lg text-xs font-bold bg-primary/20 text-primary border border-primary/30 flex items-center gap-1">
|
||||
<span class="msym text-sm">dashboard</span> Visual Builder
|
||||
</button>
|
||||
<button onclick="pbSetMode('html')" id="pb-mode-html"
|
||||
class="px-3 py-1.5 rounded-lg text-xs font-bold glass text-on-surface-variant flex items-center gap-1">
|
||||
<span class="msym text-sm">code</span> HTML Editor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- VISUAL BUILDER -->
|
||||
<div id="pb-visual" class="space-y-3">
|
||||
<div id="pb-blocks" class="space-y-3"></div>
|
||||
<!-- Add block toolbar -->
|
||||
<div class="glass rounded-2xl p-4">
|
||||
<p class="text-xs text-on-surface-variant mb-3 font-bold uppercase tracking-widest">Add Block</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button onclick="pbAddBlock('heading')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1">
|
||||
<span class="msym text-sm">title</span> Heading
|
||||
</button>
|
||||
<button onclick="pbAddBlock('paragraph')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1">
|
||||
<span class="msym text-sm">notes</span> Paragraph
|
||||
</button>
|
||||
<button onclick="pbAddBlock('list')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1">
|
||||
<span class="msym text-sm">format_list_bulleted</span> List
|
||||
</button>
|
||||
<button onclick="pbAddBlock('divider')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1">
|
||||
<span class="msym text-sm">horizontal_rule</span> Divider
|
||||
</button>
|
||||
<button onclick="pbAddBlock('hero')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1 text-primary">
|
||||
<span class="msym text-sm">auto_awesome</span> Hero
|
||||
</button>
|
||||
<button onclick="pbAddBlock('contact')" class="glass glass-hover px-3 py-2 rounded-xl text-xs font-bold flex items-center gap-1">
|
||||
<span class="msym text-sm">mail</span> Contact
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTML EDITOR -->
|
||||
<div id="pb-html" class="hidden">
|
||||
<div class="glass rounded-2xl p-4">
|
||||
<textarea id="pb-html-editor"
|
||||
class="w-full bg-surface/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-on-surface focus:outline-none focus:border-primary/50 transition-colors resize-none"
|
||||
style="height:500px;direction:ltr"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="pb-status" class="text-xs text-primary mt-3 hidden">✅ Saved successfully</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@@ -2355,67 +2393,279 @@ async function retryMetaExtraction() {
|
||||
|
||||
|
||||
<script>
|
||||
// ── Pages Editor ─────────────────────────────────────────────────────────────
|
||||
let _currentPage = 'privacy';
|
||||
// ── Page Builder ─────────────────────────────────────────────────────────────
|
||||
let _pbPage = 'privacy';
|
||||
let _pbMode = 'visual';
|
||||
let _pbBlocks = [];
|
||||
let _pbRawHTML = '';
|
||||
|
||||
function switchPageTab(page) {
|
||||
_currentPage = page;
|
||||
['privacy','terms'].forEach(p => {
|
||||
const btn = document.getElementById('pages-tab-' + p);
|
||||
if (p === page) {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all';
|
||||
const PB_PAGES = {
|
||||
privacy: { label: 'Privacy', url: '/privacy' },
|
||||
terms: { label: 'Terms', url: '/terms' },
|
||||
about: { label: 'About', url: '/about' },
|
||||
index: { label: 'Home', url: '/' },
|
||||
};
|
||||
|
||||
// ── Switch page ───────────────────────────────────────────────────────────────
|
||||
function pbSwitch(page) {
|
||||
_pbPage = page;
|
||||
document.querySelectorAll('.pb-tab').forEach(b => {
|
||||
b.className = 'pb-tab px-4 py-2 rounded-xl text-xs font-bold glass text-on-surface-variant hover:text-primary transition-all';
|
||||
});
|
||||
const active = document.getElementById('pb-tab-' + page);
|
||||
if (active) active.className = 'pb-tab px-4 py-2 rounded-xl text-xs font-bold bg-primary text-surface transition-all';
|
||||
document.getElementById('pb-preview-btn').href = PB_PAGES[page]?.url || '/' + page;
|
||||
pbLoad();
|
||||
}
|
||||
|
||||
// ── Mode toggle ───────────────────────────────────────────────────────────────
|
||||
function pbSetMode(mode) {
|
||||
_pbMode = mode;
|
||||
const mv = document.getElementById('pb-mode-visual');
|
||||
const mh = document.getElementById('pb-mode-html');
|
||||
const vv = document.getElementById('pb-visual');
|
||||
const vh = document.getElementById('pb-html');
|
||||
if (mode === 'visual') {
|
||||
mv.className = 'px-3 py-1.5 rounded-lg text-xs font-bold bg-primary/20 text-primary border border-primary/30 flex items-center gap-1';
|
||||
mh.className = 'px-3 py-1.5 rounded-lg text-xs font-bold glass text-on-surface-variant flex items-center gap-1';
|
||||
vv.classList.remove('hidden'); vh.classList.add('hidden');
|
||||
pbRenderBlocks();
|
||||
} else {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all';
|
||||
mh.className = 'px-3 py-1.5 rounded-lg text-xs font-bold bg-primary/20 text-primary border border-primary/30 flex items-center gap-1';
|
||||
mv.className = 'px-3 py-1.5 rounded-lg text-xs font-bold glass text-on-surface-variant flex items-center gap-1';
|
||||
vh.classList.remove('hidden'); vv.classList.add('hidden');
|
||||
// Sync blocks → HTML
|
||||
document.getElementById('pb-html-editor').value = _pbRawHTML;
|
||||
}
|
||||
});
|
||||
document.getElementById('pages-preview-btn').href = '/' + page;
|
||||
document.getElementById('pages-save-status').classList.add('hidden');
|
||||
loadPageContent(page);
|
||||
}
|
||||
|
||||
async function loadPageContent(page) {
|
||||
const editor = document.getElementById('pages-editor');
|
||||
editor.value = 'جاري التحميل...';
|
||||
// ── Load page content ─────────────────────────────────────────────────────────
|
||||
async function pbLoad() {
|
||||
const blocksEl = document.getElementById('pb-blocks');
|
||||
blocksEl.innerHTML = '<div class="text-on-surface-variant text-sm p-4">Loading...</div>';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + page, {
|
||||
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN }
|
||||
});
|
||||
const res = await fetch('/admin/pages/' + _pbPage, { headers: { 'Authorization': 'Bearer ' + JWT_TOKEN } });
|
||||
const data = await res.json();
|
||||
editor.value = data.content || '';
|
||||
_pbRawHTML = data.content || '';
|
||||
_pbBlocks = pbParseHTML(_pbRawHTML);
|
||||
if (_pbMode === 'visual') pbRenderBlocks();
|
||||
else document.getElementById('pb-html-editor').value = _pbRawHTML;
|
||||
} catch(e) {
|
||||
editor.value = 'خطأ في التحميل: ' + e.message;
|
||||
blocksEl.innerHTML = '<div class="text-red-400 text-sm p-4">Error: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function savePageContent() {
|
||||
const btn = document.getElementById('pages-save-btn');
|
||||
const status = document.getElementById('pages-save-status');
|
||||
const content = document.getElementById('pages-editor').value;
|
||||
// ── Parse HTML → blocks ───────────────────────────────────────────────────────
|
||||
function pbParseHTML(html) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const blocks = [];
|
||||
// Find main content area
|
||||
const main = doc.querySelector('main') || doc.querySelector('.space-y-6') || doc.body;
|
||||
main.querySelectorAll('h1,h2,h3,p,ul,ol,hr,div[class*="hero"]').forEach((el, i) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === 'hr') {
|
||||
blocks.push({ id: i, type: 'divider', content: '' });
|
||||
} else if (['h1','h2','h3'].includes(tag)) {
|
||||
blocks.push({ id: i, type: 'heading', level: tag, content: el.textContent.trim() });
|
||||
} else if (tag === 'p') {
|
||||
const txt = el.textContent.trim();
|
||||
if (txt) blocks.push({ id: i, type: 'paragraph', content: txt });
|
||||
} else if (tag === 'ul' || tag === 'ol') {
|
||||
const items = [...el.querySelectorAll('li')].map(li => li.textContent.trim()).join('\n');
|
||||
if (items) blocks.push({ id: i, type: 'list', ordered: tag === 'ol', content: items });
|
||||
}
|
||||
});
|
||||
// If nothing found, create a single raw block
|
||||
if (!blocks.length) {
|
||||
const bodyText = doc.body.innerText?.trim();
|
||||
if (bodyText) blocks.push({ id: 0, type: 'paragraph', content: bodyText.substring(0, 500) + '...' });
|
||||
else blocks.push({ id: 0, type: 'paragraph', content: 'This page has no editable content yet.' });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// ── Render blocks in visual editor ───────────────────────────────────────────
|
||||
function pbRenderBlocks() {
|
||||
const container = document.getElementById('pb-blocks');
|
||||
if (!_pbBlocks.length) {
|
||||
container.innerHTML = '<div class="glass rounded-2xl p-6 text-center text-on-surface-variant text-sm">No blocks yet — add one below</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = _pbBlocks.map((b, idx) => pbBlockHTML(b, idx)).join('');
|
||||
}
|
||||
|
||||
function pbBlockHTML(b, idx) {
|
||||
const moveUp = idx > 0 ? `<button onclick="pbMoveBlock(${idx},-1)" class="glass glass-hover p-1 rounded-lg" title="Move up"><span class="msym text-sm">arrow_upward</span></button>` : '';
|
||||
const moveDown = idx < _pbBlocks.length-1 ? `<button onclick="pbMoveBlock(${idx},1)" class="glass glass-hover p-1 rounded-lg" title="Move down"><span class="msym text-sm">arrow_downward</span></button>` : '';
|
||||
const controls = `<div class="flex items-center gap-1 shrink-0">
|
||||
${moveUp}${moveDown}
|
||||
<button onclick="pbDeleteBlock(${idx})" class="glass glass-hover p-1 rounded-lg text-red-400" title="Delete"><span class="msym text-sm">delete</span></button>
|
||||
</div>`;
|
||||
|
||||
let inner = '';
|
||||
if (b.type === 'heading') {
|
||||
inner = `<div class="flex items-center gap-3">
|
||||
<select onchange="_pbBlocks[${idx}].level=this.value;pbRenderBlocks()" class="bg-surface-high border border-outline-variant rounded-lg px-2 py-1 text-xs">
|
||||
<option value="h1" ${b.level==='h1'?'selected':''}>H1</option>
|
||||
<option value="h2" ${b.level==='h2'?'selected':''}>H2</option>
|
||||
<option value="h3" ${b.level==='h3'?'selected':''}>H3</option>
|
||||
</select>
|
||||
<input type="text" value="${b.content.replace(/"/g,'"')}"
|
||||
oninput="_pbBlocks[${idx}].content=this.value"
|
||||
class="flex-1 bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-sm font-bold outline-none focus:border-primary transition-colors"
|
||||
placeholder="Heading text..."/>
|
||||
</div>`;
|
||||
} else if (b.type === 'paragraph') {
|
||||
inner = `<textarea rows="3" oninput="_pbBlocks[${idx}].content=this.value"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-sm outline-none focus:border-primary transition-colors resize-none"
|
||||
placeholder="Paragraph text...">${b.content.replace(/</g,'<')}</textarea>`;
|
||||
} else if (b.type === 'list') {
|
||||
inner = `<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs text-on-surface-variant">Type:</span>
|
||||
<label class="text-xs flex items-center gap-1"><input type="radio" name="list-${idx}" value="false" ${!b.ordered?'checked':''} onchange="_pbBlocks[${idx}].ordered=false"> Bullets</label>
|
||||
<label class="text-xs flex items-center gap-1"><input type="radio" name="list-${idx}" value="true" ${b.ordered?'checked':''} onchange="_pbBlocks[${idx}].ordered=true"> Numbered</label>
|
||||
</div>
|
||||
<textarea rows="4" oninput="_pbBlocks[${idx}].content=this.value"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs font-mono outline-none focus:border-primary transition-colors resize-none"
|
||||
placeholder="One item per line...">${b.content.replace(/</g,'<')}</textarea>`;
|
||||
} else if (b.type === 'divider') {
|
||||
inner = `<div class="border-t border-outline-variant my-2 opacity-50"></div>`;
|
||||
} else if (b.type === 'hero') {
|
||||
inner = `<div class="space-y-2">
|
||||
<input type="text" value="${(b.title||'').replace(/"/g,'"')}" oninput="_pbBlocks[${idx}].title=this.value"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-base font-bold outline-none focus:border-primary transition-colors" placeholder="Hero title..."/>
|
||||
<input type="text" value="${(b.subtitle||'').replace(/"/g,'"')}" oninput="_pbBlocks[${idx}].subtitle=this.value"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-sm outline-none focus:border-primary transition-colors" placeholder="Hero subtitle..."/>
|
||||
</div>`;
|
||||
} else if (b.type === 'contact') {
|
||||
inner = `<input type="email" value="${(b.email||'').replace(/"/g,'"')}" oninput="_pbBlocks[${idx}].email=this.value"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-sm outline-none focus:border-primary transition-colors" placeholder="contact@example.com"/>`;
|
||||
}
|
||||
|
||||
const typeLabels = { heading:'Heading', paragraph:'Paragraph', list:'List', divider:'Divider', hero:'Hero', contact:'Contact' };
|
||||
const typeColors = { heading:'text-primary', paragraph:'text-on-surface-variant', list:'text-yellow-400', divider:'text-outline-variant', hero:'text-purple-400', contact:'text-blue-400' };
|
||||
|
||||
return `<div class="glass rounded-2xl p-4 border border-outline-variant/20 hover:border-primary/20 transition-all">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-xs font-bold uppercase tracking-widest ${typeColors[b.type]||''}">${typeLabels[b.type]||b.type}</span>
|
||||
${controls}
|
||||
</div>
|
||||
${inner}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Block actions ─────────────────────────────────────────────────────────────
|
||||
function pbAddBlock(type) {
|
||||
const newBlock = { id: Date.now(), type, content: '', level: 'h2', ordered: false, title: '', subtitle: '', email: '' };
|
||||
_pbBlocks.push(newBlock);
|
||||
pbRenderBlocks();
|
||||
// Scroll to new block
|
||||
setTimeout(() => {
|
||||
const last = document.getElementById('pb-blocks').lastElementChild;
|
||||
last?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function pbDeleteBlock(idx) {
|
||||
_pbBlocks.splice(idx, 1);
|
||||
pbRenderBlocks();
|
||||
}
|
||||
|
||||
function pbMoveBlock(idx, dir) {
|
||||
const newIdx = idx + dir;
|
||||
if (newIdx < 0 || newIdx >= _pbBlocks.length) return;
|
||||
[_pbBlocks[idx], _pbBlocks[newIdx]] = [_pbBlocks[newIdx], _pbBlocks[idx]];
|
||||
pbRenderBlocks();
|
||||
}
|
||||
|
||||
// ── Blocks → HTML ─────────────────────────────────────────────────────────────
|
||||
function pbBlocksToHTML(blocks) {
|
||||
return blocks.map(b => {
|
||||
if (b.type === 'heading') return `<${b.level||'h2'} class="text-primary font-bold mb-2">${b.content}</${b.level||'h2'}>`;
|
||||
if (b.type === 'paragraph') return `<p>${b.content}</p>`;
|
||||
if (b.type === 'divider') return `<hr class="border-outline-variant my-4"/>`;
|
||||
if (b.type === 'list') {
|
||||
const tag = b.ordered ? 'ol' : 'ul';
|
||||
const cls = b.ordered ? 'list-decimal' : 'list-disc';
|
||||
const items = b.content.split('\n').filter(Boolean).map(i => `<li>${i.trim()}</li>`).join('');
|
||||
return `<${tag} class="${cls} pl-5 space-y-1">${items}</${tag}>`;
|
||||
}
|
||||
if (b.type === 'hero') return `<div class="text-center py-8"><h1 class="text-3xl font-bold text-primary mb-2">${b.title||''}</h1><p class="text-on-surface-variant">${b.subtitle||''}</p></div>`;
|
||||
if (b.type === 'contact') return `<p>Contact: <a href="mailto:${b.email}" class="text-primary hover:underline">${b.email}</a></p>`;
|
||||
return '';
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ── Inject blocks into page HTML ──────────────────────────────────────────────
|
||||
function pbInjectBlocks(rawHTML, blocksHTML) {
|
||||
// Try to inject into existing content area
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(rawHTML, 'text/html');
|
||||
const main = doc.querySelector('main');
|
||||
if (main) {
|
||||
// Find inner content div and replace only content sections
|
||||
const contentDiv = main.querySelector('.space-y-6, .content-area, [id*="content"]');
|
||||
if (contentDiv) {
|
||||
contentDiv.innerHTML = blocksHTML;
|
||||
} else {
|
||||
// Append at end of main
|
||||
const existing = main.innerHTML;
|
||||
const headerMatch = existing.match(/(<div[^>]*flex[^>]*>[\s\S]*?<\/div>\s*<\/div>)/);
|
||||
if (headerMatch) {
|
||||
main.innerHTML = headerMatch[0] + `<div class="space-y-4 text-on-surface-variant leading-relaxed text-sm">${blocksHTML}</div>`;
|
||||
} else {
|
||||
main.innerHTML += `<div class="space-y-4 text-on-surface-variant leading-relaxed text-sm">${blocksHTML}</div>`;
|
||||
}
|
||||
}
|
||||
const s = new XMLSerializer();
|
||||
return '<!DOCTYPE html>' + s.serializeToString(doc.documentElement);
|
||||
}
|
||||
return rawHTML; // fallback — unchanged
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
async function pbSave() {
|
||||
const btn = document.getElementById('pb-save-btn');
|
||||
const status = document.getElementById('pb-status');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="msym text-sm">progress_activity</span> جاري الحفظ...';
|
||||
btn.innerHTML = '<span class="msym text-base">progress_activity</span> Saving...';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + _currentPage, {
|
||||
let finalHTML;
|
||||
if (_pbMode === 'html') {
|
||||
finalHTML = document.getElementById('pb-html-editor').value;
|
||||
} else {
|
||||
const blocksHTML = pbBlocksToHTML(_pbBlocks);
|
||||
finalHTML = pbInjectBlocks(_pbRawHTML, blocksHTML);
|
||||
}
|
||||
const res = await fetch('/admin/pages/' + _pbPage, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + JWT_TOKEN },
|
||||
body: JSON.stringify({ content })
|
||||
body: JSON.stringify({ content: finalHTML })
|
||||
});
|
||||
if (res.ok) {
|
||||
status.textContent = '✅ Saved!';
|
||||
status.classList.remove('hidden');
|
||||
setTimeout(() => status.classList.add('hidden'), 3000);
|
||||
_pbRawHTML = finalHTML;
|
||||
toast('Page saved!', 'success');
|
||||
} else {
|
||||
toast('Save failed', 'error');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('فشل الحفظ: ' + e.message);
|
||||
toast('Error: ' + e.message);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="msym text-sm">save</span> حفظ';
|
||||
btn.innerHTML = '<span class="msym text-base">save</span> Save';
|
||||
}
|
||||
|
||||
// ── Wire into showPage ────────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const origShowPage = window.showPage;
|
||||
if (origShowPage) {
|
||||
window.showPage = function(page, btn) {
|
||||
origShowPage(page, btn);
|
||||
if (page === 'pages') loadPageContent(_currentPage);
|
||||
if (page === 'pages') pbSwitch(_pbPage);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user