Files
pingu-concerts/app/templates/profile.html
T

326 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="{{ language() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ profile.display_name }} · MetalCircle</title>
<link rel="icon" type="image/png" href="/static/images/metalcircle-circle.png">
<link rel="stylesheet" href="/static/css/style.css">
<style>
.profile-layout { display: grid; grid-template-columns: minmax(240px, .8fr) minmax(0, 1.4fr); gap: 24px; align-items: start; }
.profile-card, .badges-card, .profile-edit { padding: 24px; background: linear-gradient(145deg, rgba(24,24,24,.96), rgba(45,12,12,.8)); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 14px 32px rgba(0,0,0,.35); }
.profile-card { text-align: center; border-top: 4px solid var(--accent); }
.avatar, .avatar-fallback { width: 132px; height: 132px; margin: 0 auto 16px; border-radius: 50%; border: 3px solid var(--accent); object-fit: cover; }
.avatar-fallback { display: grid; place-items: center; background: #283048; font-size: 3rem; font-weight: 800; }
.profile-name { margin: 0; font-size: 1.7rem; }
.profile-handle, .profile-since { color: var(--muted); margin: 6px 0 0; }
.instagram-link { display: inline-flex; margin-top: 12px; padding: 8px 12px; color: #fff; background: #7f1d1d; border: 1px solid #dc2626; border-radius: 9px; font-weight: 700; }
.instagram-link:hover { color: #f87171; }
.profile-error { margin-bottom: 18px; padding: 12px 14px; color: #fecaca; background: #450a0a; border: 1px solid #b91c1c; border-radius: 9px; }
.profile-success { margin-bottom: 18px; padding: 12px 14px; color: #bbf7d0; background: #052e16; border: 1px solid #15803d; border-radius: 9px; }
.instagram-current { display: block; margin: -8px 0 14px; color: #f87171; }
.avatar-upload-status { display: block; min-height: 1.2em; margin: -8px 0 12px; color: var(--muted); font-size: .85rem; }
.stat { margin-top: 22px; padding: 16px; background: rgba(5,5,5,.72); border: 1px solid #5f1515; border-radius: 12px; }
.stat strong { display: block; color: #c4b5fd; font-size: 2.2rem; }
.kutte { position: relative; min-height: 320px; padding: 58px 36px 28px; background-image: linear-gradient(90deg, rgba(0,0,0,.2) 0 46%, rgba(255,255,255,.035) 46% 54%, rgba(0,0,0,.2) 54%), url('/static/images/black-denim-texture.webp'); background-size: 100% 100%, 520px auto; background-repeat: no-repeat, repeat; border: 4px solid #292524; border-radius: 28px 28px 14px 14px; box-shadow: inset 0 0 28px 8px rgba(0,0,0,.8), 0 10px 30px rgba(0,0,0,.45); }
.kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; }
.kutte::before { left: 0; border-radius: 24px 0 45% 0; }
.kutte::after { right: 0; border-radius: 0 24px 0 45%; }
.patch-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: row; gap: 14px; align-items: start; }
.patch-grid > p { grid-column: 1 / -1; }
.patch-button { display: flex; width: 100%; min-width: 0; justify-content: center; padding:0; border:0; background:transparent; color:inherit; cursor:pointer; }
.patch { padding: 5px; display: grid; place-items: center; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; }
.patch-image-frame { width: 100%; height: 140px; }
.patch-icon-frame { width: 105px; height: 105px; }
.patch-icon { font-size: 2.5rem; }
.patch-image { width: 100%; height: 100%; max-width: 140px; max-height: 140px; object-fit: contain; display: block; }
.patch.locked { opacity: .38; filter: grayscale(1); }
.patch.earned { border: 0; background: transparent; box-shadow: 2px 3px 7px rgba(0,0,0,.7); }
.patch-dialog { width:min(92vw,460px); padding:24px; color:#f8fafc; background:#171717; border:1px solid var(--border); border-radius:14px; box-shadow:0 20px 60px #000; }
.patch-dialog::backdrop { background:rgba(0,0,0,.78); }
.patch-dialog-description { line-height:1.55; }
.patch-dialog-reason { padding-top:10px; border-top:1px solid var(--border); color:var(--muted); }
.patch-dialog-preview { max-width:150px; max-height:150px; object-fit:contain; }
.patch-dialog-close { float:right; margin:0; }
.profile-edit { margin-top: 24px; padding: 0; overflow: hidden; }
.profile-edit-summary { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:16px 22px; color:#fca5a5; cursor:pointer; list-style:none; font-size:1.15rem; font-weight:700; }
.profile-edit-summary::-webkit-details-marker { display:none; }
.profile-edit-summary::after { content:"+"; color:var(--muted); font-size:1.4rem; line-height:1; }
.profile-edit[open] > .profile-edit-summary::after { content:""; }
.profile-edit-content { padding:0 22px 22px; }
.account-actions { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; margin-top:22px; padding-top:16px; border-top:1px solid var(--border); }
.account-delete { flex:1; color:var(--muted); }
.account-delete summary { color:#fca5a5; cursor:pointer; font-weight:700; }
.account-delete p { font-size:.85rem; }
.data-export { flex:0 0 auto; margin:0; text-align:right; }
.data-export a { color:#a8a29e; font-size:.8rem; }
.data-export a:hover { color:#fca5a5; }
.profile-edit form { margin: 0; max-width: none; }
.profile-edit input { margin-bottom: 14px; }
.friend-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 16px; }
.friend-actions form { margin: 0; }
.friend-note { margin: 14px 0 0; color: var(--muted); font-size: .9rem; }
.private-note { margin: 12px 0 0; padding: 10px; color: var(--muted); background: #0a0a0a; border-radius: 9px; }
.visibility-options { display: grid; gap: 8px; margin: 6px 0 18px; }
.visibility-options label { display: flex; align-items: flex-start; gap: 9px; margin: 0; }
.visibility-options input { width: auto; margin: 4px 0 0; }
.connections { margin-top: 24px; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; }
.connections-details > summary { color:#fca5a5; cursor:pointer; font-size:1.35rem; font-weight:700; }
.connections-content { margin-top:18px; }
.blocked-users { margin-top:18px; padding-top:12px; border-top:1px solid var(--border); }
.blocked-users > summary { width:fit-content; color:var(--muted); cursor:pointer; font-size:.78rem; text-decoration:underline; }
.connection-list { display: grid; gap: 9px; }
.connection-row { display: flex; align-items: center; gap: 11px; padding: 11px; background: #090909; border: 1px solid var(--border); border-radius: 10px; }
.connection-row > a { display: flex; min-width: 0; flex: 1; align-items: center; gap: 10px; }
.connection-avatar { width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; object-fit: cover; background: #292524; display: grid; place-items: center; }
.connection-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.connection-actions form { margin: 0; }
.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>
<div class="header-inner">
<a href="/" class="logo"><img class="brand-logo" src="/static/images/metalcircle-full.png" alt="MetalCircle"></a>
{% include '_header_controls.html' %}
</div>
</header>
<main>
<div class="page-title">
<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 }}">
{% else %}
<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 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>
{% if not is_own_profile %}
<div class="friend-actions">
{% if can_view_details and not block_status.blocked_by_viewer and not block_status.blocked_viewer %}
<form method="post" action="/users/{{ profile.username }}/follow">
<input type="hidden" name="action" value="{% if is_following_user %}unfollow{% else %}follow{% endif %}">
<button class="button {% if is_following_user %}button-secondary{% endif %}" type="submit">{% if is_following_user %}{{ _('Nutzer nicht mehr folgen') }}{% else %}{{ _('Nutzer folgen') }}{% endif %}</button>
</form>
{% endif %}
{% 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>
{% endif %}
{% if not friendship %}
<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>
{% 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>
{% else %}
<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>
{% else %}
<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 %}
{% 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>
<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 | t }} {{ _('anzeigen') }}">
<span class="patch earned patch-{{ badge.code }} {% 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 | 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 | t }}">{% else %}<div class="patch-icon">{{ badge.icon }}</div>{% endif %}
<h3>{{ badge.name | t }}</h3><p class="patch-dialog-description">{{ badge.long_description | t }}</p>
<p><strong>{{ _('Erhalten am:') }}</strong> {{ badge.awarded_at }}</p>
{% if badge.trigger_concert %}<p class="patch-dialog-reason"><strong>{{ _('Ausgelöst durch:') }}</strong> <a href="/concerts/{{ badge.trigger_concert.id }}">{{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}</a></p>{% else %}<p class="patch-dialog-reason"><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>
{% endif %}
</div>
</div>
</section>
</div>
{% 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>
<div class="connections-content">
{% if connections.incoming %}
<h3>{{ _('Offene Anfragen') }}</h3>
<div class="connection-list">
{% for connection in connections.incoming %}
<div class="connection-row">
<a href="/users/{{ connection.username }}">
{% 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 }}/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>
<div class="connection-list">
{% for connection in connections.friends %}
<div class="connection-row">
<a href="/users/{{ connection.username }}">
{% 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>
{% 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 %}
<details class="blocked-users">
<summary>{{ _('Blockierte Nutzer') }}{% if connections.blocked %} ({{ connections.blocked|length }}){% endif %}</summary>
{% if connections.blocked %}
<div class="connection-list">
{% for connection in connections.blocked %}
<div class="connection-row">
<a href="/users/{{ connection.username }}">
{% 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>
{% endfor %}
</div>
{% else %}
<p class="friend-note">{{ _('Keine blockierten Nutzer.') }}</p>
{% endif %}
</details>
</div>
</details>
</section>
{% endif %}
{% if is_own_profile %}
<details class="profile-edit"{% if form_error or form_success %} open{% endif %}>
<summary class="profile-edit-summary">{{ _('Profil bearbeiten') }}</summary>
<div class="profile-edit-content">
{% 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>
<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 %}
<fieldset>
<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>
</div>
</fieldset>
<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>
</form>
<section id="notification-settings">
<h2>{{ _('Push-Benachrichtigungen') }}</h2>
<p>{{ _('Wähle, welche Hinweise du auf deinen angemeldeten Android-Geräten erhalten möchtest. Nachrichteninhalte werden nicht angezeigt.') }}</p>
<form method="post" action="/profile/notifications" class="notification-preferences">
{% for kind, label in [('friend_request', 'Freundschaftsanfragen'), ('direct_message', 'Direktnachrichten'), ('event_invitation', 'Veranstaltungseinladungen')] %}
<label><input type="checkbox" name="{{ kind }}" value="true"{% if push_preferences is not defined or push_preferences[kind] %} checked{% endif %}> {{ label | t }}</label>
{% endfor %}
<button class="button" type="submit">{{ _('Benachrichtigungen speichern') }}</button>
</form>
</section>
<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?') | 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>
</div>
</div>
</details>
{% endif %}
</main>
<script>
const profileForm = document.getElementById("profile-form");
if (profileForm) {
profileForm.addEventListener("submit", async event => {
if (profileForm.dataset.optimized === "true") return;
const input = document.getElementById("avatar");
const file = input.files[0];
if (!file) return;
event.preventDefault();
const status = document.getElementById("avatar-upload-status");
const button = profileForm.querySelector('button[type="submit"]');
button.disabled = true;
status.textContent = "Profilbild wird optimiert …";
try {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, 1000 / 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();
const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", .88));
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
const transfer = new DataTransfer();
transfer.items.add(new File([blob], "profilbild.jpg", {type: "image/jpeg"}));
input.files = transfer.files;
profileForm.dataset.optimized = "true";
status.textContent = `Optimiert: ${Math.ceil(blob.size / 1024)} KB wird hochgeladen …`;
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.') | tojson }};
button.disabled = false;
}
});
}
</script>
</body>
</html>