import { useCallback, useRef, useState } from 'react'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; import { Plus, Trash2, Flame } from 'lucide-react'; import { Widget } from '../Widget'; type Goal = { id: string; text: string; done: boolean; }; type DailyGoalsState = { goals: Goal[]; lastDate: string; streak: number; lastCompletedDate: string | null; }; const USER_STATE_KEY = ['USER_STATE']; const GOALS_STATE_KEY = 'daily-goals'; const todayStr = () => new Date().toISOString().slice(0, 10); const yesterdayStr = () => { const d = new Date(); d.setDate(d.getDate() - 1); return d.toISOString().slice(0, 10); }; function useDailyGoalsState() { const client = useClient(); const { isAuthenticated } = useAuth(); const queryClient = useQueryClient(); const clientRef = useRef(client); clientRef.current = client; const { data: userState = {} } = useQuery>({ queryKey: USER_STATE_KEY, enabled: isAuthenticated, queryFn: () => client.get('/user/state'), staleTime: Infinity, }); const stored = userState[GOALS_STATE_KEY] as DailyGoalsState | undefined; const today = todayStr(); const state: DailyGoalsState = (() => { if (!stored) return { goals: [], lastDate: today, streak: 0, lastCompletedDate: null }; if (stored.lastDate === today) return stored; // New day — check if yesterday had all goals completed for streak const allDoneYesterday = stored.goals.length > 0 && stored.goals.every((g) => g.done); const wasYesterday = stored.lastDate === yesterdayStr(); const streak = allDoneYesterday && wasYesterday ? stored.streak + 1 : allDoneYesterday ? 1 : 0; const lastCompletedDate = allDoneYesterday ? stored.lastDate : stored.lastCompletedDate; return { goals: stored.goals.map((g) => ({ ...g, done: false })), lastDate: today, streak, lastCompletedDate, }; })(); const setState = useCallback( (update: DailyGoalsState | ((prev: DailyGoalsState) => DailyGoalsState)) => { const currentUserState = queryClient.getQueryData>(USER_STATE_KEY) ?? {}; const current = (currentUserState[GOALS_STATE_KEY] as DailyGoalsState | undefined) ?? state; const next = typeof update === 'function' ? update(current) : update; queryClient.setQueryData(USER_STATE_KEY, { ...currentUserState, [GOALS_STATE_KEY]: next }); clientRef.current.patch('/user/state', { [GOALS_STATE_KEY]: next }).catch(() => {}); }, [queryClient, state], ); return [state, setState] as const; } let nextGoalId = Date.now(); export const DailyGoals = () => { const [state, setState] = useDailyGoalsState(); const [newGoal, setNewGoal] = useState(''); const inputRef = useRef(null); const addGoal = () => { const text = newGoal.trim(); if (!text) return; nextGoalId++; setState((s) => ({ ...s, goals: [...s.goals, { id: String(nextGoalId), text, done: false }], })); setNewGoal(''); inputRef.current?.focus(); }; const toggleGoal = (id: string) => { setState((s) => ({ ...s, goals: s.goals.map((g) => (g.id === id ? { ...g, done: !g.done } : g)), })); }; const removeGoal = (id: string) => { setState((s) => ({ ...s, goals: s.goals.filter((g) => g.id !== id), })); }; const { goals, streak } = state; const doneCount = goals.filter((g) => g.done).length; return (
{streak > 0 && (
{streak} day streak
)} {goals.length > 0 && (
{goals.map((goal) => (
{goal.text}
))}
)} {goals.length > 0 && (
{doneCount}/{goals.length} completed
)}
setNewGoal(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Enter') { ev.preventDefault(); addGoal(); } }} className="flex-1 rounded-md border border-border bg-background px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" />
); };