diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 6b9c51f..6a81440 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -1,422 +1,338 @@ -"use client"; - -import { useCallback, useEffect, useState, useRef } from "react"; - -type Recurrence = "none" | "weekly" | "monthly"; - -interface Goal { - id: string; - title: string; - target: number; - current: number; - unit: string; - recurrence: Recurrence; - period_start: string; -} - -const RECURRENCE_LABELS: Record = { - none: "One-time", - weekly: "Weekly", - monthly: "Monthly", -}; - -export default function GoalTracker() { - const [goals, setGoals] = useState([]); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(null); - const [minutesAgo, setMinutesAgo] = useState(0); - const [title, setTitle] = useState(""); - const [target, setTarget] = useState(7); - const [unit, setUnit] = useState("commits"); - const [recurrence, setRecurrence] = useState("none"); - const [creating, setCreating] = useState(false); - const [createError, setCreateError] = useState(null); - const [confirmingId, setConfirmingId] = useState(null); - const [deletingId, setDeletingId] = 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(); - setGoals(data.goals ?? []); - }, []); - - useEffect(() => { - loadGoals() - .catch(() => {}) - .finally(() => { - setLoading(false); - setLastUpdated(new Date()); - setMinutesAgo(0); - }); - }, [loadGoals]); - - 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 }), - }); - - if (!response.ok) { - throw new Error("Failed to create goal"); - } - } catch { - setCreateError("Failed to create goal. Please try again."); - setCreating(false); - return; - } - - setTitle(""); - setTarget(7); - setUnit("commits"); - setRecurrence("none"); - 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); - - try { - const res = await fetch(`/api/goals/${id}`, { method: "DELETE" }); - if (!res.ok) { - setGoals(previousGoals); - } - } catch { - setGoals(previousGoals); - } finally { - setDeletingId(null); - } - } - - 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 ✓"; - return "Completed ✓"; - } - return ""; - } - - useEffect(() => { - 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; - } - - for (const g of goals) { - const isCompleted = g.current >= g.target; - const wasCompleted = prevGoalsRef.current.get(g.id); - - if (wasCompleted === false && isCompleted) { - if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setActiveConfettiGoalId(g.id); - setTimeout(() => { - setActiveConfettiGoalId((curr) => (curr === g.id ? null : curr)); - }, 2500); - } - } - - prevGoalsRef.current.set(g.id, isCompleted); - } - }, [goals]); - - useEffect(() => { - if (!lastUpdated) return; - const interval = setInterval(() => { - const diff = Math.floor((Date.now() - lastUpdated.getTime()) / 60000); - setMinutesAgo(diff); - }, 60000); - return () => clearInterval(interval); - }, [lastUpdated]); - - if (loading) { - return ( -
-
- {[1, 2, 3].map((i) => ( -
-
-
-
- ))} -
- ); - } - - return ( -
-

Weekly Goals

- - {goals.length === 0 ? ( -

- No goals yet. Create one below. -

- ) : ( -
    - {goals.map((goal) => { - const pct = Math.min((goal.current / goal.target) * 100, 100); - const isConfirming = confirmingId === goal.id; - const isDeleting = deletingId === goal.id; - const completed = goal.current >= goal.target; - const completionLabel = getCompletionLabel(goal); - - return ( -
  • - {activeConfettiGoalId === goal.id && } -
    -
    -
    - {goal.title} - {goal.recurrence !== "none" && ( - - {RECURRENCE_LABELS[goal.recurrence]} - - )} -
    - {completed && ( - - {completionLabel} - - )} -
    - -
    - - {goal.current}/{goal.target} {goal.unit} - - - {isConfirming ? ( - - Delete? - - / - - - ) : ( - - )} -
    -
    - -
    -
    -
    -
  • - ); - })} -
- )} - - {lastUpdated && ( -

- {minutesAgo === 0 ? "Updated just now" : `Updated ${minutesAgo} min ago`} -

- )} - - {/* Goal Creation Form */} -
-
- - setTitle(e.target.value)} - placeholder="Make 10 commits" - required - disabled={creating} - className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition placeholder:text-[var(--muted-foreground)] focus:border-[var(--accent)]" - /> -
- -
-
- - setTarget(Number(e.target.value))} - disabled={creating} - className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition focus:border-[var(--accent)]" - /> -
-
- - setUnit(e.target.value)} - placeholder="commits" - disabled={creating} - className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition focus:border-[var(--accent)]" - /> -
-
- - {/* Recurrence Picker */} -
- -
- {(["none", "weekly", "monthly"] as Recurrence[]).map((r) => ( - - ))} -
- {recurrence !== "none" && ( -

- {recurrence === "weekly" ? "Resets every Monday." : "Resets on the 1st of each month."} -

- )} -
- - - - {createError && ( -

{createError}

- )} -
-
- ); -} - -function ConfettiBurst() { - const [particles, setParticles] = useState>([]); - - useEffect(() => { - const colors = ["var(--accent)", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899"]; - const newParticles = []; - for (let i = 0; i < 35; i++) { - const angle = Math.random() * Math.PI * 2; - const distance = 30 + Math.random() * 140; - newParticles.push({ - id: i, - x: Math.cos(angle) * distance, - y: Math.sin(angle) * distance - 20, - color: colors[Math.random() * colors.length | 0], - rot: Math.random() * 360 + 180, - scale: 0.5 + Math.random() * 0.7, - speed: 0.8 + Math.random() * 0.6, - }); - } - setParticles(newParticles); - }, []); - - return ( -
- - {particles.map((p) => ( -
- ))} -
- ); -} \ No newline at end of file +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +type Recurrence = "none" | "weekly" | "monthly"; + +interface Goal { + id: string; + title: string; + target: number; + current: number; + unit: string; + recurrence: Recurrence; + period_start: string; +} + +const RECURRENCE_LABELS: Record = { + none: "One-time", + weekly: "Weekly", + monthly: "Monthly", +}; + +export default function GoalTracker() { + + const [mounted, setMounted] = useState(false); +const [reducedMotion, setReducedMotion] = useState(false); + const [goals, setGoals] = useState([]); + const [loading, setLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + const [title, setTitle] = useState(""); + const [target, setTarget] = useState(7); + const [unit, setUnit] = useState("commits"); + const [recurrence, setRecurrence] = useState("none"); + const [creating, setCreating] = useState(false); + const [createError, setCreateError] = useState(null); + const [confirmingId, setConfirmingId] = useState(null); + const [deletingId, setDeletingId] = useState(null); + + const loadGoals = useCallback(async () => { + const response = await fetch("/api/goals"); + const data: { goals: Goal[] } = await response.json(); + setGoals(data.goals ?? []); + }, []); + + useEffect(() => { + loadGoals() + .catch(() => {}) + .finally(() => { + setLoading(false); + setLastUpdated(new Date()); + }); + }, [loadGoals]); + useEffect(() => { + const mediaQuery = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ); + + setReducedMotion(mediaQuery.matches); + + const timeout = setTimeout(() => { + setMounted(true); + }, 50); + + return () => clearTimeout(timeout); +}, []); + + 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 }), + }); + + if (!response.ok) { + throw new Error("Failed to create goal"); + } + } catch { + setCreateError("Failed to create goal. Please try again."); + setCreating(false); + return; + } + + setTitle(""); + setTarget(7); + setUnit("commits"); + setRecurrence("none"); + 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); + + try { + const res = await fetch(`/api/goals/${id}`, { method: "DELETE" }); + if (!res.ok) { + setGoals(previousGoals); + } + } catch { + setGoals(previousGoals); + } finally { + setDeletingId(null); + } + } + + 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 ✓"; + return "Completed ✓"; + } + return ""; + } + + + if (loading) { + return ( +
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ); + } + + return ( +
+

Weekly Goals

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

+ No goals yet. Create one below. +

+ ) : ( +
    + {goals.map((goal) => { + const pct = Math.min((goal.current / goal.target) * 100, 100); + const isConfirming = confirmingId === goal.id; + const isDeleting = deletingId === goal.id; + const completed = goal.current >= goal.target; + const completionLabel = getCompletionLabel(goal); + + return ( +
  • +
    +
    +
    + {goal.title} + {goal.recurrence !== "none" && ( + + {RECURRENCE_LABELS[goal.recurrence]} + + )} +
    + {completed && ( + + {completionLabel} + + )} +
    + +
    + + {goal.current}/{goal.target} {goal.unit} + + + {isConfirming ? ( + + Delete? + + / + + + ) : ( + + )} +
    +
    + +
    +
    +
    +
  • + ); + })} +
+ )} + + + {/* Goal Creation Form */} +
+
+ + setTitle(e.target.value)} + placeholder="Make 10 commits" + required + disabled={creating} + className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition placeholder:text-[var(--muted-foreground)] focus:border-[var(--accent)]" + /> +
+ +
+
+ + setTarget(Number(e.target.value))} + disabled={creating} + className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition focus:border-[var(--accent)]" + /> +
+
+ + setUnit(e.target.value)} + placeholder="commits" + disabled={creating} + className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm text-[var(--foreground)] outline-none transition focus:border-[var(--accent)]" + /> +
+
+ + {/* Recurrence Picker */} +
+ +
+ {(["none", "weekly", "monthly"] as Recurrence[]).map((r) => ( + + ))} +
+ {recurrence !== "none" && ( +

+ {recurrence === "weekly" ? "Resets every Monday." : "Resets on the 1st of each month."} +

+ )} +
+ + + + {createError && ( +

{createError}

+ )} +
+
+ ); +}