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

234 lines
17 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="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ profile.display_name }} · Pingu Concerts</title>
<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; }
.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; }
.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; } }
</style>
</head>
<body>
<header>
<div class="header-inner">
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></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 (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 %}
</div>
{% 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">
<h2>🤝 Freunde</h2>
{% 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 %}
</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>
</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>