Add minimal admin UI with JSON editor and live preview iframe
This commit is contained in:
parent
5b38054273
commit
e9ebb99204
|
|
@ -0,0 +1,252 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Club Builder Admin</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: #111827;
|
||||||
|
color: #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 220px;
|
||||||
|
border-right: 1px solid #1f2937;
|
||||||
|
background: #020617;
|
||||||
|
padding: 0.75rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.sidebar h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
.page-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.page-list li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.page-btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.page-btn.active {
|
||||||
|
background: #111827;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-right: 1px solid #1f2937;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.editor-header span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.editor textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
background: #020617;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.editor button {
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #10b981;
|
||||||
|
color: #022c22;
|
||||||
|
}
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
width: 45%;
|
||||||
|
min-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.preview-header {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #020617;
|
||||||
|
}
|
||||||
|
.preview iframe {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<h1>Pages</h1>
|
||||||
|
<ul class="page-list" id="page-list"></ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<section class="editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<span id="editor-title">No page selected</span>
|
||||||
|
<button class="btn-primary" id="save-btn" disabled>Save</button>
|
||||||
|
<span class="status" id="status"></span>
|
||||||
|
</div>
|
||||||
|
<textarea id="json-editor" placeholder="Select a page from the left…"></textarea>
|
||||||
|
</section>
|
||||||
|
<section class="preview">
|
||||||
|
<div class="preview-header">
|
||||||
|
Preview: <span id="preview-label">–</span>
|
||||||
|
</div>
|
||||||
|
<iframe id="preview-frame" src="about:blank"></iframe>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Same origin: /api/... and /preview/... on builder-preview.clubdaguerre.de
|
||||||
|
const API_BASE = '';
|
||||||
|
|
||||||
|
const pageListEl = document.getElementById('page-list');
|
||||||
|
const editorTitleEl = document.getElementById('editor-title');
|
||||||
|
const previewLabelEl = document.getElementById('preview-label');
|
||||||
|
const previewFrameEl = document.getElementById('preview-frame');
|
||||||
|
const jsonEditorEl = document.getElementById('json-editor');
|
||||||
|
const saveBtnEl = document.getElementById('save-btn');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
|
let currentSlug = null;
|
||||||
|
|
||||||
|
async function loadPages() {
|
||||||
|
const res = await fetch(API_BASE + '/api/pages');
|
||||||
|
const pages = await res.json();
|
||||||
|
|
||||||
|
pageListEl.innerHTML = '';
|
||||||
|
pages.forEach(page => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'page-btn';
|
||||||
|
btn.textContent = page.title || page.slug || '(untitled)';
|
||||||
|
btn.dataset.slug = page.slug;
|
||||||
|
btn.onclick = () => selectPage(page.slug, btn);
|
||||||
|
li.appendChild(btn);
|
||||||
|
pageListEl.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectPage(slug, buttonEl) {
|
||||||
|
currentSlug = slug;
|
||||||
|
statusEl.textContent = 'Loading…';
|
||||||
|
saveBtnEl.disabled = true;
|
||||||
|
|
||||||
|
document.querySelectorAll('.page-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
if (buttonEl) buttonEl.classList.add('active');
|
||||||
|
|
||||||
|
const res = await fetch(API_BASE + '/api/pages/' + encodeURIComponent(slug));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
editorTitleEl.textContent = slug;
|
||||||
|
previewLabelEl.textContent = slug;
|
||||||
|
jsonEditorEl.value = JSON.stringify(data, null, 2);
|
||||||
|
saveBtnEl.disabled = false;
|
||||||
|
statusEl.textContent = 'Loaded';
|
||||||
|
|
||||||
|
previewFrameEl.src = API_BASE + '/preview/' + encodeURIComponent(slug) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCurrent() {
|
||||||
|
if (!currentSlug) return;
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(jsonEditorEl.value);
|
||||||
|
} catch (e) {
|
||||||
|
alert('JSON is invalid: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveBtnEl.disabled = true;
|
||||||
|
statusEl.textContent = 'Saving…';
|
||||||
|
const res = await fetch(API_BASE + '/api/pages/' + encodeURIComponent(currentSlug), {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(parsed),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
statusEl.textContent = 'Error: ' + res.status;
|
||||||
|
saveBtnEl.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusEl.textContent = 'Saved';
|
||||||
|
saveBtnEl.disabled = false;
|
||||||
|
|
||||||
|
// refresh preview
|
||||||
|
previewFrameEl.src = API_BASE + '/preview/' + encodeURIComponent(currentSlug) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonEditorEl.addEventListener('input', () => {
|
||||||
|
if (!currentSlug) return;
|
||||||
|
statusEl.textContent = 'Changed (not saved)';
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtnEl.addEventListener('click', saveCurrent);
|
||||||
|
|
||||||
|
loadPages().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
statusEl.textContent = 'Error loading pages';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue