From 8d37df249d414c456ec70f9e65e606a6575174ab Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 18 Feb 2026 16:42:54 -0500 Subject: [PATCH] docs(dashboard): update Carousel plan for post-TypeScript conversion Major updates: - Reduce smart sections from 5 to 4 (removed 'In Progress') - Continue Reading: 0% < progress < 100% - Recently Added: newest items - Recently Read: progress >= 100% - Not Started: progress = 0% or no record - Update paths from web/ts/ to web/src/ structure - Add handler types instead of duplicate template types - Types defined in internal/handlers/dashboard.go - Templates import handlers.SectionData, handlers.BookInfo directly New features: - Drag-and-drop section reordering - Section visibility toggles - Items per section slider - Manual progress marking (mark as read/unread) TypeScript updates: - Use (window as any).api from web/src/api.ts - Use (window as any).showToast from web/src/toast.ts - Import types from web/src/types/dashboard.d.ts - Event delegation via data-action attributes Add verification checklist for comprehensive plan review: - Type definition verification against actual API responses - API contract and endpoint verification - Cross-reference verification for template-handler types - Progressive enhancement testing - Build and deployment verification --- CAROUSEL_DASHBOARD_PLAN.md | 882 +++++-- CAROUSEL_DASHBOARD_VERIFICATION_CHECKLIST.md | 2389 ++++++++++++++++++ 2 files changed, 3046 insertions(+), 225 deletions(-) create mode 100644 CAROUSEL_DASHBOARD_VERIFICATION_CHECKLIST.md diff --git a/CAROUSEL_DASHBOARD_PLAN.md b/CAROUSEL_DASHBOARD_PLAN.md index 81bfef6..ab7b9ca 100644 --- a/CAROUSEL_DASHBOARD_PLAN.md +++ b/CAROUSEL_DASHBOARD_PLAN.md @@ -3,12 +3,12 @@ ## Overview Transform the current dashboard into a **production-ready** horizontal carousel layout like Audiobookshelf/Kavita, with: -- Smart sections (Continue Reading, Recently Added, etc.) -- User collections as sections -- Filter-based smart sections (custom collections with auto-assign rules) +- **4 Smart sections**: Continue Reading, Recently Added, Recently Read, Not Started +- User collections as sections (manual or filter-based) - Separate dashboard per library - Full accessibility, keyboard nav, and touch gestures - **SSR-first architecture** (data pre-populated server-side, HTMX for updates) +- **Drag-and-drop reordering** with user preference persistence --- @@ -17,19 +17,20 @@ Transform the current dashboard into a **production-ready** horizontal carousel **IMPORTANT:** This plan assumes the **TypeScript Conversion Plan** has been completed first. **Required Infrastructure from TypeScript Conversion Plan:** -- ✅ `web/ts/core/api.ts` - Centralized API client with auth -- ✅ `web/ts/core/toast.ts` - Toast notification system -- ✅ `web/ts/shared/events.ts` - Event delegation utilities -- ✅ `web/ts/core/storage.ts` - localStorage wrapper -- ✅ `web/ts/core/dom.ts` - DOM utilities (escapeHtml, etc.) +- ✅ `web/src/api.ts` - Centralized API client with auth +- ✅ `web/src/toast.ts` - Toast notification system +- ✅ `web/src/events.ts` - Event delegation utilities +- ✅ `web/src/storage.ts` - localStorage wrapper +- ✅ `web/src/dom.ts` - DOM utilities (escapeHtml, etc.) +- ✅ `web/src/types/api.d.ts` - Type definitions for all API responses - ✅ Event delegation pattern established (data attributes) - ✅ TypeScript compilation pipeline in place (`npm run build:ts`) **Execution Order:** -1. Complete TypeScript Conversion Plan (16-21 days) +1. Complete TypeScript Conversion Plan (20-25.5 days) 2. Execute this updated Carousel Dashboard Plan (3-4 days) -**Timeline:** 19-25 days total (no rework, consistent patterns) +**Timeline:** 23-29.5 days total (no rework, consistent patterns) --- @@ -49,18 +50,22 @@ This plan **adheres to** all PROJECT_GUIDELINES.md requirements with explicit us ✅ **Frontend Standards** (Updated for Post-TypeScript Conversion): - **TailwindCSS classes ONLY** - no custom CSS -- **TypeScript** in `web/ts/features/dashboard/` (no inline JavaScript) +- **TypeScript** in `web/src/` (no inline JavaScript) - **Procedural/imperative style** - no OOP (classes, inheritance, this-capture) - **SSR for initial data** - no AJAX on page load - **Progressive enhancement** - works without JavaScript - **HTMX for CRUD operations** (library switching, settings updates) -- **Event delegation pattern** - `data-action` attributes (no inline `onclick`) -- **Shared API client** - `apiClient` from `web/ts/core/api.ts` +- **Event delegation pattern** - `data-action` attributes +- **API client** - `(window as any).api` from `web/src/api.ts` +- **Toast notifications** - `(window as any).showToast` from `web/src/toast.ts` +- **Type definitions** - `import type { ... } from './types/api'` ✅ **Code Organization**: -- **Template types in templates/types.go** - SectionData, BookCardData +- **Handler types in internal/handlers/dashboard.go** - SectionData, BookInfo (enhanced with template fields) +- **Templates use handler types directly** - no duplicate types in templates package - **All business logic in services** - reusable for SSR/API/mobile -- **TypeScript in web/ts/features/dashboard/** - follows TypeScript Conversion Plan structure +- **TypeScript in web/src/** - follows TypeScript Conversion Plan structure +- **Type definitions in web/src/types/dashboard.d.ts** - recreate handler JSON for TypeScript ✅ **Database Operations**: - **Merge into existing schema.sql** - no migration files @@ -126,13 +131,12 @@ CREATE TABLE smart_section_types ( is_global BOOLEAN DEFAULT false -- true = uses global data (Recently Added), false = per-user ); --- Insert default sections +-- Insert default sections (4 smart sections + user collections) INSERT INTO smart_section_types (section_key, title, description, icon, default_priority, is_global) VALUES -('continue-reading', 'Continue Reading', 'Books you''re currently reading', '📖', 1, false), -('in-progress', 'In Progress', 'Books you''ve started but not finished', '📚', 2, false), -('recently-added', 'Recently Added', 'Newly added items to this library', '🆕', 3, true), -('recently-read', 'Recently Read', 'Books you''ve finished', '✅', 4, false), -('unread', 'Not Started', 'Books you haven''t read yet', '📕', 5, false); +('continue-reading', 'Continue Reading', 'Books you''re currently reading (0 < progress < 1)', '📖', 1, false), +('recently-added', 'Recently Added', 'Newly added items to this library', '🆕', 2, true), +('recently-read', 'Recently Read', 'Books you''ve finished (progress >= 1)', '✅', 3, false), +('unread', 'Not Started', 'Books you haven''t read yet (progress = 0 or no record)', '📕', 4, false); ``` #### 1.2 Regenerate Database Code @@ -195,23 +199,19 @@ func (s *DashboardService) GetSectionItems( continueReading, _ := s.getContinueReading(ctx, userID, libraryID, limit) results = append(results, SectionItems{SectionKey: "continue-reading", Items: continueReading}) - // 2. In Progress - items with progress > 0 - inProgress, _ := s.getInProgress(ctx, userID, libraryID, limit) - results = append(results, SectionItems{SectionKey: "in-progress", Items: inProgress}) - - // 3. Recently Added - newest items in library + // 2. Recently Added - newest items in library recentlyAdded, _ := s.getRecentlyAdded(ctx, libraryID, limit) results = append(results, SectionItems{SectionKey: "recently-added", Items: recentlyAdded}) - // 4. Recently Read - items with progress = 1 + // 3. Recently Read - items with progress >= 1 recentlyRead, _ := s.getRecentlyRead(ctx, userID, libraryID, limit) results = append(results, SectionItems{SectionKey: "recently-read", Items: recentlyRead}) - // 5. Not Started - items with no progress + // 4. Not Started - items with progress = 0 OR no reading_progress record unread, _ := s.getUnread(ctx, userID, libraryID, limit) results = append(results, SectionItems{SectionKey: "unread", Items: unread}) - // 6. User collections marked for dashboard + // 5. User collections marked for dashboard collectionItems, _ := s.getCollectionSections(ctx, userID, libraryID, limit) results = append(results, collectionItems...) @@ -283,20 +283,18 @@ func (s *DashboardService) getContinueReading(ctx context.Context, userID, libra // Ordered by last_read_at DESC } -func (s *DashboardService) getInProgress(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]database.MediaItems, error) { - // Query media items WHERE progress > 0 -} - func (s *DashboardService) getRecentlyAdded(ctx context.Context, libraryID uuid.UUID, limit int) ([]database.MediaItems, error) { // Query media items ORDER BY created_at DESC } func (s *DashboardService) getRecentlyRead(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]database.MediaItems, error) { // Query media items WHERE progress >= 1 (completed) + // Books manually marked as read (progress set to 1) appear here } func (s *DashboardService) getUnread(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]database.MediaItems, error) { - // Query media items with no reading_progress record + // Query media items WHERE progress = 0 OR no reading_progress record + // Books manually marked as unread (progress set to 0) appear here } func (s *DashboardService) getCollectionSections(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]SectionItems, error) { @@ -321,6 +319,29 @@ func (s *DashboardService) GetDashboardPreferences(ctx context.Context, userID, - ✅ Procedural/imperative style (no OOP) - ✅ Returns raw data - handler formats for templates +### Manual Progress Marking + +**Users can manually set reading status** - progress value is the single source of truth: + +- **Mark as Unread** → Set `progress = 0` → Book appears in "Not Started" section +- **Mark as Read** → Set `progress = 1` → Book appears in "Recently Read" section +- Uses existing reading_progress endpoint (no new API needed) + +**How it works:** +``` +Device sync: progress = 0.35 (35% through book) +User marks as read: progress = 1.0 (now in "Recently Read") +User marks as unread: progress = 0.0 (now in "Not Started") +``` + +**Benefits:** +- Users can "give up" on a book without it cluttering "Continue Reading" +- Users can mark partially-read books as complete +- Simple implementation (just set progress to 0 or 1) +- Consistent with automatic progress tracking from devices + +**Note**: The reading_progress endpoint and database table already exist. No backend changes needed for manual marking. + --- ### **Phase 3: Database Queries** (1-2 hours) @@ -384,13 +405,36 @@ import ( "strconv" "bookhoard/internal/database" "bookhoard/internal/services" - "bookhoard/templates" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) +// SectionData represents a dashboard section (carousel) +// Used by: Templates (SSR), API JSON responses +// Template-specific fields: Type, Icon, ViewAllURL, Priority +type SectionData struct { + ID string `json:"id"` + Type string `json:"type"` // "smart" or "collection" + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` // Template-specific: emoji + Items []BookInfo `json:"items"` + ViewAllURL string `json:"view_all_url"` // Template-specific: navigation + Priority int `json:"priority"` // Template-specific: display order +} + +// BookInfo represents a book in a carousel card +// Used by: Templates (SSR), API JSON responses +// Unwraps pgtype fields for template convenience +type BookInfo struct { + ID string `json:"id"` + Title string `json:"title"` + Author string `json:"author"` + CoverImagePath string `json:"cover_image_path"` +} + type DashboardHandler struct { db *database.Queries dashboardService *services.DashboardService @@ -483,7 +527,6 @@ func getSectionType(key string) string { // Return "smart" or "collection" based on key smartSections := map[string]bool{ "continue-reading": true, - "in-progress": true, "recently-added": true, "recently-read": true, "unread": true, @@ -497,7 +540,6 @@ func getSectionType(key string) string { func getSectionTitle(key string) string { titles := map[string]string{ "continue-reading": "Continue Reading", - "in-progress": "In Progress", "recently-added": "Recently Added", "recently-read": "Recently Read", "unread": "Not Started", @@ -511,7 +553,6 @@ func getSectionTitle(key string) string { func getSectionIcon(key string) string { icons := map[string]string{ "continue-reading": "📖", - "in-progress": "📚", "recently-added": "🆕", "recently-read": "✅", "unread": "📕", @@ -525,7 +566,6 @@ func getSectionIcon(key string) string { func getSectionViewAllURL(key string) string { urls := map[string]string{ "continue-reading": "/section/continue-reading", - "in-progress": "/section/in-progress", "recently-added": "/section/recently-added", "recently-read": "/history", "unread": "/section/unread", @@ -656,8 +696,8 @@ frontendProtected.GET("/dashboard", func(c echo.Context) error { } } - // Build sections (converts service items to template types) - sections := buildSections(sectionItems, prefs) + // Build sections (converts service items to handler types) + sections := buildSections(sectionItems) var buf bytes.Buffer err = templates.Dashboard(user, sections, libData, libraryID).Render(c.Request().Context(), &buf) @@ -778,16 +818,16 @@ var smartSectionDefs = map[string]struct { ViewAllURL string Priority int }{ - "continue-reading": {"Continue Reading", "Books you're currently reading", "📖", "/section/continue-reading", 1}, - "in-progress": {"In Progress", "Books you've started but not finished", "📚", "/section/in-progress", 2}, - "recently-added": {"Recently Added", "Newly added items to this library", "🆕", "/section/recently-added", 3}, - "recently-read": {"Recently Read", "Books you've finished", "✅", "/history", 4}, - "unread": {"Not Started", "Books you haven't read yet", "📕", "/section/unread", 5}, + "continue-reading": {"Continue Reading", "Books you're currently reading (0 < progress < 1)", "📖", "/section/continue-reading", 1}, + "recently-added": {"Recently Added", "Newly added items to this library", "🆕", "/section/recently-added", 2}, + "recently-read": {"Recently Read", "Books you've finished (progress >= 1)", "✅", "/history", 3}, + "unread": {"Not Started", "Books you haven't read yet (progress = 0 or no record)", "📕", "/section/unread", 4}, } -// buildSections converts service SectionItems to template SectionData -func buildSections(items []services.SectionItems, prefs database.UserDashboardPreferences) []templates.SectionData { - var sections []templates.SectionData +// buildSections converts service SectionItems to handler SectionData +// Uses handlers.SectionData (NOT templates.SectionData) per guidelines +func buildSections(items []services.SectionItems) []handlers.SectionData { + var sections []handlers.SectionData for _, si := range items { def, isSmart := smartSectionDefs[si.SectionKey] @@ -811,11 +851,11 @@ func buildSections(items []services.SectionItems, prefs database.UserDashboardPr priority = 100 } - // Convert database.MediaItems to template.BookCardData - bookCards := make([]templates.BookCardData, len(si.Items)) + // Convert database.MediaItems to handlers.BookInfo + bookCards := make([]handlers.BookInfo, len(si.Items)) for i, item := range si.Items { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) - bookCards[i] = templates.BookCardData{ + bookCards[i] = handlers.BookInfo{ ID: itemUUID.String(), Title: item.Title, Author: item.Author.String, @@ -823,7 +863,7 @@ func buildSections(items []services.SectionItems, prefs database.UserDashboardPr } } - sections = append(sections, templates.SectionData{ + sections = append(sections, handlers.SectionData{ ID: si.SectionKey, Type: sectionType, Title: title, @@ -841,40 +881,50 @@ func buildSections(items []services.SectionItems, prefs database.UserDashboardPr --- -### **Phase 7: Template Types** (30 min) +### **Phase 7: Handler Types** (included in Phase 4) -**File: `templates/types.go`** (ADD to existing file) +**NOTE**: Types are defined in `internal/handlers/dashboard.go` (see Phase 4), NOT in `templates/types.go`. -Add new types to support dashboard: +**CRITICAL GUIDELINE COMPLIANCE**: +- ✅ Types defined ONCE in handlers package +- ✅ Templates import and use `handlers.SectionData`, `handlers.BookInfo` directly +- ❌ NO duplicate types in `templates/types.go` (violates PROJECT_GUIDELINES.md) + +**Type Definitions** (from Phase 4): ```go -// SectionData represents a dashboard section (carousel) +// In internal/handlers/dashboard.go + type SectionData struct { - ID string `json:"id"` - Type string `json:"type"` // "smart", "collection" - Title string `json:"title"` - Description string `json:"description"` - Icon string `json:"icon"` - Items []BookCardData `json:"items"` - ViewAllURL string `json:"view_all_url"` - Priority int `json:"priority"` - IsHidden bool `json:"is_hidden"` + ID string `json:"id"` + Type string `json:"type"` // Template-specific + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` // Template-specific + Items []BookInfo `json:"items"` + ViewAllURL string `json:"view_all_url"` // Template-specific + Priority int `json:"priority"` // Template-specific } -// BookCardData represents a book in a carousel card -type BookCardData struct { - ID string `json:"id"` +type BookInfo struct { + ID string `json:"id"` // UUID converted to string Title string `json:"title"` - Author string `json:"author"` - CoverImagePath string `json:"cover_image_path"` + Author string `json:"author"` // pgtype.Text unwrapped + CoverImagePath string `json:"cover_image_path"` // pgtype.Text unwrapped } ``` +**Why handler types?** +1. **Single source of truth** - No parallel type systems +2. **Template convenience** - pgtype fields unwrapped, UUIDs converted +3. **Template-specific fields** - Icon, ViewAllURL, Priority computed for display +4. **Guidelines compliance** - "NEVER duplicate types between handlers and templates" + --- ### **Phase 8: Settings Template** (2 hours) -**COMPLIANCE**: Use template types, TailwindSSR, SSR +**COMPLIANCE**: Use handler/database types, TailwindCSS, SSR **File: `templates/settings.templ`** (new file) @@ -893,7 +943,7 @@ templ Settings(user User, userDB database.Users, dashPrefs database.UserDashboar Settings - Bookhoard - + @@ -991,9 +1041,9 @@ templ Settings(user User, userDB database.Users, dashPrefs database.UserDashboar - - - + + + } @@ -1005,32 +1055,35 @@ templ Settings(user User, userDB database.Users, dashPrefs database.UserDashboar **COMPLIANCE**: - ✅ Use TailwindCSS classes ONLY (no custom CSS) -- ✅ Use template types (SectionData, BookCardData, User, LibraryData) +- ✅ Use **handler types** (handlers.SectionData, handlers.BookInfo) - NO duplicate template types - ✅ SSR for initial data - ✅ HTMX for updates - ✅ **Event delegation pattern** (no inline onclick) - ✅ **Data attributes** for TypeScript integration -#### 6.1 Main Dashboard Template +#### 8.1 Main Dashboard Template **File: `templates/dashboard.templ`** (REPLACE existing) ```templ package templates -templ Dashboard(user User, sections []SectionData, libraries []LibraryData, currentLibraryID string) { +import ( + "bookhoard/internal/handlers" +) + +templ Dashboard(user User, sections []handlers.SectionData, libraries []LibraryData, currentLibraryID string) { Dashboard - Bookhoard - - - - - - - + + + + + + @Header(user, "/dashboard") @@ -1094,13 +1147,15 @@ templ Dashboard(user User, sections []SectionData, libraries []LibraryData, curr } ``` -#### 6.2 Section Carousel Component +#### 8.2 Section Carousel Component **File: `templates/components.templ`** (ADD to existing file if exists, or new file) ```templ package templates -templ SectionCarousel(section SectionData) { +import "bookhoard/internal/handlers" + +templ SectionCarousel(section handlers.SectionData) {
@@ -1169,7 +1224,7 @@ templ SectionCarousel(section SectionData) {
} -templ BookCard(item BookCardData) { +templ BookCard(item handlers.BookInfo) {
} -templ DashboardSettingsModal(sections []SectionData) { +templ DashboardSettingsModal(sections []handlers.SectionData) {