feat: implement archiving functionality for counters and update related logic

This commit is contained in:
2026-08-25 13:25:21 +02:00
parent 6ccf897fc4
commit 3df1e8d7fa
9 changed files with 182 additions and 35 deletions
+89 -5
View File
@@ -34,6 +34,8 @@ export default function Home() {
const [loading, setLoading] = useState(true);
const [historyCounterId, setHistoryCounterId] = useState<number | null>(null);
const [editMode, setEditMode] = useState(false);
const [archivedCounters, setArchivedCounters] = useState<Counter[]>([]);
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() {
</svg>
Stats
</Link>
<button
onClick={() => setShowArchived(v => !v)}
className={`flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg border transition-colors ${
showArchived
? 'border-ctp-yellow/50 bg-ctp-yellow/10 text-ctp-yellow hover:bg-ctp-yellow/20'
: 'border-ctp-surface1 text-ctp-subtext1 hover:bg-ctp-surface0'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="21 8 21 21 3 21 3 8" />
<rect x="1" y="3" width="22" height="5" />
<line x1="10" y1="12" x2="14" y2="12" />
</svg>
Archived
</button>
{addingGroup ? (
<div className="flex gap-2">
<input
@@ -396,6 +454,30 @@ export default function Home() {
</button>
</div>
)}
{showArchived && (
<div className="mt-10">
<h2 className="text-lg font-bold text-ctp-overlay1 mb-3">Archived</h2>
{archivedCounters.length === 0 ? (
<p className="text-ctp-overlay0 text-sm">No archived counters.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 opacity-60">
{archivedCounters.map(counter => (
<CounterCard
key={counter.id}
counter={counter}
onIncrement={() => {}}
onDecrement={() => {}}
onEdit={c => { setEditingCounter(c); setModalOpen(true); }}
onHistory={setHistoryCounterId}
onPrefetch={handlePrefetch}
editMode={false}
/>
))}
</div>
)}
</div>
)}
</div>
<CounterModal
@@ -405,6 +487,8 @@ export default function Home() {
onClose={() => { setModalOpen(false); setEditingCounter(null); }}
onSave={handleSaveCounter}
onDelete={handleDeleteCounter}
onArchive={handleArchiveCounter}
onUnarchive={handleUnarchiveCounter}
/>
<CounterDetailModal