diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 0ed963e64..0d616ebd3 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -20,6 +20,7 @@ interface Goal { created_at: string; goal_reset_version: number; is_public: boolean; + category: GoalCategory | null; } interface GoalHistory { @@ -32,8 +33,10 @@ interface GoalHistory { } type Recurrence = "none" | "weekly" | "monthly"; +type GoalCategory = "side-project" | "work" | "dsa" | "open-source"; const VALID_RECURRENCES = ["none", "weekly", "monthly"] as const; +const VALID_CATEGORIES = ["side-project", "work", "dsa", "open-source"] as const; const MAX_TITLE_LEN = 100; const MAX_UNIT_LEN = 30; const MIN_TARGET = 1; @@ -201,7 +204,7 @@ try { return Response.json({ error: "Invalid request body" }, { status: 400 }); } - const { title, target, unit, recurrence, deadline } = body as Record; + const { title, target, unit, recurrence, deadline, category } = body as Record; if (typeof title !== "string" || title.trim().length === 0) { return Response.json({ error: "title must be a non-empty string" }, { status: 400 }); @@ -229,6 +232,9 @@ try { const safeRecurrence: Recurrence = VALID_RECURRENCES.includes(recurrence as Recurrence) ? (recurrence as Recurrence) : "none"; + const safeCategory: GoalCategory | null = VALID_CATEGORIES.includes(category as GoalCategory) + ? (category as GoalCategory) + : null; let safeDeadline: string | null = null; if (typeof deadline === "string") { @@ -285,6 +291,7 @@ try { recurrence: safeRecurrence, period_start: getPeriodStart(safeRecurrence), deadline: safeDeadline, + category: safeCategory, current: 0, goal_reset_version: 0, }) @@ -300,6 +307,7 @@ try { target: goal.target, unit: goal.unit, recurrence: goal.recurrence, + category: goal.category, }).catch(() => {}); return Response.json({ goal }, { status: 201 }); diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 1098c698b..8ea2165dc 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -12,6 +12,7 @@ import WidgetSkeleton, { SkeletonBlock } from "./WidgetSkeleton"; type Recurrence = "none" | "weekly" | "monthly"; +type GoalCategory = "side-project" | "work" | "dsa" | "open-source"; interface Goal { id: string; @@ -25,6 +26,7 @@ interface Goal { period_start: string; last_synced_at: string | null; week_start: string | null; + category?: GoalCategory | null; last_period: { period_start: string; period_end: string; @@ -40,6 +42,22 @@ const RECURRENCE_LABELS: Record = { monthly: "Monthly", }; +const CATEGORY_LABELS: Record = { + "side-project": "Side Project", + work: "Work", + dsa: "DSA", + "open-source": "Open Source", +}; + +const CATEGORY_BADGE_CLASSES: Record = { + "side-project": "bg-purple-500/10 text-purple-400 border-purple-500/30", + work: "bg-blue-500/10 text-blue-400 border-blue-500/30", + dsa: "bg-amber-500/10 text-amber-400 border-amber-500/30", + "open-source": "bg-emerald-500/10 text-emerald-400 border-emerald-500/30", +}; + +const CATEGORY_OPTIONS: GoalCategory[] = ["side-project", "work", "dsa", "open-source"]; + export function useGoalTracker() { const [goals, setGoals] = useState([]); const [loading, setLoading] = useState(true); @@ -52,6 +70,8 @@ export function useGoalTracker() { const [unit, setUnit] = useState("commits"); const [recurrence, setRecurrence] = useState("none"); const [deadline, setDeadline] = useState(""); + const [category, setCategory] = useState(""); + const [activeCategoryFilter, setActiveCategoryFilter] = useState(null); const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); const [confirmingId, setConfirmingId] = useState(null); @@ -162,7 +182,14 @@ export function useGoalTracker() { try { const result = await submitGoalWithRefresh({ - payload: { title, target, unit, recurrence, deadline: deadline || null }, + payload: { + title, + target, + unit, + recurrence, + deadline: deadline || null, + category: category || null, + }, handleSync, loadGoals, }); @@ -177,6 +204,7 @@ export function useGoalTracker() { setUnit("commits"); setRecurrence("none"); setDeadline(""); + setCategory(""); if (unit === "commits" || unit === "prs") { await handleSync(); @@ -294,6 +322,10 @@ export function useGoalTracker() { setRecurrence, deadline, setDeadline, + category, + setCategory, + activeCategoryFilter, + setActiveCategoryFilter, creating, createError, confirmingId, @@ -332,6 +364,10 @@ export default function GoalTracker() { setRecurrence, deadline, setDeadline, + category, + setCategory, + activeCategoryFilter, + setActiveCategoryFilter, creating, createError, confirmingId, @@ -530,9 +566,50 @@ export default function GoalTracker() { ) : ( + <> + {goals.some((g) => g.category) && ( +
+ + {CATEGORY_OPTIONS.map((c) => ( + + ))} +
+ )} +
    - {goals.map((goal) => { + {goals + .filter((goal) => !activeCategoryFilter || goal.category === activeCategoryFilter) + .map((goal) => { const pct = goal.current > 0 && goal.target > 0 ? Math.max( @@ -567,6 +644,13 @@ export default function GoalTracker() {
    {goal.title} + {goal.category && ( + + {CATEGORY_LABELS[goal.category]} + + )} {goal.recurrence !== "none" && ( + )} {lastUpdated && ( @@ -825,6 +910,29 @@ export default function GoalTracker() {
    +
    + + +
    + {recurrence === "none" && (