fix(podcasts+ui): user-test round 3 — five browser-reported defects
1. Add-modal 'Download order' never revealed: the CSS sibling selector
(#podcast-download-all:checked ~ .podcast-add-order) can't fire —
the checkbox was nested inside a wrapper div, so the select row was
no SIBLING of the input. Restructured: input, label, and order row
are now siblings under one flex-wrap wrapper.
2. Order + auto-download dropdowns always displayed the LAST option
('Newest first'/'Newest') regardless of the saved value: templ's
selected={ boolExpr } compiles to selected="false" — and the mere
PRESENCE of the attribute selects the option, so every option was
selected and the browser honored the last. Sweep to the boolean-
attribute form selected?={...} (omits on false) across
podcast_detail (14), book_detail_modals (10: manga type/reading
direction selects + abridged/explicit checkboxes — same defect,
always-checked), collection_modal (2: hide-finished/show-up-next
always-checked). Values were SAVING correctly all along; this was
pure display. audio_reader/collections :selected/:checked are
Alpine bindings — unaffected.
3. Remove Feed (and every hx-confirm) still showed the browser dialog
for signed-in users: the global confirm modal rendered at the end
of SidebarSignIn — signed-IN pages have no SidebarSignIn, so no
#confirm-modal existed and confirmDialog() degraded to
window.confirm. Moved the modal into Header proper (rendered on
every page, after </aside>).
4. Facet bar at scale: values now carry feed COUNTS, sort by frequency
DESC then alphabetically, cap at 6 chips per group flat, and the
rest collapse into a native <details> '+N more' expander — hundreds
of hosts/tags degrade gracefully instead of a wall of chips.
Verified live on the dev instance (order select reflects the saved
override, override row persists, reveal fires, modal renders on
signed-in pages). Podcast + logout + media-pin suites green; vitest
167 green.
This commit is contained in:
1 parent
15f41e6fd4
commit
2fd0828c06
13 files changed
+1004
-948
No files matched your search
+56
-29
@@ -2,11 +2,16 @@ package templates
|
||||
|
||||
import "net/url"
|
||||
|
||||
// PodcastFacetLink links a facet value back into the podcasts page's
|
||||
// server-side filter (the /bookshelf FacetLink targets the wrong
|
||||
// surface for podcasts).
|
||||
templ PodcastFacetLink(facet string, value string) {
|
||||
<a href={ "/podcasts?" + facet + "=" + url.QueryEscape(value) } class="badge hover:opacity-80 transition-opacity" style="background-color: var(--surface-secondary); color: var(--text-secondary); text-decoration: none;">{ value }</a>
|
||||
// PodcastFacetChip is the facet-bar chip: value + feed count, linking
|
||||
// back into the podcasts page's server-side filter (the /bookshelf
|
||||
// FacetLink targets the wrong surface for podcasts).
|
||||
templ PodcastFacetChip(facet string, v FacetValue) {
|
||||
<a href={ "/podcasts?" + facet + "=" + url.QueryEscape(v.Value) } class="badge hover:opacity-80 transition-opacity" style="background-color: var(--surface-secondary); color: var(--text-secondary); text-decoration: none;">
|
||||
{ v.Value }
|
||||
if v.Count > 1 {
|
||||
<span class="opacity-70 ml-1">{ podcastInt(v.Count) }</span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
|
||||
// The /podcasts page (plan task 1.9, D13/D14/D18): Add Feed wizard,
|
||||
@@ -99,15 +104,9 @@ templ PodcastsFilterBar(data PodcastsPageData) {
|
||||
if len(data.Facets["hosts"]) > 0 || len(data.Facets["tags"]) > 0 || len(data.Facets["language"]) > 0 {
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2 text-sm">
|
||||
<span class="font-semibold mr-1" style="color: var(--text-secondary)">Filter:</span>
|
||||
for _, h := range data.Facets["hosts"] {
|
||||
@PodcastFacetLink("host", h)
|
||||
}
|
||||
for _, t := range data.Facets["tags"] {
|
||||
@PodcastFacetLink("tag", t)
|
||||
}
|
||||
for _, l := range data.Facets["language"] {
|
||||
@PodcastFacetLink("language", l)
|
||||
}
|
||||
@podcastFacetGroup("host", data.Facets["hosts"])
|
||||
@podcastFacetGroup("tag", data.Facets["tags"])
|
||||
@podcastFacetGroup("language", data.Facets["language"])
|
||||
if data.Query != "" {
|
||||
<a href="/podcasts" class="badge" style="background-color: var(--surface-secondary); color: var(--text-secondary)">clear</a>
|
||||
}
|
||||
@@ -115,6 +114,30 @@ templ PodcastsFilterBar(data PodcastsPageData) {
|
||||
}
|
||||
}
|
||||
|
||||
// podcastFacetBatchSize: how many facet chips render flat before the
|
||||
// rest collapse into a "+N more" expander (native <details>, no JS).
|
||||
// With hundreds of feeds the flat row would be a wall of chips; the
|
||||
// frequency sort keeps the useful ones up front.
|
||||
var podcastFacetBatchSize = 6
|
||||
|
||||
templ podcastFacetGroup(facet string, values []FacetValue) {
|
||||
if len(values) > 0 {
|
||||
for _, v := range values[:min(len(values), podcastFacetBatchSize)] {
|
||||
@PodcastFacetChip(facet, v)
|
||||
}
|
||||
if len(values) > podcastFacetBatchSize {
|
||||
<details class="inline-block">
|
||||
<summary class="badge cursor-pointer select-none" style="background-color: var(--surface-secondary); color: var(--text-secondary)">+{ podcastInt(len(values) - podcastFacetBatchSize) } more</summary>
|
||||
<span class="inline-flex flex-wrap gap-2 align-middle ml-2">
|
||||
for _, v := range values[podcastFacetBatchSize:] {
|
||||
@PodcastFacetChip(facet, v)
|
||||
}
|
||||
</span>
|
||||
</details>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PodcastsSections renders the Continue section (feeds with in-progress
|
||||
// episodes, most recently listened first — plan 2.5) ahead of the
|
||||
// alphabetical All Feeds grid.
|
||||
@@ -238,23 +261,27 @@ templ PodcastAddFeedModal(libraries []LibraryData) {
|
||||
} else if len(libraries) == 1 {
|
||||
<input type="hidden" name="library_id" value={ libraries[0].ID }/>
|
||||
}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
|
||||
<input type="checkbox" id="podcast-download-all" name="download_all" value="true" class="cursor-pointer"/>
|
||||
<label for="podcast-download-all" style="color: var(--text-secondary)">Download all episodes now</label>
|
||||
</div>
|
||||
<div class="podcast-add-order flex items-center gap-2 text-sm pl-6">
|
||||
<label for="podcast-download-order" style="color: var(--text-secondary)">Download order</label>
|
||||
<select
|
||||
id="podcast-download-order"
|
||||
name="download_order"
|
||||
class="input w-auto py-1.5 cursor-pointer"
|
||||
title="Listen/download order for this feed — chapters land in this order"
|
||||
>
|
||||
<option value="feed" selected>Feed order</option>
|
||||
<option value="reverse">Feed order (reversed)</option>
|
||||
<option value="oldest">Oldest first (pub date)</option>
|
||||
<option value="newest">Newest first (pub date)</option>
|
||||
</select>
|
||||
<!-- Download order (§0 item 5c): revealed by pure CSS when
|
||||
the checkbox is checked — #podcast-download-all:checked ~
|
||||
.podcast-add-order. The checkbox and this row MUST stay
|
||||
siblings under this wrapper for the selector to fire. -->
|
||||
<div class="podcast-add-order flex items-center gap-2 pl-6">
|
||||
<label for="podcast-download-order" style="color: var(--text-secondary)">Download order</label>
|
||||
<select
|
||||
id="podcast-download-order"
|
||||
name="download_order"
|
||||
class="input w-auto py-1.5 cursor-pointer"
|
||||
title="Listen/download order for this feed — chapters land in this order"
|
||||
>
|
||||
<option value="feed" selected>Feed order</option>
|
||||
<option value="reverse">Feed order (reversed)</option>
|
||||
<option value="oldest">Oldest first (pub date)</option>
|
||||
<option value="newest">Newest first (pub date)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="podcast-add-feed-error" class="text-sm hidden" style="color: var(--status-danger)"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
|
||||
Reference in new issue
Block a user