huge blog refactor

tidying up data structures; improvements to blog admin UI/UX, etc.
This commit is contained in:
ari melody 2025-11-08 12:54:31 +00:00
parent eaa2f6587d
commit 0c2aaa0b38
Signed by: ari
GPG key ID: CF99829C92678188
18 changed files with 432 additions and 239 deletions

View file

@ -23,17 +23,19 @@ func Handler(app *model.AppState) http.Handler {
}
type (
blogPost struct {
*model.BlogPost
Author *model.Account
}
blogPostGroup struct {
blogPostCollection struct {
Year int
Posts []*blogPost
Posts []*model.BlogPost
}
)
func (c *blogPostCollection) Clone() blogPostCollection {
return blogPostCollection{
Year: c.Year,
Posts: slices.Clone(c.Posts),
}
}
func serveBlogIndex(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session)
@ -45,44 +47,36 @@ func serveBlogIndex(app *model.AppState) http.Handler {
return
}
collections := []*blogPostGroup{}
collectionPosts := []*blogPost{}
collectionYear := -1
collections := []*blogPostCollection{}
collection := blogPostCollection{
Posts: []*model.BlogPost{},
Year: -1,
}
for i, post := range posts {
author, err := controller.GetAccountByID(app.DB, post.AuthorID)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err)
continue
if i == 0 {
collection.Year = post.PublishDate.Year()
}
if collectionYear == -1 {
collectionYear = post.CreatedAt.Year()
}
authoredPost := blogPost{
BlogPost: post,
Author: author,
}
if post.CreatedAt.Year() != collectionYear || i == len(posts) - 1 {
if i == len(posts) - 1 {
collectionPosts = append([]*blogPost{&authoredPost}, collectionPosts...)
if post.PublishDate.Year() != collection.Year {
clone := collection.Clone()
collections = append(collections, &clone)
collection = blogPostCollection{
Year: post.PublishDate.Year(),
Posts: []*model.BlogPost{},
}
collections = append(collections, &blogPostGroup{
Year: collectionYear,
Posts: slices.Clone(collectionPosts),
})
collectionPosts = []*blogPost{}
collectionYear = post.CreatedAt.Year()
}
collectionPosts = append(collectionPosts, &authoredPost)
collection.Posts = append(collection.Posts, post)
if i == len(posts) - 1 {
collections = append(collections, &collection)
}
}
type blogsData struct {
core.AdminPageData
TotalPosts int
Collections []*blogPostGroup
Collections []*blogPostCollection
}
err = templates.BlogsTemplate.Execute(w, blogsData{
@ -108,7 +102,11 @@ func serveBlogPost(app *model.AppState) http.Handler {
post, err := controller.GetBlogPost(app.DB, blogID)
if err != nil {
fmt.Printf("can't find blog with ID %s\n", blogID)
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog %s: %v\n", blogID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if post == nil {
http.NotFound(w, r)
return
}

View file

@ -109,19 +109,8 @@ func adminIndexHandler(app *model.AppState) http.Handler {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
var latestBlogPost *BlogPost = nil
if len(blogPosts) > 0 {
author, err := controller.GetAccountByID(app.DB, blogPosts[0].AuthorID)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull latest blog post author %s: %v\n", blogPosts[0].AuthorID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
latestBlogPost = &BlogPost{
BlogPost: blogPosts[0],
Author: author,
}
}
var latestBlogPost *model.BlogPost = nil
if len(blogPosts) > 0 { latestBlogPost = blogPosts[0] }
blogCount, err := controller.GetBlogPostCount(app.DB, false)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull blog post count: %v\n", err)
@ -137,7 +126,7 @@ func adminIndexHandler(app *model.AppState) http.Handler {
ArtistCount int
Tracks []*model.Track
TrackCount int
BlogPost *BlogPost
LatestBlogPost *model.BlogPost
BlogCount int
}
@ -149,7 +138,7 @@ func adminIndexHandler(app *model.AppState) http.Handler {
ArtistCount: artistCount,
Tracks: tracks,
TrackCount: trackCount,
BlogPost: latestBlogPost,
LatestBlogPost: latestBlogPost,
BlogCount: blogCount,
})
if err != nil {

View file

@ -1,4 +1,5 @@
.blog-collection {
margin-bottom: 1em;
display: flex;
flex-direction: column;
gap: .5em;

View file

@ -5,7 +5,7 @@ input[type="text"] {
border: none;
border-radius: 4px;
outline: none;
color: inherit;
color: var(--fg-3);
background-color: var(--bg-1);
box-shadow: var(--shadow-sm);
}
@ -67,6 +67,17 @@ input[type="text"] {
background-color: var(--bg-3);
}
#blogpost #publish-date {
padding: .4em .5em;
font-family: inherit;
font-size: inherit;
border-radius: 4px;
border: none;
background-color: var(--bg-1);
color: var(--fg-3);
box-shadow: var(--shadow-sm);
}
#blogpost textarea {
width: calc(100% - 2em);
margin: 0;
@ -95,6 +106,12 @@ input[type="text"] {
box-shadow: var(--shadow-sm);
}
#blogpost .social-post-details {
margin: 1em 0 1em 0;
display: flex;
gap: 1em;
}
#blogpost .blog-actions {
margin-top: 1em;
}

View file

@ -1,9 +1,12 @@
const blogID = document.getElementById("blogpost").dataset.id;
const titleInput = document.getElementById("title");
const publishDateInput = document.getElementById("publish-date");
const descInput = document.getElementById("description");
const mdInput = document.getElementById("markdown");
const blueskyActorInput = document.getElementById("bluesky-actor");
const blueskyPostInput = document.getElementById("bluesky-post");
const blueskyRecordInput = document.getElementById("bluesky-record");
const fediverseAccountInput = document.getElementById("fediverse-account");
const fediverseStatusInput = document.getElementById("fediverse-status");
const visInput = document.getElementById("visibility");
const saveBtn = document.getElementById("save");
const deleteBtn = document.getElementById("delete");
@ -12,11 +15,18 @@ saveBtn.addEventListener("click", () => {
fetch("/api/v1/blog/" + blogID, {
method: "PUT",
body: JSON.stringify({
title: titleInput.value,
title: titleInput.innerText,
publish_date: publishDateInput.value + ":00Z",
description: descInput.value,
markdown: mdInput.value,
bluesky_actor: blueskyActorInput.value,
bluesky_post: blueskyPostInput.value,
bluesky: {
actor: blueskyActorInput.value,
record: blueskyRecordInput.value,
},
fediverse: {
account: fediverseAccountInput.value,
status: fediverseStatusInput.value,
},
visible: visInput.value === "true",
}),
headers: { "Content-Type": "application/json" }
@ -53,11 +63,13 @@ deleteBtn.addEventListener("click", () => {
});
});
[titleInput, descInput, mdInput, blueskyActorInput, blueskyPostInput, visInput].forEach(input => {
input.addEventListener("change", () => {
saveBtn.disabled = false;
[titleInput, publishDateInput, descInput, mdInput, visInput,
blueskyActorInput, blueskyRecordInput,
fediverseAccountInput, fediverseStatusInput].forEach(input => {
input.addEventListener("change", () => {
saveBtn.disabled = false;
});
input.addEventListener("keypress", () => {
saveBtn.disabled = false;
});
});
input.addEventListener("keypress", () => {
saveBtn.disabled = false;
});
});

View file

@ -2,7 +2,7 @@
<div class="blogpost">
<h3 class="title"><a href="/admin/blogs/{{.ID}}">{{.Title}}</a>{{if not .Visible}} <small>(Not published)</small>{{end}}</h3>
<p class="meta">
<span class="author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
<span class="author"><img src="/img/favicon.png" alt="" width="32" height="32"/> {{.Author.DisplayName}}</span>
<span class="date">&bull; {{.PrintDate}}</span>
</p>
<p class="description">{{.Description}}</p>

View file

@ -26,6 +26,9 @@
>{{.Post.Title}}</div>
</h2>
<label for="publish-date">Publish Date</label>
<input type="datetime-local" name="publish-date" id="publish-date" value="{{.Post.TextPublishDate}}">
<label for="description">Description</label>
<textarea
id="description"
@ -43,20 +46,47 @@
rows="30"
>{{.Post.Markdown}}</textarea>
<label for="bluesky-actor">Bluesky Author DID</label>
<input
type="text"
name="bluesky-actor"
id="bluesky-actor"
placeholder="did:plc:1234abcd..."
value="{{if .Post.BlueskyActorID}}{{.Post.BlueskyActorID}}{{end}}">
<label for="bluesky-post">Bluesky Post ID</label>
<input
type="text"
name="bluesky-post"
id="bluesky-post"
placeholder="3m109a03..."
value="{{if .Post.BlueskyPostID}}{{.Post.BlueskyPostID}}{{end}}">
<div class="social-post-details">
<div class="social-post-item">
<label for="bluesky-actor">Bluesky Author DID</label>
<input
type="text"
name="bluesky-actor"
id="bluesky-actor"
placeholder="did:plc:1234abcd..."
value="{{if .Post.Bluesky}}{{.Post.Bluesky.ActorDID}}{{end}}">
</div>
<div class="social-post-item">
<label for="bluesky-record">Bluesky Post ID</label>
<input
type="text"
name="bluesky-record"
id="bluesky-record"
placeholder="3m109a03..."
value="{{if .Post.Bluesky}}{{.Post.Bluesky.RecordID}}{{end}}">
</div>
</div>
<div class="social-post-details">
<div class="social-post-item">
<label for="fediverse-account">Fediverse Account</label>
<input
type="text"
name="fediverse-account"
id="fediverse-account"
placeholder="@me@my.fediverse.place"
value="{{if .Post.Fediverse}}{{.Post.Fediverse.AccountID}}{{end}}">
</div>
<div class="social-post-item">
<label for="fediverse-status">Fediverse Status ID</label>
<input
type="text"
name="fediverse-status"
id="fediverse-status"
placeholder="never consistent ¯\_(ツ)_/¯"
value="{{if .Post.Fediverse}}{{.Post.Fediverse.StatusID}}{{end}}">
</div>
</div>
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility">

View file

@ -59,8 +59,8 @@
<h2><a href="/admin/blogs/">Latest Blog Post</a> <small>({{.BlogCount}} total)</small></h2>
<a class="button new" id="create-post">Create New</a>
</div>
{{if .BlogPost}}
{{block "blogpost" .BlogPost}}{{end}}
{{if .LatestBlogPost}}
{{block "blogpost" .LatestBlogPost}}{{end}}
{{else}}
<p>There are no blog posts.</p>
{{end}}