- server/db.ts: sql.js with migration system (hex_maps, hexes, hex_features) - server/routes/maps.ts: CRUD for hex maps - server/routes/hexes.ts: Bulk hex upsert, region load, sparse storage - server/index.ts: Express 5, CORS, tile serving, SPA fallback - src/data/api-client.ts: Frontend HTTP client for all API endpoints - src/main.ts: Auto-save with 1s debounce, load map state on startup - Port 3002 (Kiepenkerl uses 3001) - Graceful fallback when API unavailable (works without server too) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
import { Router } from 'express';
|
|
import { getDb, saveDb } from '../db.js';
|
|
|
|
const router = Router();
|
|
|
|
// List all maps
|
|
router.get('/', (_req, res) => {
|
|
const db = getDb();
|
|
const rows = db.exec('SELECT * FROM hex_maps ORDER BY updated_at DESC');
|
|
if (rows.length === 0) {
|
|
res.json([]);
|
|
return;
|
|
}
|
|
const maps = rows[0].values.map(row => rowToMap(rows[0].columns, row));
|
|
res.json(maps);
|
|
});
|
|
|
|
// Get single map
|
|
router.get('/:id', (req, res) => {
|
|
const db = getDb();
|
|
const rows = db.exec('SELECT * FROM hex_maps WHERE id = ?', [req.params.id]);
|
|
if (rows.length === 0 || rows[0].values.length === 0) {
|
|
res.status(404).json({ error: 'Map not found' });
|
|
return;
|
|
}
|
|
res.json(rowToMap(rows[0].columns, rows[0].values[0]));
|
|
});
|
|
|
|
// Create map
|
|
router.post('/', (req, res) => {
|
|
const db = getDb();
|
|
const { name, image_width, image_height, tile_url, min_zoom, max_zoom, hex_size, origin_x, origin_y } = req.body;
|
|
db.run(
|
|
`INSERT INTO hex_maps (name, image_width, image_height, tile_url, min_zoom, max_zoom, hex_size, origin_x, origin_y)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[name ?? 'Untitled', image_width ?? 8000, image_height ?? 12000,
|
|
tile_url ?? '/tiles/{z}/{x}/{y}.jpg', min_zoom ?? 0, max_zoom ?? 6,
|
|
hex_size ?? 48, origin_x ?? 0, origin_y ?? 0],
|
|
);
|
|
const idRows = db.exec('SELECT last_insert_rowid()');
|
|
const id = idRows[0].values[0][0];
|
|
saveDb();
|
|
res.status(201).json({ id });
|
|
});
|
|
|
|
// Update map
|
|
router.put('/:id', (req, res) => {
|
|
const db = getDb();
|
|
const { name, hex_size, origin_x, origin_y } = req.body;
|
|
const sets: string[] = [];
|
|
const params: any[] = [];
|
|
|
|
if (name !== undefined) { sets.push('name = ?'); params.push(name); }
|
|
if (hex_size !== undefined) { sets.push('hex_size = ?'); params.push(hex_size); }
|
|
if (origin_x !== undefined) { sets.push('origin_x = ?'); params.push(origin_x); }
|
|
if (origin_y !== undefined) { sets.push('origin_y = ?'); params.push(origin_y); }
|
|
|
|
if (sets.length === 0) {
|
|
res.status(400).json({ error: 'No fields to update' });
|
|
return;
|
|
}
|
|
|
|
sets.push("updated_at = datetime('now')");
|
|
params.push(req.params.id);
|
|
|
|
db.run(`UPDATE hex_maps SET ${sets.join(', ')} WHERE id = ?`, params);
|
|
saveDb();
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Delete map
|
|
router.delete('/:id', (req, res) => {
|
|
const db = getDb();
|
|
db.run('DELETE FROM hex_maps WHERE id = ?', [req.params.id]);
|
|
saveDb();
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
function rowToMap(columns: string[], values: any[]) {
|
|
const obj: any = {};
|
|
columns.forEach((col, i) => { obj[col] = values[i]; });
|
|
return obj;
|
|
}
|
|
|
|
export default router;
|