758 lines
34 KiB
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// 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>
|