Skip to content

Commit 6cbbdab

Browse files
authored
feat(website): add dynamic stats from npm and GitHub APIs (#39)
* feat(website): add dynamic stats from npm and GitHub APIs - Create useStats hook to fetch real-time version, downloads, and stars - Replace hardcoded values in App.tsx stats bar with dynamic data - Replace hardcoded version in Hero.tsx badge with dynamic data - Add 5-minute cache to reduce API calls - Fallback to default values if APIs fail * fix: address CodeRabbit and Devin review comments - Fix duplicate API calls: remove useStats from Hero, pass version as prop from App - Fix falsy checks: use typeof checks for downloads and stargazers_count to correctly handle zero values instead of showing defaults
1 parent c1c1792 commit 6cbbdab

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

docs/skillkit/App.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Attribution } from './components/Attribution';
1212
import { AdvancedFeatures } from './components/AdvancedFeatures';
1313
import { UseCases } from './components/UseCases';
1414
import { TeamEnterprise } from './components/TeamEnterprise';
15+
import { useStats } from './hooks/useStats';
1516

1617
const GITHUB_ICON = (
1718
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
@@ -34,6 +35,8 @@ function scrollToSection(e: React.MouseEvent, sectionId: string): void {
3435
}
3536

3637
export default function App(): React.ReactElement {
38+
const stats = useStats();
39+
3740
return (
3841
<div className="min-h-screen text-zinc-100 font-sans selection:bg-white selection:text-black" style={{ backgroundColor: '#000000' }}>
3942
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-zinc-800 backdrop-blur-md" style={{ backgroundColor: 'rgba(0,0,0,0.9)' }}>
@@ -102,7 +105,7 @@ export default function App(): React.ReactElement {
102105
</nav>
103106

104107
<main className="pt-14">
105-
<Hero />
108+
<Hero version={stats.version} />
106109

107110
<div className="border-b border-zinc-800/50 py-2.5" style={{ background: 'linear-gradient(to bottom, rgba(9,9,11,0.95), rgba(0,0,0,1))' }}>
108111
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
@@ -114,7 +117,7 @@ export default function App(): React.ReactElement {
114117
className="flex items-center gap-1.5 text-zinc-500 hover:text-white transition-colors group"
115118
>
116119
<span className="text-zinc-600 group-hover:text-zinc-400">v</span>
117-
<span className="text-white font-medium">1.9.0</span>
120+
<span className="text-white font-medium">{stats.version}</span>
118121
</a>
119122
<span className="text-zinc-800">·</span>
120123
<a
@@ -126,7 +129,7 @@ export default function App(): React.ReactElement {
126129
<svg className="w-3 h-3 text-zinc-600 group-hover:text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
127130
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
128131
</svg>
129-
<span className="text-white font-medium">2.4k</span>
132+
<span className="text-white font-medium">{stats.downloads}</span>
130133
</a>
131134
<span className="text-zinc-800">·</span>
132135
<a
@@ -138,7 +141,7 @@ export default function App(): React.ReactElement {
138141
<svg className="w-3 h-3 text-zinc-600 group-hover:text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
139142
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
140143
</svg>
141-
<span className="text-white font-medium">66</span>
144+
<span className="text-white font-medium">{stats.stars}</span>
142145
</a>
143146
<span className="text-zinc-800 hidden sm:inline">·</span>
144147
<a

docs/skillkit/components/Hero.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import React, { useState, useEffect } from 'react';
22
import { Button } from './Button';
33

4+
interface HeroProps {
5+
version: string;
6+
}
7+
48
const ASCII_LOGO = `
59
███████╗██╗ ██╗██╗██╗ ██╗ ██╗ ██╗██╗████████╗
610
██╔════╝██║ ██╔╝██║██║ ██║ ██║ ██╔╝██║╚══██╔══╝
@@ -55,7 +59,7 @@ const COPY_ICON = (
5559
</svg>
5660
);
5761

58-
export function Hero(): React.ReactElement {
62+
export function Hero({ version }: HeroProps): React.ReactElement {
5963
const [copied, setCopied] = useState(false);
6064
const [visibleLines, setVisibleLines] = useState(0);
6165
const [typingIndex, setTypingIndex] = useState(0);
@@ -126,7 +130,7 @@ export function Hero(): React.ReactElement {
126130
<div className="animate-fade-in">
127131
<div className="inline-flex items-center space-x-2 border border-zinc-800 bg-zinc-900/50 px-2 py-0.5 mb-3 backdrop-blur-sm">
128132
<span className="flex h-1.5 w-1.5 bg-white rounded-full"></span>
129-
<span className="text-xs font-mono text-zinc-400">v1.8.0</span>
133+
<span className="text-xs font-mono text-zinc-400">v{version}</span>
130134
</div>
131135

132136
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold tracking-tight text-white mb-3 font-mono">

docs/skillkit/hooks/useStats.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState, useEffect } from 'react';
2+
3+
interface Stats {
4+
version: string;
5+
downloads: string;
6+
stars: number;
7+
loading: boolean;
8+
}
9+
10+
const CACHE_KEY = 'skillkit_stats_cache';
11+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
12+
13+
interface CachedStats {
14+
data: Omit<Stats, 'loading'>;
15+
timestamp: number;
16+
}
17+
18+
function formatDownloads(count: number): string {
19+
if (count >= 1000000) {
20+
return `${(count / 1000000).toFixed(1)}M`;
21+
}
22+
if (count >= 1000) {
23+
return `${(count / 1000).toFixed(1)}k`;
24+
}
25+
return count.toString();
26+
}
27+
28+
function getCachedStats(): CachedStats | null {
29+
try {
30+
const cached = localStorage.getItem(CACHE_KEY);
31+
if (cached) {
32+
const parsed: CachedStats = JSON.parse(cached);
33+
if (Date.now() - parsed.timestamp < CACHE_TTL) {
34+
return parsed;
35+
}
36+
}
37+
} catch {
38+
// Ignore localStorage errors
39+
}
40+
return null;
41+
}
42+
43+
function setCachedStats(data: Omit<Stats, 'loading'>): void {
44+
try {
45+
const cached: CachedStats = {
46+
data,
47+
timestamp: Date.now(),
48+
};
49+
localStorage.setItem(CACHE_KEY, JSON.stringify(cached));
50+
} catch {
51+
// Ignore localStorage errors
52+
}
53+
}
54+
55+
export function useStats(): Stats {
56+
const [stats, setStats] = useState<Stats>({
57+
version: '1.9.0',
58+
downloads: '2.4k',
59+
stars: 66,
60+
loading: true,
61+
});
62+
63+
useEffect(() => {
64+
const cached = getCachedStats();
65+
if (cached) {
66+
setStats({ ...cached.data, loading: false });
67+
return;
68+
}
69+
70+
async function fetchStats(): Promise<void> {
71+
try {
72+
const [npmResponse, githubResponse] = await Promise.allSettled([
73+
fetch('https://api.npmjs.org/downloads/point/last-month/skillkit'),
74+
fetch('https://api.github.com/repos/rohitg00/skillkit'),
75+
]);
76+
77+
let downloads = '2.4k';
78+
let stars = 66;
79+
let version = '1.9.0';
80+
81+
if (npmResponse.status === 'fulfilled' && npmResponse.value.ok) {
82+
const npmData = await npmResponse.value.json();
83+
if (typeof npmData.downloads === 'number' && Number.isFinite(npmData.downloads)) {
84+
downloads = formatDownloads(npmData.downloads);
85+
}
86+
}
87+
88+
if (githubResponse.status === 'fulfilled' && githubResponse.value.ok) {
89+
const githubData = await githubResponse.value.json();
90+
if (typeof githubData.stargazers_count === 'number' && Number.isFinite(githubData.stargazers_count)) {
91+
stars = githubData.stargazers_count;
92+
}
93+
}
94+
95+
try {
96+
const registryResponse = await fetch('https://registry.npmjs.org/skillkit/latest');
97+
if (registryResponse.ok) {
98+
const registryData = await registryResponse.json();
99+
if (registryData.version) {
100+
version = registryData.version;
101+
}
102+
}
103+
} catch {
104+
// Use default version
105+
}
106+
107+
const newStats = { version, downloads, stars };
108+
setCachedStats(newStats);
109+
setStats({ ...newStats, loading: false });
110+
} catch {
111+
setStats((prev) => ({ ...prev, loading: false }));
112+
}
113+
}
114+
115+
fetchStats();
116+
}, []);
117+
118+
return stats;
119+
}

0 commit comments

Comments
 (0)