- Fixed song add form (3 HTMX bugs: reset on typeahead, wrong query param, artist-only find-or-create) - Added Played checkbox to add form; inline edit (✎/Save/Cancel) for setlist rows - View fixes: date format, location name instead of ID in jam list, notes on location detail, song count column, location name links to detail - Title typeahead + cross-filtering (artist↔title filter each other) - Fixed datalist accumulation bug: HTMX 2.0 inherits hx-swap from parent; added explicit hx-swap="innerHTML" on both inputs - Setlist sort by id (ULID = insertion order); created rejected by PocketBase - Location delete guard: OnRecordDelete hook blocks if jams exist; FK enforcement is app-level not SQLite-level Loose ends to pick up: - Focus-after-add not user-confirmed yet (setTimeout approach) - jams.participants and setlist.position still in DB schema but unused in UI (optional migration to clean up) - Logout flow and dashboard ranking not browser-tested
240 lines
6.5 KiB
Text
240 lines
6.5 KiB
Text
package views
|
|
|
|
import "github.com/pocketbase/pocketbase/core"
|
|
|
|
// JamList renders the list of all jams.
|
|
templ JamList(jams []*core.Record, locationNames map[string]string, songCounts map[string]SongCount) {
|
|
@Layout("Jams") {
|
|
<div class="level">
|
|
<div class="level-left">
|
|
<h1 class="title">Jams</h1>
|
|
</div>
|
|
<div class="level-right">
|
|
<a class="button is-primary" href="/jams/new">+ New Jam</a>
|
|
</div>
|
|
</div>
|
|
if len(jams) == 0 {
|
|
<p class="has-text-grey">No jams logged yet.</p>
|
|
} else {
|
|
<table class="table is-fullwidth is-striped is-hoverable">
|
|
<thead>
|
|
<tr><th>Date</th><th>Location</th><th>Songs</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, j := range jams {
|
|
<tr>
|
|
<td><a href={ templ.SafeURL("/jams/" + j.Id) }>{ dateOnly(j.GetString("date")) }</a></td>
|
|
<td>{ locationNames[j.GetString("location")] }</td>
|
|
<td>{ songCountDisplay(songCounts[j.Id]) }</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
}
|
|
}
|
|
|
|
// JamNew renders the new-jam creation form.
|
|
templ JamNew(locations []*core.Record) {
|
|
@Layout("New Jam") {
|
|
<h1 class="title">New Jam</h1>
|
|
<form method="POST" action="/jams" class="box" style="max-width:480px">
|
|
<div class="field">
|
|
<label class="label">Date</label>
|
|
<div class="control">
|
|
<input class="input" type="date" name="date" required/>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="label">Location</label>
|
|
<div class="control">
|
|
<div class="select is-fullwidth">
|
|
<select name="location" required>
|
|
<option value="">— select —</option>
|
|
for _, loc := range locations {
|
|
<option value={ loc.Id }>{ loc.GetString("name") }</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="label">Notes</label>
|
|
<div class="control">
|
|
<textarea class="textarea" name="notes" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="field mt-4">
|
|
<div class="control">
|
|
<button class="button is-primary" type="submit">Create Jam</button>
|
|
<a class="button ml-2" href="/jams">Cancel</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
}
|
|
}
|
|
|
|
// SetlistRow is the view model for a single row in the setlist table.
|
|
type SetlistRow struct {
|
|
ID string
|
|
Artist string
|
|
Title string
|
|
Played bool
|
|
}
|
|
|
|
// JamDetail renders a single jam and its setlist.
|
|
templ JamDetail(jam *core.Record, location *core.Record, rows []SetlistRow) {
|
|
@Layout("Jam — " + dateOnly(jam.GetString("date"))) {
|
|
<div class="level">
|
|
<div class="level-left">
|
|
<h1 class="title">{ dateOnly(jam.GetString("date")) } — <a href={ templ.SafeURL("/locations/" + location.Id) }>{ location.GetString("name") }</a></h1>
|
|
</div>
|
|
</div>
|
|
<div class="columns">
|
|
<div class="column is-two-thirds">
|
|
<h2 class="subtitle">Setlist</h2>
|
|
<table class="table is-fullwidth" id="setlist">
|
|
<thead>
|
|
<tr><th>Artist</th><th>Title</th><th>Played</th><th></th></tr>
|
|
</thead>
|
|
<tbody id="setlist-rows">
|
|
for _, row := range rows {
|
|
@SetlistRowFrag(jam.Id, row)
|
|
}
|
|
</tbody>
|
|
</table>
|
|
<div class="mt-4" id="song-add-form">
|
|
@SongAddForm(jam.Id)
|
|
</div>
|
|
</div>
|
|
<div class="column">
|
|
if jam.GetString("notes") != "" {
|
|
<p><strong>Notes:</strong> { jam.GetString("notes") }</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
// SetlistRowFrag renders a single setlist row (used for HTMX swap).
|
|
templ SetlistRowFrag(jamID string, row SetlistRow) {
|
|
<tr id={ "row-" + row.ID }>
|
|
<td>{ row.Artist }</td>
|
|
<td>{ row.Title }</td>
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
if row.Played {
|
|
checked
|
|
}
|
|
hx-patch={ "/setlist/" + row.ID + "/played" }
|
|
hx-target={ "#row-" + row.ID }
|
|
hx-swap="outerHTML"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<button
|
|
class="button is-small is-ghost"
|
|
hx-get={ "/setlist/" + row.ID + "/edit" }
|
|
hx-target={ "#row-" + row.ID }
|
|
hx-swap="outerHTML"
|
|
>✎</button>
|
|
<button
|
|
class="delete"
|
|
hx-delete={ "/setlist/" + row.ID }
|
|
hx-target={ "#row-" + row.ID }
|
|
hx-swap="outerHTML"
|
|
hx-confirm="Remove this song from the setlist?"
|
|
></button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
|
|
// SetlistEditFrag renders a setlist row as an inline edit form.
|
|
templ SetlistEditFrag(row SetlistRow) {
|
|
<tr id={ "row-" + row.ID }>
|
|
<td><input class="input is-small" type="text" name="artist" value={ row.Artist } required/></td>
|
|
<td><input class="input is-small" type="text" name="title" value={ row.Title } required/></td>
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
name="played"
|
|
if row.Played {
|
|
checked
|
|
}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<button
|
|
class="button is-small is-primary mr-1"
|
|
hx-patch={ "/setlist/" + row.ID }
|
|
hx-include="closest tr"
|
|
hx-target={ "#row-" + row.ID }
|
|
hx-swap="outerHTML"
|
|
>Save</button>
|
|
<button
|
|
class="button is-small"
|
|
hx-get={ "/setlist/" + row.ID + "/view" }
|
|
hx-target={ "#row-" + row.ID }
|
|
hx-swap="outerHTML"
|
|
>Cancel</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
|
|
// SongAddForm renders the add-song form for a jam. Artist has a typeahead
|
|
// datalist (suggestions only; free-form text is always accepted).
|
|
templ SongAddForm(jamID string) {
|
|
<form
|
|
hx-post={ "/jams/" + jamID + "/songs" }
|
|
hx-target="#setlist-rows"
|
|
hx-swap="beforeend"
|
|
hx-on--after-request="if(event.detail.requestConfig.verb==='post') { this.reset(); const a=this.querySelector('[name=artist]'); setTimeout(()=>{a.focus();a.scrollIntoView({behavior:'smooth',block:'nearest'});},0); }"
|
|
>
|
|
<div class="field has-addons">
|
|
<div class="control is-expanded">
|
|
<input
|
|
class="input"
|
|
type="text"
|
|
name="artist"
|
|
placeholder="Artist"
|
|
list="song-artist-list"
|
|
hx-get="/songs/search"
|
|
hx-trigger="input changed delay:300ms, change from:[name=title]"
|
|
hx-target="#song-artist-list"
|
|
hx-swap="innerHTML"
|
|
hx-include="[name=title]"
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
<datalist id="song-artist-list"></datalist>
|
|
</div>
|
|
<div class="control is-expanded">
|
|
<input
|
|
class="input"
|
|
type="text"
|
|
name="title"
|
|
placeholder="Title"
|
|
list="song-title-list"
|
|
hx-get="/songs/search"
|
|
hx-vals='{"field":"title"}'
|
|
hx-trigger="input changed delay:300ms, change from:[name=artist]"
|
|
hx-target="#song-title-list"
|
|
hx-swap="innerHTML"
|
|
hx-include="[name=artist]"
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
<datalist id="song-title-list"></datalist>
|
|
</div>
|
|
<div class="control">
|
|
<label class="checkbox" style="padding: 0.5em 0.75em">
|
|
<input type="checkbox" name="played"/> Played
|
|
</label>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-link" type="submit">Add</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
}
|