commit dfbd40d7a6ef15db10ed805a354c6851349fe93c
parent 95ef2977add2edf137f8cafda5bdb15c59247913
Author: brookjeynes <me@brookjeynes.dev>
Date: Sun, 3 May 2026 14:46:19 +1000
feat: artist lookup in new song page
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
6 files changed, 84 insertions(+), 2 deletions(-)
diff --git a/internal/db/artist.go b/internal/db/artist.go
@@ -3,6 +3,7 @@ package db
import (
"fmt"
"lyrics/internal/domain"
+ "strings"
"time"
)
@@ -24,3 +25,33 @@ func UpsertArtist(e Execer, artist domain.Artist) error {
return nil
}
+
+func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) {
+ artistName = strings.TrimSpace(artistName)
+ if artistName == "" {
+ return nil, nil
+ }
+
+ rows, err := e.Query(`
+ select id, name, bio
+ from artists
+ where name like ? collate nocase
+ order by name
+ limit 20
+ `, "%"+artistName+"%")
+ if err != nil {
+ return nil, fmt.Errorf("failed to query artists: %w", err)
+ }
+ defer rows.Close()
+
+ var results []domain.Artist
+ for rows.Next() {
+ var artist domain.Artist
+ if err := rows.Scan(&artist.ID, &artist.Name, &artist.Bio); err != nil {
+ return nil, fmt.Errorf("failed to scan artist row: %w", err)
+ }
+ results = append(results, artist)
+ }
+
+ return results, rows.Err()
+}
diff --git a/internal/server/router.go b/internal/server/router.go
@@ -17,8 +17,11 @@ 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.Route("/artist", func(router chi.Router) {
+ router.Get("/new", ss.NewArtist)
+ router.Post("/new", ss.NewArtist)
+ router.Get("/search", ss.SearchArtist)
+ })
router.Handle("/static/*", ss.Static())
diff --git a/internal/server/search-artist.go b/internal/server/search-artist.go
@@ -0,0 +1,26 @@
+package server
+
+import (
+ "lyrics/internal/db"
+ artistsearchdatalist "lyrics/internal/server/ui/partials/artist-search-datalist"
+ "net/http"
+)
+
+func (ss *ServerState) SearchArtist(w http.ResponseWriter, r *http.Request) {
+ logger := ss.logger.With("handler", "SearchArtist")
+
+ switch r.Method {
+ case http.MethodGet:
+ artist := r.URL.Query().Get("artist")
+
+ results, err := db.SearchArtist(ss.db, artist)
+ if err != nil {
+ logger.Error("failed to query db for artists", "err", err)
+ return
+ }
+
+ artistsearchdatalist.ArtistSearchDatalist(artistsearchdatalist.ArtistSearchDatalistParams{
+ Artists: results,
+ }).Render(r.Context(), w)
+ }
+}
diff --git a/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.go b/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.go
@@ -0,0 +1,7 @@
+package artistsearchdatalist
+
+import "lyrics/internal/domain"
+
+type ArtistSearchDatalistParams struct {
+ Artists []domain.Artist
+}
diff --git a/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.templ b/internal/server/ui/partials/artist-search-datalist/artist-search-datalist.templ
@@ -0,0 +1,9 @@
+package artistsearchdatalist
+
+templ ArtistSearchDatalist(params ArtistSearchDatalistParams) {
+ <datalist id="artist-suggestions">
+ for _, a := range params.Artists {
+ <option value={ a.Name }></option>
+ }
+ </datalist>
+}
diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ
@@ -26,8 +26,13 @@ templ NewSong(params NewSongParams) {
<label>
<span>artist</span>
<input
+ hx-get="/artist/search"
+ hx-trigger="input changed delay:500ms, load"
+ hx-target="#artist-suggestions"
+ hx-swap="outerHTML"
type="text"
name="artist"
+ list="artist-suggestions"
:required="i === 0"
pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
/>
@@ -41,6 +46,7 @@ templ NewSong(params NewSongParams) {
</li>
</template>
</ol>
+ <datalist id="artist-suggestions"></datalist>
</fieldset>
<label>
title: