1- import { useState , useEffect } from "react" ;
2- import { Octokit } from "@octokit/rest " ;
1+ import { useEffect , useState } from "react" ;
2+ import { type DisplayEvent } from "../pages/api/github " ;
33
4- type DisplayEvent = {
5- id : string ;
6- actor : { login : string ; avatar_url : string } ;
7- verb : string ;
8- object : string ;
9- description ?: string ;
10- repo : string ;
11- url : string ;
12- timestamp : string ;
13- } ;
14-
15- const octokit = new Octokit ( ) ;
16- const detailCache = new Map < string , { title : string ; body : string ; merged ?: boolean ; url : string } > ( ) ;
17-
18- const cleanDescription = ( text : string | null ) => {
19- if ( ! text ) return "" ;
20- return text . replace ( / [ # * ` _ ] / g, "" ) . trim ( ) ;
21- } ;
22-
23- const formatDate = ( isoString : string ) => {
24- return new Date ( isoString ) . toLocaleDateString ( undefined , {
25- month : "short" ,
26- day : "numeric" ,
27- hour : "numeric" ,
28- minute : "numeric"
29- } ) ;
30- } ;
31-
32- const fetchDetails = async ( apiUrl : string ) => {
33- if ( detailCache . has ( apiUrl ) ) return detailCache . get ( apiUrl ) ! ;
34- try {
35- const res = await octokit . request ( `GET ${ apiUrl } ` ) ;
36- const data = {
37- title : res . data . title || "Untitled" ,
38- body : cleanDescription ( res . data . body ) ,
39- merged : res . data . merged ,
40- url : res . data . html_url
41- } ;
42- detailCache . set ( apiUrl , data ) ;
43- return data ;
44- } catch {
45- return { title : "Private or Deleted Content" , body : "" , url : "#" } ;
46- }
47- } ;
48-
49- export default function GitHubActivity ( ) {
50- const [ loading , setLoading ] = useState ( true ) ;
4+ export default function GitHubFeed ( ) {
515 const [ events , setEvents ] = useState < DisplayEvent [ ] > ( [ ] ) ;
6+ const [ loading , setLoading ] = useState ( true ) ;
7+ const [ error , setError ] = useState < string | null > ( null ) ;
528
539 useEffect ( ( ) => {
54- async function fetchGitHubActivity ( ) {
55- try {
56- const res = await octokit . rest . activity . listPublicEventsForUser ( {
57- username : "dsnsgithub" ,
58- per_page : 30 ,
59- headers : { "X-GitHub-Api-Version" : "2022-11-28" }
60- } ) ;
61-
62- const allowedActions = [ "opened" , "closed" , "reopened" ] ;
63- const ignoredTypes = [ "PushEvent" , "IssueCommentEvent" , "PullRequestReviewCommentEvent" , "CommitCommentEvent" ] ;
64-
65- const rawEvents = res . data . filter ( ( e ) => ! ignoredTypes . includes ( e . type ! ) ) ;
10+ let cancelled = false ;
6611
67- const processedPRs = new Set < string > ( ) ;
68- const tempEvents : DisplayEvent [ ] = [ ] ;
69-
70- for ( const event of rawEvents ) {
71- const { payload, actor, repo, created_at, id, type } = event ;
72- if ( ! created_at || ! id ) continue ;
73-
74- const base = {
75- id,
76- actor : { login : actor . login , avatar_url : actor . avatar_url } ,
77- repo : repo . name ,
78- timestamp : formatDate ( created_at )
79- } ;
80-
81- if ( type === "PullRequestEvent" || type === "PullRequestReviewEvent" ) {
82- const pr = ( payload as any ) . pull_request ;
83- const prKey = `${ repo . name } #${ pr . number } ` ;
84-
85- if ( processedPRs . has ( prKey ) ) continue ;
12+ async function load ( ) {
13+ try {
14+ const res = await fetch ( "/api/github" ) ;
8615
87- let verb = "" ;
88- if ( type === "PullRequestEvent" && ( payload as any ) . action === "closed" && pr . merged ) {
89- verb = "merged" ;
90- } else if ( type === "PullRequestReviewEvent" ) {
91- const state = ( payload as any ) . review . state . toLowerCase ( ) ;
92- if ( state === "commented" ) continue ;
93- verb = state . replace ( "_" , " " ) ;
94- } else if ( type === "PullRequestEvent" && allowedActions . includes ( ( payload as any ) . action ) ) {
95- verb = ( payload as any ) . action ;
96- } else {
97- continue ;
98- }
16+ if ( ! res . ok ) {
17+ throw new Error ( `GitHub feed failed (${ res . status } )` ) ;
18+ }
9919
100- const details = await fetchDetails ( pr . url ) ;
101- processedPRs . add ( prKey ) ;
20+ const data : DisplayEvent [ ] = await res . json ( ) ;
10221
103- tempEvents . push ( {
104- ...base ,
105- verb : verb + " pull request" ,
106- object : `${ details . title } (#${ pr . number } )` ,
107- description : details . body ,
108- url : details . url
109- } ) ;
110- } else if ( type === "IssuesEvent" ) {
111- const issue = ( payload as any ) . issue ;
112- const details = await fetchDetails ( issue . url ) ;
113- tempEvents . push ( {
114- ...base ,
115- verb : ( payload as any ) . action + " issue" ,
116- object : `${ details . title } (#${ issue . number } )` ,
117- description : details . body ,
118- url : details . url
119- } ) ;
120- } else if ( type === "WatchEvent" ) {
121- tempEvents . push ( {
122- ...base ,
123- verb : "starred" ,
124- object : repo . name ,
125- url : `https://github.com/${ repo . name } `
126- } ) ;
127- } else if ( type === "ForkEvent" ) {
128- const forkee = ( payload as any ) . forkee ;
129- tempEvents . push ( {
130- ...base ,
131- verb : "forked" ,
132- object : repo . name ,
133- description : `Original repository created by ${ forkee . full_name } ` ,
134- url : forkee . html_url
135- } ) ;
136- }
22+ if ( ! cancelled ) {
23+ setEvents ( data ) ;
24+ }
25+ } catch ( err ) {
26+ if ( ! cancelled ) {
27+ setError ( "Failed to load GitHub activity" ) ;
28+ console . error ( err ) ;
13729 }
138- setEvents ( tempEvents ) ;
139- } catch ( e ) {
140- console . error ( "Error fetching GitHub events:" , e ) ;
14130 } finally {
142- setLoading ( false ) ;
31+ if ( ! cancelled ) {
32+ setLoading ( false ) ;
33+ }
14334 }
14435 }
14536
146- fetchGitHubActivity ( ) ;
37+ load ( ) ;
38+ return ( ) => {
39+ cancelled = true ;
40+ } ;
14741 } , [ ] ) ;
14842
149- if ( loading ) {
43+ if ( loading || error ) {
15044 return (
15145 < div className = "flex flex-col space-y-3" >
152- { [ ... Array ( 8 ) ] . map ( ( _ , i ) => (
46+ { Array . from ( { length : 8 } ) . map ( ( _ , i ) => (
15347 < div key = { i } className = "mx-2 animate-pulse rounded-xl bg-viola-50 p-4 sm:mx-4 sm:p-5" >
15448 < div className = "flex items-start space-x-3 sm:space-x-4" >
15549 < div className = "h-8 w-8 shrink-0 rounded-full bg-viola-200 sm:h-10 sm:w-10" />
156- < div className = "min-w-0 flex-1" >
157- < div className = "mb-1 flex flex-col sm:mb-0 sm:flex-row sm:items-center sm:justify-between" >
158- < div className = "h-4 w-3/4 rounded bg-viola-200 sm:w-1/2" />
159- < div className = "mt-2 h-3 w-16 rounded bg-viola-100 sm:mt-0" />
160- </ div >
161- < div className = "mt-3 space-y-2" >
162- < div className = "h-3 w-full rounded bg-viola-100/70" />
163- < div className = "h-3 w-5/6 rounded bg-viola-100/70" />
164- </ div >
165- < div className = "mt-4 h-3 w-24 rounded bg-viola-200/60" />
50+ < div className = "flex-1 space-y-2" >
51+ < div className = "h-4 w-3/4 rounded bg-viola-200" />
52+ < div className = "h-3 w-full rounded bg-viola-100/70" />
53+ < div className = "h-3 w-5/6 rounded bg-viola-100/70" />
54+ < div className = "h-3 w-24 rounded bg-viola-200/60" />
16655 </ div >
16756 </ div >
16857 </ div >
@@ -174,32 +63,30 @@ export default function GitHubActivity() {
17463 return (
17564 < div className = "flex flex-col space-y-3" >
17665 { events . map ( ( item ) => (
177- < div
178- key = { item . id }
179- className = "group mx-2 rounded-xl border border-transparent bg-viola-50 p-4 transition-all duration-500 hover:scale-[1.01] hover:transition hover:duration-500 sm:mx-4 sm:p-5"
180- >
66+ < div key = { item . id } className = "group mx-2 rounded-xl bg-viola-50 p-4 transition-transform hover:scale-[1.01] sm:mx-4 sm:p-5" >
18167 < a href = { item . url } target = "_blank" rel = "noopener noreferrer" className = "flex items-start space-x-3 sm:space-x-4" >
18268 < img src = { item . actor . avatar_url } alt = { item . actor . login } className = "h-8 w-8 shrink-0 rounded-full border-2 border-white shadow-sm sm:h-10 sm:w-10" />
18369
18470 < div className = "min-w-0 flex-1" >
185- < div className = "mb-1 flex flex-col sm:mb-0 sm:flex-row sm:items-center sm:justify-between" >
186- < p className = "text-sm font-bold leading-tight text-gray-900" >
71+ < div className = "flex flex-col sm:flex-row sm:items-center sm:justify-between" >
72+ < p className = "text-sm font-bold text-gray-900" >
18773 { item . actor . login }
18874 < span className = "mx-0.5 font-normal text-gray-500" > { item . verb } </ span >
189- < span className = "break-words text-viola-700" > { item . object } </ span >
75+ < span className = "text-viola-700" > { item . object } </ span >
19076 </ p >
191- < span className = "mt-1 shrink-0 text-[10px] text-gray-400 sm:mt-0 sm:text-xs" > { item . timestamp } </ span >
77+
78+ < span className = "mt-1 text-[10px] text-gray-400 sm:mt-0 sm:text-xs" > { item . timestamp } </ span >
19279 </ div >
19380
19481 { item . description && (
19582 < div
19683 className = "mt-2"
19784 style = { {
198- maskImage : "linear-gradient(to bottom, black 70%, transparent 100% )" ,
199- WebkitMaskImage : "linear-gradient(to bottom, black 70%, transparent 100% )"
85+ maskImage : "linear-gradient(to bottom, black 70%, transparent)" ,
86+ WebkitMaskImage : "linear-gradient(to bottom, black 70%, transparent)"
20087 } }
20188 >
202- < p className = "line-clamp-3 whitespace-pre-wrap text-xs leading-relaxed text-gray-600" > { item . description } </ p >
89+ < p className = "line-clamp-3 whitespace-pre-wrap text-xs text-gray-600" > { item . description } </ p >
20390 </ div >
20491 ) }
20592
0 commit comments