# TypeScript Conversion Plan for Bookhoard ## Executive Summary Convert **~2,800 lines of inline JavaScript** across 14 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` | 114 | Header dropdowns, theme/user menus | | `web/src/device-management.ts` | `device-management.js` | 102 | 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 (~2,800 lines) | Template | Script Lines | Primary Functions | |----------|--------------|-------------------| | `dashboard.templ` | ~456 | Statistics, recent activity | | `docs.templ` | ~392 | Documentation search, sidebar toggle | | `devices.templ` | ~386 | Token regeneration, sync URL display | | `collections.templ` | ~362 | Bulk operations, filtering | | `unlinked_books.templ` | ~312 | Book matching, linking, bulk operations | | `collection_rules.templ` | ~239 | Rule CRUD operations, testing | | `admin_library.templ` | ~229 | Admin library scan | | `bookshelf.templ` | ~200 | Book viewing, pagination | | `api_explorer.templ` | ~103 | API testing, cURL generation | | `admin.templ` | ~29 | Admin dashboard actions | | `login.templ` | ~29 | Login theme selection | | `index.templ` | ~24 | Landing page theme preview | | `admin_profile.templ` | ~11 | Admin profile management | | `progress.templ` | ~6 | Reading progress | | `conflicts.templ` | 0 | **No inline JS - uses onclick handlers only** | | `queue.templ` | 0 | **No inline JS - uses onclick handlers only** | | `analytics.templ` | 0 | **No inline JS - uses onclick handlers only** | **Total: ~2,800 lines of inline JavaScript to convert** **Note:** `conflicts.templ`, `queue.templ`, and `analytics.templ` have NO `` in `` - **Why:** Theme logic already exists in `web/src/theme.ts`, remove duplication - **Verify:** Theme dropdown still works after cleanup **6.2 Verify Progressive Enhancement** - Test all pages with JavaScript disabled - Ensure forms submit with full page reload (see Progressive Enhancement Pattern below) - Verify critical paths work without JS **6.3 Build Configuration** - Update `tsconfig.json` with new file structure (if needed) - Verify all `.ts` files compile to `.js` in `web/static/` - Add source maps for debugging (optional) **6.4 Testing** - Verify all features work with TypeScript - Check keyboard navigation - Test form submissions - Validate error handling **6.5 Documentation Search Integration** - Create `web/src/docs.ts` - `toggleSidebar()` - sidebar visibility toggle for mobile - Documentation search functionality: - Initialize lunr.js search index - Search input debouncing - Display search results - Navigate to search results - Remove inline script from `docs.templ` - **Note:** Template uses external libraries (lunr.js, lunr-flex.js) for search - Keep it simple - no complex search UI, just basic functionality **Build Verification:** - Run `npm run build:ts` to verify entire project compiles - Confirm all `.js` files generated in `web/static/` - No TypeScript compilation errors - Full manual testing of all features - Test documentation search functionality - Verify sidebar toggle works on mobile **Deliverable:** Clean templates, all inline JavaScript replaced with TypeScript modules, documentation search functional --- ## Code Style Guidelines ### 1. Procedural/Imperative with Functional Techniques **✅ GOOD:** ```typescript // Pure function, no classes function createRuleItem(rule: Rule): HTMLElement { const div = document.createElement('div'); div.className = 'rule-item'; div.innerHTML = renderRuleHTML(rule); return div; } // Event delegation (one listener) document.addEventListener('click', (e) => { const target = e.target as HTMLElement; const deleteBtn = target.closest('[data-action="delete-rule"]'); if (deleteBtn) { const ruleId = deleteBtn.dataset.ruleId; deleteRule(ruleId); } }); ``` **❌ BAD:** ```typescript // Class-based, OOP class RuleManager { private rules: Rule[] = []; constructor() { ... } addRule(rule: Rule) { ... } } ``` ### 2. Type Definitions **Define TypeScript Interfaces Matching Go Handler JSON Tags:** ```typescript // Matches internal/handlers/book_matching.go // Check JSON tags: `json:"progress_id"`, `json:"device_id"`, etc. // Reference: Find struct definition in Go handler file interface UnlinkedBookData { progress_id: string; device_id: string; device_name: string; device_type: 'koreader' | 'kobo' | 'web'; title_from_device: string; file_path: string; sha256: string; last_sync_time: string; confidence_score: number; potential_matches: PotentialMatchData[]; } interface PotentialMatchData { media_item_id: string; title: string; author: string; confidence: number; cover_image_path?: string; } ``` **Important: Database Rows vs Handler Structs** Some endpoints return database rows directly (e.g., `SearchMediaItemsRow`), not handler-defined structs. Always check the endpoint's return statement: ```go // internal/handlers/media.go:SearchMediaItems() return c.JSON(http.StatusOK, partialResults) // Returns []SearchMediaItemsRow from database ``` When in doubt, grep the endpoint function and check what it actually returns. **Only Create Frontend-Specific Types When Necessary:** ```typescript // OK: Frontend-specific state type SearchState = { query: string; results: MediaItem[]; selectedIndex: number; }; // OK: UI-specific config type ToastType = 'error' | 'success' | 'info'; ``` ### 3. Error Handling **Integrate with Toast System:** ```typescript async function deleteRule(ruleId: string): Promise { try { const response = await (window as any).api.delete(`/collections/rules/${ruleId}`); if (!response.ok) { const error = await response.json(); (window as any).showToast.error(error.message || 'Failed to delete rule'); return; } (window as any).showToast.success('Rule deleted'); removeRuleFromDOM(ruleId); } catch (error) { (window as any).showToast.error('Network error: Unable to connect to server'); console.error('Delete rule error:', error); } } ``` ### 4. API Client Pattern **Procedural API Client (No Classes):** ```typescript // web/src/api.ts function getAuthHeader(): string { const token = localStorage.getItem('token'); return token ? `Bearer ${token}` : ''; } async function apiGet(url: string): Promise { return fetch(`/api${url}`, { headers: { 'Authorization': getAuthHeader(), 'Content-Type': 'application/json' } }); } async function apiPost(url: string, data: unknown): Promise { return fetch(`/api${url}`, { method: 'POST', headers: { 'Authorization': getAuthHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } async function apiDelete(url: string): Promise { return fetch(`/api${url}`, { method: 'DELETE', headers: { 'Authorization': getAuthHeader() } }); } async function apiPut(url: string, data: unknown): Promise { return fetch(`/api${url}`, { method: 'PUT', headers: { 'Authorization': getAuthHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } // Export to window for use in other modules (window as any).api = { get: apiGet, post: apiPost, delete: apiDelete, put: apiPut }; ``` ### 5. Event Handler Pattern (Pragmatic Mix) **Use onclick for Simple Static Content:** ```html ``` **When to use:** - Static server-rendered HTML - Simple function calls to well-named functions - When you want explicit, readable HTML **Use data-action + Event Delegation for Dynamic Content:** ```html ``` ```typescript // Event delegation (one listener handles all dynamic items) document.addEventListener('click', async (e) => { const target = e.target as HTMLElement; const deleteBtn = target.closest('[data-action="delete-rule"]'); if (deleteBtn) { const ruleId = deleteBtn.dataset.ruleId; if (ruleId && confirm('Are you sure you want to delete this rule?')) { await deleteRule(ruleId); } } }); ``` **When to use:** - Dynamic content (rows added after page load) - Lists where every item needs the same handler - When event delegation genuinely simplifies the code **Use Form Interception for Progressive Enhancement:** ```html
``` **When to use:** - Forms that must work without JavaScript - Progressive enhancement is required - Critical user flows **❌ DON'T:** - Mandate data-action everywhere (over-engineering) - Remove all onclick handlers (unnecessary refactoring) **✅ DO:** - Remove inline ` ``` ### After: TypeScript Module (Browser Globals) **TypeScript Module:** ```typescript // web/src/collections.ts interface Rule { id: string; field: string; operator: string; value: string; } function escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function loadRules(collectionId: string): Promise { try { const response = await (window as any).api.get(`/collections/${collectionId}/rules`); if (!response.ok) { throw new Error('Failed to load rules'); } const rules: Rule[] = await response.json(); renderRules(rules); } catch (error) { console.error('Load rules error:', error); (window as any).showToast.error('Failed to load rules'); } } function renderRules(collectionId: string, rules: Rule[]): void { const container = document.getElementById('rules-container'); if (!container) return; container.innerHTML = rules.map(rule => `
${escapeHtml(rule.field)}
`).join(''); } async function loadRules(collectionId: string): Promise { try { const response = await (window as any).api.get(`/collections/${collectionId}/rules`); if (!response.ok) { throw new Error('Failed to load rules'); } const rules: Rule[] = await response.json(); renderRules(collectionId, rules); } catch (error) { console.error('Load rules error:', error); (window as any).showToast.error('Failed to load rules'); } } async function deleteRule(collectionId: string, ruleId: string): Promise { if (!confirm('Are you sure you want to delete this rule?')) { return; } try { const response = await (window as any).api.delete(`/collections/${collectionId}/rules/${ruleId}`); if (!response.ok) { throw new Error('Failed to delete rule'); } (window as any).showToast.success('Rule deleted'); const ruleElement = document.querySelector(`[data-rule-id="${ruleId}"]`); ruleElement?.remove(); } catch (error) { console.error('Delete rule error:', error); (window as any).showToast.error('Failed to delete rule'); } } // Export to window for HTML access (window as any).loadRules = loadRules; (window as any).deleteRule = deleteRule; // Initialize on page load if (typeof document !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeCollectionRules); } else { initializeCollectionRules(); } } function initializeCollectionRules() { const collectionId = document.body.dataset.collectionId; if (collectionId) { loadRules(collectionId); } } ``` **Cleaned Template:** ```html
{ range .Rules }
{ .Field }
{ end }
``` **Benefits:** - ✅ No inline JavaScript logic (functions, fetch calls, state management) - ✅ Simple onclick calls are explicit and easy to understand - ✅ Type-safe API calls through procedural api module - ✅ Better error handling with toast integration - ✅ Reusable module attached to window - ✅ SSR still works (initial rules in HTML) - ✅ Progressive enhancement (form can submit without JS) - ✅ No ES modules or bundler needed (browser globals) --- *Generated: 2025-02-17* *Updated: 2025-02-18 (Revised to match existing codebase patterns)* *Updated: 2025-02-18 (Fixed file paths, removed unnecessary build scripts, added manual type sync strategy)* *Updated: 2025-02-18 (Changed to .d.ts for type definitions, added build verification steps, documented progressive enhancement pattern)* *Updated: 2025-02-18 (Corrected type definitions to use snake_case matching Go JSON tags, simplified progressive enhancement section, added missing types)* *Updated: 2025-02-18 (Clarified database row vs handler struct confusion, documented template-handler type sharing, added HTMX progressive enhancement details, added common pitfalls appendix)* *Updated: 2025-02-18 (Added missing templates: analytics, queue, conflicts, docs; added 10 missing type definitions; updated line counts to ~6,300; revised timeline to 20-25 days; added all missing phases and tasks)* *Updated: 2025-02-18 (VERIFIED CORRECTIONS: Fixed line counts (~2,800 actual vs ~6,300 estimated), corrected analytics/queue/conflicts type definitions to match actual Go structs, noted HTMX forms lack action attributes, reduced timeline to 13-18 days)* *Follows: PROJECT_GUIDELINES.md* *Architecture: Hybrid SSR + TypeScript CRUD (Browser Globals, No ES Modules)*