From d709510a28dc710e05952f3ececda6e0f7ecbbc8 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 18 Feb 2026 16:38:12 -0500 Subject: [PATCH] docs(typescript): update conversion plan with accurate line counts and template analysis --- TYPESCRIPT_CONVERSION_PLAN.md | 1310 +++++++++++++++++++++++++-------- 1 file changed, 992 insertions(+), 318 deletions(-) diff --git a/TYPESCRIPT_CONVERSION_PLAN.md b/TYPESCRIPT_CONVERSION_PLAN.md index 6776e54..1e8e364 100644 --- a/TYPESCRIPT_CONVERSION_PLAN.md +++ b/TYPESCRIPT_CONVERSION_PLAN.md @@ -2,7 +2,7 @@ ## 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. +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. @@ -15,32 +15,38 @@ Convert **~5,500 lines of inline JavaScript** across 13 template files into orga |--------|--------|-------|---------| | `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 | +| `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 (~5,500 lines) +### Inline JavaScript in Templates (~2,800 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 | +| `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: ~5,500 lines of inline JavaScript to convert** +**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 +- 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 -- Configure build process for all modules -- Add source maps for debugging +- 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 @@ -360,7 +785,27 @@ interface CollectionData { - Test form submissions - Validate error handling -**Deliverable:** Clean templates, all inline JavaScript replaced with TypeScript modules +**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 --- @@ -401,25 +846,43 @@ class RuleManager { ### 2. Type Definitions -**Reuse Go Handler Types:** +**Define TypeScript Interfaces Matching Go Handler JSON Tags:** ```typescript -// From internal/handlers/book_matching.go +// 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 { - ProgressID: string; - DeviceID: string; - TitleFromDevice: string; - SHA256: string; - PotentialMatches: PotentialMatchData[]; + 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 { - MediaItemID: string; - Title: string; - Author: string; - Confidence: number; + 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 @@ -439,18 +902,18 @@ type ToastType = 'error' | 'success' | 'info'; ```typescript async function deleteRule(ruleId: string): Promise { try { - const response = await apiClient.delete(`/api/collections/rules/${ruleId}`); + const response = await (window as any).api.delete(`/collections/rules/${ruleId}`); if (!response.ok) { const error = await response.json(); - window.showToast.error(error.message || 'Failed to delete rule'); + (window as any).showToast.error(error.message || 'Failed to delete rule'); return; } - window.showToast.success('Rule deleted'); + (window as any).showToast.success('Rule deleted'); removeRuleFromDOM(ruleId); } catch (error) { - window.showToast.error('Network error: Unable to connect to server'); + (window as any).showToast.error('Network error: Unable to connect to server'); console.error('Delete rule error:', error); } } @@ -458,62 +921,83 @@ async function deleteRule(ruleId: string): Promise { ### 4. API Client Pattern -**Centralized API Client:** +**Procedural API Client (No Classes):** ```typescript -// web/ts/core/api.ts -class APIClient { - private baseURL = '/api'; - private getAuthHeader(): string { - const token = localStorage.getItem('token'); - return token ? `Bearer ${token}` : ''; - } - - async get(url: string): Promise { - return fetch(`${this.baseURL}${url}`, { - headers: { - 'Authorization': this.getAuthHeader(), - 'Content-Type': 'application/json' - } - }); - } - - async post(url: string, data: unknown): Promise { - return fetch(`${this.baseURL}${url}`, { - method: 'POST', - headers: { - 'Authorization': this.getAuthHeader(), - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - } - - async delete(url: string): Promise { - return fetch(`${this.baseURL}${url}`, { - method: 'DELETE', - headers: { - 'Authorization': this.getAuthHeader() - } - }); - } +// web/src/api.ts +function getAuthHeader(): string { + const token = localStorage.getItem('token'); + return token ? `Bearer ${token}` : ''; } -export const apiClient = new APIClient(); +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 Delegation Pattern +### 5. Event Handler Pattern (Pragmatic Mix) -**Replace Inline Handlers:** +**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 + ``` -**Handle with Event Delegation:** ```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"]'); @@ -526,31 +1010,117 @@ document.addEventListener('click', async (e) => { } }); ``` +**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 ` + +
``` +**Options (outside scope of this plan):** +1. Add `action` attributes to HTMX forms for true progressive enhancement +2. Accept that login/register flows require JavaScript (current state) +3. Create separate server-rendered login fallback pages + +**This conversion plan maintains the current behavior** - it does not fix or worsen progressive enhancement. The goal is to convert inline JavaScript to TypeScript without breaking existing functionality. + +**HTMX Forms (Current State):** +```html + + + + + +
+
+``` + +**How Current HTMX Forms Work:** +- With JavaScript: HTMX intercepts submit, posts to API, updates DOM without reload +- Without JavaScript: **Form does NOT submit** (no action attribute) +- This is pre-existing behavior - not introduced by this conversion + +**When to Add TypeScript Form Interception:** +Use TypeScript interception only when you need custom behavior beyond HTMX: + +```typescript +// Example: Custom form handling with redirect +// web/src/auth.ts +document.addEventListener('DOMContentLoaded', () => { + const form = document.querySelector('form[action="/api/auth/login"]') as HTMLFormElement; + if (!form) return; + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + const response = await fetch('/api/auth/login', { + method: 'POST', + body: formData + }); + + if (response.ok) { + const data = await response.json(); + localStorage.setItem('token', data.access_token); + window.location.href = '/dashboard'; + } else { + (window as any).showToast.error('Login failed'); + } + }); +}); +``` + +**Guidelines:** +- Prefer HTMX for forms (already progressive enhanced) +- Add TypeScript only when you need custom logic +- All forms must have `action` and `method` attributes +- Test with JavaScript disabled before completing conversion +- HTMX: works without JS, enhanced with JS +- Custom TypeScript: must handle both cases + --- ## Build & Deployment ### TypeScript Configuration -**Update `tsconfig.json`:** +**Current `tsconfig.json` (already configured correctly):** ```json { "compilerOptions": { @@ -558,21 +1128,19 @@ document.addEventListener('click', async (e) => { "module": "none", "lib": ["ES2020", "DOM"], "outDir": "./web/static", - "rootDir": "./web/ts", + "rootDir": "./web/src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": false, - "sourceMap": true, + "sourceMap": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "resolveJsonModule": true + "noFallthroughCasesInSwitch": true }, "include": [ - "web/ts/**/*.ts", "web/src/**/*.ts" ], "exclude": [ @@ -581,63 +1149,54 @@ document.addEventListener('click', async (e) => { } ``` +**Key points:** +- `module: "none"` = Browser globals (no ES modules) +- `rootDir: "./web/src"` = All TypeScript in src/ +- `outDir: "./web/static"` = Flat output structure +- No bundler needed - TypeScript compiles directly to browser-compatible JavaScript + ### Build Scripts -**Update `package.json`:** +**Current `package.json` (no changes needed):** ```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" + "build:ts:watch": "tsc --watch" } } ``` +**Note:** No additional build scripts needed. Docker handles production builds, and `tsc` compilation is environment-agnostic. Run `npm run build:ts:watch` for development. + ### Output Structure -**Compiled JavaScript:** +**Compiled JavaScript (flat structure in web/static/):** ``` 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 +├── toast.js # ✅ Already exists +├── theme.js # ✅ Already exists +├── header.js # ✅ Already exists +├── device-management.js # ✅ Already exists +├── search.js # NEW (from search.js) +├── storage.js # NEW +├── dom.js # NEW +├── api.js # NEW +├── collections.js # NEW +├── linking.js # NEW +├── bookshelf.js # NEW +├── api-explorer.js # NEW +├── admin.js # NEW +├── analytics.js # NEW +├── queue.js # NEW +├── conflicts.js # NEW +└── docs.js # NEW ``` +**No subdirectories** - TypeScript with `module: "none"` compiles to flat output matching source structure. + --- ## Testing Strategy @@ -781,38 +1340,131 @@ git checkout backup-before-collections -- templates/collections.templ |-------|----------|--------------|--------------| | Phase 1: Standalone Conversion | 1 day | None | Search converted to TS | | Phase 2: Core Utilities | 1-2 days | None | Storage, DOM, API, events modules | -| Phase 3: Low Complexity | 3-4 days | Phase 2 | Search, header, admin in TS | -| Phase 4: Medium Complexity | 4-5 days | Phase 2, 3 | Collections, bookshelf, devices in TS | -| Phase 5: High Complexity | 5-6 days | Phase 4 | Linking, API explorer in TS | -| Phase 6: Integration & Cleanup | 2-3 days | All phases | Clean templates, verified functionality | -| **Total** | **16-21 days** | | **Full TypeScript conversion** | +| Phase 3: Low Complexity | 2-3 days | Phase 2 | Search, header, admin, analytics in TS | +| Phase 4: Medium Complexity | 3-4 days | Phase 2, 3 | Collections, bookshelf, devices, queue in TS | +| Phase 5: High Complexity | 4-5 days | Phase 4 | Linking, API explorer, conflicts in TS | +| Phase 6: Integration & Cleanup | 2-3 days | All phases | Clean templates, docs search, verified functionality | +| **Total** | **13-18 days** | | **Full TypeScript conversion** | + +**Note:** Timeline reduced from initial estimate based on actual line count verification (~2,800 lines vs original ~6,300 estimate). Three templates (conflicts, queue, analytics) have NO inline script blocks and only need TypeScript modules for their onclick handlers. --- ## Success Criteria -- [ ] All inline JavaScript removed from templates +- [ ] All inline JavaScript logic removed from templates (functions, fetch calls, state management) - [ ] All features work with TypeScript - [ ] TypeScript compilation passes with strict mode -- [ ] Progressive enhancement maintained (pages work without JS) +- [ ] No regression in existing progressive enhancement behavior (HTMX forms unchanged) - [ ] No type `any` (except for legacy API responses) -- [ ] Event delegation replaces inline handlers -- [ ] Build process integrated into npm scripts +- [ ] Event handlers use pragmatic mix of onclick, data-action, and form interception as appropriate +- [ ] **Type definitions use snake_case field names matching actual API responses** +- [ ] **Type definitions verified against endpoint returns (not just handler structs)** +- [ ] Type definition comments reference source files: + - Database rows: `// Matches internal/database/queries.sql.go:SearchMediaItemsRow` + - Handler structs: `// Matches handlers.BookInfo JSON: media_item_id, title, author` +- [ ] `import type { ... } from './types/api'` pattern used for type checking - [ ] Full manual testing completed - [ ] No regression in functionality - [ ] Bundle sizes monitored and optimized - [ ] SSR still works for all initial page loads - [ ] All CRUD operations use existing `/api/*` endpoints +- [ ] Login template duplicate theme logic removed (lines 62-88) +- [ ] Search functionality uses correct types matching database SearchMediaItemsRow +- [ ] Analytics page loads data and displays charts correctly +- [ ] Queue management CRUD operations work (process, clear, filter) +- [ ] Conflict resolution works (individual and bulk operations) +- [ ] Documentation search functionality works +- [ ] All 10 missing type definitions added (analytics, queue, conflicts) +- [ ] Total converted: ~2,800 lines of inline script blocks --- ## Next Steps 1. **Review and approve** this plan -2. **Setup Phase 1**: Convert `search.js` → TypeScript -3. **Begin Phase 2**: Create core utility modules +2. **Setup Phase 1**: Convert `search.js` → TypeScript (ensure snake_case field names) +3. **Begin Phase 2**: Create core utility modules (including `types/api.d.ts` with snake_case types) 4. **Iterate** through phases 3-5 (feature conversion) -5. **Phase 6**: Final integration and cleanup +5. **Phase 6**: Final integration and cleanup (remove login.templ duplicate theme code, lines 62-88) + +--- + +## Appendix: Common Pitfalls + +### Database Rows vs Handler Structs + +**Problem:** Some handler files define structs that don't match what the endpoint actually returns. + +**Example:** +```go +// internal/handlers/search.go +// This struct is DEFINED but never used! +type MediaItemSummary struct { + ID string `json:"id"` + Title string `json:"title"` + Authors []SearchAuthor `json:"authors"` // Wrong! API returns string, not array +} + +// internal/handlers/media.go +// The endpoint actually returns database rows: +func (mh *MediaHandler) SearchMediaItems(c echo.Context) error { + partialResults, err := mh.db.SearchMediaItems(...) // Returns []SearchMediaItemsRow + return c.JSON(http.StatusOK, partialResults) // Has library_id, author (string), etc. +} +``` + +**Solution:** +1. Grep the endpoint function to find what it actually returns +2. Check the return statement: `return c.JSON(..., variable)` +3. Find that variable's type definition +4. Use that type's JSON tags, not some unrelated handler struct + +**Verification:** +```bash +# Find endpoint implementation +rg "func.*SearchMediaItems" internal/handlers/ +# Check what it returns +rg -A 5 "return c.JSON" internal/handlers/media.go +# Find the returned type +rg "type SearchMediaItemsRow" internal/database/ +``` + +--- + +## Appendix: Progressive Enhancement Test Checklist + +Use this checklist when converting templates to ensure progressive enhancement is maintained: + +**Before removing inline JavaScript:** +- [ ] Identify all forms in the template +- [ ] Verify each form has `action` and `method` attributes +- [ ] Verify each form has a submit button (not just button with onclick) +- [ ] Test form submission works with JavaScript disabled +- [ ] Identify all critical user flows (login, create, update, delete) +- [ ] Verify each critical flow has a non-JavaScript fallback + +**After TypeScript conversion:** +- [ ] Test with JavaScript disabled (DevTools → Disable JavaScript) +- [ ] Verify forms still submit with full page reload +- [ ] Verify navigation links work (full page reload) +- [ ] Verify all data is visible on initial load (SSR) +- [ ] Test critical user flows work without JavaScript +- [ ] Enable JavaScript and verify enhanced experience + +**What to test without JavaScript:** +- [ ] Page loads and displays data +- [ ] Forms can be submitted +- [ ] Navigation links work +- [ ] No console errors + +**What requires JavaScript (expected to fail without JS):** +- [ ] Toast notifications +- [ ] AJAX form submission (no page reload) +- [ ] Modal dialogs +- [ ] Dynamic content updates +- [ ] Keyboard navigation shortcuts +- [ ] Auto-complete search --- @@ -857,13 +1509,11 @@ git checkout backup-before-collections -- templates/collections.templ ``` -### After: TypeScript Module + Event Delegation +### After: TypeScript Module (Browser Globals) **TypeScript Module:** ```typescript -// web/ts/features/collections/rules.ts -import { apiClient } from '../../core/api.js'; -import { escapeHtml } from '../../core/dom.js'; +// web/src/collections.ts interface Rule { id: string; @@ -872,21 +1522,15 @@ interface Rule { value: string; } -function renderRules(rules: Rule[]): void { - const container = document.getElementById('rules-container'); - if (!container) return; - - container.innerHTML = rules.map(rule => ` -
- ${escapeHtml(rule.field)} - -
- `).join(''); +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 apiClient.get(`/collections/${collectionId}/rules`); + const response = await (window as any).api.get(`/collections/${collectionId}/rules`); if (!response.ok) { throw new Error('Failed to load rules'); @@ -896,7 +1540,35 @@ async function loadRules(collectionId: string): Promise { renderRules(rules); } catch (error) { console.error('Load rules error:', error); - window.showToast.error('Failed to load rules'); + (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'); } } @@ -906,45 +1578,41 @@ async function deleteRule(collectionId: string, ruleId: string): Promise { } try { - const response = await apiClient.delete(`/collections/${collectionId}/rules/${ruleId}`); + const response = await (window as any).api.delete(`/collections/${collectionId}/rules/${ruleId}`); if (!response.ok) { throw new Error('Failed to delete rule'); } - window.showToast.success('Rule deleted'); + (window as any).showToast.success('Rule deleted'); - // Remove from DOM const ruleElement = document.querySelector(`[data-rule-id="${ruleId}"]`); ruleElement?.remove(); } catch (error) { console.error('Delete rule error:', error); - window.showToast.error('Failed to delete rule'); + (window as any).showToast.error('Failed to delete rule'); } } -// Event delegation for delete buttons -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; - const collectionId = deleteBtn.dataset.collectionId; - - if (ruleId && collectionId) { - deleteRule(collectionId, ruleId); - } - } -}); +// Export to window for HTML access +(window as any).loadRules = loadRules; +(window as any).deleteRule = deleteRule; // Initialize on page load -document.addEventListener('DOMContentLoaded', () => { +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:** @@ -954,9 +1622,9 @@ document.addEventListener('DOMContentLoaded', () => { - - - + + +
@@ -964,9 +1632,7 @@ document.addEventListener('DOMContentLoaded', () => { { range .Rules }
{ .Field } -
@@ -977,16 +1643,24 @@ document.addEventListener('DOMContentLoaded', () => { ``` **Benefits:** -- ✅ No inline JavaScript -- ✅ Event delegation (one listener) -- ✅ Type-safe API calls -- ✅ Better error handling -- ✅ Reusable module +- ✅ 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* +*Architecture: Hybrid SSR + TypeScript CRUD (Browser Globals, No ES Modules)*