Add band and venue follows with concert diary

This commit is contained in:
kai
2026-08-29 09:25:39 +02:00
parent d79e303396
commit 159515b0eb
11 changed files with 506 additions and 8 deletions
+2
View File
@@ -2,6 +2,8 @@
<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 &amp; 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>
{% if user.is_admin %}<a href="/admin">⚙️ Verwaltung</a>{% endif %}
<a href="/datenschutz">Datenschutz</a>
+44
View File
@@ -25,6 +25,14 @@
.flyer-source { display: block; width: 100%; flex: 0 0 100%; margin: 10px 0 0; color: #a8a29e; font-size: .85rem; text-align: center; }
.flyer-source a { color: #f87171; text-decoration: underline; }
.flyer-disclaimer { display: block; margin-top: 4px; font-size: .75rem; }
.follow-actions { display:flex; flex-wrap:wrap; justify-content:center; gap:8px; margin:0 0 20px; }
.follow-actions form { margin:0; }
.follow-button { padding:8px 11px; color:#fca5a5; background:#171212; border:1px solid var(--border); border-radius:9px; cursor:pointer; }
.follow-button.is-following { color:#fff; border-color:#dc2626; background:#7f1d1d; }
.diary-section { margin-top:26px; padding-top:22px; border-top:1px solid var(--border); }
.diary-form { max-width:none; }
.diary-form select, .diary-form input, .diary-form textarea { width:100%; }
.diary-form textarea { min-height:130px; resize:vertical; }
</style>
</head>
@@ -70,6 +78,19 @@
{{ concert.artist }}
</h1>
<div class="follow-actions" id="following">
{% for band in concert_bands %}<form method="post" action="/concerts/{{ concert.id }}/follow-band">
<input type="hidden" name="band_key" value="{{ band.key }}">
<input type="hidden" name="action" value="{% if band.is_following %}unfollow{% else %}follow{% endif %}">
<button class="follow-button {% if band.is_following %}is-following{% endif %}" type="submit">{% if band.is_following %}★ {{ band.name }} gefolgt{% else %}☆ {{ band.name }} folgen{% endif %}</button>
</form>{% endfor %}
{% if concert.venue.id %}<form method="post" action="/concerts/{{ concert.id }}/follow-venue">
<input type="hidden" name="action" value="{% if follows_venue %}unfollow{% else %}follow{% endif %}">
<button class="follow-button {% if follows_venue %}is-following{% endif %}" type="submit">{% if follows_venue %}★ Location gefolgt{% else %}☆ Location folgen{% endif %}</button>
</form>{% endif %}
<a class="follow-button" href="/following">Meine gefolgten Inhalte</a>
</div>
<div class="concert-info">
@@ -366,6 +387,29 @@
{% endif %}
{% if concert.is_past %}
<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">
<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>
<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 %}
{% else %}
<p>Das private Tagebuch ist verfügbar, wenn du dieses vergangene Konzert als „Zugesagt“ markiert hattest.</p>
{% endif %}
</section>
{% endif %}
<section class="comments-section" id="comments">
<h2>💬 Kommentare</h2>
+1 -1
View File
@@ -9,7 +9,7 @@
<h2>Verantwortlicher</h2>
<p>Kai Piekny, Fleyerstr. 33, 58097 Hagen<br><a href="mailto:konzert@pinguholic.de">konzert@pinguholic.de</a></p>
<h2>Welche Daten werden gespeichert?</h2>
<p>Für die Nutzung werden insbesondere Benutzername, E-Mail-Adresse, Passwort-Hash, Anzeigename, optionale Profil- und Instagram-Angaben, Profilbild, Freundschaften, Nachrichten, Veranstaltungsteilnahmen, Kommentare, Fotos, Patches sowie von dir angelegte Veranstaltungen gespeichert.</p>
<p>Für die Nutzung werden insbesondere Benutzername, E-Mail-Adresse, Passwort-Hash, Anzeigename, optionale Profil- und Instagram-Angaben, Profilbild, Freundschaften, Nachrichten, Veranstaltungsteilnahmen, gefolgte Bands und Locations, private Konzerttagebuch-Einträge, Kommentare, Fotos, Patches sowie von dir angelegte Veranstaltungen gespeichert.</p>
<h2>Wofür werden sie verwendet?</h2>
<p>Die Daten werden ausschließlich für Anmeldung, Kontoverwaltung, Veranstaltungsfunktionen, Freundschaften, Nachrichten, Benachrichtigungen und die von dir gewählten Sichtbarkeitseinstellungen verarbeitet.</p>
<h2>Rechtsgrundlage und Speicherdauer</h2>
+7
View File
@@ -0,0 +1,7 @@
<!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}
</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.favorite_song %}<p class="diary-song">🎵 Lieblingssong: {{ entry.favorite_song }}</p>{% endif %}{% if entry.notes %}<p class="diary-notes">{{ entry.notes }}</p>{% 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>
+6
View File
@@ -60,6 +60,12 @@
<input type="text" name="artist" value="{{ concert.artist }}" {% if not can_edit_title %}disabled{% endif %} required>
</label>
</p>
<p>
<label>Bands / Line-up <small>(eine Band pro Zeile)</small><br>
<textarea name="band_names" rows="3" maxlength="4000" {% if not can_edit_title %}disabled{% endif %}>{{ band_names }}</textarea>
</label>
<small>Diese Namen werden einzeln für die Folgen-Funktion verwendet.</small>
</p>
{% if can_edit_details %}
<p>
<label>Veranstaltungsort<br>
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Gefolgt · MetalCircle</title><link rel="stylesheet" href="/static/css/style.css"><style>
.follow-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}.follow-card{padding:18px;background:var(--surface);border:1px solid var(--border);border-radius:12px}.follow-list{display:flex;flex-wrap:wrap;gap:8px;padding:0;list-style:none}.follow-list li{display:flex;align-items:center;gap:6px;padding:7px 10px;background:#171212;border:1px solid var(--border);border-radius:999px}.follow-list form{margin:0}.follow-remove{padding:0;color:#fca5a5;background:none;border:0;cursor:pointer;font-size:1rem}.follow-events{display:grid;gap:10px}.follow-event{display:block;padding:14px;background:#0b0b0b;border:1px solid var(--border);border-radius:10px}.follow-event:hover{border-color:#ef4444}.follow-reason{color:var(--muted);font-size:.82rem}
</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>⭐ Gefolgt</h1><p>Bands, Locations und passende kommende Veranstaltungen.</p></div>
<div class="follow-grid"><section class="follow-card"><h2>Bands</h2><ul class="follow-list">{% for band in followed_bands %}<li>{{ band.name }}<form method="post" action="/following/bands/remove"><input type="hidden" name="band_key" value="{{ band.key }}"><button class="follow-remove" type="submit" title="Nicht mehr folgen" aria-label="{{ band.name }} nicht mehr folgen">×</button></form></li>{% else %}<li>Noch keine Band gefolgt.</li>{% endfor %}</ul></section>
<section class="follow-card"><h2>Locations</h2><ul class="follow-list">{% for venue in followed_venues %}<li>{{ venue.name }}{% if venue.city %}, {{ venue.city }}{% endif %}<form method="post" action="/following/venues/remove"><input type="hidden" name="venue_id" value="{{ venue.id }}"><button class="follow-remove" type="submit" title="Nicht mehr folgen" aria-label="{{ venue.name }} nicht mehr folgen">×</button></form></li>{% else %}<li>Noch keiner Location gefolgt.</li>{% endfor %}</ul></section></div>
<section class="follow-card" style="margin-top:18px"><h2>Kommende Treffer</h2><div class="follow-events">{% for event in events %}<a class="follow-event" href="/concerts/{{ event.id }}"><strong>{{ event.artist }}</strong><br>{{ event.date }} · {{ event.time }}{% if event.venue %} · {{ event.venue }}{% endif %}<div class="follow-reason">{% if event.matched_band %}Band{% endif %}{% if event.matched_band and event.matched_venue %} und {% endif %}{% if event.matched_venue %}Location{% endif %} gefolgt</div></a>{% else %}<p>Momentan gibt es keine kommenden Treffer.</p>{% endfor %}</div></section>
</main></body></html>
+10 -2
View File
@@ -170,6 +170,13 @@
</p>
<p>
<label for="band-names">Bands / Line-up <small>(eine Band pro Zeile)</small><br>
<textarea id="band-names" name="band_names" rows="3" maxlength="4000" placeholder="dArtagnan&#10;Supportband"></textarea>
</label>
<small>Der Veranstaltungstitel bleibt frei. Diese Namen werden einzeln für „Band folgen“ verwendet.</small>
</p>
<!-- ================================================= -->
<!-- Veranstaltungsort -->
<!-- ================================================= -->
@@ -895,6 +902,7 @@ flyerInput.addEventListener(
// Originaldateien verarbeiten.
const concertForm = flyerInput.form;
const artistInput = document.getElementById("artist");
const bandNamesInput = document.getElementById("band-names");
const startDatetimeInput = document.getElementById("start-datetime");
const duplicateWarning = document.getElementById("duplicate-warning");
const duplicateConfirmed = document.getElementById("duplicate-confirmed");
@@ -934,7 +942,7 @@ async function checkForDuplicates(showWarning = true) {
if (duplicateRequestController) duplicateRequestController.abort();
duplicateRequestController = new AbortController();
try {
const params = new URLSearchParams({artist, start_date: startDate});
const params = new URLSearchParams({artist, start_date: startDate, band_names: bandNamesInput.value});
const response = await fetch(`/api/concerts/duplicates?${params}`, {
signal: duplicateRequestController.signal
});
@@ -948,7 +956,7 @@ async function checkForDuplicates(showWarning = true) {
}
}
[artistInput, startDatetimeInput].forEach(input => input.addEventListener("input", () => {
[artistInput, bandNamesInput, startDatetimeInput].forEach(input => input.addEventListener("input", () => {
duplicateConfirmed.value = "false";
concertForm.dataset.readyToSubmit = "false";
clearTimeout(duplicateTimeout);