Skip to content

Commit 9a8a544

Browse files
committed
improve npm package stats
1 parent ec7eae5 commit 9a8a544

File tree

3 files changed

+124
-36
lines changed

3 files changed

+124
-36
lines changed

apps/dashboard/src/NpmDownloads.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PackageDownloadsList } from './components/npm/PackageDownloadsList';
44
// List of npm packages to display download counts for
55
const packages = [
66
'vite',
7+
'vitest',
78
'rolldown-vite',
89
'rolldown',
910
'tsdown',
@@ -17,10 +18,10 @@ const packages = [
1718
function NpmDownloads() {
1819
return (
1920
<>
20-
<main className='max-w-6xl mx-auto px-8 py-8 flex flex-col gap-8'>
21-
<div className='bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-8 py-8 rounded-xl shadow-sm'>
21+
<main className='max-w-7xl mx-auto px-8 py-8 flex flex-col gap-8'>
22+
<div>
2223
<h2 className='mb-6 text-slate-800 dark:text-slate-100 text-3xl font-bold tracking-tight'>
23-
NPM Weekly Downloads
24+
NPM Package Statistics
2425
</h2>
2526

2627
<PackageDownloadsList packages={packages} />
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Card } from '@vibe/ui';
2+
import { Download, Package } from 'lucide-react';
3+
import { useEffect, useState } from 'react';
4+
5+
interface PackageCardProps {
6+
packageName: string;
7+
}
8+
9+
function formatNumber(num: number): string {
10+
if (num >= 1000000) {
11+
return `${(num / 1000000).toFixed(1)}M`;
12+
}
13+
if (num >= 1000) {
14+
return `${(num / 1000).toFixed(1)}K`;
15+
}
16+
return num.toLocaleString();
17+
}
18+
19+
export function PackageCard({ packageName }: PackageCardProps) {
20+
const [downloads, setDownloads] = useState<number | null>(null);
21+
const [version, setVersion] = useState<string | null>(null);
22+
const [loading, setLoading] = useState(true);
23+
24+
useEffect(() => {
25+
const fetchPackageData = async () => {
26+
try {
27+
const [downloadsRes, packageRes] = await Promise.all([
28+
fetch(`https://api.npmjs.org/downloads/point/last-week/${packageName}`),
29+
fetch(`https://registry.npmjs.org/${packageName}/latest`),
30+
]);
31+
32+
if (downloadsRes.ok) {
33+
const downloadsData = await downloadsRes.json();
34+
setDownloads(downloadsData.downloads);
35+
}
36+
37+
if (packageRes.ok) {
38+
const packageData = await packageRes.json();
39+
setVersion(packageData.version);
40+
}
41+
} catch (error) {
42+
console.error(`Failed to fetch data for ${packageName}:`, error);
43+
} finally {
44+
setLoading(false);
45+
}
46+
};
47+
48+
void fetchPackageData();
49+
}, [packageName]);
50+
51+
const handleClick = () => {
52+
window.open(`https://www.npmjs.com/package/${packageName}`, '_blank', 'noopener,noreferrer');
53+
};
54+
55+
return (
56+
<Card className='hover:shadow-lg transition-all cursor-pointer group'>
57+
<div
58+
className='p-6'
59+
onClick={handleClick}
60+
onKeyDown={(e) => {
61+
if (e.key === 'Enter' || e.key === ' ') {
62+
e.preventDefault();
63+
handleClick();
64+
}
65+
}}
66+
role='button'
67+
tabIndex={0}
68+
>
69+
<div className='flex items-start justify-between mb-4'>
70+
<h3 className='font-mono text-lg font-semibold text-slate-800 dark:text-slate-100 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors'>
71+
{packageName}
72+
</h3>
73+
<Package className='w-5 h-5 text-slate-400 dark:text-slate-500' />
74+
</div>
75+
76+
<div className='space-y-3'>
77+
<div className='flex items-center gap-2'>
78+
<div className='p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg'>
79+
<Package className='w-4 h-4 text-blue-600 dark:text-blue-400' />
80+
</div>
81+
<div>
82+
<p className='text-xs text-slate-500 dark:text-slate-400'>Version</p>
83+
<p className='text-sm font-semibold text-slate-900 dark:text-white'>
84+
{loading ? (
85+
<span className='inline-block h-4 w-16 bg-slate-200 dark:bg-slate-700 rounded animate-pulse' />
86+
) : (
87+
version || 'N/A'
88+
)}
89+
</p>
90+
</div>
91+
</div>
92+
93+
<div className='flex items-center gap-2'>
94+
<div className='p-2 bg-green-100 dark:bg-green-900/30 rounded-lg'>
95+
<Download className='w-4 h-4 text-green-600 dark:text-green-400' />
96+
</div>
97+
<div>
98+
<p className='text-xs text-slate-500 dark:text-slate-400'>Weekly Downloads</p>
99+
<p className='text-sm font-semibold text-slate-900 dark:text-white'>
100+
{loading ? (
101+
<span className='inline-block h-4 w-20 bg-slate-200 dark:bg-slate-700 rounded animate-pulse' />
102+
) : downloads !== null ? (
103+
formatNumber(downloads)
104+
) : (
105+
'N/A'
106+
)}
107+
</p>
108+
</div>
109+
</div>
110+
</div>
111+
</div>
112+
</Card>
113+
);
114+
}
Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,15 @@
1+
import { PackageCard } from './PackageCard';
2+
13
interface PackageDownloadsListProps {
24
packages: string[];
35
}
46

57
export function PackageDownloadsList({ packages }: PackageDownloadsListProps) {
6-
const handleCardClick = (packageName: string) => {
7-
const npmUrl = `https://www.npmjs.com/package/${packageName}`;
8-
window.open(npmUrl, '_blank', 'noopener,noreferrer');
9-
};
10-
118
return (
12-
<div className='mx-auto mb-8 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shadow-sm max-w-lg w-fit'>
13-
<ul className='list-none p-0 m-0'>
14-
{packages.map((packageName) => (
15-
<li
16-
key={packageName}
17-
className='flex items-center justify-between px-4 py-2 border-b border-slate-200 dark:border-slate-700 transition-all duration-200 cursor-pointer gap-3 min-w-fit hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-2 focus:outline-blue-500 focus:outline-offset-[-2px] focus:bg-slate-50 dark:focus:bg-slate-700 last:border-b-0'
18-
onClick={() => handleCardClick(packageName)}
19-
role='button'
20-
tabIndex={0}
21-
onKeyDown={(e) => {
22-
if (e.key === 'Enter' || e.key === ' ') {
23-
e.preventDefault();
24-
handleCardClick(packageName);
25-
}
26-
}}
27-
>
28-
<span className='font-mono text-sm font-medium text-gray-700 dark:text-gray-300 bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded border border-slate-300 dark:border-slate-600 min-w-fit whitespace-nowrap'>
29-
{packageName}
30-
</span>
31-
<img
32-
className='h-auto max-h-5 flex-shrink-0'
33-
src={`https://img.shields.io/npm/dw/${packageName}?label=npm`}
34-
alt={`Weekly downloads for ${packageName}`}
35-
loading='lazy'
36-
/>
37-
</li>
38-
))}
39-
</ul>
9+
<div className='grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'>
10+
{packages.map((packageName) => (
11+
<PackageCard key={packageName} packageName={packageName} />
12+
))}
4013
</div>
4114
);
4215
}

0 commit comments

Comments
 (0)