<?php
require_once dirname(__DIR__) . '/config.php';
if (!IS_INSTALLED) { header('Location: ../setup.php'); exit; }
require_once ROOT_PATH . '/includes/db.php';
require_once ROOT_PATH . '/includes/functions.php';
require_once ROOT_PATH . '/includes/auth.php';
require_once ROOT_PATH . '/includes/markdown.php';
require_login(); require_role('editor');
// AJAX preview
if (isset($_GET['preview_md'])) {
header('Content-Type: text/html; charset=UTF-8');
echo md($_POST['content'] ?? '');
exit;
}
$editSlug = trim($_GET['edit'] ?? '');
$isNew = ($editSlug === '__new__');
$page = (!$isNew && $editSlug) ? get_page($editSlug) : null;
// Handle delete
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
verify_csrf();
$delSlug = trim($_POST['slug'] ?? '');
if ($delSlug && has_role('admin') && !in_array($delSlug, ['landing','bibliography'], true)) {
db()->prepare("DELETE FROM pages WHERE slug=?")->execute([$delSlug]);
flash('success', 'Page deleted.');
}
header('Location: pages.php'); exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') !== 'delete') {
verify_csrf();
$slug = make_slug(trim($_POST['slug'] ?? ''));
$title = trim($_POST['title'] ?? '');
$content = $_POST['content'] ?? '';
$enabled = isset($_POST['enabled']) ? 1 : 0;
$showInNav = isset($_POST['show_in_nav']) ? 1 : 0;
$navLabel = trim($_POST['nav_label'] ?? '');
$sortOrder = (int)($_POST['sort_order'] ?? 0);
$navTopicIds = implode(',', array_filter(array_map('intval', $_POST['nav_topic_ids'] ?? [])));
$parentId = (int)($_POST['parent_id'] ?? 0) ?: null;
if (!$slug || !$title) {
flash('error', 'Slug and title are required.');
} else {
$existing = get_page($slug);
// Prevent a page from being its own parent
if ($parentId && $existing && $existing['id'] === $parentId) {
$parentId = null;
}
if ($existing) {
db()->prepare(
"UPDATE pages SET title=?,content=?,enabled=?,show_in_nav=?,nav_label=?,sort_order=?,nav_topic_ids=?,parent_id=?,updated_at=CURRENT_TIMESTAMP WHERE slug=?"
)->execute([$title, $content, $enabled, $showInNav, $navLabel, $sortOrder, $navTopicIds, $parentId, $slug]);
} else {
db()->prepare(
"INSERT INTO pages (slug,title,content,enabled,show_in_nav,nav_label,sort_order,nav_topic_ids,parent_id) VALUES (?,?,?,?,?,?,?,?,?)"
)->execute([$slug, $title, $content, $enabled, $showInNav, $navLabel, $sortOrder, $navTopicIds, $parentId]);
}
flash('success', 'Page saved.');
header('Location: pages.php?edit=' . rawurlencode($slug)); exit;
}
// Re-populate on error
$page = compact('slug','title','content','enabled','showInNav','navLabel','sortOrder','navTopicIds','parentId');
$page['show_in_nav'] = $showInNav;
$page['nav_label'] = $navLabel;
$page['sort_order'] = $sortOrder;
$page['nav_topic_ids'] = $navTopicIds;
$page['parent_id'] = $parentId;
}
$allPages = get_all_pages();
$allTopics = get_topics(null, true);
$adminTitle = ($isNew || $page) ? ('Edit Page: ' . ($page ? h($page['title']) : 'New Page')) : 'Static Pages';
include ROOT_PATH . '/admin/header.php';
?>
<style>
.toolbar { display:flex; flex-wrap:wrap; gap:3px; margin-bottom:6px; }
.toolbar button { padding:3px 8px; font-size:12px; cursor:pointer;
background:#C0C0C0; border:1px solid #808080; }
.toolbar button:hover { background:#A0A0FF; color:#FFF; }
#page-content { font-family:'Courier New',monospace; font-size:13px; min-height:400px; resize:vertical; }
#preview-panel { background:#FFF; border:2px inset #808080; padding:10px;
min-height:400px; overflow-y:auto; font-size:14px; }
.tab-bar button { padding:4px 12px; font-size:13px; cursor:pointer; background:#A0A0A0;
color:#FFF; border:2px solid #808080; border-bottom:none; }
.tab-bar button.active { background:var(--titlebar-bg,#5B2D8E); color:#FFF; }
.media-grid { display:flex; flex-wrap:wrap; gap:6px; max-height:200px; overflow-y:auto;
border:1px inset #808080; padding:6px; background:#FFF; }
.media-thumb { width:80px; height:60px; object-fit:cover; cursor:pointer; border:2px solid transparent; }
.media-thumb:hover { border-color:#5B2D8E; }
</style>
<?php if (!$page && !$isNew): ?>
<!-- Page list -->
<div style="margin-bottom:10px">
<?php if (has_role('admin')): ?>
<a href="pages.php?edit=__new__" class="btn btn-primary">+ New Custom Page</a>
<?php endif; ?>
</div>
<div class="admin-card">
<div class="admin-card-title">📄 Pages</div>
<div class="admin-card-body">
<table class="admin-table">
<thead><tr><th>Slug</th><th>Title</th><th>Nav Label</th><th>Parent</th><th>Status</th><th>Nav</th><th></th></tr></thead>
<tbody>
<?php
// Render top-level pages first, then their children indented below
$topLevel = array_filter($allPages, fn($p) => empty($p['parent_id']));
$childMap = [];
foreach ($allPages as $p) {
if (!empty($p['parent_id'])) $childMap[$p['parent_id']][] = $p;
}
$renderPageRow = function(array $p, bool $isChild) use ($childMap, &$renderPageRow): void {
$isBuiltIn = in_array($p['slug'], ['landing','bibliography'], true);
$parentPage = !empty($p['parent_id']) ? get_page_by_id((int)$p['parent_id']) : null;
?>
<tr <?= $isChild ? 'style="background:rgba(0,0,0,0.03)"' : '' ?>>
<td><?= $isChild ? '<span style="color:#888;margin-right:4px">↳</span>' : '' ?><code><?= h($p['slug']) ?></code></td>
<td><?= h($p['title']) ?></td>
<td><?= h($p['nav_label']) ?></td>
<td style="font-size:11px"><?= $parentPage ? h($parentPage['title']) : '<span style="color:#888">—</span>' ?></td>
<td><span class="badge <?= $p['enabled'] ? 'badge-pub' : 'badge-draft' ?>"><?= $p['enabled'] ? 'Enabled' : 'Disabled' ?></span></td>
<td><?= $p['show_in_nav'] ? '✓' : '✕' ?></td>
<td>
<a href="pages.php?edit=<?= urlencode($p['slug']) ?>" class="btn btn-sm">✎ Edit</a>
<?php if ($p['enabled'] && $p['slug'] !== '__new__'): ?>
<a href="<?= h(base_url($p['slug'] === 'landing' ? '' : 'page/' . $p['slug'])) ?>"
target="_blank" class="btn btn-sm">👁 View</a>
<?php endif; ?>
<?php if (!$isBuiltIn && has_role('admin')): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Delete this page?')">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="slug" value="<?= h($p['slug']) ?>">
<button type="submit" class="btn btn-danger btn-sm">Del</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php
// Render children of this page immediately after
if (!empty($childMap[$p['id']])) {
foreach ($childMap[$p['id']] as $child) {
$renderPageRow($child, true);
}
}
};
foreach ($topLevel as $p) $renderPageRow($p, false);
?>
</tbody>
</table>
<p style="font-size:12px;color:#555;margin-top:8px">
The <strong>Landing</strong> page content appears above the topic grid on the homepage when enabled.<br>
Custom pages are accessible at <code>/page/your-slug</code> and can appear in the nav. Sub-pages appear as nav dropdown items under their parent.
</p>
</div>
</div>
<?php else: ?>
<!-- Edit form (new or existing page) -->
<form method="post" id="page-form">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<?php if ($isNew): ?>
<div class="form-row" style="margin-bottom:10px">
<label style="font-weight:bold;font-size:12px">Page Slug <small>(URL-safe, e.g. <code>about-us</code>)</small></label>
<input type="text" name="slug" class="input-full" required placeholder="e.g. about-us"
id="new-page-slug" value="<?= h($page['slug'] ?? '') ?>">
</div>
<?php else: ?>
<input type="hidden" name="slug" value="<?= h($page['slug']) ?>">
<?php endif; ?>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;align-items:flex-end">
<div style="flex:3;min-width:200px">
<label style="font-weight:bold;font-size:12px">Page Title</label>
<input type="text" name="title" id="page-title-input" class="input-full" required
value="<?= h($page['title'] ?? '') ?>">
</div>
<div style="flex:2;min-width:150px">
<label style="font-weight:bold;font-size:12px">Nav Label <small>(shown in navigation)</small></label>
<input type="text" name="nav_label" class="input-full" value="<?= h($page['nav_label'] ?? ($page['title'] ?? '')) ?>">
</div>
<div style="min-width:80px">
<label style="font-weight:bold;font-size:12px">Order</label>
<input type="number" name="sort_order" class="input-full" min="0" value="<?= (int)($page['sort_order'] ?? 0) ?>">
</div>
</div>
<?php
$isBuiltInPage = !$isNew && in_array($page['slug'] ?? '', ['landing','bibliography'], true);
$currentParentId = (int)($page['parent_id'] ?? 0);
// Pages eligible as parents: all pages except self, excluding landing from being a child
$eligibleParents = array_filter($allPages, fn($p) =>
$p['slug'] !== ($page['slug'] ?? '__self__')
);
?>
<?php if (!$isBuiltInPage): ?>
<div class="form-row" style="margin-bottom:10px">
<label style="font-weight:bold;font-size:12px">Parent Page <small>(optional — makes this a sub-page)</small></label>
<select name="parent_id" style="font-size:13px;padding:4px 6px;border:2px solid;border-color:#808080 #FFF #FFF #808080;max-width:360px">
<option value="0">— None (top-level page) —</option>
<?php foreach ($eligibleParents as $ep): ?>
<option value="<?= (int)$ep['id'] ?>" <?= $currentParentId === (int)$ep['id'] ? 'selected' : '' ?>>
<?= h($ep['title']) ?><?= $ep['nav_label'] && $ep['nav_label'] !== $ep['title'] ? ' (' . h($ep['nav_label']) . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
<p style="font-size:11px;color:#555;margin:3px 0 0">Sub-pages appear as dropdown items under the parent in the nav, and are listed at the bottom of the parent page.</p>
</div>
<?php endif; ?>
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:10px">
<label style="font-size:13px;cursor:pointer">
<input type="checkbox" name="enabled" value="1" <?= !empty($page['enabled']) ? 'checked' : '' ?>>
Enabled (publicly visible)
</label>
<label style="font-size:13px;cursor:pointer">
<input type="checkbox" name="show_in_nav" value="1" <?= !isset($page['show_in_nav']) || $page['show_in_nav'] ? 'checked' : '' ?>>
Show in navigation
</label>
</div>
<?php if (!empty($allTopics)): ?>
<div class="admin-card" style="margin-bottom:10px">
<div class="admin-card-title">📁 Navigation Sub-menu Topics <small style="font-weight:normal">(shown as dropdown under this page in the nav)</small></div>
<div class="admin-card-body">
<?php
$navTopicRaw = $page['nav_topic_ids'] ?? '';
$currentNavTopicIds = !empty($navTopicRaw)
? array_filter(array_map('intval', explode(',', $navTopicRaw)))
: [];
?>
<div style="display:flex;flex-wrap:wrap;gap:4px">
<?php foreach ($allTopics as $t):
$depth = $t['parent_id'] ? (get_topic_by_id((int)$t['parent_id'])['parent_id'] ? ' » ' : ' › ') : '';
?>
<label style="font-size:12px;cursor:pointer;display:flex;align-items:center;gap:3px;padding:3px 8px;background:#EDE8F5;border:1px solid #9888B0">
<input type="checkbox" name="nav_topic_ids[]" value="<?= $t['id'] ?>"
<?= in_array((int)$t['id'], $currentNavTopicIds, true) ? 'checked' : '' ?>>
<?= $depth . h($t['title']) ?>
</label>
<?php endforeach; ?>
</div>
<p style="font-size:11px;color:#666;margin:6px 0 0">If any are selected, the nav item for this page becomes a dropdown. Topics are shown in their current sort order.</p>
</div>
</div>
<?php endif; ?>
<div class="admin-card" style="margin-bottom:10px">
<div class="admin-card-title">Content <small style="font-weight:normal">(Markdown)</small></div>
<div class="admin-card-body">
<div class="toolbar" id="md-toolbar">
<button type="button" data-wrap="**" data-wrap-end="**" title="Bold"><strong>B</strong></button>
<button type="button" data-wrap="*" data-wrap-end="*" title="Italic"><em>I</em></button>
<button type="button" data-wrap="~~" data-wrap-end="~~" title="Strikethrough"><del>S</del></button>
<button type="button" data-prefix="# " title="H1">H1</button>
<button type="button" data-prefix="## " title="H2">H2</button>
<button type="button" data-prefix="### " title="H3">H3</button>
<button type="button" data-prefix="> " title="Blockquote">“”</button>
<button type="button" data-prefix="- " title="Bullet list">• List</button>
<button type="button" data-prefix="1. " title="Numbered list"># List</button>
<button type="button" data-wrap="`" data-wrap-end="`" title="Code span"></></button>
<button type="button" data-block="```\n" data-block-end="\n```" title="Code block">Block</button>
<button type="button" id="btn-link" title="Link">🔗 Link</button>
<button type="button" id="btn-img" title="Image">📷 Image</button>
<button type="button" id="btn-video" title="Video">🎥 Video</button>
<button type="button" id="btn-audio" title="Audio">🎵 Audio</button>
<button type="button" id="btn-pdf" title="PDF embed">📄 PDF</button>
<button type="button" id="btn-table" title="Table">📊 Table</button>
<button type="button" data-insert="\n---\n" title="HR">― HR</button>
<span style="margin-left:8px">
<button type="button" id="btn-preview">Preview</button>
<button type="button" id="btn-edit-mode" class="active">Write</button>
</span>
</div>
<div id="editor-area">
<textarea name="content" id="page-content" class="input-full"><?= h($page['content'] ?? '') ?></textarea>
</div>
<div id="preview-panel" style="display:none"><em style="color:#888">Loading…</em></div>
<!-- Media upload & insert -->
<div style="margin-top:8px">
<strong style="font-size:12px">Upload & Insert Media</strong>
<div style="display:flex;gap:8px;margin:6px 0;flex-wrap:wrap;align-items:center">
<input type="file" id="media-file-input" accept="image/*,audio/*,application/pdf,.pdf" style="font-size:12px">
<button type="button" id="media-upload-btn" class="btn btn-sm">Upload & Insert</button>
<span id="media-upload-status" style="font-size:12px;color:#555"></span>
</div>
<strong style="font-size:12px">Existing Media (click to insert):</strong>
<div class="media-grid" id="media-grid">
<?php
$mediaFiles = db()->query("SELECT * FROM media ORDER BY created_at DESC LIMIT 80")->fetchAll();
foreach ($mediaFiles as $mf):
$isImg = ($mf['file_type'] === 'image');
$isPdf = ($mf['file_type'] === 'pdf');
$subdir = $isImg ? 'images' : ($isPdf ? 'pdf' : 'audio');
$murl = base_url('uploads/' . $subdir . '/' . $mf['filename']);
?>
<?php if ($isImg): ?>
<img src="<?= h($murl) ?>" alt="<?= h($mf['original_name']) ?>" class="media-thumb"
data-url="<?= h($murl) ?>" data-type="image" data-name="<?= h($mf['original_name']) ?>"
title="<?= h($mf['original_name']) ?>">
<?php elseif ($isPdf): ?>
<span style="cursor:pointer;padding:4px;border:2px solid #5B2D8E;font-size:11px;background:#E8E8FF;display:inline-block;max-width:100px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis"
data-url="<?= h($murl) ?>" data-type="pdf" data-name="<?= h($mf['original_name']) ?>"
class="media-pdf-item" title="Click to embed PDF: <?= h($mf['original_name']) ?>">
📄 <?= h(substr($mf['original_name'], 0, 16)) ?>
</span>
<?php else: ?>
<span style="cursor:pointer;padding:4px;border:1px solid #808080;font-size:11px;background:#EEE"
data-url="<?= h($murl) ?>" data-type="audio" data-name="<?= h($mf['original_name']) ?>"
class="media-audio-item" title="Click to insert audio">
🎵 <?= h(substr($mf['original_name'], 0, 14)) ?>
</span>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<button type="submit" class="btn btn-primary">✓ Save Page</button>
<a href="pages.php" class="btn">← All Pages</a>
<?php if (!$isNew && !empty($page['enabled']) && !empty($page['slug'])): ?>
<a href="<?= h(base_url($page['slug'] === 'landing' ? '' : 'page/' . $page['slug'])) ?>"
target="_blank" class="btn">👁 View Live</a>
<?php endif; ?>
</div>
</form>
<script>
(function(){
var textarea = document.getElementById('page-content');
var previewPanel = document.getElementById('preview-panel');
var editorArea = document.getElementById('editor-area');
var btnPreview = document.getElementById('btn-preview');
var btnEdit = document.getElementById('btn-edit-mode');
// Preview / Write toggle
btnPreview.addEventListener('click', function(){
var fd = new FormData();
fd.append('content', textarea.value);
fetch('pages.php?preview_md=1', {method:'POST', body:fd})
.then(function(r){ return r.text(); })
.then(function(html){
previewPanel.innerHTML = html;
editorArea.style.display = 'none';
previewPanel.style.display = 'block';
btnPreview.classList.add('active');
btnEdit.classList.remove('active');
});
});
btnEdit.addEventListener('click', function(){
editorArea.style.display = '';
previewPanel.style.display = 'none';
btnEdit.classList.add('active');
btnPreview.classList.remove('active');
textarea.focus();
});
// Toolbar — data-attribute buttons
document.getElementById('md-toolbar').addEventListener('click', function(e){
var btn = e.target.closest('button[data-wrap],button[data-prefix],button[data-insert],button[data-block]');
if (!btn) return;
textarea.focus();
var start = textarea.selectionStart, end = textarea.selectionEnd;
var sel = textarea.value.substring(start, end);
var text = textarea.value;
var ins;
if (btn.dataset.wrap) {
var w = btn.dataset.wrap, we = btn.dataset.wrapEnd || w;
ins = w + (sel || 'text') + we;
} else if (btn.dataset.prefix) {
ins = '\n' + btn.dataset.prefix + (sel || 'text');
} else if (btn.dataset.insert) {
ins = btn.dataset.insert.replace(/\\n/g, '\n');
} else if (btn.dataset.block) {
var bs = btn.dataset.block.replace(/\\n/g,'\n'), be = (btn.dataset.blockEnd||'').replace(/\\n/g,'\n');
ins = bs + (sel || 'code here') + be;
} else { return; }
textarea.value = text.substring(0,start) + ins + text.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + ins.length;
});
// Special toolbar buttons
document.getElementById('btn-link').addEventListener('click', function(){
var url = prompt('Enter URL:'); if (!url) return;
var lbl = prompt('Link text:', 'link text') || url;
insertAt('[' + lbl + '](' + url + ')');
});
document.getElementById('btn-img').addEventListener('click', function(){
var url = prompt('Image URL or filename (from uploads):'); if (!url) return;
var alt = prompt('Alt text:', 'image') || 'image';
insertAt('');
});
document.getElementById('btn-video').addEventListener('click', function(){
var url = prompt('Paste video URL (YouTube, Vimeo, Rumble, Odysee, BitChute, PeerTube, TikTok):\n\nOr paste an embed <iframe> code:');
if (!url) return;
if (url.trim().startsWith('<')) {
insertAt('\n' + url.trim() + '\n');
} else {
insertAt('\n[video](' + url.trim() + ')\n');
}
});
document.getElementById('btn-audio').addEventListener('click', function(){
var url = prompt('Audio file URL or filename:'); if (!url) return;
insertAt('\n[audio](' + url + ')\n');
});
document.getElementById('btn-pdf').addEventListener('click', function(){
var url = prompt('PDF URL (https://…) or uploaded filename:\n\nTip: Upload the PDF first using the media section below, then click it in the grid to insert automatically.');
if (!url) return;
url = url.trim();
if (!/^https?:\/\//.test(url) && !/^\//.test(url) && url.indexOf('/') === -1) {
url = '<?= h(base_url('uploads/pdf/')) ?>' + url;
}
insertAt('\n[pdf](' + url + ')\n');
});
document.getElementById('btn-table').addEventListener('click', function(){
var cols = parseInt(prompt('Columns:', '3'), 10) || 3;
var rows = parseInt(prompt('Data rows:', '2'), 10) || 2;
var hdr = '| ' + Array.from({length:cols}, function(_,i){ return 'Col '+(i+1); }).join(' | ') + ' |';
var sep = '| ' + Array.from({length:cols}, function(){ return '---'; }).join(' | ') + ' |';
var row = '| ' + Array.from({length:cols}, function(){ return 'Cell'; }).join(' | ') + ' |';
insertAt('\n' + hdr + '\n' + sep + '\n' + Array.from({length:rows},function(){return row;}).join('\n') + '\n');
});
function insertAt(text) {
textarea.focus();
var s = textarea.selectionStart;
textarea.value = textarea.value.substring(0,s) + text + textarea.value.substring(textarea.selectionEnd);
textarea.selectionStart = textarea.selectionEnd = s + text.length;
}
// Media upload & insert
document.getElementById('media-upload-btn').addEventListener('click', function(){
var fileInput = document.getElementById('media-file-input');
if (!fileInput.files.length) { alert('Select a file first.'); return; }
var fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('csrf_token', '<?= csrf_token() ?>');
var status = document.getElementById('media-upload-status');
status.textContent = 'Uploading…';
fetch('<?= h(base_url('admin/upload.php')) ?>', {method:'POST', body:fd})
.then(function(r){ return r.json(); })
.then(function(data){
if (data.success) {
status.textContent = 'Uploaded!';
var url = data.url;
var name = data.original_name;
if (data.file_type === 'image') {
insertAt('\n\n');
} else if (data.file_type === 'pdf') {
insertAt('\n[pdf](' + url + ')\n');
} else {
insertAt('\n[audio](' + url + ')\n');
}
// Prepend to grid
var grid = document.getElementById('media-grid');
var el;
if (data.file_type === 'image') {
el = document.createElement('img');
el.src = url; el.className = 'media-thumb';
el.dataset.url = url; el.dataset.type = 'image'; el.dataset.name = name;
el.title = name;
} else if (data.file_type === 'pdf') {
el = document.createElement('span');
el.textContent = '\u{1F4C4} ' + name.substring(0,16);
el.className = 'media-pdf-item';
el.dataset.url = url; el.dataset.type = 'pdf'; el.dataset.name = name;
el.title = 'Click to embed PDF: ' + name;
el.style.cssText = 'cursor:pointer;padding:4px;border:2px solid #5B2D8E;font-size:11px;background:#E8E8FF;display:inline-block;max-width:100px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis';
} else {
el = document.createElement('span');
el.textContent = '\u{1F3B5} ' + name.substring(0,14);
el.className = 'media-audio-item';
el.dataset.url = url; el.dataset.type = 'audio'; el.dataset.name = name;
el.style.cssText = 'cursor:pointer;padding:4px;border:1px solid #808080;font-size:11px;background:#EEE';
}
grid.insertBefore(el, grid.firstChild);
fileInput.value = '';
setTimeout(function(){ status.textContent = ''; }, 3000);
} else {
status.textContent = 'Error: ' + (data.error || 'Unknown error');
}
})
.catch(function(){ status.textContent = 'Upload failed.'; });
});
// Click media grid item to insert
document.getElementById('media-grid').addEventListener('click', function(e){
var el = e.target.closest('[data-url]');
if (!el) return;
var url = el.dataset.url;
var name = el.dataset.name || 'file';
var type = el.dataset.type || 'image';
if (type === 'image') {
insertAt('\n\n');
} else if (type === 'pdf') {
insertAt('\n[pdf](' + url + ')\n');
} else {
insertAt('\n[audio](' + url + ')\n');
}
});
// Auto-generate slug from title for new pages
var titleInp = document.getElementById('page-title-input');
var slugInp = document.getElementById('new-page-slug');
if (titleInp && slugInp) {
titleInp.addEventListener('input', function(){
if (!slugInp.dataset.manual) {
slugInp.value = titleInp.value.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
}
});
slugInp.addEventListener('input', function(){ slugInp.dataset.manual = '1'; });
}
})();
</script>
<?php endif; ?>
<?php include ROOT_PATH . '/admin/footer.php'; ?>