Add bilingual UI and improve event actions layout
Add persistent German/English switching, translated UI messages and English 12-hour times. Align desktop event actions while preserving mobile layouts and record bilingual project guidance. Include pending diary deletion exclusions. Validate both languages with nine tests.
This commit is contained in:
+37
-25
@@ -1,50 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="{{ language() }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Konzerttagebuch · MetalCircle</title>
|
||||
<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)}
|
||||
.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 '_user_menu.html' %}</div></header>
|
||||
<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> {% include '_language_switch.html' %}
|
||||
</header>
|
||||
<main>
|
||||
<div class="page-title"><h1>📓 Konzerttagebuch</h1><p>Besuchte Konzerte werden automatisch eingetragen. Deine Erinnerungen bleiben privat.</p></div>
|
||||
<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 %}
|
||||
<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?');"><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 %}
|
||||
{% 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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</article>
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<section class="empty"><p>Noch keine Einträge. Vergangene zugesagte Konzerte erscheinen hier automatisch.</p></section>
|
||||
<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;
|
||||
@@ -56,7 +68,7 @@ document.querySelectorAll(".diary-form").forEach(form => {
|
||||
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.`;
|
||||
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 …";
|
||||
@@ -81,7 +93,7 @@ document.querySelectorAll(".diary-form").forEach(form => {
|
||||
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.";
|
||||
status.textContent = {{ _('Das Bild konnte nicht optimiert werden. Bitte ein kleineres JPG, PNG oder WebP wählen.') | tojson }};
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user