commit 005b97f07e4fe0da72cd00b13616b03dc74c7c24
parent 15e95722dcb0bf2b93123299e814dd4d9e4c1fdf
Author: brookjeynes <me@brookjeynes.dev>
Date: Sun, 3 May 2026 14:21:06 +1000
feat(ui): add create artist page
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
5 files changed, 117 insertions(+), 3 deletions(-)
diff --git a/internal/server/new-artist.go b/internal/server/new-artist.go
@@ -0,0 +1,70 @@
+package server
+
+import (
+ "fmt"
+ "lyrics/internal/domain"
+ "lyrics/internal/server/htmx"
+ newartist "lyrics/internal/server/ui/views/new-artist"
+ "net/http"
+)
+
+type NewArtistForm struct {
+ Name string `schema:"name"`
+ Bio string `schema:"bio"`
+}
+
+func parseNewArtistForm(form NewArtistForm) (domain.Artist, error) {
+ var artist domain.Artist
+
+ // name
+ if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Name) {
+ return artist, fmt.Errorf("Artist name must not be empty or have leading/trailing whitespace")
+ }
+
+ // bio
+ if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Bio) {
+ return artist, fmt.Errorf("Artist biography must not be empty or have leading/trailing whitespace")
+ }
+
+ artist = domain.Artist{
+ Name: form.Name,
+ Bio: form.Bio,
+ }
+
+ return artist, nil
+}
+
+func (ss *ServerState) NewArtist(w http.ResponseWriter, r *http.Request) {
+ logger := ss.logger.With("handler", "NewArtist")
+
+ switch r.Method {
+ case http.MethodGet:
+ newartist.NewArtist(newartist.NewArtistParams{}).Render(r.Context(), w)
+ case http.MethodPost:
+ err := r.ParseForm()
+ if err != nil {
+ logger.Error("failed to parse http form", "err", err)
+ htmx.HxError(w, http.StatusBadRequest, "Failed to add artist.")
+ return
+ }
+
+ var newArtistForm NewArtistForm
+ err = decoder.Decode(&newArtistForm, r.PostForm)
+ if err != nil {
+ logger.Error("failed to decode http form", "err", err)
+ htmx.HxError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ newArtist, err := parseNewArtistForm(newArtistForm)
+ if err != nil {
+ logger.Error("failed to parse new artist form", "err", err)
+ htmx.HxError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ logger.Debug("added artist", "artist", newArtist)
+
+ htmx.HxRedirect(w, "/")
+ }
+}
diff --git a/internal/server/new-song.go b/internal/server/new-song.go
@@ -36,7 +36,7 @@ func parseNewSongForm(form NewSongForm) (domain.Song, error) {
return song, fmt.Errorf("Title must not be empty or have leading/trailing whitespace")
}
- if form.Lyrics == "" {
+ if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Lyrics) {
return song, fmt.Errorf("Lyrics must not be empty")
}
@@ -109,7 +109,7 @@ func (ss *ServerState) NewSong(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
logger.Error("failed to parse http form", "err", err)
- htmx.HxError(w, http.StatusBadRequest, "Failed to add new song.")
+ htmx.HxError(w, http.StatusBadRequest, "Failed to add song.")
return
}
@@ -128,7 +128,7 @@ func (ss *ServerState) NewSong(w http.ResponseWriter, r *http.Request) {
return
}
- logger.Debug("added new song", "song", newSong)
+ logger.Debug("added song", "song", newSong)
htmx.HxRedirect(w, "/")
}
diff --git a/internal/server/router.go b/internal/server/router.go
@@ -17,6 +17,9 @@ func (ss *ServerState) Router() http.Handler {
router.Get("/new", ss.NewSong)
router.Post("/new", ss.NewSong)
+ router.Get("/artist/new", ss.NewArtist)
+ router.Post("/artist/new", ss.NewArtist)
+
router.Handle("/static/*", ss.Static())
return router
diff --git a/internal/server/ui/views/new-artist/new-artist.go b/internal/server/ui/views/new-artist/new-artist.go
@@ -0,0 +1,3 @@
+package newartist
+
+type NewArtistParams struct{}
diff --git a/internal/server/ui/views/new-artist/new-artist.templ b/internal/server/ui/views/new-artist/new-artist.templ
@@ -0,0 +1,38 @@
+package newartist
+
+import (
+ "lyrics/internal/domain"
+ "lyrics/internal/server/ui/layouts/base"
+)
+
+templ NewArtist(params NewArtistParams) {
+ @base.Base(base.BaseParams{Title: "new artist"}) {
+ <h1>Add an artist</h1>
+ <form
+ hx-post="/artist/new"
+ hx-swap="none"
+ hx-disable-elt="#add-artist-btn"
+ >
+ <label>
+ name:
+ <input
+ name="name"
+ type="text"
+ pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
+ required
+ />
+ </label>
+ <label>
+ biography:
+ <textarea
+ name="biography"
+ rows="20"
+ spellcheck
+ ></textarea>
+ </label>
+ <button id="add-artist-btn" type="submit">
+ add artist
+ </button>
+ </form>
+ }
+}