Umbau-Projekt (SSR und Rest)

This commit is contained in:
2026-09-03 22:20:55 +02:00
parent 13363cb568
commit c66df283d1
37 changed files with 3719 additions and 344 deletions
+25
View File
@@ -0,0 +1,25 @@
{{define "base"}}<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} · Kontrollverlust</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<header>
<nav id="legal">
<a href="/static/docs/impressum.html">Impressum</a> -
<a href="/static/docs/datenschutz.html">Datenschutz</a> -
<a href="/static/docs/beitragsordnung.html">Beitragsordnung</a> -
<a href="/stats.html">Statistik</a>
</nav>
<a href="/feed"><h1>Kontrollverlust</h1></a>
</header>
<main>
{{template "content" .}}
</main>
</body>
</html>
{{end}}
+30
View File
@@ -0,0 +1,30 @@
{{define "content"}}
{{with .Form}}
<section id="confirm-delete">
<h2>Beitrag löschen?</h2>
<article class="card">
<div class="threadline-wrapper">
<div class="threadline"></div>
<div class="body">
<div class="content">{{content .Content}}</div>
{{if .HasImage}}<p class="muted">Das angehängte Bild wird mit gelöscht.</p>{{end}}
</div>
</div>
</article>
<p>Der Text und das Bild werden entfernt. Der Beitrag bleibt als
<span class="muted">[deleted]</span>-Platzhalter stehen, damit
{{if .IsRoot}}der Thread{{else}}die Antworten darunter{{end}} nicht
verwaisen.
{{if .Replies}}Die {{.Replies}} Antwort(en) darauf bleiben erhalten.{{end}}</p>
<p><strong>Das lässt sich nicht rückgängig machen.</strong></p>
<form method="post" action="/entry/{{.PID}}/delete">
<input type="hidden" name="return_to" value="{{.BackURL}}">
<button type="submit">Endgültig löschen</button>
</form>
<p class="muted"><a href="{{.BackURL}}">Abbrechen</a></p>
</section>
{{end}}
{{end}}
+19
View File
@@ -0,0 +1,19 @@
{{define "content"}}
{{with .Form}}
<section id="compose">
{{/* Kein enctype/Dateifeld: das angehängte Bild bleibt beim Bearbeiten
unverändert (siehe handleEditEntry). Ein Dateifeld hier würde
versprechen, was der Endpunkt nicht tut. */}}
<form method="post" action="/entry/{{.PID}}/edit">
<input type="hidden" name="return_to" value="{{.BackURL}}">
<h2>Beitrag bearbeiten</h2>
{{with $.Err}}<p class="msg">{{.Msg}}</p>{{end}}
<label for="content">Text</label>
<textarea id="content" name="content" rows="6" maxlength="1000"
required>{{.Content}}</textarea>
<button type="submit">Speichern</button>
</form>
<p class="muted"><a href="{{.BackURL}}">Abbrechen</a></p>
</section>
{{end}}
{{end}}
+75
View File
@@ -0,0 +1,75 @@
{{/*
Ein Beitrag und die Blätternavigation -- als eigene Blöcke, damit Hauptfeed
und Nutzerfeed dieselbe Darstellung benutzen. Das Gegenstück zu renderEntry
in web/js/feed.js.
Jede Aktion ist ein echtes Formular mit return_to: ohne JavaScript ist eine
Interaktion immer ein Seitenwechsel, und der Anker #e{{.PID}} bringt den
Nutzer danach an genau den Beitrag zurück, den er bedient hat.
*/}}
{{define "entry"}}
<article class="card" id="e{{.PID}}">
{{if .Deleted}}
<div class="muted byline">
<span>[deleted] · <time datetime="{{timeAttr .CreatedAt}}">{{time .CreatedAt}}</time></span>
</div>
<div class="threadline-wrapper">
<div class="threadline"></div>
<div class="body"><div class="content">[deleted]</div></div>
</div>
{{else}}
<div class="muted byline">
<a href="{{profile .Username}}"><img class="avatar avatar-sm" src="{{avatar .Avatar}}" alt="" loading="lazy"></a>
<span><a href="{{profile .Username}}">{{.Username}}</a> ·
<time datetime="{{timeAttr .CreatedAt}}">{{time .CreatedAt}}</time></span>
<details class="report">
<summary>Melden</summary>
<form class="report-form" method="post" action="/entry/{{.PID}}/report">
<input type="hidden" name="return_to" value="{{.ReturnTo}}#e{{.PID}}">
<label for="reason-{{.PID}}">Grund <span class="muted">(optional)</span></label>
<input id="reason-{{.PID}}" name="reason" type="text" maxlength="500" autocomplete="off">
<button type="submit">Absenden</button>
</form>
</details>
</div>
<div class="threadline-wrapper">
<div class="threadline"></div>
<div class="body">
<div class="content">{{content .Content}}</div>
{{if .Filepath}}<img src="/{{.Filepath}}" alt="" loading="lazy">{{end}}
<div class="interactions">
<a class="button reply" href="/beitrag/{{.PID}}">Antworten ({{.ReplyCount}})</a>
{{if .EditURL}}<a class="button" href="{{.EditURL}}">Bearbeiten</a>{{end}}
{{if .DeleteURL}}<a class="button" href="{{.DeleteURL}}">Löschen</a>{{end}}
{{if .LoggedIn}}
<form class="row" method="post" action="/entry/{{.PID}}/vote">
<input type="hidden" name="return_to" value="{{.ReturnTo}}#e{{.PID}}">
<button class="ghost{{if eq .Selected "left"}} selected{{end}}"
name="mode" value="left">Links</button>
<span class="muted">{{.Left}} / {{.Right}}</span>
<button class="ghost{{if eq .Selected "right"}} selected{{end}}"
name="mode" value="right">Rechts</button>
</form>
{{else}}
{{/* Ohne Anmeldung nur der Stand -- ein Formular hier würde
ausschließlich 401 liefern. */}}
<div class="row">
<span class="muted">{{.Left}} / {{.Right}} ·
<a href="/auth/login">anmelden zum Abstimmen</a></span>
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</article>
{{end}}
{{define "pager"}}
<nav class="pager">
{{if .PrevURL}}<a class="button" href="{{.PrevURL}}">← Neuere</a>{{end}}
<span class="muted">Seite {{.HumanPage}} von {{.Pages}}</span>
{{if .NextURL}}<a class="button" href="{{.NextURL}}">Ältere →</a>{{end}}
</nav>
{{end}}
+34
View File
@@ -0,0 +1,34 @@
{{define "content"}}
{{with .Form}}
{{if .CanPost}}
<section id="compose">
{{/* enctype ist Pflicht, sonst schickt der Browser nur den Dateinamen
statt der Datei -- ohne JavaScript gibt es keinen zweiten Versuch. */}}
<form method="post" action="/entry/create" enctype="multipart/form-data">
<input type="hidden" name="page" value="{{.Page}}">
{{with $.Err}}<p class="msg">{{.Msg}}</p>{{end}}
<label for="content">Neuer Beitrag</label>
<textarea id="content" name="content" rows="4" maxlength="1000"
placeholder="Was gibt's Neues?">{{.Compose.Content}}</textarea>
<label for="file">Bild <span class="muted">(optional)</span></label>
<input id="file" name="file" type="file" accept="image/*">
{{if .Compose.HadFile}}
<p class="msg">Dein Bild musst du erneut auswählen – Browser können
Dateifelder aus Sicherheitsgründen nicht vorbelegen.</p>
{{end}}
<button type="submit">Eintrag erstellen</button>
</form>
</section>
{{else if .IsTOR}}
<p class="muted">Über TOR ist das Schreiben von Beiträgen derzeit nicht
möglich. Lesen, Abstimmen und Melden funktionieren.</p>
{{end}}
<section id="feed">
{{range .Entries}}{{template "entry" .}}{{else}}
<p class="muted">Noch keine Beiträge.</p>
{{end}}
</section>
{{template "pager" .}}
{{end}}
{{end}}
+16
View File
@@ -0,0 +1,16 @@
{{define "content"}}
<section id="auth-box">
<form method="post" action="/auth/login">
<h2>Login</h2>
{{with .Err}}<p class="msg">{{.Msg}}</p>{{end}}
<label for="user">Nutzername</label>
<input id="user" name="user" autocomplete="username" required
{{if .Form}}value="{{.Form.User}}"{{end}}>
<label for="pass">Passwort</label>
<input id="pass" name="pass" type="password" autocomplete="current-password" required>
<input name="timeout" type="hidden" value="86400">
<button type="submit">Einloggen</button>
</form>
<p class="muted">Noch kein Konto? <a href="/auth/register">Registrieren</a></p>
</section>
{{end}}
+23
View File
@@ -0,0 +1,23 @@
{{define "content"}}
<section id="auth-box">
<form method="post" action="/auth/newuser">
<h2>Registrieren</h2>
<label for="user">Nutzername</label>
<input id="user" name="user" autocomplete="username" required minlength="3" maxlength="32"
{{if .Form}}value="{{.Form.User}}"{{end}}>
{{with fieldErr .Err "user"}}<p class="msg">{{.}}</p>{{end}}
<label for="pass1">Passwort <span class="muted">(mindestens 10 Zeichen)</span></label>
<input id="pass1" name="pass1" type="password" autocomplete="new-password" required minlength="10">
{{with fieldErr .Err "pass1"}}<p class="msg">{{.}}</p>{{end}}
<label for="pass2">Passwort wiederholen</label>
<input id="pass2" name="pass2" type="password" autocomplete="new-password" required minlength="10">
{{with fieldErr .Err "pass2"}}<p class="msg">{{.}}</p>{{end}}
<button type="submit">Konto erstellen</button>
</form>
<p class="muted">Schon registriert? <a href="/auth/login">Zum Login</a></p>
</section>
{{end}}
+15
View File
@@ -0,0 +1,15 @@
{{define "content"}}
{{with .Form}}
<section id="report-done">
<h2>Meldung eingegangen</h2>
<p>Danke – deine Meldung zu Beitrag
<a href="/beitrag/{{.PID}}">#{{.PID}}</a> ist eingegangen und wird geprüft.</p>
{{if .Reason}}
<p class="muted">Dein Grund: {{.Reason}}</p>
{{else}}
<p class="muted">Du hast keinen Grund angegeben.</p>
{{end}}
<p><a class="button" href="{{.BackURL}}">Zurück zum Beitrag</a></p>
</section>
{{end}}
{{end}}
+41
View File
@@ -0,0 +1,41 @@
{{define "content"}}
{{with .Form}}
{{if .Ancestors}}
<section id="ancestors" class="muted">
{{range .Ancestors}}{{template "entry" .}}{{end}}
</section>
{{end}}
<section id="focus">
{{template "entry" .Entry}}
</section>
{{if .CanPost}}
<section id="compose">
<form method="post" action="/entry/create" enctype="multipart/form-data">
<input type="hidden" name="reply_to" value="{{.Entry.PID}}">
{{with $.Err}}<p class="msg">{{.Msg}}</p>{{end}}
<label for="content">Antworten</label>
<textarea id="content" name="content" rows="4" maxlength="1000"
placeholder="Deine Antwort">{{.Compose.Content}}</textarea>
<label for="file">Bild <span class="muted">(optional)</span></label>
<input id="file" name="file" type="file" accept="image/*">
{{if .Compose.HadFile}}
<p class="msg">Dein Bild musst du erneut auswählen – Browser können
Dateifelder aus Sicherheitsgründen nicht vorbelegen.</p>
{{end}}
<button type="submit">Antwort abschicken</button>
</form>
</section>
{{else if .IsTOR}}
<p class="muted">Über TOR ist das Schreiben von Beiträgen derzeit nicht
möglich. Lesen, Abstimmen und Melden funktionieren.</p>
{{end}}
<section id="replies">
{{range .Replies}}{{template "entry" .}}{{else}}
<p class="muted">Noch keine Antworten.</p>
{{end}}
</section>
{{end}}
{{end}}
+11
View File
@@ -0,0 +1,11 @@
{{define "content"}}
{{with .Form}}
<h2>Beiträge von {{.Username}}</h2>
<section id="feed">
{{range .Entries}}{{template "entry" .}}{{else}}
<p class="muted">Dieser Nutzer hat noch nichts geschrieben.</p>
{{end}}
</section>
{{template "pager" .}}
{{end}}
{{end}}