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:
+61
-59
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="{{ language() }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -70,6 +70,7 @@
|
||||
.connection-actions .button { padding: 7px 10px; font-size: .85rem; }
|
||||
@media (max-width: 720px) { .profile-layout { grid-template-columns: 1fr; } .kutte { padding-left: 20px; padding-right: 20px; } .account-actions { flex-direction:column; } .data-export { text-align:left; } }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/css/language.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -77,81 +78,82 @@
|
||||
<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>Profil</h1>
|
||||
<p>Deine Konzertgeschichte und deine Kutte.</p>
|
||||
<h1>{{ _('Profil') }}</h1>
|
||||
<p>{{ _('Deine Konzertgeschichte und deine Kutte.') }}</p>
|
||||
</div>
|
||||
<div class="profile-layout">
|
||||
<section class="profile-card">
|
||||
{% if profile.avatar_path %}
|
||||
<img class="avatar" src="{{ profile.avatar_path }}" alt="Profilbild von {{ profile.display_name }}">
|
||||
<img class="avatar" src="{{ profile.avatar_path }}" alt="{{ _('Profilbild von') }} {{ profile.display_name }}">
|
||||
{% else %}
|
||||
<div class="avatar-fallback" aria-label="Kein Profilbild">{{ profile.display_name[:1] }}</div>
|
||||
<div class="avatar-fallback" aria-label="{{ _('Kein Profilbild') }}">{{ profile.display_name[:1] }}</div>
|
||||
{% endif %}
|
||||
<h2 class="profile-name">{{ profile.display_name }}</h2>
|
||||
<p class="profile-handle">@{{ profile.username }}</p>
|
||||
{% if can_view_details %}<p class="profile-since">Dabei seit {{ profile.created_at }}</p>{% endif %}
|
||||
{% if can_view_details %}<p class="profile-since">{{ _('Dabei seit') }} {{ profile.created_at }}</p>{% endif %}
|
||||
{% if can_view_details and profile.instagram_url %}
|
||||
<a class="instagram-link" href="{{ profile.instagram_url }}" target="_blank" rel="noopener noreferrer">◎ Instagram · @{{ profile.instagram_handle }}</a>
|
||||
{% endif %}
|
||||
<div class="stat"><strong>{{ attended_count }}</strong>besuchte Konzerte</div>
|
||||
<div class="stat"><strong>{{ attended_count }}</strong>{{ _('besuchte Konzerte') }}</div>
|
||||
{% if not is_own_profile %}
|
||||
<div class="friend-actions">
|
||||
{% if not block_status.blocked_by_viewer and not block_status.blocked_viewer %}
|
||||
{% if (user.is_admin or profile.is_admin) and (not friendship or friendship.status != 'accepted') %}
|
||||
<a class="button" href="/messages/{{ profile.username }}">Nachricht senden</a>
|
||||
<a class="button" href="/messages/{{ profile.username }}">{{ _('Nachricht senden') }}</a>
|
||||
{% endif %}
|
||||
{% if not friendship %}
|
||||
<form method="post" action="/users/{{ profile.username }}/friend-request"><button class="button" type="submit">Freundschaft anfragen</button></form>
|
||||
<form method="post" action="/users/{{ profile.username }}/friend-request"><button class="button" type="submit">{{ _('Freundschaft anfragen') }}</button></form>
|
||||
{% elif friendship.status == 'accepted' %}
|
||||
<a class="button" href="/messages/{{ profile.username }}">Nachricht senden</a>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/remove"><button class="button button-secondary" type="submit">Freundschaft beenden</button></form>
|
||||
<a class="button" href="/messages/{{ profile.username }}">{{ _('Nachricht senden') }}</a>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/remove"><button class="button button-secondary" type="submit">{{ _('Freundschaft beenden') }}</button></form>
|
||||
{% elif friendship.addressee_id == user.id %}
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/accept"><button class="button" type="submit">Annehmen</button></form>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/decline"><button class="button button-secondary" type="submit">Ablehnen</button></form>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/accept"><button class="button" type="submit">{{ _('Annehmen') }}</button></form>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/decline"><button class="button button-secondary" type="submit">{{ _('Ablehnen') }}</button></form>
|
||||
{% else %}
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/remove"><button class="button button-secondary" type="submit">Anfrage zurückziehen</button></form>
|
||||
<form method="post" action="/friendships/{{ friendship.id }}/remove"><button class="button button-secondary" type="submit">{{ _('Anfrage zurückziehen') }}</button></form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if block_status.blocked_by_viewer %}
|
||||
<form method="post" action="/users/{{ profile.username }}/unblock"><button class="button button-secondary" type="submit">Blockierung aufheben</button></form>
|
||||
<form method="post" action="/users/{{ profile.username }}/unblock"><button class="button button-secondary" type="submit">{{ _('Blockierung aufheben') }}</button></form>
|
||||
{% else %}
|
||||
<form method="post" action="/users/{{ profile.username }}/block" onsubmit="return confirm('Diesen Nutzer wirklich blockieren?');"><button class="button button-secondary" type="submit">Nutzer blockieren</button></form>
|
||||
<form method="post" action="/users/{{ profile.username }}/block" onsubmit="return confirm({{ _('Diesen Nutzer wirklich blockieren?') | tojson | forceescape }});"><button class="button button-secondary" type="submit">{{ _('Nutzer blockieren') }}</button></form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if block_status.blocked_viewer and not block_status.blocked_by_viewer %}<p class="private-note">Interaktionen mit diesem Nutzer sind nicht verfügbar.</p>{% endif %}
|
||||
{% if not can_view_details %}<p class="private-note">Weitere Profildetails sind nur für Freunde sichtbar.</p>{% endif %}
|
||||
{% if block_status.blocked_viewer and not block_status.blocked_by_viewer %}<p class="private-note">{{ _('Interaktionen mit diesem Nutzer sind nicht verfügbar.') }}</p>{% endif %}
|
||||
{% if not can_view_details %}<p class="private-note">{{ _('Weitere Profildetails sind nur für Freunde sichtbar.') }}</p>{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
<section class="badges-card">
|
||||
<h2>🥋 Virtuelle Kutte</h2>
|
||||
<p>Deine freigeschalteten Patches. Besuchs-Patches werden jeweils durch die höchste Stufe ersetzt.</p>
|
||||
<h2>{{ _('🥋 Virtuelle Kutte') }}</h2>
|
||||
<p>{{ _('Deine freigeschalteten Patches. Besuchs-Patches werden jeweils durch die höchste Stufe ersetzt.') }}</p>
|
||||
<div class="kutte">
|
||||
<div class="patch-grid">
|
||||
{% for badge in badges %}
|
||||
{% if badge.earned %}
|
||||
<button class="patch-button" type="button" onclick="document.getElementById('patch-{{ badge.code }}').showModal()" aria-label="Details zu {{ badge.name }} anzeigen">
|
||||
<span class="patch earned {% if badge.image_path %}patch-image-frame{% else %}patch-icon-frame{% endif %}" title="{{ badge.name }} – {{ badge.description }}">
|
||||
<button class="patch-button" type="button" onclick="document.getElementById('patch-{{ badge.code }}').showModal()" aria-label="{{ _('Details zu') }} {{ badge.name | t }} {{ _('anzeigen') }}">
|
||||
<span class="patch earned {% if badge.image_path %}patch-image-frame{% else %}patch-icon-frame{% endif %}" title="{{ badge.name | t }} – {{ badge.description | t }}">
|
||||
{% if badge.image_path %}
|
||||
<img class="patch-image" src="{{ badge.image_path }}" alt="Patch {{ badge.name }}">
|
||||
<img class="patch-image" src="{{ badge.image_path }}" alt="Patch {{ badge.name | t }}">
|
||||
{% else %}
|
||||
<span class="patch-icon">{{ badge.icon }}</span>
|
||||
{% endif %}
|
||||
</span></button>
|
||||
<dialog class="patch-dialog" id="patch-{{ badge.code }}">
|
||||
<form method="dialog"><button class="button button-secondary patch-dialog-close" aria-label="Schließen">×</button></form>
|
||||
{% if badge.image_path %}<img class="patch-dialog-preview" src="{{ badge.image_path }}" alt="Patch {{ badge.name }}">{% else %}<div class="patch-icon">{{ badge.icon }}</div>{% endif %}
|
||||
<h3>{{ badge.name }}</h3><p>{{ badge.description }}</p>
|
||||
<p><strong>Erhalten am:</strong> {{ badge.awarded_at }}</p>
|
||||
{% if badge.trigger_concert %}<p><strong>Ausgelöst durch:</strong> <a href="/concerts/{{ badge.trigger_concert.id }}">{{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}</a></p>{% else %}<p><strong>Grund:</strong> {{ badge.description }}</p>{% endif %}
|
||||
<form method="dialog"><button class="button button-secondary patch-dialog-close" aria-label="{{ _('Schließen') }}">×</button></form>
|
||||
{% if badge.image_path %}<img class="patch-dialog-preview" src="{{ badge.image_path }}" alt="Patch {{ badge.name | t }}">{% else %}<div class="patch-icon">{{ badge.icon }}</div>{% endif %}
|
||||
<h3>{{ badge.name | t }}</h3><p>{{ badge.description | t }}</p>
|
||||
<p><strong>{{ _('Erhalten am:') }}</strong> {{ badge.awarded_at }}</p>
|
||||
{% if badge.trigger_concert %}<p><strong>{{ _('Ausgelöst durch:') }}</strong> <a href="/concerts/{{ badge.trigger_concert.id }}">{{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}</a></p>{% else %}<p><strong>{{ _('Grund:') }}</strong> {{ badge.description | t }}</p>{% endif %}
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not profile.earned_codes %}
|
||||
<p>Noch keine Patches – dein erstes Konzert wartet!</p>
|
||||
<p>{{ _('Noch keine Patches – dein erstes Konzert wartet!') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,10 +162,10 @@
|
||||
{% if is_own_profile %}
|
||||
<section class="connections">
|
||||
<details class="connections-details" {% if connections.incoming %}open{% endif %}>
|
||||
<summary>🤝 Freunde{% if connections.friends %} · {{ connections.friends|length }}{% endif %}</summary>
|
||||
<summary>{{ _('🤝 Freunde') }}{% if connections.friends %} · {{ connections.friends|length }}{% endif %}</summary>
|
||||
<div class="connections-content">
|
||||
{% if connections.incoming %}
|
||||
<h3>Offene Anfragen</h3>
|
||||
<h3>{{ _('Offene Anfragen') }}</h3>
|
||||
<div class="connection-list">
|
||||
{% for connection in connections.incoming %}
|
||||
<div class="connection-row">
|
||||
@@ -172,15 +174,15 @@
|
||||
<strong>{{ connection.display_name }} <small>@{{ connection.username }}</small></strong>
|
||||
</a>
|
||||
<div class="connection-actions">
|
||||
<form method="post" action="/friendships/{{ connection.id }}/accept"><button class="button" type="submit">Annehmen</button></form>
|
||||
<form method="post" action="/friendships/{{ connection.id }}/decline"><button class="button button-secondary" type="submit">Ablehnen</button></form>
|
||||
<form method="post" action="/friendships/{{ connection.id }}/accept"><button class="button" type="submit">{{ _('Annehmen') }}</button></form>
|
||||
<form method="post" action="/friendships/{{ connection.id }}/decline"><button class="button button-secondary" type="submit">{{ _('Ablehnen') }}</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if connections.friends %}
|
||||
<h3>Deine Freunde</h3>
|
||||
<h3>{{ _('Deine Freunde') }}</h3>
|
||||
<div class="connection-list">
|
||||
{% for connection in connections.friends %}
|
||||
<div class="connection-row">
|
||||
@@ -188,15 +190,15 @@
|
||||
{% if connection.avatar_path %}<img class="connection-avatar" src="{{ connection.avatar_path }}" alt="">{% else %}<span class="connection-avatar">{{ connection.display_name[:1] }}</span>{% endif %}
|
||||
<strong>{{ connection.display_name }} <small>@{{ connection.username }}</small></strong>
|
||||
</a>
|
||||
<div class="connection-actions"><form method="post" action="/friendships/{{ connection.id }}/remove"><button class="button button-secondary" type="submit">Entfernen</button></form></div>
|
||||
<div class="connection-actions"><form method="post" action="/friendships/{{ connection.id }}/remove"><button class="button button-secondary" type="submit">{{ _('Entfernen') }}</button></form></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if connections.outgoing %}<p class="friend-note">{{ connections.outgoing|length }} gesendete Anfrage(n) warten noch auf Antwort.</p>{% endif %}
|
||||
{% if not connections.incoming and not connections.friends and not connections.outgoing %}<p class="friend-note">Noch keine Verbindungen. Finde andere User über die Suche auf der Startseite.</p>{% endif %}
|
||||
{% if connections.outgoing %}<p class="friend-note">{{ connections.outgoing|length }} {{ _('gesendete Anfrage(n) warten noch auf Antwort.') }}</p>{% endif %}
|
||||
{% if not connections.incoming and not connections.friends and not connections.outgoing %}<p class="friend-note">{{ _('Noch keine Verbindungen. Finde andere User über die Suche auf der Startseite.') }}</p>{% endif %}
|
||||
<details class="blocked-users">
|
||||
<summary>Blockierte Nutzer{% if connections.blocked %} ({{ connections.blocked|length }}){% endif %}</summary>
|
||||
<summary>{{ _('Blockierte Nutzer') }}{% if connections.blocked %} ({{ connections.blocked|length }}){% endif %}</summary>
|
||||
{% if connections.blocked %}
|
||||
<div class="connection-list">
|
||||
{% for connection in connections.blocked %}
|
||||
@@ -205,12 +207,12 @@
|
||||
{% if connection.avatar_path %}<img class="connection-avatar" src="{{ connection.avatar_path }}" alt="">{% else %}<span class="connection-avatar">{{ connection.display_name[:1] }}</span>{% endif %}
|
||||
<strong>{{ connection.display_name }} <small>@{{ connection.username }}</small></strong>
|
||||
</a>
|
||||
<div class="connection-actions"><form method="post" action="/users/{{ connection.username }}/unblock"><button class="button button-secondary" type="submit">Freigeben</button></form></div>
|
||||
<div class="connection-actions"><form method="post" action="/users/{{ connection.username }}/unblock"><button class="button button-secondary" type="submit">{{ _('Freigeben') }}</button></form></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="friend-note">Keine blockierten Nutzer.</p>
|
||||
<p class="friend-note">{{ _('Keine blockierten Nutzer.') }}</p>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
@@ -219,37 +221,37 @@
|
||||
{% endif %}
|
||||
{% if is_own_profile %}
|
||||
<section class="profile-edit">
|
||||
<h2>Profil bearbeiten</h2>
|
||||
{% if form_error %}<p class="profile-error" role="alert">{{ form_error }}</p>{% endif %}
|
||||
<h2>{{ _('Profil bearbeiten') }}</h2>
|
||||
{% if form_error %}<p class="profile-error" role="alert">{{ form_error | t }}</p>{% endif %}
|
||||
{% if form_success %}<p class="profile-success" role="status">{{ form_success }}</p>{% endif %}
|
||||
<form method="post" action="/profile" enctype="multipart/form-data" id="profile-form">
|
||||
<label for="display-name">Anzeigename</label>
|
||||
<label for="display-name">{{ _('Anzeigename') }}</label>
|
||||
<input id="display-name" name="display_name" type="text" value="{{ profile.display_name }}" maxlength="100">
|
||||
<label for="instagram-url">Instagram-Profil <small>(optional)</small></label>
|
||||
<input id="instagram-url" name="instagram_url" type="text" value="{{ instagram_input if instagram_input is not none else (profile.instagram_url or '') }}" placeholder="@name oder https://instagram.com/name" autocomplete="url">
|
||||
{% if profile.instagram_url %}<a class="instagram-current" href="{{ profile.instagram_url }}" target="_blank" rel="noopener noreferrer">Aktuell verknüpft: @{{ profile.instagram_handle }}</a>{% endif %}
|
||||
<label for="instagram-url">{{ _('Instagram-Profil') }} <small>(optional)</small></label>
|
||||
<input id="instagram-url" name="instagram_url" type="text" value="{{ instagram_input if instagram_input is not none else (profile.instagram_url or '') }}" placeholder="{{ _('@name oder https://instagram.com/name') }}" autocomplete="url">
|
||||
{% if profile.instagram_url %}<a class="instagram-current" href="{{ profile.instagram_url }}" target="_blank" rel="noopener noreferrer">{{ _('Aktuell verknüpft: @') }}{{ profile.instagram_handle }}</a>{% endif %}
|
||||
<fieldset>
|
||||
<legend>Wer darf alle Profildetails sehen?</legend>
|
||||
<legend>{{ _('Wer darf alle Profildetails sehen?') }}</legend>
|
||||
<div class="visibility-options">
|
||||
<label><input type="radio" name="profile_visibility" value="public" {% if profile.profile_visibility == 'public' %}checked{% endif %}> <span><strong>Alle User</strong><br><small>Jeder angemeldete User sieht auch Instagram und Beitrittsdatum.</small></span></label>
|
||||
<label><input type="radio" name="profile_visibility" value="friends" {% if profile.profile_visibility == 'friends' %}checked{% endif %}> <span><strong>Nur Freunde</strong><br><small>Username, Profilbild, Konzertanzahl und Kutte bleiben immer sichtbar.</small></span></label>
|
||||
<label><input type="radio" name="profile_visibility" value="nobody" {% if profile.profile_visibility == 'nobody' %}checked{% endif %}> <span><strong>Niemand</strong><br><small>Zusätzliche Details siehst nur du; Admins behalten administrativen Zugriff.</small></span></label>
|
||||
<label><input type="radio" name="profile_visibility" value="public" {% if profile.profile_visibility == 'public' %}checked{% endif %}> <span><strong>{{ _('Alle User') }}</strong><br><small>{{ _('Jeder angemeldete User sieht auch Instagram und Beitrittsdatum.') }}</small></span></label>
|
||||
<label><input type="radio" name="profile_visibility" value="friends" {% if profile.profile_visibility == 'friends' %}checked{% endif %}> <span><strong>{{ _('Nur Freunde') }}</strong><br><small>{{ _('Username, Profilbild, Konzertanzahl und Kutte bleiben immer sichtbar.') }}</small></span></label>
|
||||
<label><input type="radio" name="profile_visibility" value="nobody" {% if profile.profile_visibility == 'nobody' %}checked{% endif %}> <span><strong>{{ _('Niemand') }}</strong><br><small>{{ _('Zusätzliche Details siehst nur du; Admins behalten administrativen Zugriff.') }}</small></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<label for="avatar">Profilbild</label>
|
||||
<label for="avatar">{{ _('Profilbild') }}</label>
|
||||
<input id="avatar" name="avatar" type="file" accept="image/jpeg,image/png,image/webp">
|
||||
<span class="avatar-upload-status" id="avatar-upload-status">Große Bilder werden vor dem Upload automatisch optimiert.</span>
|
||||
<button class="button" type="submit">Profil speichern</button>
|
||||
<span class="avatar-upload-status" id="avatar-upload-status">{{ _('Große Bilder werden vor dem Upload automatisch optimiert.') }}</span>
|
||||
<button class="button" type="submit">{{ _('Profil speichern') }}</button>
|
||||
</form>
|
||||
<div class="account-actions">
|
||||
<details class="account-delete">
|
||||
<summary>Account löschen</summary>
|
||||
<p>Diese Aktion löscht dein Konto dauerhaft und kann nicht rückgängig gemacht werden.</p>
|
||||
<form method="post" action="/profile/delete" onsubmit="return confirm('Account wirklich dauerhaft löschen?');">
|
||||
<button type="submit" class="admin-danger">Account dauerhaft löschen</button>
|
||||
<summary>{{ _('Account löschen') }}</summary>
|
||||
<p>{{ _('Diese Aktion löscht dein Konto dauerhaft und kann nicht rückgängig gemacht werden.') }}</p>
|
||||
<form method="post" action="/profile/delete" onsubmit="return confirm({{ _('Account wirklich dauerhaft löschen?') | tojson | forceescape }});">
|
||||
<button type="submit" class="admin-danger">{{ _('Account dauerhaft löschen') }}</button>
|
||||
</form>
|
||||
</details>
|
||||
<p class="data-export"><a href="/profile/export">Meine gespeicherten Daten exportieren (JSON)</a></p>
|
||||
<p class="data-export"><a href="/profile/export">{{ _('Meine gespeicherten Daten exportieren (JSON)') }}</a></p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
@@ -288,7 +290,7 @@ if (profileForm) {
|
||||
profileForm.requestSubmit();
|
||||
} catch (error) {
|
||||
console.error("Avatar optimization failed:", error);
|
||||
status.textContent = "Das Bild konnte nicht verarbeitet werden. Bitte ein kleineres JPG, PNG oder WebP wählen.";
|
||||
status.textContent = {{ _('Das Bild konnte nicht verarbeitet werden. Bitte ein kleineres JPG, PNG oder WebP wählen.') | tojson }};
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user