@@ -9,9 +9,58 @@ interface EventType {
99 } ;
1010}
1111
12+ interface FetchError {
13+ message : string ;
14+ isRateLimited : boolean ;
15+ statusCode ?: number ;
16+ }
17+
18+ // Custom error class for ActivityFeed errors
19+ class ActivityFeedError extends Error {
20+ constructor (
21+ message : string ,
22+ public isRateLimited = false ,
23+ public statusCode ?: number
24+ ) {
25+ super ( message ) ;
26+ this . name = "ActivityFeedError" ;
27+ }
28+ }
29+
30+ // Type guard to validate if data is EventType[]
31+ const isEventTypeArray = ( data : unknown ) : data is EventType [ ] => {
32+ if ( ! Array . isArray ( data ) ) return false ;
33+ return data . every ( ( item ) => {
34+ if ( typeof item !== "object" || item === null ) return false ;
35+
36+ // Validate required fields
37+ if (
38+ typeof item . id !== "string" ||
39+ typeof item . type !== "string" ||
40+ typeof item . created_at !== "string"
41+ ) {
42+ return false ;
43+ }
44+
45+ // Validate optional repo field
46+ if ( item . repo !== undefined ) {
47+ if (
48+ typeof item . repo !== "object" ||
49+ item . repo === null ||
50+ typeof item . repo . name !== "string"
51+ ) {
52+ return false ;
53+ }
54+ }
55+
56+ return true ;
57+ } ) ;
58+ } ;
59+
1260export default function ActivityFeed ( { username } : { username : string } ) {
1361 const [ events , setEvents ] = useState < EventType [ ] > ( [ ] ) ;
1462 const [ loading , setLoading ] = useState ( true ) ;
63+ const [ error , setError ] = useState < FetchError | null > ( null ) ;
1564
1665 // 🕒 time ago function
1766 const getTimeAgo = ( dateString : string ) => {
@@ -29,16 +78,65 @@ export default function ActivityFeed({ username }: { username: string }) {
2978 const fetchEvents = async ( ) => {
3079 try {
3180 setLoading ( true ) ;
81+ setError ( null ) ;
3282
3383 const res = await fetch (
3484 `https://api.github.com/users/${ username } /events`
3585 ) ;
86+
87+ // ✅ Check HTTP response success
88+ if ( ! res . ok ) {
89+ const isRateLimited = res . status === 403 ;
90+ const errorData = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
91+ const errorMessage =
92+ typeof errorData === "object" &&
93+ errorData !== null &&
94+ "message" in errorData &&
95+ typeof errorData . message === "string"
96+ ? errorData . message
97+ : `HTTP ${ res . status } : Failed to fetch events` ;
98+
99+ throw new ActivityFeedError (
100+ isRateLimited
101+ ? "GitHub API rate limit exceeded. Try again later."
102+ : errorMessage ,
103+ isRateLimited ,
104+ res . status
105+ ) ;
106+ }
107+
36108 const data = await res . json ( ) ;
37109
110+ // ✅ Validate response structure matches EventType[]
111+ if ( ! isEventTypeArray ( data ) ) {
112+ throw new ActivityFeedError (
113+ "Invalid API response structure. Expected array of events." ,
114+ false ,
115+ 200
116+ ) ;
117+ }
118+
38119 setEvents ( data ) ;
39- setLoading ( false ) ;
40120 } catch ( err ) {
41- console . error ( err ) ;
121+ const fetchError : FetchError = {
122+ message : "Failed to fetch activity. Please try again." ,
123+ isRateLimited : false ,
124+ } ;
125+
126+ // Handle ActivityFeedError instances
127+ if ( err instanceof ActivityFeedError ) {
128+ fetchError . message = err . message ;
129+ fetchError . isRateLimited = err . isRateLimited ;
130+ fetchError . statusCode = err . statusCode ;
131+ } else if ( err instanceof Error ) {
132+ // Handle generic errors
133+ fetchError . message = err . message || fetchError . message ;
134+ }
135+
136+ setError ( fetchError ) ;
137+ console . error ( "ActivityFeed fetch error:" , fetchError ) ;
138+ setEvents ( [ ] ) ;
139+ } finally {
42140 setLoading ( false ) ;
43141 }
44142 } ;
@@ -56,9 +154,25 @@ export default function ActivityFeed({ username }: { username: string }) {
56154 </ h2 >
57155
58156 { loading ? (
59- < p className = "text-center" > Loading...</ p >
157+ < p className = "text-center text-gray-500" > Loading activity...</ p >
158+ ) : error ? (
159+ < div className = "border border-red-300 rounded-lg p-4 mb-3 bg-red-50 dark:bg-red-900/20" >
160+ < p className = "text-sm font-semibold text-red-700 dark:text-red-300" >
161+ ⚠️ { error . message }
162+ </ p >
163+ { error . isRateLimited && (
164+ < p className = "text-xs text-red-600 dark:text-red-400 mt-2" >
165+ You've hit GitHub's API rate limit. The limit resets in 1 hour.
166+ </ p >
167+ ) }
168+ { error . statusCode === 404 && (
169+ < p className = "text-xs text-red-600 dark:text-red-400 mt-2" >
170+ User not found. Please check the username.
171+ </ p >
172+ ) }
173+ </ div >
60174 ) : events . length === 0 ? (
61- < p className = "text-center" > No activity found</ p >
175+ < p className = "text-center text-gray-500 " > No activity found</ p >
62176 ) : (
63177 events . slice ( 0 , 10 ) . map ( ( event ) => (
64178 < div
0 commit comments