add blog index to admin dashboard
This commit is contained in:
parent
ee8bf6543e
commit
65366032fd
10 changed files with 603 additions and 91 deletions
110
admin/blog/blog.go
Normal file
110
admin/blog/blog.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/admin/core"
|
||||||
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/controller"
|
||||||
|
"arimelody-web/model"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
blogID := r.PathValue("id")
|
||||||
|
w.Write([]byte(blogID))
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("/", handleBlogIndex(app))
|
||||||
|
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
blogPost struct {
|
||||||
|
*model.BlogPost
|
||||||
|
Author *model.Account
|
||||||
|
}
|
||||||
|
|
||||||
|
blogPostGroup struct {
|
||||||
|
Year int
|
||||||
|
Posts []*blogPost
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleBlogIndex(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := controller.GetBlogPosts(app.DB, false, -1, 0)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
collections := []*blogPostGroup{}
|
||||||
|
collectionPosts := []*blogPost{}
|
||||||
|
collectionYear := -1
|
||||||
|
for i, post := range posts {
|
||||||
|
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 collectionYear == -1 {
|
||||||
|
collectionYear = post.CreatedAt.Year()
|
||||||
|
}
|
||||||
|
|
||||||
|
authoredPost := blogPost{
|
||||||
|
BlogPost: post,
|
||||||
|
Author: author,
|
||||||
|
}
|
||||||
|
|
||||||
|
if post.CreatedAt.Year() != collectionYear || i == len(posts) - 1 {
|
||||||
|
if i == len(posts) - 1 {
|
||||||
|
collectionPosts = append(collectionPosts, &authoredPost)
|
||||||
|
}
|
||||||
|
collections = append(collections, &blogPostGroup{
|
||||||
|
Year: collectionYear,
|
||||||
|
Posts: slices.Clone(collectionPosts),
|
||||||
|
})
|
||||||
|
collectionPosts = []*blogPost{}
|
||||||
|
collectionYear = post.CreatedAt.Year()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionPosts = append(collectionPosts, &authoredPost)
|
||||||
|
}
|
||||||
|
|
||||||
|
type blogsData struct {
|
||||||
|
core.AdminPageData
|
||||||
|
TotalPosts int
|
||||||
|
Collections []*blogPostGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
err = templates.BlogsTemplate.Execute(w, blogsData{
|
||||||
|
AdminPageData: core.AdminPageData{
|
||||||
|
Session: session,
|
||||||
|
},
|
||||||
|
TotalPosts: len(posts),
|
||||||
|
Collections: collections,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin blog index: %v\n", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"arimelody-web/admin/account"
|
"arimelody-web/admin/account"
|
||||||
"arimelody-web/admin/auth"
|
"arimelody-web/admin/auth"
|
||||||
|
"arimelody-web/admin/blog"
|
||||||
"arimelody-web/admin/core"
|
"arimelody-web/admin/core"
|
||||||
"arimelody-web/admin/logs"
|
"arimelody-web/admin/logs"
|
||||||
"arimelody-web/admin/music"
|
"arimelody-web/admin/music"
|
||||||
|
|
@ -24,8 +25,10 @@ func Handler(app *model.AppState) http.Handler {
|
||||||
mux.Handle("/totp", auth.LoginTOTPHandler(app))
|
mux.Handle("/totp", auth.LoginTOTPHandler(app))
|
||||||
mux.Handle("/logout", core.RequireAccount(auth.LogoutHandler(app)))
|
mux.Handle("/logout", core.RequireAccount(auth.LogoutHandler(app)))
|
||||||
|
|
||||||
mux.Handle("/music/", core.RequireAccount(http.StripPrefix("/music", music.Handler(app))))
|
|
||||||
mux.Handle("/logs", core.RequireAccount(logs.Handler(app)))
|
mux.Handle("/logs", core.RequireAccount(logs.Handler(app)))
|
||||||
|
mux.Handle("/music/", core.RequireAccount(http.StripPrefix("/music", music.Handler(app))))
|
||||||
|
mux.Handle("/blogs/", core.RequireAccount(http.StripPrefix("/blogs", blog.Handler(app))))
|
||||||
|
|
||||||
mux.Handle("/account/", core.RequireAccount(http.StripPrefix("/account", account.Handler(app))))
|
mux.Handle("/account/", core.RequireAccount(http.StripPrefix("/account", account.Handler(app))))
|
||||||
|
|
||||||
mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -42,13 +45,13 @@ func Handler(app *model.AppState) http.Handler {
|
||||||
view.ServeFiles("./admin/static"))).ServeHTTP(w, r)
|
view.ServeFiles("./admin/static"))).ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mux.Handle("/", core.RequireAccount(AdminIndexHandler(app)))
|
mux.Handle("/", core.RequireAccount(adminIndexHandler(app)))
|
||||||
|
|
||||||
// response wrapper to make sure a session cookie exists
|
// response wrapper to make sure a session cookie exists
|
||||||
return core.EnforceSession(app, mux)
|
return core.EnforceSession(app, mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminIndexHandler(app *model.AppState) http.Handler {
|
func adminIndexHandler(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) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
|
||||||
256
admin/releasehttp.go
Normal file
256
admin/releasehttp.go
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"arimelody-web/admin/core"
|
||||||
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/controller"
|
||||||
|
"arimelody-web/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveReleases(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/")
|
||||||
|
releaseID := slices[0]
|
||||||
|
|
||||||
|
var action string = ""
|
||||||
|
if len(slices) > 1 {
|
||||||
|
action = slices[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(releaseID) > 0 {
|
||||||
|
serveRelease(app, releaseID, action).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleasesData struct {
|
||||||
|
core.AdminPageData
|
||||||
|
Releases []*model.Release
|
||||||
|
}
|
||||||
|
|
||||||
|
releases, err := controller.GetAllReleases(app.DB, false, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = templates.ReleasesTemplate.Execute(w, ReleasesData{
|
||||||
|
AdminPageData: core.AdminPageData{
|
||||||
|
Path: r.URL.Path,
|
||||||
|
Session: session,
|
||||||
|
},
|
||||||
|
Releases: releases,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to serve releases page: %s\n", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveRelease(app *model.AppState, releaseID string, action string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
release, err := controller.GetRelease(app.DB, releaseID, true)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("WARN: Failed to fetch full release data for %s: %s\n", releaseID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(action) > 0 {
|
||||||
|
switch action {
|
||||||
|
case "editcredits":
|
||||||
|
serveEditCredits(release).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "addcredit":
|
||||||
|
serveAddCredit(app, release).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "newcredit":
|
||||||
|
serveNewCredit(app).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "editlinks":
|
||||||
|
serveEditLinks(release).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "edittracks":
|
||||||
|
serveEditTracks(release).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "addtrack":
|
||||||
|
serveAddTrack(app, release).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
case "newtrack":
|
||||||
|
serveNewTrack(app).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseResponse struct {
|
||||||
|
core.AdminPageData
|
||||||
|
Release *model.Release
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, track := range release.Tracks { track.Number = i + 1 }
|
||||||
|
|
||||||
|
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
||||||
|
AdminPageData: core.AdminPageData{ Path: r.URL.Path, Session: session },
|
||||||
|
Release: release,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve admin release page for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveEditCredits(release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.EditCreditsTemplate.Execute(w, release)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve edit credits component for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
ReleaseID string;
|
||||||
|
Artists []*model.Artist
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err = templates.AddCreditTemplate.Execute(w, response{
|
||||||
|
ReleaseID: release.ID,
|
||||||
|
Artists: artists,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve add credits component for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveNewCredit(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
split := strings.Split(r.URL.Path, "/")
|
||||||
|
artistID := split[len(split) - 1]
|
||||||
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if artist == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err = templates.NewCreditTemplate.Execute(w, artist)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve new credit component for %s: %s\n", artist.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveEditLinks(release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err := templates.EditLinksTemplate.Execute(w, release)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve edit links component for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveEditTracks(release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
|
||||||
|
type editTracksData struct { Release *model.Release }
|
||||||
|
|
||||||
|
for i, track := range release.Tracks { track.Number = i + 1 }
|
||||||
|
|
||||||
|
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
ReleaseID string;
|
||||||
|
Tracks []*model.Track
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err = templates.AddTrackTemplate.Execute(w, response{
|
||||||
|
ReleaseID: release.ID,
|
||||||
|
Tracks: tracks,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to add tracks component for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveNewTrack(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
split := strings.Split(r.URL.Path, "/")
|
||||||
|
trackID := split[len(split) - 1]
|
||||||
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if track == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
err = templates.NewTrackTemplate.Execute(w, track)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARN: Failed to serve new track component for %s: %s\n", track.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -264,10 +264,6 @@ code {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 .5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.cards {
|
.cards {
|
||||||
|
|
|
||||||
80
admin/static/blog.css
Normal file
80
admin/static/blog.css
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
.blog-collection h2 {
|
||||||
|
margin: .5em 1em;
|
||||||
|
font-size: 1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost {
|
||||||
|
padding: 1em;
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-2);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .title small {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .6em;
|
||||||
|
transform: translateY(-0.1em);
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .description {
|
||||||
|
margin: .5em 0 .6em 0;
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .8em;
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .meta .author {
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .meta .author img {
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
margin-right: .2em;
|
||||||
|
display: inline-block;
|
||||||
|
transform: translate(0, 4px);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .actions {
|
||||||
|
margin-top: .5em;
|
||||||
|
display: flex;
|
||||||
|
gap: .3em;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--fg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .actions a {
|
||||||
|
padding: .3em .5em;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-3);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogpost .actions a:hover {
|
||||||
|
background-color: var(--bg-0);
|
||||||
|
color: var(--fg-3);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
0
admin/static/blog.js
Normal file
0
admin/static/blog.js
Normal file
33
admin/templates/html/blogs.html
Normal file
33
admin/templates/html/blogs.html
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>Blog - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/admin/static/blog.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Blog Posts <small>({{.TotalPosts}} total)</small></h2>
|
||||||
|
<a class="button new" id="create-post">Create New</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{if .Collections}}
|
||||||
|
<div class="blog-group">
|
||||||
|
{{range .Collections}}
|
||||||
|
{{if .Posts}}
|
||||||
|
<div class="blog-collection">
|
||||||
|
<h2>{{.Year}}</h2>
|
||||||
|
{{range .Posts}}
|
||||||
|
{{block "blogpost" .}}{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p>There are no blog posts.</p>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/admin/static/blog.js"></script>
|
||||||
|
{{end}}
|
||||||
14
admin/templates/html/components/blog/blogpost.html
Normal file
14
admin/templates/html/components/blog/blogpost.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{{define "blogpost"}}
|
||||||
|
<div class="blogpost">
|
||||||
|
<h3 class="title"><a href="/admin/blogs/{{.ID}}">{{.Title}}</a>{{if not .Visible}} <small>(Not published)</small>{{end}}</h3>
|
||||||
|
<p class="meta">
|
||||||
|
<span class="author"><img src="/img/favicon.png" alt="{{.Author.Username}}'s avatar" width="32" height="32"/> {{.Author.Username}}</span>
|
||||||
|
<span class="date">• {{.PrintDate}}</span>
|
||||||
|
</p>
|
||||||
|
<p class="description">{{.Description}}</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/admin/blogs/{{.ID}}">Edit</a>
|
||||||
|
<a href="/blog/{{.ID}}">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -27,7 +27,9 @@
|
||||||
<div class="nav-item{{if eq .Path "/logs"}} active{{end}}">
|
<div class="nav-item{{if eq .Path "/logs"}} active{{end}}">
|
||||||
<a href="/admin/logs">logs</a>
|
<a href="/admin/logs">logs</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p class="section-label">music</p>
|
<p class="section-label">music</p>
|
||||||
<div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
|
<div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}">
|
||||||
<a href="/admin/music/releases/">releases</a>
|
<a href="/admin/music/releases/">releases</a>
|
||||||
|
|
@ -38,6 +40,13 @@
|
||||||
<div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
|
<div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}">
|
||||||
<a href="/admin/music/tracks/">tracks</a>
|
<a href="/admin/music/tracks/">tracks</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p class="section-label">blog</p>
|
||||||
|
<div class="nav-item{{if hasPrefix .Path "/blogs"}} active{{end}}">
|
||||||
|
<a href="/admin/blogs/">blog</a>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="flex-fill"></div>
|
<div class="flex-fill"></div>
|
||||||
|
|
|
||||||
|
|
@ -17,63 +17,9 @@ var prideflagHTML string
|
||||||
//go:embed "html/index.html"
|
//go:embed "html/index.html"
|
||||||
var indexHTML string
|
var indexHTML string
|
||||||
|
|
||||||
//go:embed "html/register.html"
|
|
||||||
var registerHTML string
|
|
||||||
//go:embed "html/login.html"
|
|
||||||
var loginHTML string
|
|
||||||
//go:embed "html/login-totp.html"
|
|
||||||
var loginTotpHTML string
|
|
||||||
//go:embed "html/totp-confirm.html"
|
|
||||||
var totpConfirmHTML string
|
|
||||||
//go:embed "html/totp-setup.html"
|
|
||||||
var totpSetupHTML string
|
|
||||||
//go:embed "html/logout.html"
|
|
||||||
var logoutHTML string
|
|
||||||
|
|
||||||
//go:embed "html/logs.html"
|
//go:embed "html/logs.html"
|
||||||
var logsHTML string
|
var logsHTML string
|
||||||
|
|
||||||
//go:embed "html/edit-account.html"
|
|
||||||
var editAccountHTML string
|
|
||||||
|
|
||||||
//go:embed "html/releases.html"
|
|
||||||
var releasesHTML string
|
|
||||||
//go:embed "html/artists.html"
|
|
||||||
var artistsHTML string
|
|
||||||
//go:embed "html/tracks.html"
|
|
||||||
var tracksHTML string
|
|
||||||
|
|
||||||
//go:embed "html/edit-release.html"
|
|
||||||
var editReleaseHTML string
|
|
||||||
//go:embed "html/edit-artist.html"
|
|
||||||
var editArtistHTML string
|
|
||||||
//go:embed "html/edit-track.html"
|
|
||||||
var editTrackHTML string
|
|
||||||
|
|
||||||
//go:embed "html/components/credit/newcredit.html"
|
|
||||||
var componentNewCreditHTML string
|
|
||||||
//go:embed "html/components/credit/addcredit.html"
|
|
||||||
var componentAddCreditHTML string
|
|
||||||
//go:embed "html/components/credit/editcredits.html"
|
|
||||||
var componentEditCreditsHTML string
|
|
||||||
|
|
||||||
//go:embed "html/components/link/editlinks.html"
|
|
||||||
var componentEditLinksHTML string
|
|
||||||
|
|
||||||
//go:embed "html/components/release/release.html"
|
|
||||||
var componentReleaseHTML string
|
|
||||||
//go:embed "html/components/artist/artist.html"
|
|
||||||
var componentArtistHTML string
|
|
||||||
//go:embed "html/components/track/track.html"
|
|
||||||
var componentTrackHTML string
|
|
||||||
|
|
||||||
//go:embed "html/components/track/newtrack.html"
|
|
||||||
var componentNewTrackHTML string
|
|
||||||
//go:embed "html/components/track/addtrack.html"
|
|
||||||
var componentAddTrackHTML string
|
|
||||||
//go:embed "html/components/track/edittracks.html"
|
|
||||||
var componentEditTracksHTML string
|
|
||||||
|
|
||||||
var BaseTemplate = template.Must(
|
var BaseTemplate = template.Must(
|
||||||
template.New("base").Funcs(
|
template.New("base").Funcs(
|
||||||
template.FuncMap{
|
template.FuncMap{
|
||||||
|
|
@ -84,6 +30,13 @@ var BaseTemplate = template.Must(
|
||||||
prideflagHTML,
|
prideflagHTML,
|
||||||
}, "\n")))
|
}, "\n")))
|
||||||
|
|
||||||
|
//go:embed "html/components/release/release.html"
|
||||||
|
var componentReleaseHTML string
|
||||||
|
//go:embed "html/components/artist/artist.html"
|
||||||
|
var componentArtistHTML string
|
||||||
|
//go:embed "html/components/track/track.html"
|
||||||
|
var componentTrackHTML string
|
||||||
|
|
||||||
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
indexHTML,
|
indexHTML,
|
||||||
|
|
@ -95,37 +48,83 @@ var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/login.html"
|
||||||
|
var loginHTML string
|
||||||
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
|
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
|
||||||
|
|
||||||
|
//go:embed "html/login-totp.html"
|
||||||
|
var loginTotpHTML string
|
||||||
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
|
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
|
||||||
|
|
||||||
|
//go:embed "html/register.html"
|
||||||
|
var registerHTML string
|
||||||
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
|
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
|
||||||
|
|
||||||
|
//go:embed "html/logout.html"
|
||||||
|
var logoutHTML string
|
||||||
var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
|
var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
|
||||||
|
|
||||||
|
//go:embed "html/edit-account.html"
|
||||||
|
var editAccountHTML string
|
||||||
var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
|
var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
|
||||||
|
|
||||||
|
//go:embed "html/totp-setup.html"
|
||||||
|
var totpSetupHTML string
|
||||||
var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
|
var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
|
||||||
|
|
||||||
|
//go:embed "html/totp-confirm.html"
|
||||||
|
var totpConfirmHTML string
|
||||||
var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML))
|
var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{
|
var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{
|
||||||
"parseLevel": parseLevel,
|
"parseLevel": func (level log.LogLevel) string {
|
||||||
"titleCase": titleCase,
|
switch level {
|
||||||
"toLower": toLower,
|
case log.LEVEL_INFO:
|
||||||
"prettyTime": prettyTime,
|
return "INFO"
|
||||||
|
case log.LEVEL_WARN:
|
||||||
|
return "WARN"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d?", level)
|
||||||
|
},
|
||||||
|
"titleCase": func (logType string) string {
|
||||||
|
runes := []rune(logType)
|
||||||
|
for i, r := range runes {
|
||||||
|
if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
|
||||||
|
runes[i] = r + ('A' - 'a')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
},
|
||||||
|
"toLower": func (str string) string {
|
||||||
|
return strings.ToLower(str)
|
||||||
|
},
|
||||||
|
"prettyTime": func (t time.Time) string {
|
||||||
|
return t.Format("02 Jan 2006, 15:04:05")
|
||||||
|
},
|
||||||
}).Parse(logsHTML))
|
}).Parse(logsHTML))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/releases.html"
|
||||||
|
var releasesHTML string
|
||||||
var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
releasesHTML,
|
releasesHTML,
|
||||||
componentReleaseHTML,
|
componentReleaseHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
|
//go:embed "html/artists.html"
|
||||||
|
var artistsHTML string
|
||||||
var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
artistsHTML,
|
artistsHTML,
|
||||||
componentArtistHTML,
|
componentArtistHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
|
//go:embed "html/tracks.html"
|
||||||
|
var tracksHTML string
|
||||||
var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
tracksHTML,
|
tracksHTML,
|
||||||
|
|
@ -135,13 +134,21 @@ var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/edit-release.html"
|
||||||
|
var editReleaseHTML string
|
||||||
var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
editReleaseHTML,
|
editReleaseHTML,
|
||||||
componentTrackHTML,
|
componentTrackHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
//go:embed "html/edit-artist.html"
|
||||||
|
var editArtistHTML string
|
||||||
var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
|
var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
|
||||||
|
|
||||||
|
//go:embed "html/edit-track.html"
|
||||||
|
var editTrackHTML string
|
||||||
var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
strings.Join([]string{
|
strings.Join([]string{
|
||||||
editTrackHTML,
|
editTrackHTML,
|
||||||
|
|
@ -151,41 +158,45 @@ var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/components/credit/newcredit.html"
|
||||||
|
var componentNewCreditHTML string
|
||||||
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
|
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
|
||||||
|
|
||||||
|
//go:embed "html/components/credit/addcredit.html"
|
||||||
|
var componentAddCreditHTML string
|
||||||
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
|
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
|
||||||
|
|
||||||
|
//go:embed "html/components/credit/editcredits.html"
|
||||||
|
var componentEditCreditsHTML string
|
||||||
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
|
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/components/link/editlinks.html"
|
||||||
|
var componentEditLinksHTML string
|
||||||
var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
|
var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
|
||||||
|
|
||||||
|
|
||||||
|
//go:embed "html/components/track/newtrack.html"
|
||||||
|
var componentNewTrackHTML string
|
||||||
var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
|
var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
|
||||||
|
|
||||||
|
//go:embed "html/components/track/addtrack.html"
|
||||||
|
var componentAddTrackHTML string
|
||||||
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
|
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
|
||||||
|
|
||||||
|
//go:embed "html/components/track/edittracks.html"
|
||||||
|
var componentEditTracksHTML string
|
||||||
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))
|
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func parseLevel(level log.LogLevel) string {
|
//go:embed "html/blogs.html"
|
||||||
switch level {
|
var blogsHTML string
|
||||||
case log.LEVEL_INFO:
|
//go:embed "html/components/blog/blogpost.html"
|
||||||
return "INFO"
|
var componentBlogPostHTML string
|
||||||
case log.LEVEL_WARN:
|
var BlogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
return "WARN"
|
strings.Join([]string{
|
||||||
}
|
blogsHTML,
|
||||||
return fmt.Sprintf("%d?", level)
|
componentBlogPostHTML,
|
||||||
}
|
}, "\n"),
|
||||||
func titleCase(logType string) string {
|
))
|
||||||
runes := []rune(logType)
|
|
||||||
for i, r := range runes {
|
|
||||||
if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
|
|
||||||
runes[i] = r + ('A' - 'a')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(runes)
|
|
||||||
}
|
|
||||||
func toLower(str string) string {
|
|
||||||
return strings.ToLower(str)
|
|
||||||
}
|
|
||||||
func prettyTime(t time.Time) string {
|
|
||||||
// return t.Format("2006-01-02 15:04:05")
|
|
||||||
// return t.Format("15:04:05, 2 Jan 2006")
|
|
||||||
return t.Format("02 Jan 2006, 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue