Add band and venue follows with concert diary
This commit is contained in:
@@ -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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 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);
|
||||
|
||||
Reference in New Issue
Block a user