Project_Horus/admin.html

758 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — Rawand Lorentzen</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Syne:wght@400;700;800&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f0f2f5;
--bg2: #e8ebef;
--surface: #ffffff;
--border: #d8dde6;
--border2: #c4ccd8;
--accent: #2563eb;
--accent-h: #1d4ed8;
--accent-lt: #eff6ff;
--text: #1e2530;
--text-mid: #4a5568;
--text-dim: #8896aa;
--mono: 'JetBrains Mono', monospace;
--sans: 'Syne', sans-serif;
--body: 'Inter', sans-serif;
--danger: #dc2626;
--danger-lt: #fef2f2;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: var(--body); min-height: 100vh; }
nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
padding: 0 48px; height: 60px;
display: flex; justify-content: space-between; align-items: center;
background: rgba(240,242,245,0.92); backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border);
}
.nav-logo { font-family: var(--sans); font-size: 16px; font-weight: 800; color: var(--text); }
.nav-logo span { color: var(--accent); }
.nav-right { display: flex; align-items: center; gap: 12px; }
.nav-tag { font-family: var(--mono); font-size: 11px; color: var(--accent); background: var(--accent-lt); padding: 4px 10px; border-radius: 4px; border: 1px solid rgba(37,99,235,0.2); }
.page { padding: 80px 48px 60px; max-width: 960px; margin: 0 auto; }
/* LOGIN */
#loginView { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 48px; width: 100%; max-width: 400px;
}
.login-eyebrow { font-family: var(--mono); font-size: 11px; color: var(--accent); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 12px; }
.login-title { font-family: var(--sans); font-size: 28px; font-weight: 800; letter-spacing: -1px; margin-bottom: 32px; }
label { display: block; font-size: 12px; font-weight: 500; color: var(--text-mid); margin-bottom: 6px; }
input[type="password"], input[type="text"], input[type="url"], textarea, select {
width: 100%; padding: 10px 14px; font-family: var(--body); font-size: 14px;
background: var(--bg); border: 1.5px solid var(--border); border-radius: 8px;
color: var(--text); outline: none; transition: border-color 0.15s;
}
input[type="password"]:focus, input[type="text"]:focus, input[type="url"]:focus, textarea:focus, select:focus {
border-color: var(--accent);
}
textarea { resize: vertical; font-family: var(--mono); font-size: 13px; line-height: 1.7; min-height: 280px; }
.btn {
padding: 10px 20px; font-family: var(--body); font-size: 14px; font-weight: 500;
border-radius: 8px; border: 1.5px solid transparent; cursor: pointer; transition: all 0.15s;
display: inline-flex; align-items: center; gap: 8px; text-decoration: none;
}
.btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
.btn-primary:hover { background: var(--accent-h); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost { background: var(--surface); color: var(--text-mid); border-color: var(--border); }
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: var(--danger-lt); color: var(--danger); border-color: rgba(220,38,38,0.3); font-size: 12px; padding: 6px 12px; }
.btn-danger:hover { background: var(--danger); color: white; }
.btn-sm { font-size: 12px; padding: 6px 12px; }
.btn-full { width: 100%; justify-content: center; margin-top: 20px; }
.error-msg { font-size: 13px; color: var(--danger); margin-top: 10px; font-family: var(--mono); }
/* ADMIN */
#adminView { display: none; }
.page-header { margin-bottom: 32px; padding-top: 20px; }
.page-eyebrow { font-family: var(--mono); font-size: 11px; color: var(--accent); letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 8px; }
.page-title { font-family: var(--sans); font-size: 36px; font-weight: 800; letter-spacing: -1px; }
/* Section tabs (top level) */
.sections {
display: flex; gap: 4px; margin-bottom: 32px;
border-bottom: 1px solid var(--border); padding-bottom: 0;
}
.sec-btn {
font-size: 14px; font-weight: 600; padding: 10px 20px; cursor: pointer;
color: var(--text-mid); border-bottom: 2px solid transparent; margin-bottom: -1px;
transition: all 0.15s; background: none; border-top: none; border-left: none; border-right: none;
font-family: var(--sans);
}
.sec-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.sec-btn:hover:not(.active) { color: var(--text); }
.sec-panel { display: none; }
.sec-panel.active { display: block; }
/* Sub tabs */
.sub-tabs { display: flex; gap: 4px; margin-bottom: 24px; }
.sub-tab {
font-size: 12px; font-weight: 500; padding: 6px 14px; cursor: pointer;
border-radius: 6px; color: var(--text-mid); border: 1px solid transparent;
background: none; font-family: var(--body); transition: all 0.15s;
}
.sub-tab.active { background: var(--accent-lt); color: var(--accent); border-color: rgba(37,99,235,0.2); }
.sub-tab:hover:not(.active) { background: var(--bg2); color: var(--text); }
.sub-tab[style*="none"] { display: none !important; }
.sub-panel { display: none; }
.sub-panel.active { display: block; }
/* List */
.item-list { display: flex; flex-direction: column; gap: 10px; }
.item-row {
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
padding: 16px 20px; display: flex; align-items: center; gap: 14px;
}
.item-date { font-family: var(--mono); font-size: 11px; color: var(--text-dim); width: 86px; flex-shrink: 0; }
.item-title { font-size: 14px; font-weight: 500; flex: 1; color: var(--text); }
.item-meta { font-family: var(--mono); font-size: 11px; color: var(--text-dim); max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item-actions { display: flex; gap: 8px; flex-shrink: 0; }
.list-empty { text-align: center; padding: 48px; color: var(--text-dim); font-size: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.list-count { font-family: var(--mono); font-size: 11px; color: var(--text-dim); }
/* Editor card */
.editor-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 32px; }
.field { margin-bottom: 20px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
.editor-actions { display: flex; gap: 10px; margin-top: 20px; align-items: center; }
.save-status { font-family: var(--mono); font-size: 11px; color: var(--text-dim); }
.save-status.ok { color: #16a34a; }
.save-status.err { color: var(--danger); }
/* Markdown editor tabs */
.md-tabs { display: flex; gap: 4px; margin-bottom: 8px; }
.md-tab { font-size: 12px; padding: 4px 10px; border-radius: 6px; cursor: pointer; font-family: var(--mono); border: 1px solid transparent; color: var(--text-dim); background: none; }
.md-tab.active { background: var(--accent-lt); color: var(--accent); border-color: rgba(37,99,235,0.2); }
.preview-area {
background: var(--bg); border: 1.5px solid var(--border); border-radius: 8px;
padding: 20px; min-height: 280px; font-size: 15px; line-height: 1.8; color: var(--text-mid);
}
.preview-area h1,.preview-area h2,.preview-area h3 { font-family: var(--sans); color: var(--text); margin: 20px 0 10px; letter-spacing: -0.5px; }
.preview-area h1 { font-size: 26px; } .preview-area h2 { font-size: 20px; } .preview-area h3 { font-size: 17px; }
.preview-area p { margin-bottom: 12px; }
.preview-area code { font-family: var(--mono); font-size: 13px; background: var(--bg2); padding: 2px 6px; border-radius: 4px; color: var(--accent); }
.preview-area pre { background: var(--text); color: #e2e8f0; border-radius: 8px; padding: 20px; overflow-x: auto; margin: 14px 0; }
.preview-area pre code { background: none; color: inherit; padding: 0; }
.preview-area ul,.preview-area ol { padding-left: 24px; margin-bottom: 12px; }
.preview-area li { margin-bottom: 4px; }
.preview-area blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--text-dim); font-style: italic; margin: 14px 0; }
.preview-area a { color: var(--accent); }
.preview-area hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
@media (max-width: 640px) {
nav { padding: 0 20px; }
.page { padding: 80px 20px 40px; }
.login-card { padding: 32px 24px; }
.field-row { grid-template-columns: 1fr; }
.sec-btn { padding: 8px 12px; font-size: 13px; }
}
</style>
</head>
<body>
<nav>
<div class="nav-logo">Rawand<span>.</span></div>
<div class="nav-right">
<span class="nav-tag" id="navTag" style="display:none">// admin</span>
<a href="/" class="btn btn-ghost btn-sm">← Tilbage</a>
<button id="logoutBtn" class="btn btn-ghost btn-sm" style="display:none">Log ud</button>
</div>
</nav>
<!-- LOGIN -->
<div id="loginView" class="page">
<div class="login-card">
<div class="login-eyebrow">Admin</div>
<div class="login-title">Log ind</div>
<div class="field">
<label for="usernameInput">Brugernavn</label>
<input type="text" id="usernameInput" placeholder="brugernavn" autofocus autocomplete="username">
</div>
<div class="field">
<label for="passwordInput">Adgangskode</label>
<input type="password" id="passwordInput" placeholder="••••••••" autocomplete="current-password">
</div>
<div id="loginError" class="error-msg" style="display:none"></div>
<button class="btn btn-primary btn-full" id="loginBtn">Log ind →</button>
</div>
</div>
<!-- ADMIN -->
<div id="adminView" class="page">
<div class="page-header">
<div class="page-eyebrow">// admin panel</div>
<div class="page-title">Administration</div>
</div>
<div class="sections">
<button class="sec-btn active" data-sec="blog">Blog</button>
<button class="sec-btn" data-sec="notes">Noter</button>
<button class="sec-btn" data-sec="projects">Projekter</button>
<button class="sec-btn" data-sec="links">Links</button>
</div>
<!-- BLOG -->
<div class="sec-panel active" id="sec-blog">
<div class="sub-tabs">
<button class="sub-tab active" data-stab="blog-list">Alle indlæg</button>
<button class="sub-tab" data-stab="blog-new">Nyt indlæg</button>
<button class="sub-tab" data-stab="blog-edit" id="blog-edit-tab" style="display:none">Rediger</button>
</div>
<div class="sub-panel active" id="stab-blog-list">
<div class="list-header">
<span class="list-count" id="blog-count"></span>
<button class="btn btn-primary btn-sm" onclick="switchSubTab('blog','new')">+ Nyt indlæg</button>
</div>
<div class="item-list" id="blog-list"></div>
</div>
<div class="sub-panel" id="stab-blog-new">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="blog-new-title" placeholder="Skriv en titel...">
</div>
<div class="field">
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'blog-new-content','blog-new-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'blog-new-content','blog-new-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="blog-new-content" placeholder="Skriv indlæg i Markdown..."></textarea>
<div id="blog-new-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="createItem('blog')">Publicer</button>
<button class="btn btn-ghost" onclick="clearForm('blog','new'); switchSubTab('blog','list')">Annuller</button>
<span class="save-status" id="blog-new-status"></span>
</div>
</div>
</div>
<div class="sub-panel" id="stab-blog-edit">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="blog-edit-title">
</div>
<div class="field">
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'blog-edit-content','blog-edit-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'blog-edit-content','blog-edit-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="blog-edit-content"></textarea>
<div id="blog-edit-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveEdit('blog')">Gem ændringer</button>
<button class="btn btn-ghost" onclick="switchSubTab('blog','list')">Annuller</button>
<span class="save-status" id="blog-edit-status"></span>
</div>
</div>
</div>
</div>
<!-- NOTES -->
<div class="sec-panel" id="sec-notes">
<div class="sub-tabs">
<button class="sub-tab active" data-stab="notes-list">Alle noter</button>
<button class="sub-tab" data-stab="notes-new">Ny note</button>
<button class="sub-tab" data-stab="notes-edit" id="notes-edit-tab" style="display:none">Rediger</button>
</div>
<div class="sub-panel active" id="stab-notes-list">
<div class="list-header">
<span class="list-count" id="notes-count"></span>
<button class="btn btn-primary btn-sm" onclick="switchSubTab('notes','new')">+ Ny note</button>
</div>
<div class="item-list" id="notes-list"></div>
</div>
<div class="sub-panel" id="stab-notes-new">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="notes-new-title" placeholder="Noteoverskrift...">
</div>
<div class="field">
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'notes-new-content','notes-new-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'notes-new-content','notes-new-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="notes-new-content" placeholder="Skriv note i Markdown..."></textarea>
<div id="notes-new-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="createItem('notes')">Gem note</button>
<button class="btn btn-ghost" onclick="clearForm('notes','new'); switchSubTab('notes','list')">Annuller</button>
<span class="save-status" id="notes-new-status"></span>
</div>
</div>
</div>
<div class="sub-panel" id="stab-notes-edit">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="notes-edit-title">
</div>
<div class="field">
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'notes-edit-content','notes-edit-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'notes-edit-content','notes-edit-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="notes-edit-content"></textarea>
<div id="notes-edit-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveEdit('notes')">Gem ændringer</button>
<button class="btn btn-ghost" onclick="switchSubTab('notes','list')">Annuller</button>
<span class="save-status" id="notes-edit-status"></span>
</div>
</div>
</div>
</div>
<!-- PROJECTS -->
<div class="sec-panel" id="sec-projects">
<div class="sub-tabs">
<button class="sub-tab active" data-stab="projects-list">Alle projekter</button>
<button class="sub-tab" data-stab="projects-new">Nyt projekt</button>
<button class="sub-tab" data-stab="projects-edit" id="projects-edit-tab" style="display:none">Rediger</button>
</div>
<div class="sub-panel active" id="stab-projects-list">
<div class="list-header">
<span class="list-count" id="projects-count"></span>
<button class="btn btn-primary btn-sm" onclick="switchSubTab('projects','new')">+ Nyt projekt</button>
</div>
<div class="item-list" id="projects-list"></div>
</div>
<div class="sub-panel" id="stab-projects-new">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="projects-new-title" placeholder="Projektnavn...">
</div>
<div class="field-row">
<div>
<label>URL (valgfri)</label>
<input type="text" id="projects-new-url" placeholder="https://...">
</div>
<div>
<label>Tags (kommaseparerede, valgfri)</label>
<input type="text" id="projects-new-tags" placeholder="react, node, design">
</div>
</div>
<div class="field">
<label>Beskrivelse (Markdown, valgfri)</label>
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'projects-new-description','projects-new-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'projects-new-description','projects-new-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="projects-new-description" placeholder="Beskriv projektet..."></textarea>
<div id="projects-new-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="createItem('projects')">Gem projekt</button>
<button class="btn btn-ghost" onclick="clearForm('projects','new'); switchSubTab('projects','list')">Annuller</button>
<span class="save-status" id="projects-new-status"></span>
</div>
</div>
</div>
<div class="sub-panel" id="stab-projects-edit">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="projects-edit-title">
</div>
<div class="field-row">
<div>
<label>URL (valgfri)</label>
<input type="text" id="projects-edit-url">
</div>
<div>
<label>Tags (kommaseparerede, valgfri)</label>
<input type="text" id="projects-edit-tags">
</div>
</div>
<div class="field">
<label>Beskrivelse (Markdown, valgfri)</label>
<div class="md-tabs">
<button class="md-tab active" onclick="mdTab(this,'projects-edit-description','projects-edit-preview','write')">Skriv</button>
<button class="md-tab" onclick="mdTab(this,'projects-edit-description','projects-edit-preview','preview')">Forhåndsvisning</button>
</div>
<textarea id="projects-edit-description"></textarea>
<div id="projects-edit-preview" class="preview-area" style="display:none"></div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveEdit('projects')">Gem ændringer</button>
<button class="btn btn-ghost" onclick="switchSubTab('projects','list')">Annuller</button>
<span class="save-status" id="projects-edit-status"></span>
</div>
</div>
</div>
</div>
<!-- LINKS -->
<div class="sec-panel" id="sec-links">
<div class="sub-tabs">
<button class="sub-tab active" data-stab="links-list">Alle links</button>
<button class="sub-tab" data-stab="links-new">Nyt link</button>
<button class="sub-tab" data-stab="links-edit" id="links-edit-tab" style="display:none">Rediger</button>
</div>
<div class="sub-panel active" id="stab-links-list">
<div class="list-header">
<span class="list-count" id="links-count"></span>
<button class="btn btn-primary btn-sm" onclick="switchSubTab('links','new')">+ Nyt link</button>
</div>
<div class="item-list" id="links-list"></div>
</div>
<div class="sub-panel" id="stab-links-new">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="links-new-title" placeholder="Linkets navn...">
</div>
<div class="field">
<label>URL</label>
<input type="text" id="links-new-url" placeholder="https://...">
</div>
<div class="field-row">
<div>
<label>Kategori (valgfri)</label>
<input type="text" id="links-new-category" placeholder="f.eks. design, tools, inspiration">
</div>
<div>
<label>Beskrivelse (valgfri)</label>
<input type="text" id="links-new-description" placeholder="Kort beskrivelse...">
</div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="createItem('links')">Gem link</button>
<button class="btn btn-ghost" onclick="clearForm('links','new'); switchSubTab('links','list')">Annuller</button>
<span class="save-status" id="links-new-status"></span>
</div>
</div>
</div>
<div class="sub-panel" id="stab-links-edit">
<div class="editor-card">
<div class="field">
<label>Titel</label>
<input type="text" id="links-edit-title">
</div>
<div class="field">
<label>URL</label>
<input type="text" id="links-edit-url">
</div>
<div class="field-row">
<div>
<label>Kategori (valgfri)</label>
<input type="text" id="links-edit-category">
</div>
<div>
<label>Beskrivelse (valgfri)</label>
<input type="text" id="links-edit-description">
</div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveEdit('links')">Gem ændringer</button>
<button class="btn btn-ghost" onclick="switchSubTab('links','list')">Annuller</button>
<span class="save-status" id="links-edit-status"></span>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const TOKEN_KEY = 'admin_token';
const editSlugs = {};
function getToken() { return localStorage.getItem(TOKEN_KEY); }
function setToken(t) { localStorage.setItem(TOKEN_KEY, t); }
function clearToken() { localStorage.removeItem(TOKEN_KEY); }
async function api(method, path, body) {
const opts = {
method,
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (getToken() || '') }
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch('/api' + path, opts);
return { ok: res.ok, status: res.status, data: await res.json() };
}
function showAdmin() {
document.getElementById('loginView').style.display = 'none';
document.getElementById('adminView').style.display = 'block';
document.getElementById('navTag').style.display = 'inline-flex';
document.getElementById('logoutBtn').style.display = 'inline-flex';
loadList('blog');
loadList('notes');
loadList('projects');
loadList('links');
}
function showLogin() {
document.getElementById('loginView').style.display = 'flex';
document.getElementById('adminView').style.display = 'none';
document.getElementById('navTag').style.display = 'none';
document.getElementById('logoutBtn').style.display = 'none';
}
(async () => {
if (getToken()) {
const r = await api('GET', '/blog');
if (r.ok) { showAdmin(); return; }
clearToken();
}
showLogin();
})();
async function login() {
const username = document.getElementById('usernameInput').value;
const pw = document.getElementById('passwordInput').value;
const btn = document.getElementById('loginBtn');
const err = document.getElementById('loginError');
err.style.display = 'none';
btn.disabled = true; btn.textContent = 'Logger ind...';
const r = await api('POST', '/auth/login', { username, password: pw });
if (r.ok) {
setToken(r.data.token);
showAdmin();
} else {
err.textContent = r.data.error || 'Fejl ved login';
err.style.display = 'block';
btn.disabled = false; btn.textContent = 'Log ind →';
}
}
document.getElementById('loginBtn').addEventListener('click', login);
document.getElementById('usernameInput').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('passwordInput').focus(); });
document.getElementById('passwordInput').addEventListener('keydown', e => { if (e.key === 'Enter') login(); });
document.getElementById('logoutBtn').addEventListener('click', async () => {
await api('POST', '/auth/logout');
clearToken(); showLogin();
});
// Section tabs
document.querySelectorAll('.sec-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.sec-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.sec-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('sec-' + btn.dataset.sec).classList.add('active');
});
});
// Sub tabs
document.querySelectorAll('.sub-tab').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.stab;
const [type] = id.split('-');
const container = document.getElementById('sec-' + type);
container.querySelectorAll('.sub-tab').forEach(b => b.classList.remove('active'));
container.querySelectorAll('.sub-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('stab-' + id).classList.add('active');
});
});
function switchSubTab(type, sub) {
const container = document.getElementById('sec-' + type);
container.querySelectorAll('.sub-tab').forEach(b => b.classList.remove('active'));
container.querySelectorAll('.sub-panel').forEach(p => p.classList.remove('active'));
const tab = container.querySelector(`[data-stab="${type}-${sub}"]`);
if (tab) { tab.style.display = ''; tab.classList.add('active'); }
document.getElementById('stab-' + type + '-' + sub).classList.add('active');
}
function mdTab(btn, textareaId, previewId, mode) {
btn.closest('.md-tabs').querySelectorAll('.md-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const ta = document.getElementById(textareaId);
const pv = document.getElementById(previewId);
if (mode === 'preview') {
pv.innerHTML = marked.parse(ta.value || '*Intet indhold endnu...*');
pv.style.display = 'block'; ta.style.display = 'none';
} else {
pv.style.display = 'none'; ta.style.display = 'block';
}
}
function val(id) { return (document.getElementById(id) || {}).value || ''; }
function status(id, msg, type) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = msg;
el.className = 'save-status ' + (type || '');
if (type === 'ok') setTimeout(() => { el.textContent = ''; }, 2000);
}
function clearForm(type, mode) {
['title','content','description','url','tags','category'].forEach(f => {
const el = document.getElementById(`${type}-${mode}-${f}`);
if (el) el.value = '';
});
}
// Load list
async function loadList(type) {
const r = await api('GET', '/' + type);
const list = document.getElementById(type + '-list');
const countEl = document.getElementById(type + '-count');
if (!r.ok) { list.innerHTML = '<div class="list-empty">Kunne ikke hente data.</div>'; return; }
const items = r.data;
if (countEl) countEl.textContent = items.length + ' ' + { blog: 'indlæg', notes: 'noter', projects: 'projekter', links: 'links' }[type];
if (!items.length) {
list.innerHTML = '<div class="list-empty">Ingen ' + { blog: 'indlæg', notes: 'noter', projects: 'projekter', links: 'links' }[type] + ' endnu.</div>';
return;
}
list.innerHTML = items.map(item => renderRow(type, item)).join('');
}
function renderRow(type, item) {
let meta = '';
let extraAction = '';
if (type === 'blog') {
meta = `<span class="item-meta">${item.date}</span>`;
extraAction = `<a href="/blog/${item.slug}" target="_blank" class="btn btn-ghost btn-sm">Vis ↗</a>`;
} else if (type === 'notes') {
meta = `<span class="item-meta">${item.date}</span>`;
} else if (type === 'projects') {
meta = `<span class="item-meta">${item.tags || item.date}</span>`;
if (item.url) extraAction = `<a href="${escHtml(item.url)}" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">Åbn ↗</a>`;
} else if (type === 'links') {
meta = `<span class="item-meta">${item.category || '—'}</span>`;
extraAction = `<a href="${escHtml(item.url)}" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">Besøg ↗</a>`;
}
return `
<div class="item-row">
${meta}
<span class="item-title">${escHtml(item.title)}</span>
<div class="item-actions">
${extraAction}
<button class="btn btn-ghost btn-sm" onclick="startEdit('${type}','${item.slug}')">Rediger</button>
<button class="btn btn-danger" onclick="deleteItem('${type}','${item.slug}',this)">Slet</button>
</div>
</div>`;
}
function escHtml(s) {
return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Create
async function createItem(type) {
let body = {};
if (type === 'blog' || type === 'notes') {
body.title = val(`${type}-new-title`);
body.content = val(`${type}-new-content`);
if (!body.title || !body.content) { status(`${type}-new-status`, 'Titel og indhold er påkrævet.', 'err'); return; }
} else if (type === 'projects') {
body.title = val('projects-new-title');
body.url = val('projects-new-url');
body.tags = val('projects-new-tags');
body.description = val('projects-new-description');
if (!body.title) { status('projects-new-status', 'Titel er påkrævet.', 'err'); return; }
} else if (type === 'links') {
body.title = val('links-new-title');
body.url = val('links-new-url');
body.category = val('links-new-category');
body.description = val('links-new-description');
if (!body.title || !body.url) { status('links-new-status', 'Titel og URL er påkrævet.', 'err'); return; }
}
const r = await api('POST', '/' + type, body);
if (r.ok) {
status(`${type}-new-status`, '✓ Gemt!', 'ok');
clearForm(type, 'new');
await loadList(type);
setTimeout(() => switchSubTab(type, 'list'), 1200);
} else {
status(`${type}-new-status`, r.data.error || 'Fejl.', 'err');
}
}
// Start edit
async function startEdit(type, slug) {
const r = await api('GET', '/' + type + '/' + slug);
if (!r.ok) return;
const item = r.data;
editSlugs[type] = slug;
if (type === 'blog' || type === 'notes') {
document.getElementById(`${type}-edit-title`).value = item.title || '';
document.getElementById(`${type}-edit-content`).value = item.content || '';
} else if (type === 'projects') {
document.getElementById('projects-edit-title').value = item.title || '';
document.getElementById('projects-edit-url').value = item.url || '';
document.getElementById('projects-edit-tags').value = item.tags || '';
document.getElementById('projects-edit-description').value = item.description || '';
} else if (type === 'links') {
document.getElementById('links-edit-title').value = item.title || '';
document.getElementById('links-edit-url').value = item.url || '';
document.getElementById('links-edit-category').value = item.category || '';
document.getElementById('links-edit-description').value = item.description || '';
}
document.getElementById(`${type}-edit-tab`).style.display = '';
switchSubTab(type, 'edit');
}
// Save edit
async function saveEdit(type) {
const slug = editSlugs[type];
if (!slug) return;
let body = {};
if (type === 'blog' || type === 'notes') {
body.title = val(`${type}-edit-title`);
body.content = val(`${type}-edit-content`);
} else if (type === 'projects') {
body.title = val('projects-edit-title');
body.url = val('projects-edit-url');
body.tags = val('projects-edit-tags');
body.description = val('projects-edit-description');
} else if (type === 'links') {
body.title = val('links-edit-title');
body.url = val('links-edit-url');
body.category = val('links-edit-category');
body.description = val('links-edit-description');
}
const r = await api('PUT', '/' + type + '/' + slug, body);
if (r.ok) {
status(`${type}-edit-status`, '✓ Gemt!', 'ok');
await loadList(type);
setTimeout(() => switchSubTab(type, 'list'), 1200);
} else {
status(`${type}-edit-status`, r.data.error || 'Fejl.', 'err');
}
}
// Delete
async function deleteItem(type, slug, btn) {
const labels = { blog: 'dette indlæg', notes: 'denne note', projects: 'dette projekt', links: 'dette link' };
if (!confirm('Er du sikker på, at du vil slette ' + labels[type] + '?')) return;
btn.disabled = true;
const r = await api('DELETE', '/' + type + '/' + slug);
if (r.ok) { await loadList(type); }
else { btn.disabled = false; alert('Fejl ved sletning.'); }
}
</script>
</body>
</html>