Admin-Panel: Schema-Designer, Dropzone mit Auto-Erkennung, Linkliste, Versionen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:00:40 +02:00
parent e9b59fe4c8
commit 4e0ba397be
7 changed files with 645 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}QR-Tabellen{% endblock %} · Admin</title>
<link rel="stylesheet" href="{{ url_for('static', filename='admin.css') }}">
</head>
<body>
<header class="topbar">
<span class="brand">QR-Tabellen</span>
<nav>
<a href="{{ url_for('admin_index') }}">Übersicht</a>
<a href="{{ url_for('admin_new') }}">Neue Tabelle</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flash">
{% for category, message in messages %}
<li class="flash-{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,87 @@
{% extends "admin/base.html" %}
{% block title %}Übersicht{% endblock %}
{% block content %}
<section class="dropzone-section">
<h2>CSV hochladen</h2>
<form method="post" action="{{ url_for('admin_upload') }}" enctype="multipart/form-data" id="upload-form">
<label class="dropzone" id="dropzone">
<input type="file" name="csv" accept=".csv,text/csv" id="csv-input">
<span>Befüllte CSV hier ablegen oder klicken —<br>
die Tabelle wird am Dateinamen bzw. an den Spalten erkannt.</span>
</label>
<div class="upload-row">
<label for="slug-select">Zieltabelle:</label>
<select name="slug" id="slug-select">
<option value="">automatisch erkennen</option>
{% for t in tables %}
<option value="{{ t.slug }}">{{ t.title }}</option>
{% endfor %}
</select>
<button type="submit">Hochladen &amp; veröffentlichen</button>
</div>
</form>
</section>
<section>
<h2>Gehostete Tabellen</h2>
{% if not tables %}
<p class="empty">Noch keine Tabellen — <a href="{{ url_for('admin_new') }}">lege die erste an</a>.</p>
{% endif %}
{% for t in tables %}
<article class="table-card">
<div class="card-qr">{{ qr_svg(t.url) | safe }}</div>
<div class="card-body">
<h3>{{ t.title }}</h3>
<p class="card-url">
{% if t.published %}
<a href="{{ t.url }}">{{ t.url }}</a>
{% else %}
<span class="muted">{{ t.url }} — noch keine CSV hochgeladen</span>
{% endif %}
</p>
<p class="card-meta">
{{ t.versions | length }} Version(en)
{% if t.last_upload %}· letzte: {{ t.last_upload[:-4] }}{% endif %}
</p>
<p class="card-actions">
<a class="btn" href="{{ url_for('admin_template', slug=t.slug) }}">CSV-Vorlage</a>
{% if t.versions %}
<details class="versions">
<summary class="btn">Versionen (CSV-Download)</summary>
<ul>
{% for v in t.versions %}
<li><a href="{{ url_for('admin_download', slug=t.slug, filename=v) }}">{{ v }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}
<form method="post" action="{{ url_for('admin_delete', slug=t.slug) }}"
onsubmit="return confirm('Tabelle {{ t.slug }} samt aller Versionen löschen?')">
<button type="submit" class="btn btn-danger">Löschen</button>
</form>
</p>
</div>
</article>
{% endfor %}
</section>
<script>
const zone = document.getElementById("dropzone");
const input = document.getElementById("csv-input");
const form = document.getElementById("upload-form");
["dragover", "dragenter"].forEach(ev =>
zone.addEventListener(ev, e => { e.preventDefault(); zone.classList.add("drag"); }));
["dragleave", "drop"].forEach(ev =>
zone.addEventListener(ev, () => zone.classList.remove("drag")));
zone.addEventListener("drop", e => {
e.preventDefault();
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
form.submit();
}
});
input.addEventListener("change", () => { if (input.files.length) form.submit(); });
</script>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "admin/base.html" %}
{% block title %}Neue Tabelle{% endblock %}
{% block content %}
<section>
<h2>Neue Tabelle anlegen</h2>
<form method="post" class="schema-form">
<label class="field">
<span>Titel</span>
<input type="text" name="title" required placeholder="z. B. Projekttage Streetfotografie Stationen"
value="{{ title or '' }}">
</label>
<h3>Spalten</h3>
<div id="columns">
{% set prefill = columns or [
{"label": "Nr.", "type": "number"},
{"label": "Titel", "type": "text"},
{"label": "Beschreibung", "type": "text_long"},
{"label": "Link", "type": "url"},
{"label": "Bewertung", "type": "rating_5"},
] %}
{% for col in prefill %}
<div class="column-row">
<input type="text" name="label" placeholder="Spaltenname" value="{{ col.label }}">
<select name="type">
{% for value, label in types.items() %}
<option value="{{ value }}" {% if col.type == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-danger remove-row"></button>
</div>
{% endfor %}
</div>
<p><button type="button" class="btn" id="add-row">+ Spalte</button></p>
<p class="hint">Leere Spaltennamen werden übersprungen. Link-Spalten bekommen
automatisch QR-Codes; mehrere Link-Spalten sind möglich.</p>
<button type="submit" class="btn btn-primary">Tabelle anlegen</button>
</form>
</section>
<script>
const columns = document.getElementById("columns");
document.getElementById("add-row").addEventListener("click", () => {
const row = columns.querySelector(".column-row").cloneNode(true);
row.querySelector("input").value = "";
row.querySelector("select").selectedIndex = 0;
columns.appendChild(row);
});
columns.addEventListener("click", e => {
if (e.target.classList.contains("remove-row") && columns.children.length > 1) {
e.target.closest(".column-row").remove();
}
});
</script>
{% endblock %}