feat(typescript): add core infrastructure modules
- Add centralized API type definitions (types/api.d.ts) - Interfaces for all API responses matching Go handler JSON - Snake_case field names matching actual API responses - Source file references in comments for verification - Add API client module (api.ts) - Procedural get/post/put/delete functions - Automatic auth header injection - Exported to window for cross-module access - Add DOM utilities (dom.ts) - escapeHtml for safe HTML rendering - querySelector wrappers with null checks - Element creation helpers - Add event delegation helpers (events.ts) - Reusable event delegation pattern - Data attribute selectors for dynamic content - Add localStorage wrapper (storage.ts) - Type-safe token management - Theme persistence helpers
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,781 +0,0 @@
|
|||||||
# TypeScript Conversion Plan Verification Checklist
|
|
||||||
|
|
||||||
Use this checklist to comprehensively audit the TypeScript Conversion Plan in a single pass. Each item includes verification steps to confirm accuracy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Type Definition Verification
|
|
||||||
|
|
||||||
### 1.1 Verify All Types Match Actual API Responses
|
|
||||||
|
|
||||||
**For each type definition in `web/src/types/api.d.ts` (planned):**
|
|
||||||
|
|
||||||
- [ ] Locate the API endpoint in `internal/handlers/*.go`
|
|
||||||
- [ ] Check what the endpoint **actually returns** (return statement)
|
|
||||||
- [ ] Determine if it returns:
|
|
||||||
- Handler-defined struct (e.g., `handlers.BookInfo`)
|
|
||||||
- Database row (e.g., `database.SearchMediaItemsRow`)
|
|
||||||
- Map/slice of either
|
|
||||||
- [ ] Find the source type definition:
|
|
||||||
- Handler structs: `internal/handlers/*.go`
|
|
||||||
- Database rows: `internal/database/queries.sql.go`
|
|
||||||
- [ ] Verify all JSON tags match TypeScript interface fields (snake_case)
|
|
||||||
- [ ] Map pgtype fields to TypeScript types:
|
|
||||||
- `pgtype.Text` → `string | undefined`
|
|
||||||
- `pgtype.UUID` → `string`
|
|
||||||
- `pgtype.Timestamp` → `string` (ISO datetime)
|
|
||||||
- `pgtype.Numeric` → `number` or `string`
|
|
||||||
- `pgtype.Bool` → `boolean`
|
|
||||||
- `[]string` → `string[]`
|
|
||||||
|
|
||||||
**Common Pitfalls:**
|
|
||||||
- Handler structs may be defined but unused (e.g., `search.go`'s `MediaItemSummary`)
|
|
||||||
- Endpoints may return database rows directly, not handler structs
|
|
||||||
- JSON tags may use snake_case while Go fields use PascalCase
|
|
||||||
- Arrays vs single values (e.g., `author` string vs `authors` array)
|
|
||||||
|
|
||||||
**Verification Commands:**
|
|
||||||
```bash
|
|
||||||
# Find endpoint implementation
|
|
||||||
rg "func.*SearchMediaItems" internal/handlers/
|
|
||||||
|
|
||||||
# Check return type
|
|
||||||
rg -A 10 "func.*SearchMediaItems" internal/handlers/media.go | grep "return c.JSON"
|
|
||||||
|
|
||||||
# Find type definition
|
|
||||||
rg "type SearchMediaItemsRow struct" internal/database/queries.sql.go
|
|
||||||
|
|
||||||
# Check JSON tags
|
|
||||||
rg 'json:"' internal/database/queries.sql.go | grep "SearchMediaItemsRow"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Verify No Duplicate/Conflicting Type Definitions
|
|
||||||
|
|
||||||
- [ ] No two TypeScript interfaces describe the same API response
|
|
||||||
- [ ] No interface fields contradict actual JSON response
|
|
||||||
- [ ] No unused handler structs are referenced in type comments
|
|
||||||
- [ ] All inline types in `.ts` files could move to `types/api.d.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. API Contract Verification
|
|
||||||
|
|
||||||
### 2.1 Verify All API Endpoints Referenced in Plan Exist
|
|
||||||
|
|
||||||
**For each endpoint mentioned:**
|
|
||||||
|
|
||||||
- [ ] Endpoint exists in `internal/handlers/*.go`
|
|
||||||
- [ ] Route is registered in routing code
|
|
||||||
- [ ] HTTP method matches (GET/POST/PUT/DELETE)
|
|
||||||
- [ ] Response format matches plan's type definitions
|
|
||||||
- [ ] Authentication requirements match plan assumptions
|
|
||||||
|
|
||||||
**Check Commands:**
|
|
||||||
```bash
|
|
||||||
# Find endpoint definition
|
|
||||||
rg "POST.*collections.*rules" internal/handlers/
|
|
||||||
|
|
||||||
# Check route registration
|
|
||||||
rg "collections.*rules" cmd/server/ or internal/router/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Verify Authentication Patterns
|
|
||||||
|
|
||||||
- [ ] All API endpoints that require auth are documented
|
|
||||||
- [ ] Token storage approach matches (`localStorage.getItem('token')`)
|
|
||||||
- [ ] Auth header format consistent (`Bearer ${token}`)
|
|
||||||
- [ ] 401 handling documented (token clearing, redirect)
|
|
||||||
|
|
||||||
### 2.3 Verify Error Response Formats
|
|
||||||
|
|
||||||
- [ ] Error responses use consistent structure (`{"error": "message"}`)
|
|
||||||
- [ ] Toast integration documented for all error cases
|
|
||||||
- [ ] Network error handling documented
|
|
||||||
- [ ] Validation error handling documented (400 responses)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Cross-Reference Verification
|
|
||||||
|
|
||||||
### 3.1 Template-Handler Type Sharing
|
|
||||||
|
|
||||||
**For each template that imports handlers:**
|
|
||||||
|
|
||||||
- [ ] Template imports `internal/handlers` package
|
|
||||||
- [ ] Template uses handler types for SSR data (e.g., `handlers.BookInfo`)
|
|
||||||
- [ ] TypeScript interfaces match the same handler JSON responses
|
|
||||||
- [ ] No duplicate type definitions between handlers and templates
|
|
||||||
- [ ] Template-only types are clearly marked (e.g., `PageData`, `UnsafeHTML`)
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
```bash
|
|
||||||
# Find templates importing handlers
|
|
||||||
rg 'import.*handlers' templates/*.templ
|
|
||||||
|
|
||||||
# Check handler type usage in templates
|
|
||||||
rg 'handlers\.(CollectionData|BookInfo|UserProfile)' templates/*.templ
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Existing TypeScript Module Patterns
|
|
||||||
|
|
||||||
**Verify all existing `.ts` modules follow documented patterns:**
|
|
||||||
|
|
||||||
- [ ] Procedural style (no classes, no `this`)
|
|
||||||
- [ ] Functions exported to `window` object
|
|
||||||
- [ ] No ES module imports/exports (browser globals)
|
|
||||||
- [ ] Proper type annotations (`import type` for type-only imports)
|
|
||||||
- [ ] Consistent error handling with toast integration
|
|
||||||
|
|
||||||
**Check existing modules:**
|
|
||||||
- `web/src/toast.ts`
|
|
||||||
- `web/src/theme.ts`
|
|
||||||
- `web/src/header.ts`
|
|
||||||
- `web/src/device-management.ts`
|
|
||||||
|
|
||||||
### 3.3 JavaScript to TypeScript Mapping
|
|
||||||
|
|
||||||
**For each inline script in templates:**
|
|
||||||
|
|
||||||
- [ ] Identify all `<script>` blocks in templates
|
|
||||||
- [ ] Count total lines of inline JavaScript
|
|
||||||
- [ ] Verify plan's line count estimates are accurate
|
|
||||||
- [ ] Check for duplicate functionality (e.g., login.templ theme code vs theme.ts)
|
|
||||||
- [ ] Verify no critical functionality is missed in conversion plan
|
|
||||||
|
|
||||||
**Verification Commands:**
|
|
||||||
```bash
|
|
||||||
# Count script blocks
|
|
||||||
rg '<script>' templates/*.templ | wc -l
|
|
||||||
|
|
||||||
# Count inline JS lines
|
|
||||||
rg '<script>' templates/*.templ -A 100 | grep -v '^--$' | wc -l
|
|
||||||
|
|
||||||
# Find duplicate functions
|
|
||||||
rg "function applyTheme" templates/*.templ web/src/*.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Architecture Verification
|
|
||||||
|
|
||||||
### 4.1 Verify Hybrid SSR Architecture Preserved
|
|
||||||
|
|
||||||
**For each feature conversion:**
|
|
||||||
|
|
||||||
- [ ] Initial page load uses SSR (Go templates with data)
|
|
||||||
- [ ] TypeScript only handles CRUD operations (create, update, delete)
|
|
||||||
- [ ] No new HTML endpoints needed (use existing `/api/*` endpoints)
|
|
||||||
- [ ] Service layer remains source of truth
|
|
||||||
- [ ] No handler duplication between SSR and API
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Verify templates use handler data
|
|
||||||
rg 'CollectionData\|BookInfo\|UserProfile' templates/*.templ
|
|
||||||
|
|
||||||
# Verify API endpoints exist
|
|
||||||
rg 'GET.*collections\|POST.*collections' internal/handlers/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Verify Progressive Enhancement
|
|
||||||
|
|
||||||
**For each form/user flow:**
|
|
||||||
|
|
||||||
- [ ] Form has `action` and `method` attributes
|
|
||||||
- [ ] Form submits correctly without JavaScript (test in browser)
|
|
||||||
- [ ] HTMX attributes enhance but don't replace standard behavior
|
|
||||||
- [ ] Critical paths work without JS (login, create, update, delete)
|
|
||||||
- [ ] TypeScript interception is optional enhancement, not requirement
|
|
||||||
|
|
||||||
**Test Procedure:**
|
|
||||||
1. Open DevTools → Disable JavaScript
|
|
||||||
2. Navigate to page
|
|
||||||
3. Verify form submits with full page reload
|
|
||||||
4. Re-enable JavaScript
|
|
||||||
5. Verify enhanced behavior works
|
|
||||||
|
|
||||||
### 4.3 Verify No Backend Modifications for Frontend Tasks
|
|
||||||
|
|
||||||
**For each TypeScript module:**
|
|
||||||
|
|
||||||
- [ ] Only uses existing API endpoints
|
|
||||||
- [ ] No new handler functions needed
|
|
||||||
- [ ] No database schema changes needed
|
|
||||||
- [ ] No migration files needed
|
|
||||||
- [ ] All functionality achievable with current API
|
|
||||||
|
|
||||||
**Red Flags:**
|
|
||||||
- Plan mentions "need to add endpoint for..."
|
|
||||||
- TypeScript code requires new backend structure
|
|
||||||
- Data transformation needed in handler
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Build & Deployment Verification
|
|
||||||
|
|
||||||
### 5.1 Verify TypeScript Configuration
|
|
||||||
|
|
||||||
- [ ] `tsconfig.json` exists and is correctly configured
|
|
||||||
- [ ] `module: "none"` for browser globals
|
|
||||||
- [ ] `outDir: "./web/static"` for flat output
|
|
||||||
- [ ] `rootDir: "./web/src"` for source organization
|
|
||||||
- [ ] `strict: true` for type safety
|
|
||||||
- [ ] `.d.ts` files don't generate JS output
|
|
||||||
- [ ] Source maps configured appropriately
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Verify tsconfig.json
|
|
||||||
cat tsconfig.json
|
|
||||||
|
|
||||||
# Test compilation
|
|
||||||
npm run build:ts
|
|
||||||
|
|
||||||
# Check output
|
|
||||||
ls -la web/static/*.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Verify Build Scripts
|
|
||||||
|
|
||||||
- [ ] `package.json` has correct build scripts
|
|
||||||
- [ ] `npm run build:ts` compiles TypeScript
|
|
||||||
- [ ] `npm run build:ts:watch` for development
|
|
||||||
- [ ] No additional bundlers needed (webpack, vite, etc.)
|
|
||||||
- [ ] Build output matches plan's expectations
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Verify scripts
|
|
||||||
cat package.json | grep -A 5 '"scripts"'
|
|
||||||
|
|
||||||
# Test build
|
|
||||||
npm run build:ts
|
|
||||||
|
|
||||||
# Verify output structure
|
|
||||||
ls -la web/static/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Verify Docker/Podman Integration
|
|
||||||
|
|
||||||
- [ ] TypeScript compilation happens during Docker build
|
|
||||||
- [ ] No local binary builds needed
|
|
||||||
- [ `.gitignore` excludes `.js` files but includes `.ts` files
|
|
||||||
- [ ] Production build includes minified CSS
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Check Dockerfile
|
|
||||||
rg "npm run build" Dockerfile
|
|
||||||
|
|
||||||
# Check .gitignore
|
|
||||||
rg "\.js$" .gitignore
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Phase Dependency Verification
|
|
||||||
|
|
||||||
### 6.1 Verify Phase Ordering
|
|
||||||
|
|
||||||
**For each phase:**
|
|
||||||
|
|
||||||
- [ ] All dependencies are completed in previous phases
|
|
||||||
- [ ] No circular dependencies between phases
|
|
||||||
- [ ] Core utilities (Phase 2) before feature modules (Phases 3-5)
|
|
||||||
- [ ] Standalone files (Phase 1) before inline conversion
|
|
||||||
- [ ] Template cleanup (Phase 6) after all conversions complete
|
|
||||||
|
|
||||||
**Check dependency graph:**
|
|
||||||
```
|
|
||||||
Phase 1 (search.js) → No dependencies ✓
|
|
||||||
Phase 2 (utils) → No dependencies ✓
|
|
||||||
Phase 3 (low complexity) → Needs Phase 2 ✓
|
|
||||||
Phase 4 (medium complexity) → Needs Phase 2, 3 ✓
|
|
||||||
Phase 5 (high complexity) → Needs Phase 4 ✓
|
|
||||||
Phase 6 (cleanup) → Needs all phases ✓
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Verify Phase Completeness
|
|
||||||
|
|
||||||
**For each phase:**
|
|
||||||
|
|
||||||
- [ ] All deliverables are listed
|
|
||||||
- [ ] All files to create/delete/update are specified
|
|
||||||
- [ ] Build verification steps are included
|
|
||||||
- [ ] Testing steps are documented
|
|
||||||
- [ ] Rollback strategy is mentioned
|
|
||||||
|
|
||||||
### 6.3 Verify Timeline Estimates
|
|
||||||
|
|
||||||
**For each phase:**
|
|
||||||
|
|
||||||
- [ ] Effort estimates are realistic
|
|
||||||
- [ ] Line counts match actual code
|
|
||||||
- [ ] Total duration is achievable (16-21 days)
|
|
||||||
- [ ] No phase exceeds 6 days (break down if needed)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Code Style Verification
|
|
||||||
|
|
||||||
### 7.1 Verify Procedural Style Compliance
|
|
||||||
|
|
||||||
**Check all proposed code examples:**
|
|
||||||
|
|
||||||
- [ ] No classes used
|
|
||||||
- [ ] No `this` references
|
|
||||||
- [ ] No inheritance
|
|
||||||
- [ ] Functions are standalone, not methods
|
|
||||||
- [ ] State is in variables/parameters, not properties
|
|
||||||
- [ ] Event delegation used where appropriate
|
|
||||||
|
|
||||||
**Examples should follow:**
|
|
||||||
```typescript
|
|
||||||
// ✅ GOOD: Procedural
|
|
||||||
function deleteRule(ruleId: string): void {
|
|
||||||
// implementation
|
|
||||||
}
|
|
||||||
window.deleteRule = deleteRule;
|
|
||||||
|
|
||||||
// ❌ BAD: OOP
|
|
||||||
class RuleManager {
|
|
||||||
deleteRule(ruleId: string): void {
|
|
||||||
// implementation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Verify Event Handler Patterns
|
|
||||||
|
|
||||||
**Check appropriate use of:**
|
|
||||||
|
|
||||||
- [ ] `onclick` for simple static content
|
|
||||||
- [ ] `data-action` + event delegation for dynamic content
|
|
||||||
- [ ] Form interception for progressive enhancement
|
|
||||||
- [ ] No over-engineering (not everything needs data-action)
|
|
||||||
|
|
||||||
**Plan should include:**
|
|
||||||
- When to use each pattern
|
|
||||||
- Examples of each pattern
|
|
||||||
- Rationale for pattern selection
|
|
||||||
|
|
||||||
### 7.3 Verify TailwindCSS Usage
|
|
||||||
|
|
||||||
- [ ] No custom CSS in TypeScript files
|
|
||||||
- [ ] All styling uses Tailwind classes
|
|
||||||
- [ ] No inline `style` attributes except for dynamic values
|
|
||||||
- [ ] Theme variables used appropriately (`var(--bg-primary)`, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Documentation Verification
|
|
||||||
|
|
||||||
### 8.1 Verify Type Definition Documentation
|
|
||||||
|
|
||||||
**For each type in `web/src/types/api.d.ts`:**
|
|
||||||
|
|
||||||
- [ ] Comment references source file (database or handler)
|
|
||||||
- [ ] Comment includes line numbers or struct name
|
|
||||||
- [ ] Fields have inline comments if purpose unclear
|
|
||||||
- [ ] Complex types have usage examples
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```typescript
|
|
||||||
// Matches database.SearchMediaItemsRow from /api/media-items/search
|
|
||||||
// Source: internal/database/queries.sql.go:SearchMediaItemsRow
|
|
||||||
// Used in: search.ts
|
|
||||||
export interface MediaItemSummary {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Verify Code Example Accuracy
|
|
||||||
|
|
||||||
**For each code example in plan:**
|
|
||||||
|
|
||||||
- [ ] Code actually compiles/runs
|
|
||||||
- [ ] Imports are correct for module system
|
|
||||||
- [ ] Type annotations match actual types
|
|
||||||
- [ ] Variable names match plan's conventions
|
|
||||||
- [ ] No syntax errors
|
|
||||||
|
|
||||||
**Test by:**
|
|
||||||
- Copying example to actual `.ts` file
|
|
||||||
- Running `npm run build:ts`
|
|
||||||
- Checking for compilation errors
|
|
||||||
|
|
||||||
### 8.3 Verify Template Examples
|
|
||||||
|
|
||||||
**For each template example:**
|
|
||||||
|
|
||||||
- [ ] Valid Go template syntax (`templ` not `html`)
|
|
||||||
- [ ] Correct handler type references
|
|
||||||
- [ ] Script tags use correct paths
|
|
||||||
- [ ] No inline JavaScript (after conversion)
|
|
||||||
- [ ] HTMX attributes are correct
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Template Integration Verification
|
|
||||||
|
|
||||||
### 9.1 Verify Template Cleanup Tasks
|
|
||||||
|
|
||||||
**For each template mentioned in Phase 6:**
|
|
||||||
|
|
||||||
- [ ] Current template file exists
|
|
||||||
- [ ] Line numbers for duplicate code are accurate
|
|
||||||
- [ ] Removal won't break functionality
|
|
||||||
- [ ] Replacement TypeScript module exists
|
|
||||||
- [ ] Script tag added to template `<head>`
|
|
||||||
|
|
||||||
**Verify specific tasks:**
|
|
||||||
- [ ] login.templ lines 62-88 (duplicate theme code)
|
|
||||||
- [ ] Any other duplicate function calls
|
|
||||||
- [ ] Any unused inline script blocks
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Find duplicate theme code
|
|
||||||
rg "function applyTheme" templates/login.templ web/src/theme.ts
|
|
||||||
|
|
||||||
# Count script blocks per template
|
|
||||||
rg '<script>' templates/login.templ -c
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Verify SSR Data Preservation
|
|
||||||
|
|
||||||
**For each template conversion:**
|
|
||||||
|
|
||||||
- [ ] Initial data still rendered server-side
|
|
||||||
- [ ] Template still uses handler types (e.g., `handlers.CollectionData`)
|
|
||||||
- [ ] No client-side data fetching for initial load
|
|
||||||
- [ ] TypeScript only enhances, doesn't replace SSR
|
|
||||||
|
|
||||||
**Example check:**
|
|
||||||
```go
|
|
||||||
// ✅ CORRECT: Template receives data from handler
|
|
||||||
templ Collections(collections []handlers.CollectionData)
|
|
||||||
|
|
||||||
// ❌ WRONG: Template fetches data client-side
|
|
||||||
// This would break progressive enhancement
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Progressive Enhancement Verification
|
|
||||||
|
|
||||||
### 10.1 Verify Critical User Flows
|
|
||||||
|
|
||||||
**For each critical flow (login, CRUD operations):**
|
|
||||||
|
|
||||||
- [ ] Works without JavaScript (standard form submission)
|
|
||||||
- [ ] Enhanced with JavaScript (AJAX, toasts, etc.)
|
|
||||||
- [ ] No JavaScript required for basic functionality
|
|
||||||
- [ ] Graceful degradation for mobile/slow connections
|
|
||||||
|
|
||||||
**Test procedure:**
|
|
||||||
1. Disable JavaScript in browser
|
|
||||||
2. Attempt each critical flow
|
|
||||||
3. Verify functionality works (with page reloads)
|
|
||||||
4. Enable JavaScript
|
|
||||||
5. Verify enhanced experience
|
|
||||||
|
|
||||||
### 10.2 Verify HTMX Usage
|
|
||||||
|
|
||||||
**For each HTMX form:**
|
|
||||||
|
|
||||||
- [ ] Has `action` and `method` attributes (fallback)
|
|
||||||
- [ ] HTMX attributes (`hx-post`, `hx-target`, etc.) enhance behavior
|
|
||||||
- [ ] No TypeScript interception needed (HTMX handles it)
|
|
||||||
- [ ] Works without JavaScript enabled
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Find HTMX forms
|
|
||||||
rg 'hx-post|hx-get' templates/*.templ
|
|
||||||
|
|
||||||
# Verify standard attributes exist
|
|
||||||
rg '<form' templates/*.templ -A 3 | grep "action="
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.3 Verify Error Handling
|
|
||||||
|
|
||||||
**For error scenarios:**
|
|
||||||
|
|
||||||
- [ ] Network errors show toast notification
|
|
||||||
- [ ] 401 errors clear tokens and redirect
|
|
||||||
- [ ] 404/500 errors display user-friendly messages
|
|
||||||
- [ ] Validation errors (400) show field-specific feedback
|
|
||||||
- [ ] No console errors in normal operation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Missing Information Detection
|
|
||||||
|
|
||||||
### 11.1 Check for Unaddressed Template Scripts
|
|
||||||
|
|
||||||
**For each template with `<script>` blocks:**
|
|
||||||
|
|
||||||
- [ ] Script is mentioned in conversion plan
|
|
||||||
- [ ] Conversion phase is specified
|
|
||||||
- [ ] TypeScript module is planned
|
|
||||||
- [ ] No inline JavaScript is overlooked
|
|
||||||
|
|
||||||
**Find all scripts:**
|
|
||||||
```bash
|
|
||||||
# List all templates with scripts
|
|
||||||
rg '<script>' templates/*.templ -l
|
|
||||||
|
|
||||||
# Count scripts per template
|
|
||||||
rg '<script>' templates/*.templ -c
|
|
||||||
```
|
|
||||||
|
|
||||||
### 11.2 Check for Missing API Endpoints
|
|
||||||
|
|
||||||
**For each TypeScript operation mentioned:**
|
|
||||||
|
|
||||||
- [ ] API endpoint exists
|
|
||||||
- [ ] Endpoint is documented in plan
|
|
||||||
- [ ] Type definition matches endpoint response
|
|
||||||
- [ ] Authentication requirements are clear
|
|
||||||
|
|
||||||
**Find all fetch calls:**
|
|
||||||
```bash
|
|
||||||
# Find fetch in plan
|
|
||||||
rg "fetch\(" TYPESCRIPT_CONVERSION_PLAN.md | grep "/api/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 11.3 Check for Missing Build Steps
|
|
||||||
|
|
||||||
**Verify plan includes:**
|
|
||||||
|
|
||||||
- [ ] TypeScript compilation command
|
|
||||||
- [ ] Watch mode command for development
|
|
||||||
- [ ] Production build steps
|
|
||||||
- [ ] Docker integration
|
|
||||||
- [ ] Verification steps after build
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Contradiction Detection
|
|
||||||
|
|
||||||
### 12.1 Check for Internal Contradictions
|
|
||||||
|
|
||||||
**Search for conflicting statements:**
|
|
||||||
|
|
||||||
- [ ] Architecture described differently in different sections
|
|
||||||
- [ ] Type definitions that contradict each other
|
|
||||||
- [ ] Phase ordering that conflicts with dependencies
|
|
||||||
- [ ] Code style examples that violate guidelines
|
|
||||||
|
|
||||||
**Common contradictions:**
|
|
||||||
- "No classes" but example shows class
|
|
||||||
- "Use snake_case" but example uses camelCase
|
|
||||||
- "Progressive enhancement required" but form lacks action attribute
|
|
||||||
- "Don't modify backend" but plan suggests new endpoint
|
|
||||||
|
|
||||||
### 12.2 Check for Contradictions with Codebase
|
|
||||||
|
|
||||||
**Verify plan matches reality:**
|
|
||||||
|
|
||||||
- [ ] Existing TypeScript files match plan's patterns
|
|
||||||
- [ ] Build configuration matches plan's description
|
|
||||||
- [ ] Template structure matches plan's assumptions
|
|
||||||
- [ ] API endpoints match plan's documentation
|
|
||||||
|
|
||||||
**Cross-check:**
|
|
||||||
```bash
|
|
||||||
# Verify existing TS patterns
|
|
||||||
rg "class " web/src/*.ts
|
|
||||||
# Should return nothing if plan is correct
|
|
||||||
|
|
||||||
# Verify build config
|
|
||||||
rg "module.*none" tsconfig.json
|
|
||||||
# Should match plan
|
|
||||||
```
|
|
||||||
|
|
||||||
### 12.3 Check for Contradictions with Guidelines
|
|
||||||
|
|
||||||
**Compare with PROJECT_GUIDELINES.md:**
|
|
||||||
|
|
||||||
- [ ] No OOP in TypeScript (procedural only)
|
|
||||||
- [ ] Use TailwindCSS (no custom CSS)
|
|
||||||
- [ ] Progressive enhancement required
|
|
||||||
- [ ] SSR for initial loads
|
|
||||||
- [ ] Service layer as source of truth
|
|
||||||
- [ ] No handler duplication
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Realism Verification
|
|
||||||
|
|
||||||
### 13.1 Verify Line Count Estimates
|
|
||||||
|
|
||||||
**For each template/script mentioned:**
|
|
||||||
|
|
||||||
- [ ] Count actual lines in file
|
|
||||||
- [ ] Compare to plan's estimate
|
|
||||||
- [ ] Estimate is within ±20% of actual
|
|
||||||
|
|
||||||
**Count lines:**
|
|
||||||
```bash
|
|
||||||
# Count inline JS in template
|
|
||||||
rg -A 1000 '<script>' templates/collection_rules.templ | wc -l
|
|
||||||
|
|
||||||
# Count total template lines
|
|
||||||
wc -l templates/*.templ
|
|
||||||
```
|
|
||||||
|
|
||||||
### 13.2 Verify Phase Duration Estimates
|
|
||||||
|
|
||||||
**For each phase:**
|
|
||||||
|
|
||||||
- [ ] Effort matches complexity (low = 1-2 days, medium = 3-4, high = 5-6)
|
|
||||||
- [ ] Total lines to convert justify duration
|
|
||||||
- [ ] Phase dependencies don't create bottlenecks
|
|
||||||
- [ ] Parallel work is possible where stated
|
|
||||||
|
|
||||||
### 13.3 Verify Technical Feasibility
|
|
||||||
|
|
||||||
**Check for over-ambitious goals:**
|
|
||||||
|
|
||||||
- [ ] No entirely new architecture proposed
|
|
||||||
- [ ] No unproven technologies
|
|
||||||
- [ ] No breaking changes to existing systems
|
|
||||||
- [ ] Rollback is possible if conversion fails
|
|
||||||
- [ ] Testing is feasible within timeline
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Testing Verification
|
|
||||||
|
|
||||||
### 14.1 Verify Testing Strategy
|
|
||||||
|
|
||||||
**Plan should include:**
|
|
||||||
|
|
||||||
- [ ] Unit testing approach (if applicable)
|
|
||||||
- [ ] Integration testing approach
|
|
||||||
- [ ] Manual testing checklist
|
|
||||||
- [ ] Browser testing matrix
|
|
||||||
- [ ] Progressive enhancement testing
|
|
||||||
- [ ] Performance testing (bundle sizes)
|
|
||||||
|
|
||||||
### 14.2 Verify Test Coverage
|
|
||||||
|
|
||||||
**For each converted feature:**
|
|
||||||
|
|
||||||
- [ ] Test steps are documented
|
|
||||||
- [ ] Success criteria are defined
|
|
||||||
- [ ] Edge cases are considered
|
|
||||||
- [ ] Error cases are tested
|
|
||||||
- [ ] Progressive enhancement is verified
|
|
||||||
|
|
||||||
### 14.3 Verify Rollback Strategy
|
|
||||||
|
|
||||||
**For each phase:**
|
|
||||||
|
|
||||||
- [ ] Git branch strategy is defined
|
|
||||||
- [ ] Quick rollback procedure is documented
|
|
||||||
- [ ] Feature flags or gradual rollout possible
|
|
||||||
- [ ] No destructive changes until verified
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Success Criteria Verification
|
|
||||||
|
|
||||||
### 15.1 Verify All Success Criteria are Measurable
|
|
||||||
|
|
||||||
**For each success criterion:**
|
|
||||||
|
|
||||||
- [ ] Can be verified with yes/no check
|
|
||||||
- [ ] Has clear definition of "done"
|
|
||||||
- [ ] Can be tested automatically or manually
|
|
||||||
- [ ] Is realistic given timeline
|
|
||||||
|
|
||||||
### 15.2 Verify Success Criteria Match Goals
|
|
||||||
|
|
||||||
**Check criteria align with:**
|
|
||||||
|
|
||||||
- [ ] TypeScript conversion (all JS converted)
|
|
||||||
- [ ] Type safety (no `any` except legacy)
|
|
||||||
- [ ] Architecture preservation (SSR, progressive enhancement)
|
|
||||||
- [ ] Code quality (procedural style, no OOP)
|
|
||||||
- [ ] Functionality (no regressions)
|
|
||||||
|
|
||||||
### 15.3 Verify Success Criteria are Complete
|
|
||||||
|
|
||||||
**Ensure criteria cover:**
|
|
||||||
|
|
||||||
- [ ] Code conversion completeness
|
|
||||||
- [ ] Type safety standards
|
|
||||||
- [ ] Compilation/build success
|
|
||||||
- [ ] Testing completion
|
|
||||||
- [ ] Documentation updates
|
|
||||||
- [ ] No regressions
|
|
||||||
- [ ] Performance standards
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary Checklist
|
|
||||||
|
|
||||||
Before approving the TypeScript conversion plan, verify:
|
|
||||||
|
|
||||||
### Critical (Must Pass)
|
|
||||||
- [ ] All type definitions match actual API responses (check database layer)
|
|
||||||
- [ ] No unused handler structs referenced in type definitions
|
|
||||||
- [ ] All API endpoints referenced actually exist
|
|
||||||
- [ ] Progressive enhancement maintained for all critical flows
|
|
||||||
- [ ] No backend modifications required for frontend tasks
|
|
||||||
- [ ] Build configuration is correct and tested
|
|
||||||
- [ ] Phase dependencies are acyclic and logical
|
|
||||||
- [ ] All inline JavaScript is accounted for in conversion plan
|
|
||||||
|
|
||||||
### Important (Should Pass)
|
|
||||||
- [ ] Code style examples match documented guidelines
|
|
||||||
- [ ] Template cleanup tasks have accurate line numbers
|
|
||||||
- [ ] Timeline estimates are realistic
|
|
||||||
- [ ] Testing strategy is comprehensive
|
|
||||||
- [ ] Rollback strategy is clear
|
|
||||||
- [ ] No internal contradictions in plan
|
|
||||||
- [ ] No contradictions with codebase reality
|
|
||||||
- [ ] No contradictions with PROJECT_GUIDELINES.md
|
|
||||||
|
|
||||||
### Nice to Have
|
|
||||||
- [ ] Documentation is clear and complete
|
|
||||||
- [ ] Examples are accurate and helpful
|
|
||||||
- [ ] Common pitfalls are documented
|
|
||||||
- [ ] Verification commands are provided
|
|
||||||
- [ ] Success criteria are measurable
|
|
||||||
- [ ] Plan is easy to follow for implementers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Instructions
|
|
||||||
|
|
||||||
1. **Before Starting Review:** Read the entire TYPESCRIPT_CONVERSION_PLAN.md
|
|
||||||
2. **During Review:** Go through each section of this checklist systematically
|
|
||||||
3. **For Each Item:** Run the provided verification commands
|
|
||||||
4. **Document Findings:** Note any issues found with file/line references
|
|
||||||
5. **Categorize Issues:** Mark as Critical, Important, or Nice to Have
|
|
||||||
6. **Verify Fixes:** After fixing issues, re-run relevant checklist sections
|
|
||||||
|
|
||||||
## Common Issues Found
|
|
||||||
|
|
||||||
1. **Type Mismatches:** Handler structs defined but not used by endpoints
|
|
||||||
2. **Missing Fields:** TypeScript interfaces missing fields that API returns
|
|
||||||
3. **Wrong Field Types:** `string` vs `string[]`, `author` vs `authors`
|
|
||||||
4. **Duplicate Code:** Inline JavaScript duplicating existing TypeScript
|
|
||||||
5. **Overlooked Scripts:** `<script>` blocks not mentioned in plan
|
|
||||||
6. **Missing Endpoints:** API references that don't exist
|
|
||||||
7. **Broken Dependencies:** Phase B depends on Phase A, but ordered incorrectly
|
|
||||||
8. **Contradictions:** Plan says one thing, examples show another
|
|
||||||
9. **Unrealistic Estimates:** 500 lines of complex code in 1 day
|
|
||||||
10. **Missing Testing:** No way to verify conversion worked
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Created: 2025-02-18*
|
|
||||||
*Purpose: Comprehensive verification of TypeScript conversion plan*
|
|
||||||
*Version: 1.0*
|
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
function getAuthHeader(): string {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return token ? `Bearer ${token}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiGet(url: string): Promise<Response> {
|
||||||
|
return fetch(`/api${url}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiPost(url: string, data?: unknown): Promise<Response> {
|
||||||
|
return fetch(`/api${url}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiPut(url: string, data?: unknown): Promise<Response> {
|
||||||
|
return fetch(`/api${url}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiDelete(url: string): Promise<Response> {
|
||||||
|
return fetch(`/api${url}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiPatch(url: string, data?: unknown): Promise<Response> {
|
||||||
|
return fetch(`/api${url}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Authorization': getAuthHeader(),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleResponse<T>(response: Response): Promise<T> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||||
|
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVoidResponse(response: Response): Promise<void> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||||
|
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(error: unknown, context: string): void {
|
||||||
|
console.error(`${context}:`, error);
|
||||||
|
const message = error instanceof Error ? error.message : 'An unexpected error occurred';
|
||||||
|
if ((window as any).showToast?.error) {
|
||||||
|
(window as any).showToast.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).api = {
|
||||||
|
get: apiGet,
|
||||||
|
post: apiPost,
|
||||||
|
put: apiPut,
|
||||||
|
delete: apiDelete,
|
||||||
|
patch: apiPatch,
|
||||||
|
handleResponse,
|
||||||
|
handleVoidResponse,
|
||||||
|
handleError
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAuthHeader,
|
||||||
|
apiGet,
|
||||||
|
apiPost,
|
||||||
|
apiPut,
|
||||||
|
apiDelete,
|
||||||
|
apiPatch,
|
||||||
|
handleResponse,
|
||||||
|
handleVoidResponse,
|
||||||
|
handleError
|
||||||
|
};
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
function escapeHtml(text: string): string {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySelector<T extends Element>(selector: string): T | null {
|
||||||
|
return document.querySelector<T>(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySelectorAll<T extends Element>(selector: string): NodeListOf<T> {
|
||||||
|
return document.querySelectorAll<T>(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElementById<T extends HTMLElement>(id: string): T | null {
|
||||||
|
return document.getElementById(id) as T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement<K extends keyof HTMLElementTagNameMap>(
|
||||||
|
tagName: K,
|
||||||
|
attributes?: Record<string, string>,
|
||||||
|
children?: (string | Node)[]
|
||||||
|
): HTMLElementTagNameMap[K] {
|
||||||
|
const element = document.createElement(tagName);
|
||||||
|
|
||||||
|
if (attributes) {
|
||||||
|
Object.entries(attributes).forEach(([key, value]) => {
|
||||||
|
if (key === 'className') {
|
||||||
|
element.className = value;
|
||||||
|
} else if (key === 'dataset') {
|
||||||
|
Object.entries(JSON.parse(value)).forEach(([dataKey, dataValue]) => {
|
||||||
|
element.dataset[dataKey] = String(dataValue);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
children.forEach(child => {
|
||||||
|
if (typeof child === 'string') {
|
||||||
|
element.appendChild(document.createTextNode(child));
|
||||||
|
} else {
|
||||||
|
element.appendChild(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showElement(element: HTMLElement | null): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideElement(element: HTMLElement | null): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleElement(element: HTMLElement | null): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTextContent(element: HTMLElement | null, text: string): void {
|
||||||
|
if (element) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInnerHTML(element: HTMLElement | null, html: string): void {
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addClass(element: HTMLElement | null, className: string): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.add(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeClass(element: HTMLElement | null, className: string): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.remove(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleClass(element: HTMLElement | null, className: string): void {
|
||||||
|
if (element) {
|
||||||
|
element.classList.toggle(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasClass(element: HTMLElement | null, className: string): boolean {
|
||||||
|
return element ? element.classList.contains(className) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).dom = {
|
||||||
|
escapeHtml,
|
||||||
|
querySelector,
|
||||||
|
querySelectorAll,
|
||||||
|
getElementById,
|
||||||
|
createElement,
|
||||||
|
showElement,
|
||||||
|
hideElement,
|
||||||
|
toggleElement,
|
||||||
|
setTextContent,
|
||||||
|
setInnerHTML,
|
||||||
|
addClass,
|
||||||
|
removeClass,
|
||||||
|
toggleClass,
|
||||||
|
hasClass
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
escapeHtml,
|
||||||
|
querySelector,
|
||||||
|
querySelectorAll,
|
||||||
|
getElementById,
|
||||||
|
createElement,
|
||||||
|
showElement,
|
||||||
|
hideElement,
|
||||||
|
toggleElement,
|
||||||
|
setTextContent,
|
||||||
|
setInnerHTML,
|
||||||
|
addClass,
|
||||||
|
removeClass,
|
||||||
|
toggleClass,
|
||||||
|
hasClass
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
function onDelegatedClick(selector: string, handler: (element: HTMLElement, event: MouseEvent) => void): void {
|
||||||
|
document.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const element = target.closest(selector) as HTMLElement | null;
|
||||||
|
if (element) {
|
||||||
|
handler(element, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelegatedSubmit(selector: string, handler: (form: HTMLFormElement, event: Event) => void): void {
|
||||||
|
document.addEventListener('submit', (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const form = target.closest(selector) as HTMLFormElement | null;
|
||||||
|
if (form) {
|
||||||
|
handler(form, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelegatedChange(selector: string, handler: (element: HTMLElement, event: Event) => void): void {
|
||||||
|
document.addEventListener('change', (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const element = target.closest(selector) as HTMLElement | null;
|
||||||
|
if (element) {
|
||||||
|
handler(element, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelegatedKeydown(selector: string, handler: (element: HTMLElement, event: KeyboardEvent) => void): void {
|
||||||
|
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const element = target.closest(selector) as HTMLElement | null;
|
||||||
|
if (element) {
|
||||||
|
handler(element, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataAttribute(element: HTMLElement, name: string): string | undefined {
|
||||||
|
return element.dataset[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDataAttribute(element: HTMLElement, name: string, value: string): void {
|
||||||
|
element.dataset[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(element: HTMLElement | null, handler: (event: MouseEvent) => void): void {
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('click', handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(element: HTMLFormElement | null, handler: (event: Event) => void): void {
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('submit', handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(element: HTMLElement | null, handler: (event: Event) => void): void {
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('change', handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(element: HTMLElement | null, handler: (event: KeyboardEvent) => void): void {
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('keydown', handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(element: HTMLElement | null, handler: (event: Event) => void): void {
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('input', handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preventDefault(event: Event): void {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPropagation(event: Event): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).events = {
|
||||||
|
onDelegatedClick,
|
||||||
|
onDelegatedSubmit,
|
||||||
|
onDelegatedChange,
|
||||||
|
onDelegatedKeydown,
|
||||||
|
getDataAttribute,
|
||||||
|
setDataAttribute,
|
||||||
|
onClick,
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
onKeydown,
|
||||||
|
onInput,
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
onDelegatedClick,
|
||||||
|
onDelegatedSubmit,
|
||||||
|
onDelegatedChange,
|
||||||
|
onDelegatedKeydown,
|
||||||
|
getDataAttribute,
|
||||||
|
setDataAttribute,
|
||||||
|
onClick,
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
onKeydown,
|
||||||
|
onInput,
|
||||||
|
preventDefault,
|
||||||
|
stopPropagation
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
function getToken(): string | null {
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(token: string): void {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToken(): void {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRefreshToken(): string | null {
|
||||||
|
return localStorage.getItem('refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRefreshToken(token: string): void {
|
||||||
|
localStorage.setItem('refresh_token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRefreshToken(): void {
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTheme(): string {
|
||||||
|
return localStorage.getItem('theme') || 'tokyo-night';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(theme: string): void {
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedLibrary(): string | null {
|
||||||
|
return localStorage.getItem('selectedLibrary');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedLibrary(libraryId: string): void {
|
||||||
|
localStorage.setItem('selectedLibrary', libraryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedBook(): string | null {
|
||||||
|
return localStorage.getItem('selectedBook');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedBook(bookId: string): void {
|
||||||
|
localStorage.setItem('selectedBook', bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll(): void {
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).storage = {
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
removeToken,
|
||||||
|
getRefreshToken,
|
||||||
|
setRefreshToken,
|
||||||
|
removeRefreshToken,
|
||||||
|
getTheme,
|
||||||
|
setTheme,
|
||||||
|
getSelectedLibrary,
|
||||||
|
setSelectedLibrary,
|
||||||
|
getSelectedBook,
|
||||||
|
setSelectedBook,
|
||||||
|
clearAll
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
removeToken,
|
||||||
|
getRefreshToken,
|
||||||
|
setRefreshToken,
|
||||||
|
removeRefreshToken,
|
||||||
|
getTheme,
|
||||||
|
setTheme,
|
||||||
|
getSelectedLibrary,
|
||||||
|
setSelectedLibrary,
|
||||||
|
getSelectedBook,
|
||||||
|
setSelectedBook,
|
||||||
|
clearAll
|
||||||
|
};
|
||||||
Vendored
+313
@@ -0,0 +1,313 @@
|
|||||||
|
// ============================================
|
||||||
|
// 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
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Matches database.SearchMediaItemsRow from /api/media-items/search
|
||||||
|
// Source: internal/database/queries.sql.go:6123-6170 SearchMediaItemsRow
|
||||||
|
// Endpoint: internal/handlers/media.go:SearchMediaItems()
|
||||||
|
// Note: internal/handlers/search.go has an unused MediaItemSummary - ignore it
|
||||||
|
// Used in: search.ts
|
||||||
|
export interface MediaItemSummary {
|
||||||
|
id: string;
|
||||||
|
library_id: string;
|
||||||
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
isbn?: string;
|
||||||
|
description?: string;
|
||||||
|
file_path: string;
|
||||||
|
file_size?: number;
|
||||||
|
mime_type?: string;
|
||||||
|
cover_image_path?: string;
|
||||||
|
series?: string;
|
||||||
|
series_number?: number;
|
||||||
|
tags?: string[];
|
||||||
|
asin?: string;
|
||||||
|
date_published?: string;
|
||||||
|
publisher?: string;
|
||||||
|
contributors?: string[];
|
||||||
|
language?: string;
|
||||||
|
edition?: string;
|
||||||
|
page_count?: number;
|
||||||
|
genre?: string;
|
||||||
|
copyright_year?: number;
|
||||||
|
goodreads_id?: string;
|
||||||
|
openlibrary_id?: string;
|
||||||
|
google_books_id?: string;
|
||||||
|
added_by_admin_id?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
format_group: string;
|
||||||
|
format_mimetype?: string;
|
||||||
|
is_reflowable?: boolean;
|
||||||
|
has_fixed_layout?: boolean;
|
||||||
|
total_characters?: number;
|
||||||
|
chapter_count?: number;
|
||||||
|
entitlement_id?: string;
|
||||||
|
revision_number?: number;
|
||||||
|
kobo_content_id?: string;
|
||||||
|
kobo_metadata?: string;
|
||||||
|
tags_search?: string[];
|
||||||
|
contributors_search?: string[];
|
||||||
|
file_sha256?: string;
|
||||||
|
opf_identifier?: string;
|
||||||
|
opf_uuid?: string;
|
||||||
|
hash_confidence?: string;
|
||||||
|
library_name: string;
|
||||||
|
library_type_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.CollectionData / CollectionResponse JSON response
|
||||||
|
// Source: internal/handlers/collections.go:123-131 CollectionResponse
|
||||||
|
// Used in: collections.ts
|
||||||
|
export interface CollectionData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
|
auto_assign_rules?: unknown;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
media_item_id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
cover_image_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.UnlinkedBookData JSON response
|
||||||
|
// Used in: unlinked_books.ts, unlinked_books.templ
|
||||||
|
export 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PotentialMatchData {
|
||||||
|
media_item_id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
confidence: number;
|
||||||
|
cover_image_path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches collection rule objects
|
||||||
|
// Used in: collection_rules.ts
|
||||||
|
export 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';
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches API test rule responses
|
||||||
|
// Used in: collection_rules.ts (test results)
|
||||||
|
export interface TestRuleMatch {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
cover_image_path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.SearchResponse (internal/handlers/search.go)
|
||||||
|
export interface SearchResponse {
|
||||||
|
results: SearchBookResponse[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBookResponse {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
authors: SearchAuthor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAuthor {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches AuthResponse (internal/handlers/auth.go:59-65)
|
||||||
|
export interface AuthResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
user: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
role: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ReadingStatsResponse (internal/handlers/analytics.go:26-35)
|
||||||
|
// Used in: analytics.ts
|
||||||
|
export interface ReadingStatsResponse {
|
||||||
|
total_books_read: number;
|
||||||
|
total_pages_read: number;
|
||||||
|
total_reading_time_minutes: number;
|
||||||
|
average_session_time_minutes: number;
|
||||||
|
longest_session_minutes: number;
|
||||||
|
most_active_day_of_week: string;
|
||||||
|
completion_rate: number;
|
||||||
|
daily_reading_minutes: DailyReading[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyReading {
|
||||||
|
date: string;
|
||||||
|
minutes: number;
|
||||||
|
pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.DeviceUsageResponse (internal/handlers/analytics.go:43-45)
|
||||||
|
// Note: Response is wrapped: { devices: DeviceUsage[] }
|
||||||
|
// Used in: analytics.ts
|
||||||
|
export interface DeviceUsageResponse {
|
||||||
|
devices: DeviceUsage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceUsage {
|
||||||
|
device_id: string;
|
||||||
|
device_name: string;
|
||||||
|
device_type: string;
|
||||||
|
sync_count: number;
|
||||||
|
last_sync: string;
|
||||||
|
total_time_seconds: number;
|
||||||
|
total_time_minutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.PopularBooksResponse (internal/handlers/analytics.go:57-59)
|
||||||
|
// Note: Response is wrapped: { books: PopularBook[] }
|
||||||
|
// Used in: analytics.ts
|
||||||
|
export interface PopularBooksResponse {
|
||||||
|
books: PopularBook[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopularBook {
|
||||||
|
media_item_id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
read_count: number;
|
||||||
|
avg_completion: number;
|
||||||
|
last_read: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.QueueItemResponse (internal/handlers/queue.go:35-51)
|
||||||
|
// Used in: queue.ts
|
||||||
|
export interface QueueItemResponse {
|
||||||
|
id: string;
|
||||||
|
device_id: string;
|
||||||
|
device_name: string;
|
||||||
|
device_type: string;
|
||||||
|
media_item_id?: string;
|
||||||
|
media_title?: string;
|
||||||
|
user_email: string;
|
||||||
|
sync_type: string;
|
||||||
|
priority: number;
|
||||||
|
attempts: number;
|
||||||
|
max_attempts: number;
|
||||||
|
status: string;
|
||||||
|
error_message?: string;
|
||||||
|
created_at: string;
|
||||||
|
processed_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.QueueStatsResponse (internal/handlers/queue.go:27-33)
|
||||||
|
// Used in: queue.ts
|
||||||
|
export interface QueueStatsResponse {
|
||||||
|
pending_count: number;
|
||||||
|
processing_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
completed_count: number;
|
||||||
|
total_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ConflictDetailResponse (internal/handlers/conflicts.go:42-53)
|
||||||
|
// Used in: conflicts.ts
|
||||||
|
export interface ConflictDetailResponse {
|
||||||
|
id: string;
|
||||||
|
media_item_id: string;
|
||||||
|
media_item_title: string;
|
||||||
|
conflict_type: string;
|
||||||
|
conflict_data: Record<string, ConflictSourceData>;
|
||||||
|
resolution_status: string;
|
||||||
|
resolution_data?: Record<string, unknown>;
|
||||||
|
resolved_by?: string;
|
||||||
|
resolved_at?: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ConflictSourceData (internal/handlers/conflicts.go:36-40)
|
||||||
|
export interface ConflictSourceData {
|
||||||
|
source: string;
|
||||||
|
timestamp: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ConflictListResponse (internal/handlers/conflicts.go:55-59)
|
||||||
|
// Used in: conflicts.ts
|
||||||
|
export interface ConflictListResponse {
|
||||||
|
conflicts: ConflictDetailResponse[];
|
||||||
|
total: number;
|
||||||
|
unresolved: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ConflictResolveResponse (internal/handlers/conflicts.go:61-65)
|
||||||
|
// Used in: conflicts.ts
|
||||||
|
export interface ConflictResolveResponse {
|
||||||
|
conflict_resolved: boolean;
|
||||||
|
applied_to: Record<string, boolean>;
|
||||||
|
devices_synced: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.BulkResolveResponse (internal/handlers/conflicts.go:424-429)
|
||||||
|
// Used in: conflicts.ts
|
||||||
|
export interface BulkResolveResponse {
|
||||||
|
results: ConflictResult[];
|
||||||
|
total: number;
|
||||||
|
success: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches handlers.ConflictResult (internal/handlers/conflicts.go:431-436)
|
||||||
|
export interface ConflictResult {
|
||||||
|
conflict_id: string;
|
||||||
|
status: string;
|
||||||
|
error?: string;
|
||||||
|
winner?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user