Skip to content

Commit 7807e2f

Browse files
committed
Refactor GitHub feed to use API route
Moves GitHub API fetching and processing logic from the frontend component to a dedicated API route. This improves performance by fetching data on the server and allows for caching.
1 parent 6ec388f commit 7807e2f

3 files changed

Lines changed: 200 additions & 157 deletions

File tree

src/components/GithubFeed.tsx

Lines changed: 43 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,57 @@
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

src/components/port/RecentGames.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface Game {
1717
gameType: string;
1818
mode: string | null;
1919
map: string | null;
20-
ended?: number; // Optional property
20+
ended?: number;
2121
}
2222

2323
interface RecentGamesData {

0 commit comments

Comments
 (0)