import { useEffect, useMemo, useState } from "react"; import type { Goal } from "@fusion/core"; import { Plus, Sparkles } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { draftGoalDescription, getRefineErrorMessage } from "../api"; import "./GoalsView.css"; export interface GoalsViewProps { initialGoals?: Goal[]; } const MAX_ACTIVE_GOALS = 5; const WARNING_THRESHOLD = 3; const CAP_ERROR_MESSAGE = "Cannot activate more than 5 goals. Resolve an active goal before activating another."; const GOAL_DESCRIPTION_TOGGLE_LENGTH = 280; function isCapError(payload: unknown): boolean { return Boolean(payload && typeof payload === "object" && "code" in payload && (payload as { code?: unknown }).code === "ACTIVE_GOAL_LIMIT_EXCEEDED"); } export function GoalsView({ initialGoals }: GoalsViewProps) { const [goals, setGoals] = useState(() => initialGoals ?? []); const [loading, setLoading] = useState(initialGoals === undefined); const [errorMessage, setErrorMessage] = useState(null); const [isAddFormOpen, setIsAddFormOpen] = useState(false); const [addTitle, setAddTitle] = useState(""); const [addDescription, setAddDescription] = useState(""); const [addError, setAddError] = useState(null); const [isCreating, setIsCreating] = useState(false); const [isDraftingDescription, setIsDraftingDescription] = useState(false); const [editGoalId, setEditGoalId] = useState(null); const [editTitle, setEditTitle] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editError, setEditError] = useState(null); const [isSavingEdit, setIsSavingEdit] = useState(false); const [expandedGoalDescriptions, setExpandedGoalDescriptions] = useState>(() => new Set()); useEffect(() => { if (initialGoals !== undefined) { return; } let active = true; const loadGoals = async () => { try { setLoading(true); setErrorMessage(null); const response = await fetch("/api/goals"); if (!response.ok) { throw new Error(`Failed to load goals (${response.status})`); } const payload = (await response.json()) as { goals?: Goal[] }; if (!active) { return; } setGoals(Array.isArray(payload.goals) ? payload.goals : []); } catch { if (!active) { return; } setErrorMessage("Unable to load goals right now. Please try again."); } finally { if (active) { setLoading(false); } } }; void loadGoals(); return () => { active = false; }; }, [initialGoals]); const activeCount = useMemo(() => goals.filter((goal) => goal.status === "active").length, [goals]); const showWarning = activeCount >= WARNING_THRESHOLD && activeCount <= MAX_ACTIVE_GOALS; function openAddForm() { setErrorMessage(null); setAddError(null); setIsAddFormOpen(true); } function openEdit(goal: Goal) { setEditGoalId(goal.id); setEditTitle(goal.title); setEditDescription(goal.description ?? ""); setEditError(null); } function cancelEdit() { setEditGoalId(null); setEditTitle(""); setEditDescription(""); setEditError(null); } function closeAddForm() { setIsAddFormOpen(false); setAddTitle(""); setAddDescription(""); setAddError(null); setIsDraftingDescription(false); } async function draftAddGoalDescription() { const title = addTitle.trim(); if (!title) { setAddError("Title is required."); return; } try { setIsDraftingDescription(true); setAddError(null); const description = await draftGoalDescription(title); setAddDescription(description); } catch (error) { setAddError(getRefineErrorMessage(error)); } finally { setIsDraftingDescription(false); } } async function submitAddGoal() { const title = addTitle.trim(); if (!title) { setAddError("Title is required."); return; } try { setIsCreating(true); setAddError(null); setErrorMessage(null); const response = await fetch("/api/goals", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ title, description: addDescription, }), }); if (response.ok) { const createdGoal = (await response.json()) as Goal; setGoals((current) => [...current, createdGoal]); closeAddForm(); return; } let payload: unknown = null; try { payload = await response.json(); } catch { payload = null; } if (response.status === 409 && isCapError(payload)) { setErrorMessage(CAP_ERROR_MESSAGE); return; } setAddError("Unable to create goal right now. Please try again."); } catch { setAddError("Unable to create goal right now. Please try again."); } finally { setIsCreating(false); } } async function saveEditGoal() { if (!editGoalId) { return; } const title = editTitle.trim(); if (!title) { setEditError("Title is required."); return; } try { setIsSavingEdit(true); setEditError(null); const response = await fetch(`/api/goals/${editGoalId}`, { method: "PATCH", headers: { "content-type": "application/json", }, body: JSON.stringify({ title, description: editDescription }), }); if (!response.ok) { throw new Error(`Failed to update goal (${response.status})`); } const updatedGoal = (await response.json()) as Goal; setGoals((current) => current.map((goal) => (goal.id === updatedGoal.id ? updatedGoal : goal))); cancelEdit(); } catch { setEditError("Unable to save goal right now. Please try again."); } finally { setIsSavingEdit(false); } } function isDescriptionToggleVisible(description: string): boolean { return description.length > GOAL_DESCRIPTION_TOGGLE_LENGTH || description.includes("\n"); } function toggleGoalDescription(goalId: string) { setExpandedGoalDescriptions((current) => { const next = new Set(current); if (next.has(goalId)) { next.delete(goalId); } else { next.add(goalId); } return next; }); } async function updateGoalArchiveStatus(goal: Goal) { const endpoint = goal.status === "active" ? `/api/goals/${goal.id}/archive` : `/api/goals/${goal.id}/unarchive`; try { setErrorMessage(null); const response = await fetch(endpoint, { method: "POST", }); if (response.ok) { const updatedGoal = (await response.json()) as Goal; setGoals((current) => current.map((entry) => (entry.id === updatedGoal.id ? updatedGoal : entry))); return; } let payload: unknown = null; try { payload = await response.json(); } catch { payload = null; } if (response.status === 409 && isCapError(payload)) { setErrorMessage(CAP_ERROR_MESSAGE); return; } setErrorMessage("Unable to update goal status right now. Please try again."); } catch { setErrorMessage("Unable to update goal status right now. Please try again."); } } return (

Goals

{activeCount} active goals

{isAddFormOpen ? (
setAddTitle(event.target.value)} data-testid="goals-form-title" />