Files
pingu-concerts/app/templates/admin_patches.html
T

110 lines
5.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Patch-Bilder · MetalCircle</title>
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
<link rel="stylesheet" href="/static/css/style.css">
<style>
.patch-admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
.patch-admin-groups { display:grid; gap:12px; }
.patch-admin-group { padding:12px; background:#0b0b0b; border:1px solid var(--border); border-radius:10px; }
.patch-admin-group > summary { color:#fca5a5; cursor:pointer; font-size:1.05rem; font-weight:700; }
.patch-admin-group .patch-admin-grid { margin-top:14px; }
.patch-admin-card { display: grid; gap: 12px; align-content: start; }
.patch-preview { width: 140px; height: 140px; object-fit: contain; padding: 4px; background: #242424; border: 2px dashed #78716c; border-radius: 12px; }
.patch-placeholder { display: grid; place-items: center; font-size: 2.5rem; }
.patch-upload { display: grid; gap: 8px; }
.patch-upload-status { min-height: 1.2em; color: var(--muted); font-size: .85rem; }
</style>
</head>
<body>
<header><div class="header-inner"><a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>{% include '_user_menu.html' %}</div></header>
<main>
<div class="page-title"><h1>🧵 Patch-Bilder</h1><p>Grafiken für die Patches auf der virtuellen Kutte verwalten.</p></div>
{% set admin_section = 'patches' %}{% include '_admin_nav.html' %}
<section class="admin-section">
<div class="patch-admin-groups">
{% for group in patch_groups %}
<details class="patch-admin-group">
<summary>{{ group.label }} · {{ group.patches|length }}</summary>
<div class="patch-admin-grid">
{% for patch in group.patches %}
<article class="admin-row patch-admin-card">
{% if patch.image_path %}<img class="patch-preview" src="{{ patch.image_path }}" alt="{{ patch.name }}">{% else %}<div class="patch-preview patch-placeholder">{{ patch.icon }}</div>{% endif %}
<div><strong>{{ patch.name }}</strong><div class="admin-meta">{{ patch.description }} · {{ patch.code }}</div></div>
<form class="patch-upload" method="post" action="/admin/patches/{{ patch.code }}" enctype="multipart/form-data">
<input type="file" name="image" accept="image/jpeg,image/png,image/webp" required>
<span class="patch-upload-status">Das Bild wird vor dem Upload automatisch optimiert.</span>
<button class="button" type="submit">{% if patch.image_path %}Bild ersetzen{% else %}Bild hochladen{% endif %}</button>
</form>
{% if patch.image_path %}<form method="post" action="/admin/patches/{{ patch.code }}/delete" onsubmit="return confirm('Patch-Bild entfernen und wieder das Symbol verwenden?');"><button class="admin-danger" type="submit">Bild entfernen</button></form>{% endif %}
</article>
{% endfor %}
</div>
</details>
{% endfor %}
</div>
</section>
</main>
<script>
const patchForms = document.querySelectorAll(".patch-upload");
async function optimizePatchImage(file) {
const bitmap = await createImageBitmap(file);
const maxSide = 800;
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
const context = canvas.getContext("2d");
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
bitmap.close();
let quality = 0.9;
let blob;
do {
blob = await new Promise(resolve => canvas.toBlob(resolve, "image/webp", quality));
quality -= 0.1;
} while (blob && blob.size > 700 * 1024 && quality >= 0.5);
if (!blob) {
throw new Error("Bild konnte nicht verarbeitet werden");
}
return new File([blob], "patch.webp", {type: "image/webp"});
}
patchForms.forEach(form => {
form.addEventListener("submit", async event => {
if (form.dataset.optimized === "true") return;
event.preventDefault();
const input = form.querySelector('input[type="file"]');
const status = form.querySelector(".patch-upload-status");
const button = form.querySelector('button[type="submit"]');
const file = input.files[0];
if (!file) return;
button.disabled = true;
status.textContent = "Bild wird optimiert …";
try {
const optimized = await optimizePatchImage(file);
const transfer = new DataTransfer();
transfer.items.add(optimized);
input.files = transfer.files;
form.dataset.optimized = "true";
status.textContent = `Optimiert: ${Math.ceil(optimized.size / 1024)} KB wird hochgeladen …`;
form.requestSubmit();
} catch (error) {
console.error("Patch image optimization failed:", error);
status.textContent = "Das Bild konnte nicht optimiert werden. Bitte ein kleineres JPG, PNG oder WebP wählen.";
button.disabled = false;
}
});
});
</script>
</body>
</html>