Skip to content

Commit 407b06f

Browse files
committed
Feat: Add Github activity feed component
Replaces the static 'RecentProjects' component with a dynamic feed fetching public GitHub events. This provides a more engaging and up-to-date view of recent activity.
1 parent a63e57c commit 407b06f

4 files changed

Lines changed: 178 additions & 89 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@formkit/auto-animate": "^0.8.2",
2121
"@iconify-json/fa6-brands": "^1.2.5",
2222
"@iconify-json/mdi": "^1.2.2",
23+
"@octokit/core": "^7.0.6",
2324
"@types/react": "^18.3.3",
2425
"@types/react-dom": "^18.3.0",
2526
"@types/whois-json": "^2.0.4",

src/components/GithubFeed.svelte

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script lang="ts">
2+
import { Octokit } from "@octokit/core";
3+
import { onMount } from "svelte";
4+
5+
type DisplayEvent = {
6+
id: string;
7+
actor: { login: string; avatar_url: string };
8+
verb: string;
9+
object: string;
10+
description?: string;
11+
repo: string;
12+
url: string;
13+
timestamp: string;
14+
};
15+
16+
let loading = true;
17+
let events: DisplayEvent[] = [];
18+
19+
const octokit = new Octokit();
20+
const detailCache = new Map<string, { title: string; body: string; merged?: boolean }>();
21+
22+
function cleanDescription(text: string | null) {
23+
if (!text) return "";
24+
return text.replace(/[#*`_]/g, "").trim();
25+
}
26+
27+
async function fetchDetails(apiUrl: string) {
28+
if (detailCache.has(apiUrl)) return detailCache.get(apiUrl)!;
29+
try {
30+
const res = await octokit.request(`GET ${apiUrl}`);
31+
const data = {
32+
title: res.data.title || "Untitled",
33+
body: cleanDescription(res.data.body),
34+
merged: res.data.merged,
35+
url: res.data.html_url
36+
};
37+
detailCache.set(apiUrl, data);
38+
return data;
39+
} catch {
40+
return { title: "Private or Deleted Content", body: "" };
41+
}
42+
}
43+
44+
function formatDate(isoString: string) {
45+
return new Date(isoString).toLocaleDateString(undefined, {
46+
month: "short",
47+
day: "numeric",
48+
hour: "numeric",
49+
minute: "numeric"
50+
});
51+
}
52+
53+
async function run() {
54+
try {
55+
const res = await octokit.request("GET /users/{username}/events/public", {
56+
username: "dsnsgithub",
57+
headers: { "X-GitHub-Api-Version": "2022-11-28" }
58+
});
59+
60+
const allowedActions = ["opened", "closed", "reopened"];
61+
const ignoredTypes = ["PushEvent", "IssueCommentEvent", "PullRequestReviewCommentEvent", "CommitCommentEvent"];
62+
const rawEvents = res.data.filter((e) => !ignoredTypes.includes(e.type));
63+
64+
const processedPRs = new Set<string>();
65+
const tempEvents: DisplayEvent[] = [];
66+
67+
for (const event of rawEvents) {
68+
const { payload, actor, repo, created_at, id, type } = event;
69+
const base = { id, actor, repo: repo.name, timestamp: formatDate(created_at) };
70+
71+
if (type === "PullRequestEvent" || type === "PullRequestReviewEvent") {
72+
const pr = payload.pull_request;
73+
const prKey = `${repo.name}#${pr.number}`;
74+
if (processedPRs.has(prKey)) continue;
75+
76+
const details = await fetchDetails(pr.url);
77+
78+
let verb = "";
79+
if (details.merged) {
80+
verb = "merged";
81+
} else if (type === "PullRequestReviewEvent") {
82+
const state = payload.review.state.toLowerCase();
83+
if (state === "commented") continue;
84+
verb = state.replace("_", " ");
85+
} else {
86+
if (!allowedActions.includes(payload.action)) continue;
87+
verb = payload.action;
88+
}
89+
90+
processedPRs.add(prKey);
91+
tempEvents.push({
92+
...base,
93+
verb: verb + " pull request",
94+
object: `${details.title} (#${pr.number}).`,
95+
description: details.body,
96+
url: details.url
97+
});
98+
} else if (type === "IssuesEvent") {
99+
const details = await fetchDetails(payload.issue.url);
100+
101+
tempEvents.push({
102+
...base,
103+
verb: payload.action + " issue",
104+
object: `${details.title} (#${payload.issue.number}).`,
105+
description: details.body,
106+
url: details.url
107+
});
108+
} else if (type === "WatchEvent") {
109+
tempEvents.push({ ...base, verb: "starred", object: repo.name, url: `https://github.com/${repo.name}` });
110+
} else if (type === "ForkEvent") {
111+
tempEvents.push({ ...base, verb: "forked", object: repo.name, description: `To ${payload.forkee.full_name}`, url: payload.forkee.html_url });
112+
}
113+
}
114+
events = tempEvents;
115+
} catch (e) {
116+
console.error("Error:", e);
117+
} finally {
118+
loading = false;
119+
}
120+
}
121+
122+
onMount(run);
123+
</script>
124+
125+
{#if loading}
126+
<div class="space-y-4 p-4">
127+
{#each Array(3) as _}
128+
<div class="h-28 w-full animate-pulse rounded-xl bg-viola-50" />
129+
{/each}
130+
</div>
131+
{:else}
132+
<div class="flex flex-col space-y-3">
133+
{#each events as item (item.id)}
134+
<div class="group mx-4 rounded-xl border border-transparent bg-viola-50 p-5 transition-all hover:border-viola-200 hover:bg-viola-100/40">
135+
<a href={item.url} target="_blank" rel="noopener noreferrer" class="flex items-start space-x-4">
136+
<img src={item.actor.avatar_url} alt="" class="h-10 w-10 rounded-full border-2 border-white shadow-sm" />
137+
138+
<div class="flex-1 overflow-hidden">
139+
<div class="flex items-center justify-between">
140+
<p class="truncate pr-2 text-sm font-bold text-gray-900">
141+
{item.actor.login}
142+
<span class="mx-1 font-normal text-gray-500">{item.verb}</span>
143+
<span class="text-viola-700">{item.object}</span>
144+
</p>
145+
<span class="shrink-0 text-xs text-gray-400">
146+
{item.timestamp}
147+
</span>
148+
</div>
149+
150+
{#if item.description}
151+
<div class="description-fade mt-2">
152+
<p class="line-clamp-3 whitespace-pre-wrap text-xs leading-relaxed text-gray-600">
153+
{item.description}
154+
</p>
155+
</div>
156+
{/if}
157+
158+
<p class="mt-2 text-[11px] font-semibold tracking-wide text-viola-600/80">
159+
{item.repo}
160+
</p>
161+
</div>
162+
</a>
163+
</div>
164+
{/each}
165+
</div>
166+
{/if}
167+
168+
<style>
169+
.description-fade p {
170+
mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
171+
-webkit-mask-image: linear-gradient(to bottom, black 70%, transparent 100%);
172+
overflow: hidden;
173+
}
174+
</style>

src/components/RecentProjects.svelte

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

src/pages/index.astro

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import scheduliIcon from "../assets/schedule.svg";
55
import betterHudIcon from "../assets/betterhud.png";
66
import { Icon } from "astro-icon/components";
77
import DiscordStatus from "../components/DiscordStatus.svelte";
8-
import RecentProjects from "../components/RecentProjects.svelte";
8+
import GithubFeed from "../components/GithubFeed.svelte";
99
---
1010

1111
<Layout title="dsns.dev" description="Check out what I do, and explore some of my projects.">
@@ -67,12 +67,8 @@ import RecentProjects from "../components/RecentProjects.svelte";
6767
</div>
6868

6969
<div class="m-2 rounded-lg bg-viola-100 p-6 px-4 shadow-lg md:px-6 lg:p-8">
70-
<h2 class="mb-4 text-2xl font-bold">Recently Updated Projects</h2>
71-
<div
72-
class="max-h-[100vw] overflow-y-scroll scrollbar scrollbar-track-viola-200 scrollbar-thumb-viola-300 scrollbar-track-rounded-full scrollbar-thumb-rounded-full scrollbar-w-2 lg:max-h-[30vw]"
73-
>
74-
<RecentProjects client:idle />
75-
</div>
70+
<h2 class="mb-4 text-2xl font-bold">GitHub Feed</h2>
71+
<GithubFeed client:idle />
7672
</div>
7773
</div>
7874
</Layout>

0 commit comments

Comments
 (0)