feat(dashboard): collapsible This Week widget, library-scoped + switch-surviving; title into the subbar
Three dashboard complaints, one build: - The widget collapses to a single summary row (This Week · minutes · pages · sessions · books · streak · Full stats) on every screen size and expands to the full grid via a details/summary toggle — the carousel sections start immediately below it again. - The 'Your Library' heading moved into the library switcher subbar (LibrarySwitcher gains an optional title param; collections/series keep the plain 'Library' label). The standalone heading block is gone. - Root-caused the JS-switch vanishing: renderDashboardCollections replaces ALL of #collections-container's innerHTML, so anything server-rendered inside it (title, widget) died on the first library switch. The widget now re-fetches ITSELF: a /dashboard/week-widget fragment (same resolveLibrary family-lens rules as the page) is pulled by htmx whenever the switcher dispatches week-refresh — carrying the current #library-select value — so the widget both survives switches and RE-SCOPES to the switched library. Initial SSR renders the same scope the sections use. ThisWeek (and the streak calendar) gained an optional library filter. Tests: TestReadingStatsWeekLibraryScope — per-library totals, scoped streaks, invisible-library isolation, malformed id 400. Full suite PASS.
This commit is contained in:
1 parent
5a2c1585ae
commit
0914c935d9
16 files changed
+576
-271
No files matched your search
@@ -473,13 +473,18 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID))
|
||||
allSectionsData := handlers.BuildSections(allSections, libraryID)
|
||||
|
||||
// “This week” widget: the user's rolling 7-day reading pulse, from
|
||||
// the same session seed the /stats page uses. Suppressed when the
|
||||
// service fails (dashboard always renders).
|
||||
// "This week" widget: the user's rolling 7-day reading pulse for
|
||||
// the CURRENTLY SELECTED library (the same resolution the sections
|
||||
// use), from the same session seed the /stats page serves.
|
||||
// Suppressed when the service fails (dashboard always renders).
|
||||
var week *templates.WeekStatsData
|
||||
if cfg.StatisticsService != nil {
|
||||
weekScope := pgtype.UUID{}
|
||||
if !libRes.IsAll {
|
||||
weekScope = libRes.LibUUID
|
||||
}
|
||||
weekSvc, weekErr := cfg.StatisticsService.ThisWeek(c.Request().Context(),
|
||||
pgtype.UUID{Bytes: userUUID, Valid: true}, user.Timezone)
|
||||
pgtype.UUID{Bytes: userUUID, Valid: true}, user.Timezone, weekScope)
|
||||
if weekErr == nil {
|
||||
week = &templates.WeekStatsData{
|
||||
WeekMinutes: weekSvc.WeekMinutes,
|
||||
@@ -489,6 +494,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
PrevMinutes: weekSvc.PrevMinutes,
|
||||
ActiveToday: weekSvc.ActiveToday,
|
||||
CurrentStreak: weekSvc.CurrentStreak,
|
||||
LibraryID: libRes.LibraryID,
|
||||
}
|
||||
} else {
|
||||
log.Printf("ThisWeek widget failed: %v", weekErr)
|
||||
@@ -840,6 +846,46 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Week-widget fragment: the collapsible "This Week" strip re-fetches
|
||||
// itself here (htmx) whenever the library switcher dispatches
|
||||
// week-refresh, carrying the current #library-select value. Same
|
||||
// resolution rules as the dashboard page (family-lens safe).
|
||||
frontendProtected.GET("/dashboard/week-widget", func(c *echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, cfg)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
|
||||
}
|
||||
if cfg.StatisticsService == nil {
|
||||
return echo.NewHTTPError(http.StatusServiceUnavailable, "stats unavailable")
|
||||
}
|
||||
libRes := resolveLibrary(c, cfg, user.ID)
|
||||
weekScope := pgtype.UUID{}
|
||||
if !libRes.IsAll {
|
||||
weekScope = libRes.LibUUID
|
||||
}
|
||||
userUUID, _ := uuid.Parse(user.ID)
|
||||
weekSvc, weekErr := cfg.StatisticsService.ThisWeek(c.Request().Context(),
|
||||
uuidToPGType(userUUID), user.Timezone, weekScope)
|
||||
if weekErr != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, weekErr.Error())
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = templates.DashboardWeekWidget(templates.WeekStatsData{
|
||||
WeekMinutes: weekSvc.WeekMinutes,
|
||||
WeekPages: weekSvc.WeekPages,
|
||||
WeekSessions: weekSvc.WeekSessions,
|
||||
WeekBooks: weekSvc.WeekBooks,
|
||||
PrevMinutes: weekSvc.PrevMinutes,
|
||||
ActiveToday: weekSvc.ActiveToday,
|
||||
CurrentStreak: weekSvc.CurrentStreak,
|
||||
LibraryID: libRes.LibraryID,
|
||||
}).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Custom Section Builder page
|
||||
frontendProtected.GET("/custom-section", func(c *echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, cfg)
|
||||
|
||||
Reference in new issue
Block a user