Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 7 additions & 4 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 @@ -102,7 +105,7 @@ export default function App(): React.ReactElement {
</nav>

<main className="pt-14">
<Hero />
<Hero version={stats.version} />

<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))' }}>
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
Expand All @@ -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
8 changes: 6 additions & 2 deletions docs/skillkit/components/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useState, useEffect } from 'react';
import { Button } from './Button';

interface HeroProps {
version: string;
}

const ASCII_LOGO = `
███████╗██╗ ██╗██╗██╗ ██╗ ██╗ ██╗██╗████████╗
██╔════╝██║ ██╔╝██║██║ ██║ ██║ ██╔╝██║╚══██╔══╝
Expand Down Expand Up @@ -55,7 +59,7 @@ const COPY_ICON = (
</svg>
);

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);
Expand Down Expand Up @@ -126,7 +130,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{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 (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;
}