Profilbilder: Avatar-Upload, Anzeige auf Profil & Feed
- user.avatar-Spalte (Schema, migrate.sh, Migrationsdoku)
- POST /user/avatar (geschützt): Upload via storeImage, ersetzt/löscht altes Bild
- avatar in /u/{name}/info, /user/info und im entrySelect-Join (Feed-Karten)
- Account-Löschung entfernt auch das Avatar-File
- Frontend: Avatar im Profilkopf + Byline der Karten (50px), Upload-Form,
Platzhalterbild no_profile_pic.jpg als Fallback
- .card img per :not(.avatar) von Beitragsbildern getrennt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+31
-2
@@ -158,17 +158,46 @@ a.button:hover, button:hover {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.card img {
|
||||
/* Beitragsbild (nicht der Byline-Avatar). */
|
||||
.card img:not(.avatar) {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
/* Fokussierter Beitrag zeigt das Bild in voller Breite. */
|
||||
#focus .card img {
|
||||
#focus .card img:not(.avatar) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* --- Avatare --- */
|
||||
img.avatar {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background-color: #eee;
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
img.avatar-sm { width: 50px; height: 50px; }
|
||||
img.avatar-lg { width: 72px; height: 72px; }
|
||||
|
||||
/* Byline mit kleinem Avatar neben Name + Zeitstempel. */
|
||||
.byline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Profilkopf: großer Avatar links neben Name und "Mitglied seit". */
|
||||
.profile-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Ahnenkette: der Strich links symbolisiert den Thread oberhalb des Beitrags. */
|
||||
#ancestors .card {
|
||||
border-left: 3px solid gray;
|
||||
|
||||
+9
-1
@@ -21,6 +21,11 @@ function escapeHtml(s) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// avatarUrl liefert den Bildpfad oder das Platzhalterbild bei leerem Avatar.
|
||||
function avatarUrl(path) {
|
||||
return path ? `/${path}` : "/static/assets/no_profile_pic.jpg";
|
||||
}
|
||||
|
||||
function linkify(s) {
|
||||
const escaped = escapeHtml(s);
|
||||
const url = /https?:\/\/[^\s<]+/g;
|
||||
@@ -57,7 +62,10 @@ function renderEntry(e) {
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="muted"><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}</div>
|
||||
<div class="muted byline">
|
||||
<a href="/u/${user}"><img class="avatar avatar-sm" src="${avatarUrl(e.avatar)}" alt="" loading="lazy"></a>
|
||||
<span><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}</span>
|
||||
</div>
|
||||
<div class="content">${linkify(e.content)}</div>
|
||||
${media}
|
||||
<div class="interactions">
|
||||
|
||||
@@ -14,6 +14,20 @@ document.getElementById("rename-form").addEventListener("submit", async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("avatar-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await api.post("/user/avatar", new FormData(e.target));
|
||||
if (res.ok) {
|
||||
// Cache umgehen, damit das neue Bild sofort erscheint.
|
||||
document.getElementById("profile-avatar").src = `${avatarUrl(res.data.avatar)}?t=${Date.now()}`;
|
||||
e.target.reset();
|
||||
document.getElementById("avatar-msg").textContent = "";
|
||||
} else {
|
||||
document.getElementById("avatar-msg").textContent =
|
||||
res.data.error || "Hochladen fehlgeschlagen.";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("delete-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!confirm("Konto wirklich unwiderruflich löschen?")) return;
|
||||
@@ -34,6 +48,7 @@ async function init() {
|
||||
}
|
||||
|
||||
document.getElementById("profile-name").textContent = info.username;
|
||||
document.getElementById("profile-avatar").src = avatarUrl(info.avatar);
|
||||
const since = new Date(info.created_at * 1000).toLocaleDateString("de-DE");
|
||||
document.getElementById("profile-info").textContent = `Mitglied seit ${since}`;
|
||||
|
||||
|
||||
+15
-2
@@ -19,12 +19,25 @@
|
||||
|
||||
<main>
|
||||
<section id="profile">
|
||||
<h2 id="profile-name"></h2>
|
||||
<p id="profile-info" class="muted"></p>
|
||||
<div class="profile-head">
|
||||
<img id="profile-avatar" class="avatar avatar-lg" src="/static/assets/no_profile_pic.jpg" alt="">
|
||||
<div>
|
||||
<h2 id="profile-name"></h2>
|
||||
<p id="profile-info" class="muted"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nur für den Eigentümer sichtbar -->
|
||||
<div id="settings" hidden>
|
||||
<h2>Einstellungen</h2>
|
||||
<details>
|
||||
<summary>Profilbild ändern</summary>
|
||||
<form id="avatar-form">
|
||||
<input name="avatar" type="file" accept="image/*">
|
||||
<button type="submit">Bild hochladen</button>
|
||||
</form>
|
||||
<p class="msg" id="avatar-msg"></p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Name ändern</summary>
|
||||
<form id="rename-form">
|
||||
|
||||
Reference in New Issue
Block a user