`
- **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)*