diff --git a/docs/skillkit/App.tsx b/docs/skillkit/App.tsx index b110ea16..0b87b626 100644 --- a/docs/skillkit/App.tsx +++ b/docs/skillkit/App.tsx @@ -12,6 +12,7 @@ import { Attribution } from './components/Attribution'; import { AdvancedFeatures } from './components/AdvancedFeatures'; import { UseCases } from './components/UseCases'; import { TeamEnterprise } from './components/TeamEnterprise'; +import { useStats } from './hooks/useStats'; const GITHUB_ICON = ( @@ -34,6 +35,8 @@ function scrollToSection(e: React.MouseEvent, sectionId: string): void { } export default function App(): React.ReactElement { + const stats = useStats(); + return (
- +
@@ -114,7 +117,7 @@ export default function App(): React.ReactElement { className="flex items-center gap-1.5 text-zinc-500 hover:text-white transition-colors group" > v - 1.9.0 + {stats.version} · - 2.4k + {stats.downloads} · - 66 + {stats.stars} · ); -export function Hero(): React.ReactElement { +export function Hero({ version }: HeroProps): React.ReactElement { const [copied, setCopied] = useState(false); const [visibleLines, setVisibleLines] = useState(0); const [typingIndex, setTypingIndex] = useState(0); @@ -126,7 +130,7 @@ export function Hero(): React.ReactElement {
- v1.8.0 + v{version}

diff --git a/docs/skillkit/hooks/useStats.ts b/docs/skillkit/hooks/useStats.ts new file mode 100644 index 00000000..b9d4d61c --- /dev/null +++ b/docs/skillkit/hooks/useStats.ts @@ -0,0 +1,119 @@ +import { useState, useEffect } from 'react'; + +interface Stats { + version: string; + downloads: string; + stars: number; + loading: boolean; +} + +const CACHE_KEY = 'skillkit_stats_cache'; +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes + +interface CachedStats { + data: Omit; + timestamp: number; +} + +function formatDownloads(count: number): string { + if (count >= 1000000) { + return `${(count / 1000000).toFixed(1)}M`; + } + if (count >= 1000) { + return `${(count / 1000).toFixed(1)}k`; + } + return count.toString(); +} + +function getCachedStats(): CachedStats | null { + try { + const cached = localStorage.getItem(CACHE_KEY); + if (cached) { + const parsed: CachedStats = JSON.parse(cached); + if (Date.now() - parsed.timestamp < CACHE_TTL) { + return parsed; + } + } + } catch { + // Ignore localStorage errors + } + return null; +} + +function setCachedStats(data: Omit): void { + try { + const cached: CachedStats = { + data, + timestamp: Date.now(), + }; + localStorage.setItem(CACHE_KEY, JSON.stringify(cached)); + } catch { + // Ignore localStorage errors + } +} + +export function useStats(): Stats { + const [stats, setStats] = useState({ + version: '1.9.0', + downloads: '2.4k', + stars: 66, + loading: true, + }); + + useEffect(() => { + const cached = getCachedStats(); + if (cached) { + setStats({ ...cached.data, loading: false }); + return; + } + + async function fetchStats(): Promise { + try { + const [npmResponse, githubResponse] = await Promise.allSettled([ + fetch('https://api.npmjs.org/downloads/point/last-month/skillkit'), + fetch('https://api.github.com/repos/rohitg00/skillkit'), + ]); + + let downloads = '2.4k'; + let stars = 66; + let version = '1.9.0'; + + if (npmResponse.status === 'fulfilled' && npmResponse.value.ok) { + const npmData = await npmResponse.value.json(); + if (typeof npmData.downloads === 'number' && Number.isFinite(npmData.downloads)) { + downloads = formatDownloads(npmData.downloads); + } + } + + if (githubResponse.status === 'fulfilled' && githubResponse.value.ok) { + const githubData = await githubResponse.value.json(); + if (typeof githubData.stargazers_count === 'number' && Number.isFinite(githubData.stargazers_count)) { + stars = githubData.stargazers_count; + } + } + + try { + const registryResponse = await fetch('https://registry.npmjs.org/skillkit/latest'); + if (registryResponse.ok) { + const registryData = await registryResponse.json(); + if (registryData.version) { + version = registryData.version; + } + } + } catch { + // Use default version + } + + const newStats = { version, downloads, stars }; + setCachedStats(newStats); + setStats({ ...newStats, loading: false }); + } catch { + setStats((prev) => ({ ...prev, loading: false })); + } + } + + fetchStats(); + }, []); + + return stats; +}