Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/skillkit/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
Expand All @@ -34,6 +35,8 @@ function scrollToSection(e: React.MouseEvent, sectionId: string): void {
}

export default function App(): React.ReactElement {
const stats = useStats();

return (
<div className="min-h-screen text-zinc-100 font-sans selection:bg-white selection:text-black" style={{ backgroundColor: '#000000' }}>
<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)' }}>
Expand Down Expand Up @@ -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"
>
<span className="text-zinc-600 group-hover:text-zinc-400">v</span>
<span className="text-white font-medium">1.9.0</span>
<span className="text-white font-medium">{stats.version}</span>
</a>
<span className="text-zinc-800">·</span>
<a
Expand All @@ -126,7 +129,7 @@ export default function App(): React.ReactElement {
<svg className="w-3 h-3 text-zinc-600 group-hover:text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
<span className="text-white font-medium">2.4k</span>
<span className="text-white font-medium">{stats.downloads}</span>
</a>
<span className="text-zinc-800">·</span>
<a
Expand All @@ -138,7 +141,7 @@ export default function App(): React.ReactElement {
<svg className="w-3 h-3 text-zinc-600 group-hover:text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<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" />
</svg>
<span className="text-white font-medium">66</span>
<span className="text-white font-medium">{stats.stars}</span>
</a>
<span className="text-zinc-800 hidden sm:inline">·</span>
<a
Expand Down
4 changes: 3 additions & 1 deletion docs/skillkit/components/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Button } from './Button';
import { useStats } from '../hooks/useStats';

const ASCII_LOGO = `
███████╗██╗ ██╗██╗██╗ ██╗ ██╗ ██╗██╗████████╗
Expand Down Expand Up @@ -60,6 +61,7 @@ export function Hero(): React.ReactElement {
const [visibleLines, setVisibleLines] = useState(0);
const [typingIndex, setTypingIndex] = useState(0);
const [currentText, setCurrentText] = useState('');
const stats = useStats();

useEffect(() => {
if (visibleLines >= TERMINAL_LINES.length) {
Expand Down Expand Up @@ -126,7 +128,7 @@ export function Hero(): React.ReactElement {
<div className="animate-fade-in">
<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">
<span className="flex h-1.5 w-1.5 bg-white rounded-full"></span>
<span className="text-xs font-mono text-zinc-400">v1.8.0</span>
<span className="text-xs font-mono text-zinc-400">v{stats.version}</span>
</div>

<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold tracking-tight text-white mb-3 font-mono">
Expand Down
119 changes: 119 additions & 0 deletions docs/skillkit/hooks/useStats.ts
Original file line number Diff line number Diff line change
@@ -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<Stats, 'loading'>;
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<Stats, 'loading'>): 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<Stats>({
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<void> {
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 (npmData.downloads) {
downloads = formatDownloads(npmData.downloads);
}
}

if (githubResponse.status === 'fulfilled' && githubResponse.value.ok) {
const githubData = await githubResponse.value.json();
if (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;
}