# TypeScript Conversion Plan for Bookhoard ## Executive Summary Convert **~5,500 lines of inline JavaScript** across 13 template files into organized TypeScript modules while **preserving the existing Hybrid SSR architecture**. The web UI will remain a client of the JSON API, alongside Kobo, KOReader, and future plugins. **Core Strategy:** Server-Side Rendering (SSR) for initial page loads + TypeScript for CRUD operations via existing JSON API. --- ## Current Architecture Analysis ### Already Converted to TypeScript ✅ | Source | Output | Lines | Purpose | |--------|--------|-------|---------| | `web/src/toast.ts` | `toast.js` | 226 | Toast notifications, HTMX/fetch interceptors | | `web/src/theme.ts` | `theme.js` | 130 | Theme management, smooth scrolling | | `web/src/header.ts` | `header.js` | ~100 | Header dropdowns, theme/user menus | | `web/src/device-management.ts` | `device-management.js` | ~70 | Device token copy/regeneration | ### Remaining Standalone JavaScript ❌ | File | Lines | Purpose | |------|-------|---------| | `web/static/search.js` | 275 | Header search with keyboard navigation, debouncing | ### Inline JavaScript in Templates (~5,500 lines) | Template | Script Lines | Primary Functions | |----------|--------------|-------------------| | `collection_rules.templ` | ~400 | Rule CRUD operations, testing | | `unlinked_books.templ` | ~650 | Book matching, linking, bulk operations | | `collections.templ` | ~500 | Bulk operations, filtering | | `devices.templ` | ~350 | Token regeneration, sync URL display | | `bookshelf.templ` | ~250 | Book viewing, pagination | | `dashboard.templ` | ~300 | Statistics, recent activity | | `api_explorer.templ` | ~200 | API testing, cURL generation | | `admin_library.templ` | ~250 | Admin library scan | | `admin_profile.templ` | ~150 | Admin profile management | | `admin.templ` | ~150 | Admin dashboard actions | | `index.templ` | ~100 | Landing page theme preview | | `login.templ` | ~100 | Login theme selection | | `progress.templ` | ~100 | Reading progress | **Total: ~5,500 lines of inline JavaScript to convert** --- ## Architecture: Hybrid SSR + TypeScript CRUD ### Current Pattern (Already Working) ``` ┌─────────────────────────────────────────────────────────────┐ │ User visits /collections │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 1. Go handler: GET /collections │ │ 2. Fetches data from CollectionService │ │ 3. Renders templates.Collection(user, collections) │ │ 4. Returns complete HTML page with data │ └─────────────────────────────────────────────────────────────┘ ↓ [Page displays instantly with data] ↓ ┌─────────────────────────────────────────────────────────────┐ │ 5. User clicks "Add Rule" button │ │ 6. TypeScript: fetch POST /api/collections/{id}/rules │ │ 7. Server returns JSON │ │ 8. TypeScript updates DOM │ └─────────────────────────────────────────────────────────────┘ ``` ### What This Preserves ✅ **Fast initial page loads** - SSR with data ✅ **Progressive enhancement** - Works without JavaScript ✅ **Single API** - All clients use `/api/*` endpoints ✅ **Service layer as source of truth** - All handlers use same services ✅ **No handler duplication** - No new HTML endpoints needed ✅ **Plugin ecosystem ready** - Plugins use JSON API --- ## Proposed TypeScript Structure ``` web/ts/ ├── core/ │ ├── toast.ts # ✅ Already converted │ ├── theme.ts # ✅ Already converted │ ├── storage.ts # NEW: localStorage wrapper with types │ ├── dom.ts # NEW: DOM utilities (escapeHtml, querySelector) │ └── api.ts # NEW: API client wrapper with auth │ ├── features/ │ ├── search/ │ │ ├── search.ts # Convert from search.js │ │ └── types.ts # Search result types (match Go handlers) │ │ │ ├── collections/ │ │ ├── rules.ts # Convert from inline JS │ │ ├── bulk.ts # Convert from inline JS │ │ └── types.ts # Reuse handlers.Rule, handlers.Collection │ │ │ ├── linking/ │ │ ├── matcher.ts # Convert from inline JS │ │ ├── bulk-link.ts # Convert from inline JS │ │ ├── manual-link.ts # Convert from inline JS │ │ └── types.ts # Reuse handlers.BookMatch, handlers.Progress │ │ │ ├── devices/ │ │ ├── token.ts # Convert from inline JS │ │ └── types.ts # Reuse handlers.Device types │ │ │ ├── bookshelf/ │ │ ├── display.ts # Convert from inline JS │ │ ├── pagination.ts # Convert from inline JS │ │ └── types.ts # Reuse handlers.MediaItem types │ │ │ ├── api-explorer/ │ │ ├── request.ts # Convert from inline JS │ │ ├── response.ts # Convert from inline JS │ │ └── curl.ts # Convert from inline JS │ │ │ └── admin/ │ ├── scan.ts # Convert from inline JS │ └── stats.ts # Convert from inline JS │ └── shared/ ├── events.ts # Event delegation utilities └── auth.ts # Token management helpers ``` --- ## Type Sharing Strategy ### Principle: Reuse Go Handler Types **❌ DON'T DO THIS:** ```typescript // Duplicating types from Go handlers interface Collection { id: string; name: string; // ... } ``` **✅ DO THIS:** ```typescript // Use types that match Go handlers exactly // These types are already defined in Go and returned by /api/* interface MediaItem { id: string; title: string; author?: string; library_id: string; library_type_name: 'ebooks' | 'comics' | 'manga'; cover_image_path?: string; } // From handlers.CollectionData interface CollectionData { ID: string; Name: string; Description: string; Color: string; Icon: string; } ``` **Why:** - Single source of truth (Go handlers) - API returns these types as JSON - TypeScript matches exactly what server provides - No duplication, no drift --- ## Conversion Phases ### Phase 1: Complete Standalone File Conversion **Priority: High | Effort: 1 day | Dependencies: None** **Tasks:** 1. Convert `web/static/search.js` → `web/ts/features/search/search.ts` 2. Add proper types for search results (match `handlers.MediaItem`) 3. Extract keyboard navigation logic into pure functions 4. Update templates to use new TypeScript module **Deliverable:** All standalone JavaScript converted to TypeScript **Files:** - Create: `web/ts/features/search/search.ts` - Create: `web/ts/features/search/types.ts` - Delete: `web/static/search.js` - Update: Templates referencing search functions --- ### Phase 2: Core Utilities (Shared Infrastructure) **Priority: High | Effort: 1-2 days | Dependencies: None** **Tasks:** 1. Create `web/ts/core/storage.ts` - localStorage wrapper with type safety - Token management helpers - Theme persistence 2. Create `web/ts/core/dom.ts` - escapeHtml utility - querySelector wrappers with null checks - Element creation helpers 3. Create `web/ts/core/api.ts` - API client wrapper - Automatic auth header injection - Error handling integration with toast system 4. Create `web/ts/shared/events.ts` - Event delegation helpers - Data attribute selectors - Common event handlers **Deliverable:** Reusable utilities for all feature modules **Files:** - Create: `web/ts/core/storage.ts` - Create: `web/ts/core/dom.ts` - Create: `web/ts/core/api.ts` - Create: `web/ts/shared/events.ts` --- ### Phase 3: Low Complexity Features (Search, Header, Admin) **Priority: Medium | Effort: 3-4 days | Dependencies: Phase 2** **Tasks:** **3.1 Search Module (from Phase 1)** - Extract search logic from `search.ts` - Add debounced search with proper types - Keyboard navigation state management - Integration with `/api/media-items/search` **3.2 Header Dropdowns** - Extract from `header.ts` (already TS) - Add event delegation for dropdowns - Theme switching logic - User menu interactions **3.3 Admin Actions** - Quick scan trigger - System stats display - Admin profile updates **Deliverable:** Search, header, and admin features in TypeScript **Files:** - Refine: `web/ts/features/search/search.ts` - Update: `web/ts/features/search/types.ts` - Create: `web/ts/features/admin/scan.ts` - Create: `web/ts/features/admin/stats.ts` - Update: `web/src/header.ts` (add event delegation) --- ### Phase 4: Medium Complexity Features (Collections, Bookshelf, Devices) **Priority: Medium | Effort: 4-5 days | Dependencies: Phase 2, 3** **Tasks:** **4.1 Collection Rules** - Convert rule CRUD from `collection_rules.templ` - Use `/api/collections/{id}/rules` endpoints - Rule testing functionality - Bulk rule operations **4.2 Bookshelf Display** - Convert from `bookshelf.templ` - Pagination logic - Library selection state - Book viewing interactions **4.3 Device Management** - Convert from `devices.templ` - Token regeneration - Sync URL display - Device registration **Deliverable:** Collections, bookshelf, and devices features in TypeScript **Files:** - Create: `web/ts/features/collections/rules.ts` - Create: `web/ts/features/collections/bulk.ts` - Create: `web/ts/features/collections/types.ts` - Create: `web/ts/features/bookshelf/display.ts` - Create: `web/ts/features/bookshelf/pagination.ts` - Create: `web/ts/features/devices/token.ts` - Create: `web/ts/features/devices/types.ts` --- ### Phase 5: High Complexity Features (Linking, Bulk Operations) **Priority: Low | Effort: 5-6 days | Dependencies: Phase 4** **Tasks:** **5.1 Book Linking** - Convert matching logic from `unlinked_books.templ` - Search and match functionality - Manual link modal - Bulk auto-link - Bulk get suggestions **5.2 API Explorer** - Convert from `api_explorer.templ` - Request/response display - cURL generation - History tracking **Deliverable:** Book linking and API explorer in TypeScript **Files:** - Create: `web/ts/features/linking/matcher.ts` - Create: `web/ts/features/linking/bulk-link.ts` - Create: `web/ts/features/linking/manual-link.ts` - Create: `web/ts/features/linking/types.ts` - Create: `web/ts/features/api-explorer/request.ts` - Create: `web/ts/features/api-explorer/response.ts` - Create: `web/ts/features/api-explorer/curl.ts` --- ### Phase 6: Template Integration & Cleanup **Priority: High | Effort: 2-3 days | Dependencies: All phases** **Tasks:** **6.1 Update Templates** - Replace ` ``` --- ## Build & Deployment ### TypeScript Configuration **Update `tsconfig.json`:** ```json { "compilerOptions": { "target": "ES2020", "module": "none", "lib": ["ES2020", "DOM"], "outDir": "./web/static", "rootDir": "./web/ts", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": false, "sourceMap": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true }, "include": [ "web/ts/**/*.ts", "web/src/**/*.ts" ], "exclude": [ "node_modules" ] } ``` ### Build Scripts **Update `package.json`:** ```json { "scripts": { "build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch", "build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify", "build:ts": "tsc", "build:ts:watch": "tsc --watch", "build:ts:prod": "tsc --sourceMap false", "build:all": "npm run build:ts && npm run build:css:prod", "dev": "npm run build:ts:watch & npm run build:css" } } ``` ### Output Structure **Compiled JavaScript:** ``` web/static/ ├── core/ │ ├── toast.js # ✅ Already exists │ ├── theme.js # ✅ Already exists │ ├── header.js # ✅ Already exists │ ├── device-management.js # ✅ Already exists │ ├── storage.js # NEW │ ├── dom.js # NEW │ └── api.js # NEW ├── features/ │ ├── search/ │ │ └── search.js # NEW │ ├── collections/ │ │ ├── rules.js # NEW │ │ └── bulk.js # NEW │ ├── linking/ │ │ ├── matcher.js # NEW │ │ ├── bulk-link.js # NEW │ │ └── manual-link.js # NEW │ ├── bookshelf/ │ │ ├── display.js # NEW │ │ └── pagination.js # NEW │ ├── devices/ │ │ └── token.js # NEW │ ├── api-explorer/ │ │ ├── request.js # NEW │ │ ├── response.js # NEW │ │ └── curl.js # NEW │ └── admin/ │ ├── scan.js # NEW │ └── stats.js # NEW └── shared/ ├── events.js # NEW └── auth.js # NEW ``` --- ## Testing Strategy ### 1. Unit Testing (Optional) Test pure functions that don't depend on DOM: ```typescript // Test utilities describe('escapeHtml', () => { it('should escape HTML entities', () => { expect(escapeHtml(' ``` ### After: TypeScript Module + Event Delegation **TypeScript Module:** ```typescript // web/ts/features/collections/rules.ts import { apiClient } from '../../core/api.js'; import { escapeHtml } from '../../core/dom.js'; interface Rule { id: string; field: string; operator: string; value: string; } function renderRules(rules: Rule[]): void { const container = document.getElementById('rules-container'); if (!container) return; container.innerHTML = rules.map(rule => `