11---
22export const prerender = false ;
3- import type { DisplayEvent } from " ../pages/api/github" ;
43
5- const response = await fetch (new URL (" /api/github" , Astro .url ));
6- const events: DisplayEvent [] = await response .json ();
4+ import { Octokit } from " @octokit/rest" ;
5+
6+ interface DisplayEvent {
7+ id: string ;
8+ actor: { login: string ; avatar_url: string };
9+ verb: string ;
10+ object: string ;
11+ description? : string ;
12+ repo: string ;
13+ url: string ;
14+ timestamp: string ;
15+ }
16+
17+ const octokit = new Octokit ({
18+ auth: import .meta .env .GITHUB_TOKEN
19+ });
20+
21+ const detailCache = new Map <string , { title: string ; body: string ; merged? : boolean ; url: string }>();
22+
23+ const cleanDescription = (text : string | null | undefined ) => (text ? text .replace (/ [#*`_] / g , " " ).trim () : " " );
24+
25+ const formatDate = (iso : string ) =>
26+ new Date (iso ).toLocaleDateString (undefined , {
27+ month: " short" ,
28+ day: " numeric" ,
29+ hour: " numeric" ,
30+ minute: " numeric"
31+ });
32+
33+ async function fetchDetails(apiUrl : string ) {
34+ if (detailCache .has (apiUrl )) return detailCache .get (apiUrl )! ;
35+ try {
36+ const res = await octokit .request (` GET ${apiUrl } ` );
37+ const data = {
38+ title: (res .data as any ).title ?? " Untitled" ,
39+ body: cleanDescription ((res .data as any ).body ),
40+ merged: (res .data as any ).merged ,
41+ url: (res .data as any ).html_url
42+ };
43+ detailCache .set (apiUrl , data );
44+ return data ;
45+ } catch {
46+ return { title: " Private or Deleted Content" , body: " " , url: " #" };
47+ }
48+ }
49+
50+ let events: DisplayEvent [] = [];
51+
52+ try {
53+ const res = await octokit .rest .activity .listPublicEventsForUser ({
54+ username: " dsnsgithub" ,
55+ per_page: 30 ,
56+ headers: { " X-GitHub-Api-Version" : " 2022-11-28" }
57+ });
58+
59+ const allowedActions = [" opened" , " closed" , " reopened" ];
60+ const ignoredTypes = [" PushEvent" , " IssueCommentEvent" , " PullRequestReviewCommentEvent" , " CommitCommentEvent" ];
61+ const rawEvents = res .data .filter ((e ) => ! ignoredTypes .includes (e .type ! ));
62+
63+ const processedPRs = new Set <string >();
64+
65+ for (const event of rawEvents ) {
66+ const { payload, actor, repo, created_at, id, type } = event ;
67+ if (! created_at || ! id ) continue ;
68+
69+ const base = {
70+ id ,
71+ actor: { login: actor .login , avatar_url: actor .avatar_url },
72+ repo: repo .name ,
73+ timestamp: formatDate (created_at )
74+ };
75+
76+ if (type === " PullRequestEvent" || type === " PullRequestReviewEvent" ) {
77+ const pr = (payload as any ).pull_request ;
78+ const prKey = ` ${repo .name }#${pr .number } ` ;
79+ if (! pr || processedPRs .has (prKey )) continue ;
80+
81+ let verb = " " ;
82+ if (type === " PullRequestEvent" && (payload as any ).action === " closed" && pr .merged ) {
83+ verb = " merged" ;
84+ } else if (type === " PullRequestReviewEvent" ) {
85+ const state = (payload as any ).review .state ?.toLowerCase ();
86+ if (state === " commented" ) continue ;
87+ verb = state .replace (" _" , " " );
88+ } else if (type === " PullRequestEvent" && allowedActions .includes ((payload as any ).action )) {
89+ verb = (payload as any ).action ;
90+ } else continue ;
91+
92+ const details = await fetchDetails (pr .url );
93+ processedPRs .add (prKey );
94+ events .push ({ ... base , verb: verb + " pull request" , object: ` ${details .title } (#${pr .number }) ` , description: details .body , url: details .url });
95+ } else if (type === " IssuesEvent" ) {
96+ const issue = (payload as any ).issue ;
97+ if (! issue ) continue ;
98+ const details = await fetchDetails (issue .url );
99+ events .push ({ ... base , verb: (payload as any ).action + " issue" , object: ` ${details .title } (#${issue .number }) ` , description: details .body , url: details .url });
100+ } else if (type === " WatchEvent" ) {
101+ events .push ({ ... base , verb: " starred" , object: repo .name , url: ` https://github.com/${repo .name } ` });
102+ } else if (type === " ForkEvent" ) {
103+ const forkee = (payload as any ).forkee ;
104+ if (! forkee ) continue ;
105+ events .push ({ ... base , verb: " forked" , object: repo .name , description: ` Original repository created by ${forkee .full_name } ` , url: forkee .html_url });
106+ }
107+ }
108+ } catch (err ) {
109+ console .error (" GitHub fetch error:" , err );
110+ }
7111---
8112
9113<div class =" flex flex-col space-y-3" >
10- { events .map (( item ) => (
11- < div class = " group mx-2 rounded-xl bg-viola-50 p-4 transition-transform hover:scale-[1.01] sm:mx-4 sm:p-5 " >
12- < a href = { item . url } target = " _blank " rel = " noopener noreferrer " class = " flex items-start space-x-3 sm:space-x-4 " >
13- < img
14- src = { item . actor . avatar_url }
15- alt = { item . actor . login }
16- class = " h-8 w-8 shrink-0 rounded-full border-2 border-white shadow-sm sm:h-10 sm:w-10 bg-viola-100 "
17- />
18-
19- <div class = " min-w-0 flex-1 " >
20- < div class = " flex flex-col sm:flex-row sm:items-center sm:justify-between " >
21- < p class = " text-sm font-bold text-gray-900 " >
22- { item .actor . login }
23- <span class = " mx-0.5 font-normal text-gray-500 " > { item .verb } </span >
24- < span class = " text-viola-700 " > { item . object } </ span >
25- </ p >
26- < span class = " mt-1 text-[10px] text-gray-400 sm:mt-0 sm:text-xs " > { item . timestamp } </ span >
27- </ div >
28-
29- { item . description && (
30- < div class = " mt-2 " style = " mask-image: linear-gradient(to bottom, black 70%, transparent); -webkit-mask-image: linear-gradient(to bottom, black 70%, transparent); " >
31- < p class = " line-clamp-3 whitespace-pre-wrap text-xs text-gray-600 " > { item . description } </ p >
32- </ div >
33- ) }
34-
35- < p class = " mt-2 truncate text-[10px] font-semibold tracking-wide text-viola-700/80 sm:text-[11px] " > { item . repo } </ p >
36- </ div >
37- </ a >
38- </ div >
39- )) }
40- </div >
114+ { events .length === 0 && < p class = " py-4 text-center text-gray-500 " >No recent activity found.</ p > }
115+
116+ {
117+ events . map (( item ) => (
118+ < div class = " group mx-2 rounded-xl bg-viola-50 p-4 transition-transform hover:scale-[1.01] sm:mx-4 sm:p-5 " >
119+ < a href = { item . url } target = " _blank " rel = " noopener noreferrer " class = " flex items-start space-x-3 sm:space-x-4 " >
120+ < img src = { item . actor . avatar_url } alt = { item . actor . login } class = " h-8 w-8 shrink-0 rounded-full border-2 border-white bg-viola-100 shadow-sm sm:h-10 sm:w-10" />
121+
122+ < div class = " min-w-0 flex-1 " >
123+ <div class = " flex flex-col sm: flex-row sm:items-center sm:justify-between " >
124+ < p class = " text-sm font-bold text-gray-900 " >
125+ { item . actor . login }
126+ < span class = " mx-0.5 font-normal text-gray-500 " > { item .verb } </ span >
127+ <span class = " text-viola-700 " > { item .object } </span >
128+ </ p >
129+ < span class = " mt-1 text-[10px] text-gray-400 sm:mt-0 sm:text-xs " > { item . timestamp } </ span >
130+ </ div >
131+
132+ { item . description && (
133+ < div class = " mt-2 " style = " mask-image: linear-gradient(to bottom, black 70%, transparent); -webkit-mask-image: linear-gradient(to bottom, black 70%, transparent); " >
134+ < p class = " line-clamp-3 whitespace-pre-wrap text-xs text-gray-600 " > { item . description } </ p >
135+ </ div >
136+ ) }
137+
138+ < p class = " mt-2 truncate text-[10px] font-semibold tracking-wide text-viola-700/80 sm:text-[11px] " > { item . repo } </ p >
139+ </ div >
140+ </ a >
141+ </ div >
142+ ))
143+ }
144+ </div >
0 commit comments