bmc_hub/app/modules/manual/templates/admin.html

249 lines
9.8 KiB
HTML
Raw Normal View History

{% extends "shared/frontend/base.html" %}
{% block title %}Manual Admin - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.admin-shell {
max-width: 1200px;
margin: 0 auto;
}
.editor-card {
background: var(--bg-card);
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.08);
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.85rem;
}
.preview-box {
min-height: 120px;
border-radius: 8px;
border: 1px dashed rgba(0,0,0,0.2);
padding: 0.75rem;
background: var(--bg-body);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4 admin-shell">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-1"><i class="bi bi-sliders me-2"></i>Manual Admin</h2>
<div class="text-muted">Opret og redigér manualartikler (MVP editor)</div>
</div>
<a href="/manual" class="btn btn-outline-primary"><i class="bi bi-journal-richtext me-1"></i>Se manualer</a>
</div>
<div class="row g-3">
<div class="col-12 col-lg-8">
<div class="editor-card p-3">
<div class="row g-2">
<div class="col-12 col-md-8">
<label class="form-label">Titel</label>
<input id="title" class="form-control" placeholder="Hvordan opretter jeg en sag?">
</div>
<div class="col-12 col-md-4">
<label class="form-label">Sværhedsgrad</label>
<select id="difficulty" class="form-select">
<option value="beginner">beginner</option>
<option value="advanced">advanced</option>
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label">Modul</label>
<input id="module" class="form-control" placeholder="sag">
</div>
<div class="col-12 col-md-8">
<label class="form-label">Tags (kommasepareret)</label>
<input id="tags" class="form-control" placeholder="sag, ticket, opgave">
</div>
<div class="col-12">
<label class="form-label">Kort intro</label>
<textarea id="summary" class="form-control" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label">Markdown indhold</label>
<textarea id="content" class="form-control mono" rows="10" oninput="renderPreview()"></textarea>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Steps JSON</label>
<textarea id="steps" class="form-control mono" rows="8">[]</textarea>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Relationer JSON</label>
<textarea id="relations" class="form-control mono" rows="8">[]</textarea>
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button id="saveBtn" class="btn btn-primary" onclick="createManual()">
<i class="bi bi-save me-1"></i>Gem manual
</button>
<button class="btn btn-outline-secondary" onclick="resetForm()">Nulstil</button>
</div>
<div id="status" class="small mt-2 text-muted"></div>
</div>
<div class="editor-card p-3 mt-3">
<h5><i class="bi bi-eye me-2"></i>Preview</h5>
<div id="preview" class="preview-box"></div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="editor-card p-3">
<h5><i class="bi bi-list-check me-2"></i>Seneste manualer</h5>
<div class="list-group list-group-flush">
{% for article in articles %}
<div class="list-group-item">
<div class="fw-semibold">{{ article.title }}</div>
<div class="small text-muted mb-2">{{ article.module }} • {{ article.difficulty }}</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-primary" href="/manual/{{ article.slug }}">Åbn</a>
<button class="btn btn-sm btn-outline-secondary" onclick="loadManual('{{ article.slug }}')">Rediger</button>
</div>
</div>
{% else %}
<div class="text-muted">Ingen manualer endnu.</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let editingId = null;
function normalizeEditorText(value) {
return String(value || '')
.replace(/&lt;br\s*\/?&gt;/gi, '\n')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/\\n/g, '\n');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.innerText = text || '';
return div.innerHTML;
}
function renderPreview() {
const value = document.getElementById('content').value || '';
const html = escapeHtml(value).replace(/\n/g, '<br>');
document.getElementById('preview').innerHTML = html;
}
function parseJsonField(id) {
const raw = document.getElementById(id).value || '[]';
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
throw new Error(`Ugyldig JSON i feltet ${id}`);
}
}
async function createManual() {
const status = document.getElementById('status');
const saveBtn = document.getElementById('saveBtn');
status.textContent = 'Gemmer...';
try {
const payload = {
title: document.getElementById('title').value.trim(),
module: document.getElementById('module').value.trim(),
difficulty: document.getElementById('difficulty').value,
summary: document.getElementById('summary').value.trim(),
content: document.getElementById('content').value,
tags: (document.getElementById('tags').value || '').split(',').map(t => t.trim()).filter(Boolean),
steps: parseJsonField('steps'),
relations: parseJsonField('relations')
};
if (!payload.title || !payload.module || !payload.content) {
throw new Error('Titel, modul og indhold er påkrævet.');
}
const endpoint = editingId ? `/api/v1/manual/${editingId}` : '/api/v1/manual';
const method = editingId ? 'PUT' : 'POST';
const response = await fetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Kunne ikke gemme manual.');
}
status.textContent = (editingId ? 'Manual opdateret: ' : 'Manual gemt: ') + (data.slug || data.title);
saveBtn.innerHTML = '<i class="bi bi-save me-1"></i>Gem manual';
window.setTimeout(() => {
window.location.href = '/manual/' + data.slug;
}, 500);
} catch (error) {
status.textContent = 'Fejl: ' + (error.message || error);
}
}
function resetForm() {
editingId = null;
document.getElementById('title').value = '';
document.getElementById('module').value = '';
document.getElementById('summary').value = '';
document.getElementById('content').value = '';
document.getElementById('tags').value = '';
document.getElementById('steps').value = '[]';
document.getElementById('relations').value = '[]';
document.getElementById('status').textContent = '';
document.getElementById('saveBtn').innerHTML = '<i class="bi bi-save me-1"></i>Gem manual';
renderPreview();
}
async function loadManual(slug) {
const status = document.getElementById('status');
status.textContent = 'Henter manual...';
try {
const response = await fetch(`/api/v1/manual/${encodeURIComponent(slug)}`, {
method: 'GET',
credentials: 'include'
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Kunne ikke hente manual.');
}
editingId = data.id;
document.getElementById('title').value = data.title || '';
document.getElementById('module').value = data.module || '';
document.getElementById('summary').value = normalizeEditorText(data.summary);
document.getElementById('content').value = normalizeEditorText(data.content);
document.getElementById('difficulty').value = data.difficulty || 'beginner';
document.getElementById('tags').value = (data.tags || []).join(', ');
const normalizedSteps = (data.steps || []).map((step) => ({
...step,
content: normalizeEditorText(step.content)
}));
document.getElementById('steps').value = JSON.stringify(normalizedSteps, null, 2);
document.getElementById('relations').value = JSON.stringify(data.relations || [], null, 2);
document.getElementById('saveBtn').innerHTML = '<i class="bi bi-pencil-square me-1"></i>Opdater manual';
renderPreview();
status.textContent = 'Redigerer: ' + (data.title || slug);
} catch (error) {
status.textContent = 'Fejl: ' + (error.message || error);
}
}
renderPreview();
</script>
{% endblock %}