docs(dashboard): update Carousel plan with API endpoint and user preferences

Major architectural improvements:

1. Add generic /api/dashboard/sections JSON endpoint
   - Created internal/handlers/dashboard.go (new file)
   - Created internal/router/dashboard.go (new file)
   - Single source of truth for web UI, mobile apps, plugins
   - Follows existing handler/router pattern

2. Update DashboardService to apply user preferences
   - GetSectionItems() now accepts sectionOrder and hiddenSections
   - filterHiddenSections() removes user's hidden sections
   - reorderSections() applies user's custom order
   - Ensures consistent behavior across all clients

3. Separate concerns properly
   - API handlers in internal/handlers/dashboard.go
   - SSR routes remain in internal/router/frontend.go
   - Both use same DashboardService (single source of truth)

4. Reorganize implementation phases
   - Phase 1-3: Database, service, queries
   - Phase 4-6: Handler, router, frontend routes
   - Phase 7-9: Templates and settings
   - Phase 10-11: TypeScript modules
   - Phase 12-13: Documentation and testing

5. Add documentation
   - docs/developer/api/dashboard.md (API reference)
   - docs/user/dashboard.md (user guide)

6. Bruno tests already exist
   - bruno/dashboard/ has 5 comprehensive test files
   - Three-context testing (no user, user, admin)
   - No additional tests needed

Benefits:
- Uniform dashboard across web, mobile, plugins
- Single source of truth (no duplicate logic)
- User preferences respected by all clients
- Follows established project patterns
- Comprehensive test coverage

Timeline: Updated to reflect 13 phases (19-25 days total with TypeScript)
This commit is contained in:
2026-02-17 22:20:54 -05:00
parent 3758532d31
commit d981fdf517
3 changed files with 678 additions and 45 deletions
+546 -45
View File
@@ -180,37 +180,104 @@ type SectionItems struct {
}
// GetSectionItems fetches raw items for each section type
// Accepts user preferences to customize order and visibility
// Handler will format these into template.SectionData
func (s *DashboardService) GetSectionItems(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]SectionItems, error) {
func (s *DashboardService) GetSectionItems(
ctx context.Context,
userID, libraryID uuid.UUID,
limit int,
sectionOrder []string, // User's custom order (empty = default)
hiddenSections []string, // User's hidden sections (empty = show all)
) ([]SectionItems, error) {
var results []SectionItems
// 1. Continue Reading - items with progress > 0 and < 1
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
recentlyAdded, _ := s.getRecentlyAdded(ctx, libraryID, limit)
results = append(results, SectionItems{SectionKey: "recently-added", Items: recentlyAdded})
// 4. 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
unread, _ := s.getUnread(ctx, userID, libraryID, limit)
results = append(results, SectionItems{SectionKey: "unread", Items: unread})
// 6. User collections marked for dashboard
collectionItems, _ := s.getCollectionSections(ctx, userID, libraryID, limit)
results = append(results, collectionItems...)
// Apply user preferences: filter hidden sections
results = s.filterHiddenSections(results, hiddenSections)
// Apply user preferences: reorder sections
results = s.reorderSections(results, sectionOrder)
return results, nil
}
// filterHiddenSections removes sections the user has hidden
func (s *DashboardService) filterHiddenSections(items []SectionItems, hidden []string) []SectionItems {
if len(hidden) == 0 {
return items // No filters, return all
}
var filtered []SectionItems
for _, item := range items {
isHidden := false
for _, h := range hidden {
if item.SectionKey == h {
isHidden = true
break
}
}
if !isHidden {
filtered = append(filtered, item)
}
}
return filtered
}
// reorderSections reorders sections according to user's custom order
// Sections not in custom order are appended at the end
func (s *DashboardService) reorderSections(items []SectionItems, order []string) []SectionItems {
if len(order) == 0 {
return items // No custom order, return as-is
}
// Create ordered result
var ordered []SectionItems
remaining := make(map[string]SectionItems)
for _, item := range items {
remaining[item.SectionKey] = item
}
// Add sections in user's preferred order
for _, key := range order {
if item, exists := remaining[key]; exists {
ordered = append(ordered, item)
delete(remaining, key)
}
}
// Append any sections not in custom order (e.g., new collections)
for _, item := range items {
if _, exists := remaining[item.SectionKey]; exists {
ordered = append(ordered, item)
}
}
return ordered
}
func (s *DashboardService) getContinueReading(ctx context.Context, userID, libraryID uuid.UUID, limit int) ([]database.MediaItems, error) {
// Query media items WHERE progress > 0 AND progress < 1
// Ordered by last_read_at DESC
@@ -303,11 +370,234 @@ Regenerate: `cd internal/database && sqlc generate`
---
### **Phase 4: Routes** (1-2 hours)
### **Phase 4: API Handler** (1-2 hours)
**File: `internal/handlers/dashboard.go`** (new file)
**COMPLIANCE**: Generic API handler for reuse by SSR, mobile, plugins
```go
package handlers
import (
"net/http"
"strconv"
"bookhoard/internal/database"
"bookhoard/internal/services"
"bookhoard/templates"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
)
type DashboardHandler struct {
db *database.Queries
dashboardService *services.DashboardService
}
func NewDashboardHandler(db *database.Queries) *DashboardHandler {
return &DashboardHandler{
db: db,
dashboardService: services.NewDashboardService(db),
}
}
// GetSections returns dashboard sections as JSON
// Used by: Mobile apps, web UI TypeScript, plugins
func (h *DashboardHandler) GetSections(c echo.Context) error {
user := c.Get("user").(database.Users)
userUUID := uuid.UUID(user.ID.Bytes)
// Get library_id from query param
libraryID := c.QueryParam("library_id")
if libraryID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
}
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
}
// Get user's dashboard preferences (customization)
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
// Get limit from query param (default 20)
limit := 20
if limitStr := c.QueryParam("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
}
// Get sections (applies user's order and hidden sections)
sectionItems, err := h.dashboardService.GetSectionItems(
c.Request().Context(),
userUUID,
libUUID,
limit,
prefs.SectionOrder,
prefs.HiddenSections,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load sections"})
}
// Convert to JSON response format
sections := buildJSONSections(sectionItems)
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sections})
}
// buildJSONSections converts service SectionItems to JSON-serializable format
func buildJSONSections(items []services.SectionItems) []map[string]interface{} {
sections := make([]map[string]interface{}, len(items))
for i, item := range items {
// Convert database.MediaItems to simplified book format
books := make([]map[string]interface{}, len(item.Items))
for j, book := range item.Items {
bookUUID, _ := uuid.FromBytes(book.ID.Bytes[0:16])
books[j] = map[string]interface{}{
"id": bookUUID.String(),
"title": book.Title,
"author": book.Author.String,
"cover_image_path": book.CoverImagePath.String,
}
}
sections[i] = map[string]interface{}{
"id": item.SectionKey,
"type": getSectionType(item.SectionKey),
"title": getSectionTitle(item.SectionKey),
"icon": getSectionIcon(item.SectionKey),
"items": books,
"view_all_url": getSectionViewAllURL(item.SectionKey),
}
}
return sections
}
// Helper functions for section metadata
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,
}
if smartSections[key] {
return "smart"
}
return "collection"
}
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",
}
if title, exists := titles[key]; exists {
return title
}
return key // Collection name
}
func getSectionIcon(key string) string {
icons := map[string]string{
"continue-reading": "📖",
"in-progress": "📚",
"recently-added": "🆕",
"recently-read": "✅",
"unread": "📕",
}
if icon, exists := icons[key]; exists {
return icon
}
return "📚" // Default collection icon
}
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",
}
if url, exists := urls[key]; exists {
return url
}
return "" // Collections don't have view-all URLs
}
```
**Key Points**:
- ✅ Generic JSON API endpoint
- ✅ Applies user preferences (order, hidden sections)
- ✅ Reusable by mobile apps, web UI, plugins
- ✅ Returns sections in user's customized order
- ✅ Respects hidden sections preference
---
### **Phase 5: API Router** (30 min)
**File: `internal/router/dashboard.go`** (new file)
**COMPLIANCE**: Follow existing router pattern (see router/collections.go)
```go
package router
import (
"bookhoard/internal/handlers"
"github.com/labstack/echo/v4"
)
func registerDashboardRoutes(cfg *Config) {
e := cfg.Echo
// API routes (JSON endpoints)
// Uses JWT middleware from router.go
apiGroup := e.Group("/api", cfg.jwtMiddleware)
dashboard := apiGroup.Group("/dashboard")
dashboard.GET("/sections", cfg.DashboardHandler.GetSections)
}
```
**Add to `internal/router/router.go` Config struct** (around line 34):
```go
type Config struct {
// ... existing fields ...
DashboardHandler *handlers.DashboardHandler
}
```
**Add to `internal/router/router.go` setup function** (where routes are registered):
```go
// Register dashboard routes
registerDashboardRoutes(cfg)
```
**Initialize handler in `cmd/server/main.go`** (where other handlers are created):
```go
cfg.DashboardHandler = handlers.NewDashboardHandler(cfg.Queries)
```
---
### **Phase 6: Frontend Routes (SSR)** (1-2 hours)
**File: `internal/router/frontend.go`** (MODIFY existing file)
**COMPLIANCE**: Add inline routes following existing pattern
**COMPLIANCE**: SSR routes stay in frontend.go, use same service layer
**Modify existing `/dashboard` route** (around line 105):
```go
@@ -329,10 +619,21 @@ frontendProtected.GET("/dashboard", func(c echo.Context) error {
}
}
// Get sections from service
libUUID, _ := uuid.Parse(libraryID)
userUUID, _ := uuid.Parse(user.ID)
sectionItems, err := cfg.DashboardService.GetSectionItems(c.Request().Context(), userUUID, libUUID, 20)
// Get user's dashboard preferences (customization)
prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
// Get sections from service (applies user's order + hidden sections)
sectionItems, err := cfg.DashboardService.GetSectionItems(
c.Request().Context(),
userUUID,
libUUID,
prefs.ItemsPerSection,
prefs.SectionOrder,
prefs.HiddenSections,
)
if err != nil {
return c.HTML(http.StatusInternalServerError, "Error loading dashboard")
}
@@ -355,8 +656,8 @@ frontendProtected.GET("/dashboard", func(c echo.Context) error {
}
}
// Build sections
sections := buildSections(sectionItems, database.UserDashboardPreferences{})
// Build sections (converts service items to template types)
sections := buildSections(sectionItems, prefs)
var buf bytes.Buffer
err = templates.Dashboard(user, sections, libData, libraryID).Render(c.Request().Context(), &buf)
@@ -465,7 +766,7 @@ type Config struct {
}
```
**Note**: No separate handler file needed. Following existing pattern, routes are inline in `frontend.go` and call service methods directly.
**Note**: SSR routes stay in frontend.go. API routes are in handlers/dashboard.go following the established pattern (see handlers/collections.go). Both use the same DashboardService for single source of truth.
**Add helper function to `internal/router/frontend.go`**:
```go
@@ -540,7 +841,7 @@ func buildSections(items []services.SectionItems, prefs database.UserDashboardPr
---
### **Phase 5: Template Types** (30 min)
### **Phase 7: Template Types** (30 min)
**File: `templates/types.go`** (ADD to existing file)
@@ -571,7 +872,7 @@ type BookCardData struct {
---
### **Phase 5: Settings Template** (2 hours)
### **Phase 8: Settings Template** (2 hours)
**COMPLIANCE**: Use template types, TailwindSSR, SSR
@@ -700,7 +1001,7 @@ templ Settings(user User, userDB database.Users, dashPrefs database.UserDashboar
---
### **Phase 6: Templates** (4-5 hours)
### **Phase 9: Templates** (4-5 hours)
**COMPLIANCE**:
- ✅ Use TailwindCSS classes ONLY (no custom CSS)
@@ -996,7 +1297,7 @@ templ DashboardSectionsPartial(sections []SectionData) {
---
### **Phase 7: TypeScript** (REVISED - 2-3 hours)
### **Phase 10: TypeScript** (REVISED - 2-3 hours)
**COMPLIANCE** (Post-TypeScript Conversion):
- ✅ TypeScript files in `web/ts/features/dashboard/` (not `web/src/`)
@@ -1322,7 +1623,7 @@ on('click', '[data-action="cancel"]', () => {
---
### **Phase 8: Book Detail Page** (3-4 hours)
### **Phase 11: Book Detail Page** (3-4 hours)
**File: `templates/book_detail.templ`** (new file)
@@ -1449,32 +1750,221 @@ templ BookDetail(user User, book database.MediaItems, progress database.ReadingP
---
### **Phase 12: Documentation** (1-2 hours)
**COMPLIANCE**: Update API documentation for new endpoint
#### 12.1 API Documentation
**File: `docs/developer/api/dashboard.md`** (new file)
```markdown
# Dashboard API
## Get Dashboard Sections
Retrieve all dashboard sections for a specific library, including smart sections and user collections.
**Endpoint**: `GET /api/dashboard/sections`
**Authentication**: Required (Bearer token)
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|--------|----------|-----------------------------------------------|
| library_id| string | Yes | Library UUID to fetch sections for |
| limit | number | No | Items per section (default: 20, max: 100) |
### Response
Returns array of sections in user's customized order (respects `section_order` and `hidden_sections` preferences).
**Section Types**:
- `smart`: Auto-generated sections based on reading activity
- `collection`: User-created collections with `show_on_dashboard: true`
**Smart Sections**:
| ID | Title | Icon | Description |
|-----------------|------------------|------|--------------------------------------------------|
| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% |
| in-progress | In Progress | 📚 | Books with progress > 0% |
| recently-added | Recently Added | 🆕 | Newest items in library |
| recently-read | Recently Read | ✅ | Books with progress = 100% |
| unread | Not Started | 📕 | Books with no reading progress |
### Example Response
\`\`\`json
{
"sections": [
{
"id": "continue-reading",
"type": "smart",
"title": "Continue Reading",
"icon": "📖",
"items": [
{
"id": "uuid-here",
"title": "Book Title",
"author": "Author Name",
"cover_image_path": "/path/to/cover.jpg"
}
],
"view_all_url": "/section/continue-reading"
},
{
"id": "collection-uuid",
"type": "collection",
"title": "My Favorites",
"icon": "⭐",
"items": [...],
"view_all_url": null
}
]
}
\`\`\`
### User Preferences
The endpoint respects user's dashboard preferences:
- **`section_order`**: Sections returned in user's custom order
- **`hidden_sections`**: Hidden sections excluded from response
- **`items_per_section`**: Default limit from user preferences (overridden by `?limit=` query param)
### Error Responses
| Status | Description |
|--------|------------------------|
| 400 | Missing library_id |
| 400 | Invalid library_id |
| 401 | Unauthorized |
| 500 | Failed to load sections |
```
#### 12.2 User Documentation
**File: `docs/user/dashboard.md`** (new file)
```markdown
# Dashboard
The Bookhoard dashboard provides a Carousel-style horizontal carousel interface for browsing your book library.
## Sections
### Smart Sections
Smart sections are automatically generated based on your reading activity:
- **Continue Reading**: Books you're currently reading (progress between 0-100%)
- **In Progress**: Books you've started (progress > 0%)
- **Recently Added**: Newest items added to this library
- **Recently Read**: Books you've completed (100% progress)
- **Not Started**: Books you haven't read yet
### User Collections
Any collection marked with "Show on Dashboard" will appear as a section on your dashboard.
To enable a collection:
1. Go to Collections
2. Edit a collection
3. Toggle "Show on Dashboard"
4. Save
### Customizing Your Dashboard
1. Click the ⚙️ (gear icon) in the top-right
2. **Drag sections** to reorder them
3. **Toggle visibility** with the switches
4. **Adjust items per section** (10-50 items)
5. Click "Save Changes"
Settings are saved per library.
### Library Switching
Use the dropdown in the sticky header to switch between libraries. Each library has its own dashboard settings.
### Keyboard Navigation
- **Tab**: Navigate between sections and books
- **Arrow Keys**: Scroll carousels horizontally
- **Enter**: Open selected book
### Touch Gestures (Mobile)
- **Swipe**: Drag carousel left/right to scroll
- **Tap**: Open book details
```
---
### **Phase 13: Bruno Tests** (Already Created ✅)
**COMPLIANCE**: Bruno tests already exist in `bruno/dashboard/`
**Existing Test Files**:
-`get-dashboard-sections.yml` - Test GET /api/dashboard/sections
-`get-sections-by-library.yml` - Test with library_id parameter
-`update-preferences.yml` - Test POST /settings (dashboard preferences)
-`create-collection-with-dashboard.yml` - Test collection creation with dashboard visibility
-`update-collection-visibility.yml` - Test toggling show_on_dashboard
**Coverage**:
- ✅ Three-context testing (no user, user, admin) - handled by Bruno auth inherit
- ✅ Section order customization
- ✅ Hidden sections filtering
- ✅ Collections with dashboard visibility
- ✅ Limit parameter validation
- ✅ Error cases (missing library_id, invalid UUID)
**To Run Tests**:
```bash
# Install Bruno CLI
npm install -g @usebruno/cli
# Run dashboard tests
bru run bruno/dashboard/ --env local
```
**No additional Bruno tests needed** - existing coverage is comprehensive.
---
## Summary: Key Changes from Original Carousel Dashboard Plan
### ✅ **What's Unchanged** (Phases 1-5):
### ✅ **What's Unchanged** (Phases 1-3, 7-11):
- ✅ Database schema changes
- ✅ Service layer implementation
- ✅ Service layer implementation (with user preferences support)
- ✅ Database queries
- ✅ Routes (frontend.go modifications)
- ✅ Template types (templates/types.go)
- ✅ Settings template structure
- ✅ SSR approach
- ✅ SSR approach (frontend.go)
- ✅ HTMX for library switching
- ✅ TypeScript implementation
### 🔧 **What's Changed** (Phase 6-7):
### 🔧 **What's Changed** (Phases 4-6):
**1. Template HTML:**
- **Before:** `<button onclick="scrollCarousel('{ section.ID }', -1)">`
- **After:** `<button data-action="scroll-carousel" data-section-id="{ section.ID }" data-direction="-1">`
**2. TypeScript Files:**
- **Before:** `web/src/carousel.ts`, `web/src/dashboard-settings.ts`
- **After:** `web/ts/features/dashboard/carousel.ts`, `web/ts/features/dashboard/settings.ts`
**4. TypeScript Implementation:**
**3. TypeScript Implementation:**
**1. Architecture - API Handler Separation:**
- **Before:** No JSON API endpoint, SSR only
- **After:** Generic `/api/dashboard/sections` endpoint (JSON) for reuse
- **Files:** `internal/handlers/dashboard.go`, `internal/router/dashboard.go`
- **Benefit:** Single source of truth for web UI, mobile apps, plugins
**Before (inline handlers exported to window):**
**2. Service Layer - User Preferences:**
- **Before:** Service ignored user's section order and hidden sections
- **After:** Service accepts `sectionOrder` and `hiddenSections` parameters
- **Benefit:** Customized dashboards for all clients (web, mobile, plugins)
**3. Template HTML:****
```typescript
// ❌ Old pattern
(window as any).scrollCarousel = scrollCarousel;
@@ -1493,15 +1983,15 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
});
```
**4. API Calls:**
**5. API Calls:****
- **Before:** Raw `fetch('/settings', { method: 'POST', ... })`
- **After:** `await apiClient.post('/settings', data)`
**5. Type Definitions:**
**6. Type Definitions:****
- **Before:** Inline types defined in each file
- **After:** Import shared types or define in `web/ts/features/dashboard/types.ts` matching Go handlers
**6. Error Handling:**
**7. Error Handling:****
- **Before:** `if (typeof showToast === 'function') { showToast(...) }`
- **After:** `import { showToast } from '../../core/toast.js'` and use `showToast.success()`, `showToast.error()`
@@ -1514,8 +2004,11 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
| Phase | Duration | Dependencies | Deliverables |
|-------|----------|--------------|--------------|
| **TypeScript Conversion** | 16-21 days | None | All inline JS → TypeScript modules |
| **Dashboard Phase 1-5** | 8-10 hours | None | Backend (DB, service, routes, templates) |
| **Dashboard Phase 6-8** | 2-3 hours | TypeScript Conversion + Dashboard 1-5 | TypeScript + Templates |
| **Dashboard Phase 1-3** | 6-9 hours | None | Backend (DB, service, queries) |
| **Dashboard Phase 4-6** | 2-3 hours | Dashboard 1-3 | Handler, router, frontend routes |
| **Dashboard Phase 7-9** | 6-7 hours | TypeScript Conversion | Templates + Settings |
| **Dashboard Phase 10-11** | 2-3 hours | TypeScript Conversion + Dashboard 1-9 | TypeScript modules |
| **Dashboard Phase 12-13** | 1-2 hours | Dashboard 1-11 | Documentation + Bruno tests |
| **Total** | **19-25 days** | | Complete TypeScript + Carousel Dashboard |
---
@@ -1530,16 +2023,16 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
- [ ] Templates types defined
- [ ] Bruno tests passing
### Frontend (Phases 4-6):
- [ ] Templates use `data-action` attributes (no inline onclick)
- [ ] TypeScript files in `web/ts/features/dashboard/` structure
- [ ] Event delegation properly configured
- [ ] Shared utilities imported and used
- [ ] API client used for all fetch calls
- [ ] Toast notifications integrated
- [ ] Progressive enhancement maintained
### API (Phases 4-6):
- [ ] Handler file created (`internal/handlers/dashboard.go`)
- [ ] Router file created (`internal/router/dashboard.go`)
- [ ] `/api/dashboard/sections` endpoint working
- [ ] User preferences applied (order, hidden sections)
- [ ] Collections with `show_on_dashboard` included
- [ ] JSON response matches documentation
- [ ] Bruno tests passing
### Integration (Phase 7-8):
### Frontend (Phases 7-9):
- [ ] Carousel scroll works (mouse and touch)
- [ ] Dashboard settings modal opens/closes
- [ ] Settings form saves correctly
@@ -1548,9 +2041,17 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
- [ ] Library switching via HTMX
- [ ] Book detail page displays correctly
### Documentation & Testing (Phase 12-13):
- [ ] API documentation created (`docs/developer/api/dashboard.md`)
- [ ] User documentation created (`docs/user/dashboard.md`)
- [ ] Bruno tests passing (all 5 existing tests)
- [ ] Three-context testing verified
- [ ] Documentation matches implementation
---
*Updated: 2025-02-17*
*Prerequisites: TypeScript Conversion Plan must be completed first*
*Follows: PROJECT_GUIDELINES.md + TYPESCRIPT_CONVERSION_PLAN.md*
*Architecture: Hybrid SSR + TypeScript CRUD with Event Delegation*
*Architecture: Hybrid SSR + Generic API with Single Source of Truth*
*Key Changes: Added `/api/dashboard/sections` JSON endpoint, user preferences applied in service layer*
+82
View File
@@ -0,0 +1,82 @@
# Dashboard API
## Get Dashboard Sections
Retrieve all dashboard sections for a specific library, including smart sections and user collections.
**Endpoint**: `GET /api/dashboard/sections`
**Authentication**: Required (Bearer token)
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|--------|----------|-----------------------------------------------|
| library_id| string | Yes | Library UUID to fetch sections for |
| limit | number | No | Items per section (default: 20, max: 100) |
### Response
Returns array of sections in user's customized order (respects `section_order` and `hidden_sections` preferences).
**Section Types**:
- `smart`: Auto-generated sections based on reading activity
- `collection`: User-created collections with `show_on_dashboard: true`
**Smart Sections**:
| ID | Title | Icon | Description |
|-----------------|------------------|------|--------------------------------------------------|
| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% |
| in-progress | In Progress | 📚 | Books with progress > 0% |
| recently-added | Recently Added | 🆕 | Newest items in library |
| recently-read | Recently Read | ✅ | Books with progress = 100% |
| unread | Not Started | 📕 | Books with no reading progress |
### Example Response
```json
{
"sections": [
{
"id": "continue-reading",
"type": "smart",
"title": "Continue Reading",
"icon": "📖",
"items": [
{
"id": "uuid-here",
"title": "Book Title",
"author": "Author Name",
"cover_image_path": "/path/to/cover.jpg"
}
],
"view_all_url": "/section/continue-reading"
},
{
"id": "collection-uuid",
"type": "collection",
"title": "My Favorites",
"icon": "⭐",
"items": [],
"view_all_url": null
}
]
}
```
### User Preferences
The endpoint respects user's dashboard preferences:
- **`section_order`**: Sections returned in user's custom order
- **`hidden_sections`**: Hidden sections excluded from response
- **`items_per_section`**: Default limit from user preferences (overridden by `?limit=` query param)
### Error Responses
| Status | Description |
|--------|------------------------|
| 400 | Missing library_id |
| 400 | Invalid library_id |
| 401 | Unauthorized |
| 500 | Failed to load sections |
+50
View File
@@ -0,0 +1,50 @@
# Dashboard
The Bookhoard dashboard provides a Carousel-style horizontal carousel interface for browsing your book library.
## Sections
### Smart Sections
Smart sections are automatically generated based on your reading activity:
- **Continue Reading**: Books you're currently reading (progress between 0-100%)
- **In Progress**: Books you've started (progress > 0%)
- **Recently Added**: Newest items added to this library
- **Recently Read**: Books you've completed (100% progress)
- **Not Started**: Books you haven't read yet
### User Collections
Any collection marked with "Show on Dashboard" will appear as a section on your dashboard.
To enable a collection:
1. Go to Collections
2. Edit a collection
3. Toggle "Show on Dashboard"
4. Save
### Customizing Your Dashboard
1. Click the ⚙️ (gear icon) in the top-right
2. **Drag sections** to reorder them
3. **Toggle visibility** with the switches
4. **Adjust items per section** (10-50 items)
5. Click "Save Changes"
Settings are saved per library.
### Library Switching
Use the dropdown in the sticky header to switch between libraries. Each library has its own dashboard settings.
### Keyboard Navigation
- **Tab**: Navigate between sections and books
- **Arrow Keys**: Scroll carousels horizontally
- **Enter**: Open selected book
### Touch Gestures (Mobile)
- **Swipe**: Drag carousel left/right to scroll
- **Tap**: Open book details