286 lines
21 KiB
HTML
286 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<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: flex; flex-wrap: wrap; gap: 14px; justify-content: flex-start; align-items: flex-start; }
|
||
.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: fit-content; height: fit-content; }
|
||
.patch-icon-frame { width: 105px; height: 105px; }
|
||
.patch-icon { font-size: 2.5rem; }
|
||
.patch-image { width: auto; height: auto; 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); }
|
||
.profile-edit { margin-top: 24px; }
|
||
.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>
|
||
</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>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 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?');"><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 %}
|
||
<div class="patch earned {% if badge.image_path %}patch-image-frame{% else %}patch-icon-frame{% endif %}" title="{{ badge.name }} – {{ badge.description }}" aria-label="{{ badge.name }}: {{ badge.description }}">
|
||
{% if badge.image_path %}
|
||
<img class="patch-image" src="{{ badge.image_path }}" alt="Patch {{ badge.name }}">
|
||
{% else %}
|
||
<span class="patch-icon">{{ badge.icon }}</span>
|
||
{% endif %}
|
||
</div>
|
||
{% 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 %}
|
||
<section class="profile-edit">
|
||
<h2>Profil bearbeiten</h2>
|
||
{% if form_error %}<p class="profile-error" role="alert">{{ form_error }}</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>
|
||
<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>
|
||
</form>
|
||
</details>
|
||
<p class="data-export"><a href="/profile/export">Meine gespeicherten Daten exportieren (JSON)</a></p>
|
||
</div>
|
||
</section>
|
||
{% 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.";
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|