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"`
|
AuthorID string `db:"author"`
|
||||||
Markdown string `db:"markdown"`
|
Markdown string `db:"markdown"`
|
||||||
HTML template.HTML `db:"html"`
|
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 {
|
main {
|
||||||
|
width: min(calc(100% - 4rem), 900px);
|
||||||
margin: 0 auto 1rem auto;
|
margin: 0 auto 1rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blog p:hover,
|
||||||
|
.comment p:hover {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
article.blog {
|
article.blog {
|
||||||
font-family: 'Lora', serif;
|
font-family: 'Lora', serif;
|
||||||
}
|
}
|
||||||
|
@ -15,16 +28,29 @@ article.blog {
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog p:hover {
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog sub {
|
.blog sub {
|
||||||
opacity: .75;
|
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 {
|
.blog img {
|
||||||
margin: 0 auto;
|
|
||||||
max-height: 50%;
|
max-height: 50%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
@ -44,3 +70,124 @@ article.blog {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
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
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/templates"
|
"arimelody-web/templates"
|
||||||
|
|
||||||
|
@ -20,16 +23,14 @@ var mdRenderer = html.NewRenderer(html.RendererOptions{
|
||||||
func BlogHandler(app *model.AppState) http.Handler {
|
func BlogHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := model.Blog{
|
blog := model.Blog{
|
||||||
Title: "hello world!~",
|
Title: "hello world!",
|
||||||
Description: "lorem ipsum yadda yadda something boobies babababababababa",
|
Description: "lorem ipsum yadda yadda something boobies babababababababa",
|
||||||
Visible: true,
|
Visible: true,
|
||||||
Date: time.Now(),
|
Date: time.Now(),
|
||||||
AuthorID: "ari",
|
AuthorID: "ari",
|
||||||
Markdown:
|
Markdown:
|
||||||
`
|
`
|
||||||
# hello, world!
|
**i'm ari!**
|
||||||
|
|
||||||
i'm ari!
|
|
||||||
|
|
||||||
she/her 🏳️⚧️🏳️🌈💫🦆🇮🇪
|
she/her 🏳️⚧️🏳️🌈💫🦆🇮🇪
|
||||||
|
|
||||||
|
@ -72,7 +73,26 @@ thank you for stopping by- i hope you have a lovely rest of your day! 💫
|
||||||
- impact meme
|
- impact meme
|
||||||
- OpenTerminal
|
- OpenTerminal
|
||||||
- Silver.js
|
- 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>"
|
// 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))
|
md := mdParser.Parse([]byte(blog.Markdown))
|
||||||
blog.HTML = template.HTML(markdown.Render(md, mdRenderer))
|
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>
|
<hr>
|
||||||
{{.HTML}}
|
{{.HTML}}
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- comments section here! -->
|
<div class="interactions">
|
||||||
</article>
|
<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>
|
<script type="module" src="/script/blog.js"></script>
|
||||||
</main>
|
</main>
|
||||||
{{end}}
|
{{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)
|
err := templates.MusicGatewayTemplate.Execute(w, response)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue