diff --git a/admin/blog/blog.go b/admin/blog/blog.go index 294ac66..806f34c 100644 --- a/admin/blog/blog.go +++ b/admin/blog/blog.go @@ -66,7 +66,7 @@ func serveBlogIndex(app *model.AppState) http.Handler { if post.CreatedAt.Year() != collectionYear || i == len(posts) - 1 { if i == len(posts) - 1 { - collectionPosts = append([]*blogPost{&authoredPost}, collectionPosts...) + collectionPosts = append(collectionPosts, &authoredPost) } collections = append(collections, &blogPostGroup{ Year: collectionYear, diff --git a/admin/http.go b/admin/http.go index 55265cb..4bbc9b6 100644 --- a/admin/http.go +++ b/admin/http.go @@ -62,69 +62,39 @@ func adminIndexHandler(app *model.AppState) http.Handler { releases, err := controller.GetAllReleases(app.DB, false, 3, true) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } releaseCount, err := controller.GetReleaseCount(app.DB, false) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } artists, err := controller.GetAllArtists(app.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } artistCount, err := controller.GetArtistCount(app.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } tracks, err := controller.GetOrphanTracks(app.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } trackCount, err := controller.GetTrackCount(app.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - type BlogPost struct { - *model.BlogPost - Author *model.Account - } - blogPosts, err := controller.GetBlogPosts(app.DB, false, 1, 0) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull blog posts: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - var latestBlogPost *BlogPost = nil - if len(blogPosts) > 0 { - author, err := controller.GetAccountByID(app.DB, blogPosts[0].AuthorID) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull latest blog post author %s: %v\n", blogPosts[0].AuthorID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - latestBlogPost = &BlogPost{ - BlogPost: blogPosts[0], - Author: author, - } - } - blogCount, err := controller.GetBlogPostCount(app.DB, false) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull blog post count: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -137,8 +107,6 @@ func adminIndexHandler(app *model.AppState) http.Handler { ArtistCount int Tracks []*model.Track TrackCount int - BlogPost *BlogPost - BlogCount int } err = templates.IndexTemplate.Execute(w, IndexData{ @@ -149,11 +117,9 @@ func adminIndexHandler(app *model.AppState) http.Handler { ArtistCount: artistCount, Tracks: tracks, TrackCount: trackCount, - BlogPost: latestBlogPost, - BlogCount: blogCount, }) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/admin/static/blog.css b/admin/static/blog.css index 72d3cef..a7f281b 100644 --- a/admin/static/blog.css +++ b/admin/static/blog.css @@ -1,11 +1,5 @@ -.blog-collection { - display: flex; - flex-direction: column; - gap: .5em; -} - .blog-collection h2 { - margin: 0 0 0 1em; + margin: .5em 1em; font-size: 1em; text-transform: uppercase; font-weight: 600; diff --git a/admin/static/blog.js b/admin/static/blog.js index 3ce9111..e69de29 100644 --- a/admin/static/blog.js +++ b/admin/static/blog.js @@ -1,25 +0,0 @@ -document.addEventListener('readystatechange', () => { - const newBlogBtn = document.getElementById("create-post"); - if (newBlogBtn) newBlogBtn.addEventListener("click", event => { - event.preventDefault(); - const id = prompt("Enter an ID for this blog post:"); - if (id == null || id == "") return; - - fetch("/api/v1/blog", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({id}) - }).then(res => { - if (res.ok) location = "/admin/blogs/" + id; - else { - res.text().then(err => { - alert(err); - console.error(err); - }); - } - }).catch(err => { - alert("Failed to create release. Check the console for details."); - console.error(err); - }); - }); -}); diff --git a/admin/templates/html/index.html b/admin/templates/html/index.html index 90214f1..af08c09 100644 --- a/admin/templates/html/index.html +++ b/admin/templates/html/index.html @@ -4,7 +4,6 @@ - {{end}} {{define "content"}} @@ -53,18 +52,6 @@ {{block "track" .}}{{end}} {{end}} - -
-
-

Latest Blog Post ({{.BlogCount}} total)

- Create New -
- {{if .BlogPost}} - {{block "blogpost" .BlogPost}}{{end}} - {{else}} -

There are no blog posts.

- {{end}} -
@@ -72,5 +59,4 @@ - {{end}} diff --git a/admin/templates/templates.go b/admin/templates/templates.go index 924188b..6244540 100644 --- a/admin/templates/templates.go +++ b/admin/templates/templates.go @@ -43,7 +43,6 @@ var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( componentReleaseHTML, componentArtistHTML, componentTrackHTML, - componentBlogPostHTML, }, "\n"), )) diff --git a/api/api.go b/api/api.go index 68a08a9..5312a55 100644 --- a/api/api.go +++ b/api/api.go @@ -1,14 +1,14 @@ package api import ( - "context" - "fmt" - "net/http" - "os" - "strings" + "context" + "fmt" + "net/http" + "os" + "strings" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/model" ) func Handler(app *model.AppState) http.Handler { @@ -45,7 +45,7 @@ func Handler(app *model.AppState) http.Handler { http.NotFound(w, r) } })) - artistIndexHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: // GET /api/v1/artist @@ -56,9 +56,7 @@ func Handler(app *model.AppState) http.Handler { default: http.NotFound(w, r) } - }) - mux.Handle("/v1/artist/", artistIndexHandler) - mux.Handle("/v1/artist", artistIndexHandler) + })) // RELEASE ENDPOINTS @@ -89,7 +87,7 @@ func Handler(app *model.AppState) http.Handler { http.NotFound(w, r) } })) - musicIndexHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: // GET /api/v1/music @@ -100,9 +98,7 @@ func Handler(app *model.AppState) http.Handler { default: http.NotFound(w, r) } - }) - mux.Handle("/v1/music/", musicIndexHandler) - mux.Handle("/v1/music", musicIndexHandler) + })) // TRACK ENDPOINTS @@ -133,7 +129,7 @@ func Handler(app *model.AppState) http.Handler { http.NotFound(w, r) } })) - trackIndexHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: // GET /api/v1/track (admin) @@ -144,17 +140,7 @@ func Handler(app *model.AppState) http.Handler { default: http.NotFound(w, r) } - }) - mux.Handle("/v1/track/", trackIndexHandler) - mux.Handle("/v1/track", trackIndexHandler) - - // BLOG ENDPOINTS - - mux.Handle("GET /v1/blog/{id}", ServeBlog(app)) - mux.Handle("PUT /v1/blog/{id}", requireAccount(UpdateBlog(app))) - mux.Handle("DELETE /v1/blog/{id}", requireAccount(DeleteBlog(app))) - mux.Handle("GET /v1/blog", ServeAllBlogs(app)) - mux.Handle("POST /v1/blog", requireAccount(CreateBlog(app))) + })) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, err := getSession(app, r) diff --git a/api/artist.go b/api/artist.go index 322bc5d..01899a6 100644 --- a/api/artist.go +++ b/api/artist.go @@ -99,7 +99,7 @@ func CreateArtist(app *model.AppState) http.Handler { } if artist.ID == "" { - http.Error(w, "Artist ID cannot be blank", http.StatusBadRequest) + http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest) return } if artist.Name == "" { artist.Name = artist.ID } @@ -107,7 +107,7 @@ func CreateArtist(app *model.AppState) http.Handler { err = controller.CreateArtist(app.DB, &artist) if err != nil { if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Artist %s already exists", artist.ID), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) return } fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err) diff --git a/api/blog.go b/api/blog.go deleted file mode 100644 index f935b95..0000000 --- a/api/blog.go +++ /dev/null @@ -1,217 +0,0 @@ -package api - -import ( - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "time" -) - -func ServeAllBlogs(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - onlyVisible := true - if session != nil && session.Account != nil { - onlyVisible = false - } - - posts, err := controller.GetBlogPosts(app.DB, onlyVisible, -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 - } - - type ( - BlogAuthor struct { - ID string `json:"id"` - Avatar string `json:"avatar"` - } - - BlogPost struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Author BlogAuthor `json:"author"` - CreatedAt time.Time `json:"created_at"` - } - ) - resPosts := []*BlogPost{} - - for _, post := range posts { - author, err := controller.GetAccountByID(app.DB, post.AuthorID) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch author for blog post %s: %v\n", post.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - resPosts = append(resPosts, &BlogPost{ - ID: post.ID, - Title: post.Title, - Description: post.Description, - Author: BlogAuthor{ - ID: author.Username, - Avatar: author.AvatarURL.String, - }, - CreatedAt: post.CreatedAt, - }) - } - - err = json.NewEncoder(w).Encode(resPosts) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to serve blog posts: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} - -func ServeBlog(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - privileged := session != nil && session.Account != nil - blogID := r.PathValue("id") - - blog, err := controller.GetBlogPost(app.DB, blogID) - 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 %s: %v\n", blogID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - if !blog.Visible && !privileged { - http.NotFound(w, r) - return - } - - err = json.NewEncoder(w).Encode(blog) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to serve blog post %s: %v\n", blogID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} - -func CreateBlog(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - var blog model.BlogPost - err := json.NewDecoder(r.Body).Decode(&blog) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - if blog.ID == "" { - http.Error(w, "Post ID cannot be empty", http.StatusBadRequest) - return - } - - if blog.Title == "" { blog.Title = blog.ID } - - if !blog.CreatedAt.Equal(time.Unix(0, 0)) { - blog.CreatedAt = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) - } - - blog.AuthorID = session.Account.ID - - err = controller.CreateBlogPost(app.DB, &blog) - if err != nil { - if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Post %s already exists", blog.ID), http.StatusBadRequest) - return - } - fmt.Printf("WARN: Failed to create blog %s: %s\n", blog.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - app.Log.Info(log.TYPE_BLOG, "Blog post \"%s\" created by \"%s\".", blog.ID, session.Account.Username) - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(blog) - if err != nil { - fmt.Printf("WARN: Blog post %s created, but failed to send JSON response: %s\n", blog.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func UpdateBlog(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - blogID := r.PathValue("id") - - blog, err := controller.GetBlogPost(app.DB, blogID) - 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 %s: %v\n", blogID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = json.NewDecoder(r.Body).Decode(blog) - if err != nil { - fmt.Printf("WARN: Failed to update blog %s: %s\n", blog.ID, err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - err = controller.UpdateBlogPost(app.DB, blogID, blog) - if err != nil { - if strings.Contains(err.Error(), "no rows") { - http.NotFound(w, r) - return - } - fmt.Printf("WARN: Failed to update release %s: %s\n", blogID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - - app.Log.Info(log.TYPE_BLOG, "Blog post \"%s\" updated by \"%s\".", blog.ID, session.Account.Username) - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(blog) - if err != nil { - fmt.Printf("WARN: Blog post %s created, but failed to send JSON response: %s\n", blog.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func DeleteBlog(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - blogID := r.PathValue("id") - - rowsAffected, err := controller.DeleteBlogPost(app.DB, blogID) - if err != nil { - fmt.Printf("WARN: Failed to delete blog post %s: %s\n", blogID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if rowsAffected == 0 { - http.NotFound(w, r) - return - } - - app.Log.Info(log.TYPE_BLOG, "Blog post \"%s\" deleted by \"%s\".", blogID, session.Account.Username) - }) -} diff --git a/api/release.go b/api/release.go index f2bf479..69b7f12 100644 --- a/api/release.go +++ b/api/release.go @@ -200,7 +200,7 @@ func CreateRelease(app *model.AppState) http.Handler { } if release.ID == "" { - http.Error(w, "Release ID cannot be empty", http.StatusBadRequest) + http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) return } @@ -216,7 +216,7 @@ func CreateRelease(app *model.AppState) http.Handler { err = controller.CreateRelease(app.DB, &release) if err != nil { if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Release %s already exists", release.ID), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) return } fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err) diff --git a/api/track.go b/api/track.go index ac5b83b..4e48418 100644 --- a/api/track.go +++ b/api/track.go @@ -86,7 +86,7 @@ func CreateTrack(app *model.AppState) http.Handler { } if track.Title == "" { - http.Error(w, "Track title cannot be empty", http.StatusBadRequest) + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) return } @@ -121,7 +121,7 @@ func UpdateTrack(app *model.AppState, track *model.Track) http.Handler { } if track.Title == "" { - http.Error(w, "Track title cannot be empty", http.StatusBadRequest) + http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest) return } diff --git a/controller/blog.go b/controller/blog.go index f0b5742..7fb201e 100644 --- a/controller/blog.go +++ b/controller/blog.go @@ -44,18 +44,6 @@ func GetBlogPosts(db *sqlx.DB, onlyVisible bool, limit int, offset int) ([]*mode return blogs, nil } -func GetBlogPostCount(db *sqlx.DB, onlyVisible bool) (int, error) { - query := "SELECT count(*) FROM blogpost" - if onlyVisible { - query += " WHERE visible=true" - } - - var count int - err := db.Get(&count, query) - - return count, err -} - 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) " + @@ -93,18 +81,3 @@ func UpdateBlogPost(db *sqlx.DB, postID string, post *model.BlogPost) error { return err } - -func DeleteBlogPost(db *sqlx.DB, postID string) (int64, error) { - result, err := db.Exec( - "DELETE FROM blogpost "+ - "WHERE id=$1", - postID, - ) - if err != nil { - return 0, err - } - - rowsAffected, _ := result.RowsAffected() - - return rowsAffected, nil -} diff --git a/model/blog.go b/model/blog.go index c1d44a9..4a2d03a 100644 --- a/model/blog.go +++ b/model/blog.go @@ -11,17 +11,17 @@ import ( type ( BlogPost struct { - ID string `json:"id" db:"id"` - Title string `json:"title" db:"title"` - Description string `json:"description" db:"description"` - Visible bool `json:"visible" db:"visible"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - ModifiedAt sql.NullTime `json:"modified_at" db:"modified_at"` - AuthorID string `json:"author" db:"author"` - Markdown string `json:"markdown" db:"markdown"` - HTML template.HTML `json:"html" db:"html"` - BlueskyActorID *string `json:"bluesky_actor" db:"bluesky_actor"` - BlueskyPostID *string `json:"bluesky_post" db:"bluesky_post"` + 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"` } ) @@ -40,12 +40,12 @@ func (b *BlogPost) GetMonth() string { } func (b *BlogPost) PrintDate() string { - return b.CreatedAt.Format("2 January 2006, 15:04") + 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, 15:04") + return b.ModifiedAt.Time.Format("2 January 2006, 03:04") }