2025-04-02 23:04:09 +01:00
|
|
|
package view
|
|
|
|
|
|
|
|
|
|
import (
|
2025-04-02 23:49:20 +01:00
|
|
|
"fmt"
|
2025-04-02 23:04:09 +01:00
|
|
|
"html/template"
|
|
|
|
|
"net/http"
|
2025-04-02 23:49:20 +01:00
|
|
|
"os"
|
2025-06-24 01:32:30 +01:00
|
|
|
"slices"
|
2025-04-02 23:04:09 +01:00
|
|
|
|
2025-04-02 23:49:20 +01:00
|
|
|
"arimelody-web/controller"
|
2025-04-02 23:04:09 +01:00
|
|
|
"arimelody-web/model"
|
|
|
|
|
"arimelody-web/templates"
|
|
|
|
|
|
|
|
|
|
"github.com/gomarkdown/markdown"
|
|
|
|
|
"github.com/gomarkdown/markdown/html"
|
|
|
|
|
"github.com/gomarkdown/markdown/parser"
|
|
|
|
|
)
|
|
|
|
|
|
2025-06-24 01:32:30 +01:00
|
|
|
type (
|
2025-11-08 12:54:31 +00:00
|
|
|
blogView struct {
|
|
|
|
|
Collections []*blogPostCollection
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
blogPostCollection struct {
|
|
|
|
|
Year int
|
|
|
|
|
Posts []*model.BlogPost
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
blogPostView struct {
|
2025-06-24 01:32:30 +01:00
|
|
|
*model.BlogPost
|
2025-11-08 12:54:31 +00:00
|
|
|
BlogHTML template.HTML
|
2025-06-24 01:32:30 +01:00
|
|
|
Comments []*model.ThreadViewPost
|
|
|
|
|
Likes int
|
|
|
|
|
Boosts int
|
|
|
|
|
BlueskyURL string
|
|
|
|
|
MastodonURL string
|
|
|
|
|
}
|
|
|
|
|
)
|
2025-05-21 18:38:50 +01:00
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
func (c *blogPostCollection) Clone() blogPostCollection {
|
|
|
|
|
return blogPostCollection{
|
|
|
|
|
Year: c.Year,
|
|
|
|
|
Posts: slices.Clone(c.Posts),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 23:04:09 +01:00
|
|
|
var mdRenderer = html.NewRenderer(html.RendererOptions{
|
|
|
|
|
Flags: html.CommonFlags | html.HrefTargetBlank,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
func BlogHandler(app *model.AppState) http.Handler {
|
2025-11-07 17:48:06 +00:00
|
|
|
mux := http.NewServeMux()
|
2025-04-02 23:49:20 +01:00
|
|
|
|
2025-11-07 17:48:06 +00:00
|
|
|
mux.HandleFunc("/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
ServeBlogPost(app, r.PathValue("id")).ServeHTTP(w, r)
|
|
|
|
|
})
|
2025-06-24 01:32:30 +01:00
|
|
|
|
2025-11-07 17:48:06 +00:00
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
2025-11-08 12:54:31 +00:00
|
|
|
posts, err := controller.GetBlogPosts(app.DB, true, -1, 0)
|
2025-06-24 01:32:30 +01:00
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err)
|
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
collections := []*blogPostCollection{}
|
|
|
|
|
collection := blogPostCollection{
|
|
|
|
|
Posts: []*model.BlogPost{},
|
|
|
|
|
Year: -1,
|
|
|
|
|
}
|
|
|
|
|
for i, post := range posts {
|
2025-06-24 01:32:30 +01:00
|
|
|
if i == 0 {
|
2025-11-08 12:54:31 +00:00
|
|
|
collection.Year = post.PublishDate.Year()
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
if post.PublishDate.Year() != collection.Year {
|
|
|
|
|
clone := collection.Clone()
|
|
|
|
|
collections = append(collections, &clone)
|
|
|
|
|
collection = blogPostCollection{
|
|
|
|
|
Year: post.PublishDate.Year(),
|
|
|
|
|
Posts: []*model.BlogPost{},
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
collection.Posts = append(collection.Posts, post)
|
|
|
|
|
|
|
|
|
|
if i == len(posts) - 1 {
|
|
|
|
|
collections = append(collections, &collection)
|
|
|
|
|
}
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
2025-04-02 23:49:20 +01:00
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
err = templates.BlogTemplate.Execute(w, blogView{
|
2025-06-24 01:32:30 +01:00
|
|
|
Collections: collections,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err)
|
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-11-07 17:48:06 +00:00
|
|
|
|
|
|
|
|
return mux
|
2025-04-02 23:49:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-06-24 01:32:30 +01:00
|
|
|
func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
blog, err := controller.GetBlogPost(app.DB, blogPostID)
|
|
|
|
|
if err != nil {
|
2025-11-08 12:54:31 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post %s: %v\n", blogPostID, err)
|
2025-06-24 01:32:30 +01:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-11-08 12:54:31 +00:00
|
|
|
if blog == nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-06-24 01:32:30 +01:00
|
|
|
|
|
|
|
|
if !blog.Visible {
|
|
|
|
|
session, err := controller.GetSessionFromRequest(app, r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
|
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if session == nil || session.Account == nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 23:04:09 +01:00
|
|
|
// blog.Markdown += " <i class=\"end-mark\"></i>"
|
|
|
|
|
|
|
|
|
|
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs)
|
|
|
|
|
md := mdParser.Parse([]byte(blog.Markdown))
|
2025-11-08 12:54:31 +00:00
|
|
|
blogHTML := template.HTML(markdown.Render(md, mdRenderer))
|
2025-04-02 23:04:09 +01:00
|
|
|
|
2025-04-02 23:49:20 +01:00
|
|
|
comments := []*model.ThreadViewPost{}
|
2025-06-24 01:32:30 +01:00
|
|
|
likeCount := 0
|
|
|
|
|
boostCount := 0
|
|
|
|
|
var blueskyURL string
|
|
|
|
|
var blueskyPost *model.ThreadViewPost
|
2025-11-08 12:54:31 +00:00
|
|
|
if blog.Bluesky != nil {
|
2025-11-08 18:42:02 +00:00
|
|
|
blueskyPost, err = controller.FetchThreadViewPost(app, blog.Bluesky.ActorDID, blog.Bluesky.RecordID)
|
2025-06-24 01:32:30 +01:00
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
|
|
|
|
|
} else {
|
2025-11-08 18:42:02 +00:00
|
|
|
replies := []*model.ThreadViewPost{}
|
|
|
|
|
for _, reply := range blueskyPost.Replies {
|
|
|
|
|
if reply.Post.Author.DID != blog.Bluesky.ActorDID {
|
|
|
|
|
replies = append(replies, reply)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
comments = append(comments, replies...)
|
2025-06-24 01:32:30 +01:00
|
|
|
likeCount += blueskyPost.Post.LikeCount
|
|
|
|
|
boostCount += blueskyPost.Post.RepostCount
|
2025-11-08 12:54:31 +00:00
|
|
|
blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, blog.Bluesky.RecordID)
|
2025-06-24 01:32:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:54:31 +00:00
|
|
|
err = templates.BlogPostTemplate.Execute(w, blogPostView{
|
2025-06-24 01:32:30 +01:00
|
|
|
BlogPost: blog,
|
2025-11-08 12:54:31 +00:00
|
|
|
BlogHTML: blogHTML,
|
2025-06-24 01:32:30 +01:00
|
|
|
Comments: comments,
|
|
|
|
|
Likes: likeCount,
|
|
|
|
|
Boosts: boostCount,
|
|
|
|
|
BlueskyURL: blueskyURL,
|
2025-04-02 23:49:20 +01:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
2025-06-24 01:32:30 +01:00
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err)
|
2025-04-02 23:49:20 +01:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-04-02 23:04:09 +01:00
|
|
|
})
|
|
|
|
|
}
|