diff --git a/app/api/counters/[id]/log/route.ts b/app/api/counters/[id]/log/route.ts new file mode 100644 index 0000000..40d6d69 --- /dev/null +++ b/app/api/counters/[id]/log/route.ts @@ -0,0 +1,39 @@ +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(request: Request, { params }: Params) { + const { id } = await params; + const { date, delta } = await request.json(); + + if (!date || typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return NextResponse.json({ error: 'Invalid date' }, { status: 400 }); + } + if (delta !== 1 && delta !== -1) { + return NextResponse.json({ error: 'Delta must be 1 or -1' }, { status: 400 }); + } + + const existing = db.prepare('SELECT * FROM counters WHERE id = ? AND archived_at IS NULL').get(Number(id)) as Record | undefined; + if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + // Store at noon local server time so SQLite's 'localtime' grouping matches the picked date + const createdAt = new Date(`${date}T12:00:00`).getTime(); + if (isNaN(createdAt)) return NextResponse.json({ error: 'Invalid date' }, { status: 400 }); + + const result = db.transaction(() => { + const updated = db.prepare( + delta === 1 + ? 'UPDATE counters SET value = value + 1 WHERE id = ? RETURNING *' + : 'UPDATE counters SET value = value - 1 WHERE id = ? AND value > 0 RETURNING *' + ).get(Number(id)) as Record | undefined; + if (!updated) return null; + db.prepare('INSERT INTO events (counter_id, delta, created_at) VALUES (?, ?, ?)').run(Number(id), delta, createdAt); + return updated; + })(); + + if (!result) return NextResponse.json({ error: 'Counter cannot go below 0' }, { status: 400 }); + return NextResponse.json(result); +} diff --git a/app/components/CounterDetailModal.tsx b/app/components/CounterDetailModal.tsx index 151ca3f..9576773 100644 --- a/app/components/CounterDetailModal.tsx +++ b/app/components/CounterDetailModal.tsx @@ -15,6 +15,7 @@ interface Props { counterId: number | null; cache?: Map; onClose: () => void; + onLog?: (counter: Counter) => void; } function toLocalDateStr(d: Date): string { @@ -47,12 +48,18 @@ function computeLongestStreak(sortedDates: string[]): number { return best; } -export default function CounterDetailModal({ counterId, cache, onClose }: Props) { +export default function CounterDetailModal({ counterId, cache, onClose, onLog }: Props) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); + const [logDate, setLogDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 1); return toLocalDateStr(d); }); + const [logging, setLogging] = useState(false); + const [logError, setLogError] = useState(''); useEffect(() => { if (!counterId) return; + const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); + setLogDate(toLocalDateStr(yesterday)); + setLogError(''); const cached = cache?.get(counterId); if (cached) { setData(cached); setLoading(false); return; } setData(null); @@ -70,6 +77,33 @@ export default function CounterDetailModal({ counterId, cache, onClose }: Props) return () => window.removeEventListener('keydown', handler); }, [counterId, onClose]); + async function handleLog(delta: number) { + if (!counterId || !logDate) return; + setLogging(true); + setLogError(''); + try { + const res = await fetch(`/api/counters/${counterId}/log`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: logDate, delta }), + }); + if (!res.ok) { + const body = await res.json(); + setLogError(body.error ?? 'Failed to log entry'); + return; + } + const updated: Counter = await res.json(); + cache?.delete(counterId); + const histRes = await fetch(`/api/counters/${counterId}/history`); + const d: HistoryData = await histRes.json(); + cache?.set(counterId, d); + setData(d); + onLog?.(updated); + } finally { + setLogging(false); + } + } + if (!counterId) return null; const dateSet = new Set(data?.dailyActivity.map(d => d.date) ?? []); @@ -165,6 +199,42 @@ export default function CounterDetailModal({ counterId, cache, onClose }: Props) )} + + {/* Log past entry */} +
+

Log past entry

+
+ setLogDate(e.target.value)} + className="rounded-lg border border-ctp-surface1 bg-ctp-surface0 text-ctp-text px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ctp-mauve" + /> + + + {logError &&

{logError}

} +
+
)} diff --git a/app/page.tsx b/app/page.tsx index f47abb2..d94a01c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -495,6 +495,10 @@ export default function Home() { counterId={historyCounterId} cache={historyCache.current} onClose={() => setHistoryCounterId(null)} + onLog={updated => { + setCounters(prev => prev.map(c => c.id === updated.id ? { ...c, value: updated.value } : c)); + setArchivedCounters(prev => prev.map(c => c.id === updated.id ? { ...c, value: updated.value } : c)); + }} /> );