92 lines
8.2 KiB
HTML
92 lines
8.2 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Konzerttagebuch · MetalCircle</title>
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
.diary-list{display:grid;gap:14px}.diary-entry{padding:18px;background:linear-gradient(145deg,#101010,#211111);border:1px solid var(--border);border-left:3px solid #b91c1c;border-radius:12px}.diary-entry h2{margin:0 0 5px}.diary-meta{color:var(--muted)}.diary-rating{color:#fbbf24;font-size:1.2rem}.diary-notes{white-space:pre-wrap}.diary-song{color:#fca5a5}.diary-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:14px 0}.diary-gallery-item{position:relative;aspect-ratio:4/3;overflow:hidden;border-radius:9px;background:#050505}.diary-gallery-item img{width:100%;height:100%;object-fit:cover}.diary-gallery-delete{position:absolute;right:5px;top:5px;margin:0}.diary-gallery-delete button{width:30px;height:30px;padding:0;border-radius:50%;background:rgba(0,0,0,.82);color:#fff;border:1px solid #ef4444}.diary-patches{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}.diary-patch{display:flex;align-items:center;gap:8px;padding:8px 10px;background:#080808;border:1px solid #7f1d1d;border-radius:9px}.diary-patch img{width:48px;height:48px;object-fit:contain}.diary-patch-icon{font-size:1.7rem}.diary-edit{margin-top:16px;padding-top:12px;border-top:1px solid var(--border)}.diary-edit summary{color:#fca5a5;cursor:pointer;font-weight:700}.diary-form{display:grid;gap:8px;margin-top:14px;max-width:none}.diary-form select,.diary-form input,.diary-form textarea{width:100%}.diary-form textarea{min-height:130px;resize:vertical}.diary-empty-note{color:var(--muted)}
|
||
</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>📓 Konzerttagebuch</h1><p>Besuchte Konzerte werden automatisch eingetragen. Deine Erinnerungen bleiben privat.</p></div>
|
||
<div class="diary-list">
|
||
{% for entry in entries %}
|
||
<article class="diary-entry" id="concert-{{ entry.concert_id }}">
|
||
<h2><a href="/concerts/{{ entry.concert_id }}">{{ entry.artist }}</a></h2>
|
||
<p class="diary-meta">{{ entry.date }}{% if entry.venue %} · {{ entry.venue }}{% endif %}</p>
|
||
{% if entry.rating %}<div class="diary-rating" aria-label="{{ entry.rating }} von 5 Sternen">{% for _ in range(entry.rating) %}★{% endfor %}{% for _ in range(5-entry.rating) %}☆{% endfor %}</div>{% else %}<p class="diary-empty-note">Noch nicht bewertet.</p>{% endif %}
|
||
{% if entry.favorite_song %}<p class="diary-song">🎵 Lieblingssong: {{ entry.favorite_song }}</p>{% endif %}
|
||
{% if entry.notes %}<p class="diary-notes">{{ entry.notes }}</p>{% endif %}
|
||
{% if entry.photos %}<div class="diary-gallery" aria-label="Bildergalerie zu {{ entry.artist }}">{% for photo in entry.photos %}<div class="diary-gallery-item"><img src="{{ photo.path }}" alt="Erinnerungsfoto {{ loop.index }} zu {{ entry.artist }}" loading="lazy"><form class="diary-gallery-delete" method="post" action="/diary/photos/{{ photo.id }}/delete" onsubmit="return confirm('Dieses Bild entfernen?');"><button type="submit" aria-label="Bild {{ loop.index }} entfernen">×</button></form></div>{% endfor %}</div>{% endif %}
|
||
{% if entry.badges %}<div class="diary-patches">{% for badge in entry.badges %}<div class="diary-patch" title="{{ badge.description }}">{% if badge.image_path %}<img src="{{ badge.image_path }}" alt="Patch {{ badge.name }}">{% else %}<span class="diary-patch-icon">{{ badge.icon }}</span>{% endif %}<span><strong>{{ badge.name }}</strong><br><small>Hier erhalten · {{ badge.awarded_at }}</small></span></div>{% endfor %}</div>{% endif %}
|
||
<details class="diary-edit" {% if not entry.rating and not entry.notes and not entry.favorite_song and not entry.photos %}open{% endif %}>
|
||
<summary>Eintrag bearbeiten</summary>
|
||
<form class="diary-form" method="post" action="/concerts/{{ entry.concert_id }}/diary" enctype="multipart/form-data">
|
||
<label>Bewertung <small>(optional)</small></label>
|
||
<select name="rating"><option value="">Noch nicht bewertet</option>{% for value in range(1,6) %}<option value="{{ value }}" {% if entry.rating == value %}selected{% endif %}>{{ value }} von 5 Sternen</option>{% endfor %}</select>
|
||
<label>Lieblingssong <small>(optional)</small></label>
|
||
<input name="favorite_song" maxlength="255" value="{{ entry.favorite_song }}">
|
||
<label>Erinnerungen <small>(optional)</small></label>
|
||
<textarea name="notes" maxlength="5000" placeholder="Was ist dir von diesem Abend geblieben?">{{ entry.notes }}</textarea>
|
||
<label>Galeriebilder <small>({{ entry.photos|length }}/3, optional)</small></label>
|
||
<input class="diary-photo-input" name="photos" type="file" accept="image/jpeg,image/png,image/webp" multiple data-free-slots="{{ 3-entry.photos|length }}" {% if entry.photos|length >= 3 %}disabled{% endif %}>
|
||
<small class="diary-photo-status">Bis zu drei Bilder; große Bilder werden automatisch optimiert.</small>
|
||
<button class="button" type="submit">Eintrag speichern</button>
|
||
</form>
|
||
</details>
|
||
</article>
|
||
{% else %}
|
||
<section class="empty"><p>Noch keine Einträge. Vergangene zugesagte Konzerte erscheinen hier automatisch.</p></section>
|
||
{% endfor %}
|
||
</div>
|
||
</main>
|
||
<script>
|
||
document.querySelectorAll(".diary-form").forEach(form => {
|
||
form.addEventListener("submit", async event => {
|
||
if (form.dataset.optimized === "true") return;
|
||
const input = form.querySelector(".diary-photo-input");
|
||
const files = Array.from(input.files);
|
||
if (!files.length) return;
|
||
event.preventDefault();
|
||
const status = form.querySelector(".diary-photo-status");
|
||
const button = form.querySelector('button[type="submit"]');
|
||
button.disabled = true;
|
||
if (files.length > Number(input.dataset.freeSlots)) {
|
||
status.textContent = `Du kannst noch ${input.dataset.freeSlots} Bild(er) hinzufügen.`;
|
||
button.disabled = false; return;
|
||
}
|
||
status.textContent = "Erinnerungsfoto wird optimiert …";
|
||
try {
|
||
const transfer = new DataTransfer();
|
||
let totalSize = 0;
|
||
for (const [index, file] of files.entries()) {
|
||
const bitmap = await createImageBitmap(file);
|
||
const scale = Math.min(1, 1800 / 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 = .88; let blob;
|
||
do { blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality)); quality -= .08; } while (blob && blob.size > 900 * 1024 && quality >= .48);
|
||
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
||
totalSize += blob.size;
|
||
transfer.items.add(new File([blob], `konzert-erinnerung-${index+1}.jpg`, {type:"image/jpeg"}));
|
||
}
|
||
input.files = transfer.files; form.dataset.optimized = "true";
|
||
status.textContent = `${files.length} Bild(er) optimiert: ${Math.ceil(totalSize / 1024)} KB – wird hochgeladen …`;
|
||
form.requestSubmit();
|
||
} catch (error) {
|
||
console.error("Diary 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>
|