Add social features and harden beta release
This commit is contained in:
+140
-5
@@ -13,9 +13,15 @@
|
||||
.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: #0f1420; border-radius: 12px; }
|
||||
.stat strong { display: block; color: #c4b5fd; font-size: 2.2rem; }
|
||||
.kutte { position: relative; min-height: 320px; padding: 58px 36px 28px; background: linear-gradient(90deg, #151515 0 46%, #222 46% 54%, #151515 54%); border: 4px solid #353535; border-radius: 28px 28px 14px 14px; box-shadow: inset 0 0 0 2px #090909; }
|
||||
.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%; }
|
||||
@@ -26,10 +32,25 @@
|
||||
.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-style: solid; border-color: #a78bfa; box-shadow: 0 0 12px rgba(167, 139, 250, .35); }
|
||||
.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>
|
||||
@@ -37,7 +58,7 @@
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||
<a href="/profile">Mein Profil</a>
|
||||
{% include '_user_menu.html' %}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
@@ -54,8 +75,30 @@
|
||||
{% endif %}
|
||||
<h2 class="profile-name">{{ profile.display_name }}</h2>
|
||||
<p class="profile-handle">@{{ profile.username }}</p>
|
||||
<p class="profile-since">Dabei seit {{ profile.created_at }}</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>
|
||||
@@ -81,18 +124,110 @@
|
||||
</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>
|
||||
<form method="post" action="/profile" enctype="multipart/form-data">
|
||||
{% 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/webp", .86));
|
||||
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(new File([blob], "profilbild.webp", {type: "image/webp"}));
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user