@@ -89,16 +89,44 @@ export function StatsPage({ onNavigate, from = "graph" }: { onNavigate: (v: "her
8989 const [ filter , setFilter ] = useState < Kind | "all" > ( "all" )
9090 const [ openId , setOpenId ] = useState < string | null > ( null )
9191
92+ // Live PR state from GitHub's public API; the JSON status is the fallback.
93+ const [ live , setLive ] = useState < Record < string , Status > > ( { } )
94+ useEffect ( ( ) => {
95+ let cancelled = false
96+ Promise . all (
97+ items . filter ( i => i . kind === "pr" ) . map ( async ( it ) => {
98+ const m = it . url . match ( / g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ p u l l \/ ( \d + ) / )
99+ if ( ! m ) return null
100+ try {
101+ const r = await fetch ( `https://api.github.com/repos/${ m [ 1 ] } /${ m [ 2 ] } /pulls/${ m [ 3 ] } ` )
102+ if ( ! r . ok ) return null
103+ const d = await r . json ( )
104+ const s : Status = d . merged ? "merged" : d . state === "open" ? "open" : "closed"
105+ return [ it . id , s ] as const
106+ } catch { return null }
107+ } )
108+ ) . then ( rows => {
109+ if ( cancelled ) return
110+ const map : Record < string , Status > = { }
111+ for ( const row of rows ) if ( row ) map [ row [ 0 ] ] = row [ 1 ]
112+ setLive ( map )
113+ } )
114+ return ( ) => { cancelled = true }
115+ } , [ ] )
116+
117+ const eff = ( i : Item ) : Status => live [ i . id ] ?? i . status
118+
92119 const stats = useMemo ( ( ) => {
93120 const by = ( k : Kind ) => items . filter ( i => i . kind === k ) . length
94- const prMerged = items . filter ( i => i . kind === "pr" && i . status === "merged" ) . length
95- const prOpen = items . filter ( i => i . kind === "pr" && i . status === "open" ) . length
121+ const prMerged = items . filter ( i => i . kind === "pr" && eff ( i ) === "merged" ) . length
122+ const prOpen = items . filter ( i => i . kind === "pr" && eff ( i ) === "open" ) . length
96123 const repos = new Set ( items . map ( i => i . repo ) ) . size
97124 return {
98125 total : items . length , pr : by ( "pr" ) , review : by ( "review" ) , issue : by ( "issue" ) ,
99126 prMerged, prOpen, repos,
100127 }
101- } , [ ] )
128+ // eslint-disable-next-line react-hooks/exhaustive-deps
129+ } , [ live ] )
102130
103131 const filtered = useMemo ( ( ) => {
104132 const list = filter === "all" ? items : items . filter ( i => i . kind === filter )
@@ -117,7 +145,7 @@ export function StatsPage({ onNavigate, from = "graph" }: { onNavigate: (v: "her
117145 sub ?: string ; filter ?: Kind | "all"
118146 } > = [
119147 { label : "Contributions" , value : stats . total , icon : LayoutGrid , color : CYAN , sub : `${ stats . repos } repositories` , filter : "all" } ,
120- { label : "Pull Requests" , value : stats . pr , icon : GitPullRequest , color : KIND . pr . color , sub : `${ items . filter ( i => i . kind === "pr" && i . status === " merged" ) . length } merged · ${ items . filter ( i => i . kind === "pr" && i . status === "open" ) . length } open ` , filter : "pr" } ,
148+ { label : "Pull Requests" , value : stats . pr , icon : GitPullRequest , color : KIND . pr . color , sub : `${ stats . prMerged } merged${ stats . prOpen ? ` · ${ stats . prOpen } open` : "" } ` , filter : "pr" } ,
121149 { label : "Reviews" , value : stats . review , icon : MessageSquareText , color : KIND . review . color , sub : "on real OSS PRs" , filter : "review" } ,
122150 { label : "Issues & Triage" , value : stats . issue , icon : CircleDot , color : KIND . issue . color , sub : "reported & reproduced" , filter : "issue" } ,
123151 ]
@@ -260,7 +288,7 @@ export function StatsPage({ onNavigate, from = "graph" }: { onNavigate: (v: "her
260288 { filtered . map ( ( it , i ) => {
261289 const k = KIND [ it . kind ]
262290 const Icon = k . icon
263- const st = STATUS [ it . status ]
291+ const st = STATUS [ eff ( it ) ]
264292 const isOpen = openId === it . id
265293 return (
266294 < motion . div
0 commit comments