club-builder/admin_ui/index.html

393 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 1 50%;
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 {
flex: 1 1 50%;
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;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.preview-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preview-controls {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
}
.device-btn {
padding: 0.15rem 0.5rem;
border-radius: 999px;
border: 1px solid #4b5563;
background: #020617;
color: #e5e7eb;
cursor: pointer;
font-size: 0.75rem;
}
.device-btn.active {
background: #4b5563;
color: #f9fafb;
}
.preview-open {
padding: 0.15rem 0.45rem;
border-radius: 999px;
border: 1px solid #4b5563;
text-decoration: none;
color: #e5e7eb;
font-size: 0.75rem;
}
.preview-expand {
padding: 0.15rem 0.45rem;
border-radius: 999px;
border: 1px solid #4b5563;
background: #020617;
color: #e5e7eb;
font-size: 0.75rem;
cursor: pointer;
}
.preview-inner {
flex: 1;
display: flex;
justify-content: center;
align-items: stretch;
padding: 0.5rem;
box-sizing: border-box;
background: #000;
overflow: auto;
}
.preview iframe {
border: none;
height: 100%;
max-height: 100%;
background: #000;
}
/* Device width presets for iframe */
.device-mobile #preview-frame {
width: 390px; /* typical phone */
}
.device-tablet #preview-frame {
width: 820px; /* typical tablet */
}
.device-desktop #preview-frame {
width: 1200px; /* typical desktop layout */
max-width: 100%;
}
/* Adjust column ratios per mode */
.device-mobile .editor {
flex-basis: 50%;
}
.device-mobile .preview {
flex-basis: 50%;
}
.device-tablet .editor {
flex-basis: 40%;
}
.device-tablet .preview {
flex-basis: 60%;
}
.device-desktop .editor {
flex-basis: 35%;
}
.device-desktop .preview {
flex-basis: 65%;
}
/* Full-width preview mode: hide editor, preview takes all */
.preview-full .editor {
display: none;
}
.preview-full .preview {
flex-basis: 100%;
width: 100%;
}
</style>
</head>
<body class="device-desktop">
<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">
<span class="preview-title">
Preview: <span id="preview-label"></span>
</span>
<div class="preview-controls">
<button class="device-btn" data-mode="mobile">📱</button>
<button class="device-btn" data-mode="tablet">📱📱</button>
<button class="device-btn active" data-mode="desktop">🖥</button>
<a id="open-preview" class="preview-open" href="#" target="_blank" rel="noopener noreferrer"></a>
<button id="expand-preview" class="preview-expand" title="Toggle full-width preview"></button>
</div>
</div>
<div class="preview-inner">
<iframe id="preview-frame" src="about:blank"></iframe>
</div>
</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');
const deviceButtons = document.querySelectorAll('.device-btn');
const openPreviewEl = document.getElementById('open-preview');
const expandPreviewEl = document.getElementById('expand-preview');
let currentSlug = null;
function setDeviceMode(mode) {
document.body.classList.remove('device-mobile', 'device-tablet', 'device-desktop');
document.body.classList.add('device-' + mode);
deviceButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
deviceButtons.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.mode;
setDeviceMode(mode);
});
});
expandPreviewEl.addEventListener('click', () => {
document.body.classList.toggle('preview-full');
});
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';
const url = API_BASE + '/preview/' + encodeURIComponent(slug) + '/';
previewFrameEl.src = url;
openPreviewEl.href = url;
}
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
const url = API_BASE + '/preview/' + encodeURIComponent(currentSlug) + '/';
previewFrameEl.src = url;
openPreviewEl.href = url;
}
jsonEditorEl.addEventListener('input', () => {
if (!currentSlug) return;
statusEl.textContent = 'Changed (not saved)';
});
saveBtnEl.addEventListener('click', saveCurrent);
// Default device mode = desktop
setDeviceMode('desktop');
loadPages().catch(err => {
console.error(err);
statusEl.textContent = 'Error loading pages';
});
</script>
</body>
</html>