From eae075ae78f526d47e3849c9dd9ebdad4dbdba5b Mon Sep 17 00:00:00 2001 From: Mehtab Singh Date: Wed, 20 May 2026 23:04:20 +0530 Subject: [PATCH] fix: enforce 100-char max length on goal title (#384) --- src/app/api/goals/route.ts | 9 +- src/components/GoalTracker.tsx | 884 +++++++++++++++++---------------- 2 files changed, 464 insertions(+), 429 deletions(-) diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 0a8eb0cd..6538fe2f 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -113,10 +113,17 @@ export async function POST(req: Request) { recurrence?: Recurrence; }; - if (!body.title || !body.target) { + if (!body.title || !body.target) { return Response.json({ error: "title and target required" }, { status: 400 }); } + if (body.title.trim().length > 100) { + return Response.json( + { error: "Goal title must be 100 characters or fewer" }, + { status: 400 } + ); + } + const recurrence: Recurrence = body.recurrence ?? "none"; if (!["none", "weekly", "monthly"].includes(recurrence)) { return Response.json({ error: "Invalid recurrence value" }, { status: 400 }); diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index ad8407a4..2d458d52 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -1,428 +1,456 @@ -"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 ( -
-
- Loading weekly goals -