@illuma-ai/code-sandbox 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3507 +0,0 @@
1
- /**
2
- * fullstack-starter — Production-equivalent full-stack starter template.
3
- *
4
- * A properly structured Express + React application with:
5
- * - sql.js (SQLite WASM) as a true relational DB — same schemas work on Postgres
6
- * - React Router v6 for client-side routing
7
- * - React Context for auth + app state management
8
- * - JWT authentication with role-based access
9
- * - .ranger manifest for AI agent context
10
- *
11
- * Architecture:
12
- * ┌─────────────────────────────────────────────────────┐
13
- * │ server.js — Entry, middleware chain │
14
- * │ config.js — Environment / .env config │
15
- * │ db/database.js — sql.js init + migrations │
16
- * │ db/migrations/001.js — Schema definitions (typed) │
17
- * │ db/repositories/ — CRUD repos over SQL │
18
- * │ middleware/ — Auth, errors, static │
19
- * │ routes/ — Express route definitions │
20
- * │ public/ — React SPA (CDN-loaded) │
21
- * │ public/store/ — React Context providers │
22
- * │ public/pages/ — Route-level components │
23
- * │ public/components/ — Reusable UI components │
24
- * │ .ranger — AI agent instruction file │
25
- * │ .env.example — Env var documentation │
26
- * └─────────────────────────────────────────────────────┘
27
- *
28
- * DATABASE:
29
- * Uses sql.js (SQLite compiled to WASM) — runs in the browser.
30
- * Schemas use standard SQL types (INTEGER, TEXT, REAL, BOOLEAN, TIMESTAMP).
31
- * To migrate to Postgres: change db/database.js to use 'pg', keep same schemas.
32
- * The repository layer abstracts all SQL — swap the query runner, not the repos.
33
- */
34
-
35
- import type { FileMap } from "../types";
36
-
37
- // ---------------------------------------------------------------------------
38
- // .ranger — AI Agent Instruction File
39
- // ---------------------------------------------------------------------------
40
-
41
- const RANGER_MANIFEST = `# .ranger — Project Manifest
42
- # This file provides context to the Ranger AI agent about project structure,
43
- # conventions, and schemas. Update this file when you add new files, routes,
44
- # or database tables.
45
-
46
- ## Project Type
47
- type: fullstack-express-react
48
- runtime: nodepod (browser-based Node.js)
49
- database: sql.js (SQLite WASM — Postgres-compatible schemas)
50
- frontend: React 18 (CDN) + React Router v6 + React Context
51
- styling: Tailwind CSS (CDN)
52
-
53
- ## File Structure
54
- \`\`\`
55
- ├── server.js # Express entry point — middleware chain, server start
56
- ├── config.js # Environment config — reads process.env with defaults
57
- ├── .env.example # Environment variable documentation
58
- ├── package.json # Dependencies (express, sql.js)
59
- ├── Dockerfile # Multi-stage production Docker build
60
- ├── docker-compose.yml # Local dev + optional Postgres profile
61
- ├── .dockerignore # Docker build exclusions
62
- ├── README.md # Project docs, API reference, deploy guide
63
- ├── CONTRIBUTING.md # Contribution guidelines, adding resources
64
-
65
- ├── db/
66
- │ ├── database.js # sql.js initialization, migration runner, query helpers
67
- │ └── migrations/
68
- │ └── 001_initial.js # Initial schema — users, items tables
69
-
70
- ├── db/repositories/
71
- │ ├── userRepository.js # User CRUD — findById, findByEmail, create, update
72
- │ └── itemRepository.js # Item CRUD — findAll, findById, create, update, delete, stats
73
-
74
- ├── middleware/
75
- │ ├── auth.js # JWT auth — generateToken, authenticate, requireRole
76
- │ ├── errorHandler.js # Global error handler + 404 with SPA fallback
77
- │ ├── staticFiles.js # Static file serving (fs.readFileSync for Nodepod)
78
- │ └── validate.js # Request body validation helpers
79
-
80
- ├── routes/
81
- │ ├── auth.js # POST /api/auth/register, POST /api/auth/login, GET /api/auth/me
82
- │ └── items.js # GET/POST /api/items, GET/PUT/DELETE /api/items/:id, GET /api/items/stats
83
-
84
- ├── public/
85
- │ ├── index.html # HTML shell — loads React, Router, Tailwind from CDN
86
- │ ├── styles.css # Custom CSS animations
87
- │ │
88
- │ ├── lib/
89
- │ │ └── api.js # API client — fetch wrapper with auth token handling
90
- │ │
91
- │ ├── store/
92
- │ │ ├── AuthContext.js # Auth state — user, token, login/logout/register actions
93
- │ │ └── AppContext.js # App state — items, loading, filters, CRUD actions
94
- │ │
95
- │ ├── pages/
96
- │ │ ├── WelcomePage.js # Landing page — hero, features, architecture overview
97
- │ │ ├── LoginPage.js # Login form with demo credentials
98
- │ │ ├── RegisterPage.js # Registration form
99
- │ │ └── DashboardPage.js # Main app — stats, items list, create/edit/delete
100
- │ │
101
- │ ├── components/
102
- │ │ ├── Navbar.js # Top nav — logo, nav links, auth status
103
- │ │ ├── Toast.js # Notification toasts
104
- │ │ ├── ItemCard.js # Item display with status/priority badges
105
- │ │ └── ItemForm.js # Create/edit item form with validation
106
- │ │
107
- │ └── App.js # Root — providers, router, route definitions
108
- \`\`\`
109
-
110
- ## Database Schema
111
-
112
- ### users
113
- | Column | Type | Constraints | Notes |
114
- |------------|-----------|-------------------------------|--------------------------|
115
- | id | INTEGER | PRIMARY KEY AUTOINCREMENT | Maps to SERIAL in Postgres |
116
- | email | TEXT | UNIQUE NOT NULL | |
117
- | password | TEXT | NOT NULL | Hashed (base64 in sandbox) |
118
- | name | TEXT | NOT NULL | |
119
- | role | TEXT | NOT NULL DEFAULT 'user' | 'admin' or 'user' |
120
- | created_at | TEXT | DEFAULT CURRENT_TIMESTAMP | ISO 8601 string |
121
-
122
- ### items
123
- | Column | Type | Constraints | Notes |
124
- |-------------|-----------|-------------------------------|--------------------------|
125
- | id | INTEGER | PRIMARY KEY AUTOINCREMENT | Maps to SERIAL in Postgres |
126
- | title | TEXT | NOT NULL | |
127
- | description | TEXT | | |
128
- | status | TEXT | NOT NULL DEFAULT 'todo' | 'todo', 'in_progress', 'done' |
129
- | priority | TEXT | NOT NULL DEFAULT 'medium' | 'low', 'medium', 'high' |
130
- | user_id | INTEGER | REFERENCES users(id) | Foreign key |
131
- | created_at | TEXT | DEFAULT CURRENT_TIMESTAMP | |
132
- | updated_at | TEXT | DEFAULT CURRENT_TIMESTAMP | |
133
-
134
- ## API Routes
135
-
136
- ### Health
137
- | Method | Path | Auth | Body | Response |
138
- |--------|---------------------|----------|-----------------------------------|-----------------------|
139
- | GET | /api/health | None | — | { status, uptime } |
140
-
141
- ### Authentication
142
- | Method | Path | Auth | Body | Response |
143
- |--------|---------------------|----------|-----------------------------------|-----------------------|
144
- | POST | /api/auth/register | None | { email, password, name } | { user, token } |
145
- | POST | /api/auth/login | None | { email, password } | { user, token } |
146
- | GET | /api/auth/me | Required | — | { user } |
147
-
148
- ### Items
149
- | Method | Path | Auth | Body | Response |
150
- |--------|---------------------|----------|-----------------------------------|-----------------------|
151
- | GET | /api/items | Required | — | { items: [...] } |
152
- | POST | /api/items | Required | { title, description?, status?, priority? } | { item } |
153
- | GET | /api/items/stats | Required | — | { total, byStatus, byPriority } |
154
- | GET | /api/items/:id | Required | — | { item } |
155
- | PUT | /api/items/:id | Required | { title?, description?, status?, priority? } | { item } |
156
- | DELETE | /api/items/:id | Required | — | { message } |
157
-
158
- ## Coding Conventions
159
- - Backend: CommonJS (require/module.exports), ES5-compatible for Nodepod
160
- - Frontend: React.createElement (aliased as 'h'), no JSX — loaded from CDN
161
- - React Router: ReactRouterDOM global, v6 API (Routes, Route, Link, Outlet, useParams, useNavigate)
162
- - State: React Context with useReducer for complex state, useState for simple
163
- - API calls: Use /public/lib/api.js client — handles auth headers, JSON parsing, errors
164
- - SQL: Use parameterized queries (?) — never string interpolation
165
- - Error handling: All async route handlers wrapped in try/catch, errors forwarded to errorHandler
166
- - Validation: Use middleware/validate.js for request body validation
167
-
168
- ## Postgres Migration Guide
169
- To deploy to production with Postgres:
170
- 1. Replace db/database.js: use 'pg' Pool instead of sql.js
171
- 2. Update query syntax: ? params → $1, $2 (Postgres style)
172
- 3. Replace INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
173
- 4. Replace TEXT for timestamps → TIMESTAMP WITH TIME ZONE
174
- 5. Add connection pooling config to config.js
175
- 6. Everything else (repos, routes, middleware) stays the same
176
- `;
177
-
178
- // ---------------------------------------------------------------------------
179
- // .env.example
180
- // ---------------------------------------------------------------------------
181
-
182
- const ENV_EXAMPLE = `# =============================================================================
183
- # Environment Configuration
184
- # Copy to .env and fill in values for your deployment
185
- # =============================================================================
186
-
187
- # Server
188
- PORT=3000
189
- NODE_ENV=development
190
-
191
- # Database
192
- # In sandbox: uses sql.js (SQLite WASM, in-memory)
193
- # In production: DATABASE_URL=postgresql://user:pass@host:5432/dbname
194
- DATABASE_URL=sqlite::memory:
195
-
196
- # Authentication
197
- JWT_SECRET=change-me-in-production
198
- JWT_EXPIRES_IN=24h
199
-
200
- # CORS
201
- CORS_ORIGIN=*
202
- `;
203
-
204
- // ---------------------------------------------------------------------------
205
- // config.js
206
- // ---------------------------------------------------------------------------
207
-
208
- const CONFIG_JS = `/**
209
- * config.js — Centralized configuration.
210
- *
211
- * Reads from process.env with sensible defaults for the sandbox.
212
- * In production, set real values via .env file or environment variables.
213
- */
214
- var config = {
215
- port: parseInt(process.env.PORT || '3000', 10),
216
- nodeEnv: process.env.NODE_ENV || 'development',
217
- jwtSecret: process.env.JWT_SECRET || 'sandbox-secret-key',
218
- jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
219
- corsOrigin: process.env.CORS_ORIGIN || '*',
220
- databaseUrl: process.env.DATABASE_URL || 'sqlite::memory:',
221
- };
222
-
223
- module.exports = config;
224
- `;
225
-
226
- // ---------------------------------------------------------------------------
227
- // db/database.js — sql.js initialization + migration runner
228
- // ---------------------------------------------------------------------------
229
-
230
- const DATABASE_JS = `/**
231
- * db/database.js — SQLite database via sql.js (WASM).
232
- *
233
- * Provides a Postgres-compatible interface:
234
- * - db.run(sql, params) — Execute INSERT/UPDATE/DELETE, returns { changes }
235
- * - db.get(sql, params) — SELECT single row, returns object or undefined
236
- * - db.all(sql, params) — SELECT multiple rows, returns array
237
- * - db.exec(sql) — Execute raw SQL (DDL, multi-statement)
238
- *
239
- * Migration runner applies numbered migration files in order.
240
- * Tracks applied migrations in a _migrations table.
241
- *
242
- * POSTGRES MIGRATION:
243
- * Replace this file with a 'pg' Pool wrapper exposing the same interface.
244
- * Change ? params to $1, $2 style. Everything else stays the same.
245
- */
246
-
247
- var initSqlJs;
248
- try {
249
- initSqlJs = require('sql.js');
250
- } catch (e) {
251
- // Fallback: sql.js not installed yet, will be loaded later
252
- initSqlJs = null;
253
- }
254
-
255
- var _db = null;
256
- var _ready = null;
257
-
258
- var WASM_CDN_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.11.0/dist/sql-wasm.wasm';
259
-
260
- /**
261
- * Initialize the database. Called once at server startup.
262
- * Returns a promise that resolves when the DB is ready.
263
- */
264
- function initialize() {
265
- if (_ready) return _ready;
266
-
267
- _ready = _initializeInternal();
268
- return _ready;
269
- }
270
-
271
- async function _initializeInternal() {
272
- if (!initSqlJs) {
273
- initSqlJs = require('sql.js');
274
- }
275
-
276
- // PERF: Pre-fetch the WASM binary ourselves using browser fetch().
277
- // sql.js detects Nodepod as Node.js (because process.versions.node exists)
278
- // and tries to use fs.readFileSync() to load the WASM binary — which fails.
279
- // By fetching the binary ourselves and passing it as wasmBinary, we bypass
280
- // sql.js's environment detection entirely.
281
- console.log('[DB] Fetching sql.js WASM binary from CDN...');
282
- var wasmResponse = await fetch(WASM_CDN_URL);
283
- if (!wasmResponse.ok) {
284
- throw new Error('[DB] Failed to fetch WASM binary: HTTP ' + wasmResponse.status);
285
- }
286
- var wasmBinary = new Uint8Array(await wasmResponse.arrayBuffer());
287
- console.log('[DB] WASM binary loaded (' + wasmBinary.length + ' bytes)');
288
-
289
- // Initialize sql.js with the pre-fetched WASM binary
290
- var SQL = await initSqlJs({ wasmBinary: wasmBinary });
291
-
292
- _db = new SQL.Database();
293
-
294
- // Enable WAL mode equivalent (not applicable in-memory, but documents intent)
295
- _db.run('PRAGMA journal_mode=MEMORY');
296
- _db.run('PRAGMA foreign_keys=ON');
297
-
298
- // Create migrations tracking table
299
- _db.run(\`
300
- CREATE TABLE IF NOT EXISTS _migrations (
301
- id INTEGER PRIMARY KEY AUTOINCREMENT,
302
- name TEXT NOT NULL UNIQUE,
303
- applied_at TEXT DEFAULT CURRENT_TIMESTAMP
304
- )
305
- \`);
306
-
307
- // Run migrations
308
- await _runMigrations();
309
-
310
- console.log('[DB] SQLite database initialized (sql.js WASM)');
311
- return _db;
312
- }
313
-
314
- /**
315
- * Run all pending migrations in order.
316
- */
317
- async function _runMigrations() {
318
- var migrations = require('./migrations/001_initial');
319
-
320
- var applied = {};
321
- var rows = _db.exec('SELECT name FROM _migrations');
322
- if (rows.length > 0) {
323
- rows[0].values.forEach(function(row) {
324
- applied[row[0]] = true;
325
- });
326
- }
327
-
328
- migrations.forEach(function(migration) {
329
- if (!applied[migration.name]) {
330
- console.log('[DB] Running migration: ' + migration.name);
331
- // Use exec() instead of run() — migrations may contain multiple statements
332
- _db.exec(migration.up);
333
- _db.run('INSERT INTO _migrations (name) VALUES (?)', [migration.name]);
334
- }
335
- });
336
- }
337
-
338
- /**
339
- * Execute a query that modifies data (INSERT, UPDATE, DELETE).
340
- * @param {string} sql - SQL with ? placeholders
341
- * @param {Array} params - Parameter values
342
- * @returns {{ changes: number }} Number of affected rows
343
- */
344
- function run(sql, params) {
345
- _db.run(sql, params || []);
346
- var result = _db.exec('SELECT changes() as changes');
347
- var changes = result.length > 0 ? result[0].values[0][0] : 0;
348
- return { changes: changes };
349
- }
350
-
351
- /**
352
- * Execute a SELECT query returning a single row.
353
- * @param {string} sql - SQL with ? placeholders
354
- * @param {Array} params - Parameter values
355
- * @returns {Object|undefined} Row as { column: value } or undefined
356
- */
357
- function get(sql, params) {
358
- var stmt = _db.prepare(sql);
359
- stmt.bind(params || []);
360
- var row = undefined;
361
- if (stmt.step()) {
362
- row = stmt.getAsObject();
363
- }
364
- stmt.free();
365
- return row;
366
- }
367
-
368
- /**
369
- * Execute a SELECT query returning all matching rows.
370
- * @param {string} sql - SQL with ? placeholders
371
- * @param {Array} params - Parameter values
372
- * @returns {Array<Object>} Array of { column: value } objects
373
- */
374
- function all(sql, params) {
375
- var stmt = _db.prepare(sql);
376
- stmt.bind(params || []);
377
- var rows = [];
378
- while (stmt.step()) {
379
- rows.push(stmt.getAsObject());
380
- }
381
- stmt.free();
382
- return rows;
383
- }
384
-
385
- /**
386
- * Execute raw SQL (for DDL or multi-statement queries).
387
- * @param {string} sql - Raw SQL
388
- */
389
- function exec(sql) {
390
- _db.exec(sql);
391
- }
392
-
393
- /**
394
- * Get the last inserted row ID.
395
- * @returns {number}
396
- */
397
- function lastInsertRowId() {
398
- var result = _db.exec('SELECT last_insert_rowid() as id');
399
- return result.length > 0 ? result[0].values[0][0] : 0;
400
- }
401
-
402
- module.exports = {
403
- initialize: initialize,
404
- run: run,
405
- get: get,
406
- all: all,
407
- exec: exec,
408
- lastInsertRowId: lastInsertRowId,
409
- };
410
- `;
411
-
412
- // ---------------------------------------------------------------------------
413
- // db/migrations/001_initial.js
414
- // ---------------------------------------------------------------------------
415
-
416
- const MIGRATION_001 = `/**
417
- * Migration 001 — Initial schema.
418
- *
419
- * Creates the core tables with proper types, constraints, and indexes.
420
- * These schemas are designed to map directly to Postgres:
421
- *
422
- * SQLite → Postgres equivalents:
423
- * INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
424
- * TEXT → VARCHAR or TEXT
425
- * TEXT (timestamps) → TIMESTAMP WITH TIME ZONE
426
- * REAL → DOUBLE PRECISION
427
- * INTEGER (boolean) → BOOLEAN
428
- */
429
-
430
- module.exports = [
431
- {
432
- name: '001_create_users',
433
- up: \`
434
- CREATE TABLE IF NOT EXISTS users (
435
- id INTEGER PRIMARY KEY AUTOINCREMENT,
436
- email TEXT NOT NULL UNIQUE,
437
- password TEXT NOT NULL,
438
- name TEXT NOT NULL,
439
- role TEXT NOT NULL DEFAULT 'user',
440
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
441
- )
442
- \`,
443
- },
444
- {
445
- name: '002_create_items',
446
- up: \`
447
- CREATE TABLE IF NOT EXISTS items (
448
- id INTEGER PRIMARY KEY AUTOINCREMENT,
449
- title TEXT NOT NULL,
450
- description TEXT,
451
- status TEXT NOT NULL DEFAULT 'todo',
452
- priority TEXT NOT NULL DEFAULT 'medium',
453
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
454
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
455
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
456
- )
457
- \`,
458
- },
459
- {
460
- name: '003_create_items_indexes',
461
- up: \`
462
- CREATE INDEX IF NOT EXISTS idx_items_user_id ON items(user_id);
463
- CREATE INDEX IF NOT EXISTS idx_items_status ON items(status);
464
- CREATE INDEX IF NOT EXISTS idx_items_priority ON items(priority);
465
- \`,
466
- },
467
- {
468
- name: '004_seed_data',
469
- up: \`
470
- INSERT INTO users (email, password, name, role) VALUES
471
- ('admin@example.com', 'YWRtaW4xMjM=', 'Admin User', 'admin'),
472
- ('user@example.com', 'dXNlcjEyMw==', 'Demo User', 'user');
473
-
474
- INSERT INTO items (title, description, status, priority, user_id) VALUES
475
- ('Set up project structure', 'Initialize the repository with proper folder structure and configuration files', 'done', 'high', 1),
476
- ('Design database schema', 'Create the initial database tables with proper types, constraints, and indexes', 'in_progress', 'high', 1),
477
- ('Build authentication flow', 'Implement JWT-based login and registration with role-based access control', 'todo', 'medium', 1),
478
- ('Create API endpoints', 'Build RESTful CRUD endpoints for all resources', 'todo', 'medium', 2),
479
- ('Write frontend components', 'Build React components for dashboard, forms, and navigation', 'todo', 'low', 2);
480
- \`,
481
- },
482
- ];
483
- `;
484
-
485
- // ---------------------------------------------------------------------------
486
- // db/repositories/userRepository.js
487
- // ---------------------------------------------------------------------------
488
-
489
- const USER_REPOSITORY_JS = `/**
490
- * db/repositories/userRepository.js — User data access layer.
491
- *
492
- * All database queries for the users table. Uses parameterized SQL.
493
- * This file works identically with Postgres — just change ? to $1, $2.
494
- */
495
-
496
- var db = require('../database');
497
-
498
- /**
499
- * Find a user by ID.
500
- * @param {number} id
501
- * @returns {Object|undefined} User object (without password)
502
- */
503
- function findById(id) {
504
- var user = db.get(
505
- 'SELECT id, email, name, role, created_at FROM users WHERE id = ?',
506
- [id]
507
- );
508
- return user || undefined;
509
- }
510
-
511
- /**
512
- * Find a user by email (includes password for auth).
513
- * @param {string} email
514
- * @returns {Object|undefined} User object with password
515
- */
516
- function findByEmail(email) {
517
- return db.get('SELECT * FROM users WHERE email = ?', [email]) || undefined;
518
- }
519
-
520
- /**
521
- * Create a new user.
522
- * @param {{ email: string, password: string, name: string, role?: string }} data
523
- * @returns {Object} Created user (without password)
524
- */
525
- function create(data) {
526
- var role = data.role || 'user';
527
- db.run(
528
- 'INSERT INTO users (email, password, name, role) VALUES (?, ?, ?, ?)',
529
- [data.email, data.password, data.name, role]
530
- );
531
- var id = db.lastInsertRowId();
532
- return findById(id);
533
- }
534
-
535
- /**
536
- * Update a user.
537
- * @param {number} id
538
- * @param {{ email?: string, name?: string, role?: string }} data
539
- * @returns {Object|undefined} Updated user
540
- */
541
- function update(id, data) {
542
- var fields = [];
543
- var values = [];
544
-
545
- if (data.email !== undefined) { fields.push('email = ?'); values.push(data.email); }
546
- if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
547
- if (data.role !== undefined) { fields.push('role = ?'); values.push(data.role); }
548
-
549
- if (fields.length === 0) return findById(id);
550
-
551
- values.push(id);
552
- db.run('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', values);
553
- return findById(id);
554
- }
555
-
556
- /**
557
- * Get all users (without passwords).
558
- * @returns {Array<Object>}
559
- */
560
- function findAll() {
561
- return db.all('SELECT id, email, name, role, created_at FROM users ORDER BY created_at DESC');
562
- }
563
-
564
- /**
565
- * Count total users.
566
- * @returns {number}
567
- */
568
- function count() {
569
- var row = db.get('SELECT COUNT(*) as count FROM users');
570
- return row ? row.count : 0;
571
- }
572
-
573
- module.exports = {
574
- findById: findById,
575
- findByEmail: findByEmail,
576
- create: create,
577
- update: update,
578
- findAll: findAll,
579
- count: count,
580
- };
581
- `;
582
-
583
- // ---------------------------------------------------------------------------
584
- // db/repositories/itemRepository.js
585
- // ---------------------------------------------------------------------------
586
-
587
- const ITEM_REPOSITORY_JS = `/**
588
- * db/repositories/itemRepository.js — Item data access layer.
589
- *
590
- * All database queries for the items table. Uses parameterized SQL.
591
- * This file works identically with Postgres — just change ? to $1, $2.
592
- */
593
-
594
- var db = require('../database');
595
-
596
- /**
597
- * Find all items, optionally filtered by user_id, status, or priority.
598
- * @param {{ userId?: number, status?: string, priority?: string }} filters
599
- * @returns {Array<Object>}
600
- */
601
- function findAll(filters) {
602
- var where = [];
603
- var params = [];
604
- filters = filters || {};
605
-
606
- if (filters.userId) {
607
- where.push('i.user_id = ?');
608
- params.push(filters.userId);
609
- }
610
- if (filters.status) {
611
- where.push('i.status = ?');
612
- params.push(filters.status);
613
- }
614
- if (filters.priority) {
615
- where.push('i.priority = ?');
616
- params.push(filters.priority);
617
- }
618
-
619
- var sql = \`
620
- SELECT i.*, u.name as user_name, u.email as user_email
621
- FROM items i
622
- LEFT JOIN users u ON i.user_id = u.id
623
- \`;
624
- if (where.length > 0) {
625
- sql += ' WHERE ' + where.join(' AND ');
626
- }
627
- sql += ' ORDER BY i.created_at DESC';
628
-
629
- return db.all(sql, params);
630
- }
631
-
632
- /**
633
- * Find a single item by ID.
634
- * @param {number} id
635
- * @returns {Object|undefined}
636
- */
637
- function findById(id) {
638
- return db.get(
639
- \`SELECT i.*, u.name as user_name, u.email as user_email
640
- FROM items i
641
- LEFT JOIN users u ON i.user_id = u.id
642
- WHERE i.id = ?\`,
643
- [id]
644
- ) || undefined;
645
- }
646
-
647
- /**
648
- * Create a new item.
649
- * @param {{ title: string, description?: string, status?: string, priority?: string, user_id: number }} data
650
- * @returns {Object} Created item
651
- */
652
- function create(data) {
653
- var status = data.status || 'todo';
654
- var priority = data.priority || 'medium';
655
- var description = data.description || '';
656
-
657
- db.run(
658
- 'INSERT INTO items (title, description, status, priority, user_id) VALUES (?, ?, ?, ?, ?)',
659
- [data.title, description, status, priority, data.user_id]
660
- );
661
- var id = db.lastInsertRowId();
662
- return findById(id);
663
- }
664
-
665
- /**
666
- * Update an existing item.
667
- * @param {number} id
668
- * @param {{ title?: string, description?: string, status?: string, priority?: string }} data
669
- * @returns {Object|undefined} Updated item
670
- */
671
- function update(id, data) {
672
- var fields = [];
673
- var values = [];
674
-
675
- if (data.title !== undefined) { fields.push('title = ?'); values.push(data.title); }
676
- if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description); }
677
- if (data.status !== undefined) { fields.push('status = ?'); values.push(data.status); }
678
- if (data.priority !== undefined) { fields.push('priority = ?'); values.push(data.priority); }
679
-
680
- if (fields.length === 0) return findById(id);
681
-
682
- fields.push("updated_at = datetime('now')");
683
- values.push(id);
684
- db.run('UPDATE items SET ' + fields.join(', ') + ' WHERE id = ?', values);
685
- return findById(id);
686
- }
687
-
688
- /**
689
- * Delete an item by ID.
690
- * @param {number} id
691
- * @returns {boolean} True if deleted
692
- */
693
- function remove(id) {
694
- var result = db.run('DELETE FROM items WHERE id = ?', [id]);
695
- return result.changes > 0;
696
- }
697
-
698
- /**
699
- * Get aggregate statistics.
700
- * @returns {{ total: number, byStatus: Object, byPriority: Object }}
701
- */
702
- function getStats() {
703
- var total = db.get('SELECT COUNT(*) as count FROM items');
704
- var byStatus = db.all('SELECT status, COUNT(*) as count FROM items GROUP BY status');
705
- var byPriority = db.all('SELECT priority, COUNT(*) as count FROM items GROUP BY priority');
706
-
707
- var statusMap = {};
708
- byStatus.forEach(function(row) { statusMap[row.status] = row.count; });
709
-
710
- var priorityMap = {};
711
- byPriority.forEach(function(row) { priorityMap[row.priority] = row.count; });
712
-
713
- return {
714
- total: total ? total.count : 0,
715
- byStatus: statusMap,
716
- byPriority: priorityMap,
717
- };
718
- }
719
-
720
- module.exports = {
721
- findAll: findAll,
722
- findById: findById,
723
- create: create,
724
- update: update,
725
- remove: remove,
726
- getStats: getStats,
727
- };
728
- `;
729
-
730
- // ---------------------------------------------------------------------------
731
- // middleware/auth.js
732
- // ---------------------------------------------------------------------------
733
-
734
- const AUTH_JS = `/**
735
- * middleware/auth.js — JWT authentication.
736
- *
737
- * In the sandbox, JWT is a base64-encoded JSON payload (no crypto).
738
- * In production, swap for the 'jsonwebtoken' package with real signing.
739
- *
740
- * Provides:
741
- * - generateToken(payload) — Create a JWT
742
- * - authenticate — Middleware to verify JWT and attach req.user
743
- * - requireRole(role) — Middleware to enforce role-based access
744
- */
745
-
746
- var config = require('../config');
747
-
748
- /**
749
- * Generate a JWT token.
750
- * Sandbox: base64-encoded JSON. Production: use jsonwebtoken.sign().
751
- *
752
- * @param {{ id: number, email: string, role: string }} payload
753
- * @returns {string} Token
754
- */
755
- function generateToken(payload) {
756
- var data = {
757
- sub: payload.id,
758
- email: payload.email,
759
- role: payload.role,
760
- iat: Date.now(),
761
- exp: Date.now() + 24 * 60 * 60 * 1000, // 24h
762
- };
763
- // Sandbox: base64 encode (NOT secure — for demo only)
764
- // Production: return jwt.sign(data, config.jwtSecret, { expiresIn: config.jwtExpiresIn });
765
- return btoa(JSON.stringify(data));
766
- }
767
-
768
- /**
769
- * Verify and decode a JWT token.
770
- * @param {string} token
771
- * @returns {Object|null} Decoded payload or null
772
- */
773
- function verifyToken(token) {
774
- try {
775
- // Sandbox: base64 decode
776
- // Production: return jwt.verify(token, config.jwtSecret);
777
- var data = JSON.parse(atob(token));
778
- if (data.exp && data.exp < Date.now()) return null;
779
- return data;
780
- } catch (e) {
781
- return null;
782
- }
783
- }
784
-
785
- /**
786
- * Hash a password.
787
- * Sandbox: base64 encode. Production: use bcrypt.hash().
788
- */
789
- function hashPassword(password) {
790
- return btoa(password);
791
- }
792
-
793
- /**
794
- * Compare a password with its hash.
795
- * Sandbox: base64 compare. Production: use bcrypt.compare().
796
- */
797
- function comparePassword(password, hash) {
798
- return btoa(password) === hash;
799
- }
800
-
801
- /**
802
- * Authentication middleware — verifies JWT from Authorization header.
803
- * Attaches decoded user info to req.user.
804
- */
805
- function authenticate(req, res, next) {
806
- var authHeader = req.headers.authorization;
807
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
808
- return res.status(401).json({ error: 'Authentication required' });
809
- }
810
-
811
- var token = authHeader.split(' ')[1];
812
- var decoded = verifyToken(token);
813
- if (!decoded) {
814
- return res.status(401).json({ error: 'Invalid or expired token' });
815
- }
816
-
817
- req.user = { id: decoded.sub, email: decoded.email, role: decoded.role };
818
- next();
819
- }
820
-
821
- /**
822
- * Role-based access middleware.
823
- * @param {...string} roles — Allowed roles
824
- * @returns {Function} Express middleware
825
- */
826
- function requireRole() {
827
- var roles = Array.prototype.slice.call(arguments);
828
- return function(req, res, next) {
829
- if (!req.user) {
830
- return res.status(401).json({ error: 'Authentication required' });
831
- }
832
- if (roles.indexOf(req.user.role) === -1) {
833
- return res.status(403).json({ error: 'Insufficient permissions' });
834
- }
835
- next();
836
- };
837
- }
838
-
839
- module.exports = {
840
- generateToken: generateToken,
841
- verifyToken: verifyToken,
842
- hashPassword: hashPassword,
843
- comparePassword: comparePassword,
844
- authenticate: authenticate,
845
- requireRole: requireRole,
846
- };
847
- `;
848
-
849
- // ---------------------------------------------------------------------------
850
- // middleware/errorHandler.js
851
- // ---------------------------------------------------------------------------
852
-
853
- const ERROR_HANDLER_JS = `/**
854
- * middleware/errorHandler.js — Global error handling.
855
- *
856
- * - notFound: 404 handler that serves index.html for SPA routes
857
- * - errorHandler: Catches all errors, returns structured JSON
858
- */
859
-
860
- var fs = require('fs');
861
- var path = require('path');
862
-
863
- /**
864
- * 404 handler — serves index.html for SPA routes, 404 JSON for API routes.
865
- */
866
- function notFound(req, res, next) {
867
- // API routes get a JSON 404
868
- if (req.path.startsWith('/api/')) {
869
- return res.status(404).json({ error: 'Endpoint not found: ' + req.method + ' ' + req.path });
870
- }
871
-
872
- // SPA fallback — serve index.html for all non-API routes
873
- try {
874
- var html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf-8');
875
- res.setHeader('Content-Type', 'text/html');
876
- res.send(html);
877
- } catch (e) {
878
- res.status(404).json({ error: 'Not found' });
879
- }
880
- }
881
-
882
- /**
883
- * Global error handler — catches thrown errors and returns structured JSON.
884
- */
885
- function errorHandler(err, req, res, next) {
886
- var status = err.status || err.statusCode || 500;
887
- var message = err.message || 'Internal server error';
888
-
889
- console.error('[Error] ' + req.method + ' ' + req.path + ' — ' + status + ': ' + message);
890
-
891
- res.status(status).json({
892
- error: message,
893
- ...(process.env.NODE_ENV === 'development' ? { stack: err.stack } : {}),
894
- });
895
- }
896
-
897
- module.exports = {
898
- notFound: notFound,
899
- errorHandler: errorHandler,
900
- };
901
- `;
902
-
903
- // ---------------------------------------------------------------------------
904
- // middleware/staticFiles.js
905
- // ---------------------------------------------------------------------------
906
-
907
- const STATIC_FILES_JS = `/**
908
- * middleware/staticFiles.js — Static file serving for Nodepod.
909
- *
910
- * Nodepod doesn't support express.static() or res.sendFile().
911
- * This middleware reads files via fs.readFileSync and sends them
912
- * with proper Content-Type headers.
913
- *
914
- * PRODUCTION: Replace this entire middleware with express.static('public').
915
- */
916
-
917
- var fs = require('fs');
918
- var path = require('path');
919
-
920
- var MIME_TYPES = {
921
- '.html': 'text/html; charset=utf-8',
922
- '.js': 'application/javascript; charset=utf-8',
923
- '.css': 'text/css; charset=utf-8',
924
- '.json': 'application/json; charset=utf-8',
925
- '.svg': 'image/svg+xml',
926
- '.png': 'image/png',
927
- '.jpg': 'image/jpeg',
928
- '.ico': 'image/x-icon',
929
- '.woff': 'font/woff',
930
- '.woff2': 'font/woff2',
931
- '.ttf': 'font/ttf',
932
- };
933
-
934
- /**
935
- * Static file serving middleware.
936
- * Serves files from the /public directory.
937
- */
938
- function serveStatic(req, res, next) {
939
- if (req.method !== 'GET' && req.method !== 'HEAD') return next();
940
-
941
- // Skip API routes
942
- if (req.path.startsWith('/api/')) return next();
943
-
944
- var reqPath = req.path === '/' ? '/index.html' : req.path;
945
- var filePath = path.join(__dirname, '..', 'public', reqPath);
946
- var ext = path.extname(filePath);
947
-
948
- // If no extension and not an API route, try serving index.html (SPA fallback)
949
- if (!ext) {
950
- filePath = path.join(__dirname, '..', 'public', 'index.html');
951
- ext = '.html';
952
- }
953
-
954
- try {
955
- var isBinary = ext === '.png' || ext === '.jpg' || ext === '.ico' || ext === '.woff' || ext === '.woff2' || ext === '.ttf';
956
- var data = fs.readFileSync(filePath, isBinary ? undefined : 'utf-8');
957
- res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
958
- res.send(data);
959
- } catch (e) {
960
- next();
961
- }
962
- }
963
-
964
- module.exports = serveStatic;
965
- `;
966
-
967
- // ---------------------------------------------------------------------------
968
- // middleware/validate.js
969
- // ---------------------------------------------------------------------------
970
-
971
- const VALIDATE_JS = `/**
972
- * middleware/validate.js — Request validation helpers.
973
- *
974
- * Simple validation middleware factory. For production, consider
975
- * using Joi, Zod, or express-validator.
976
- */
977
-
978
- /**
979
- * Validate that required fields exist in req.body.
980
- * @param {Array<string>} fields — Required field names
981
- * @returns {Function} Express middleware
982
- */
983
- function requireFields(fields) {
984
- return function(req, res, next) {
985
- var missing = [];
986
- fields.forEach(function(field) {
987
- if (req.body[field] === undefined || req.body[field] === null || req.body[field] === '') {
988
- missing.push(field);
989
- }
990
- });
991
-
992
- if (missing.length > 0) {
993
- return res.status(400).json({
994
- error: 'Missing required fields: ' + missing.join(', '),
995
- });
996
- }
997
- next();
998
- };
999
- }
1000
-
1001
- /**
1002
- * Validate that a field value is one of the allowed values.
1003
- * @param {string} field — Field name in req.body
1004
- * @param {Array<string>} allowed — Allowed values
1005
- * @returns {Function} Express middleware
1006
- */
1007
- function allowedValues(field, allowed) {
1008
- return function(req, res, next) {
1009
- if (req.body[field] !== undefined && allowed.indexOf(req.body[field]) === -1) {
1010
- return res.status(400).json({
1011
- error: field + ' must be one of: ' + allowed.join(', '),
1012
- });
1013
- }
1014
- next();
1015
- };
1016
- }
1017
-
1018
- module.exports = {
1019
- requireFields: requireFields,
1020
- allowedValues: allowedValues,
1021
- };
1022
- `;
1023
-
1024
- // ---------------------------------------------------------------------------
1025
- // routes/auth.js
1026
- // ---------------------------------------------------------------------------
1027
-
1028
- const ROUTES_AUTH_JS = `/**
1029
- * routes/auth.js — Authentication endpoints.
1030
- *
1031
- * POST /api/auth/register — Create new account
1032
- * POST /api/auth/login — Sign in, receive JWT
1033
- * GET /api/auth/me — Get current user profile
1034
- */
1035
-
1036
- var express = require('express');
1037
- var router = express.Router();
1038
- var auth = require('../middleware/auth');
1039
- var validate = require('../middleware/validate');
1040
- var userRepo = require('../db/repositories/userRepository');
1041
-
1042
- /**
1043
- * POST /api/auth/register
1044
- * Body: { email, password, name }
1045
- * Returns: { user, token }
1046
- */
1047
- router.post('/register',
1048
- validate.requireFields(['email', 'password', 'name']),
1049
- function(req, res) {
1050
- try {
1051
- // Check if email already exists
1052
- var existing = userRepo.findByEmail(req.body.email);
1053
- if (existing) {
1054
- return res.status(409).json({ error: 'Email already registered' });
1055
- }
1056
-
1057
- // Create user with hashed password
1058
- var user = userRepo.create({
1059
- email: req.body.email,
1060
- password: auth.hashPassword(req.body.password),
1061
- name: req.body.name,
1062
- role: 'user',
1063
- });
1064
-
1065
- var token = auth.generateToken(user);
1066
- res.status(201).json({ user: user, token: token });
1067
- } catch (err) {
1068
- console.error('[Auth] Register error:', err.message);
1069
- res.status(500).json({ error: 'Registration failed' });
1070
- }
1071
- }
1072
- );
1073
-
1074
- /**
1075
- * POST /api/auth/login
1076
- * Body: { email, password }
1077
- * Returns: { user, token }
1078
- */
1079
- router.post('/login',
1080
- validate.requireFields(['email', 'password']),
1081
- function(req, res) {
1082
- try {
1083
- var user = userRepo.findByEmail(req.body.email);
1084
- if (!user) {
1085
- return res.status(401).json({ error: 'Invalid email or password' });
1086
- }
1087
-
1088
- if (!auth.comparePassword(req.body.password, user.password)) {
1089
- return res.status(401).json({ error: 'Invalid email or password' });
1090
- }
1091
-
1092
- // Return user without password
1093
- var safeUser = {
1094
- id: user.id,
1095
- email: user.email,
1096
- name: user.name,
1097
- role: user.role,
1098
- created_at: user.created_at,
1099
- };
1100
-
1101
- var token = auth.generateToken(safeUser);
1102
- res.json({ user: safeUser, token: token });
1103
- } catch (err) {
1104
- console.error('[Auth] Login error:', err.message);
1105
- res.status(500).json({ error: 'Login failed' });
1106
- }
1107
- }
1108
- );
1109
-
1110
- /**
1111
- * GET /api/auth/me
1112
- * Headers: Authorization: Bearer <token>
1113
- * Returns: { user }
1114
- */
1115
- router.get('/me', auth.authenticate, function(req, res) {
1116
- try {
1117
- var user = userRepo.findById(req.user.id);
1118
- if (!user) {
1119
- return res.status(404).json({ error: 'User not found' });
1120
- }
1121
- res.json({ user: user });
1122
- } catch (err) {
1123
- console.error('[Auth] Me error:', err.message);
1124
- res.status(500).json({ error: 'Failed to fetch profile' });
1125
- }
1126
- });
1127
-
1128
- module.exports = router;
1129
- `;
1130
-
1131
- // ---------------------------------------------------------------------------
1132
- // routes/items.js
1133
- // ---------------------------------------------------------------------------
1134
-
1135
- const ROUTES_ITEMS_JS = `/**
1136
- * routes/items.js — Item CRUD endpoints.
1137
- *
1138
- * All routes require authentication (JWT).
1139
- *
1140
- * GET /api/items — List items (with optional filters)
1141
- * POST /api/items — Create item
1142
- * GET /api/items/stats — Get aggregate statistics
1143
- * GET /api/items/:id — Get single item
1144
- * PUT /api/items/:id — Update item
1145
- * DELETE /api/items/:id — Delete item
1146
- */
1147
-
1148
- var express = require('express');
1149
- var router = express.Router();
1150
- var auth = require('../middleware/auth');
1151
- var validate = require('../middleware/validate');
1152
- var itemRepo = require('../db/repositories/itemRepository');
1153
-
1154
- // All item routes require authentication
1155
- router.use(auth.authenticate);
1156
-
1157
- /**
1158
- * GET /api/items
1159
- * Query: ?status=todo&priority=high
1160
- * Returns: { items: [...] }
1161
- */
1162
- router.get('/', function(req, res) {
1163
- try {
1164
- var filters = {
1165
- status: req.query.status || undefined,
1166
- priority: req.query.priority || undefined,
1167
- };
1168
- var items = itemRepo.findAll(filters);
1169
- res.json({ items: items });
1170
- } catch (err) {
1171
- console.error('[Items] List error:', err.message);
1172
- res.status(500).json({ error: 'Failed to fetch items' });
1173
- }
1174
- });
1175
-
1176
- /**
1177
- * GET /api/items/stats
1178
- * Returns: { total, byStatus, byPriority }
1179
- */
1180
- router.get('/stats', function(req, res) {
1181
- try {
1182
- var stats = itemRepo.getStats();
1183
- res.json(stats);
1184
- } catch (err) {
1185
- console.error('[Items] Stats error:', err.message);
1186
- res.status(500).json({ error: 'Failed to fetch stats' });
1187
- }
1188
- });
1189
-
1190
- /**
1191
- * GET /api/items/:id
1192
- * Returns: { item }
1193
- */
1194
- router.get('/:id', function(req, res) {
1195
- try {
1196
- var item = itemRepo.findById(parseInt(req.params.id, 10));
1197
- if (!item) {
1198
- return res.status(404).json({ error: 'Item not found' });
1199
- }
1200
- res.json({ item: item });
1201
- } catch (err) {
1202
- console.error('[Items] Get error:', err.message);
1203
- res.status(500).json({ error: 'Failed to fetch item' });
1204
- }
1205
- });
1206
-
1207
- /**
1208
- * POST /api/items
1209
- * Body: { title, description?, status?, priority? }
1210
- * Returns: { item }
1211
- */
1212
- router.post('/',
1213
- validate.requireFields(['title']),
1214
- validate.allowedValues('status', ['todo', 'in_progress', 'done']),
1215
- validate.allowedValues('priority', ['low', 'medium', 'high']),
1216
- function(req, res) {
1217
- try {
1218
- var item = itemRepo.create({
1219
- title: req.body.title,
1220
- description: req.body.description || '',
1221
- status: req.body.status || 'todo',
1222
- priority: req.body.priority || 'medium',
1223
- user_id: req.user.id,
1224
- });
1225
- res.status(201).json({ item: item });
1226
- } catch (err) {
1227
- console.error('[Items] Create error:', err.message);
1228
- res.status(500).json({ error: 'Failed to create item' });
1229
- }
1230
- }
1231
- );
1232
-
1233
- /**
1234
- * PUT /api/items/:id
1235
- * Body: { title?, description?, status?, priority? }
1236
- * Returns: { item }
1237
- */
1238
- router.put('/:id',
1239
- validate.allowedValues('status', ['todo', 'in_progress', 'done']),
1240
- validate.allowedValues('priority', ['low', 'medium', 'high']),
1241
- function(req, res) {
1242
- try {
1243
- var existing = itemRepo.findById(parseInt(req.params.id, 10));
1244
- if (!existing) {
1245
- return res.status(404).json({ error: 'Item not found' });
1246
- }
1247
-
1248
- var item = itemRepo.update(parseInt(req.params.id, 10), {
1249
- title: req.body.title,
1250
- description: req.body.description,
1251
- status: req.body.status,
1252
- priority: req.body.priority,
1253
- });
1254
- res.json({ item: item });
1255
- } catch (err) {
1256
- console.error('[Items] Update error:', err.message);
1257
- res.status(500).json({ error: 'Failed to update item' });
1258
- }
1259
- }
1260
- );
1261
-
1262
- /**
1263
- * DELETE /api/items/:id
1264
- * Returns: { message }
1265
- */
1266
- router.delete('/:id', function(req, res) {
1267
- try {
1268
- var deleted = itemRepo.remove(parseInt(req.params.id, 10));
1269
- if (!deleted) {
1270
- return res.status(404).json({ error: 'Item not found' });
1271
- }
1272
- res.json({ message: 'Item deleted' });
1273
- } catch (err) {
1274
- console.error('[Items] Delete error:', err.message);
1275
- res.status(500).json({ error: 'Failed to delete item' });
1276
- }
1277
- });
1278
-
1279
- module.exports = router;
1280
- `;
1281
-
1282
- // ---------------------------------------------------------------------------
1283
- // server.js
1284
- // ---------------------------------------------------------------------------
1285
-
1286
- const SERVER_JS = `/**
1287
- * server.js — Express application entry point.
1288
- *
1289
- * Middleware chain:
1290
- * 1. Body parsing (JSON)
1291
- * 2. Request logging
1292
- * 3. Static file serving (Nodepod-compatible)
1293
- * 4. API routes (auth, items)
1294
- * 5. SPA fallback (404 → index.html)
1295
- * 6. Error handler
1296
- */
1297
-
1298
- var express = require('express');
1299
- var config = require('./config');
1300
- var db = require('./db/database');
1301
- var serveStatic = require('./middleware/staticFiles');
1302
- var errorHandler = require('./middleware/errorHandler');
1303
- var authRoutes = require('./routes/auth');
1304
- var itemRoutes = require('./routes/items');
1305
-
1306
- var app = express();
1307
-
1308
- // ---------------------------------------------------------------------------
1309
- // Middleware
1310
- // ---------------------------------------------------------------------------
1311
-
1312
- // Parse JSON bodies
1313
- app.use(express.json());
1314
-
1315
- // Request logging
1316
- app.use(function(req, res, next) {
1317
- var start = Date.now();
1318
- res.on('finish', function() {
1319
- var duration = Date.now() - start;
1320
- console.log(
1321
- '[' + req.method + '] ' + req.path + ' → ' + res.statusCode + ' (' + duration + 'ms)'
1322
- );
1323
- });
1324
- next();
1325
- });
1326
-
1327
- // Static files (must be before API routes for / to serve index.html)
1328
- app.use(serveStatic);
1329
-
1330
- // ---------------------------------------------------------------------------
1331
- // API Routes
1332
- // ---------------------------------------------------------------------------
1333
-
1334
- // Health check — used by Docker HEALTHCHECK, load balancers, and uptime monitors
1335
- app.get('/api/health', function(req, res) {
1336
- res.json({ status: 'ok', uptime: process.uptime() });
1337
- });
1338
-
1339
- app.use('/api/auth', authRoutes);
1340
- app.use('/api/items', itemRoutes);
1341
-
1342
- // ---------------------------------------------------------------------------
1343
- // Error handling
1344
- // ---------------------------------------------------------------------------
1345
-
1346
- app.use(errorHandler.notFound);
1347
- app.use(errorHandler.errorHandler);
1348
-
1349
- // ---------------------------------------------------------------------------
1350
- // Start server after DB initialization
1351
- // ---------------------------------------------------------------------------
1352
-
1353
- console.log('[Server] Initializing database...');
1354
-
1355
- db.initialize().then(function() {
1356
- console.log('[Server] Database ready, starting HTTP server...');
1357
- app.listen(config.port, function() {
1358
- console.log('');
1359
- console.log('===========================================');
1360
- console.log(' Server running on port ' + config.port);
1361
- console.log(' Database: SQLite (sql.js WASM)');
1362
- console.log(' Environment: ' + config.nodeEnv);
1363
- console.log('===========================================');
1364
- console.log('');
1365
- console.log('Demo accounts:');
1366
- console.log(' admin@example.com / admin123');
1367
- console.log(' user@example.com / user123');
1368
- console.log('');
1369
- });
1370
- }).catch(function(err) {
1371
- console.error('[FATAL] Failed to initialize database:', err && err.message ? err.message : err);
1372
- console.error('[FATAL] Stack:', err && err.stack ? err.stack : 'no stack');
1373
- });
1374
- `;
1375
-
1376
- // ---------------------------------------------------------------------------
1377
- // public/index.html
1378
- // ---------------------------------------------------------------------------
1379
-
1380
- const INDEX_HTML = `<!DOCTYPE html>
1381
- <html lang="en">
1382
- <head>
1383
- <meta charset="UTF-8">
1384
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1385
- <title>Starter App</title>
1386
- <script src="https://cdn.tailwindcss.com"><\/script>
1387
- <script>
1388
- tailwind.config = {
1389
- theme: {
1390
- extend: {
1391
- colors: {
1392
- primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a' },
1393
- }
1394
- }
1395
- }
1396
- }
1397
- <\/script>
1398
- <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"><\/script>
1399
- <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"><\/script>
1400
- <script src="https://unpkg.com/@remix-run/router@1.21.0/dist/router.umd.min.js"><\/script>
1401
- <script src="https://unpkg.com/react-router@6.28.0/dist/umd/react-router.production.min.js"><\/script>
1402
- <script src="https://unpkg.com/react-router-dom@6.28.0/dist/umd/react-router-dom.production.min.js"><\/script>
1403
- <script src="https://unpkg.com/chart.js@4.4.0/dist/chart.umd.js"><\/script>
1404
- <link rel="stylesheet" href="/styles.css">
1405
- </head>
1406
- <body class="bg-gray-50 min-h-screen antialiased">
1407
- <div id="root"></div>
1408
-
1409
- <!-- Lib -->
1410
- <script src="/lib/api.js"><\/script>
1411
-
1412
- <!-- Store -->
1413
- <script src="/store/AuthContext.js"><\/script>
1414
- <script src="/store/AppContext.js"><\/script>
1415
-
1416
- <!-- Components -->
1417
- <script src="/components/Toast.js"><\/script>
1418
- <script src="/components/Navbar.js"><\/script>
1419
- <script src="/components/ItemCard.js"><\/script>
1420
- <script src="/components/ItemForm.js"><\/script>
1421
- <script src="/components/StatsChart.js"><\/script>
1422
-
1423
- <!-- Pages -->
1424
- <script src="/pages/WelcomePage.js"><\/script>
1425
- <script src="/pages/LoginPage.js"><\/script>
1426
- <script src="/pages/RegisterPage.js"><\/script>
1427
- <script src="/pages/DashboardPage.js"><\/script>
1428
-
1429
- <!-- App (must be last) -->
1430
- <script src="/App.js"><\/script>
1431
- </body>
1432
- </html>`;
1433
-
1434
- // ---------------------------------------------------------------------------
1435
- // public/styles.css
1436
- // ---------------------------------------------------------------------------
1437
-
1438
- const STYLES_CSS = `/*
1439
- * Ranger Theme Variables — chart colors and design tokens.
1440
- * HSL values stored WITHOUT hsl() wrapper; consume as: color: hsl(var(--chart-1));
1441
- */
1442
-
1443
- /* --- Light mode (default) ----------------------------------------------- */
1444
- :root {
1445
- --text-primary: #1a1a1a;
1446
- --text-secondary: #565869;
1447
- --text-tertiary: #8e8ea0;
1448
- --surface-primary: #ffffff;
1449
- --surface-secondary: #f9fafb;
1450
- --surface-tertiary: #f3f4f6;
1451
- --border-light: #e5e7eb;
1452
- --border-medium: #d1d5db;
1453
- --border-heavy: #9ca3af;
1454
- --background: 0 0% 100%;
1455
- --foreground: 0 0% 3.9%;
1456
- --card: 0 0% 100%;
1457
- --card-foreground: 0 0% 3.9%;
1458
- --primary: 0 0% 9%;
1459
- --primary-foreground: 0 0% 98%;
1460
- --secondary: 0 0% 96.1%;
1461
- --secondary-foreground: 0 0% 9%;
1462
- --muted: 0 0% 96.1%;
1463
- --muted-foreground: 0 0% 45.1%;
1464
- --accent: 0 0% 96.1%;
1465
- --accent-foreground: 0 0% 9%;
1466
- --destructive: 0 84.2% 60.2%;
1467
- --destructive-foreground: 0 0% 98%;
1468
- --border: 0 0% 89.8%;
1469
- --input: 0 0% 89.8%;
1470
- --ring: 0 0% 3.9%;
1471
- --radius: 0.5rem;
1472
- --chart-1: 12 76% 61%;
1473
- --chart-2: 173 58% 39%;
1474
- --chart-3: 197 37% 24%;
1475
- --chart-4: 43 74% 66%;
1476
- --chart-5: 27 87% 67%;
1477
- }
1478
-
1479
- /* --- Dark mode ---------------------------------------------------------- */
1480
- .dark {
1481
- --text-primary: #f3f4f6;
1482
- --text-secondary: #d1d5db;
1483
- --text-tertiary: #6b7280;
1484
- --surface-primary: #111827;
1485
- --surface-secondary: #1f2937;
1486
- --surface-tertiary: #374151;
1487
- --border-light: #3a3a3b;
1488
- --border-medium: #4b5563;
1489
- --border-heavy: #6b7280;
1490
- --background: 0 0% 7%;
1491
- --foreground: 0 0% 98%;
1492
- --card: 0 0% 3.9%;
1493
- --card-foreground: 0 0% 98%;
1494
- --primary: 0 0% 98%;
1495
- --primary-foreground: 0 0% 9%;
1496
- --secondary: 0 0% 14.9%;
1497
- --secondary-foreground: 0 0% 98%;
1498
- --muted: 0 0% 14.9%;
1499
- --muted-foreground: 0 0% 63.9%;
1500
- --accent: 0 0% 14.9%;
1501
- --accent-foreground: 0 0% 98%;
1502
- --destructive: 0 62.8% 40.6%;
1503
- --destructive-foreground: 0 0% 98%;
1504
- --border: 0 0% 14.9%;
1505
- --input: 0 0% 14.9%;
1506
- --ring: 0 0% 83.1%;
1507
- --chart-1: 220 70% 50%;
1508
- --chart-2: 160 60% 45%;
1509
- --chart-3: 30 80% 55%;
1510
- --chart-4: 280 65% 60%;
1511
- --chart-5: 340 75% 55%;
1512
- }
1513
-
1514
- /* --- Animations --------------------------------------------------------- */
1515
- .fade-in {
1516
- animation: fadeIn 0.3s ease-out;
1517
- }
1518
- @keyframes fadeIn {
1519
- from { opacity: 0; transform: translateY(8px); }
1520
- to { opacity: 1; transform: translateY(0); }
1521
- }
1522
- .slide-in {
1523
- animation: slideIn 0.3s ease-out;
1524
- }
1525
- @keyframes slideIn {
1526
- from { opacity: 0; transform: translateX(-12px); }
1527
- to { opacity: 1; transform: translateX(0); }
1528
- }
1529
- .card-hover {
1530
- transition: transform 0.2s ease, box-shadow 0.2s ease;
1531
- }
1532
- .card-hover:hover {
1533
- transform: translateY(-2px);
1534
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
1535
- }
1536
- .toast-enter {
1537
- animation: toastIn 0.3s ease-out;
1538
- }
1539
- @keyframes toastIn {
1540
- from { opacity: 0; transform: translateY(-16px) scale(0.95); }
1541
- to { opacity: 1; transform: translateY(0) scale(1); }
1542
- }
1543
- `;
1544
-
1545
- // ---------------------------------------------------------------------------
1546
- // public/lib/api.js
1547
- // ---------------------------------------------------------------------------
1548
-
1549
- const API_JS = `/**
1550
- * lib/api.js — API client.
1551
- *
1552
- * Wraps fetch() with:
1553
- * - Automatic Authorization header from stored token
1554
- * - JSON parsing
1555
- * - Error handling with structured error messages
1556
- */
1557
-
1558
- window.ApiClient = (function() {
1559
- var TOKEN_KEY = 'auth_token';
1560
-
1561
- function getToken() {
1562
- try { return localStorage.getItem(TOKEN_KEY); } catch(e) { return null; }
1563
- }
1564
-
1565
- function setToken(token) {
1566
- try { if (token) localStorage.setItem(TOKEN_KEY, token); else localStorage.removeItem(TOKEN_KEY); } catch(e) {}
1567
- }
1568
-
1569
- function clearToken() {
1570
- try { localStorage.removeItem(TOKEN_KEY); } catch(e) {}
1571
- }
1572
-
1573
- /**
1574
- * Make an API request.
1575
- * @param {string} path — API path (e.g., '/api/items')
1576
- * @param {Object} options — fetch options
1577
- * @returns {Promise<Object>} Parsed JSON response
1578
- */
1579
- async function request(path, options) {
1580
- options = options || {};
1581
- var headers = { 'Content-Type': 'application/json' };
1582
- var token = getToken();
1583
- if (token) {
1584
- headers['Authorization'] = 'Bearer ' + token;
1585
- }
1586
- options.headers = Object.assign(headers, options.headers || {});
1587
-
1588
- var response = await fetch(path, options);
1589
- var data;
1590
- try {
1591
- data = await response.json();
1592
- } catch (e) {
1593
- data = { error: 'Invalid response from server' };
1594
- }
1595
-
1596
- if (!response.ok) {
1597
- var err = new Error(data.error || 'Request failed');
1598
- err.status = response.status;
1599
- err.data = data;
1600
- throw err;
1601
- }
1602
- return data;
1603
- }
1604
-
1605
- return {
1606
- getToken: getToken,
1607
- setToken: setToken,
1608
- clearToken: clearToken,
1609
- get: function(path) { return request(path, { method: 'GET' }); },
1610
- post: function(path, body) { return request(path, { method: 'POST', body: JSON.stringify(body) }); },
1611
- put: function(path, body) { return request(path, { method: 'PUT', body: JSON.stringify(body) }); },
1612
- del: function(path) { return request(path, { method: 'DELETE' }); },
1613
- };
1614
- })();
1615
- `;
1616
-
1617
- // ---------------------------------------------------------------------------
1618
- // public/store/AuthContext.js
1619
- // ---------------------------------------------------------------------------
1620
-
1621
- const AUTH_CONTEXT_JS = `/**
1622
- * store/AuthContext.js — Authentication state management.
1623
- *
1624
- * Provides:
1625
- * - AuthProvider: Wraps app with auth state
1626
- * - useAuth(): Hook to access { user, token, loading, login, register, logout }
1627
- *
1628
- * Uses React Context + useReducer for predictable state updates.
1629
- */
1630
-
1631
- var h = React.createElement;
1632
-
1633
- // Action types
1634
- var AUTH_ACTIONS = {
1635
- SET_LOADING: 'SET_LOADING',
1636
- LOGIN_SUCCESS: 'LOGIN_SUCCESS',
1637
- LOGOUT: 'LOGOUT',
1638
- SET_USER: 'SET_USER',
1639
- };
1640
-
1641
- // Reducer
1642
- function authReducer(state, action) {
1643
- switch (action.type) {
1644
- case AUTH_ACTIONS.SET_LOADING:
1645
- return Object.assign({}, state, { loading: action.payload });
1646
- case AUTH_ACTIONS.LOGIN_SUCCESS:
1647
- return Object.assign({}, state, {
1648
- user: action.payload.user,
1649
- token: action.payload.token,
1650
- loading: false,
1651
- });
1652
- case AUTH_ACTIONS.SET_USER:
1653
- return Object.assign({}, state, { user: action.payload, loading: false });
1654
- case AUTH_ACTIONS.LOGOUT:
1655
- return { user: null, token: null, loading: false };
1656
- default:
1657
- return state;
1658
- }
1659
- }
1660
-
1661
- // Context
1662
- var AuthContext = React.createContext(null);
1663
-
1664
- /**
1665
- * AuthProvider — Wraps children with auth state.
1666
- * On mount, checks for stored token and fetches user profile.
1667
- */
1668
- window.AuthProvider = function AuthProvider(props) {
1669
- var initialState = { user: null, token: null, loading: true };
1670
- var stateAndDispatch = React.useReducer(authReducer, initialState);
1671
- var state = stateAndDispatch[0];
1672
- var dispatch = stateAndDispatch[1];
1673
-
1674
- // Check stored token on mount
1675
- React.useEffect(function() {
1676
- var token = ApiClient.getToken();
1677
- if (!token) {
1678
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: false });
1679
- return;
1680
- }
1681
-
1682
- ApiClient.get('/api/auth/me')
1683
- .then(function(data) {
1684
- dispatch({ type: AUTH_ACTIONS.LOGIN_SUCCESS, payload: { user: data.user, token: token } });
1685
- })
1686
- .catch(function() {
1687
- ApiClient.clearToken();
1688
- dispatch({ type: AUTH_ACTIONS.LOGOUT });
1689
- });
1690
- }, []);
1691
-
1692
- /**
1693
- * Login with email and password.
1694
- * @returns {Promise<Object>} User data
1695
- */
1696
- var login = React.useCallback(function(email, password) {
1697
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: true });
1698
- return ApiClient.post('/api/auth/login', { email: email, password: password })
1699
- .then(function(data) {
1700
- ApiClient.setToken(data.token);
1701
- dispatch({ type: AUTH_ACTIONS.LOGIN_SUCCESS, payload: data });
1702
- return data;
1703
- })
1704
- .catch(function(err) {
1705
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: false });
1706
- throw err;
1707
- });
1708
- }, []);
1709
-
1710
- /**
1711
- * Register a new account.
1712
- * @returns {Promise<Object>} User data
1713
- */
1714
- var register = React.useCallback(function(email, password, name) {
1715
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: true });
1716
- return ApiClient.post('/api/auth/register', { email: email, password: password, name: name })
1717
- .then(function(data) {
1718
- ApiClient.setToken(data.token);
1719
- dispatch({ type: AUTH_ACTIONS.LOGIN_SUCCESS, payload: data });
1720
- return data;
1721
- })
1722
- .catch(function(err) {
1723
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: false });
1724
- throw err;
1725
- });
1726
- }, []);
1727
-
1728
- /**
1729
- * Logout — clear token and state.
1730
- */
1731
- var logout = React.useCallback(function() {
1732
- ApiClient.clearToken();
1733
- dispatch({ type: AUTH_ACTIONS.LOGOUT });
1734
- }, []);
1735
-
1736
- var value = {
1737
- user: state.user,
1738
- token: state.token,
1739
- loading: state.loading,
1740
- login: login,
1741
- register: register,
1742
- logout: logout,
1743
- isAuthenticated: !!state.user,
1744
- };
1745
-
1746
- return h(AuthContext.Provider, { value: value }, props.children);
1747
- };
1748
-
1749
- /**
1750
- * useAuth — Hook to access auth state and actions.
1751
- * @returns {{ user, token, loading, login, register, logout, isAuthenticated }}
1752
- */
1753
- window.useAuth = function useAuth() {
1754
- var context = React.useContext(AuthContext);
1755
- if (!context) throw new Error('useAuth must be used within AuthProvider');
1756
- return context;
1757
- };
1758
- `;
1759
-
1760
- // ---------------------------------------------------------------------------
1761
- // public/store/AppContext.js
1762
- // ---------------------------------------------------------------------------
1763
-
1764
- const APP_CONTEXT_JS = `/**
1765
- * store/AppContext.js — Application state management.
1766
- *
1767
- * Manages items, loading states, filters, and toasts.
1768
- * Uses React Context + useReducer.
1769
- *
1770
- * Provides:
1771
- * - AppProvider: Wraps app with state
1772
- * - useApp(): Hook to access state and CRUD actions
1773
- */
1774
-
1775
- var h = React.createElement;
1776
-
1777
- var APP_ACTIONS = {
1778
- SET_LOADING: 'SET_LOADING',
1779
- SET_ITEMS: 'SET_ITEMS',
1780
- ADD_ITEM: 'ADD_ITEM',
1781
- UPDATE_ITEM: 'UPDATE_ITEM',
1782
- REMOVE_ITEM: 'REMOVE_ITEM',
1783
- SET_STATS: 'SET_STATS',
1784
- SET_FILTER: 'SET_FILTER',
1785
- ADD_TOAST: 'ADD_TOAST',
1786
- REMOVE_TOAST: 'REMOVE_TOAST',
1787
- };
1788
-
1789
- function appReducer(state, action) {
1790
- switch (action.type) {
1791
- case APP_ACTIONS.SET_LOADING:
1792
- return Object.assign({}, state, { loading: action.payload });
1793
- case APP_ACTIONS.SET_ITEMS:
1794
- return Object.assign({}, state, { items: action.payload, loading: false });
1795
- case APP_ACTIONS.ADD_ITEM:
1796
- return Object.assign({}, state, { items: [action.payload].concat(state.items) });
1797
- case APP_ACTIONS.UPDATE_ITEM:
1798
- return Object.assign({}, state, {
1799
- items: state.items.map(function(i) { return i.id === action.payload.id ? action.payload : i; }),
1800
- });
1801
- case APP_ACTIONS.REMOVE_ITEM:
1802
- return Object.assign({}, state, {
1803
- items: state.items.filter(function(i) { return i.id !== action.payload; }),
1804
- });
1805
- case APP_ACTIONS.SET_STATS:
1806
- return Object.assign({}, state, { stats: action.payload });
1807
- case APP_ACTIONS.SET_FILTER:
1808
- return Object.assign({}, state, {
1809
- filters: Object.assign({}, state.filters, action.payload),
1810
- });
1811
- case APP_ACTIONS.ADD_TOAST:
1812
- return Object.assign({}, state, {
1813
- toasts: state.toasts.concat([action.payload]),
1814
- });
1815
- case APP_ACTIONS.REMOVE_TOAST:
1816
- return Object.assign({}, state, {
1817
- toasts: state.toasts.filter(function(t) { return t.id !== action.payload; }),
1818
- });
1819
- default:
1820
- return state;
1821
- }
1822
- }
1823
-
1824
- var AppContext = React.createContext(null);
1825
-
1826
- window.AppProvider = function AppProvider(props) {
1827
- var initialState = {
1828
- items: [],
1829
- stats: null,
1830
- loading: false,
1831
- filters: { status: '', priority: '' },
1832
- toasts: [],
1833
- };
1834
- var stateAndDispatch = React.useReducer(appReducer, initialState);
1835
- var state = stateAndDispatch[0];
1836
- var dispatch = stateAndDispatch[1];
1837
-
1838
- var toastId = React.useRef(0);
1839
-
1840
- // Toast helper
1841
- function addToast(message, type) {
1842
- var id = ++toastId.current;
1843
- dispatch({ type: APP_ACTIONS.ADD_TOAST, payload: { id: id, message: message, type: type || 'info' } });
1844
- setTimeout(function() {
1845
- dispatch({ type: APP_ACTIONS.REMOVE_TOAST, payload: id });
1846
- }, 3000);
1847
- }
1848
-
1849
- // CRUD actions
1850
- function fetchItems(filters) {
1851
- dispatch({ type: APP_ACTIONS.SET_LOADING, payload: true });
1852
- var query = '';
1853
- if (filters) {
1854
- var parts = [];
1855
- if (filters.status) parts.push('status=' + filters.status);
1856
- if (filters.priority) parts.push('priority=' + filters.priority);
1857
- if (parts.length > 0) query = '?' + parts.join('&');
1858
- }
1859
- return ApiClient.get('/api/items' + query)
1860
- .then(function(data) {
1861
- dispatch({ type: APP_ACTIONS.SET_ITEMS, payload: data.items });
1862
- return data.items;
1863
- })
1864
- .catch(function(err) {
1865
- dispatch({ type: APP_ACTIONS.SET_LOADING, payload: false });
1866
- addToast(err.message, 'error');
1867
- });
1868
- }
1869
-
1870
- function fetchStats() {
1871
- return ApiClient.get('/api/items/stats')
1872
- .then(function(data) {
1873
- dispatch({ type: APP_ACTIONS.SET_STATS, payload: data });
1874
- return data;
1875
- })
1876
- .catch(function(err) {
1877
- addToast(err.message, 'error');
1878
- });
1879
- }
1880
-
1881
- function createItem(data) {
1882
- return ApiClient.post('/api/items', data)
1883
- .then(function(result) {
1884
- dispatch({ type: APP_ACTIONS.ADD_ITEM, payload: result.item });
1885
- addToast('Item created', 'success');
1886
- fetchStats();
1887
- return result.item;
1888
- })
1889
- .catch(function(err) {
1890
- addToast(err.message, 'error');
1891
- throw err;
1892
- });
1893
- }
1894
-
1895
- function updateItem(id, data) {
1896
- return ApiClient.put('/api/items/' + id, data)
1897
- .then(function(result) {
1898
- dispatch({ type: APP_ACTIONS.UPDATE_ITEM, payload: result.item });
1899
- addToast('Item updated', 'success');
1900
- fetchStats();
1901
- return result.item;
1902
- })
1903
- .catch(function(err) {
1904
- addToast(err.message, 'error');
1905
- throw err;
1906
- });
1907
- }
1908
-
1909
- function deleteItem(id) {
1910
- return ApiClient.del('/api/items/' + id)
1911
- .then(function() {
1912
- dispatch({ type: APP_ACTIONS.REMOVE_ITEM, payload: id });
1913
- addToast('Item deleted', 'success');
1914
- fetchStats();
1915
- })
1916
- .catch(function(err) {
1917
- addToast(err.message, 'error');
1918
- throw err;
1919
- });
1920
- }
1921
-
1922
- function setFilter(filter) {
1923
- dispatch({ type: APP_ACTIONS.SET_FILTER, payload: filter });
1924
- }
1925
-
1926
- var value = {
1927
- items: state.items,
1928
- stats: state.stats,
1929
- loading: state.loading,
1930
- filters: state.filters,
1931
- toasts: state.toasts,
1932
- fetchItems: fetchItems,
1933
- fetchStats: fetchStats,
1934
- createItem: createItem,
1935
- updateItem: updateItem,
1936
- deleteItem: deleteItem,
1937
- setFilter: setFilter,
1938
- addToast: addToast,
1939
- };
1940
-
1941
- return h(AppContext.Provider, { value: value }, props.children);
1942
- };
1943
-
1944
- window.useApp = function useApp() {
1945
- var context = React.useContext(AppContext);
1946
- if (!context) throw new Error('useApp must be used within AppProvider');
1947
- return context;
1948
- };
1949
- `;
1950
-
1951
- // ---------------------------------------------------------------------------
1952
- // public/components/Toast.js
1953
- // ---------------------------------------------------------------------------
1954
-
1955
- const TOAST_JS = `var h = React.createElement;
1956
-
1957
- /**
1958
- * Toast — Notification toast displayed at top-right.
1959
- */
1960
- window.Toast = function Toast(props) {
1961
- var toasts = props.toasts || [];
1962
- if (toasts.length === 0) return null;
1963
-
1964
- var colors = {
1965
- success: 'bg-emerald-500',
1966
- error: 'bg-red-500',
1967
- info: 'bg-blue-500',
1968
- warning: 'bg-amber-500',
1969
- };
1970
-
1971
- return h('div', { className: 'fixed top-4 right-4 z-50 space-y-2' },
1972
- toasts.map(function(toast) {
1973
- return h('div', {
1974
- key: toast.id,
1975
- className: 'toast-enter px-4 py-3 rounded-lg text-white text-sm font-medium shadow-lg ' + (colors[toast.type] || colors.info),
1976
- }, toast.message);
1977
- })
1978
- );
1979
- };
1980
- `;
1981
-
1982
- // ---------------------------------------------------------------------------
1983
- // public/components/Navbar.js
1984
- // ---------------------------------------------------------------------------
1985
-
1986
- const NAVBAR_JS = `var h = React.createElement;
1987
- var RR = ReactRouterDOM;
1988
-
1989
- /**
1990
- * Navbar — Top navigation bar with logo, links, and auth status.
1991
- */
1992
- window.Navbar = function Navbar() {
1993
- var auth = window.useAuth();
1994
-
1995
- return h('header', { className: 'bg-white border-b border-gray-200 sticky top-0 z-40' },
1996
- h('div', { className: 'max-w-6xl mx-auto px-4 sm:px-6' },
1997
- h('div', { className: 'flex items-center justify-between h-14' },
1998
- // Logo + nav
1999
- h('div', { className: 'flex items-center gap-6' },
2000
- h(RR.Link, { to: '/', className: 'flex items-center gap-2' },
2001
- h('div', { className: 'w-7 h-7 bg-primary-600 rounded-lg flex items-center justify-center' },
2002
- h('span', { className: 'text-white font-bold text-sm' }, 'S')
2003
- ),
2004
- h('span', { className: 'font-semibold text-gray-800' }, 'Starter')
2005
- ),
2006
- auth.isAuthenticated ? h('nav', { className: 'hidden sm:flex items-center gap-4' },
2007
- h(RR.NavLink, {
2008
- to: '/dashboard',
2009
- className: function(a) { return 'text-sm font-medium ' + (a.isActive ? 'text-primary-600' : 'text-gray-500 hover:text-gray-700'); },
2010
- }, 'Dashboard')
2011
- ) : null
2012
- ),
2013
-
2014
- // Auth section
2015
- h('div', { className: 'flex items-center gap-3' },
2016
- auth.isAuthenticated
2017
- ? h('div', { className: 'flex items-center gap-3' },
2018
- h('span', { className: 'text-sm text-gray-500 hidden sm:block' }, auth.user.name),
2019
- h('span', { className: 'text-xs px-2 py-0.5 rounded-full font-medium ' +
2020
- (auth.user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600')
2021
- }, auth.user.role),
2022
- h('button', {
2023
- onClick: function() { auth.logout(); },
2024
- className: 'text-sm text-gray-500 hover:text-gray-700',
2025
- }, 'Sign out')
2026
- )
2027
- : h('div', { className: 'flex items-center gap-2' },
2028
- h(RR.Link, {
2029
- to: '/login',
2030
- className: 'text-sm text-gray-600 hover:text-gray-800 px-3 py-1.5',
2031
- }, 'Sign in'),
2032
- h(RR.Link, {
2033
- to: '/register',
2034
- className: 'text-sm bg-primary-600 text-white px-3 py-1.5 rounded-lg hover:bg-primary-700',
2035
- }, 'Get started')
2036
- )
2037
- )
2038
- )
2039
- )
2040
- );
2041
- };
2042
- `;
2043
-
2044
- // ---------------------------------------------------------------------------
2045
- // public/components/ItemCard.js
2046
- // ---------------------------------------------------------------------------
2047
-
2048
- const ITEM_CARD_JS = `var h = React.createElement;
2049
-
2050
- var STATUS_STYLES = {
2051
- todo: { bg: 'bg-gray-100', text: 'text-gray-600', label: 'To Do' },
2052
- in_progress: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'In Progress' },
2053
- done: { bg: 'bg-emerald-100', text: 'text-emerald-700', label: 'Done' },
2054
- };
2055
-
2056
- var PRIORITY_STYLES = {
2057
- low: { bg: 'bg-gray-100', text: 'text-gray-500', label: 'Low' },
2058
- medium: { bg: 'bg-amber-100', text: 'text-amber-700', label: 'Medium' },
2059
- high: { bg: 'bg-red-100', text: 'text-red-700', label: 'High' },
2060
- };
2061
-
2062
- /**
2063
- * ItemCard — Displays a single item with status/priority badges and actions.
2064
- */
2065
- window.ItemCard = function ItemCard(props) {
2066
- var item = props.item;
2067
- var onEdit = props.onEdit;
2068
- var onDelete = props.onDelete;
2069
-
2070
- var s = STATUS_STYLES[item.status] || STATUS_STYLES.todo;
2071
- var p = PRIORITY_STYLES[item.priority] || PRIORITY_STYLES.medium;
2072
-
2073
- return h('div', { className: 'bg-white rounded-lg p-4 border border-gray-200 card-hover fade-in' },
2074
- // Header row
2075
- h('div', { className: 'flex items-start justify-between mb-2' },
2076
- h('h3', { className: 'font-medium text-gray-800 flex-1 mr-2' }, item.title),
2077
- h('div', { className: 'flex gap-1.5 flex-shrink-0' },
2078
- h('span', { className: 'text-xs px-2 py-0.5 rounded-full font-medium ' + s.bg + ' ' + s.text }, s.label),
2079
- h('span', { className: 'text-xs px-2 py-0.5 rounded-full font-medium ' + p.bg + ' ' + p.text }, p.label)
2080
- )
2081
- ),
2082
-
2083
- // Description
2084
- item.description
2085
- ? h('p', { className: 'text-sm text-gray-500 mb-3 line-clamp-2' }, item.description)
2086
- : null,
2087
-
2088
- // Footer
2089
- h('div', { className: 'flex items-center justify-between' },
2090
- h('span', { className: 'text-xs text-gray-400' },
2091
- (item.user_name || 'Unknown') + ' · ' + (item.created_at || '').split('T')[0]
2092
- ),
2093
- h('div', { className: 'flex gap-2' },
2094
- h('button', {
2095
- onClick: function() { onEdit(item); },
2096
- className: 'text-xs text-primary-600 hover:text-primary-700 font-medium',
2097
- }, 'Edit'),
2098
- h('button', {
2099
- onClick: function() { onDelete(item.id); },
2100
- className: 'text-xs text-red-500 hover:text-red-600 font-medium',
2101
- }, 'Delete')
2102
- )
2103
- )
2104
- );
2105
- };
2106
- `;
2107
-
2108
- // ---------------------------------------------------------------------------
2109
- // public/components/ItemForm.js
2110
- // ---------------------------------------------------------------------------
2111
-
2112
- const ITEM_FORM_JS = `var h = React.createElement;
2113
-
2114
- /**
2115
- * ItemForm — Create or edit an item.
2116
- * @param {{ item?: Object, onSave: Function, onCancel: Function }} props
2117
- */
2118
- window.ItemForm = function ItemForm(props) {
2119
- var item = props.item;
2120
- var onSave = props.onSave;
2121
- var onCancel = props.onCancel;
2122
-
2123
- var titleState = React.useState(item ? item.title : '');
2124
- var title = titleState[0]; var setTitle = titleState[1];
2125
-
2126
- var descState = React.useState(item ? (item.description || '') : '');
2127
- var desc = descState[0]; var setDesc = descState[1];
2128
-
2129
- var statusState = React.useState(item ? item.status : 'todo');
2130
- var status = statusState[0]; var setStatus = statusState[1];
2131
-
2132
- var priorityState = React.useState(item ? item.priority : 'medium');
2133
- var priority = priorityState[0]; var setPriority = priorityState[1];
2134
-
2135
- var savingState = React.useState(false);
2136
- var saving = savingState[0]; var setSaving = savingState[1];
2137
-
2138
- function handleSubmit(e) {
2139
- e.preventDefault();
2140
- if (!title.trim()) return;
2141
- setSaving(true);
2142
- onSave({
2143
- title: title.trim(),
2144
- description: desc.trim(),
2145
- status: status,
2146
- priority: priority,
2147
- }).then(function() {
2148
- setSaving(false);
2149
- }).catch(function() {
2150
- setSaving(false);
2151
- });
2152
- }
2153
-
2154
- return h('div', { className: 'bg-white rounded-lg border border-gray-200 p-5 fade-in' },
2155
- h('h3', { className: 'text-lg font-semibold text-gray-800 mb-4' },
2156
- item ? 'Edit Item' : 'New Item'
2157
- ),
2158
- h('form', { onSubmit: handleSubmit, className: 'space-y-4' },
2159
- // Title
2160
- h('div', null,
2161
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Title'),
2162
- h('input', {
2163
- type: 'text',
2164
- value: title,
2165
- onChange: function(e) { setTitle(e.target.value); },
2166
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2167
- placeholder: 'Enter item title...',
2168
- required: true,
2169
- })
2170
- ),
2171
- // Description
2172
- h('div', null,
2173
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Description'),
2174
- h('textarea', {
2175
- value: desc,
2176
- onChange: function(e) { setDesc(e.target.value); },
2177
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none',
2178
- rows: 3,
2179
- placeholder: 'Optional description...',
2180
- })
2181
- ),
2182
- // Status + Priority row
2183
- h('div', { className: 'grid grid-cols-2 gap-4' },
2184
- h('div', null,
2185
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Status'),
2186
- h('select', {
2187
- value: status,
2188
- onChange: function(e) { setStatus(e.target.value); },
2189
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500',
2190
- },
2191
- h('option', { value: 'todo' }, 'To Do'),
2192
- h('option', { value: 'in_progress' }, 'In Progress'),
2193
- h('option', { value: 'done' }, 'Done')
2194
- )
2195
- ),
2196
- h('div', null,
2197
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Priority'),
2198
- h('select', {
2199
- value: priority,
2200
- onChange: function(e) { setPriority(e.target.value); },
2201
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500',
2202
- },
2203
- h('option', { value: 'low' }, 'Low'),
2204
- h('option', { value: 'medium' }, 'Medium'),
2205
- h('option', { value: 'high' }, 'High')
2206
- )
2207
- )
2208
- ),
2209
- // Buttons
2210
- h('div', { className: 'flex justify-end gap-2 pt-2' },
2211
- h('button', {
2212
- type: 'button',
2213
- onClick: onCancel,
2214
- className: 'px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg',
2215
- }, 'Cancel'),
2216
- h('button', {
2217
- type: 'submit',
2218
- disabled: saving || !title.trim(),
2219
- className: 'px-4 py-2 text-sm text-white bg-primary-600 hover:bg-primary-700 rounded-lg disabled:opacity-50',
2220
- }, saving ? 'Saving...' : (item ? 'Update' : 'Create'))
2221
- )
2222
- )
2223
- );
2224
- };
2225
- `;
2226
-
2227
- // ---------------------------------------------------------------------------
2228
- // public/pages/WelcomePage.js
2229
- // ---------------------------------------------------------------------------
2230
-
2231
- const WELCOME_PAGE_JS = `var h = React.createElement;
2232
- var RR = ReactRouterDOM;
2233
-
2234
- /**
2235
- * WelcomePage — Landing page with hero, features, and architecture overview.
2236
- */
2237
- window.WelcomePage = function WelcomePage() {
2238
- var auth = window.useAuth();
2239
-
2240
- return h('div', { className: 'fade-in' },
2241
- // Hero
2242
- h('section', { className: 'py-16 px-4' },
2243
- h('div', { className: 'max-w-3xl mx-auto text-center' },
2244
- h('div', { className: 'inline-flex items-center gap-2 px-3 py-1 bg-primary-50 text-primary-700 rounded-full text-sm font-medium mb-6' },
2245
- h('span', { className: 'w-2 h-2 bg-emerald-400 rounded-full' }),
2246
- 'Full-Stack Application'
2247
- ),
2248
- h('h1', { className: 'text-4xl sm:text-5xl font-bold text-gray-900 mb-4 tracking-tight' },
2249
- 'Production-Ready', h('br'),
2250
- h('span', { className: 'text-primary-600' }, 'Starter Template')
2251
- ),
2252
- h('p', { className: 'text-lg text-gray-500 mb-8 max-w-xl mx-auto' },
2253
- 'Express + React + SQLite with proper separation of concerns. ' +
2254
- 'Same code structure deploys to production with Postgres.'
2255
- ),
2256
- h('div', { className: 'flex flex-col sm:flex-row gap-3 justify-center' },
2257
- auth.isAuthenticated
2258
- ? h(RR.Link, {
2259
- to: '/dashboard',
2260
- className: 'px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium',
2261
- }, 'Go to Dashboard')
2262
- : h(React.Fragment, null,
2263
- h(RR.Link, {
2264
- to: '/register',
2265
- className: 'px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium',
2266
- }, 'Get Started'),
2267
- h(RR.Link, {
2268
- to: '/login',
2269
- className: 'px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium',
2270
- }, 'Sign In')
2271
- )
2272
- )
2273
- )
2274
- ),
2275
-
2276
- // Features
2277
- h('section', { className: 'py-12 px-4 bg-white border-t border-gray-100' },
2278
- h('div', { className: 'max-w-4xl mx-auto' },
2279
- h('h2', { className: 'text-2xl font-bold text-gray-800 text-center mb-8' }, 'Architecture'),
2280
- h('div', { className: 'grid sm:grid-cols-2 lg:grid-cols-3 gap-4' },
2281
- _featureCard('SQLite (sql.js)', 'True relational database with typed schemas, migrations, and indexes. Same SQL works on Postgres.', 'DB'),
2282
- _featureCard('React Router v6', 'Client-side routing with nested routes, URL params, navigation guards, and SPA fallback.', 'RT'),
2283
- _featureCard('React Context', 'Predictable state management with useReducer. Auth context + app state context.', 'ST'),
2284
- _featureCard('JWT Auth', 'Token-based authentication with role-based access control. Login, register, protected routes.', 'AU'),
2285
- _featureCard('REST API', 'Express routes with validation, error handling, and structured JSON responses.', 'AP'),
2286
- _featureCard('Repository Pattern', 'Data access layer abstracts SQL queries. Swap database without changing routes.', 'RP')
2287
- )
2288
- )
2289
- ),
2290
-
2291
- // Demo credentials
2292
- h('section', { className: 'py-12 px-4' },
2293
- h('div', { className: 'max-w-md mx-auto text-center' },
2294
- h('h3', { className: 'text-lg font-semibold text-gray-700 mb-4' }, 'Demo Accounts'),
2295
- h('div', { className: 'bg-white rounded-lg border border-gray-200 divide-y' },
2296
- h('div', { className: 'p-4 flex items-center justify-between' },
2297
- h('div', { className: 'text-left' },
2298
- h('p', { className: 'text-sm font-medium text-gray-800' }, 'admin@example.com'),
2299
- h('p', { className: 'text-xs text-gray-400' }, 'Password: admin123')
2300
- ),
2301
- h('span', { className: 'text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full font-medium' }, 'admin')
2302
- ),
2303
- h('div', { className: 'p-4 flex items-center justify-between' },
2304
- h('div', { className: 'text-left' },
2305
- h('p', { className: 'text-sm font-medium text-gray-800' }, 'user@example.com'),
2306
- h('p', { className: 'text-xs text-gray-400' }, 'Password: user123')
2307
- ),
2308
- h('span', { className: 'text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full font-medium' }, 'user')
2309
- )
2310
- )
2311
- )
2312
- )
2313
- );
2314
- };
2315
-
2316
- function _featureCard(title, desc, icon) {
2317
- return h('div', { className: 'p-4 rounded-lg border border-gray-100 hover:border-primary-200 transition-colors' },
2318
- h('div', { className: 'w-8 h-8 bg-primary-50 rounded-lg flex items-center justify-center text-primary-600 font-bold text-xs mb-3' }, icon),
2319
- h('h3', { className: 'font-medium text-gray-800 mb-1' }, title),
2320
- h('p', { className: 'text-sm text-gray-500' }, desc)
2321
- );
2322
- }
2323
- `;
2324
-
2325
- // ---------------------------------------------------------------------------
2326
- // public/pages/LoginPage.js
2327
- // ---------------------------------------------------------------------------
2328
-
2329
- const LOGIN_PAGE_JS = `var h = React.createElement;
2330
- var RR = ReactRouterDOM;
2331
-
2332
- /**
2333
- * LoginPage — Login form with email and password.
2334
- */
2335
- window.LoginPage = function LoginPage() {
2336
- var auth = window.useAuth();
2337
- var navigate = RR.useNavigate();
2338
-
2339
- var emailState = React.useState('');
2340
- var email = emailState[0]; var setEmail = emailState[1];
2341
-
2342
- var passState = React.useState('');
2343
- var pass = passState[0]; var setPass = passState[1];
2344
-
2345
- var errorState = React.useState('');
2346
- var error = errorState[0]; var setError = errorState[1];
2347
-
2348
- var loadingState = React.useState(false);
2349
- var loading = loadingState[0]; var setLoading = loadingState[1];
2350
-
2351
- // Redirect if already logged in
2352
- React.useEffect(function() {
2353
- if (auth.isAuthenticated) navigate('/dashboard');
2354
- }, [auth.isAuthenticated]);
2355
-
2356
- function handleSubmit(e) {
2357
- e.preventDefault();
2358
- setError('');
2359
- setLoading(true);
2360
- auth.login(email, pass)
2361
- .then(function() {
2362
- navigate('/dashboard');
2363
- })
2364
- .catch(function(err) {
2365
- setError(err.message || 'Login failed');
2366
- setLoading(false);
2367
- });
2368
- }
2369
-
2370
- return h('div', { className: 'min-h-[calc(100vh-56px)] flex items-center justify-center px-4 fade-in' },
2371
- h('div', { className: 'w-full max-w-sm' },
2372
- h('div', { className: 'text-center mb-6' },
2373
- h('h1', { className: 'text-2xl font-bold text-gray-800' }, 'Welcome back'),
2374
- h('p', { className: 'text-sm text-gray-500 mt-1' }, 'Sign in to your account')
2375
- ),
2376
-
2377
- h('form', { onSubmit: handleSubmit, className: 'bg-white rounded-lg border border-gray-200 p-6 space-y-4' },
2378
- error ? h('div', { className: 'text-sm text-red-600 bg-red-50 px-3 py-2 rounded-lg' }, error) : null,
2379
-
2380
- h('div', null,
2381
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Email'),
2382
- h('input', {
2383
- type: 'email', value: email, onChange: function(e) { setEmail(e.target.value); },
2384
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2385
- placeholder: 'admin@example.com', required: true,
2386
- })
2387
- ),
2388
-
2389
- h('div', null,
2390
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Password'),
2391
- h('input', {
2392
- type: 'password', value: pass, onChange: function(e) { setPass(e.target.value); },
2393
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2394
- placeholder: 'admin123', required: true,
2395
- })
2396
- ),
2397
-
2398
- h('button', {
2399
- type: 'submit', disabled: loading,
2400
- className: 'w-full py-2.5 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 disabled:opacity-50',
2401
- }, loading ? 'Signing in...' : 'Sign in')
2402
- ),
2403
-
2404
- h('p', { className: 'text-center text-sm text-gray-500 mt-4' },
2405
- "Don't have an account? ",
2406
- h(RR.Link, { to: '/register', className: 'text-primary-600 hover:text-primary-700 font-medium' }, 'Register')
2407
- )
2408
- )
2409
- );
2410
- };
2411
- `;
2412
-
2413
- // ---------------------------------------------------------------------------
2414
- // public/pages/RegisterPage.js
2415
- // ---------------------------------------------------------------------------
2416
-
2417
- const REGISTER_PAGE_JS = `var h = React.createElement;
2418
- var RR = ReactRouterDOM;
2419
-
2420
- /**
2421
- * RegisterPage — Registration form.
2422
- */
2423
- window.RegisterPage = function RegisterPage() {
2424
- var auth = window.useAuth();
2425
- var navigate = RR.useNavigate();
2426
-
2427
- var nameState = React.useState('');
2428
- var name = nameState[0]; var setName = nameState[1];
2429
-
2430
- var emailState = React.useState('');
2431
- var email = emailState[0]; var setEmail = emailState[1];
2432
-
2433
- var passState = React.useState('');
2434
- var pass = passState[0]; var setPass = passState[1];
2435
-
2436
- var errorState = React.useState('');
2437
- var error = errorState[0]; var setError = errorState[1];
2438
-
2439
- var loadingState = React.useState(false);
2440
- var loading = loadingState[0]; var setLoading = loadingState[1];
2441
-
2442
- React.useEffect(function() {
2443
- if (auth.isAuthenticated) navigate('/dashboard');
2444
- }, [auth.isAuthenticated]);
2445
-
2446
- function handleSubmit(e) {
2447
- e.preventDefault();
2448
- if (pass.length < 6) { setError('Password must be at least 6 characters'); return; }
2449
- setError('');
2450
- setLoading(true);
2451
- auth.register(email, pass, name)
2452
- .then(function() {
2453
- navigate('/dashboard');
2454
- })
2455
- .catch(function(err) {
2456
- setError(err.message || 'Registration failed');
2457
- setLoading(false);
2458
- });
2459
- }
2460
-
2461
- return h('div', { className: 'min-h-[calc(100vh-56px)] flex items-center justify-center px-4 fade-in' },
2462
- h('div', { className: 'w-full max-w-sm' },
2463
- h('div', { className: 'text-center mb-6' },
2464
- h('h1', { className: 'text-2xl font-bold text-gray-800' }, 'Create account'),
2465
- h('p', { className: 'text-sm text-gray-500 mt-1' }, 'Start building with the starter template')
2466
- ),
2467
-
2468
- h('form', { onSubmit: handleSubmit, className: 'bg-white rounded-lg border border-gray-200 p-6 space-y-4' },
2469
- error ? h('div', { className: 'text-sm text-red-600 bg-red-50 px-3 py-2 rounded-lg' }, error) : null,
2470
-
2471
- h('div', null,
2472
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Name'),
2473
- h('input', {
2474
- type: 'text', value: name, onChange: function(e) { setName(e.target.value); },
2475
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2476
- placeholder: 'Your name', required: true,
2477
- })
2478
- ),
2479
-
2480
- h('div', null,
2481
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Email'),
2482
- h('input', {
2483
- type: 'email', value: email, onChange: function(e) { setEmail(e.target.value); },
2484
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2485
- placeholder: 'you@example.com', required: true,
2486
- })
2487
- ),
2488
-
2489
- h('div', null,
2490
- h('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, 'Password'),
2491
- h('input', {
2492
- type: 'password', value: pass, onChange: function(e) { setPass(e.target.value); },
2493
- className: 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
2494
- placeholder: 'Min 6 characters', required: true,
2495
- })
2496
- ),
2497
-
2498
- h('button', {
2499
- type: 'submit', disabled: loading,
2500
- className: 'w-full py-2.5 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 disabled:opacity-50',
2501
- }, loading ? 'Creating account...' : 'Create account')
2502
- ),
2503
-
2504
- h('p', { className: 'text-center text-sm text-gray-500 mt-4' },
2505
- 'Already have an account? ',
2506
- h(RR.Link, { to: '/login', className: 'text-primary-600 hover:text-primary-700 font-medium' }, 'Sign in')
2507
- )
2508
- )
2509
- );
2510
- };
2511
- `;
2512
-
2513
- // ---------------------------------------------------------------------------
2514
- // public/components/StatsChart.js
2515
- // ---------------------------------------------------------------------------
2516
-
2517
- const STATS_CHART_JS = `var h = React.createElement;
2518
-
2519
- /**
2520
- * StatsChart — Renders a doughnut chart of item status distribution
2521
- * using Chart.js with Ranger theme CSS variables (--chart-1 through --chart-5).
2522
- *
2523
- * Props:
2524
- * stats — { total, byStatus: { todo, in_progress, done }, byPriority: { low, medium, high } }
2525
- *
2526
- * Uses the global Chart class from chart.js UMD CDN.
2527
- */
2528
- window.StatsChart = function StatsChart(props) {
2529
- var stats = props.stats || { total: 0, byStatus: {}, byPriority: {} };
2530
- var canvasRef = React.useRef(null);
2531
- var chartRef = React.useRef(null);
2532
-
2533
- /**
2534
- * Read a CSS custom property value from :root and convert to usable color.
2535
- * Ranger stores HSL values without the hsl() wrapper (e.g. "12 76% 61%"),
2536
- * so we wrap them: hsl(12 76% 61%).
2537
- */
2538
- function getChartColor(varName, fallback) {
2539
- var raw = getComputedStyle(document.documentElement)
2540
- .getPropertyValue(varName)
2541
- .trim();
2542
- if (!raw) return fallback;
2543
- if (raw.startsWith('#') || raw.startsWith('rgb') || raw.startsWith('hsl(')) {
2544
- return raw;
2545
- }
2546
- return 'hsl(' + raw + ')';
2547
- }
2548
-
2549
- React.useEffect(function () {
2550
- if (!canvasRef.current) return;
2551
-
2552
- if (chartRef.current) {
2553
- chartRef.current.destroy();
2554
- chartRef.current = null;
2555
- }
2556
-
2557
- var todo = (stats.byStatus && stats.byStatus.todo) || 0;
2558
- var inProgress = (stats.byStatus && stats.byStatus.in_progress) || 0;
2559
- var done = (stats.byStatus && stats.byStatus.done) || 0;
2560
-
2561
- if (todo + inProgress + done === 0) return;
2562
-
2563
- var colors = [
2564
- getChartColor('--chart-1', 'hsl(12 76% 61%)'),
2565
- getChartColor('--chart-2', 'hsl(173 58% 39%)'),
2566
- getChartColor('--chart-3', 'hsl(197 37% 24%)'),
2567
- ];
2568
-
2569
- chartRef.current = new Chart(canvasRef.current, {
2570
- type: 'doughnut',
2571
- data: {
2572
- labels: ['To Do', 'In Progress', 'Done'],
2573
- datasets: [
2574
- {
2575
- data: [todo, inProgress, done],
2576
- backgroundColor: colors,
2577
- borderWidth: 0,
2578
- hoverOffset: 6,
2579
- },
2580
- ],
2581
- },
2582
- options: {
2583
- responsive: true,
2584
- maintainAspectRatio: false,
2585
- cutout: '65%',
2586
- plugins: {
2587
- legend: {
2588
- position: 'bottom',
2589
- labels: {
2590
- padding: 16,
2591
- usePointStyle: true,
2592
- pointStyleWidth: 8,
2593
- font: { size: 12 },
2594
- color: getChartColor('--text-secondary', '#565869'),
2595
- },
2596
- },
2597
- tooltip: {
2598
- backgroundColor: 'rgba(0,0,0,0.8)',
2599
- titleFont: { size: 13 },
2600
- bodyFont: { size: 12 },
2601
- padding: 10,
2602
- cornerRadius: 8,
2603
- },
2604
- },
2605
- },
2606
- });
2607
-
2608
- return function () {
2609
- if (chartRef.current) {
2610
- chartRef.current.destroy();
2611
- chartRef.current = null;
2612
- }
2613
- };
2614
- }, [stats.byStatus && stats.byStatus.todo,
2615
- stats.byStatus && stats.byStatus.in_progress,
2616
- stats.byStatus && stats.byStatus.done]);
2617
-
2618
- var hasData =
2619
- ((stats.byStatus && stats.byStatus.todo) || 0) +
2620
- ((stats.byStatus && stats.byStatus.in_progress) || 0) +
2621
- ((stats.byStatus && stats.byStatus.done) || 0) > 0;
2622
-
2623
- return h(
2624
- 'div',
2625
- { className: 'bg-white rounded-lg border border-gray-200 p-4 card-hover' },
2626
- h('h3', { className: 'text-sm font-semibold text-gray-700 mb-3' }, 'Status Distribution'),
2627
- hasData
2628
- ? h('div', { style: { height: '200px', position: 'relative' } },
2629
- h('canvas', { ref: canvasRef })
2630
- )
2631
- : h('div', { className: 'flex items-center justify-center h-48 text-gray-400 text-sm' },
2632
- 'No data to display'
2633
- )
2634
- );
2635
- };
2636
- `;
2637
-
2638
- // ---------------------------------------------------------------------------
2639
- // public/pages/DashboardPage.js
2640
- // ---------------------------------------------------------------------------
2641
-
2642
- const DASHBOARD_PAGE_JS = `var h = React.createElement;
2643
- var RR = ReactRouterDOM;
2644
-
2645
- /**
2646
- * DashboardPage — Main application view with stats, filters, and item CRUD.
2647
- */
2648
- window.DashboardPage = function DashboardPage() {
2649
- var auth = window.useAuth();
2650
- var app = window.useApp();
2651
- var navigate = RR.useNavigate();
2652
-
2653
- var showFormState = React.useState(false);
2654
- var showForm = showFormState[0]; var setShowForm = showFormState[1];
2655
-
2656
- var editingState = React.useState(null);
2657
- var editing = editingState[0]; var setEditing = editingState[1];
2658
-
2659
- // Redirect if not authenticated
2660
- React.useEffect(function() {
2661
- if (!auth.loading && !auth.isAuthenticated) navigate('/login');
2662
- }, [auth.loading, auth.isAuthenticated]);
2663
-
2664
- // Fetch data on mount
2665
- React.useEffect(function() {
2666
- if (auth.isAuthenticated) {
2667
- app.fetchItems();
2668
- app.fetchStats();
2669
- }
2670
- }, [auth.isAuthenticated]);
2671
-
2672
- // Re-fetch when filters change
2673
- React.useEffect(function() {
2674
- if (auth.isAuthenticated) {
2675
- app.fetchItems(app.filters);
2676
- }
2677
- }, [app.filters.status, app.filters.priority]);
2678
-
2679
- function handleSave(data) {
2680
- if (editing) {
2681
- return app.updateItem(editing.id, data).then(function() {
2682
- setEditing(null);
2683
- setShowForm(false);
2684
- });
2685
- }
2686
- return app.createItem(data).then(function() {
2687
- setShowForm(false);
2688
- });
2689
- }
2690
-
2691
- function handleEdit(item) {
2692
- setEditing(item);
2693
- setShowForm(true);
2694
- }
2695
-
2696
- function handleDelete(id) {
2697
- app.deleteItem(id);
2698
- }
2699
-
2700
- function handleCancel() {
2701
- setEditing(null);
2702
- setShowForm(false);
2703
- }
2704
-
2705
- if (auth.loading) {
2706
- return h('div', { className: 'flex items-center justify-center py-20' },
2707
- h('div', { className: 'w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full animate-spin' })
2708
- );
2709
- }
2710
-
2711
- var stats = app.stats || { total: 0, byStatus: {}, byPriority: {} };
2712
-
2713
- return h('div', { className: 'max-w-5xl mx-auto px-4 sm:px-6 py-6 fade-in' },
2714
- // Header
2715
- h('div', { className: 'flex items-center justify-between mb-6' },
2716
- h('div', null,
2717
- h('h1', { className: 'text-2xl font-bold text-gray-800' }, 'Dashboard'),
2718
- h('p', { className: 'text-sm text-gray-500' }, 'Manage your items')
2719
- ),
2720
- h('button', {
2721
- onClick: function() { setEditing(null); setShowForm(true); },
2722
- className: 'px-4 py-2 bg-primary-600 text-white text-sm rounded-lg hover:bg-primary-700 font-medium',
2723
- }, '+ New Item')
2724
- ),
2725
-
2726
- // Stats cards
2727
- h('div', { className: 'grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6' },
2728
- _statCard('Total', stats.total, 'bg-gray-50'),
2729
- _statCard('To Do', stats.byStatus.todo || 0, 'bg-gray-50'),
2730
- _statCard('In Progress', stats.byStatus.in_progress || 0, 'bg-blue-50'),
2731
- _statCard('Done', stats.byStatus.done || 0, 'bg-emerald-50')
2732
- ),
2733
-
2734
- // Chart — doughnut showing status distribution
2735
- h('div', { className: 'mb-6' },
2736
- h(window.StatsChart, { stats: stats })
2737
- ),
2738
-
2739
- // Filters
2740
- h('div', { className: 'flex gap-2 mb-4' },
2741
- _filterSelect('Status', app.filters.status, ['todo', 'in_progress', 'done'],
2742
- function(v) { app.setFilter({ status: v }); }),
2743
- _filterSelect('Priority', app.filters.priority, ['low', 'medium', 'high'],
2744
- function(v) { app.setFilter({ priority: v }); })
2745
- ),
2746
-
2747
- // Form (show/hide)
2748
- showForm
2749
- ? h(window.ItemForm, { item: editing, onSave: handleSave, onCancel: handleCancel })
2750
- : null,
2751
-
2752
- // Items list
2753
- app.loading
2754
- ? h('div', { className: 'text-center py-12' },
2755
- h('div', { className: 'w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto mb-2' }),
2756
- h('p', { className: 'text-sm text-gray-400' }, 'Loading...')
2757
- )
2758
- : app.items.length === 0
2759
- ? h('div', { className: 'text-center py-12 bg-white rounded-lg border border-gray-200' },
2760
- h('p', { className: 'text-gray-400 mb-2' }, 'No items yet'),
2761
- h('button', {
2762
- onClick: function() { setShowForm(true); },
2763
- className: 'text-sm text-primary-600 hover:text-primary-700 font-medium',
2764
- }, 'Create your first item')
2765
- )
2766
- : h('div', { className: 'grid gap-3 ' + (showForm ? 'mt-4' : '') },
2767
- app.items.map(function(item) {
2768
- return h(window.ItemCard, {
2769
- key: item.id,
2770
- item: item,
2771
- onEdit: handleEdit,
2772
- onDelete: handleDelete,
2773
- });
2774
- })
2775
- )
2776
- );
2777
- };
2778
-
2779
- function _statCard(label, value, bg) {
2780
- return h('div', { className: 'rounded-lg p-3 ' + bg },
2781
- h('p', { className: 'text-xs text-gray-500 font-medium' }, label),
2782
- h('p', { className: 'text-xl font-bold text-gray-800' }, value)
2783
- );
2784
- }
2785
-
2786
- function _filterSelect(label, value, options, onChange) {
2787
- return h('select', {
2788
- value: value,
2789
- onChange: function(e) { onChange(e.target.value); },
2790
- className: 'px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500',
2791
- },
2792
- h('option', { value: '' }, 'All ' + label),
2793
- options.map(function(opt) {
2794
- var label = opt.replace('_', ' ');
2795
- label = label.charAt(0).toUpperCase() + label.slice(1);
2796
- return h('option', { key: opt, value: opt }, label);
2797
- })
2798
- );
2799
- }
2800
- `;
2801
-
2802
- // ---------------------------------------------------------------------------
2803
- // public/App.js — Root component with providers and router
2804
- // ---------------------------------------------------------------------------
2805
-
2806
- const APP_JS = `var h = React.createElement;
2807
- var RR = ReactRouterDOM;
2808
-
2809
- /**
2810
- * Detect the basename for React Router.
2811
- *
2812
- * When running inside a Nodepod preview iframe, the URL path is
2813
- * something like "/__preview__/3000/" — React Router needs to
2814
- * know this prefix so it can match routes correctly. In production
2815
- * (real server), the pathname is just "/" and basename is empty.
2816
- */
2817
- var BASENAME = (function() {
2818
- var m = window.location.pathname.match(/^(\\/__.+?__\\/\\d+)/);
2819
- return m ? m[1] : '';
2820
- })();
2821
-
2822
- /**
2823
- * ProtectedRoute — Redirects to /login if not authenticated.
2824
- */
2825
- function ProtectedRoute(props) {
2826
- var auth = window.useAuth();
2827
-
2828
- if (auth.loading) {
2829
- return h('div', { className: 'flex items-center justify-center min-h-[calc(100vh-56px)]' },
2830
- h('div', { className: 'w-6 h-6 border-2 border-primary-500 border-t-transparent rounded-full animate-spin' })
2831
- );
2832
- }
2833
-
2834
- if (!auth.isAuthenticated) {
2835
- return h(RR.Navigate, { to: '/login', replace: true });
2836
- }
2837
-
2838
- return props.children || h(RR.Outlet);
2839
- }
2840
-
2841
- /**
2842
- * Layout — App shell with navbar.
2843
- */
2844
- function Layout() {
2845
- var app = window.useApp();
2846
-
2847
- return h(React.Fragment, null,
2848
- h(window.Navbar),
2849
- h(window.Toast, { toasts: app.toasts }),
2850
- h('main', null, h(RR.Outlet))
2851
- );
2852
- }
2853
-
2854
- /**
2855
- * App — Root component. Sets up providers and router.
2856
- */
2857
- function App() {
2858
- return h(window.AuthProvider, null,
2859
- h(window.AppProvider, null,
2860
- h(RR.BrowserRouter, { basename: BASENAME },
2861
- h(RR.Routes, null,
2862
- h(RR.Route, { element: h(Layout) },
2863
- // Public routes
2864
- h(RR.Route, { index: true, element: h(window.WelcomePage) }),
2865
- h(RR.Route, { path: 'login', element: h(window.LoginPage) }),
2866
- h(RR.Route, { path: 'register', element: h(window.RegisterPage) }),
2867
- // Protected routes
2868
- h(RR.Route, { element: h(ProtectedRoute) },
2869
- h(RR.Route, { path: 'dashboard', element: h(window.DashboardPage) })
2870
- ),
2871
- // 404
2872
- h(RR.Route, { path: '*', element: h('div', { className: 'text-center py-20' },
2873
- h('h1', { className: 'text-4xl font-bold text-gray-300 mb-2' }, '404'),
2874
- h('p', { className: 'text-gray-400 mb-4' }, 'Page not found'),
2875
- h(RR.Link, { to: '/', className: 'text-primary-600 hover:text-primary-700 text-sm font-medium' }, 'Go home')
2876
- )})
2877
- )
2878
- )
2879
- )
2880
- )
2881
- );
2882
- }
2883
-
2884
- // Mount
2885
- ReactDOM.createRoot(document.getElementById('root')).render(h(App));
2886
- `;
2887
-
2888
- // ---------------------------------------------------------------------------
2889
- // Dockerfile — Multi-stage production build
2890
- // ---------------------------------------------------------------------------
2891
-
2892
- const DOCKERFILE = `# ===========================================================================
2893
- # Dockerfile — Production-ready multi-stage build
2894
- #
2895
- # Supports two modes:
2896
- # 1. SQLite (default) — zero external dependencies, great for POC/MVP
2897
- # 2. Postgres — set DATABASE_URL to switch (no code changes needed)
2898
- #
2899
- # Build:
2900
- # docker build -t starter-app .
2901
- #
2902
- # Run (SQLite — data persists via mounted volume):
2903
- # docker run -p 3000:3000 \\
2904
- # -v app-data:/app/data \\
2905
- # -e JWT_SECRET=your-secret-here \\
2906
- # starter-app
2907
- #
2908
- # Run (Postgres):
2909
- # docker run -p 3000:3000 \\
2910
- # -e DATABASE_URL=postgresql://user:pass@host:5432/dbname \\
2911
- # -e JWT_SECRET=your-secret-here \\
2912
- # starter-app
2913
- # ===========================================================================
2914
-
2915
- # --- Stage 1: Dependencies ---------------------------------------------------
2916
- FROM node:20-alpine AS deps
2917
-
2918
- WORKDIR /app
2919
-
2920
- COPY package.json package-lock.json* ./
2921
- RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev
2922
-
2923
- # --- Stage 2: Production image -----------------------------------------------
2924
- FROM node:20-alpine AS runtime
2925
-
2926
- # Security: run as non-root
2927
- RUN addgroup -S app && adduser -S app -G app
2928
-
2929
- WORKDIR /app
2930
-
2931
- # Copy dependencies from builder stage
2932
- COPY --from=deps /app/node_modules ./node_modules
2933
-
2934
- # Copy application source
2935
- COPY . .
2936
-
2937
- # Remove files not needed in production
2938
- RUN rm -f Dockerfile .dockerignore docker-compose.yml CONTRIBUTING.md
2939
-
2940
- # Create data directory for SQLite persistence (mounted as volume)
2941
- RUN mkdir -p /app/data && chown -R app:app /app/data
2942
-
2943
- # Switch to non-root user
2944
- USER app
2945
-
2946
- # Environment defaults — override at runtime via -e or .env
2947
- ENV NODE_ENV=production \\
2948
- PORT=3000 \\
2949
- DATABASE_URL=sqlite:/app/data/app.db \\
2950
- JWT_SECRET=change-me-in-production
2951
-
2952
- EXPOSE 3000
2953
-
2954
- # Health check — ensures container is marked healthy in orchestrators
2955
- HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
2956
- CMD wget -qO- http://localhost:3000/api/health || exit 1
2957
-
2958
- CMD ["node", "server.js"]
2959
- `;
2960
-
2961
- // ---------------------------------------------------------------------------
2962
- // .dockerignore
2963
- // ---------------------------------------------------------------------------
2964
-
2965
- const DOCKERIGNORE = `node_modules
2966
- npm-debug.log*
2967
- .env
2968
- .env.local
2969
- .env.*.local
2970
- .git
2971
- .gitignore
2972
- .DS_Store
2973
- *.md
2974
- !README.md
2975
- `;
2976
-
2977
- // ---------------------------------------------------------------------------
2978
- // docker-compose.yml — Local dev with optional Postgres
2979
- // ---------------------------------------------------------------------------
2980
-
2981
- const DOCKER_COMPOSE = `# docker-compose.yml — Local development and production deployment
2982
- #
2983
- # Usage:
2984
- # SQLite (default): docker compose up
2985
- # With Postgres: docker compose --profile postgres up
2986
- #
2987
- # The app works identically with both — swap DATABASE_URL and restart.
2988
-
2989
- services:
2990
- app:
2991
- build: .
2992
- ports:
2993
- - "\${PORT:-3000}:3000"
2994
- environment:
2995
- - NODE_ENV=\${NODE_ENV:-production}
2996
- - PORT=3000
2997
- - JWT_SECRET=\${JWT_SECRET:-change-me-in-production}
2998
- - JWT_EXPIRES_IN=\${JWT_EXPIRES_IN:-24h}
2999
- - CORS_ORIGIN=\${CORS_ORIGIN:-*}
3000
- - DATABASE_URL=\${DATABASE_URL:-sqlite:/app/data/app.db}
3001
- volumes:
3002
- - app-data:/app/data
3003
- restart: unless-stopped
3004
- healthcheck:
3005
- test: wget -qO- http://localhost:3000/api/health || exit 1
3006
- interval: 30s
3007
- timeout: 5s
3008
- start_period: 10s
3009
- retries: 3
3010
-
3011
- # --- Optional: Postgres (activate with --profile postgres) ----------------
3012
- postgres:
3013
- image: postgres:16-alpine
3014
- profiles: [postgres]
3015
- environment:
3016
- POSTGRES_DB: starter_app
3017
- POSTGRES_USER: app
3018
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
3019
- ports:
3020
- - "5432:5432"
3021
- volumes:
3022
- - pg-data:/var/lib/postgresql/data
3023
- healthcheck:
3024
- test: pg_isready -U app -d starter_app
3025
- interval: 10s
3026
- timeout: 5s
3027
- retries: 5
3028
-
3029
- volumes:
3030
- app-data:
3031
- pg-data:
3032
- `;
3033
-
3034
- // ---------------------------------------------------------------------------
3035
- // README.md
3036
- // ---------------------------------------------------------------------------
3037
-
3038
- const README_MD = `# Starter App
3039
-
3040
- A production-ready full-stack application built with **Express.js** and **React**.
3041
-
3042
- Designed as a proper starting point — not a toy demo. The architecture, patterns,
3043
- and code quality are what you'd ship to production. Start building features
3044
- immediately, deploy when ready.
3045
-
3046
- ## Stack
3047
-
3048
- | Layer | Technology | Notes |
3049
- |--------------|-----------------------------------------|------------------------------------|
3050
- | **Server** | Express.js 4 | Middleware chain, route modules |
3051
- | **Database** | SQLite (sql.js) / Postgres | Swap via env var, zero code changes |
3052
- | **Auth** | JWT + role-based access control | Register, login, protected routes |
3053
- | **Frontend** | React 18 + React Router v6 | SPA with client-side routing |
3054
- | **State** | React Context + useReducer | AuthContext + AppContext |
3055
- | **Styling** | Tailwind CSS (CDN) | Utility-first, responsive |
3056
-
3057
- ## Quick Start
3058
-
3059
- \`\`\`bash
3060
- # Install dependencies
3061
- npm install
3062
-
3063
- # Start development server
3064
- node server.js
3065
-
3066
- # Open http://localhost:3000
3067
- \`\`\`
3068
-
3069
- **Demo accounts** (seeded automatically):
3070
- - \`admin@example.com\` / \`admin123\` (admin role)
3071
- - \`user@example.com\` / \`user123\` (user role)
3072
-
3073
- ## Project Structure
3074
-
3075
- \`\`\`
3076
- ├── server.js # Entry point — middleware chain + startup
3077
- ├── config.js # Centralized env var config
3078
- ├── package.json # Dependencies
3079
- ├── .env.example # Environment variable documentation
3080
- ├── .ranger # AI agent context (project manifest)
3081
-
3082
- ├── db/
3083
- │ ├── database.js # Database init, migrations, query helpers
3084
- │ ├── migrations/
3085
- │ │ └── 001_initial.js # Schema: users, items tables + seed data
3086
- │ └── repositories/
3087
- │ ├── userRepository.js # User CRUD (parameterized SQL)
3088
- │ └── itemRepository.js # Item CRUD with JOINs, filters, stats
3089
-
3090
- ├── middleware/
3091
- │ ├── auth.js # JWT generation, verification, role checks
3092
- │ ├── errorHandler.js # Global error handler + 404 / SPA fallback
3093
- │ ├── staticFiles.js # Static file serving
3094
- │ └── validate.js # Request body validation helpers
3095
-
3096
- ├── routes/
3097
- │ ├── auth.js # POST /api/auth/register, login, GET /me
3098
- │ └── items.js # CRUD /api/items + GET /api/items/stats
3099
-
3100
- ├── public/ # React SPA (served as static files)
3101
- │ ├── index.html # HTML shell (loads React + Router from CDN)
3102
- │ ├── styles.css # Custom animations
3103
- │ ├── lib/api.js # API client with auth token handling
3104
- │ ├── store/
3105
- │ │ ├── AuthContext.js # Auth state (login/register/logout)
3106
- │ │ └── AppContext.js # App state (items CRUD, stats, filters)
3107
- │ ├── components/
3108
- │ │ ├── Navbar.js # Top nav with auth status
3109
- │ │ ├── Toast.js # Notification toasts
3110
- │ │ ├── ItemCard.js # Item display card
3111
- │ │ └── ItemForm.js # Item create/edit form
3112
- │ ├── pages/
3113
- │ │ ├── WelcomePage.js # Landing page
3114
- │ │ ├── LoginPage.js # Login form
3115
- │ │ ├── RegisterPage.js # Registration form
3116
- │ │ └── DashboardPage.js # Dashboard with stats + item list
3117
- │ └── App.js # Root — providers, router, routes
3118
-
3119
- ├── Dockerfile # Multi-stage production build
3120
- ├── docker-compose.yml # Local dev + optional Postgres
3121
- ├── .dockerignore # Docker build exclusions
3122
- ├── README.md # This file
3123
- └── CONTRIBUTING.md # Contribution guidelines
3124
- \`\`\`
3125
-
3126
- ## API Reference
3127
-
3128
- ### Authentication
3129
-
3130
- | Method | Endpoint | Auth | Description |
3131
- |--------|----------------------|----------|------------------------|
3132
- | POST | /api/auth/register | Public | Create account |
3133
- | POST | /api/auth/login | Public | Login, returns JWT |
3134
- | GET | /api/auth/me | Required | Get current user |
3135
-
3136
- ### Items
3137
-
3138
- | Method | Endpoint | Auth | Description |
3139
- |--------|----------------------|----------|------------------------|
3140
- | GET | /api/items | Required | List items (filterable)|
3141
- | POST | /api/items | Required | Create item |
3142
- | GET | /api/items/stats | Required | Dashboard statistics |
3143
- | GET | /api/items/:id | Required | Get single item |
3144
- | PUT | /api/items/:id | Required | Update item |
3145
- | DELETE | /api/items/:id | Required | Delete item (owner/admin) |
3146
-
3147
- **Query parameters** for GET /api/items:
3148
- - \`status\` — Filter by status (todo, in_progress, done)
3149
- - \`priority\` — Filter by priority (low, medium, high)
3150
-
3151
- ## Deployment
3152
-
3153
- ### Docker (Recommended)
3154
-
3155
- \`\`\`bash
3156
- # Build the image
3157
- docker build -t starter-app .
3158
-
3159
- # Run with SQLite (data persists in a Docker volume)
3160
- docker run -d -p 3000:3000 \\
3161
- -v app-data:/app/data \\
3162
- -e JWT_SECRET=your-secret-here \\
3163
- starter-app
3164
-
3165
- # Run with Postgres
3166
- docker run -d -p 3000:3000 \\
3167
- -e DATABASE_URL=postgresql://user:pass@host:5432/dbname \\
3168
- -e JWT_SECRET=your-secret-here \\
3169
- starter-app
3170
- \`\`\`
3171
-
3172
- ### Docker Compose
3173
-
3174
- \`\`\`bash
3175
- # SQLite mode (default)
3176
- docker compose up -d
3177
-
3178
- # With Postgres
3179
- docker compose --profile postgres up -d
3180
- \`\`\`
3181
-
3182
- ### Cloud Platforms
3183
-
3184
- Works out of the box on any platform that supports Docker or Node.js:
3185
-
3186
- | Platform | Deploy Command / Notes |
3187
- |----------------------|-----------------------------------------------------------|
3188
- | **Railway** | Connect repo, auto-detects Dockerfile |
3189
- | **Render** | New Web Service, Docker, set env vars |
3190
- | **Fly.io** | \`fly launch\`, creates fly.toml automatically |
3191
- | **AWS ECS** | Push image to ECR, create task definition |
3192
- | **Google Cloud Run** | \`gcloud run deploy --source .\` |
3193
- | **DigitalOcean** | App Platform, auto-detect Dockerfile |
3194
- | **Heroku** | \`heroku container:push web\` then \`heroku container:release web\` |
3195
-
3196
- ### Manual Deployment (No Docker)
3197
-
3198
- \`\`\`bash
3199
- npm install --omit=dev
3200
- NODE_ENV=production JWT_SECRET=your-secret node server.js
3201
- \`\`\`
3202
-
3203
- ## Database: SQLite vs Postgres
3204
-
3205
- This app runs on **SQLite by default** — zero setup, great for POC/MVP/demos.
3206
- When you need multi-server or production scale, switch to Postgres with **zero
3207
- code changes** in your route/repository layer.
3208
-
3209
- ### When to Use SQLite (Default)
3210
-
3211
- - POC, MVP, demos, hackathons
3212
- - Single-server deployments (< 1000 concurrent users)
3213
- - Embedded applications, edge deployments
3214
- - You want zero infrastructure dependencies
3215
-
3216
- ### When to Switch to Postgres
3217
-
3218
- - Multiple application servers (horizontal scaling)
3219
- - Need concurrent writes from many users
3220
- - Want managed backups, point-in-time recovery
3221
- - Compliance requirements (SOC2, HIPAA)
3222
-
3223
- ### How to Migrate
3224
-
3225
- 1. **Install the Postgres driver:**
3226
- \`\`\`bash
3227
- npm install pg
3228
- \`\`\`
3229
-
3230
- 2. **Replace \`db/database.js\`** with a Postgres version:
3231
- \`\`\`javascript
3232
- const { Pool } = require('pg');
3233
- const pool = new Pool({ connectionString: process.env.DATABASE_URL });
3234
-
3235
- module.exports = {
3236
- initialize: async () => { await runMigrations(); },
3237
- run: (sql, params) => pool.query(sql, params),
3238
- get: async (sql, params) => { const r = await pool.query(sql, params); return r.rows[0]; },
3239
- all: async (sql, params) => { const r = await pool.query(sql, params); return r.rows; },
3240
- exec: (sql) => pool.query(sql),
3241
- };
3242
- \`\`\`
3243
-
3244
- 3. **Set the environment variable:**
3245
- \`\`\`bash
3246
- DATABASE_URL=postgresql://user:pass@host:5432/dbname
3247
- \`\`\`
3248
-
3249
- 4. **Run migrations** — they execute automatically on startup. The SQL is
3250
- standard and works on both SQLite and Postgres.
3251
-
3252
- > The repository layer (\`db/repositories/*.js\`) uses parameterized SQL.
3253
- > For Postgres, change \`?\` placeholders to \`$1, $2\` style in the adapter.
3254
-
3255
- ## Environment Variables
3256
-
3257
- | Variable | Default | Description |
3258
- |----------------|----------------------------|------------------------------------|
3259
- | PORT | 3000 | Server listen port |
3260
- | NODE_ENV | development | Environment (development/production)|
3261
- | DATABASE_URL | sqlite::memory: | Database connection string |
3262
- | JWT_SECRET | sandbox-secret-key | JWT signing secret (**change this**) |
3263
- | JWT_EXPIRES_IN | 24h | Token expiry duration |
3264
- | CORS_ORIGIN | * | Allowed CORS origins |
3265
-
3266
- ## License
3267
-
3268
- MIT
3269
- `;
3270
-
3271
- // ---------------------------------------------------------------------------
3272
- // CONTRIBUTING.md
3273
- // ---------------------------------------------------------------------------
3274
-
3275
- const CONTRIBUTING_MD = `# Contributing
3276
-
3277
- Thank you for considering contributing to this project.
3278
-
3279
- ## Development Setup
3280
-
3281
- \`\`\`bash
3282
- # Clone the repository
3283
- git clone <repo-url>
3284
- cd starter-app
3285
-
3286
- # Install dependencies
3287
- npm install
3288
-
3289
- # Start the development server
3290
- node server.js
3291
- \`\`\`
3292
-
3293
- The server starts on http://localhost:3000 with hot-reloadable frontend files.
3294
-
3295
- ## Project Architecture
3296
-
3297
- This is a **monolith with clean separation of concerns**:
3298
-
3299
- \`\`\`
3300
- Request -> Express middleware chain -> Route handler -> Repository -> Database
3301
- |
3302
- Response <- JSON / HTML <- Route handler <- Repository result
3303
- \`\`\`
3304
-
3305
- ### Key Conventions
3306
-
3307
- 1. **Database access goes through repositories** — never write SQL directly in
3308
- routes. Add methods to \`db/repositories/*.js\`.
3309
-
3310
- 2. **Validation happens in middleware** — use \`middleware/validate.js\` helpers
3311
- or add new ones. Routes assume valid data.
3312
-
3313
- 3. **Auth is JWT-based** — \`middleware/auth.js\` handles token generation,
3314
- verification, and role checks. All \`/api/*\` routes (except auth) require
3315
- a valid token.
3316
-
3317
- 4. **Frontend is a static SPA** — React components live in \`public/\`. State
3318
- is managed via React Context providers in \`public/store/\`.
3319
-
3320
- 5. **Migrations are numbered** — add new files as \`db/migrations/002_*.js\`,
3321
- \`003_*.js\`, etc. They run automatically on startup.
3322
-
3323
- ## Adding a New Resource
3324
-
3325
- Example: adding a "comments" feature.
3326
-
3327
- ### 1. Database Migration
3328
-
3329
- Create \`db/migrations/002_comments.js\`:
3330
- \`\`\`javascript
3331
- module.exports = [
3332
- {
3333
- name: '005_create_comments',
3334
- up: \`
3335
- CREATE TABLE IF NOT EXISTS comments (
3336
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3337
- body TEXT NOT NULL,
3338
- item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
3339
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
3340
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
3341
- )
3342
- \`,
3343
- },
3344
- ];
3345
- \`\`\`
3346
-
3347
- ### 2. Repository
3348
-
3349
- Create \`db/repositories/commentRepository.js\`:
3350
- \`\`\`javascript
3351
- var db = require('../database');
3352
-
3353
- function findByItemId(itemId) {
3354
- return db.all(
3355
- 'SELECT c.*, u.name as author FROM comments c JOIN users u ON c.user_id = u.id WHERE c.item_id = ? ORDER BY c.created_at DESC',
3356
- [itemId]
3357
- );
3358
- }
3359
-
3360
- function create(body, itemId, userId) {
3361
- return db.run(
3362
- 'INSERT INTO comments (body, item_id, user_id) VALUES (?, ?, ?)',
3363
- [body, itemId, userId]
3364
- );
3365
- }
3366
-
3367
- module.exports = { findByItemId, create };
3368
- \`\`\`
3369
-
3370
- ### 3. Route
3371
-
3372
- Create \`routes/comments.js\`:
3373
- \`\`\`javascript
3374
- var express = require('express');
3375
- var router = express.Router();
3376
- var auth = require('../middleware/auth');
3377
- var comments = require('../db/repositories/commentRepository');
3378
-
3379
- router.get('/items/:itemId/comments', auth.authenticate, function(req, res) {
3380
- var rows = comments.findByItemId(req.params.itemId);
3381
- res.json(rows);
3382
- });
3383
-
3384
- router.post('/items/:itemId/comments', auth.authenticate, function(req, res) {
3385
- var result = comments.create(req.body.body, req.params.itemId, req.user.id);
3386
- res.status(201).json({ id: result.lastInsertRowId });
3387
- });
3388
-
3389
- module.exports = router;
3390
- \`\`\`
3391
-
3392
- ### 4. Mount the route in \`server.js\`:
3393
- \`\`\`javascript
3394
- var commentRoutes = require('./routes/comments');
3395
- app.use('/api', commentRoutes);
3396
- \`\`\`
3397
-
3398
- ### 5. Update \`.ranger\`
3399
-
3400
- Add the new table schema, routes, and files to the \`.ranger\` manifest so the
3401
- AI agent knows about your changes.
3402
-
3403
- ## Code Style
3404
-
3405
- - **JavaScript** (CommonJS) — no build step required
3406
- - **Semicolons** — yes
3407
- - **Quotes** — single quotes for strings
3408
- - **Indentation** — 2 spaces
3409
- - **Comments** — JSDoc on exported functions, inline comments for non-obvious logic
3410
- - **Error handling** — always pass errors to \`next()\` in Express middleware
3411
-
3412
- ## Testing
3413
-
3414
- \`\`\`bash
3415
- # Run all tests
3416
- npm test
3417
-
3418
- # Run with coverage
3419
- npm run test:coverage
3420
- \`\`\`
3421
-
3422
- When adding new features:
3423
- 1. Add unit tests for repository methods
3424
- 2. Add integration tests for API endpoints
3425
- 3. Test both SQLite and Postgres if applicable
3426
-
3427
- ## Pull Request Checklist
3428
-
3429
- - [ ] Code follows the project conventions above
3430
- - [ ] New database tables have a migration file
3431
- - [ ] New routes are documented in README.md API Reference
3432
- - [ ] New files are listed in \`.ranger\` manifest
3433
- - [ ] Tests pass locally
3434
- - [ ] No hardcoded secrets or credentials
3435
- `;
3436
-
3437
- // ---------------------------------------------------------------------------
3438
- // package.json
3439
- // ---------------------------------------------------------------------------
3440
-
3441
- const PACKAGE_JSON = JSON.stringify(
3442
- {
3443
- name: "starter-app",
3444
- version: "1.0.0",
3445
- description: "Full-stack Express + React starter with SQLite (sql.js)",
3446
- dependencies: {
3447
- express: "^4.18.0",
3448
- "sql.js": "^1.11.0",
3449
- },
3450
- // PRODUCTION: Add these dependencies:
3451
- // "pg": "^8.11.0" or "mongoose": "^7.0.0",
3452
- // "jsonwebtoken": "^9.0.0",
3453
- // "bcryptjs": "^2.4.3",
3454
- // "dotenv": "^16.0.0",
3455
- // "cors": "^2.8.5",
3456
- // "helmet": "^7.0.0",
3457
- // "express-rate-limit": "^7.0.0"
3458
- },
3459
- null,
3460
- 2,
3461
- );
3462
-
3463
- // ---------------------------------------------------------------------------
3464
- // Export
3465
- // ---------------------------------------------------------------------------
3466
-
3467
- export const fullstackStarterTemplate = {
3468
- entryCommand: "node server.js",
3469
- port: 3000,
3470
- files: {
3471
- ".ranger": RANGER_MANIFEST,
3472
- "package.json": PACKAGE_JSON,
3473
- ".env.example": ENV_EXAMPLE,
3474
- "config.js": CONFIG_JS,
3475
- "server.js": SERVER_JS,
3476
- Dockerfile: DOCKERFILE,
3477
- ".dockerignore": DOCKERIGNORE,
3478
- "docker-compose.yml": DOCKER_COMPOSE,
3479
- "README.md": README_MD,
3480
- "CONTRIBUTING.md": CONTRIBUTING_MD,
3481
- "db/database.js": DATABASE_JS,
3482
- "db/migrations/001_initial.js": MIGRATION_001,
3483
- "db/repositories/userRepository.js": USER_REPOSITORY_JS,
3484
- "db/repositories/itemRepository.js": ITEM_REPOSITORY_JS,
3485
- "middleware/auth.js": AUTH_JS,
3486
- "middleware/errorHandler.js": ERROR_HANDLER_JS,
3487
- "middleware/staticFiles.js": STATIC_FILES_JS,
3488
- "middleware/validate.js": VALIDATE_JS,
3489
- "routes/auth.js": ROUTES_AUTH_JS,
3490
- "routes/items.js": ROUTES_ITEMS_JS,
3491
- "public/index.html": INDEX_HTML,
3492
- "public/styles.css": STYLES_CSS,
3493
- "public/lib/api.js": API_JS,
3494
- "public/store/AuthContext.js": AUTH_CONTEXT_JS,
3495
- "public/store/AppContext.js": APP_CONTEXT_JS,
3496
- "public/components/Toast.js": TOAST_JS,
3497
- "public/components/Navbar.js": NAVBAR_JS,
3498
- "public/components/ItemCard.js": ITEM_CARD_JS,
3499
- "public/components/ItemForm.js": ITEM_FORM_JS,
3500
- "public/components/StatsChart.js": STATS_CHART_JS,
3501
- "public/pages/WelcomePage.js": WELCOME_PAGE_JS,
3502
- "public/pages/LoginPage.js": LOGIN_PAGE_JS,
3503
- "public/pages/RegisterPage.js": REGISTER_PAGE_JS,
3504
- "public/pages/DashboardPage.js": DASHBOARD_PAGE_JS,
3505
- "public/App.js": APP_JS,
3506
- } as Record<string, string>,
3507
- };