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+ }
0 commit comments