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

104 lines
10 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="{{ language() }}">
<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{background:linear-gradient(145deg,#101010,#211111);border:1px solid var(--border);border-left:3px solid #b91c1c;border-radius:12px}.diary-summary{position:relative;padding:18px 48px 18px 18px;cursor:pointer;list-style:none}.diary-summary::-webkit-details-marker{display:none}.diary-summary::after{content:"⌄";position:absolute;right:20px;top:50%;color:#fca5a5;font-size:1.5rem;transform:translateY(-50%);transition:transform .2s}.diary-entry[open]>.diary-summary::after{transform:translateY(-50%) rotate(180deg)}.diary-entry[open]>.diary-summary{border-bottom:1px solid var(--border)}.diary-entry h2{margin:0 0 5px}.diary-meta{color:var(--muted)}.diary-summary .diary-meta,.diary-summary .diary-empty-note{margin:5px 0}.diary-content{padding:4px 18px 18px}.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-delete-form{display:flex;justify-content:flex-end;width:100%;max-width:none;margin:18px 0 0}.diary-delete-link{padding:0;border:0;background:none;color:var(--muted);font-size:.78rem;text-decoration:underline;cursor:pointer}.diary-delete-link:hover{color:#fca5a5}.diary-empty-note{color:var(--muted)}
</style>
<link rel="stylesheet" href="/static/css/language.css">
</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 '_header_controls.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 %}
<details class="diary-entry" id="concert-{{ entry.concert_id }}">
<summary class="diary-summary">
<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 %}
</summary>
<div class="diary-content">
{% 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?') | tojson | forceescape }});"><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 | t }}">{% if badge.image_path %}<img src="{{ badge.image_path }}" alt="Patch {{ badge.name | t }}">{% else %}<span class="diary-patch-icon">{{ badge.icon }}</span>{% endif %}<span><strong>{{ badge.name | t }}</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>
<form class="diary-delete-form" method="post" action="/concerts/{{ entry.concert_id }}/diary/delete" onsubmit="return confirm({{ _('Wirklich unwiderruflich löschen? Die Veranstaltung kann nicht mehr eingetragen werden.') | tojson | forceescape }});">
<button class="diary-delete-link" type="submit">{{ _('Konzerteintrag löschen') }}</button>
</form>
</details>
</div>
</details>
{% else %}
<section class="empty"><p>{{ _('Noch keine Einträge. Vergangene zugesagte Konzerte erscheinen hier automatisch.') }}</p></section>
{% endfor %}
</div>
</main>
<script>
const selectedEntry = document.querySelector(location.hash);
if (selectedEntry?.classList.contains("diary-entry")) selectedEntry.open = true;
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 {count} Bild(er) hinzufügen.') | tojson }}.replace('{count}', input.dataset.freeSlots);
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.') | tojson }};
button.disabled = false;
}
});
});
</script>
</body>
</html>