style improvements and bluesky comments!

This commit is contained in:
ari melody 2025-04-02 23:49:20 +01:00
parent 1a8dc4d9ce
commit 835dd344ca
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
7 changed files with 393 additions and 13 deletions

48
controller/bluesky.go Normal file
View file

@ -0,0 +1,48 @@
package controller
import (
"arimelody-web/model"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
const BSKY_API_BASE = "https://public.api.bsky.app"
func FetchThreadViewPost(actorID string, postID string) (*model.ThreadViewPost, error) {
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", actorID, postID)
req, err := http.NewRequest(
http.MethodGet,
strings.Join([]string{BSKY_API_BASE, "xrpc", "app.bsky.feed.getPostThread"}, "/"),
nil,
)
if err != nil { panic(err) }
req.URL.RawQuery = url.Values{
"uri": { uri },
}.Encode()
req.Header.Set("User-Agent", "ari melody [https://arimelody.me]")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return nil, errors.New(fmt.Sprintf("Failed to call Bluesky API: %v", err))
}
type Data struct {
Thread model.ThreadViewPost `json:"thread"`
}
data := Data{}
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
return nil, errors.New(fmt.Sprintf("Invalid response from server: %v", err))
}
return &data.Thread, nil
}

View file

@ -17,6 +17,8 @@ type (
AuthorID string `db:"author"`
Markdown string `db:"markdown"`
HTML template.HTML `db:"html"`
BlueskyActorID string `db:"bsky_actor"`
BlueskyPostID string `db:"bsky_post"`
}
)

82
model/bluesky.go Normal file
View file

@ -0,0 +1,82 @@
package model
import (
"fmt"
"strings"
"time"
)
type (
Record struct {
Type string `json:"$type"`
CreatedAt string `json:"createdAt"`
Text string `json:"text"`
}
Profile struct {
DID string `json:"did"`
Handle string `json:"handle"`
Avatar string `json:"avatar"`
DisplayName string `json:"displayName"`
CreatedAt string `json:"createdAt"`
}
PostImage struct {
Thumbnail string `json:"thumb"`
Fullsize string `json:"fullsize"`
Alt string `json:"alt"`
}
EmbedMedia struct {
Images []PostImage `json:"images"`
}
Embed struct {
Media EmbedMedia `json:"media"`
}
Post struct {
Author Profile `json:"author"`
Record Record `json:"record"`
ReplyCount int `json:"replyCount"`
RepostCount int `json:"repostCount"`
LikeCount int `json:"likeCount"`
QuoteCount int `json:"quoteCount"`
Embed *Embed `json:"embed"`
URI string `json:"uri"`
}
ThreadViewPost struct {
Post Post `json:"post"`
Replies []*ThreadViewPost `json:"replies"`
}
)
func (record *Record) CreatedAtPrint() (string, error) {
t, err := record.CreatedAtTime()
if err != nil { return "", err }
return t.Format("15:04, 2 February 2006"), nil
}
func (record *Record) CreatedAtTime() (time.Time, error) {
return time.Parse("2006-01-02T15:04:05Z", record.CreatedAt)
}
func (post *Post) HasImage() bool {
return post.Embed != nil && len(post.Embed.Media.Images) > 0
}
func (post *Post) PostID() string {
return strings.TrimPrefix(
post.URI,
fmt.Sprintf("at://%s/app.bsky.feed.post/", post.Author.DID),
)
}
func (post *Post) BskyURL() string {
return fmt.Sprintf(
"https://bsky.app/profile/%s/post/%s",
post.Author.DID,
post.PostID(),
)
}

View file

@ -1,7 +1,20 @@
:root {
--like: rgb(223, 104, 104);
--repost: rgb(162, 223, 73);
--bluesky: rgb(16, 131, 254);
--mastodon: rgb(86, 58, 204);
}
main {
width: min(calc(100% - 4rem), 900px);
margin: 0 auto 1rem auto;
}
.blog p:hover,
.comment p:hover {
background: inherit;
}
article.blog {
font-family: 'Lora', serif;
}
@ -15,16 +28,29 @@ article.blog {
line-height: 1.5em;
}
.blog p:hover {
background: inherit;
}
.blog sub {
opacity: .75;
}
.blog pre {
max-height: 15em;
padding: .5em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
overflow: scroll;
background: #060606;
}
.blog p code {
padding: .2em .3em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
background: #060606;
}
.blog img {
margin: 0 auto;
max-height: 50%;
max-width: 100%;
@ -44,3 +70,124 @@ article.blog {
background-repeat: no-repeat;
background-position: center;
}
/* COMMENTS */
.interactions {
margin: 1em 0;
display: flex;
flex-direction: row;
gap: .5em;
flex-wrap: wrap;
}
.btn {
display: inline-block;
padding: .4em .6em;
border: 1px solid var(--on-background);
border-radius: 2px;
color: inherit;
text-decoration: none;
font-weight: 600;
}
.interactions .likes,
.interactions .reposts {
padding: 0 .5em;
min-width: fit-content;
font-family: monospace;
text-align: center;
line-height: 2em;
border: 1px solid var(--on-background);
border-radius: 2px;
text-wrap: nowrap;
}
.interactions .likes {
border-color: var(--on-background);
}
.interactions .reposts {
border-color: var(--on-background);
}
.btn.bluesky,
.btn.mastodon {
font-family: monospace;
text-wrap: nowrap;
}
.btn.bluesky {
color: var(--bluesky);
border-color: var(--bluesky);
}
.btn.mastodon {
color: var(--mastodon);
border-color: var(--mastodon);
}
.comment {
font-family: 'Inter', 'Arial', sans-serif;
font-size: 1em;
}
.comment .comment-hover {
padding: 1em;
transition: background-color .1s;
}
.comment .comment-hover:hover {
background-color: #8881;
}
.comment .comment-header a {
display: flex;
gap: .5em;
font-weight: 600;
color: var(--primary);
text-decoration: none;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
}
.comment .comment-header a .display-name {
overflow: inherit;
text-overflow: inherit;
}
.comment .comment-header a .handle {
opacity: .5;
font-family: monospace;
font-size: .9em;
overflow: inherit;
text-overflow: inherit;
}
.comment .comment-header img.avatar {
width: 1.5em;
height: 1.5em;
}
.comment .comment-body {
color: inherit;
text-decoration: none;
}
.comment p.comment-text {
margin: .5em 0;
white-space: break-spaces;
}
.comment .comment-footer .comment-date {
margin: 0;
font-size: .8em;
opacity: .5;
}
.comment .comment-replies {
margin-left: 1em;
border-left: 2px solid #8884;
}

View file

@ -1,10 +1,13 @@
package view
import (
"fmt"
"html/template"
"net/http"
"os"
"time"
"arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/templates"
@ -20,16 +23,14 @@ var mdRenderer = html.NewRenderer(html.RendererOptions{
func BlogHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
blog := model.Blog{
Title: "hello world!~",
Title: "hello world!",
Description: "lorem ipsum yadda yadda something boobies babababababababa",
Visible: true,
Date: time.Now(),
AuthorID: "ari",
Markdown:
`
# hello, world!
i'm ari!
**i'm ari!**
she/her 🏳🏳🌈💫🦆🇮🇪
@ -72,7 +73,26 @@ thank you for stopping by- i hope you have a lovely rest of your day! 💫
- impact meme
- OpenTerminal
- Silver.js
### code block test
~~~ c
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("hello world!~\n");
return 0;
}
~~~
### aridoodle
this is `+"`"+`aridoodle`+"`"+`. please take care of her.
![aridoodle](/img/aridoodle.png)
`,
BlueskyActorID: "did:plc:yct6cvgfipngizry5umzkxr3",
BlueskyPostID: "3llsudsx7pc2u",
}
// blog.Markdown += " <i class=\"end-mark\"></i>"
@ -81,6 +101,35 @@ thank you for stopping by- i hope you have a lovely rest of your day! 💫
md := mdParser.Parse([]byte(blog.Markdown))
blog.HTML = template.HTML(markdown.Render(md, mdRenderer))
templates.BlogTemplate.Execute(w, &blog)
comments := []*model.ThreadViewPost{}
blueskyPost, err := controller.FetchThreadViewPost(blog.BlueskyActorID, blog.BlueskyPostID)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
} else {
comments = append(comments, blueskyPost.Replies...)
}
type BlogView struct {
*model.Blog
Comments []*model.ThreadViewPost
Likes int
Reposts int
BlueskyURL string
MastodonURL string
}
err = templates.BlogTemplate.Execute(w, BlogView{
Blog: &blog,
Comments: blueskyPost.Replies,
Likes: 10,
Reposts: 10,
BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID),
MastodonURL: "#",
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error rendering blog post: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}

View file

@ -24,11 +24,63 @@
<hr>
{{.HTML}}
<hr>
<!-- comments section here! -->
</article>
<hr>
<div class="interactions">
<span class="likes">❤️ {{.Likes}}</span>
<span class="reposts">🔁 {{.Reposts}}</span>
<a class="btn bluesky" href="{{.BlueskyURL}}" target="_blank">Bluesky 🦋</a>
<!-- TODO: mastodon support -->
<a class="btn mastodon" href="{{.MastodonURL}}" target="_blank">Mastodon 🐘</a>
</div>
<div class="comments">
{{range .Comments}}
{{template "comment" .}}
{{end}}
</div>
<script type="module" src="/script/blog.js"></script>
</main>
{{end}}
{{define "comment"}}
<article class="comment">
<div class="comment-hover">
<div class="comment-header">
<a href="https://bsky.app/profile/{{.Post.Author.DID}}" target="_blank">
<img class="avatar" src="{{.Post.Author.Avatar}}" alt="{{.Post.Author.DisplayName}}'s avatar">
<span class="display-name">{{.Post.Author.DisplayName}}</span>
<span class="handle">@{{.Post.Author.Handle}}</span>
</a>
</div>
<a class="comment-body" href="{{.Post.BskyURL}}" target="_blank">
<div>
<p class="comment-text">{{.Post.Record.Text}}</p>
{{if .Post.HasImage}}
<p class="comment-images">
{{range .Post.Embed.Media.Images}}
<a href="{{.Fullsize}}" target="_blank">[image]</a>
{{end}}
</p>
{{end}}
</div>
<div class="comment-footer">
<!-- <span>{{.Post.LikeCount}} likes</span> -->
<!-- <span>{{.Post.RepostCount}} reposts</span> -->
<span class="comment-date">{{.Post.Record.CreatedAtPrint}}</span>
</div>
</a>
</div>
<div class="comment-replies">
{{range .Replies}}
{{template "comment" .}}
{{end}}
</div>
</article>
{{end}}

View file

@ -89,7 +89,7 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
err := templates.MusicGatewayTemplate.Execute(w, response)
if err != nil {
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
fmt.Fprintf(os.Stderr, "Error rendering music gateway for %s: %v\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}