Compare commits

...

2 commits

26 changed files with 938 additions and 400 deletions

83
controller/blog.go Normal file
View file

@ -0,0 +1,83 @@
package controller
import (
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)
func GetBlogPost(db *sqlx.DB, id string) (*model.BlogPost, error) {
var blog = model.BlogPost{}
err := db.Get(&blog, "SELECT * FROM blogpost WHERE id=$1", id)
if err != nil {
return nil, err
}
return &blog, nil
}
func GetBlogPosts(db *sqlx.DB, onlyVisible bool, limit int, offset int) ([]*model.BlogPost, error) {
var blogs = []*model.BlogPost{}
query := "SELECT * FROM blogpost ORDER BY created_at"
if onlyVisible {
query = "SELECT * FROM blogpost WHERE visible=true ORDER BY created_at"
}
var err error
if limit < 0 {
err = db.Select(&blogs, query)
} else {
err = db.Select(&blogs, query + " LIMIT $1 OFFSET $2", limit, offset)
}
if err != nil {
return nil, err
}
// for range 4 {
// blog := *blogs[len(blogs)-1]
// blog.CreatedAt = blog.CreatedAt.Add(time.Hour * -5000)
// blogs = append(blogs, &blog)
// }
return blogs, nil
}
func CreateBlogPost(db *sqlx.DB, post *model.BlogPost) error {
_, err := db.Exec(
"INSERT INTO blogpost (id,title,description,visible,author,markdown,html,bluesky_actor,bluesky_post) " +
"VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)",
post.ID,
post.Title,
post.Description,
post.Visible,
post.AuthorID,
post.Markdown,
post.HTML,
post.BlueskyActorID,
post.BlueskyPostID,
)
return err
}
func UpdateBlogPost(db *sqlx.DB, postID string, post *model.BlogPost) error {
_, err := db.Exec(
"UPDATE blogpost SET " +
"id=$2,title=$3,description=$4,visible=$5,author=$6,markdown=$7,html=$8,bluesky_actor=$9,bluesky_post=$10,modified_at=CURRENT_TIMESTAMP " +
"WHERE id=$1",
postID,
post.ID,
post.Title,
post.Description,
post.Visible,
post.AuthorID,
post.Markdown,
post.HTML,
post.BlueskyActorID,
post.BlueskyPostID,
)
return err
}

View file

@ -8,7 +8,7 @@ import (
"github.com/jmoiron/sqlx"
)
const DB_VERSION int = 4
const DB_VERSION int = 5
func CheckDBVersionAndMigrate(db *sqlx.DB) {
db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody")
@ -49,6 +49,10 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
ApplyMigration(db, "003-fail-lock")
oldDBVersion = 4
case 4:
ApplyMigration(db, "004-blog")
oldDBVersion = 5
}
}

View file

@ -1,6 +1,7 @@
package model
import (
"database/sql"
"fmt"
"html/template"
"regexp"
@ -9,20 +10,22 @@ import (
)
type (
Blog struct {
Title string `db:"title"`
Description string `db:"description"`
Visible bool `db:"visible"`
Date time.Time `db:"date"`
AuthorID string `db:"author"`
Markdown string `db:"markdown"`
HTML template.HTML `db:"html"`
BlueskyActorID string `db:"bsky_actor"`
BlueskyPostID string `db:"bsky_post"`
BlogPost struct {
ID string `db:"id"`
Title string `db:"title"`
Description string `db:"description"`
Visible bool `db:"visible"`
CreatedAt time.Time `db:"created_at"`
ModifiedAt sql.NullTime `db:"modified_at"`
AuthorID string `db:"author"`
Markdown string `db:"markdown"`
HTML template.HTML `db:"html"`
BlueskyActorID *string `db:"bluesky_actor"`
BlueskyPostID *string `db:"bluesky_post"`
}
)
func (b *Blog) TitleNormalised() string {
func (b *BlogPost) TitleNormalised() string {
rgx := regexp.MustCompile(`[^a-z0-9\-]`)
return rgx.ReplaceAllString(
strings.ReplaceAll(
@ -32,10 +35,17 @@ func (b *Blog) TitleNormalised() string {
)
}
func (b *Blog) GetMonth() string {
return fmt.Sprintf("%02d", int(b.Date.Month()))
func (b *BlogPost) GetMonth() string {
return fmt.Sprintf("%02d", int(b.CreatedAt.Month()))
}
func (b *Blog) PrintDate() string {
return b.Date.Format("02 January 2006")
func (b *BlogPost) PrintDate() string {
return b.CreatedAt.Format("2 January 2006, 03:04")
}
func (b *BlogPost) PrintModifiedDate() string {
if !b.ModifiedAt.Valid {
return ""
}
return b.ModifiedAt.Time.Format("2 January 2006, 03:04")
}

View file

@ -55,7 +55,7 @@ type (
func (record *Record) CreatedAtPrint() (string, error) {
t, err := record.CreatedAtTime()
if err != nil { return "", err }
return t.Format("15:04, 2 February 2006"), nil
return t.Format("2 Jan 2006, 15:04"), nil
}
func (record *Record) CreatedAtTime() (time.Time, error) {

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="bluesky-dark" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M9.176,7.042c2.503,1.879 5.195,5.69 6.184,7.734c0.989,-2.044 3.681,-5.855 6.184,-7.734c1.806,-1.356 4.733,-2.405 4.733,0.933c-0,0.667 -0.383,5.601 -0.607,6.403c-0.779,2.785 -3.619,3.495 -6.145,3.065c4.415,0.752 5.539,3.241 3.113,5.73c-4.608,4.728 -6.622,-1.186 -7.138,-2.701c-0.095,-0.278 -0.139,-0.408 -0.14,-0.298c-0.001,-0.11 -0.045,0.02 -0.14,0.298c-0.516,1.515 -2.53,7.429 -7.138,2.701c-2.426,-2.489 -1.302,-4.978 3.113,-5.73c-2.526,0.43 -5.366,-0.28 -6.145,-3.065c-0.224,-0.802 -0.607,-5.736 -0.607,-6.403c0,-3.338 2.927,-2.289 4.733,-0.933Z" style="fill:#fff;fill-rule:nonzero;"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="bluesky-light" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M9.176,7.042c2.503,1.879 5.195,5.69 6.184,7.734c0.989,-2.044 3.681,-5.855 6.184,-7.734c1.806,-1.356 4.733,-2.405 4.733,0.933c-0,0.667 -0.383,5.601 -0.607,6.403c-0.779,2.785 -3.619,3.495 -6.145,3.065c4.415,0.752 5.539,3.241 3.113,5.73c-4.608,4.728 -6.622,-1.186 -7.138,-2.701c-0.095,-0.278 -0.139,-0.408 -0.14,-0.298c-0.001,-0.11 -0.045,0.02 -0.14,0.298c-0.516,1.515 -2.53,7.429 -7.138,2.701c-2.426,-2.489 -1.302,-4.978 3.113,-5.73c-2.526,0.43 -5.366,-0.28 -6.145,-3.065c-0.224,-0.802 -0.607,-5.736 -0.607,-6.403c0,-3.338 2.927,-2.289 4.733,-0.933Z" style="fill-rule:nonzero;"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="boost" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M20.14,5.466c1.708,-0.458 4.028,2.658 5.179,6.953c1.151,4.295 0.699,8.154 -1.008,8.611c-1.708,0.458 -4.029,-2.658 -5.179,-6.953c-1.151,-4.295 -0.699,-8.153 1.008,-8.611Zm0.954,3.56c-0.08,1.236 0.101,2.772 0.532,4.383c0.432,1.611 1.043,3.031 1.731,4.062c0.08,-1.237 -0.101,-2.772 -0.533,-4.383c-0.432,-1.611 -1.042,-3.031 -1.73,-4.062Zm-12.3,12.963l-0.907,0.056c-1.177,0.073 -2.242,-0.693 -2.548,-1.833l-0.468,-1.747c-0.305,-1.139 0.234,-2.336 1.29,-2.861l10.773,-8.542c0.173,-0.086 0.379,-0.064 0.53,0.056c0.151,0.12 0.218,0.316 0.174,0.503c-0.318,1.746 -0.19,4.164 0.496,6.724c0.685,2.559 1.784,4.717 2.928,6.074c0.132,0.138 0.171,0.34 0.101,0.517c-0.07,0.178 -0.236,0.299 -0.427,0.311c-1.7,0.108 -5.142,0.322 -8.287,0.516c-0.044,0.385 -0.09,0.771 -0.131,1.119c-0.091,0.771 -0.713,1.371 -1.486,1.435l-0.219,0.018c-0.647,0.054 -1.241,-0.355 -1.422,-0.978l-0.397,-1.368Z" style="fill:#fff;"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="boost" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M20.14,5.466c1.708,-0.458 4.028,2.658 5.179,6.953c1.151,4.295 0.699,8.154 -1.008,8.611c-1.708,0.458 -4.029,-2.658 -5.179,-6.953c-1.151,-4.295 -0.699,-8.153 1.008,-8.611Zm0.954,3.56c-0.08,1.236 0.101,2.772 0.532,4.383c0.432,1.611 1.043,3.031 1.731,4.062c0.08,-1.237 -0.101,-2.772 -0.533,-4.383c-0.432,-1.611 -1.042,-3.031 -1.73,-4.062Zm-12.3,12.963l-0.907,0.056c-1.177,0.073 -2.242,-0.693 -2.548,-1.833l-0.468,-1.747c-0.305,-1.139 0.234,-2.336 1.29,-2.861l10.773,-8.542c0.173,-0.086 0.379,-0.064 0.53,0.056c0.151,0.12 0.218,0.316 0.174,0.503c-0.318,1.746 -0.19,4.164 0.496,6.724c0.685,2.559 1.784,4.717 2.928,6.074c0.132,0.138 0.171,0.34 0.101,0.517c-0.07,0.178 -0.236,0.299 -0.427,0.311c-1.7,0.108 -5.142,0.322 -8.287,0.516c-0.044,0.385 -0.09,0.771 -0.131,1.119c-0.091,0.771 -0.713,1.371 -1.486,1.435l-0.219,0.018c-0.647,0.054 -1.241,-0.355 -1.422,-0.978l-0.397,-1.368Z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="comment" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M26.117,14.576c-0,4.724 -4.82,8.559 -10.757,8.559c-1.015,-0 -2.025,-0.114 -3,-0.34l-5.008,1.907l-0.007,-4.418c-1.765,-1.569 -2.742,-3.601 -2.742,-5.708c0,-4.723 4.82,-8.558 10.757,-8.558c5.937,-0 10.757,3.835 10.757,8.558Zm-15.557,-1.135c-0.795,-0 -1.44,0.645 -1.44,1.44c0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Zm9.6,-0c-0.795,-0 -1.44,0.645 -1.44,1.44c-0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Zm-4.8,-0c-0.795,-0 -1.44,0.645 -1.44,1.44c-0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Z" style="fill:#fff;"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="comment" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M26.117,14.576c-0,4.724 -4.82,8.559 -10.757,8.559c-1.015,-0 -2.025,-0.114 -3,-0.34l-5.008,1.907l-0.007,-4.418c-1.765,-1.569 -2.742,-3.601 -2.742,-5.708c0,-4.723 4.82,-8.558 10.757,-8.558c5.937,-0 10.757,3.835 10.757,8.558Zm-15.557,-1.135c-0.795,-0 -1.44,0.645 -1.44,1.44c-0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Zm9.6,-0c-0.795,-0 -1.44,0.645 -1.44,1.44c-0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Zm-4.8,-0c-0.795,-0 -1.44,0.645 -1.44,1.44c-0,0.795 0.645,1.44 1.44,1.44c0.795,-0 1.44,-0.645 1.44,-1.44c-0,-0.795 -0.645,-1.44 -1.44,-1.44Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="copy-link" serif:id="copy link" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M10.578,14.996c-0.133,-1.199 0.107,-2.433 0.717,-3.512c0.254,-0.448 0.571,-0.869 0.953,-1.251l3.502,-3.503c2.274,-2.273 5.966,-2.273 8.24,0c2.273,2.274 2.273,5.966 -0,8.24l-1.962,1.962c-0.145,0.144 -0.364,0.186 -0.552,0.105c-0.188,-0.081 -0.307,-0.269 -0.302,-0.473c0.023,-0.778 -0.087,-1.559 -0.329,-2.307c-0.058,-0.179 -0.011,-0.375 0.122,-0.507l0.901,-0.902c1.103,-1.103 1.103,-2.893 0,-3.996c-1.103,-1.103 -2.893,-1.103 -3.996,-0l-3.503,3.502c-0.306,0.306 -0.527,0.665 -0.663,1.048c-0.355,0.996 -0.134,2.152 0.663,2.949c0.283,0.283 0.611,0.493 0.962,0.631c0.157,0.062 0.272,0.198 0.306,0.364c0.035,0.165 -0.016,0.336 -0.136,0.455l-1.491,1.491c-0.161,0.161 -0.411,0.193 -0.608,0.077c-0.412,-0.244 -0.8,-0.543 -1.154,-0.897c-0.973,-0.973 -1.53,-2.206 -1.67,-3.476Zm8.847,4.24c-0.254,0.448 -0.571,0.869 -0.953,1.251l-3.502,3.503c-2.274,2.273 -5.966,2.273 -8.24,-0c-2.273,-2.274 -2.273,-5.966 0,-8.24l1.962,-1.962c0.145,-0.144 0.364,-0.186 0.552,-0.105c0.188,0.081 0.307,0.269 0.302,0.473c-0.023,0.778 0.087,1.559 0.329,2.307c0.058,0.179 0.011,0.375 -0.122,0.507l-0.901,0.902c-1.103,1.103 -1.103,2.893 -0,3.996c1.103,1.103 2.893,1.103 3.996,0l3.503,-3.502c0.306,-0.306 0.527,-0.665 0.663,-1.048c0.355,-0.996 0.134,-2.152 -0.663,-2.949c-0.283,-0.283 -0.611,-0.493 -0.962,-0.631c-0.157,-0.062 -0.272,-0.198 -0.306,-0.364c-0.035,-0.165 0.016,-0.336 0.136,-0.455l1.491,-1.491c0.161,-0.161 0.411,-0.193 0.608,-0.077c0.412,0.244 0.8,0.543 1.154,0.897c0.973,0.973 1.53,2.205 1.67,3.476c0.133,1.199 -0.107,2.433 -0.717,3.512Z" style="fill:#fff;"/></svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="copy-link" serif:id="copy link" x="0" y="-0" width="30.72" height="30.72" style="fill:none;"/><g id="copy-link1" serif:id="copy link"><path d="M10.578,14.996c-0.133,-1.199 0.107,-2.433 0.717,-3.512c0.254,-0.448 0.571,-0.869 0.953,-1.251l3.502,-3.503c2.274,-2.273 5.966,-2.273 8.24,0c2.273,2.274 2.273,5.966 -0,8.24l-1.962,1.962c-0.145,0.144 -0.364,0.186 -0.552,0.105c-0.188,-0.081 -0.307,-0.269 -0.302,-0.473c0.023,-0.778 -0.087,-1.559 -0.329,-2.307c-0.058,-0.179 -0.011,-0.375 0.122,-0.507l0.901,-0.902c1.103,-1.103 1.103,-2.893 0,-3.996c-1.103,-1.103 -2.893,-1.103 -3.996,-0l-3.503,3.502c-0.306,0.306 -0.527,0.665 -0.663,1.048c-0.355,0.996 -0.134,2.152 0.663,2.949c0.283,0.283 0.611,0.493 0.962,0.631c0.157,0.062 0.272,0.198 0.306,0.364c0.035,0.165 -0.016,0.336 -0.136,0.455l-1.491,1.491c-0.161,0.161 -0.411,0.193 -0.608,0.077c-0.412,-0.244 -0.8,-0.543 -1.154,-0.897c-0.973,-0.973 -1.53,-2.206 -1.67,-3.476Zm8.847,4.24c-0.254,0.448 -0.571,0.869 -0.953,1.251l-3.502,3.503c-2.274,2.273 -5.966,2.273 -8.24,-0c-2.273,-2.274 -2.273,-5.966 0,-8.24l1.962,-1.962c0.145,-0.144 0.364,-0.186 0.552,-0.105c0.188,0.081 0.307,0.269 0.302,0.473c-0.023,0.778 0.087,1.559 0.329,2.307c0.058,0.179 0.011,0.375 -0.122,0.507l-0.901,0.902c-1.103,1.103 -1.103,2.893 -0,3.996c1.103,1.103 2.893,1.103 3.996,0l3.503,-3.502c0.306,-0.306 0.527,-0.665 0.663,-1.048c0.355,-0.996 0.134,-2.152 -0.663,-2.949c-0.283,-0.283 -0.611,-0.493 -0.962,-0.631c-0.157,-0.062 -0.272,-0.198 -0.306,-0.364c-0.035,-0.165 0.016,-0.336 0.136,-0.455l1.491,-1.491c0.161,-0.161 0.411,-0.193 0.608,-0.077c0.412,0.244 0.8,0.543 1.154,0.897c0.973,0.973 1.53,2.205 1.67,3.476c0.133,1.199 -0.107,2.433 -0.717,3.512Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="like" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M15.36,9.159c2.176,-4.134 6.527,-4.134 8.703,-2.067c2.176,2.067 2.176,6.201 0,10.335c-1.523,3.1 -5.439,6.201 -8.703,8.268c-3.264,-2.067 -7.18,-5.168 -8.703,-8.268c-2.176,-4.134 -2.176,-8.268 -0,-10.335c2.176,-2.067 6.527,-2.067 8.703,2.067Z" style="fill:#fff;"/></svg>

After

Width:  |  Height:  |  Size: 794 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="like" x="0" y="0" width="30.72" height="30.72" style="fill:none;"/><path d="M15.36,9.159c2.176,-4.134 6.527,-4.134 8.703,-2.067c2.176,2.067 2.176,6.201 0,10.335c-1.523,3.1 -5.439,6.201 -8.703,8.268c-3.264,-2.067 -7.18,-5.168 -8.703,-8.268c-2.176,-4.134 -2.176,-8.268 -0,-10.335c2.176,-2.067 6.527,-2.067 8.703,2.067Z"/></svg>

After

Width:  |  Height:  |  Size: 775 B

View file

@ -1,19 +1,15 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => {
/** @type {HTMLLinkElement} */
const commentBody = comment.querySelector('a.comment-body');
import { hijackClickEvent } from "./main.js";
comment.querySelectorAll('a').forEach((/** @type {HTMLLinkElement} */ element) => {
element.addEventListener('click', event => {
event.stopPropagation();
});
});
comment.addEventListener('click', () => {
commentBody.click();
});
comment.style.cursor = 'pointer';
comment.role = 'link';
});
document.querySelectorAll('article.blog-post').forEach(element => {
const link = element.querySelector('.blog-title a');
hijackClickEvent(element, link);
});
document.querySelectorAll('article.blog-post').forEach(element => {
const link = element.querySelector('.blog-title a');
hijackClickEvent(element, link);
});
document.getElementById('load-more').addEventListener('click', event => {
event.preventDefault();
alert('ok');
});

16
public/script/blogpost.js Normal file
View file

@ -0,0 +1,16 @@
import { hijackClickEvent } from "./main.js";
document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => {
/** @type {HTMLLinkElement} */
const commentDate = comment.querySelector('.comment-date');
hijackClickEvent(comment, commentDate);
});
document.getElementById('blog-copy-link').addEventListener('click', event => {
event.preventDefault();
if (navigator.clipboard === undefined) {
console.error("clipboard is not supported by this browser!");
return;
}
navigator.clipboard.writeText(location.protocol + "//" + location.host + location.pathname);
});

View file

@ -1,216 +1,70 @@
:root {
--like: rgb(223, 104, 104);
--repost: rgb(162, 223, 73);
--bluesky: rgb(16, 131, 254);
--mastodon: rgb(86, 58, 204);
article.blog-post {
margin-bottom: 1rem;
padding: 1.5rem;
border: 1px solid #8882;
border-radius: 4px;
background-color: #ffffff08;
transition: background-color .1s;
text-decoration: none;
cursor: pointer;
}
main {
width: min(calc(100% - 4rem), 1200px);
margin: 0 auto 1rem auto;
.blog-post h2:hover,
.blog-post p:hover {
background: none;
}
.blog p:hover,
.comment p:hover {
background: inherit;
.blog-title {
margin: 0;
}
.blog-title a {
display: inherit;
text-wrap: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
article.blog {
font-family: 'Lora', serif;
font-size: 24px;
.blog-meta {
margin: 0;
}
.blog-author {
margin: 0;
font-size: .8em;
}
.blog-author img {
width: 1.3em;
height: 1.3em;
display: inline-block;
transform: translate(0, 4px);
border-radius: 4px;
}
.blog-date {
margin-top: -1em;
opacity: .75;
}
.blog p {
line-height: 1.5em;
}
.blog sub {
opacity: .75;
}
.blog pre {
max-height: 15em;
padding: .5em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
overflow: scroll;
background: var(--background-alt);
}
.blog p code {
padding: .2em .3em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
background: var(--background-alt);
}
.blog img {
max-height: 50%;
max-width: 100%;
display: block;
}
.blog i.end-mark {
width: 1.2em;
height: 1.2em;
margin-top: -.2em;
display: inline-block;
transform: translateY(.2em);
background: url("/img/aridoodle.png");
background-size: contain;
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);
}
.comment-callout {
padding: 1em;
background: var(--background-alt);
border-radius: 2px;
border: 1px solid #8884;
text-align: center;
}
.comment-callout:hover {
background: var(--background-alt);
}
.btn.bluesky,
.btn.mastodon {
font-family: monospace;
text-wrap: nowrap;
text-decoration: none;
transition-property: color background-color;
transition-duration: .1s;
}
.btn.bluesky {
color: var(--bluesky);
border-color: var(--bluesky);
}
.btn.bluesky:hover {
background-color: var(--bluesky);
color: #fff;
}
.btn.mastodon {
color: var(--mastodon);
border-color: var(--mastodon);
}
.btn.mastodon:hover {
background-color: var(--mastodon);
color: #fff;
}
.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 {
margin: 0;
font-size: .8em;
opacity: .5;
opacity: .75;
}
.comment .comment-replies {
margin-left: 1em;
border-left: 2px solid #8884;
.blog-description {
margin: .5em 0 0 0;
display: -webkit-box;
font-size: .8em;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
#load-more {
margin: 0 auto;
padding: .5em 2em;
display: block;
font-family: inherit;
font-size: inherit;
}
@media screen and (max-width: 1500px) {
#blog-sidebar {
display: none;
}
}

314
public/style/blogpost.css Normal file
View file

@ -0,0 +1,314 @@
:root {
--like: rgb(223, 104, 104);
--boost: rgb(162, 223, 73);
--bluesky: rgb(16, 131, 254);
--mastodon: rgb(86, 58, 204);
}
main {
width: min(calc(100% - 4rem), 1200px);
margin: 0 auto 1rem auto;
}
#blog-sidebar {
position: fixed;
width: 3em;
padding: 3em;
transform: translate(-9em, -1em);
overflow: clip;
opacity: .5;
transition: opacity .2s;
}
#blog-sidebar:hover {
opacity: 1;
}
#blog-sidebar ul {
margin: 0;
padding: .3em;
list-style: none;
display: flex;
flex-direction: column;
gap: .3em;
border-radius: 4px;
border: 1px solid var(--on-background);
box-shadow: 4px 4px 4px #0001;
}
#blog-sidebar a {
width: 35px;
height: 35px;
display: block;
padding: .2em;
border-radius: 2px;
text-decoration: none;
}
#blog-sidebar a:hover {
background: #0001;
}
#blog-sidebar a:active {
background: #0002;
}
#blog-sidebar a img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
#blog-sidebar span {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5em;
}
#blog-sidebar hr {
margin: 0;
}
.blog p:hover,
.comment p:hover {
background: inherit;
}
article.blog {
font-size: 22px;
}
.blog h1 {
margin-bottom: 0;
font-size: 1.8em;
}
.blog-author {
margin: .2em 0;
}
.blog .blog-author img {
width: 1.3em;
height: 1.3em;
display: inline-block;
transform: translate(0, 6px);
border-radius: 4px;
}
.blog-date {
margin: .5em 0;
font-size: .7em;
}
.blog-modified-date {
font-style: italic;
opacity: .75;
}
#blog-content {
font-family: 'Lora', serif;
}
.blog p {
line-height: 1.5em;
}
.blog sub {
opacity: .75;
}
.blog pre {
max-height: 15em;
padding: .5em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
overflow: scroll;
background: var(--background-alt);
}
.blog p code {
padding: .2em .3em;
font-size: .9em;
border: 1px solid #8884;
border-radius: 2px;
background: var(--background-alt);
}
.blog img {
max-height: 50%;
max-width: 100%;
display: block;
}
.blog i.end-mark {
width: 1.2em;
height: 1.2em;
margin-top: -.2em;
display: inline-block;
transform: translateY(.2em);
background: url("/img/aridoodle.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* COMMENTS */
#interactions {
margin: 1em 0;
display: flex;
flex-direction: row;
gap: .5em;
flex-wrap: wrap;
align-items: center;
}
.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 button {
min-width: fit-content;
padding: 0 .75em 0 .5em;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: .5em;
font-family: monospace;
font-size: inherit;
text-align: center;
line-height: 2em;
text-wrap: nowrap;
color: inherit;
background: none;
border: 1px solid var(--on-background);
border-radius: 4px;
}
#interactions button:hover {
background: #0001;
}
#interactions button:active {
background: #0002;
}
#interactions img {
width: 1.5em;
height: 1.5em;
display: inline-block;
}
.comment-callout {
margin: 0 0 0 1em;
}
.comment-callout:hover {
background: none;
}
.bluesky {
color: var(--bluesky);
}
.mastodon {
color: var(--mastodon);
}
.comment {
font-family: 'Inter', 'Arial', sans-serif;
font-size: 1em;
}
.comment .comment-hover {
padding: 1em;
transition: background-color .1s;
cursor: pointer;
}
.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;
border-radius: 4px;
}
.comment .comment-body {
color: inherit;
text-decoration: none;
}
.comment p.comment-text {
margin: .5em 0;
white-space: break-spaces;
}
.comment .comment-footer {
margin: 0;
font-size: .8em;
}
.comment .comment-footer .comment-footer-static {
opacity: .5;
}
.comment .comment-replies {
margin-left: 1em;
border-left: 2px solid #8884;
}
.comment .comment-date {
transition: opacity .2s;
}
@media screen and (prefers-color-scheme: dark) {
#blog-sidebar a:hover {
background: #fff2;
}
#blog-sidebar a:active {
background: #fff4;
}
.comment-date {
opacity: .5;
}
}

View file

@ -38,7 +38,7 @@ a:hover {
text-decoration: underline;
}
a.link-button {
.link-button {
padding: .3em .5em;
border: 1px solid var(--links);
color: var(--links);
@ -51,7 +51,7 @@ a.link-button {
opacity: 0;
}
a.link-button:hover {
.link-button:hover {
color: #eee;
border-color: #eee;
background-color: var(--links) !important;
@ -141,8 +141,23 @@ a#backtotop:hover {
}
}
.light-only {
display: none;
}
.dark-only {
display: inherit;
}
@media (prefers-color-scheme: light) {
.light-only {
display: inherit;
}
.dark-only {
display: none;
}
a.link-button:hover {
box-shadow: none;
}
@ -161,6 +176,14 @@ a#backtotop:hover {
}
@media (prefers-color-scheme: dark) {
.light-only {
display: none;
}
.dark-only {
display: inherit;
}
body.crt {
text-shadow: 0 0 3em;
}

View file

@ -127,17 +127,39 @@ ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIM
CREATE TABLE arimelody.blogpost (
id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
description TEXT NOT NULL,
visible BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
modified_at TIMESTAMP,
author UUID NOT NULL,
markdown TEXT NOT NULL,
html TEXT NOT NULL,
bluesky_actor TEXT,
bluesky_post TEXT
);
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id);
--
-- Foreign keys
--
-- Account
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
-- Music
ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
ALTER TABLE arimelody.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE;
-- Blog
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_author_fk FOREIGN KEY (author) REFERENCES account(id) ON DELETE CASCADE;

View file

@ -0,0 +1,15 @@
CREATE TABLE arimelody.blogpost (
id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
description TEXT NOT NULL,
visible BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
modified_at TIMESTAMP,
author UUID NOT NULL,
markdown TEXT NOT NULL,
html TEXT NOT NULL,
bluesky_actor TEXT,
bluesky_post TEXT
);
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id);
ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_author_fk FOREIGN KEY (author) REFERENCES account(id) ON DELETE CASCADE;

View file

@ -33,3 +33,10 @@ var BlogTemplate = template.Must(template.ParseFiles(
filepath.Join("view", "prideflag.html"),
filepath.Join("view", "blog.html"),
))
var BlogPostTemplate = template.Must(template.ParseFiles(
filepath.Join("view", "layout.html"),
filepath.Join("view", "header.html"),
filepath.Join("view", "footer.html"),
filepath.Join("view", "prideflag.html"),
filepath.Join("view", "blogpost.html"),
))

View file

@ -5,7 +5,9 @@ import (
"html/template"
"net/http"
"os"
"time"
"slices"
"strconv"
"strings"
"arimelody-web/controller"
"arimelody-web/model"
@ -16,14 +18,26 @@ import (
"github.com/gomarkdown/markdown/parser"
)
type BlogView struct {
*model.Blog
Comments []*model.ThreadViewPost
Likes int
Reposts int
BlueskyURL string
MastodonURL string
}
type (
BlogView struct {
Collections []*BlogViewPostCollection
}
BlogViewPostCollection struct {
Name string
Posts []*BlogPostView
}
BlogPostView struct {
*model.BlogPost
Author *model.Account
Comments []*model.ThreadViewPost
Likes int
Boosts int
BlueskyURL string
MastodonURL string
}
)
var mdRenderer = html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
@ -31,78 +45,105 @@ 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!",
Description: "lorem ipsum yadda yadda something boobies babababababababa",
Visible: true,
Date: time.Now(),
AuthorID: "ari",
Markdown:
`
**i'm ari!**
if strings.Count(r.URL.Path, "/") > 1 {
http.NotFound(w, r)
return
}
she/her 🏳🏳🌈💫🦆🇮🇪
if len(r.URL.Path) > 1 {
ServeBlogPost(app, r.URL.Path[1:]).ServeHTTP(w, r)
return
}
i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention!
dbPosts, err := controller.GetBlogPosts(app.DB, true, -1, 0)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
you're very welcome to take a look around my little space on the internet here, or explore any of the other parts i inhabit!
collections := []*BlogViewPostCollection{}
posts := []*BlogPostView{}
collectionYear := 0
for i, post := range dbPosts {
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 you're looking to support me financially, that's so cool of you!! if you like, you can buy some of my music over on bandcamp so you can at least get something for your money. thank you very much either way!! 💕
if i == 0 {
collectionYear = post.CreatedAt.Year()
}
for anything else, you can reach me for any and all communications through ari@arimelody.me. if your message contains anything beyond a silly gag, i strongly recommend encrypting your message using my public pgp key, listed below!
if post.CreatedAt.Year() != collectionYear || i == len(dbPosts) - 1 {
if i == len(dbPosts) - 1 {
posts = append(posts, &BlogPostView{
BlogPost: post,
Author: author,
})
}
postsCopy := slices.Clone(posts)
collections = append(collections, &BlogViewPostCollection{
Name: strconv.Itoa(collectionYear),
Posts: postsCopy,
})
posts = []*BlogPostView{}
collectionYear = post.CreatedAt.Year()
}
thank you for stopping by- i hope you have a lovely rest of your day! 💫
posts = append(posts, &BlogPostView{
BlogPost: post,
Author: author,
})
}
## metadata
**my colours 🌈**
- primary: <span class="col-primary">#b7fd49</span>
- secondary: <span class="col-secondary">#f8e05b</span>
- tertiary: <span class="col-tertiary">#f788fe</span>
**my keys 🔑**
- pgp: [[link]](/keys/ari%20melody_0x92678188_public.asc)
- ssh (ed25519): [[link]](/keys/id_ari_ed25519.pub)
**where to find me 🛰**
- youtube
- twitch
- spotify
- bandcamp
- github
**projects i've worked on 🛠**
- catdance
- pride flag
- ipaddrgen
- 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",
err = templates.BlogTemplate.Execute(w, BlogView{
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
}
})
}
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 {
if strings.Contains(err.Error(), "no rows") {
http.NotFound(w, r)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
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
}
}
author, err := controller.GetAccountByID(app.DB, blog.AuthorID)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", blog.ID, err)
}
// blog.Markdown += " <i class=\"end-mark\"></i>"
@ -111,23 +152,32 @@ this is `+"`"+`aridoodle`+"`"+`. please take care of her.
blog.HTML = template.HTML(markdown.Render(md, mdRenderer))
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...)
}
likeCount := 0
boostCount := 0
var blueskyURL string
var blueskyPost *model.ThreadViewPost
if blog.BlueskyActorID != nil && blog.BlueskyPostID != nil {
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...)
likeCount += blueskyPost.Post.LikeCount
boostCount += blueskyPost.Post.RepostCount
blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, *blog.BlueskyPostID)
}
}
err = templates.BlogTemplate.Execute(w, BlogView{
Blog: &blog,
Comments: blueskyPost.Replies,
Likes: blueskyPost.Post.LikeCount,
Reposts: blueskyPost.Post.RepostCount,
BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID),
MastodonURL: "#",
err = templates.BlogPostTemplate.Execute(w, BlogPostView{
BlogPost: blog,
Author: author,
Comments: comments,
Likes: likeCount,
Boosts: boostCount,
BlueskyURL: blueskyURL,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error rendering blog post: %v\n", err)
fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

View file

@ -1,15 +1,15 @@
{{define "head"}}
<title>{{.Title}} - ari melody 💫</title>
<title>blog - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<meta name="description" content="{{.Description}}">
<meta name="description" content="thoughts from your local SPACEGIRL 💫">
<meta property="og:title" content="{{.Title}}">
<meta property="og:title" content="ari melody blog 💫">
<meta property="og:type" content="article">
<meta property="og:url" content="www.arimelody.me/blog/{{.Date.Year}}/{{.GetMonth}}/{{.TitleNormalised}}">
<meta property="og:image" content="https://www.arimelody.me/img/favicon.png">
<meta property="og:url" content="https://arimelody.space/blog/">
<meta property="og:image" content="https://arimelody.space/img/favicon.png">
<meta property="og:site_name" content="ari melody">
<meta property="og:description" content="{{.Description}}">
<meta property="og:description" content="thoughts from your local SPACEGIRL 💫">
<link rel="stylesheet" href="/style/main.css">
<link rel="stylesheet" href="/style/index.css">
@ -18,78 +18,34 @@
{{define "content"}}
<main>
<article class="blog">
<h1 class="typeout">{{.Title}}</h1>
<p class="blog-date">Posted by <a href="/blog/{{.AuthorID}}">{{.AuthorID}}</a> @ {{.PrintDate}}</p>
<hr>
{{.HTML}}
</article>
<h1 class="typeout"># blog</h1>
<p class="">thoughts from your local SPACEGIRL 💫</p>
<hr>
<div class="interactions">
<span class="likes">❤️ {{.Likes}}</span>
<span class="reposts">🔁 {{.Reposts}}</span>
</div>
<p class="comment-callout">
join the conversation on
<a class="btn bluesky" href="{{.BlueskyURL}}" target="_blank">Bluesky 🦋</a>
<!-- TODO: mastodon support -->
<!--
or
<a class="btn mastodon" href="{{.MastodonURL}}" target="_blank">Mastodon 🐘</a>
-->
</p>
<div class="comments">
{{range .Comments}}
{{template "comment" .}}
<div id="posts">
{{if eq (len .Collections) 0}}
<p>there are no posts! 🍃</p>
{{end}}
{{range .Collections}}
<h2 id="{{.Name}}" class="collection-name">{{.Name}}</h2>
{{range .Posts}}
<article class="blog-post">
<h3 class="blog-title"><a href="/blog/{{.ID}}">{{.Title}}</a></h3>
<p class="blog-meta">
<span class="blog-author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
<span class="blog-date">&bull; {{.PrintDate}}</span>
</p>
{{if ne .Description ""}}
<p class="blog-description">{{.Description}}</p>
{{end}}
</article>
{{end}}
{{end}}
</div>
<script type="module" src="/script/blog.js"></script>
<!-- <button type="submit" class="link-button" id="load-more">load more</button> -->
<script src="/script/blog.js" type="module" defer></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}} like{{if ne .Post.LikeCount 1}}s{{end}}</span>
&bull;
<span>{{.Post.RepostCount}} repost{{if ne .Post.RepostCount 1}}s{{end}}</span>
&bull;
<span class="comment-date">{{.Post.Record.CreatedAtPrint}}</span>
</div>
</a>
</div>
<div class="comment-replies">
{{range .Replies}}
{{template "comment" .}}
{{end}}
</div>
</article>
{{end}}

178
view/blogpost.html Normal file
View file

@ -0,0 +1,178 @@
{{define "head"}}
<title>{{.Title}} - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<meta name="description" content="{{.Description}}">
<meta property="og:title" content="{{.Title}}">
<meta property="og:type" content="article">
<meta property="og:url" content="https://arimelody.space/blog/{{.ID}}">
<meta property="og:image" content="https://arimelody.space/img/favicon.png">
<meta property="og:site_name" content="ari melody">
<meta property="og:description" content="{{.Description}}">
<link rel="stylesheet" href="/style/main.css">
<link rel="stylesheet" href="/style/index.css">
<link rel="stylesheet" href="/style/blogpost.css">
{{end}}
{{define "content"}}
<main>
<div id="blog-sidebar">
<ul>
<li>
<a href="#copy-link" id="blog-copy-link" title="copy link">
<div class="dark-only">
<img src="/img/blog/copy-link-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/copy-link-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{if ne .BlueskyURL ""}}
<li>
<a href="{{.BlueskyURL}}" id="blog-share-bsky" title="share on bluesky">
<div class="dark-only">
<img src="/img/blog/bluesky-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/bluesky-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<hr>
<li>
<a href="{{.BlueskyURL}}" id="blog-like" title="like this post">
<div class="dark-only">
<img src="/img/blog/like-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/like-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="{{.BlueskyURL}}" id="blog-boost" title="boost this post">
<div class="dark-only">
<img src="/img/blog/boost-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/boost-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
<li>
<a href="#comments" id="blog-comments" title="comments">
<div class="dark-only">
<img src="/img/blog/comment-dark.svg" alt="" width="36" height="36">
</div>
<div class="light-only">
<img src="/img/blog/comment-light.svg" alt="" width="36" height="36">
</div>
</a>
</li>
{{end}}
</ul>
</div>
<div id="blog-container">
<article class="blog">
<div id="blog-header">
<h1 class="typeout"># {{.Title}}</h1>
<p class="blog-author">by <a href="/blog?author={{.Author.Username}}">{{.Author.Username}} <img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/></a></p>
<p class="blog-date">posted {{.PrintDate}}{{if .ModifiedAt.Valid}} <span class="blog-modified-date">&bull; updated {{.PrintModifiedDate}}</span>{{end}}</p>
</div>
<hr>
<div id="blog-content">
{{.HTML}}
</div>
</article>
{{if ne .BlueskyURL ""}}
<hr>
<div id="interactions">
<button class="likes" aria-label="{{.Likes}} likes">
<div class="dark-only">
<img src="/img/blog/like-dark.svg" alt="" width="32" height="32">
</div>
<div class="light-only">
<img src="/img/blog/like-light.svg" alt="" width="32" height="32">
</div>
{{.Likes}}
</button>
<button class="boosts" aria-label="{{.Boosts}} boosts">
<div class="dark-only">
<img src="/img/blog/boost-dark.svg" alt="" width="32" height="32">
</div>
<div class="light-only">
<img src="/img/blog/boost-light.svg" alt="" width="32" height="32">
</div>
{{.Boosts}}
</button>
<p class="comment-callout">
join the conversation on
<a class="bluesky" href="{{.BlueskyURL}}">Bluesky 🦋</a>
<!-- TODO: mastodon support -->
<!--
and
<a class="mastodon" href="{{.MastodonURL}}">Mastodon 🐘</a>
-->
</p>
</div>
<div id="comments">
{{range .Comments}}
{{template "comment" .}}
{{end}}
</div>
{{end}}
<script type="module" src="/script/blogpost.js"></script>
</div>
</main>
{{end}}
{{define "comment"}}
<article class="comment">
<div class="comment-hover">
<div class="comment-header">
<a href="https://bsky.app/profile/{{.Post.Author.Handle}}" target="_blank">
<img class="avatar" src="{{.Post.Author.Avatar}}" alt="{{.Post.Author.DisplayName}}'s avatar" width="32" height="32">
<span class="display-name">{{.Post.Author.DisplayName}}</span>
<span class="handle">@{{.Post.Author.Handle}}</span>
</a>
</div>
<div class="comment-body" 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 class="comment-footer-static">
<span>{{.Post.LikeCount}} like{{if ne .Post.LikeCount 1}}s{{end}}</span>
&bull;
<span>{{.Post.RepostCount}} boost{{if ne .Post.RepostCount 1}}s{{end}}</span>
&bull;
</span>
<a href="{{.Post.BskyURL}}" class="comment-date">{{.Post.Record.CreatedAtPrint}}</a>
</div>
</div>
</div>
<div class="comment-replies">
{{range .Replies}}
{{template "comment" .}}
{{end}}
</div>
</article>
{{end}}

View file

@ -21,11 +21,11 @@
<a href="/" preload="mouseover">home</a>
</li>
<li>
<a href="/music" preload="mouseover">music</a>
<a href="/music/" preload="mouseover">music</a>
</li>
<li>
<!-- coming later! -->
<span title="coming later!">blog</span>
<a href="/blog/" preload="mouseover">blog</a>
</li>
<li>
<!-- coming later! -->