Automate diary entries and add photo galleries
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<summary>☠ {{ user.display_name }}{% if user.notification_count %} <span class="metal-notification" title="{{ user.notification_count }} neue Nachricht(en) oder Anfrage(n)" aria-label="Neue Benachrichtigungen">🤘<b>{{ user.notification_count }}</b></span>{% endif %}</summary>
|
||||
<div class="user-menu-panel">
|
||||
<a href="/profile">Mein Profil</a>
|
||||
<a href="/following">Gefolgte Bands & Locations</a>
|
||||
<a href="/diary">Konzerttagebuch</a>
|
||||
<a href="/messages">Nachrichten{% if user.notification_count %} <span class="metal-notification" title="Neue Nachrichten oder Anfragen" aria-label="Neue Nachrichten oder Anfragen">🤘<b>{{ user.notification_count }}</b></span>{% endif %}</a>
|
||||
<a href="/diary">Konzerttagebuch</a>
|
||||
<a href="/following">Gefolgte Bands & Locations</a>
|
||||
{% if user.is_admin %}<a href="/admin">⚙️ Verwaltung</a>{% endif %}
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
|
||||
@@ -391,27 +391,9 @@
|
||||
<section class="diary-section" id="diary">
|
||||
<h2>📓 Mein Konzerttagebuch</h2>
|
||||
{% if can_write_diary %}
|
||||
<p>Dieser Eintrag ist privat und nur für dich sichtbar.</p>
|
||||
<form class="diary-form" method="post" action="/concerts/{{ concert.id }}/diary" enctype="multipart/form-data">
|
||||
<label for="diary-rating">Bewertung</label>
|
||||
<select id="diary-rating" name="rating" required>
|
||||
{% for value in range(1, 6) %}<option value="{{ value }}" {% if diary_entry and diary_entry.rating == value %}selected{% endif %}>{{ value }} von 5 Sternen</option>{% endfor %}
|
||||
</select>
|
||||
<label for="favorite-song">Lieblingssong <small>(optional)</small></label>
|
||||
<input id="favorite-song" name="favorite_song" maxlength="255" value="{{ diary_entry.favorite_song if diary_entry else '' }}">
|
||||
<label for="diary-notes">Erinnerungen <small>(optional)</small></label>
|
||||
<textarea id="diary-notes" name="notes" maxlength="5000" placeholder="Was ist dir von diesem Abend geblieben?">{{ diary_entry.notes if diary_entry else '' }}</textarea>
|
||||
<label for="diary-photo">Erinnerungsfoto <small>(optional, JPG, PNG oder WEBP, max. 10 MB)</small></label>
|
||||
{% if diary_entry and diary_entry.photo_path %}
|
||||
<img src="{{ diary_entry.photo_path }}" alt="Erinnerungsfoto zu {{ concert.artist }}" style="display:block;max-width:min(100%,520px);max-height:420px;object-fit:cover;border-radius:10px;margin-bottom:8px">
|
||||
<label><input type="checkbox" name="remove_photo" value="1" style="width:auto"> Vorhandenes Foto entfernen</label>
|
||||
{% endif %}
|
||||
<input id="diary-photo" name="photo" type="file" accept="image/jpeg,image/png,image/webp">
|
||||
<button class="attendance-option" type="submit">{% if diary_entry %}Eintrag aktualisieren{% else %}Im Tagebuch speichern{% endif %}</button>
|
||||
</form>
|
||||
{% if diary_entry %}<form method="post" action="/concerts/{{ concert.id }}/diary/delete" onsubmit="return confirm('Tagebucheintrag wirklich löschen?');"><button class="button button-secondary" type="submit">Tagebucheintrag löschen</button></form>{% endif %}
|
||||
<p>Der private Eintrag wurde automatisch angelegt. <a class="button" href="/diary#concert-{{ concert.id }}">Im Konzerttagebuch bearbeiten</a></p>
|
||||
{% else %}
|
||||
<p>Das private Tagebuch ist verfügbar, wenn du dieses vergangene Konzert als „Zugesagt“ markiert hattest.</p>
|
||||
<p>Ein Tagebucheintrag wird automatisch angelegt, sobald du das vergangene Konzert als „Zugesagt“ markierst.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,91 @@
|
||||
<!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-photo{display:block;width:100%;max-width:680px;max-height:520px;object-fit:cover;border-radius:10px;margin:12px 0}.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}
|
||||
</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>Deine privaten Erinnerungen an besuchte Konzerte.</p></div><div class="diary-list">
|
||||
{% for entry in entries %}<article class="diary-entry"><h2><a href="/concerts/{{ entry.concert_id }}">{{ entry.artist }}</a></h2><p class="diary-meta">{{ entry.date }}{% if entry.venue %} · {{ entry.venue }}{% endif %}</p><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>{% if entry.photo_path %}<img class="diary-photo" src="{{ entry.photo_path }}" alt="Erinnerungsfoto zu {{ entry.artist }}" loading="lazy">{% 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.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 %}</article>
|
||||
{% else %}<section class="empty"><p>Noch keine Einträge. Markiere ein vergangenes Konzert als besucht und halte dort deine Erinnerung fest.</p></section>{% endfor %}</div>
|
||||
</main></body></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>
|
||||
|
||||
Reference in New Issue
Block a user