User
This commit is contained in:
@@ -28,26 +28,31 @@ type Entry struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFeed(w http.ResponseWriter, r *http.Request) {
|
// feedPage liefert eine Feed-Seite (20 Beiträge, neueste zuerst). Ist uid != 0,
|
||||||
page, _ := strconv.Atoi(chi.URLParam(r, "page"))
|
// werden nur die Beiträge dieses Nutzers geliefert.
|
||||||
|
func feedPage(page int, uid int64) ([]Entry, error) {
|
||||||
if page < 0 {
|
if page < 0 {
|
||||||
page = 0
|
page = 0
|
||||||
}
|
}
|
||||||
if page > 100 {
|
if page > 100 {
|
||||||
page = 100
|
page = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = 20
|
const count = 20
|
||||||
offset := page * count
|
|
||||||
|
|
||||||
rows, err := db.Query(`
|
query := `
|
||||||
SELECT e.pid, e.uid, e.created_at, e.content, e.filepath, u.username
|
SELECT e.pid, e.uid, e.created_at, e.content, e.filepath, u.username
|
||||||
FROM entry e JOIN user u ON u.uid = e.uid
|
FROM entry e JOIN user u ON u.uid = e.uid`
|
||||||
ORDER BY e.created_at DESC
|
args := []any{}
|
||||||
LIMIT ? OFFSET ?`, count, offset)
|
if uid != 0 {
|
||||||
|
query += ` WHERE e.uid = ?`
|
||||||
|
args = append(args, uid)
|
||||||
|
}
|
||||||
|
query += ` ORDER BY e.created_at DESC LIMIT ? OFFSET ?`
|
||||||
|
args = append(args, count, page*count)
|
||||||
|
|
||||||
|
rows, err := db.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@@ -59,6 +64,33 @@ func handleFeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
entries = append(entries, e)
|
entries = append(entries, e)
|
||||||
}
|
}
|
||||||
|
return entries, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
page, _ := strconv.Atoi(chi.URLParam(r, "page"))
|
||||||
|
entries, err := feedPage(page, 0)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUserFeed liefert den Feed eines einzelnen Nutzers (öffentlich).
|
||||||
|
func handleUserFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var uid int64
|
||||||
|
if err := db.QueryRow(`SELECT uid FROM user WHERE username = ?`, chi.URLParam(r, "username")).Scan(&uid); err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := strconv.Atoi(chi.URLParam(r, "page"))
|
||||||
|
entries, err := feedPage(page, uid)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, entries)
|
writeJSON(w, http.StatusOK, entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ func routes() http.Handler {
|
|||||||
r.Get("/entry/{pid}/votes", handleVotes)
|
r.Get("/entry/{pid}/votes", handleVotes)
|
||||||
|
|
||||||
// --- User (öffentlich) ---
|
// --- User (öffentlich) ---
|
||||||
r.Get("/u/{username}", handleUserPage)
|
r.Get("/u/{username}", serveUserPage)
|
||||||
|
r.Get("/u/{username}/info", handleUserPage)
|
||||||
|
r.Get("/u/{username}/feed/{page}", handleUserFeed)
|
||||||
|
|
||||||
// --- Geschützt (Session erforderlich) ---
|
// --- Geschützt (Session erforderlich) ---
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ func idFromUsername(username string) int64 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveUserPage liefert die statische Profilseite; das Frontend lädt die Daten
|
||||||
|
// dann über /u/{username}/info und /u/{username}/feed/{page}.
|
||||||
|
func serveUserPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "web/user.html")
|
||||||
|
}
|
||||||
|
|
||||||
func handleUserPage(w http.ResponseWriter, r *http.Request) {
|
func handleUserPage(w http.ResponseWriter, r *http.Request) {
|
||||||
username := chi.URLParam(r, "username")
|
username := chi.URLParam(r, "username")
|
||||||
|
|
||||||
|
|||||||
+1
-14
@@ -46,25 +46,12 @@
|
|||||||
<p class="msg" id="entry-msg"></p>
|
<p class="msg" id="entry-msg"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Konto -->
|
|
||||||
<section id="account" hidden>
|
|
||||||
<p id="account-info" class="muted"></p>
|
|
||||||
<details>
|
|
||||||
<summary>Konto löschen</summary>
|
|
||||||
<form id="delete-form">
|
|
||||||
<p class="muted">Löscht dein Konto samt Beiträgen und Votes unwiderruflich.</p>
|
|
||||||
<input name="pass1" type="password" placeholder="Passwort bestätigen" autocomplete="current-password">
|
|
||||||
<button type="submit">Konto endgültig löschen</button>
|
|
||||||
</form>
|
|
||||||
<p class="msg" id="delete-msg"></p>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Feed -->
|
<!-- Feed -->
|
||||||
<section id="feed"></section>
|
<section id="feed"></section>
|
||||||
<div id="sentinel"></div>
|
<div id="sentinel"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/js/feed.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-145
@@ -1,20 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const api = {
|
|
||||||
async get(url) {
|
|
||||||
const r = await fetch(url, { credentials: "same-origin" });
|
|
||||||
return r.json();
|
|
||||||
},
|
|
||||||
async post(url, formData) {
|
|
||||||
const r = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
credentials: "same-origin",
|
|
||||||
});
|
|
||||||
return { ok: r.ok, status: r.status, data: await r.json().catch(() => ({})) };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Headerbar (eingeloggt?) ---
|
// --- Headerbar (eingeloggt?) ---
|
||||||
let loggedIn = false;
|
let loggedIn = false;
|
||||||
|
|
||||||
@@ -22,43 +7,24 @@ async function refreshHeader() {
|
|||||||
const h = await api.get("/auth/headerbar");
|
const h = await api.get("/auth/headerbar");
|
||||||
loggedIn = !!h.loggedin;
|
loggedIn = !!h.loggedin;
|
||||||
|
|
||||||
const bar = document.getElementById("headerbar");
|
|
||||||
document.getElementById("auth-box").hidden = loggedIn;
|
document.getElementById("auth-box").hidden = loggedIn;
|
||||||
document.getElementById("compose").hidden = !loggedIn;
|
document.getElementById("compose").hidden = !loggedIn;
|
||||||
document.getElementById("account").hidden = !loggedIn;
|
|
||||||
|
|
||||||
|
const bar = document.getElementById("headerbar");
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
bar.innerHTML = `<button id="logout-btn">Logout</button>`;
|
const me = await api.get("/user/info");
|
||||||
|
const user = encodeURIComponent(me.username);
|
||||||
|
bar.innerHTML = `<a href="/u/${user}">mein Profil</a> <button id="logout-btn">Logout</button>`;
|
||||||
document.getElementById("logout-btn").addEventListener("click", async () => {
|
document.getElementById("logout-btn").addEventListener("click", async () => {
|
||||||
await api.get("/auth/logout");
|
await api.get("/auth/logout");
|
||||||
await refreshHeader();
|
await refreshHeader();
|
||||||
});
|
});
|
||||||
loadAccount();
|
|
||||||
} else {
|
} else {
|
||||||
bar.innerHTML = "";
|
bar.innerHTML = "";
|
||||||
showAuth("login");
|
showAuth("login");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAccount() {
|
|
||||||
const info = await api.get("/user/info");
|
|
||||||
const since = new Date(info.created_at * 1000).toLocaleDateString("de-DE");
|
|
||||||
document.getElementById("account-info").textContent =
|
|
||||||
`Angemeldet als ${info.username} · Mitglied seit ${since}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("delete-form").addEventListener("submit", async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!confirm("Konto wirklich unwiderruflich löschen?")) return;
|
|
||||||
const res = await api.post("/user/delete", new FormData(e.target));
|
|
||||||
if (res.ok) {
|
|
||||||
await refreshHeader();
|
|
||||||
} else {
|
|
||||||
document.getElementById("delete-msg").textContent =
|
|
||||||
res.data.error || "Löschen fehlgeschlagen.";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Login / Registrierung ---
|
// --- Login / Registrierung ---
|
||||||
const loginForm = document.getElementById("login-form");
|
const loginForm = document.getElementById("login-form");
|
||||||
const registerForm = document.getElementById("register-form");
|
const registerForm = document.getElementById("register-form");
|
||||||
@@ -108,6 +74,13 @@ registerForm.addEventListener("submit", async (e) => {
|
|||||||
await refreshHeader();
|
await refreshHeader();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Feed ---
|
||||||
|
const feed = createFeed(
|
||||||
|
document.getElementById("feed"),
|
||||||
|
document.getElementById("sentinel"),
|
||||||
|
(page) => `/entry/feed/${page}`
|
||||||
|
);
|
||||||
|
|
||||||
// --- Neuer Beitrag ---
|
// --- Neuer Beitrag ---
|
||||||
document.getElementById("entry-form").addEventListener("submit", async (e) => {
|
document.getElementById("entry-form").addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -116,117 +89,11 @@ document.getElementById("entry-form").addEventListener("submit", async (e) => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
msg.textContent = "";
|
msg.textContent = "";
|
||||||
e.target.reset();
|
e.target.reset();
|
||||||
resetFeed();
|
feed.reset();
|
||||||
} else {
|
} else {
|
||||||
msg.textContent = res.data.error || "Beitrag fehlgeschlagen.";
|
msg.textContent = res.data.error || "Beitrag fehlgeschlagen.";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Feed mit Infinite Scroll ---
|
|
||||||
let page = 0;
|
|
||||||
let loading = false;
|
|
||||||
let done = false;
|
|
||||||
|
|
||||||
function renderEntry(e) {
|
|
||||||
const el = document.createElement("article");
|
|
||||||
el.className = "card";
|
|
||||||
|
|
||||||
const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
|
|
||||||
let media = "";
|
|
||||||
if (e.filepath) {
|
|
||||||
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.innerHTML = `
|
|
||||||
<div class="muted"><strong>${escapeHtml(e.username)}</strong> · ${when}</div>
|
|
||||||
<div class="content">${linkify(e.content)}</div>
|
|
||||||
${media}
|
|
||||||
<div class="row">
|
|
||||||
<button class="ghost" data-mode="left">Links</button>
|
|
||||||
<span class="muted">– / –</span>
|
|
||||||
<button class="ghost" data-mode="right">Rechts</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
setupVoting(el, e.pid);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupVoting(el, pid) {
|
|
||||||
const box = el.querySelector(".row");
|
|
||||||
const counts = box.querySelector("span");
|
|
||||||
const buttons = box.querySelectorAll(".ghost");
|
|
||||||
|
|
||||||
function render(state) {
|
|
||||||
if (state.error) {
|
|
||||||
counts.textContent = state.error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
counts.textContent = `${state.left} / ${state.right}`;
|
|
||||||
buttons.forEach((b) =>
|
|
||||||
b.classList.toggle("selected", b.dataset.mode === state.selected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.forEach((b) =>
|
|
||||||
b.addEventListener("click", async () => {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append("mode", b.dataset.mode);
|
|
||||||
const res = await api.post(`/entry/${pid}/vote`, fd);
|
|
||||||
render(res.data);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Anfangszustand laden (read-only).
|
|
||||||
api.get(`/entry/${pid}/votes`).then(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMore() {
|
|
||||||
if (loading || done) return;
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
const entries = await api.get(`/entry/feed/${page}`);
|
|
||||||
const feed = document.getElementById("feed");
|
|
||||||
|
|
||||||
if (!entries.length) {
|
|
||||||
done = true;
|
|
||||||
const end = document.createElement("p");
|
|
||||||
end.className = "muted";
|
|
||||||
end.textContent = "YOU REACHED THE END!";
|
|
||||||
feed.appendChild(end);
|
|
||||||
} else {
|
|
||||||
entries.forEach((e) => feed.appendChild(renderEntry(e)));
|
|
||||||
page++;
|
|
||||||
}
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFeed() {
|
|
||||||
page = 0;
|
|
||||||
done = false;
|
|
||||||
document.getElementById("feed").innerHTML = "";
|
|
||||||
loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((items) => {
|
|
||||||
if (items.some((i) => i.isIntersecting)) loadMore();
|
|
||||||
});
|
|
||||||
observer.observe(document.getElementById("sentinel"));
|
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
function escapeHtml(s) {
|
|
||||||
const d = document.createElement("div");
|
|
||||||
d.textContent = s ?? "";
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function linkify(s) {
|
|
||||||
const escaped = escapeHtml(s);
|
|
||||||
const url = /https?:\/\/[^\s<]+/g;
|
|
||||||
return escaped
|
|
||||||
.replace(url, (m) => `<a href="${m}" target="_blank" rel="noopener">${m}</a>`)
|
|
||||||
.replace(/\n/g, "<br>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Start ---
|
// --- Start ---
|
||||||
refreshHeader();
|
refreshHeader();
|
||||||
loadMore();
|
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
async get(url) {
|
||||||
|
const r = await fetch(url, { credentials: "same-origin" });
|
||||||
|
return r.json();
|
||||||
|
},
|
||||||
|
async post(url, formData) {
|
||||||
|
const r = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
|
return { ok: r.ok, status: r.status, data: await r.json().catch(() => ({})) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
const d = document.createElement("div");
|
||||||
|
d.textContent = s ?? "";
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkify(s) {
|
||||||
|
const escaped = escapeHtml(s);
|
||||||
|
const url = /https?:\/\/[^\s<]+/g;
|
||||||
|
return escaped
|
||||||
|
.replace(url, (m) => `<a href="${m}" target="_blank" rel="noopener">${m}</a>`)
|
||||||
|
.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEntry(e) {
|
||||||
|
const el = document.createElement("article");
|
||||||
|
el.className = "card";
|
||||||
|
|
||||||
|
const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
|
||||||
|
const user = encodeURIComponent(e.username);
|
||||||
|
let media = "";
|
||||||
|
if (e.filepath) {
|
||||||
|
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="muted"><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}</div>
|
||||||
|
<div class="content">${linkify(e.content)}</div>
|
||||||
|
${media}
|
||||||
|
<div class="row">
|
||||||
|
<button class="ghost" data-mode="left">Links</button>
|
||||||
|
<span class="muted">– / –</span>
|
||||||
|
<button class="ghost" data-mode="right">Rechts</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
setupVoting(el, e.pid);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVoting(el, pid) {
|
||||||
|
const box = el.querySelector(".row");
|
||||||
|
const counts = box.querySelector("span");
|
||||||
|
const buttons = box.querySelectorAll(".ghost");
|
||||||
|
|
||||||
|
function render(state) {
|
||||||
|
if (state.error) {
|
||||||
|
counts.textContent = state.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
counts.textContent = `${state.left} / ${state.right}`;
|
||||||
|
buttons.forEach((b) =>
|
||||||
|
b.classList.toggle("selected", b.dataset.mode === state.selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.forEach((b) =>
|
||||||
|
b.addEventListener("click", async () => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("mode", b.dataset.mode);
|
||||||
|
const res = await api.post(`/entry/${pid}/vote`, fd);
|
||||||
|
render(res.data);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
api.get(`/entry/${pid}/votes`).then(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFeed hängt Infinite-Scroll an feedEl/sentinelEl und lädt Seiten über
|
||||||
|
// urlFor(page). Gibt { reset } zurück, um den Feed neu zu laden.
|
||||||
|
function createFeed(feedEl, sentinelEl, urlFor) {
|
||||||
|
let page = 0;
|
||||||
|
let loading = false;
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading || done) return;
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
const entries = await api.get(urlFor(page));
|
||||||
|
if (!entries.length) {
|
||||||
|
done = true;
|
||||||
|
const end = document.createElement("p");
|
||||||
|
end.className = "muted";
|
||||||
|
end.textContent = "YOU REACHED THE END!";
|
||||||
|
feedEl.appendChild(end);
|
||||||
|
} else {
|
||||||
|
entries.forEach((e) => feedEl.appendChild(renderEntry(e)));
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
page = 0;
|
||||||
|
done = false;
|
||||||
|
feedEl.innerHTML = "";
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((items) => {
|
||||||
|
if (items.some((i) => i.isIntersecting)) loadMore();
|
||||||
|
});
|
||||||
|
observer.observe(sentinelEl);
|
||||||
|
|
||||||
|
loadMore();
|
||||||
|
return { reset };
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Nutzername aus dem Pfad /u/<name>.
|
||||||
|
const username = decodeURIComponent(location.pathname.split("/")[2] || "");
|
||||||
|
|
||||||
|
document.getElementById("delete-form").addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!confirm("Konto wirklich unwiderruflich löschen?")) return;
|
||||||
|
const res = await api.post("/user/delete", new FormData(e.target));
|
||||||
|
if (res.ok) {
|
||||||
|
location.href = "/";
|
||||||
|
} else {
|
||||||
|
document.getElementById("delete-msg").textContent =
|
||||||
|
res.data.error || "Löschen fehlgeschlagen.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const info = await api.get(`/u/${encodeURIComponent(username)}/info`);
|
||||||
|
if (info.error) {
|
||||||
|
document.getElementById("profile-name").textContent = "Nutzer nicht gefunden";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("profile-name").textContent = info.username;
|
||||||
|
const since = new Date(info.created_at * 1000).toLocaleDateString("de-DE");
|
||||||
|
document.getElementById("profile-info").textContent = `Mitglied seit ${since}`;
|
||||||
|
|
||||||
|
// Eigene Seite? -> Einstellungen einblenden.
|
||||||
|
const me = await api.get("/user/info");
|
||||||
|
const isOwner = me.username === info.username;
|
||||||
|
document.getElementById("settings").hidden = !isOwner;
|
||||||
|
document.getElementById("feed-heading").textContent =
|
||||||
|
isOwner ? "Deine Beiträge" : `Beiträge von ${info.username}`;
|
||||||
|
|
||||||
|
createFeed(
|
||||||
|
document.getElementById("feed"),
|
||||||
|
document.getElementById("sentinel"),
|
||||||
|
(page) => `/u/${encodeURIComponent(username)}/feed/${page}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>kver</title>
|
||||||
|
<link rel="stylesheet" href="/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>kver</h1>
|
||||||
|
<div id="headerbar"><a href="/">zurück zum Feed</a></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h2 id="profile-name"></h2>
|
||||||
|
<p id="profile-info" class="muted"></p>
|
||||||
|
|
||||||
|
<!-- Nur für den Eigentümer sichtbar -->
|
||||||
|
<section id="settings" hidden>
|
||||||
|
<h2>Einstellungen</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Konto löschen</summary>
|
||||||
|
<form id="delete-form">
|
||||||
|
<p class="muted">Löscht dein Konto samt Beiträgen und Votes unwiderruflich.</p>
|
||||||
|
<input name="pass1" type="password" placeholder="Passwort bestätigen" autocomplete="current-password">
|
||||||
|
<button type="submit">Konto endgültig löschen</button>
|
||||||
|
</form>
|
||||||
|
<p class="msg" id="delete-msg"></p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 id="feed-heading">Beiträge</h2>
|
||||||
|
<section id="feed"></section>
|
||||||
|
<div id="sentinel"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/js/feed.js"></script>
|
||||||
|
<script src="/js/user.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user