Skip to content

Commit 8ffa48c

Browse files
committed
feat: github integration
1 parent 87d3bc5 commit 8ffa48c

File tree

5 files changed

+550
-1
lines changed

5 files changed

+550
-1
lines changed

components/Footer.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import CompactGitHubStats from "../islands/CompactGitHubStats.tsx";
2+
13
export default function Footer() {
24
return (
35
<footer class="py-12 px-4 border-t border-surface0 bg-base">
@@ -6,10 +8,16 @@ export default function Footer() {
68
<img src="/logo.svg" alt="Andromeda" class="w-8 h-8" />
79
<span class="font-semibold text-text">Andromeda</span>
810
</div>
9-
<p class="text-subtext1 mb-4">
11+
<p class="text-subtext1 mb-6">
1012
A modern, fast, and secure JavaScript & TypeScript runtime built from
1113
the ground up in Rust and powered by Nova Engine.
1214
</p>
15+
16+
{/* Community stats */}
17+
<div class="mb-6">
18+
<CompactGitHubStats showForks className="justify-center" />
19+
</div>
20+
1321
<div class="flex justify-center space-x-6">
1422
<a
1523
href="https://github.com/tryandromeda/andromeda"

islands/CompactGitHubStats.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useState, useEffect } from "preact/hooks";
2+
import { Star, GitFork } from "lucide-preact";
3+
4+
interface GitHubRepo {
5+
stargazers_count: number;
6+
forks_count: number;
7+
}
8+
9+
interface CompactGitHubStatsProps {
10+
className?: string;
11+
showForks?: boolean;
12+
}
13+
14+
export default function CompactGitHubStats({ className = "", showForks = false }: CompactGitHubStatsProps) {
15+
const [repoData, setRepoData] = useState<GitHubRepo | null>(null);
16+
const [loading, setLoading] = useState(true);
17+
18+
useEffect(() => {
19+
const fetchGitHubData = async () => {
20+
try {
21+
const response = await fetch('https://api.github.com/repos/tryandromeda/andromeda');
22+
if (response.ok) {
23+
const data = await response.json();
24+
setRepoData(data);
25+
}
26+
} catch (error) {
27+
console.error('Error fetching GitHub data:', error);
28+
} finally {
29+
setLoading(false);
30+
}
31+
};
32+
33+
fetchGitHubData();
34+
}, []);
35+
36+
const formatNumber = (num: number): string => {
37+
if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
38+
return num.toString();
39+
};
40+
41+
if (loading || !repoData) {
42+
return (
43+
<div class={`flex items-center gap-2 ${className}`}>
44+
<div class="animate-pulse bg-surface1 rounded px-2 py-1 w-16 h-6"></div>
45+
{showForks && <div class="animate-pulse bg-surface1 rounded px-2 py-1 w-16 h-6"></div>}
46+
</div>
47+
);
48+
}
49+
50+
return (
51+
<div class={`flex items-center gap-2 ${className}`}>
52+
<a
53+
href="https://github.com/tryandromeda/andromeda"
54+
target="_blank"
55+
rel="noopener noreferrer"
56+
class="flex items-center gap-1 bg-surface0 hover:bg-surface1 border border-surface1 hover:border-surface2 rounded-lg px-3 py-1.5 text-sm font-medium text-subtext1 hover:text-text transition-all duration-200"
57+
title={`${repoData.stargazers_count} GitHub stars`}
58+
>
59+
<Star size={14} class="text-yellow" />
60+
<span>{formatNumber(repoData.stargazers_count)}</span>
61+
</a>
62+
63+
{showForks && (
64+
<a
65+
href="https://github.com/tryandromeda/andromeda/forks"
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
class="flex items-center gap-1 bg-surface0 hover:bg-surface1 border border-surface1 hover:border-surface2 rounded-lg px-3 py-1.5 text-sm font-medium text-subtext1 hover:text-text transition-all duration-200"
69+
title={`${repoData.forks_count} GitHub forks`}
70+
>
71+
<GitFork size={14} class="text-blue" />
72+
<span>{formatNumber(repoData.forks_count)}</span>
73+
</a>
74+
)}
75+
</div>
76+
);
77+
}

islands/GitHubStats.tsx

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { useEffect, useState } from "preact/hooks";
2+
import { Activity, GitFork, Star, Users } from "lucide-preact";
3+
4+
interface GitHubRepo {
5+
stargazers_count: number;
6+
forks_count: number;
7+
subscribers_count: number;
8+
open_issues_count: number;
9+
updated_at: string;
10+
}
11+
12+
interface GitHubContributor {
13+
login: string;
14+
avatar_url: string;
15+
html_url: string;
16+
contributions: number;
17+
}
18+
19+
interface GitHubRelease {
20+
tag_name: string;
21+
published_at: string;
22+
download_count?: number;
23+
}
24+
25+
export default function GitHubStats() {
26+
const [repoData, setRepoData] = useState<GitHubRepo | null>(null);
27+
const [contributors, setContributors] = useState<GitHubContributor[]>([]);
28+
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
29+
null,
30+
);
31+
const [loading, setLoading] = useState(true);
32+
const [error, setError] = useState<string | null>(null);
33+
34+
useEffect(() => {
35+
const fetchGitHubData = async () => {
36+
try {
37+
setLoading(true);
38+
39+
const repoResponse = await fetch(
40+
"https://api.github.com/repos/tryandromeda/andromeda",
41+
);
42+
if (!repoResponse.ok) throw new Error("Failed to fetch repo data");
43+
const repo = await repoResponse.json();
44+
setRepoData(repo);
45+
46+
const contributorsResponse = await fetch(
47+
"https://api.github.com/repos/tryandromeda/andromeda/contributors?per_page=6",
48+
);
49+
if (!contributorsResponse.ok) {
50+
throw new Error("Failed to fetch contributors");
51+
}
52+
const contributorsData = await contributorsResponse.json();
53+
setContributors(contributorsData); // Fetch latest release
54+
const releaseResponse = await fetch(
55+
"https://api.github.com/repos/tryandromeda/andromeda/releases/latest",
56+
);
57+
if (releaseResponse.ok) {
58+
const release = await releaseResponse.json();
59+
60+
const downloadCount =
61+
release.assets?.reduce(
62+
(total: number, asset: { download_count?: number }) =>
63+
total + (asset.download_count || 0),
64+
0,
65+
) || 0;
66+
67+
setLatestRelease({
68+
tag_name: release.tag_name,
69+
published_at: release.published_at,
70+
download_count: downloadCount,
71+
});
72+
}
73+
74+
setError(null);
75+
} catch (err) {
76+
console.error("Error fetching GitHub data:", err);
77+
setError(
78+
err instanceof Error ? err.message : "Failed to load GitHub data",
79+
);
80+
} finally {
81+
setLoading(false);
82+
}
83+
};
84+
85+
fetchGitHubData();
86+
}, []);
87+
88+
const formatNumber = (num: number): string => {
89+
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
90+
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
91+
return num.toString();
92+
};
93+
94+
const formatDate = (dateString: string): string => {
95+
const date = new Date(dateString);
96+
const now = new Date();
97+
const diffMs = now.getTime() - date.getTime();
98+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
99+
100+
if (diffDays === 0) return "Today";
101+
if (diffDays === 1) return "1 day ago";
102+
if (diffDays < 30) return `${diffDays} days ago`;
103+
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
104+
return `${Math.floor(diffDays / 365)} years ago`;
105+
};
106+
107+
if (loading) {
108+
return (
109+
<div class="bg-surface0 rounded-2xl p-6 border border-surface1">
110+
<div class="animate-pulse">
111+
<div class="h-6 bg-surface1 rounded mb-4 w-48"></div>
112+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
113+
{[...Array(4)].map((_, i) => (
114+
<div key={i} class="text-center">
115+
<div class="h-8 bg-surface1 rounded mb-2"></div>
116+
<div class="h-4 bg-surface1 rounded w-16 mx-auto"></div>
117+
</div>
118+
))}
119+
</div>
120+
<div class="h-4 bg-surface1 rounded mb-2"></div>
121+
<div class="flex gap-2">
122+
{[...Array(6)].map((_, i) => (
123+
<div key={i} class="w-8 h-8 bg-surface1 rounded-full"></div>
124+
))}
125+
</div>
126+
</div>
127+
</div>
128+
);
129+
}
130+
131+
if (error) {
132+
return (
133+
<div class="bg-surface0 rounded-2xl p-6 border border-surface1">
134+
<div class="text-center text-subtext1">
135+
<Activity class="mx-auto mb-2 opacity-50" size={32} />
136+
<p class="text-sm">GitHub stats temporarily unavailable</p>
137+
</div>
138+
</div>
139+
);
140+
}
141+
142+
return (
143+
<div class="bg-surface0 rounded-2xl p-6 border border-surface1 hover:border-surface2 transition-colors">
144+
<h3 class="text-lg font-semibold text-text mb-4 flex items-center gap-2">
145+
<Activity size={20} class="text-blue" />
146+
Community Stats
147+
</h3>
148+
149+
{/* Main Stats Grid */}
150+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
151+
<div class="text-center">
152+
<div class="flex items-center justify-center gap-1 text-2xl font-bold text-yellow mb-1">
153+
<Star size={20} />
154+
{formatNumber(repoData?.stargazers_count || 0)}
155+
</div>
156+
<div class="text-xs text-subtext1">Stars</div>
157+
</div>
158+
159+
<div class="text-center">
160+
<div class="flex items-center justify-center gap-1 text-2xl font-bold text-blue mb-1">
161+
<GitFork size={20} />
162+
{formatNumber(repoData?.forks_count || 0)}
163+
</div>
164+
<div class="text-xs text-subtext1">Forks</div>
165+
</div>
166+
167+
<div class="text-center">
168+
<div class="flex items-center justify-center gap-1 text-2xl font-bold text-green mb-1">
169+
<Users size={20} />
170+
{formatNumber(repoData?.subscribers_count || 0)}
171+
</div>
172+
<div class="text-xs text-subtext1">Watchers</div>
173+
</div>
174+
175+
<div class="text-center">
176+
<div class="text-2xl font-bold text-mauve mb-1">
177+
{latestRelease?.download_count
178+
? formatNumber(latestRelease.download_count)
179+
: "—"}
180+
</div>
181+
<div class="text-xs text-subtext1">Downloads</div>
182+
</div>
183+
</div>
184+
185+
{/* Latest Release Info */}
186+
{latestRelease && (
187+
<div class="bg-surface1 rounded-lg p-3 mb-4">
188+
<div class="flex items-center justify-between text-sm">
189+
<span class="text-text font-medium">
190+
Latest: {latestRelease.tag_name}
191+
</span>
192+
<span class="text-subtext1">
193+
{formatDate(latestRelease.published_at)}
194+
</span>
195+
</div>
196+
</div>
197+
)}
198+
199+
{/* Contributors */}
200+
<div>
201+
<h4 class="text-sm font-medium text-subtext0 mb-3">Top Contributors</h4>
202+
<div class="flex gap-2 overflow-x-auto">
203+
{contributors.map((contributor) => (
204+
<a
205+
key={contributor.login}
206+
href={contributor.html_url}
207+
target="_blank"
208+
rel="noopener noreferrer"
209+
class="flex-shrink-0 group"
210+
title={`${contributor.login} (${contributor.contributions} contributions)`}
211+
>
212+
<img
213+
src={contributor.avatar_url}
214+
alt={contributor.login}
215+
class="w-8 h-8 rounded-full border-2 border-surface1 group-hover:border-blue transition-colors"
216+
/>
217+
</a>
218+
))}
219+
<a
220+
href="https://github.com/tryandromeda/andromeda/graphs/contributors"
221+
target="_blank"
222+
rel="noopener noreferrer"
223+
class="flex-shrink-0 w-8 h-8 rounded-full border-2 border-surface1 bg-surface2 hover:border-blue transition-colors flex items-center justify-center text-subtext1 hover:text-blue text-xs"
224+
title="View all contributors"
225+
>
226+
+
227+
</a>
228+
</div>
229+
</div>
230+
231+
{/* Last Updated */}
232+
{repoData?.updated_at && (
233+
<div class="mt-4 pt-3 border-t border-surface1">
234+
<p class="text-xs text-subtext1 text-center">
235+
Last updated {formatDate(repoData.updated_at)}
236+
</p>
237+
</div>
238+
)}
239+
</div>
240+
);
241+
}

0 commit comments

Comments
 (0)