this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix ESLint issues in React frontend

- Add explicit type annotations for props and state
- Use consistent type imports
- Fix strict boolean expressions in conditionals
- Add proper error handling in async operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice c5771656 5c32c88c

+104 -59
+12 -7
src/web/app/components/BragSummary.tsx
··· 11 11 12 12 function parseSummary(summary: string): DailySummary | null { 13 13 try { 14 - const parsed = JSON.parse(summary); 15 - if (parsed.projects && Array.isArray(parsed.projects)) { 14 + const parsed = JSON.parse(summary) as unknown; 15 + if ( 16 + typeof parsed === 'object' && 17 + parsed !== null && 18 + 'projects' in parsed && 19 + Array.isArray(parsed.projects) 20 + ) { 16 21 return parsed as DailySummary; 17 22 } 18 23 } catch { ··· 28 33 29 34 const handleCopy = () => { 30 35 // Copy as readable text 31 - const text = parsed 36 + const text = parsed !== null 32 37 ? parsed.projects.map(p => `${p.name}: ${p.summary}`).join('\n') 33 38 : summary; 34 - navigator.clipboard.writeText(text); 39 + void navigator.clipboard.writeText(text); 35 40 setCopied(true); 36 - setTimeout(() => setCopied(false), 2000); 41 + setTimeout(() => { setCopied(false); }, 2000); 37 42 }; 38 43 39 - if (!summary) return null; 44 + if (summary.length === 0) return null; 40 45 41 46 return ( 42 47 <div className="relative group rounded-lg p-4 mb-4 bg-gradient-to-br from-indigo-50 to-blue-50 border border-indigo-100 shadow-sm"> ··· 61 66 {parsed.projects.map((project, i) => ( 62 67 <li key={i} className="text-sm"> 63 68 <span className="font-semibold text-slate-800">{project.name}</span> 64 - {project.isNew && ( 69 + {project.isNew === true && ( 65 70 <span className="ml-1.5 px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded">NEW</span> 66 71 )} 67 72 <span className="text-slate-800">:</span>{' '}
+11 -7
src/web/app/components/DayList.tsx
··· 41 41 42 42 for (const day of days) { 43 43 const date = new Date(day.date + 'T12:00:00'); // Noon to avoid timezone issues 44 - const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 44 + const monthKey = `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}`; 45 45 const weekNum = getWeekNumber(date); 46 - const weekKey = `${date.getFullYear()}-W${String(weekNum).padStart(2, '0')}`; 46 + const weekKey = `${String(date.getFullYear())}-W${String(weekNum).padStart(2, '0')}`; 47 47 48 48 if (!grouped.has(monthKey)) { 49 49 grouped.set(monthKey, new Map()); 50 50 } 51 - const monthMap = grouped.get(monthKey)!; 51 + const monthMap = grouped.get(monthKey); 52 + if (monthMap === undefined) continue; 52 53 if (!monthMap.has(weekKey)) { 53 54 monthMap.set(weekKey, []); 54 55 } 55 - monthMap.get(weekKey)!.push(day); 56 + const weekDays = monthMap.get(weekKey); 57 + if (weekDays !== undefined) { 58 + weekDays.push(day); 59 + } 56 60 } 57 61 58 62 const result: GroupedDays[] = []; ··· 127 131 <div className="flex items-center gap-3 text-xs text-slate-500"> 128 132 <div className="flex items-center gap-1.5"> 129 133 <Layers size={14} /> 130 - <span>{day.projectCount} Projects</span> 134 + <span>{String(day.projectCount)} Projects</span> 131 135 </div> 132 136 <div className="flex items-center gap-1.5"> 133 137 <Clock size={14} /> 134 - <span>{day.sessionCount} Sessions</span> 138 + <span>{String(day.sessionCount)} Sessions</span> 135 139 </div> 136 140 </div> 137 141 </div> ··· 146 150 const { days, loading, error } = useDays(); 147 151 148 152 if (loading) return <div className="text-center py-20 text-slate-400">Loading your history...</div>; 149 - if (error) return <div className="text-center py-20 text-red-500">Error: {error}</div>; 153 + if (error !== null) return <div className="text-center py-20 text-red-500">Error: {error}</div>; 150 154 151 155 if (days.length === 0) { 152 156 return (
+31 -11
src/web/app/components/DayView.tsx
··· 5 5 import BragSummary from './BragSummary'; 6 6 import ProjectCard from './ProjectCard'; 7 7 8 + interface ParsedProject { 9 + name: string; 10 + isNew?: boolean; 11 + } 12 + 13 + interface ParsedBragSummary { 14 + projects: ParsedProject[]; 15 + } 16 + 8 17 // Parse brag summary to extract isNew flags by project name 9 18 function parseNewProjects(bragSummary: string | undefined): Set<string> { 10 - if (!bragSummary) return new Set(); 19 + if (bragSummary === undefined) return new Set(); 11 20 try { 12 - const parsed = JSON.parse(bragSummary); 13 - if (parsed.projects && Array.isArray(parsed.projects)) { 14 - return new Set( 15 - parsed.projects 16 - .filter((p: { isNew?: boolean }) => p.isNew) 17 - .map((p: { name: string }) => p.name) 18 - ); 21 + const parsed = JSON.parse(bragSummary) as unknown; 22 + if ( 23 + typeof parsed === 'object' && 24 + parsed !== null && 25 + 'projects' in parsed && 26 + Array.isArray(parsed.projects) 27 + ) { 28 + const typedParsed = parsed as ParsedBragSummary; 29 + const names: string[] = []; 30 + for (const p of typedParsed.projects) { 31 + if (p.isNew === true) { 32 + names.push(p.name); 33 + } 34 + } 35 + return new Set(names); 19 36 } 20 - } catch {} 37 + } catch { 38 + // JSON parsing failed, return empty set 39 + } 21 40 return new Set(); 22 41 } 23 42 ··· 31 50 ); 32 51 33 52 if (loading) return <div className="text-center py-20 text-slate-400">Loading day details...</div>; 34 - if (error || !day) return <div className="text-center py-20 text-red-500">Error: {error || 'Day not found'}</div>; 53 + if (error !== null) return <div className="text-center py-20 text-red-500">Error: {error}</div>; 54 + if (day === null) return <div className="text-center py-20 text-red-500">Error: Day not found</div>; 35 55 36 56 const formattedDate = new Date(day.date).toLocaleDateString(undefined, { 37 57 weekday: 'long', ··· 50 70 <h1 className="text-2xl font-bold text-slate-900">{formattedDate}</h1> 51 71 </div> 52 72 53 - {day.bragSummary && <BragSummary summary={day.bragSummary} />} 73 + {day.bragSummary !== undefined && day.bragSummary.length > 0 && <BragSummary summary={day.bragSummary} />} 54 74 55 75 <div className="space-y-3"> 56 76 {day.projects.map((project) => (
+12 -7
src/web/app/components/Layout.tsx
··· 7 7 const { stats, refetch: refetchStats } = useStats(); 8 8 const { refresh, refreshing } = useRefresh(); 9 9 10 - const handleRefresh = async () => { 11 - await refresh(); 12 - refetchStats(); 13 - window.location.reload(); 10 + const handleRefresh = () => { 11 + void refresh() 12 + .then(() => { 13 + void refetchStats(); 14 + window.location.reload(); 15 + }) 16 + .catch((err: unknown) => { 17 + console.error('Refresh failed:', err); 18 + }); 14 19 }; 15 20 16 21 return ( ··· 33 38 <span className="hidden sm:inline">Projects</span> 34 39 </Link> 35 40 36 - {stats && ( 41 + {stats !== null && ( 37 42 <div className="hidden sm:flex gap-4 text-sm text-slate-500"> 38 43 <div className="flex items-center gap-1.5"> 39 44 <Activity size={16} className="text-blue-500" /> 40 - <span className="font-medium text-slate-700">{stats.totalSessions}</span> sessions 45 + <span className="font-medium text-slate-700">{String(stats.totalSessions)}</span> sessions 41 46 </div> 42 47 <div className="flex items-center gap-1.5"> 43 48 <Calendar size={16} className="text-blue-500" /> 44 - <span className="font-medium text-slate-700">{stats.totalDays}</span> days 49 + <span className="font-medium text-slate-700">{String(stats.totalDays)}</span> days 45 50 </div> 46 51 </div> 47 52 )}
+1 -1
src/web/app/components/ProjectCard.tsx
··· 51 51 <Folder size={16} /> 52 52 </div> 53 53 <h3 className="text-sm font-bold text-slate-800">{project.name}</h3> 54 - {isNew && ( 54 + {isNew === true && ( 55 55 <span className="px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded">NEW</span> 56 56 )} 57 57 </div>
+15 -13
src/web/app/components/ProjectList.tsx
··· 52 52 return ( 53 53 <button 54 54 key={s} 55 - onClick={() => onSelect(s)} 55 + onClick={() => { onSelect(s); }} 56 56 className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 hover:bg-gray-50 ${ 57 57 s === status ? 'bg-gray-50' : '' 58 58 }`} ··· 73 73 const [showDropdown, setShowDropdown] = useState(false); 74 74 const { updateStatus } = useUpdateProjectStatus(); 75 75 76 - const handleStatusSelect = async (newStatus: ProjectStatus) => { 76 + const handleStatusSelect = (newStatus: ProjectStatus) => { 77 77 setShowDropdown(false); 78 78 if (newStatus !== project.status) { 79 - await updateStatus(project.path, newStatus); 80 - onStatusChange(); 79 + void (async () => { 80 + await updateStatus(project.path, newStatus); 81 + onStatusChange(); 82 + })(); 81 83 } 82 84 }; 83 85 ··· 96 98 <span className="text-slate-300">|</span> 97 99 <span className={`flex items-center gap-1 ${isStale ? 'text-amber-600' : ''}`}> 98 100 {isStale && <Clock size={12} />} 99 - {project.daysSinceLastSession === 0 ? 'Today' : `${project.daysSinceLastSession}d ago`} 101 + {project.daysSinceLastSession === 0 ? 'Today' : `${String(project.daysSinceLastSession)}d ago`} 100 102 </span> 101 103 </div> 102 104 </div> ··· 104 106 105 107 <StatusBadge 106 108 status={project.status} 107 - onClick={() => setShowDropdown(!showDropdown)} 109 + onClick={() => { setShowDropdown(!showDropdown); }} 108 110 showDropdown={showDropdown} 109 111 onSelect={handleStatusSelect} 110 - onClose={() => setShowDropdown(false)} 112 + onClose={() => { setShowDropdown(false); }} 111 113 /> 112 114 </div> 113 115 ); ··· 137 139 return <div className="text-center py-20 text-slate-400">Loading projects...</div>; 138 140 } 139 141 140 - if (error) { 142 + if (error !== null) { 141 143 return <div className="text-center py-20 text-red-500">Error: {error}</div>; 142 144 } 143 145 ··· 154 156 label="All" 155 157 count={allProjects.length} 156 158 isActive={filter === 'all'} 157 - onClick={() => setFilter('all')} 159 + onClick={() => { setFilter('all'); }} 158 160 /> 159 161 {ALL_STATUSES.map((s) => ( 160 162 <FilterTab ··· 162 164 label={STATUS_CONFIG[s].label} 163 165 count={counts[s]} 164 166 isActive={filter === s} 165 - onClick={() => setFilter(s)} 167 + onClick={() => { setFilter(s); }} 166 168 /> 167 169 ))} 168 170 </div> ··· 170 172 {/* Stale projects callout */} 171 173 {staleCount > 0 && filter !== 'shipped' && filter !== 'ready_to_ship' && filter !== 'abandoned' && filter !== 'ignore' && filter !== 'one_off' && filter !== 'experiment' && ( 172 174 <div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800"> 173 - <strong>{staleCount} project{staleCount > 1 ? 's' : ''}</strong> marked "In Progress" but untouched for 30+ days. 175 + <strong>{String(staleCount)} project{staleCount > 1 ? 's' : ''}</strong> marked "In Progress" but untouched for 30+ days. 174 176 </div> 175 177 )} 176 178 ··· 181 183 ) : ( 182 184 <div className="space-y-2"> 183 185 {projects.map((project) => ( 184 - <ProjectRow key={project.path} project={project} onStatusChange={refetch} /> 186 + <ProjectRow key={project.path} project={project} onStatusChange={() => { void refetch(); }} /> 185 187 ))} 186 188 </div> 187 189 )} ··· 212 214 {label} 213 215 {count > 0 && ( 214 216 <span className={`ml-1.5 ${isActive ? 'text-slate-500' : 'text-slate-400'}`}> 215 - {count} 217 + {String(count)} 216 218 </span> 217 219 )} 218 220 </button>
+4 -3
src/web/app/hooks/useProjects.ts
··· 9 9 const fetchProjects = useCallback(async () => { 10 10 try { 11 11 setLoading(true); 12 - const url = status ? `/api/projects?status=${status}` : '/api/projects'; 12 + const url = status !== undefined ? `/api/projects?status=${status}` : '/api/projects'; 13 13 const res = await fetch(url); 14 14 if (!res.ok) throw new Error('Failed to fetch projects'); 15 - setProjects(await res.json()); 15 + const data: unknown = await res.json(); 16 + setProjects(data as ProjectListItem[]); 16 17 } catch (err) { 17 18 setError(err instanceof Error ? err.message : 'Unknown error'); 18 19 } finally { ··· 21 22 }, [status]); 22 23 23 24 useEffect(() => { 24 - fetchProjects(); 25 + void fetchProjects(); 25 26 }, [fetchProjects]); 26 27 27 28 return { projects, loading, error, refetch: fetchProjects };
+12 -9
src/web/app/hooks/useWorklog.ts
··· 49 49 setLoading(true); 50 50 const res = await fetch('/api/days'); 51 51 if (!res.ok) throw new Error('Failed to fetch days'); 52 - const data = await res.json(); 53 - setDays(data); 52 + const data: unknown = await res.json(); 53 + setDays(data as DayListItem[]); 54 54 } catch (err) { 55 55 setError(err instanceof Error ? err.message : 'Unknown error'); 56 56 } finally { ··· 58 58 } 59 59 }, []); 60 60 61 - useEffect(() => { fetchDays(); }, [fetchDays]); 61 + useEffect(() => { void fetchDays(); }, [fetchDays]); 62 62 63 63 return { days, loading, error, refetch: fetchDays }; 64 64 } ··· 69 69 const [error, setError] = useState<string | null>(null); 70 70 71 71 useEffect(() => { 72 - if (!date) return; 72 + if (date === undefined) return; 73 73 const fetchDay = async () => { 74 74 try { 75 75 setLoading(true); 76 76 const res = await fetch(`/api/days/${date}`); 77 77 if (!res.ok) throw new Error('Failed to fetch day details'); 78 - const data = await res.json(); 79 - setDay(data); 78 + const data: unknown = await res.json(); 79 + setDay(data as DayDetail); 80 80 } catch (err) { 81 81 setError(err instanceof Error ? err.message : 'Unknown error'); 82 82 } finally { 83 83 setLoading(false); 84 84 } 85 85 }; 86 - fetchDay(); 86 + void fetchDay(); 87 87 }, [date]); 88 88 89 89 return { day, loading, error }; ··· 95 95 const fetchStats = useCallback(async () => { 96 96 try { 97 97 const res = await fetch('/api/stats'); 98 - if (res.ok) setStats(await res.json()); 98 + if (res.ok) { 99 + const data: unknown = await res.json(); 100 + setStats(data as Stats); 101 + } 99 102 } catch (e) { 100 103 console.error(e); 101 104 } 102 105 }, []); 103 106 104 - useEffect(() => { fetchStats(); }, [fetchStats]); 107 + useEffect(() => { void fetchStats(); }, [fetchStats]); 105 108 return { stats, refetch: fetchStats }; 106 109 } 107 110
+6 -1
src/web/app/main.tsx
··· 3 3 import App from './App'; 4 4 import './globals.css'; 5 5 6 - ReactDOM.createRoot(document.getElementById('root')!).render( 6 + const rootElement = document.getElementById('root'); 7 + if (rootElement === null) { 8 + throw new Error('Root element not found'); 9 + } 10 + 11 + ReactDOM.createRoot(rootElement).render( 7 12 <React.StrictMode> 8 13 <App /> 9 14 </React.StrictMode>