package templates import "net/url" // 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) { { v.Value } if v.Count > 1 { { podcastInt(v.Count) } } } // The /podcasts page (plan task 1.9, D13/D14/D18): Add Feed wizard, // subscription-scoped search, facet bar, All-Feeds grid. Server-side // filtering (GET form, like the series page — no JS needed); the grid // is templ-rendered server data per the rendering-ownership rule. templ PodcastsPage(user User, data PodcastsPageData, currentPath string) { Podcasts - Bookhoard @Header(user, currentPath)
@Icon("podcast", "h-5 w-5")

Podcasts

Your subscriptions

if data.HasLibrary { }
if data.ErrorMsg != "" {
{ data.ErrorMsg }
} if !data.HasLibrary {

No podcasts library yet

An admin creates a Podcasts library (with a download folder) first — then feeds can be added here.

} else { @PodcastsFilterBar(data) if len(data.Feeds) == 0 {

No subscriptions

Add an RSS feed to start listening — episodes land here and download on demand.

} else { @PodcastsSections(data) } }
@PodcastAddFeedModal(data.Libraries) } templ PodcastsFilterBar(data PodcastsPageData) {
@Icon("search", "h-4 w-4")
if len(data.Facets["hosts"]) > 0 || len(data.Facets["tags"]) > 0 || len(data.Facets["language"]) > 0 {
Filter: @podcastFacetGroup("host", data.Facets["hosts"]) @podcastFacetGroup("tag", data.Facets["tags"]) @podcastFacetGroup("language", data.Facets["language"]) if data.Query != "" { clear }
} } // podcastFacetBatchSize: how many facet chips render flat before the // rest collapse into a "+N more" expander (native
, 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 {
+{ podcastInt(len(values) - podcastFacetBatchSize) } more for _, v := range values[podcastFacetBatchSize:] { @PodcastFacetChip(facet, v) }
} } } // PodcastsSections renders the Continue section (feeds with in-progress // episodes, most recently listened first — plan 2.5) ahead of the // alphabetical All Feeds grid. templ PodcastsSections(data PodcastsPageData) { if len(data.ContinueFeeds) > 0 {

Continue

for _, feed := range data.ContinueFeeds { @PodcastFeedCardView(feed) }

All Feeds

} @PodcastsGrid(data.Feeds) } templ PodcastsGrid(feeds []PodcastFeedCard) {
for _, feed := range feeds { @PodcastFeedCardView(feed) }
} templ PodcastFeedCardView(feed PodcastFeedCard) {
if feed.ArtworkPath != "" { { } else { @Icon("podcast", "h-7 w-7") }

{ feed.DisplayTitle() }

{ feed.TypeLabel() } if feed.Host != "" { { " · " + feed.Host } }

if feed.HasContinue {

@Icon("book-open", "h-3.5 w-3.5 inline") { " " + podcastInt(feed.ContinueEpisode) + ". " + feed.ContinueTitle }

} else if feed.Description != "" {

{ feed.Description }

}
{ podcastInt(feed.DownloadedN) + " / " + podcastInt(feed.EpisodeTotal) + " downloaded" } if feed.EpisodeMatches > 0 { { podcastInt(feed.EpisodeMatches) + " matching episode" + podcastPlural(feed.EpisodeMatches) } } if feed.NewEpisodes { new episodes } if feed.IsComplete { complete } if feed.Explicit { explicit }
} templ PodcastAddFeedModal(libraries []LibraryData) { }