Add dashboard redesign, custom section builder, and enhanced search functionality

Features:
- Complete dashboard redesign with improved UI components and layout
- Implement custom section builder for personalized book organization
- Add new events tracking system for user interactions
- Enhance search functionality with better static search.js
- Update TypeScript type definitions for API responses

Backend:
- Update Go dependencies in go.mod
- Add new frontend routes in router

Templates:
- Update admin and dashboard templates with new components

Frontend:
- Refactor analytics, collections, conflicts, and queue modules
- Add new documentation features in docs.ts
- Implement linking between books and collections
- Add toast notifications for user feedback
- Include placeholder book SVG asset

This commit consolidates multiple feature additions and improvements
across the entire stack including backend, templates, and frontend.
This commit is contained in:
2026-02-25 16:56:10 -05:00
parent 5864710e4f
commit a8920a8f6c
19 changed files with 1718 additions and 533 deletions
+30 -45
View File
@@ -1,23 +1,8 @@
// ============================================
// API Type Definitions
// ============================================
// These types match the JSON responses from /api/* endpoints.
// Source of truth: Check what the endpoint ACTUALLY returns:
// 1. Database layer: internal/database/queries.sql.go (SearchMediaItemsRow, etc.)
// 2. Handler structs: internal/handlers/*.go (check json:"..." tags)
// 3. Test by calling endpoint and inspecting JSON response
//
// When API contracts change:
// 1. Find the endpoint function in internal/handlers/*.go
// 2. Check what it returns (database row or struct)
// 3. Check the JSON tags: `json:"field_name"`
// 4. Map pgtype fields to TypeScript types:
// - pgtype.Text → string | undefined
// - pgtype.UUID → string
// - pgtype.Timestamp → string (ISO datetime)
// - pgtype.Numeric → number or string (for precision)
// 5. Update the interface below with snake_case field names
// 6. Run Bruno tests to verify
// These types are globally available in all .ts files.
// No imports needed - just use the type names directly.
// ============================================
// Matches database.SearchMediaItemsRow from /api/media-items/search
@@ -25,7 +10,7 @@
// Endpoint: internal/handlers/media.go:SearchMediaItems()
// Note: internal/handlers/search.go has an unused MediaItemSummary - ignore it
// Used in: search.ts
export interface MediaItemSummary {
interface MediaItemSummary {
id: string;
library_id: string;
title: string;
@@ -77,7 +62,7 @@ export interface MediaItemSummary {
// Matches handlers.CollectionData / CollectionResponse JSON response
// Source: internal/handlers/collections.go:123-131 CollectionResponse
// Used in: collections.ts
export interface CollectionData {
interface CollectionData {
id: string;
name: string;
description: string;
@@ -90,7 +75,7 @@ export interface CollectionData {
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
// JSON tags: media_item_id, title, author, cover_image_path
// Used in: collections.templ (server-rendered), collections.ts
export interface BookInfo {
interface BookInfo {
media_item_id: string;
title: string;
author: string;
@@ -101,7 +86,7 @@ export interface BookInfo {
// CRITICAL: Must match Go handler return types EXACTLY
// Source: handlers.SectionData in collections.go (lines 73-81)
// Used in: dashboard API responses, TypeScript dashboard components
export interface SectionData {
interface SectionData {
id: string;
is_system: boolean;
title: string;
@@ -115,7 +100,7 @@ export interface SectionData {
// Matches database.UserDashboardPreferences and dashboard preferences API
// Source: internal/database/models.go:381-390
// Used in: dashboard preferences API
export interface DashboardPreferences {
interface DashboardPreferences {
library_id: string;
hidden_collections: string[];
collection_order: string[];
@@ -124,7 +109,7 @@ export interface DashboardPreferences {
// Matches handlers.UnlinkedBookData JSON response
// Used in: unlinked_books.ts, unlinked_books.templ
export interface UnlinkedBookData {
interface UnlinkedBookData {
progress_id: string;
device_id: string;
device_name: string;
@@ -137,7 +122,7 @@ export interface UnlinkedBookData {
potential_matches: PotentialMatchData[];
}
export interface PotentialMatchData {
interface PotentialMatchData {
media_item_id: string;
title: string;
author: string;
@@ -147,7 +132,7 @@ export interface PotentialMatchData {
// Matches collection rule objects
// Used in: collection_rules.ts
export interface CollectionRule {
interface CollectionRule {
id: string;
field: 'genre' | 'series' | 'author' | 'language' | 'publisher' | 'copyright_year' | 'tags';
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'greater_than' | 'less_than';
@@ -158,31 +143,31 @@ export interface CollectionRule {
// Matches API test rule responses
// Used in: collection_rules.ts (test results)
export interface TestRuleMatch {
interface TestRuleMatch {
title: string;
author: string;
cover_image_path?: string;
}
// Matches handlers.SearchResponse (internal/handlers/search.go)
export interface SearchResponse {
interface SearchResponse {
results: SearchBookResponse[];
total: number;
}
export interface SearchBookResponse {
interface SearchBookResponse {
id: string;
title: string;
authors: SearchAuthor[];
}
export interface SearchAuthor {
interface SearchAuthor {
first_name: string;
last_name: string;
}
// Matches AuthResponse (internal/handlers/auth.go:59-65)
export interface AuthResponse {
interface AuthResponse {
access_token: string;
refresh_token?: string;
token_type: string;
@@ -190,7 +175,7 @@ export interface AuthResponse {
user: UserProfile;
}
export interface UserProfile {
interface UserProfile {
id: string;
email: string;
username: string;
@@ -202,7 +187,7 @@ export interface UserProfile {
// Matches handlers.ReadingStatsResponse (internal/handlers/analytics.go:26-35)
// Used in: analytics.ts
export interface ReadingStatsResponse {
interface ReadingStatsResponse {
total_books_read: number;
total_pages_read: number;
total_reading_time_minutes: number;
@@ -213,7 +198,7 @@ export interface ReadingStatsResponse {
daily_reading_minutes: DailyReading[];
}
export interface DailyReading {
interface DailyReading {
date: string;
minutes: number;
pages: number;
@@ -222,11 +207,11 @@ export interface DailyReading {
// Matches handlers.DeviceUsageResponse (internal/handlers/analytics.go:43-45)
// Note: Response is wrapped: { devices: DeviceUsage[] }
// Used in: analytics.ts
export interface DeviceUsageResponse {
interface DeviceUsageResponse {
devices: DeviceUsage[];
}
export interface DeviceUsage {
interface DeviceUsage {
device_id: string;
device_name: string;
device_type: string;
@@ -239,11 +224,11 @@ export interface DeviceUsage {
// Matches handlers.PopularBooksResponse (internal/handlers/analytics.go:57-59)
// Note: Response is wrapped: { books: PopularBook[] }
// Used in: analytics.ts
export interface PopularBooksResponse {
interface PopularBooksResponse {
books: PopularBook[];
}
export interface PopularBook {
interface PopularBook {
media_item_id: string;
title: string;
author: string;
@@ -254,7 +239,7 @@ export interface PopularBook {
// Matches handlers.QueueItemResponse (internal/handlers/queue.go:35-51)
// Used in: queue.ts
export interface QueueItemResponse {
interface QueueItemResponse {
id: string;
device_id: string;
device_name: string;
@@ -274,7 +259,7 @@ export interface QueueItemResponse {
// Matches handlers.QueueStatsResponse (internal/handlers/queue.go:27-33)
// Used in: queue.ts
export interface QueueStatsResponse {
interface QueueStatsResponse {
pending_count: number;
processing_count: number;
failed_count: number;
@@ -284,7 +269,7 @@ export interface QueueStatsResponse {
// Matches handlers.ConflictDetailResponse (internal/handlers/conflicts.go:42-53)
// Used in: conflicts.ts
export interface ConflictDetailResponse {
interface ConflictDetailResponse {
id: string;
media_item_id: string;
media_item_title: string;
@@ -298,7 +283,7 @@ export interface ConflictDetailResponse {
}
// Matches handlers.ConflictSourceData (internal/handlers/conflicts.go:36-40)
export interface ConflictSourceData {
interface ConflictSourceData {
source: string;
timestamp: string;
data: Record<string, unknown>;
@@ -306,7 +291,7 @@ export interface ConflictSourceData {
// Matches handlers.ConflictListResponse (internal/handlers/conflicts.go:55-59)
// Used in: conflicts.ts
export interface ConflictListResponse {
interface ConflictListResponse {
conflicts: ConflictDetailResponse[];
total: number;
unresolved: number;
@@ -314,7 +299,7 @@ export interface ConflictListResponse {
// Matches handlers.ConflictResolveResponse (internal/handlers/conflicts.go:61-65)
// Used in: conflicts.ts
export interface ConflictResolveResponse {
interface ConflictResolveResponse {
conflict_resolved: boolean;
applied_to: Record<string, boolean>;
devices_synced: string[];
@@ -322,7 +307,7 @@ export interface ConflictResolveResponse {
// Matches handlers.BulkResolveResponse (internal/handlers/conflicts.go:424-429)
// Used in: conflicts.ts
export interface BulkResolveResponse {
interface BulkResolveResponse {
results: ConflictResult[];
total: number;
success: number;
@@ -330,7 +315,7 @@ export interface BulkResolveResponse {
}
// Matches handlers.ConflictResult (internal/handlers/conflicts.go:431-436)
export interface ConflictResult {
interface ConflictResult {
conflict_id: string;
status: string;
error?: string;