diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d5c844a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(td --help)", + "Bash(td create:*)", + "Bash(EPIC=td-8064d2)", + "Bash(__NEW_LINE_51256bdd53dfa89e__ td:*)", + "Bash(td list:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(td start:*)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f28ec2c --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +POSTGRES_USER=budget +POSTGRES_PASSWORD=budget +POSTGRES_DB=budget +DATABASE_URL=postgresql://budget:budget@db:5432/budget +PORT=3000 diff --git a/CLAUDE.md b/CLAUDE.md index 2072261..08a1bd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,3 +24,25 @@ td session --new # force a new session in the same terminal context ``` Task state is stored in `.todos/issues.db` (SQLite). + +## Development + +**Run production stack (Docker):** +```bash +docker compose up +``` + +**Run development stack with live reload (Docker):** +```bash +docker compose -f docker-compose.yml -f docker-compose.dev.yml up +``` + +**Frontend only (Vite dev server):** +```bash +cd client && npm install && npm run dev +``` + +**Backend only (nodemon):** +```bash +cd server && npm install && npm run dev +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8bde24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Stage 1: build the React client +FROM node:20-alpine AS client-build + +WORKDIR /app/client + +COPY client/package.json ./ +RUN npm install + +COPY client/ ./ +RUN npm run build + +# Stage 2: production server +FROM node:20-alpine AS production + +WORKDIR /app/server + +COPY server/package.json ./ +RUN npm install --omit=dev + +COPY server/ ./ + +# Copy built client assets +COPY --from=client-build /app/client/dist /app/client/dist + +EXPOSE 3000 + +CMD ["node", "src/index.js"] diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..4cbd44e --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + Budget + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..003f89d --- /dev/null +++ b/client/package.json @@ -0,0 +1,18 @@ +{ + "name": "budget-client", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.2.11" + } +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..f0edc5e --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,32 @@ +import { Routes, Route, NavLink } from 'react-router-dom'; +import PaycheckView from './pages/PaycheckView.jsx'; +import Bills from './pages/Bills.jsx'; +import Settings from './pages/Settings.jsx'; +import MonthlySummary from './pages/MonthlySummary.jsx'; +import AnnualOverview from './pages/AnnualOverview.jsx'; + +function App() { + return ( +
+ + +
+ + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} + +export default App; diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..973d2df --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.jsx'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +); diff --git a/client/src/pages/AnnualOverview.jsx b/client/src/pages/AnnualOverview.jsx new file mode 100644 index 0000000..f28d012 --- /dev/null +++ b/client/src/pages/AnnualOverview.jsx @@ -0,0 +1,5 @@ +function AnnualOverview() { + return

Annual Overview

Placeholder — coming soon.

; +} + +export default AnnualOverview; diff --git a/client/src/pages/Bills.jsx b/client/src/pages/Bills.jsx new file mode 100644 index 0000000..3aaad13 --- /dev/null +++ b/client/src/pages/Bills.jsx @@ -0,0 +1,5 @@ +function Bills() { + return

Bills

Placeholder — coming soon.

; +} + +export default Bills; diff --git a/client/src/pages/MonthlySummary.jsx b/client/src/pages/MonthlySummary.jsx new file mode 100644 index 0000000..264bf7c --- /dev/null +++ b/client/src/pages/MonthlySummary.jsx @@ -0,0 +1,5 @@ +function MonthlySummary() { + return

Monthly Summary

Placeholder — coming soon.

; +} + +export default MonthlySummary; diff --git a/client/src/pages/PaycheckView.jsx b/client/src/pages/PaycheckView.jsx new file mode 100644 index 0000000..3d4fe99 --- /dev/null +++ b/client/src/pages/PaycheckView.jsx @@ -0,0 +1,5 @@ +function PaycheckView() { + return

Paycheck View

Placeholder — coming soon.

; +} + +export default PaycheckView; diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx new file mode 100644 index 0000000..9c07995 --- /dev/null +++ b/client/src/pages/Settings.jsx @@ -0,0 +1,5 @@ +function Settings() { + return

Settings

Placeholder — coming soon.

; +} + +export default Settings; diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..ab4b186 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': 'http://localhost:3001', + }, + }, +}); diff --git a/db/migrations/.gitkeep b/db/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..0dfe8e1 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,9 @@ +services: + app: + volumes: + - ./server:/app/server + - ./client:/app/client + ports: + - "5173:5173" + environment: + NODE_ENV: development diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..530238a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + + app: + build: . + restart: unless-stopped + depends_on: + - db + ports: + - "3000:3000" + environment: + DATABASE_URL: ${DATABASE_URL} + PORT: ${PORT} + +volumes: + pgdata: diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..a6c341e --- /dev/null +++ b/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "budget-server", + "version": "1.0.0", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "pg": "^8.11.5" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} diff --git a/server/src/db.js b/server/src/db.js new file mode 100644 index 0000000..f18ec8e --- /dev/null +++ b/server/src/db.js @@ -0,0 +1,7 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +module.exports = pool; diff --git a/server/src/index.js b/server/src/index.js new file mode 100644 index 0000000..1eb1215 --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,27 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const healthRouter = require('./routes/health'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json()); + +// API routes +app.use('/api', healthRouter); + +// Serve static client files in production +const clientDist = path.join(__dirname, '../../client/dist'); +app.use(express.static(clientDist)); + +// SPA fallback — send index.html for any unmatched route +app.get('*', (req, res) => { + res.sendFile(path.join(clientDist, 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/server/src/routes/health.js b/server/src/routes/health.js new file mode 100644 index 0000000..1001491 --- /dev/null +++ b/server/src/routes/health.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); + +router.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +module.exports = router;