style improvements and bluesky comments!
This commit is contained in:
parent
1a8dc4d9ce
commit
835dd344ca
7 changed files with 393 additions and 13 deletions
48
controller/bluesky.go
Normal file
48
controller/bluesky.go
Normal 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
|
||||
}
|
|
@ -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
82
model/bluesky.go
Normal 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(),
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
59
view/blog.go
59
view/blog.go
|
@ -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.
|
||||
|
||||

|
||||
`,
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue