package templates import ( "bookhoard/internal/handlers" "fmt" ) templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string, unlinkedCount int) { Dashboard - Bookhoard @Header(user, "/dashboard") @LibrarySwitcher(libData, currentLibraryID, "Your Library", DashboardActions()) if unlinkedCount > 0 {
@Icon("alert", "h-4 w-4") { fmt.Sprintf("%d", unlinkedCount) } { pluralizeBooks(unlinkedCount) } on your devices could not be identified โ€” reading progress for them is paused until they are linked. Review โ†’
} if week != nil {
@DashboardWeekWidget(*week, activeLibType)
}
for _, section := range sections { @CollectionCarousel(section) }
@DashboardSettingsModal(allSections, hiddenCollections, itemsPerSection) @ErrorToast(errorMessage) } templ CollectionCarousel(section handlers.SectionData) {
{ section.Icon }

{ section.Title }

if section.Description != "" {

{ section.Description }

}
if section.ViewAllURL != "" { View All @Icon("arrow-right", "h-4 w-4") }
} templ BookCard(item handlers.BookInfo) {
if item.CoverImagePath != "" { { } else { { } if MediaBadgeIcon(item.Badge) != "" { @Icon(MediaBadgeIcon(item.Badge), "h-4 w-4") }

{ item.Title }

if item.Author != "" {

{ item.Author }

}
if item.HasConflict { @Icon("book-open", "h-5 w-5") } else if item.FormatGroup == "audiobook" { @Icon("headphones", "h-5 w-5") } else { @Icon("book-open", "h-5 w-5") }
} templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) { } // WeekStatsData is the dashboard's "this week" widget payload // (server-rendered from the statistics service; nil = suppressed). // Times rendered in the user's timezone at the server, per convention. type WeekStatsData struct { WeekMinutes int WeekPages int WeekSessions int WeekBooks int PrevMinutes int ActiveToday bool CurrentStreak int LibraryID string // scope the widget was rendered for ("" = all libraries) } // BookStatsData is the per-book reading-stats card on the book detail // page (nil = the viewer has no sessions on this book โ†’ hide it). type BookStatsData struct { TotalMinutes int TotalPages int SessionCount int MaxProgress float64 // 0..1 Finished bool LastSession string FirstSession string } // DashboardWeekWidget renders the collapsible "This Week" strip. The // collapsed summary row reads at a glance on every screen size; the // expanded body holds the full grid. The details element re-fetches // itself through htmx when the body sees a `week-refresh` event // (dispatched by the library switcher), carrying the CURRENT // #library-select value, so the widget tracks the switched library. templ DashboardWeekWidget(week WeekStatsData, activeLibType string) {
@Icon("chart", "h-4 w-4") This Week { fmtReadingMinutes(week.WeekMinutes) } { fmt.Sprintf("%d pages", week.WeekPages) } { fmt.Sprintf("%d sessions", week.WeekSessions) } { fmt.Sprintf("%d books", week.WeekBooks) } if week.CurrentStreak > 0 { ๐Ÿ”ฅ { fmt.Sprintf("%d", week.CurrentStreak) }-day streak } Full stats @Icon("chevron-down", "h-4 w-4 transition-transform group-open:rotate-180")
if activeLibType == "audiobooks" {

Listening Time

{ fmtReadingMinutes(week.WeekMinutes) }

{ weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes) }

Sessions

{ fmt.Sprintf("%d", week.WeekSessions) }

Books Touched

{ fmt.Sprintf("%d", week.WeekBooks) }

} else {

Reading Time

{ fmtReadingMinutes(week.WeekMinutes) }

{ weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes) }

Pages

{ fmt.Sprintf("%d", week.WeekPages) }

Sessions

{ fmt.Sprintf("%d", week.WeekSessions) }

Books Touched

{ fmt.Sprintf("%d", week.WeekBooks) }

}

Synced from your reading devices if week.CurrentStreak > 0 { if week.ActiveToday { { fmt.Sprintf(" ยท ๐Ÿ”ฅ %d-day streak โ€” keep it going today", week.CurrentStreak) } } else { { fmt.Sprintf(" ยท ๐Ÿ”ฅ %d-day streak โ€” read today to keep it", week.CurrentStreak) } } }

} // fmtReadingMinutes renders "42 min" / "1h 5min" spans. func fmtReadingMinutes(minutes int) string { if minutes >= 60 { if minutes%60 == 0 { return fmt.Sprintf("%dh", minutes/60) } return fmt.Sprintf("%dh %dmin", minutes/60, minutes%60) } return fmt.Sprintf("%d min", minutes) } func weekVsPrevLabel(minutes, prevMinutes int) string { if prevMinutes <= 0 { if minutes > 0 { return "reading time" } return "no reading yet" } percent := float64(minutes-prevMinutes) / float64(prevMinutes) * 100 return fmt.Sprintf("%+.0f%% vs last week", percent) }