Admin-Panel: Schema-Designer, Dropzone mit Auto-Erkennung, Linkliste, Versionen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
30
app/templates/admin/base.html
Normal file
30
app/templates/admin/base.html
Normal 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>
|
||||
87
app/templates/admin/index.html
Normal file
87
app/templates/admin/index.html
Normal 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 & 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 %}
|
||||
58
app/templates/admin/new.html
Normal file
58
app/templates/admin/new.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user