From 6db4c23b282cd7ebee9f34c7f7cc28176067658d Mon Sep 17 00:00:00 2001 From: Phani Sri Deepthi Date: Tue, 1 Sep 2026 00:45:35 +0530 Subject: [PATCH] fix: show progress for small goal completion --- src/components/GoalTracker.tsx | 368 ++++++++++++++++++++++++++------- 1 file changed, 293 insertions(+), 75 deletions(-) diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 15e0a0b8b..4ea361737 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; type Recurrence = "none" | "weekly" | "monthly"; @@ -38,41 +38,58 @@ export default function GoalTracker() { const [deletingId, setDeletingId] = useState(null); const [deleteError, setDeleteError] = useState(null); - const [activeConfettiGoalId, setActiveConfettiGoalId] = useState(null); + const [activeConfettiGoalId, setActiveConfettiGoalId] = + useState(null); + const prevGoalsRef = useRef>(new Map()); const initialLoadDoneRef = useRef(false); const loadGoals = useCallback(async () => { const response = await fetch("/api/goals"); const data: { goals: Goal[] } = await response.json(); + const fetchedGoals = data.goals ?? []; + setGoals(fetchedGoals); + return fetchedGoals; }, []); - /** Sync commit-based goals from GitHub, then reload */ + /** + * Sync commit-based goals from GitHub, then reload. + */ const handleSync = useCallback(async () => { setSyncing(true); setSyncError(null); + try { - const res = await fetch("/api/goals/sync", { method: "POST" }); + const res = await fetch("/api/goals/sync", { + method: "POST", + }); + if (!res.ok) { let msg = "Sync failed. Please try again."; + try { const errData = await res.json(); + if (errData && errData.error) { msg = errData.error; } } catch {} + if (res.status === 401) { msg = "Unauthorized. Please log in again."; } else if (res.status === 502) { msg = "GitHub sync failed: Expired token or missing repo scope."; } + setSyncError(msg); return; } + await loadGoals(); + setLastUpdated(new Date()); setMinutesAgo(0); } catch { @@ -82,16 +99,24 @@ export default function GoalTracker() { } }, [loadGoals]); - // On mount: load goals then auto-sync if stale + // On mount: load goals then auto-sync if stale. useEffect(() => { loadGoals() .then(async (fetchedGoals) => { const needsSync = fetchedGoals.some((g: Goal) => { - if (g.unit !== "commits") return false; - if (!g.last_synced_at) return true; + if (g.unit !== "commits") { + return false; + } + + if (!g.last_synced_at) { + return true; + } + const syncedAt = new Date(g.last_synced_at).getTime(); - return Date.now() - syncedAt > 15 * 60 * 1000; // > 15 mins + + return Date.now() - syncedAt > 15 * 60 * 1000; }); + if (needsSync) { await handleSync(); } @@ -106,14 +131,22 @@ export default function GoalTracker() { async function handleCreate(e: React.FormEvent) { e.preventDefault(); + setCreating(true); setCreateError(null); try { const response = await fetch("/api/goals", { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, target, unit, recurrence }), + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title, + target, + unit, + recurrence, + }), }); if (!response.ok) { @@ -130,24 +163,29 @@ export default function GoalTracker() { setUnit("commits"); setRecurrence("none"); - // Immediately sync if it was a commit-based goal + // Immediately sync if it was a commit-based goal. if (unit === "commits") { await handleSync(); } else { await loadGoals().catch(() => {}); } + setCreating(false); } async function handleDelete(id: string) { const previousGoals = goals; + setGoals((prev) => prev.filter((g) => g.id !== id)); setConfirmingId(null); setDeletingId(id); setDeleteError(null); try { - const res = await fetch(`/api/goals/${id}`, { method: "DELETE" }); + const res = await fetch(`/api/goals/${id}`, { + method: "DELETE", + }); + if (!res.ok) { setGoals(previousGoals); setDeleteError("Failed to delete goal. Please try again."); @@ -162,23 +200,35 @@ export default function GoalTracker() { function getCompletionLabel(goal: Goal): string { if (goal.current >= goal.target) { - if (goal.recurrence === "weekly") return "Completed this week ✓"; - if (goal.recurrence === "monthly") return "Completed this month ✓"; + if (goal.recurrence === "weekly") { + return "Completed this week ✓"; + } + + if (goal.recurrence === "monthly") { + return "Completed this month ✓"; + } + return "Completed ✓"; } + return ""; } useEffect(() => { - if (goals.length === 0) return; + if (goals.length === 0) { + return; + } if (!initialLoadDoneRef.current) { const map = new Map(); + for (const g of goals) { map.set(g.id, g.current >= g.target); } + prevGoalsRef.current = map; initialLoadDoneRef.current = true; + return; } @@ -187,10 +237,15 @@ export default function GoalTracker() { const wasCompleted = prevGoalsRef.current.get(g.id); if (wasCompleted === false && isCompleted) { - if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + if ( + !window.matchMedia("(prefers-reduced-motion: reduce)").matches + ) { setActiveConfettiGoalId(g.id); + setTimeout(() => { - setActiveConfettiGoalId((curr) => (curr === g.id ? null : curr)); + setActiveConfettiGoalId((curr) => + curr === g.id ? null : curr + ); }, 2500); } } @@ -200,26 +255,44 @@ export default function GoalTracker() { }, [goals]); useEffect(() => { - if (!lastUpdated) return; + if (!lastUpdated) { + return; + } + const interval = setInterval(() => { - const diff = Math.floor((Date.now() - lastUpdated.getTime()) / 60000); + const diff = Math.floor( + (Date.now() - lastUpdated.getTime()) / 60000 + ); + setMinutesAgo(diff); }, 60000); + return () => clearInterval(interval); }, [lastUpdated]); if (loading) { return (
-
+
Loading weekly goals +