diff --git a/app/api/counters/[id]/archive/route.ts b/app/api/counters/[id]/archive/route.ts new file mode 100644 index 0000000..6742429 --- /dev/null +++ b/app/api/counters/[id]/archive/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import db from '@/lib/db'; + +export const dynamic = 'force-dynamic'; + +type Params = { params: Promise<{ id: string }> }; + +export async function POST(_req: Request, { params }: Params) { + const { id } = await params; + const existing = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id)); + if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + const updated = db.prepare('UPDATE counters SET archived_at = ? WHERE id = ? RETURNING *').get(Date.now(), Number(id)); + return NextResponse.json(updated); +} diff --git a/app/api/counters/[id]/decrement/route.ts b/app/api/counters/[id]/decrement/route.ts index 91f88b2..b245326 100644 --- a/app/api/counters/[id]/decrement/route.ts +++ b/app/api/counters/[id]/decrement/route.ts @@ -8,7 +8,7 @@ type Params = { params: Promise<{ id: string }> }; export async function POST(_req: Request, { params }: Params) { const { id } = await params; const tx = db.transaction(() => { - const row = db.prepare('UPDATE counters SET value = value - 1 WHERE id = ? AND value > 0 RETURNING *').get(Number(id)); + const row = db.prepare('UPDATE counters SET value = value - 1 WHERE id = ? AND value > 0 AND archived_at IS NULL RETURNING *').get(Number(id)); if (row) db.prepare('INSERT INTO events (counter_id, delta, created_at) VALUES (?, -1, ?)').run(Number(id), Date.now()); return row; }); diff --git a/app/api/counters/[id]/increment/route.ts b/app/api/counters/[id]/increment/route.ts index b53e6a3..8a26429 100644 --- a/app/api/counters/[id]/increment/route.ts +++ b/app/api/counters/[id]/increment/route.ts @@ -8,7 +8,7 @@ type Params = { params: Promise<{ id: string }> }; export async function POST(_req: Request, { params }: Params) { const { id } = await params; const tx = db.transaction(() => { - const row = db.prepare('UPDATE counters SET value = value + 1 WHERE id = ? RETURNING *').get(Number(id)); + const row = db.prepare('UPDATE counters SET value = value + 1 WHERE id = ? AND archived_at IS NULL RETURNING *').get(Number(id)); if (row) db.prepare('INSERT INTO events (counter_id, delta, created_at) VALUES (?, 1, ?)').run(Number(id), Date.now()); return row; }); diff --git a/app/api/counters/[id]/unarchive/route.ts b/app/api/counters/[id]/unarchive/route.ts new file mode 100644 index 0000000..f23e70b --- /dev/null +++ b/app/api/counters/[id]/unarchive/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import db from '@/lib/db'; + +export const dynamic = 'force-dynamic'; + +type Params = { params: Promise<{ id: string }> }; + +export async function POST(_req: Request, { params }: Params) { + const { id } = await params; + const existing = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id)); + if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + const updated = db.prepare('UPDATE counters SET archived_at = NULL WHERE id = ? RETURNING *').get(Number(id)); + return NextResponse.json(updated); +} diff --git a/app/api/counters/route.ts b/app/api/counters/route.ts index 8f8d26a..f1eb57e 100644 --- a/app/api/counters/route.ts +++ b/app/api/counters/route.ts @@ -3,8 +3,12 @@ import db from '@/lib/db'; export const dynamic = 'force-dynamic'; -export async function GET() { - const counters = db.prepare('SELECT * FROM counters ORDER BY order_index ASC, id ASC').all(); +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const archived = searchParams.get('archived') === 'true'; + const counters = archived + ? db.prepare('SELECT * FROM counters WHERE archived_at IS NOT NULL ORDER BY archived_at DESC').all() + : db.prepare('SELECT * FROM counters WHERE archived_at IS NULL ORDER BY order_index ASC, id ASC').all(); return NextResponse.json(counters); } diff --git a/app/components/CounterCard.tsx b/app/components/CounterCard.tsx index 1821ac6..6730b32 100644 --- a/app/components/CounterCard.tsx +++ b/app/components/CounterCard.tsx @@ -7,6 +7,7 @@ export interface Counter { image_path: string | null; group_id: number | null; order_index: number; + archived_at?: number | null; } interface Props { @@ -69,7 +70,7 @@ export default function CounterCard({ counter, onIncrement, onDecrement, onEdit,
- {/* Delete — only shown when editing an existing counter */} - {initial && onDelete ? ( - + {initial ? ( + initial.archived_at ? ( +
+ + {onDelete && ( + + )} +
+ ) : ( + + ) ) : }
diff --git a/app/page.tsx b/app/page.tsx index d0d8927..f47abb2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -34,6 +34,8 @@ export default function Home() { const [loading, setLoading] = useState(true); const [historyCounterId, setHistoryCounterId] = useState(null); const [editMode, setEditMode] = useState(false); + const [archivedCounters, setArchivedCounters] = useState([]); + const [showArchived, setShowArchived] = useState(false); // Always-current ref so drag handlers never read stale closure state const countersRef = useRef(counters); @@ -65,6 +67,14 @@ export default function Home() { useEffect(() => { load(); }, []); useEffect(() => { router.prefetch('/stats'); }, [router]); + useEffect(() => { + if (showArchived) { + fetch('/api/counters?archived=true') + .then(r => r.json()) + .then(setArchivedCounters) + .catch(() => {}); + } + }, [showArchived]); // ── Optimistic increment/decrement ───────────────────────────────────────── const handleIncrement = useCallback(async (id: number) => { @@ -86,7 +96,9 @@ export default function Home() { if (editingCounter) { // Optimistic update — close immediately, patch in background const optimistic = { ...editingCounter, ...data }; - setCounters(prev => prev.map(c => c.id === editingCounter.id ? optimistic : c)); + const isArchived = !!editingCounter.archived_at; + const setTarget = isArchived ? setArchivedCounters : setCounters; + setTarget(prev => prev.map(c => c.id === editingCounter.id ? optimistic : c)); setModalOpen(false); setEditingCounter(null); const res = await fetch(`/api/counters/${editingCounter.id}`, { @@ -96,10 +108,9 @@ export default function Home() { }); if (res.ok) { const updated = await res.json(); - setCounters(prev => prev.map(c => c.id === updated.id ? updated : c)); + setTarget(prev => prev.map(c => c.id === updated.id ? updated : c)); } else { - // Roll back - setCounters(prev => prev.map(c => c.id === editingCounter.id ? editingCounter : c)); + setTarget(prev => prev.map(c => c.id === editingCounter.id ? editingCounter : c)); } } else { // Optimistic create with a temporary id @@ -126,9 +137,41 @@ export default function Home() { // ── Delete counter ───────────────────────────────────────────────────────── async function handleDeleteCounter(id: number) { const prev = counters.find(c => c.id === id); + const prevArchived = archivedCounters.find(c => c.id === id); setCounters(cs => cs.filter(c => c.id !== id)); + setArchivedCounters(cs => cs.filter(c => c.id !== id)); const res = await fetch(`/api/counters/${id}`, { method: 'DELETE' }); - if (!res.ok && prev) setCounters(cs => [...cs, prev].sort((a, b) => a.order_index - b.order_index)); + if (!res.ok) { + if (prev) setCounters(cs => [...cs, prev].sort((a, b) => a.order_index - b.order_index)); + if (prevArchived) setArchivedCounters(cs => [...cs, prevArchived]); + } + } + + // ── Archive / Unarchive counter ────────────────────────────────────────────────────── + async function handleArchiveCounter(id: number) { + const counter = counters.find(c => c.id === id); + if (!counter) return; + setCounters(cs => cs.filter(c => c.id !== id)); + const res = await fetch(`/api/counters/${id}/archive`, { method: 'POST' }); + if (res.ok) { + const archived = await res.json(); + if (showArchived) setArchivedCounters(prev => [archived, ...prev]); + } else { + setCounters(cs => [...cs, counter].sort((a, b) => a.order_index - b.order_index)); + } + } + + async function handleUnarchiveCounter(id: number) { + const counter = archivedCounters.find(c => c.id === id); + if (!counter) return; + setArchivedCounters(prev => prev.filter(c => c.id !== id)); + const res = await fetch(`/api/counters/${id}/unarchive`, { method: 'POST' }); + if (res.ok) { + const unarchived = await res.json(); + setCounters(prev => [...prev, unarchived].sort((a, b) => a.order_index - b.order_index)); + } else { + setArchivedCounters(prev => [...prev, counter]); + } } // ── Groups ───────────────────────────────────────────────────────────────── @@ -275,6 +318,21 @@ export default function Home() { Stats + {addingGroup ? (
)} + + {showArchived && ( +
+

Archived

+ {archivedCounters.length === 0 ? ( +

No archived counters.

+ ) : ( +
+ {archivedCounters.map(counter => ( + {}} + onDecrement={() => {}} + onEdit={c => { setEditingCounter(c); setModalOpen(true); }} + onHistory={setHistoryCounterId} + onPrefetch={handlePrefetch} + editMode={false} + /> + ))} +
+ )} +
+ )}
{ setModalOpen(false); setEditingCounter(null); }} onSave={handleSaveCounter} onDelete={handleDeleteCounter} + onArchive={handleArchiveCounter} + onUnarchive={handleUnarchiveCounter} /> c.name); +if (!cols.includes('archived_at')) { + db.exec('ALTER TABLE counters ADD COLUMN archived_at INTEGER'); +} + export { UPLOADS_DIR }; export default db;