Skip to content

Commit 36ae8fb

Browse files
committed
Refactor GitHub feed to use direct Octokit calls
Moves the GitHub API fetching logic directly into the `GithubFeed.astro` component, eliminating the need for a separate API route. This simplifies data fetching and reduces overhead.
1 parent 1e8182b commit 36ae8fb

2 files changed

Lines changed: 138 additions & 190 deletions

File tree

src/components/GithubFeed.astro

Lines changed: 138 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,144 @@
11
---
22
export 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>

src/pages/api/github.ts

Lines changed: 0 additions & 156 deletions
This file was deleted.

0 commit comments

Comments
 (0)