Umbau-Projekt (SSR und Rest)
This commit is contained in:
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
Reference in New Issue
Block a user