Add nested posts UI cutover.
CI / test (pull_request) Successful in 8m23s

Render reply trees with permission-aware inline controls, and move hunt, profile, voting, submission, and hiding flows to post storage.
This commit is contained in:
2026-08-27 07:40:14 -07:00
parent 4d994d5300
commit d3ce29031f
22 changed files with 1033 additions and 351 deletions
-28
View File
@@ -1,28 +0,0 @@
{{define "answer"}}
<section id="answer-block" class="answer{{if .Answer}} is-in{{end}}">
{{if .Answer}}
<p class="answer-kicker">Shop response</p>
<h2>Answer</h2>
<p class="byline">{{.Answer.AuthorName}} · 22 years, Bay Area</p>
<p class="answer-body">{{.Answer.Body}}</p>
{{if isAdmin .User}}
<details class="answer-editor">
<summary>Edit answer</summary>
<form class="answer-form" method="post" action="/questions/{{.Answer.QuestionID}}/answer"
hx-post="/questions/{{.Answer.QuestionID}}/answer" hx-target="#answer-block" hx-swap="outerHTML">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<label for="answer-body">Edit answer</label>
<textarea id="answer-body" name="body" rows="8" required maxlength="12000">{{.Answer.Body}}</textarea>
<div class="answer-form-actions">
<button type="submit" class="btn btn-primary">Save answer</button>
<button type="reset" class="btn btn-ghost"
onclick="this.closest('details').removeAttribute('open')">Cancel</button>
</div>
</form>
</details>
{{end}}
{{else}}
<p class="waiting">No answer yet. Check back after the hunt.</p>
{{end}}
</section>
{{end}}
+9 -9
View File
@@ -1,6 +1,6 @@
{{define "leaderboard"}}
<ol id="leaderboard" class="board" start="1">
{{if not .Questions}}
{{if not .Posts}}
<li class="empty">
{{if eq .Date .Today}}
<p class="empty-kicker">Queue empty</p>
@@ -10,20 +10,20 @@
{{end}}
</li>
{{else}}
{{range $i, $q := .Questions}}
{{range $i, $post := .Posts}}
<li class="row">
<span class="rank" aria-hidden="true">{{rank $i}}</span>
{{template "vote" (voteCtx $.User $.CSRF "list" $.Date $q)}}
{{template "vote" (voteCtx $.User $.CSRF "list" $.Date $post)}}
<div class="row-body">
<a class="q-title" href="/questions/{{$q.ID}}">{{$q.Title}}</a>
<a class="q-title" href="/questions/{{$post.ID}}">{{$post.Title}}</a>
<p class="meta">
<span>{{$q.AuthorName}}</span>
{{if $q.City}}<span class="dot" aria-hidden="true">·</span><span>{{$q.City}}</span>{{end}}
{{if $q.Answered}}<span class="badge">Answered</span>{{end}}
<span>{{$post.AuthorName}}</span>
{{if $post.City}}<span class="dot" aria-hidden="true">·</span><span>{{$post.City}}</span>{{end}}
{{if $post.Answered}}<span class="badge">Answered</span>{{end}}
</p>
{{if isAdmin $.User}}
<form class="inline-hide" method="post" action="/questions/{{$q.ID}}/hide"
hx-post="/questions/{{$q.ID}}/hide" hx-target="#leaderboard" hx-swap="outerHTML">
<form class="inline-hide" method="post" action="/questions/{{$post.ID}}/hide"
hx-post="/questions/{{$post.ID}}/hide" hx-target="#leaderboard" hx-swap="outerHTML">
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
<input type="hidden" name="view" value="list">
<button type="submit" class="linkish">Hide</button>
+67
View File
@@ -0,0 +1,67 @@
{{define "postActions"}}
<div class="post-actions">
{{if canReply .User .Root}}
<details class="post-composer">
<summary>Reply</summary>
<form class="post-form" method="post" action="/posts">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<input type="hidden" name="parent_id" value="{{.Post.ID}}">
<label for="reply-{{.Post.ID}}">Reply to {{.Post.AuthorName}}</label>
<textarea id="reply-{{.Post.ID}}" name="body" rows="5" required maxlength="12000"></textarea>
<div class="post-form-actions">
<button type="submit" class="btn btn-primary">Post reply</button>
<button type="reset" class="btn btn-ghost"
onclick="this.closest('details').removeAttribute('open')">Cancel</button>
</div>
</form>
</details>
{{end}}
{{if canEditPost .User .Post}}
<details class="post-composer">
<summary>Edit</summary>
<form class="post-form" method="post" action="/posts/{{.Post.ID}}/edit">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<label for="edit-{{.Post.ID}}">Edit post</label>
<textarea id="edit-{{.Post.ID}}" name="body" rows="5" required
maxlength="12000">{{.Post.Body}}</textarea>
<div class="post-form-actions">
<button type="submit" class="btn btn-primary">Save changes</button>
<button type="reset" class="btn btn-ghost"
onclick="this.closest('details').removeAttribute('open')">Cancel</button>
</div>
</form>
</details>
{{end}}
{{if and (not .Post.ParentID) (isAdmin .User)}}
<form class="post-hide" method="post" action="/questions/{{.Post.ID}}/hide">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="linkish">Hide</button>
</form>
{{end}}
</div>
{{end}}
{{define "threadReply"}}
{{$ctx := .}}
<article id="post-{{.Post.ID}}"
class="thread-post thread-post-{{postDepth .Depth}}{{if eq .Post.AuthorRole "admin"}} is-shop{{end}}">
<header class="post-head">
<p class="post-kicker">{{postLabel .Post}}</p>
<p class="post-meta">
<span>{{.Post.AuthorName}}</span>
<span class="dot" aria-hidden="true">·</span>
<time datetime="{{.Post.CreatedAt}}">{{postTime .Post.CreatedAt}}</time>
{{if isEdited .Post}}<span class="edited">Edited</span>{{end}}
</p>
</header>
<p class="post-body">{{.Post.Body}}</p>
{{template "postActions" .}}
{{if .Post.Replies}}
<div class="post-replies">
{{range .Post.Replies}}
{{template "threadReply" (postCtx $ctx.User $ctx.CSRF $ctx.Root . (add $ctx.Depth 1))}}
{{end}}
</div>
{{end}}
</article>
{{end}}
+13 -13
View File
@@ -1,26 +1,26 @@
{{define "vote"}}
<div id="vote-{{.Question.ID}}" class="vote">
<div id="vote-{{.Post.ID}}" class="vote">
{{if .User}}
<form method="post" action="/questions/{{.Question.ID}}/vote"
hx-post="/questions/{{.Question.ID}}/vote"
{{if eq .View "list"}}hx-target="#leaderboard" hx-swap="outerHTML"{{else}}hx-target="#vote-{{.Question.ID}}" hx-swap="outerHTML"{{end}}>
<form method="post" action="/questions/{{.Post.ID}}/vote"
hx-post="/questions/{{.Post.ID}}/vote"
{{if eq .View "list"}}hx-target="#leaderboard" hx-swap="outerHTML"{{else}}hx-target="#vote-{{.Post.ID}}" hx-swap="outerHTML"{{end}}>
<input type="hidden" name="_csrf" value="{{.CSRF}}">
{{if eq .Question.UserVote 1}}<input type="hidden" name="value" value="0">{{else}}<input type="hidden" name="value" value="1">{{end}}
{{if eq .Post.UserVote 1}}<input type="hidden" name="value" value="0">{{else}}<input type="hidden" name="value" value="1">{{end}}
<input type="hidden" name="view" value="{{.View}}">
<input type="hidden" name="date" value="{{.Date}}">
<button type="submit" class="vote-btn{{if eq .Question.UserVote 1}} is-up{{end}}" aria-label="Upvote" aria-pressed="{{if eq .Question.UserVote 1}}true{{else}}false{{end}}">
<button type="submit" class="vote-btn{{if eq .Post.UserVote 1}} is-up{{end}}" aria-label="Upvote" aria-pressed="{{if eq .Post.UserVote 1}}true{{else}}false{{end}}">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M9 3.5 15 12H3z" fill="currentColor"/></svg>
</button>
</form>
<span class="score" aria-label="Net score {{.Question.Score}}">{{.Question.Score}}</span>
<form method="post" action="/questions/{{.Question.ID}}/vote"
hx-post="/questions/{{.Question.ID}}/vote"
{{if eq .View "list"}}hx-target="#leaderboard" hx-swap="outerHTML"{{else}}hx-target="#vote-{{.Question.ID}}" hx-swap="outerHTML"{{end}}>
<span class="score" aria-label="Net score {{.Post.Score}}">{{.Post.Score}}</span>
<form method="post" action="/questions/{{.Post.ID}}/vote"
hx-post="/questions/{{.Post.ID}}/vote"
{{if eq .View "list"}}hx-target="#leaderboard" hx-swap="outerHTML"{{else}}hx-target="#vote-{{.Post.ID}}" hx-swap="outerHTML"{{end}}>
<input type="hidden" name="_csrf" value="{{.CSRF}}">
{{if eq .Question.UserVote -1}}<input type="hidden" name="value" value="0">{{else}}<input type="hidden" name="value" value="-1">{{end}}
{{if eq .Post.UserVote -1}}<input type="hidden" name="value" value="0">{{else}}<input type="hidden" name="value" value="-1">{{end}}
<input type="hidden" name="view" value="{{.View}}">
<input type="hidden" name="date" value="{{.Date}}">
<button type="submit" class="vote-btn{{if eq .Question.UserVote -1}} is-down{{end}}" aria-label="Downvote" aria-pressed="{{if eq .Question.UserVote -1}}true{{else}}false{{end}}">
<button type="submit" class="vote-btn{{if eq .Post.UserVote -1}} is-down{{end}}" aria-label="Downvote" aria-pressed="{{if eq .Post.UserVote -1}}true{{else}}false{{end}}">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M9 14.5 3 6h12z" fill="currentColor"/></svg>
</button>
</form>
@@ -28,7 +28,7 @@
<a class="vote-btn" href="/login" hx-get="/auth/prompt" hx-target="#flash" aria-label="Sign in to upvote">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M9 3.5 15 12H3z" fill="currentColor"/></svg>
</a>
<span class="score" aria-label="Net score {{.Question.Score}}">{{.Question.Score}}</span>
<span class="score" aria-label="Net score {{.Post.Score}}">{{.Post.Score}}</span>
<a class="vote-btn" href="/login" hx-get="/auth/prompt" hx-target="#flash" aria-label="Sign in to downvote">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M9 14.5 3 6h12z" fill="currentColor"/></svg>
</a>
+3 -3
View File
@@ -43,12 +43,12 @@
<section class="profile-questions" aria-labelledby="profile-q-heading">
<h2 id="profile-q-heading">{{.QuestionsLabel}}</h2>
{{if .Questions}}
{{if .Posts}}
<ul class="profile-q-list">
{{range .Questions}}
{{range .Posts}}
<li>
<a href="/questions/{{.ID}}">{{.Title}}</a>
<span class="meta">{{.HuntDate}}</span>
<span class="meta">{{.PostDate}}</span>
</li>
{{end}}
</ul>
+27 -23
View File
@@ -1,37 +1,41 @@
{{define "question"}}
{{template "header" .}}
<main id="main" class="wrap question-page">
<p class="crumb"><a href="{{if eq .Question.HuntDate .Today}}/{{else}}/hunt/{{.Question.HuntDate}}{{end}}">← {{pacificLabel .Question.HuntDate}}</a></p>
<article class="q-detail">
{{template "vote" (voteCtx .User .CSRF "question" .Question.HuntDate .Question)}}
<div>
<p class="crumb"><a href="{{if eq .Question.PostDate .Today}}/{{else}}/hunt/{{.Question.PostDate}}{{end}}">← {{pacificLabel .Question.PostDate}}</a></p>
<article id="post-{{.Question.ID}}" class="q-detail">
{{template "vote" (voteCtx .User .CSRF "question" .Question.PostDate .Question)}}
<div class="post-content">
<p class="post-kicker">Question</p>
<h1>{{.Question.Title}}</h1>
<p class="meta">
<span>{{.Question.AuthorName}}</span>
{{if .Question.City}}<span class="dot" aria-hidden="true">·</span><span>{{.Question.City}}</span>{{end}}
<span class="dot" aria-hidden="true">·</span>
<a href="{{if eq .Question.HuntDate .Today}}/{{else}}/hunt/{{.Question.HuntDate}}{{end}}">{{.Question.HuntDate}}</a>
<a href="{{if eq .Question.PostDate .Today}}/{{else}}/hunt/{{.Question.PostDate}}{{end}}">{{.Question.PostDate}}</a>
{{if eq .Question.PostState "locked"}}<span class="badge">Locked</span>{{end}}
{{if eq .Question.PostState "hidden"}}<span class="badge">Hidden</span>{{end}}
{{if isEdited .Question}}<span class="edited">Edited</span>{{end}}
</p>
<p class="q-body">{{.Question.Body}}</p>
{{if isAdmin .User}}
<form method="post" action="/questions/{{.Question.ID}}/hide"
hx-post="/questions/{{.Question.ID}}/hide" hx-target="body">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="linkish">Hide this question</button>
</form>
{{end}}
<p class="post-body">{{.Question.Body}}</p>
{{template "postActions" (postCtx .User .CSRF .Question .Question 0)}}
</div>
</article>
{{template "answer" .}}
{{if and (isAdmin .User) (not .Answer)}}
<form class="answer-form" method="post" action="/questions/{{.Question.ID}}/answer"
hx-post="/questions/{{.Question.ID}}/answer" hx-target="#answer-block" hx-swap="outerHTML">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<label for="answer-body">Write the answer</label>
<textarea id="answer-body" name="body" rows="8" required maxlength="12000"></textarea>
<button type="submit" class="btn btn-primary">Save answer</button>
</form>
{{end}}
<section class="conversation" aria-labelledby="conversation-heading">
<div class="conversation-head">
<p class="eyebrow">Thread</p>
<h2 id="conversation-heading">Conversation</h2>
</div>
{{if .Question.Replies}}
<div class="thread">
{{$page := .}}
{{range .Question.Replies}}
{{template "threadReply" (postCtx $page.User $page.CSRF $page.Question . 1)}}
{{end}}
</div>
{{else}}
<p class="waiting">No replies yet.</p>
{{end}}
</section>
</main>
{{template "footer" .}}
{{end}}