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:
2026-06-07 18:20:14 +02:00
parent cad1fa5c5c
commit 15ef7db5f7
11 changed files with 153 additions and 20 deletions
+9 -1
View File
@@ -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">
+15
View File
@@ -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}`;