Phase 1 & 2: full-stack family dashboard scaffold

- pnpm monorepo (apps/client + apps/server)
- Server: Express + node:sqlite with numbered migration runner,
  REST API for all 9 features (members, events, chores, shopping,
  meals, messages, countdowns, photos, settings)
- Client: React 18 + Vite + TypeScript + Tailwind + Framer Motion + Zustand
- Theme system: dark/light + 5 accent colors, CSS custom properties,
  anti-FOUC script, ThemeToggle on every surface
- AppShell: collapsible sidebar, animated route transitions, mobile drawer
- Phase 2 features: Calendar (custom month grid, event chips, add/edit modal),
  Chores (card grid, complete/reset, member filter, streaks),
  Shopping (multi-list tabs, animated check-off, quick-add bar, member assign)
- Family member CRUD with avatar, color picker
- Settings page: theme/accent, photo folder, slideshow, weather, date/time
- Docker: multi-stage Dockerfile, docker-compose.yml, entrypoint with PUID/PGID
- Unraid: CA XML template, CLI install script, UNRAID.md guide
- .gitignore covering node_modules, dist, db files, secrets, build artifacts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:56:30 -05:00
parent 6e44883365
commit 35ed5223a0
58 changed files with 6224 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import { Router } from 'express';
import db from '../db/db';
const router = Router();
// ── Lists ──────────────────────────────────────────────────────────────────
router.get('/lists', (_req, res) => {
res.json(db.prepare('SELECT * FROM shopping_lists ORDER BY name ASC').all());
});
router.post('/lists', (req, res) => {
const { name } = req.body as { name: string };
if (!name?.trim()) return res.status(400).json({ error: 'Name is required' });
const result = db.prepare('INSERT INTO shopping_lists (name) VALUES (?)').run(name.trim());
res.status(201).json(db.prepare('SELECT * FROM shopping_lists WHERE id = ?').get(result.lastInsertRowid));
});
router.delete('/lists/:id', (req, res) => {
const result = db.prepare('DELETE FROM shopping_lists WHERE id = ?').run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'List not found' });
res.status(204).end();
});
// ── Items ──────────────────────────────────────────────────────────────────
router.get('/lists/:listId/items', (req, res) => {
res.json(
db.prepare('SELECT * FROM shopping_items WHERE list_id = ? ORDER BY sort_order ASC, id ASC').all(req.params.listId)
);
});
router.post('/lists/:listId/items', (req, res) => {
const { name, quantity, member_id } = req.body as { name: string; quantity?: string; member_id?: number };
if (!name?.trim()) return res.status(400).json({ error: 'Name is required' });
const maxOrder = (db.prepare('SELECT MAX(sort_order) as m FROM shopping_items WHERE list_id = ?').get(req.params.listId) as any)?.m ?? 0;
const result = db
.prepare('INSERT INTO shopping_items (list_id, name, quantity, member_id, sort_order) VALUES (?, ?, ?, ?, ?)')
.run(req.params.listId, name.trim(), quantity ?? null, member_id ?? null, maxOrder + 1);
res.status(201).json(db.prepare('SELECT * FROM shopping_items WHERE id = ?').get(result.lastInsertRowid));
});
router.patch('/items/:id', (req, res) => {
const existing = db.prepare('SELECT * FROM shopping_items WHERE id = ?').get(req.params.id) as any;
if (!existing) return res.status(404).json({ error: 'Item not found' });
const { name, quantity, checked, member_id, sort_order } = req.body;
db.prepare('UPDATE shopping_items SET name=?, quantity=?, checked=?, member_id=?, sort_order=? WHERE id=?').run(
name?.trim() ?? existing.name,
quantity !== undefined ? quantity : existing.quantity,
checked !== undefined ? (checked ? 1 : 0) : existing.checked,
member_id !== undefined ? member_id : existing.member_id,
sort_order !== undefined ? sort_order : existing.sort_order,
req.params.id
);
res.json(db.prepare('SELECT * FROM shopping_items WHERE id = ?').get(req.params.id));
});
router.delete('/items/:id', (req, res) => {
const result = db.prepare('DELETE FROM shopping_items WHERE id = ?').run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Item not found' });
res.status(204).end();
});
router.delete('/lists/:listId/items/checked', (req, res) => {
db.prepare('DELETE FROM shopping_items WHERE list_id = ? AND checked = 1').run(req.params.listId);
res.status(204).end();
});
export default router;