From b912fd74be47c5328e7ad302ee9bba5eda2d9c07 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 22:25:35 -0500 Subject: [PATCH] =?UTF-8?q?refactor(ts):=20RoadmapPage=20=E2=86=92=20TypeS?= =?UTF-8?q?cript=20(issues=20board=20+=20dev=20activity=20log)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{RoadmapPage.jsx => RoadmapPage.tsx} | 124 +++++++++++++----- 1 file changed, 92 insertions(+), 32 deletions(-) rename client/pages/{RoadmapPage.jsx => RoadmapPage.tsx} (85%) diff --git a/client/pages/RoadmapPage.jsx b/client/pages/RoadmapPage.tsx similarity index 85% rename from client/pages/RoadmapPage.jsx rename to client/pages/RoadmapPage.tsx index 00493fa..bc2655d 100644 --- a/client/pages/RoadmapPage.jsx +++ b/client/pages/RoadmapPage.tsx @@ -20,10 +20,64 @@ import { RefreshCw, } from 'lucide-react'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; + +/* ─── Types ──────────────────────────────────────────────────────────────── */ + +interface IssueLabel { + id: number | string; + name: string; + color: string; +} + +interface Issue { + id: number | string; + number: number; + title: string; + body?: string | null; + html_url: string; + created_at: string; + comments?: number; + labels?: IssueLabel[]; +} + +interface Lane { + key: string; + emoji: string; + label: string; + borderColor: string; + textColor: string; + badgeClass: string; +} + +interface Agent { + name?: string; + status?: string; + time?: string; +} + +interface DevLogEntryData { + version?: string; + date?: string; + status?: string; + filesModified?: string[]; + agents?: Agent[]; + workCompleted?: string[]; +} + +interface RoadmapData { + issues?: Issue[]; + fetchedAt?: string | null; + stale?: boolean; +} + +interface DevLogData { + entries?: DevLogEntryData[]; +} /* ─── Priority lanes ─────────────────────────────────────────────────────── */ -const PRIORITY_LANES = [ +const PRIORITY_LANES: Lane[] = [ { key: 'critical', emoji: '🔴', label: 'CRITICAL', borderColor: 'border-t-red-500', textColor: 'text-red-500', badgeClass: 'bg-red-500/15 text-red-500 border-red-500/20' }, { key: 'high', emoji: '🟠', label: 'HIGH', borderColor: 'border-t-orange-500', textColor: 'text-orange-500', badgeClass: 'bg-orange-500/15 text-orange-500 border-orange-500/20' }, { key: 'medium', emoji: '🟡', label: 'MEDIUM', borderColor: 'border-t-yellow-500', textColor: 'text-yellow-500', badgeClass: 'bg-yellow-500/15 text-yellow-500 border-yellow-500/20' }, @@ -33,7 +87,7 @@ const PRIORITY_LANES = [ /* ─── Helpers ────────────────────────────────────────────────────────────── */ -function priorityFromLabels(labels = []) { +function priorityFromLabels(labels: IssueLabel[] = []): string { for (const l of labels) { if (l.name === 'priority:critical') return 'critical'; if (l.name === 'priority:high') return 'high'; @@ -44,11 +98,11 @@ function priorityFromLabels(labels = []) { return 'low'; } -function cleanTitle(title) { +function cleanTitle(title: string): string { return title.replace(/^(CRITICAL|HIGH|MEDIUM|LOW|NICE[\s-]TO[\s-]HAVE)\s*:\s*/i, '').trim(); } -function stripMarkdown(text) { +function stripMarkdown(text?: string | null): string { if (!text) return ''; return text .replace(/#{1,6}\s+[^\n]*/g, '') @@ -64,7 +118,7 @@ function stripMarkdown(text) { .trim(); } -function timeAgo(dateStr) { +function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); @@ -76,7 +130,7 @@ function timeAgo(dateStr) { return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } -function labelStyle(hex) { +function labelStyle(hex: string): React.CSSProperties { const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); @@ -89,7 +143,7 @@ function labelStyle(hex) { /* ─── Label chip ─────────────────────────────────────────────────────────── */ -function LabelChip({ label }) { +function LabelChip({ label }: { label: IssueLabel }) { return ( {timeAgo(issue.created_at)} - {issue.comments > 0 && ( + {(issue.comments ?? 0) > 0 && ( {issue.comments} @@ -163,7 +217,7 @@ function IssueCard({ issue }) { /* ─── Priority lane ──────────────────────────────────────────────────────── */ -function PriorityLane({ lane, items }) { +function PriorityLane({ lane, items }: { lane: Lane; items: Issue[] }) { return (
[l.key, 0])); +function StatsBar({ issues, fetchedAt, stale, onRefresh, refreshing }: { + issues: Issue[]; + fetchedAt?: string | null; + stale?: boolean; + onRefresh: () => void; + refreshing: boolean; +}) { + const counts: Record = Object.fromEntries(PRIORITY_LANES.map(l => [l.key, 0])); issues.forEach(issue => { const p = priorityFromLabels(issue.labels); counts[p] = (counts[p] || 0) + 1; }); - const nonEmpty = PRIORITY_LANES.filter(l => counts[l.key] > 0); + const nonEmpty = PRIORITY_LANES.filter(l => (counts[l.key] ?? 0) > 0); return (
@@ -236,7 +296,7 @@ function StatsBar({ issues, fetchedAt, stale, onRefresh, refreshing }) { /* ─── Dev log entry ──────────────────────────────────────────────────────── */ -function DevLogEntry({ entry }) { +function DevLogEntry({ entry }: { entry: DevLogEntryData }) { const [open, setOpen] = useState(false); return ( @@ -262,10 +322,10 @@ function DevLogEntry({ entry }) { {entry.status} )} - {entry.filesModified?.length > 0 && ( + {(entry.filesModified?.length ?? 0) > 0 && ( - {entry.filesModified.length} files + {entry.filesModified?.length} files )} @@ -275,11 +335,11 @@ function DevLogEntry({ entry }) {
- {entry.agents?.length > 0 && ( + {(entry.agents?.length ?? 0) > 0 && (

Agents

- {entry.agents.map((agent, idx) => ( + {entry.agents?.map((agent, idx) => ( )} - {entry.filesModified?.length > 0 && ( + {(entry.filesModified?.length ?? 0) > 0 && (

Files Modified

- {entry.filesModified.map((file, idx) => ( + {entry.filesModified?.map((file, idx) => ( {file} @@ -306,11 +366,11 @@ function DevLogEntry({ entry }) {
)} - {entry.workCompleted?.length > 0 && ( + {(entry.workCompleted?.length ?? 0) > 0 && (

Work Completed

    - {entry.workCompleted.map((work, idx) => ( + {entry.workCompleted?.map((work, idx) => (
  • {work} @@ -330,20 +390,20 @@ function DevLogEntry({ entry }) { /* ─── Main page ──────────────────────────────────────────────────────────── */ export default function RoadmapPage() { - const [roadmapData, setRoadmapData] = useState(null); - const [devLogData, setDevLogData] = useState(null); + const [roadmapData, setRoadmapData] = useState(null); + const [devLogData, setDevLogData] = useState(null); const [roadmapLoading, setRoadmapLoading] = useState(true); const [roadmapRefreshing,setRoadmapRefreshing]= useState(false); const [devLogLoading, setDevLogLoading] = useState(false); - const [roadmapError, setRoadmapError] = useState(null); - const [devLogError, setDevLogError] = useState(null); + const [roadmapError, setRoadmapError] = useState(null); + const [devLogError, setDevLogError] = useState(null); useEffect(() => { let cancelled = false; setRoadmapLoading(true); api.roadmap() - .then(data => { if (!cancelled) setRoadmapData(data); }) - .catch(err => { if (!cancelled) setRoadmapError(err.message || 'Failed to load issues'); }) + .then(data => { if (!cancelled) setRoadmapData(data as RoadmapData); }) + .catch(err => { if (!cancelled) setRoadmapError(errMessage(err, 'Failed to load issues')); }) .finally(() => { if (!cancelled) setRoadmapLoading(false); }); return () => { cancelled = true; }; }, []); @@ -351,7 +411,7 @@ export default function RoadmapPage() { const handleRefresh = () => { setRoadmapRefreshing(true); api.roadmap(true) - .then(data => setRoadmapData(data)) + .then(data => setRoadmapData(data as RoadmapData)) .catch(() => {}) .finally(() => setRoadmapRefreshing(false)); }; @@ -361,8 +421,8 @@ export default function RoadmapPage() { let cancelled = false; setDevLogLoading(true); api.devLog() - .then(data => { if (!cancelled) setDevLogData(data); }) - .catch(err => { if (!cancelled) setDevLogError(err.message || 'Failed to load activity log'); }) + .then(data => { if (!cancelled) setDevLogData(data as DevLogData); }) + .catch(err => { if (!cancelled) setDevLogError(errMessage(err, 'Failed to load activity log')); }) .finally(() => { if (!cancelled) setDevLogLoading(false); }); return () => { cancelled = true; }; }, [devLogData, devLogLoading]); @@ -480,7 +540,7 @@ export default function RoadmapPage() {
) : devLogData ? (
- {devLogData.entries.map((entry, idx) => ( + {devLogData.entries?.map((entry, idx) => ( ))}