diff --git a/Dockerfile b/Dockerfile
index 25db863..17a0597 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,14 @@ RUN npm ci
COPY . .
RUN npm run build
-FROM nginx:alpine
-COPY --from=build /app/build /usr/share/nginx/html
-EXPOSE 80
-CMD ["nginx", "-g", "daemon off;"]
+FROM node:20-alpine
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci --omit=dev
+COPY server.js ./
+COPY --from=build /app/build ./build
+
+ENV DATA_DIR=/data
+VOLUME ["/data"]
+EXPOSE 4000
+CMD ["node", "server.js"]
diff --git a/docker-compose.yml b/docker-compose.yml
index 6a89a1e..744a090 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,4 +2,9 @@ services:
prayer:
build: .
ports:
- - "3000:80"
+ - "3000:4000"
+ volumes:
+ - prayer-data:/data
+
+volumes:
+ prayer-data:
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..6284264
--- /dev/null
+++ b/server.js
@@ -0,0 +1,62 @@
+const express = require('express');
+const cors = require('cors');
+const fs = require('fs');
+const path = require('path');
+
+const app = express();
+const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
+const DATA_FILE = path.join(DATA_DIR, 'prayers.json');
+const PORT = process.env.PORT || 4000;
+
+app.use(cors());
+app.use(express.json());
+app.use(express.static(path.join(__dirname, 'build')));
+
+function readPrayers() {
+ if (!fs.existsSync(DATA_FILE)) return [];
+ try {
+ return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
+ } catch {
+ return [];
+ }
+}
+
+function writePrayers(prayers) {
+ fs.mkdirSync(DATA_DIR, { recursive: true });
+ fs.writeFileSync(DATA_FILE, JSON.stringify(prayers, null, 2));
+}
+
+app.get('/api/prayers', (req, res) => {
+ res.json(readPrayers());
+});
+
+app.post('/api/prayers', (req, res) => {
+ const prayers = readPrayers();
+ const prayer = req.body;
+ prayers.unshift(prayer);
+ writePrayers(prayers);
+ res.status(201).json(prayer);
+});
+
+app.put('/api/prayers/:id', (req, res) => {
+ const prayers = readPrayers();
+ const idx = prayers.findIndex(p => p.id === req.params.id);
+ if (idx === -1) return res.status(404).json({ error: 'Not found' });
+ prayers[idx] = { ...prayers[idx], ...req.body };
+ writePrayers(prayers);
+ res.json(prayers[idx]);
+});
+
+app.delete('/api/prayers/:id', (req, res) => {
+ const prayers = readPrayers();
+ const filtered = prayers.filter(p => p.id !== req.params.id);
+ writePrayers(filtered);
+ res.status(204).end();
+});
+
+// Serve React for all other routes
+app.get('*', (req, res) => {
+ res.sendFile(path.join(__dirname, 'build', 'index.html'));
+});
+
+app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
diff --git a/src/App.css b/src/App.css
index 34e15bb..3fad4a4 100644
--- a/src/App.css
+++ b/src/App.css
@@ -234,6 +234,13 @@ body {
gap: 0.75rem;
}
+.loading {
+ text-align: center;
+ color: var(--text-muted);
+ font-style: italic;
+ padding: 1rem 0;
+}
+
.empty {
text-align: center;
color: var(--text-muted);
diff --git a/src/App.tsx b/src/App.tsx
index 781f32e..633bc0f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,6 +8,7 @@ type Filter = 'active' | 'answered' | 'all';
export default function App() {
const {
prayers,
+ loading,
addPrayer,
updateAnswer,
markAnswered,
@@ -111,6 +112,8 @@ export default function App() {
+ {loading &&
Loading prayers…
}
+
{filtered.length === 0 && (
-
diff --git a/src/usePrayers.ts b/src/usePrayers.ts
index cc83f8c..1617a4f 100644
--- a/src/usePrayers.ts
+++ b/src/usePrayers.ts
@@ -1,23 +1,20 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { Prayer } from './types';
-const STORAGE_KEY = 'prayer-journal';
+const API = '/api/prayers';
export function usePrayers() {
- const [prayers, setPrayers] = useState(() => {
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- return stored ? JSON.parse(stored) : [];
- } catch {
- return [];
- }
- });
+ const [prayers, setPrayers] = useState([]);
+ const [loading, setLoading] = useState(true);
useEffect(() => {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(prayers));
- }, [prayers]);
+ fetch(API)
+ .then(r => r.json())
+ .then(setPrayers)
+ .finally(() => setLoading(false));
+ }, []);
- function addPrayer(request: string, tags: string[]) {
+ const addPrayer = useCallback(async (request: string, tags: string[]) => {
const prayer: Prayer = {
id: crypto.randomUUID(),
request,
@@ -26,51 +23,46 @@ export function usePrayers() {
createdAt: new Date().toISOString(),
tags,
};
- setPrayers(prev => [prayer, ...prev]);
- }
+ const saved = await fetch(API, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(prayer),
+ }).then(r => r.json());
+ setPrayers(prev => [saved, ...prev]);
+ }, []);
- function updateAnswer(id: string, answer: string) {
- setPrayers(prev =>
- prev.map(p => p.id === id ? { ...p, answer } : p)
- );
- }
+ const patch = useCallback(async (id: string, updates: Partial) => {
+ const updated = await fetch(`${API}/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updates),
+ }).then(r => r.json());
+ setPrayers(prev => prev.map(p => p.id === id ? updated : p));
+ }, []);
- function markAnswered(id: string) {
- setPrayers(prev =>
- prev.map(p =>
- p.id === id
- ? { ...p, status: 'answered', answeredAt: new Date().toISOString() }
- : p
- )
- );
- }
+ const updateAnswer = useCallback((id: string, answer: string) =>
+ patch(id, { answer }), [patch]);
- function markActive(id: string) {
- setPrayers(prev =>
- prev.map(p =>
- p.id === id ? { ...p, status: 'active', answeredAt: undefined } : p
- )
- );
- }
+ const markAnswered = useCallback((id: string) =>
+ patch(id, { status: 'answered', answeredAt: new Date().toISOString() }), [patch]);
- function archivePrayer(id: string) {
- setPrayers(prev =>
- prev.map(p => p.id === id ? { ...p, status: 'archived' } : p)
- );
- }
+ const markActive = useCallback((id: string) =>
+ patch(id, { status: 'active', answeredAt: undefined }), [patch]);
- function deletePrayer(id: string) {
+ const archivePrayer = useCallback((id: string) =>
+ patch(id, { status: 'archived' }), [patch]);
+
+ const updateRequest = useCallback((id: string, request: string) =>
+ patch(id, { request }), [patch]);
+
+ const deletePrayer = useCallback(async (id: string) => {
+ await fetch(`${API}/${id}`, { method: 'DELETE' });
setPrayers(prev => prev.filter(p => p.id !== id));
- }
-
- function updateRequest(id: string, request: string) {
- setPrayers(prev =>
- prev.map(p => p.id === id ? { ...p, request } : p)
- );
- }
+ }, []);
return {
prayers,
+ loading,
addPrayer,
updateAnswer,
markAnswered,