lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

commit cc3450b151f86ab8c167b2c5fa49dcc67110b0f0
parent 9f792b1e1e0130703bef6d61e8bae40fa9866fe8
Author: brookjeynes <me@brookjeynes.dev>
Date:   Mon, 15 Jun 2026 07:02:56 +1000

ui: design new song page

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Minternal/server/new-song.go | 41++++++++++++++++++++++++++---------------
Minternal/server/ui/layouts/base/base.go | 3++-
Minternal/server/ui/layouts/base/base.templ | 10+++++-----
Minternal/server/ui/partials/album-search-results/album-search-results.templ | 44++++++++++++++++++++++++--------------------
Minternal/server/ui/partials/artist-search-results/artist-search-results.templ | 52++++++++++++++++++++++++++++------------------------
Ainternal/server/ui/picker-list.css | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/server/ui/views/album/album.templ | 2+-
Minternal/server/ui/views/artist/artist.templ | 2+-
Minternal/server/ui/views/edit-album/edit-album.templ | 24++++++++++++------------
Minternal/server/ui/views/new-album/new-album.css | 29-----------------------------
Minternal/server/ui/views/new-album/new-album.templ | 18+++++++++---------
Ainternal/server/ui/views/new-song/new-song.css | 34++++++++++++++++++++++++++++++++++
Minternal/server/ui/views/new-song/new-song.templ | 352+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Minternal/server/ui/views/song/song.templ | 2+-
Mscripts/build-assets.sh | 2++
Atasks/done/style_and_improve_ux_for_new_song_form.txt | 3+++
Mtasks/style_and_improve_ux_for_new_song_form.txt | 2+-
17 files changed, 423 insertions(+), 261 deletions(-)

diff --git a/internal/server/new-song.go b/internal/server/new-song.go @@ -13,14 +13,15 @@ import ( ) type NewSongForm struct { - ArtistID []domain.ArtistID `schema:"artist-id"` - Title string `schema:"title"` - LatinTitle string `schema:"latin-title"` - AlbumID []domain.AlbumID `schema:"album-id"` - Genres []string `schema:"genres"` - Lyrics string `schema:"lyrics"` - FeaturingID []domain.ArtistID `schema:"featuring-id"` - ReleaseDate string `schema:"release-date"` + ArtistID []domain.ArtistID `schema:"artist-id"` + Title string `schema:"title"` + LatinTitle string `schema:"latin-title"` + AlbumID []domain.AlbumID `schema:"album-id"` + Genres []string `schema:"genres"` + Lyrics string `schema:"lyrics"` + FeaturingID []domain.ArtistID `schema:"featuring-id"` + ReleaseDate string `schema:"release-date"` + MusicBrainzRecordingID domain.MusicBrainzID `schema:"musicbrainz-recording-id"` } func parseNewSongForm(form NewSongForm) (domain.NewSong, error) { @@ -126,14 +127,24 @@ func parseNewSongForm(form NewSongForm) (domain.NewSong, error) { minDate.Format("2006-01-02"), maxDate.Format("2006-01-02")) } + // musicbrainz recording id + var musicbrainzRecordingID *domain.MusicBrainzID + if form.MusicBrainzRecordingID != "" { + if !domain.MusicBrainzIDIsValid(string(form.MusicBrainzRecordingID)) { + return song, fmt.Errorf("'%s' must be a valid MusicBrainz ID", form.MusicBrainzRecordingID) + } + musicbrainzRecordingID = &form.MusicBrainzRecordingID + } + song = domain.NewSong{ - Title: form.Title, - LatinTitle: latinTitle, - Lyrics: form.Lyrics, - ReleaseDate: releaseDate, - Genres: genres, - Albums: albums, - Artists: artists, + Title: form.Title, + LatinTitle: latinTitle, + Lyrics: form.Lyrics, + ReleaseDate: releaseDate, + Genres: genres, + Albums: albums, + Artists: artists, + MusicBrainzRecordingID: musicbrainzRecordingID, } return song, nil diff --git a/internal/server/ui/layouts/base/base.go b/internal/server/ui/layouts/base/base.go @@ -1,5 +1,6 @@ package base type BaseParams struct { - Title string + Title string + PageStylesheets []string } diff --git a/internal/server/ui/layouts/base/base.templ b/internal/server/ui/layouts/base/base.templ @@ -31,13 +31,13 @@ templ Base(params BaseParams) { <script src="/static/js/lucide-utils.js"></script> <script src="/static/js/toast.js" defer></script> <script src="/static/js/string-utils.js" defer></script> - <link rel="stylesheet" href="/static/style.css" type="text/css"/> - <link rel="stylesheet" href="/static/song.css" type="text/css"/> - <link rel="stylesheet" href="/static/artist.css" type="text/css"/> - <link rel="stylesheet" href="/static/album.css" type="text/css"/> - <link rel="stylesheet" href="/static/new-album.css" type="text/css"/> <link rel="stylesheet" href="/static/minimal.css" type="text/css"/> + <link rel="stylesheet" href="/static/style.css" type="text/css"/> + <link rel="stylesheet" href="/static/picker-list.css" type="text/css"/> <link rel="stylesheet" href="/static/toast.css" type="text/css"/> + for _, stylesheet := range params.PageStylesheets { + <link rel="stylesheet" href={ "/static/" + stylesheet } type="text/css"/> + } <title>{ params.Title }</title> </head> <body> diff --git a/internal/server/ui/partials/album-search-results/album-search-results.templ b/internal/server/ui/partials/album-search-results/album-search-results.templ @@ -4,28 +4,32 @@ import "fmt" templ AlbumSearchResults(params AlbumSearchResultsParams) { if len(params.Albums) == 0 { - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> } else { - <fieldset role="radiogroup"> + <fieldset class="picker-options" role="radiogroup"> <legend>select album</legend> - for _, album := range params.Albums { - {{ - label := album.Title - if album.LatinTitle != nil { - label = fmt.Sprintf("%s (%s)", album.Title, *album.LatinTitle) - } - }} - <label> - <input - type="radio" - value={ fmt.Sprintf("%d", album.ID) } - data-name={ album.Title } - required - @change="selected = true; albumId = $event.target.value; albumTitle = $event.target.dataset.name" - /> - { label } - </label> - } + <ul> + for _, album := range params.Albums { + {{ + label := album.Title + if album.LatinTitle != nil { + label = fmt.Sprintf("%s (%s)", album.Title, *album.LatinTitle) + } + }} + <li> + <label> + <input + type="radio" + value={ fmt.Sprintf("%d", album.ID) } + data-name={ album.Title } + required + @change="selected = true; albumId = $event.target.value; albumTitle = $event.target.dataset.name" + /> + { label } + </label> + </li> + } + </ul> </fieldset> } } diff --git a/internal/server/ui/partials/artist-search-results/artist-search-results.templ b/internal/server/ui/partials/artist-search-results/artist-search-results.templ @@ -4,32 +4,36 @@ import "fmt" templ ArtistSearchResults(params ArtistSearchResultsParams) { if len(params.Artists) == 0 { - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> } else { - <fieldset role="radiogroup"> + <fieldset class="picker-options" role="radiogroup"> <legend>select artist</legend> - for _, artist := range params.Artists { - {{ - label := artist.Name - if artist.LatinName != nil && artist.Disambiguation != nil { - label = fmt.Sprintf("%s (%s, %s)", artist.Name, *artist.LatinName, *artist.Disambiguation) - } else if artist.LatinName != nil { - label = fmt.Sprintf("%s (%s)", artist.Name, *artist.LatinName) - } else if artist.Disambiguation != nil { - label = fmt.Sprintf("%s (%s)", artist.Name, *artist.Disambiguation) - } - }} - <label> - <input - type="radio" - value={ fmt.Sprintf("%d", artist.ID) } - data-name={ artist.Name } - required - @change="selected = true; artistId = $event.target.value; artistName = $event.target.dataset.name" - /> - { label } - </label> - } + <ul> + for _, artist := range params.Artists { + {{ + label := artist.Name + if artist.LatinName != nil && artist.Disambiguation != nil { + label = fmt.Sprintf("%s (%s, %s)", artist.Name, *artist.LatinName, *artist.Disambiguation) + } else if artist.LatinName != nil { + label = fmt.Sprintf("%s (%s)", artist.Name, *artist.LatinName) + } else if artist.Disambiguation != nil { + label = fmt.Sprintf("%s (%s)", artist.Name, *artist.Disambiguation) + } + }} + <li> + <label> + <input + type="radio" + value={ fmt.Sprintf("%d", artist.ID) } + data-name={ artist.Name } + required + @change="selected = true; artistId = $event.target.value; artistName = $event.target.dataset.name" + /> + { label } + </label> + </li> + } + </ul> </fieldset> } } diff --git a/internal/server/ui/picker-list.css b/internal/server/ui/picker-list.css @@ -0,0 +1,64 @@ +.picker-list { + padding-left: 1.5rem; + + .picker-row { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .picker-main { + flex: 1; + min-width: 0; + } + + .picker-actions { + display: flex; + gap: 0.5rem; + align-items: center; + margin-top: 0.5rem; + } + + .picker-selected-item { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin: 0.5rem 0; + } + + .picker-options { + margin: 0.5rem 0 1rem; + padding-left: 0.75rem; + border-left: 1px solid var(--light-gray); + + legend { + color: var(--secondary); + font-size: 14px; + } + + ul { + list-style: none; + padding-left: 0; + margin: 0; + } + + li+li { + margin-top: 0.35rem; + } + + label { + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + cursor: pointer; + } + + input { + margin: 0; + } + + label:has(input:checked) { + color: var(--primary); + } + } +} diff --git a/internal/server/ui/views/album/album.templ b/internal/server/ui/views/album/album.templ @@ -7,7 +7,7 @@ import ( ) templ Album(params AlbumParams) { - @base.Base(base.BaseParams{Title: params.Album.Title}) { + @base.Base(base.BaseParams{Title: params.Album.Title, PageStylesheets: []string{"album.css"}}) { <article class="album-layout"> <section class="album-content"> <header> diff --git a/internal/server/ui/views/artist/artist.templ b/internal/server/ui/views/artist/artist.templ @@ -7,7 +7,7 @@ import ( ) templ Artist(params ArtistParams) { - @base.Base(base.BaseParams{Title: params.Artist.Name}) { + @base.Base(base.BaseParams{Title: params.Artist.Name, PageStylesheets: []string{"artist.css"}}) { <article> <header> <hgroup> diff --git a/internal/server/ui/views/edit-album/edit-album.templ b/internal/server/ui/views/edit-album/edit-album.templ @@ -51,28 +51,28 @@ templ EditAlbum(params EditAlbumParams) { x-data={ fmt.Sprintf("{ items: %s, uid: %d }", editAlbumArtistsJSON(params.Album.Artists), len(params.Album.Artists)) } > <legend>Artists</legend> - <ol class="artist-picker-list"> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.uid"> <li x-data="item" x-init="htmx.process($el); initLucideIcons()" > - <div class="artist-picker-row"> - <div class="artist-picker-main"> + <div class="picker-row"> + <div class="picker-main"> <label x-show="!item.selected"> <span>artist:</span> <input type="search" hx-get="/artist/search?format=suggest" hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" + hx-target="next .picker-options" hx-swap="outerHTML" name="artist" autocomplete="off" :required="!item.selected" /> </label> - <div class="selected-picker-item" x-show="item.selected"> + <div class="picker-selected-item" x-show="item.selected"> <strong x-text="item.artistName"></strong> <button type="button" @@ -83,7 +83,7 @@ templ EditAlbum(params EditAlbumParams) { </button> </div> </div> - <div class="artist-picker-actions"> + <div class="picker-actions"> <button type="button" aria-label="Remove" @@ -111,9 +111,9 @@ templ EditAlbum(params EditAlbumParams) { </div> </div> <div x-show="!item.selected"> - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> </div> - <input type="hidden" :name="item.selected ? 'artist-id' : ''" :value="item.artistId"/> + <input type="hidden" name="artist-id" :value="item.artistId"/> </li> </template> </ol> @@ -146,14 +146,14 @@ templ EditAlbum(params EditAlbumParams) { if len(params.Album.Songs) == 0 { <p class="section-label -muted">No songs yet.</p> } else { - <ol class="artist-picker-list"> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-init="htmx.process($el); initLucideIcons()"> - <div class="artist-picker-row"> - <div class="artist-picker-main"> + <div class="picker-row"> + <div class="picker-main"> <strong x-text="item.title"></strong> </div> - <div class="artist-picker-actions"> + <div class="picker-actions"> <button type="button" aria-label="Move up" diff --git a/internal/server/ui/views/new-album/new-album.css b/internal/server/ui/views/new-album/new-album.css @@ -13,32 +13,3 @@ margin-bottom: 0.5rem; } } - -.artist-picker-list { - padding-left: 1.5rem; -} - -.artist-picker-row { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.artist-picker-main { - flex: 1; - min-width: 0; -} - -.artist-picker-actions { - display: flex; - gap: 0.5rem; - align-items: center; - margin-top: 0.5rem; -} - -.selected-picker-item { - display: flex; - align-items: baseline; - gap: 0.75rem; - margin: 0.5rem 0; -} diff --git a/internal/server/ui/views/new-album/new-album.templ b/internal/server/ui/views/new-album/new-album.templ @@ -8,7 +8,7 @@ import ( ) templ NewAlbum(params NewAlbumParams) { - @base.Base(base.BaseParams{Title: "new album"}) { + @base.Base(base.BaseParams{Title: "new album", PageStylesheets: []string{"new-album.css"}}) { <h1>Add an album</h1> <form class="album-form" @@ -45,28 +45,28 @@ templ NewAlbum(params NewAlbumParams) { <section id="similar-albums" class="similar-albums" aria-label="Similar albums"></section> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> <legend>Artists</legend> - <ol class="artist-picker-list"> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-data="{ selected: false, artistId: '', artistName: '' }" x-init="htmx.process($el); initLucideIcons()" > - <div class="artist-picker-row"> - <div class="artist-picker-main"> + <div class="picker-row"> + <div class="picker-main"> <label x-show="!selected"> <span>artist:</span> <input type="search" hx-get="/artist/search?format=suggest" hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" + hx-target="next .picker-options" hx-swap="outerHTML" name="artist" autocomplete="off" :required="!selected" /> </label> - <div class="selected-picker-item" x-show="selected"> + <div class="picker-selected-item" x-show="selected"> <strong x-text="artistName"></strong> <button type="button" @@ -77,7 +77,7 @@ templ NewAlbum(params NewAlbumParams) { </button> </div> </div> - <div class="artist-picker-actions"> + <div class="picker-actions"> <button type="button" aria-label="Remove" @@ -105,9 +105,9 @@ templ NewAlbum(params NewAlbumParams) { </div> </div> <div x-show="!selected"> - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> </div> - <input type="hidden" :name="selected ? 'artist-id' : ''" :value="artistId"/> + <input type="hidden" name="artist-id" :value="artistId"/> </li> </template> </ol> diff --git a/internal/server/ui/views/new-song/new-song.css b/internal/server/ui/views/new-song/new-song.css @@ -0,0 +1,34 @@ +.song-form { + max-width: 42rem; + + fieldset { + margin: 1.5rem 0; + } + + textarea { + min-height: 18rem; + margin-top: 0.5rem; + border: 1px solid var(--dark-gray); + width: 100%; + padding: 0.75rem; + resize: vertical; + } +} + +.genre-options { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0.75rem; + margin-top: 0.5rem; + + label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + cursor: pointer; + + &:has(input:checked) { + color: var(--primary); + } + } +} diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ @@ -8,17 +8,16 @@ import ( ) templ NewSong(params NewSongParams) { - @base.Base(base.BaseParams{Title: "new song"}) { + @base.Base(base.BaseParams{Title: "new song", PageStylesheets: []string{"new-song.css"}}) { <h1>Add a song</h1> <form + class="song-form" hx-post="/song/new" hx-swap="none" hx-disable-elt="#add-song-btn" > <fieldset x-data="{ showLatinTitle: false }"> - <legend> - title - </legend> + <legend>Title</legend> <label> title: <input @@ -40,182 +39,251 @@ templ NewSong(params NewSongParams) { </label> </fieldset> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> - <legend> - albums - <button type="button" @click="items.push({ id: uid++ })">+ add</button> - </legend> - <ol> + <legend>Albums</legend> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-data="{ selected: false, albumId: '', albumTitle: '' }" x-init="htmx.process($el); initLucideIcons()" > - <label x-show="!selected"> - <span>album</span> - <input - type="search" - hx-get="/album/search?format=suggest" - hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" - hx-swap="outerHTML" - name="album" - autocomplete="off" - :required="!selected && id > 0" - /> - </label> - <p x-show="selected"> - <span x-text="albumTitle"></span> - <button - type="button" - @click="selected = false; albumId = ''; albumTitle = ''" - > - change - </button> - </p> - <button - type="button" - aria-label="Remove" - x-show="items.length > 1" - @click="items.splice(i, 1)" - > - <i data-lucide="x"></i> - </button> + <div class="picker-row"> + <div class="picker-main"> + <label x-show="!selected"> + <span>album:</span> + <input + type="search" + hx-get="/album/search?format=suggest" + hx-trigger="input changed delay:500ms" + hx-target="next .picker-options" + hx-swap="outerHTML" + name="album" + autocomplete="off" + :required="!selected && id > 0" + /> + </label> + <div class="picker-selected-item" x-show="selected"> + <strong x-text="albumTitle"></strong> + <button + type="button" + class="-secondary" + @click="selected = false; albumId = ''; albumTitle = ''" + > + change + </button> + </div> + </div> + <div class="picker-actions"> + <button + type="button" + aria-label="Remove" + x-show="items.length > 1" + @click="items.splice(i, 1)" + > + <i data-lucide="x"></i> + </button> + </div> + </div> <div x-show="!selected"> - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> </div> <input type="hidden" name="album-id" :value="albumId"/> </li> </template> </ol> + <button class="-secondary" type="button" @click="items.push({ id: uid++ })"> + add additional album + </button> </fieldset> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> - <legend> - artists - <button type="button" @click="items.push({ id: uid++ })">+ add</button> - </legend> - <ol> + <legend>Artists</legend> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-data="{ selected: false, artistId: '', artistName: '' }" x-init="htmx.process($el); initLucideIcons()" > - <label x-show="!selected"> - <span>artist</span> - <input - type="search" - hx-get="/artist/search?format=suggest" - hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" - hx-swap="outerHTML" - name="artist" - autocomplete="off" - :required="!selected" - /> - </label> - <p x-show="selected"> - <span x-text="artistName"></span> - <button - type="button" - @click="selected = false; artistId = ''; artistName = ''" - > - change - </button> - </p> - <button - type="button" - aria-label="Remove" - x-show="items.length > 1" - @click="items.splice(i, 1)" - > - <i data-lucide="x"></i> - </button> + <div class="picker-row"> + <div class="picker-main"> + <label x-show="!selected"> + <span>artist:</span> + <input + type="search" + hx-get="/artist/search?format=suggest" + hx-trigger="input changed delay:500ms" + hx-target="next .picker-options" + hx-swap="outerHTML" + name="artist" + autocomplete="off" + :required="!selected" + /> + </label> + <div class="picker-selected-item" x-show="selected"> + <strong x-text="artistName"></strong> + <button + type="button" + class="-secondary" + @click="selected = false; artistId = ''; artistName = ''" + > + change + </button> + </div> + </div> + <div class="picker-actions"> + <button + type="button" + aria-label="Remove" + x-show="items.length > 1" + @click="items.splice(i, 1)" + > + <i data-lucide="x"></i> + </button> + <button + type="button" + aria-label="Move up" + x-show="selected && items.length > 1 && i >= 1" + @click="[items[i - 1], items[i]] = [items[i], items[i - 1]]" + > + <i data-lucide="chevron-up"></i> + </button> + <button + type="button" + aria-label="Move down" + x-show="selected && items.length > 1 && i + 1 < items.length" + @click="[items[i + 1], items[i]] = [items[i], items[i + 1]]" + > + <i data-lucide="chevron-down"></i> + </button> + </div> + </div> <div x-show="!selected"> - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> </div> <input type="hidden" name="artist-id" :value="artistId"/> </li> </template> </ol> + <button class="-secondary" type="button" @click="items.push({ id: uid++ })"> + add additional artist + </button> </fieldset> - <fieldset> - <legend>genres</legend> - for _, genre := range domain.Genres { - <label> - <input type="checkbox" name="genres" value={ string(genre) }/> - <span>{ string(genre) }</span> - </label> - } - </fieldset> - <label> - lyrics: - <textarea - name="lyrics" - rows="20" - spellcheck - ></textarea> - </label> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> - <legend> - featuring - <button type="button" @click="items.push({ id: uid++ })">+ add</button> - </legend> - <ol> + <legend>Featuring</legend> + <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-data="{ selected: false, artistId: '', artistName: '' }" x-init="htmx.process($el); initLucideIcons()" > - <label x-show="!selected"> - <span>featuring</span> - <input - type="search" - hx-get="/artist/search?format=suggest" - hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" - hx-swap="outerHTML" - name="featuring" - autocomplete="off" - :required="i !== 0 && !selected" - /> - </label> - <p x-show="selected"> - <span x-text="artistName"></span> - <button - type="button" - @click="selected = false; artistId = ''; artistName = ''" - > - change - </button> - </p> - <button - type="button" - aria-label="Remove" - x-show="items.length > 1" - @click="items.splice(i, 1)" - > - <i data-lucide="x"></i> - </button> + <div class="picker-row"> + <div class="picker-main"> + <label x-show="!selected"> + <span>artist:</span> + <input + type="search" + hx-get="/artist/search?format=suggest" + hx-trigger="input changed delay:500ms" + hx-target="next .picker-options" + hx-swap="outerHTML" + name="artist" + autocomplete="off" + /> + </label> + <div class="picker-selected-item" x-show="selected"> + <strong x-text="artistName"></strong> + <button + type="button" + class="-secondary" + @click="selected = false; artistId = ''; artistName = ''" + > + change + </button> + </div> + </div> + <div class="picker-actions"> + <button + type="button" + aria-label="Remove" + x-show="items.length > 1" + @click="items.splice(i, 1)" + > + <i data-lucide="x"></i> + </button> + <button + type="button" + aria-label="Move up" + x-show="selected && items.length > 1 && i >= 1" + @click="[items[i - 1], items[i]] = [items[i], items[i - 1]]" + > + <i data-lucide="chevron-up"></i> + </button> + <button + type="button" + aria-label="Move down" + x-show="selected && items.length > 1 && i + 1 < items.length" + @click="[items[i + 1], items[i]] = [items[i], items[i + 1]]" + > + <i data-lucide="chevron-down"></i> + </button> + </div> + </div> <div x-show="!selected"> - <fieldset role="radiogroup" x-show="false"></fieldset> + <fieldset class="picker-options" role="radiogroup" x-show="false"></fieldset> </div> <input type="hidden" name="featuring-id" :value="artistId"/> </li> </template> </ol> + <button class="-secondary" type="button" @click="items.push({ id: uid++ })"> + add additional artist + </button> + </fieldset> + <fieldset> + <legend>Genres</legend> + <div class="genre-options"> + for _, genre := range domain.Genres { + <label> + <input type="checkbox" name="genres" value={ string(genre) }/> + <span>{ string(genre) }</span> + </label> + } + </div> + </fieldset> + <fieldset> + <legend>Lyrics</legend> + <label> + lyrics: + <textarea name="lyrics" rows="16" spellcheck></textarea> + </label> + </fieldset> + <fieldset> + <legend>Release</legend> + <label> + release date: + <input + name="release-date" + type="date" + min="1000-01-01" + max={ fmt.Sprintf("%s", time.Now().AddDate(3, 0, 0).Format("2006-01-02")) } + required + /> + </label> + </fieldset> + <fieldset> + <legend>MusicBrainz</legend> + <label title="External identifier used for metadata lookups such as cover art. This app does not contact MusicBrainz when saving."> + Recording ID (optional): + <input + name="musicbrainz-recording-id" + type="text" + pattern={ domain.IsValidMusicBrainzIDPattern } + placeholder="e32a4ae3-67d1-4541-84d8-de6ab2d7dec1" + /> + <i data-lucide="info"></i> + </label> </fieldset> - <label> - release date: - <input - name="release-date" - type="date" - min="1000-01-01" - max={ fmt.Sprintf("%s", time.Now().AddDate(3, 0, 0).Format("2006-01-02")) } - required - /> - </label> - <button id="add-song-btn" type="submit"> - add song - </button> + <footer aria-label="Form actions"> + <button id="add-song-btn" class="-primary" type="submit">add song</button> + </footer> </form> <script src="/static/js/new-song.js" defer></script> } diff --git a/internal/server/ui/views/song/song.templ b/internal/server/ui/views/song/song.templ @@ -8,7 +8,7 @@ import ( ) templ Song(params SongParams) { - @base.Base(base.BaseParams{Title: params.Song.Title}) { + @base.Base(base.BaseParams{Title: params.Song.Title, PageStylesheets: []string{"song.css"}}) { <article class="song-layout"> <section class="song-content"> <header> diff --git a/scripts/build-assets.sh b/scripts/build-assets.sh @@ -18,6 +18,8 @@ minify internal/server/ui/views/song/song.css -o static/files/song.css minify internal/server/ui/views/artist/artist.css -o static/files/artist.css minify internal/server/ui/views/album/album.css -o static/files/album.css minify internal/server/ui/views/new-album/new-album.css -o static/files/new-album.css +minify internal/server/ui/views/new-song/new-song.css -o static/files/new-song.css +minify internal/server/ui/picker-list.css -o static/files/picker-list.css minify static/minimal.css -o static/files/minimal.css minify internal/server/ui/js/string-utils.js -o static/files/js/string-utils.js diff --git a/tasks/done/style_and_improve_ux_for_new_song_form.txt b/tasks/done/style_and_improve_ux_for_new_song_form.txt @@ -0,0 +1,3 @@ +style and improve ux for new song form @ux +song + +this should follow the general style of the [new|edit] album page diff --git a/tasks/style_and_improve_ux_for_new_song_form.txt b/tasks/style_and_improve_ux_for_new_song_form.txt @@ -1,4 +1,4 @@ style and improve ux for new song form @ux +song id: mkmuoutrvtxkyxxs - + this should follow the general style of the [new|edit] album page