11'use client'
22
33import clsx from 'clsx'
4- import { Github } from 'lucide-react'
4+ import { Github , Download } from 'lucide-react'
55import { Fragment } from 'react'
66import useSWR from 'swr'
77import type { BrandsMap } from '~/components/ui/brand'
@@ -12,12 +12,22 @@ import { Image } from '~/components/ui/image'
1212import { Link } from '~/components/ui/link'
1313import { TiltedGridBackground } from '~/components/ui/tilted-grid-background'
1414import type { PROJECTS } from '~/data/projects'
15- import type { GithubRepository } from '~/types/data'
15+ import type { GithubRepository , NpmPackage } from '~/types/data'
1616import { fetcher } from '~/utils/misc'
1717
1818export function ProjectCard ( { project } : { project : ( typeof PROJECTS ) [ 0 ] } ) {
19- const { title, description, imgSrc, url, repo, builtWith, links } = project
20- const { data : repository } = useSWR < GithubRepository > ( `/api/github?repo=${ repo } ` , fetcher )
19+ const { title, description, imgSrc, url, repo, npm, builtWith, links } = project
20+
21+ const { data : repository } = useSWR < GithubRepository > (
22+ repo && typeof repo === 'string' ? `/api/github?repo=${ repo } ` : null ,
23+ fetcher
24+ )
25+
26+ const { data : npmPackage } = useSWR < NpmPackage > (
27+ npm && typeof npm === 'string' ? `/api/npm?package=${ npm } ` : null ,
28+ fetcher
29+ )
30+
2131 const href = url || repository ?. url
2232 const lang = repository ?. languages ?. [ 0 ]
2333
@@ -42,58 +52,90 @@ export function ProjectCard({ project }: { project: (typeof PROJECTS)[0] }) {
4252 </ div >
4353 </ div >
4454 < p className = "mb-16 line-clamp-3 grow text-lg" > { repository ?. description || description } </ p >
45- < div
46- className = { clsx (
47- 'mt-auto flex gap-6 sm:gap-9 md:grid md:gap-0' ,
48- repository ? 'grid-cols-3' : 'grid-cols-2'
49- ) }
50- >
51- { repository ? (
52- < div className = "space-y-1.5" >
53- < div className = "text-xs text-gray-600 dark:text-gray-400" >
54- < span className = "hidden sm:inline" > Github stars</ span >
55- < span className = "sm:hidden" > Stars</ span >
56- </ div >
57- < div className = "flex items-center gap-2" >
58- < div className = "flex items-center space-x-1.5" >
59- < Github size = { 16 } strokeWidth = { 1.5 } />
60- < span className = "font-medium" > { repository ?. stargazerCount } </ span >
55+ < div className = { clsx ( 'mt-auto flex gap-6 sm:gap-9 md:grid md:gap-0' , `grid-cols-3` ) } >
56+ { /* NPM Downloads */ }
57+ { npmPackage
58+ ? ( npmPackage || ( npm && typeof npm === 'object' ) ) && (
59+ < div className = "space-y-1.5" >
60+ < div className = "text-xs text-gray-600 dark:text-gray-400" >
61+ < span className = "hidden sm:inline" > Monthly downloads</ span >
62+ < span className = "sm:hidden" > Downloads</ span >
63+ </ div >
64+ < div className = "flex items-center gap-2" >
65+ < div className = "flex items-center space-x-1.5" >
66+ < Download size = { 16 } strokeWidth = { 1.5 } />
67+ < span className = "font-medium" >
68+ { npmPackage ?. downloads ||
69+ ( npm && typeof npm === 'object' ? npm . downloads : 0 ) }
70+ </ span >
71+ </ div >
72+ </ div >
73+ </ div >
74+ )
75+ : repository
76+ ? ( repository || ( repo && typeof repo === 'object' ) ) && (
77+ < div className = "space-y-1.5" >
78+ < div className = "text-xs text-gray-600 dark:text-gray-400" >
79+ < span className = "hidden sm:inline" > Github stars</ span >
80+ < span className = "sm:hidden" > Stars</ span >
81+ </ div >
82+ < div className = "flex items-center gap-2" >
83+ < div className = "flex items-center space-x-1.5" >
84+ < Github size = { 16 } strokeWidth = { 1.5 } />
85+ < span className = "font-medium" >
86+ { repository ?. stargazerCount ||
87+ ( repo && typeof repo === 'object' ? repo . stargazerCount : 0 ) }
88+ </ span >
89+ </ div >
90+ </ div >
91+ </ div >
92+ )
93+ : null }
94+
95+ { /* Links (only show if no repo and no npm) */ }
96+ { ! repository &&
97+ ! npmPackage &&
98+ ! ( repo && typeof repo === 'object' ) &&
99+ ! ( npm && typeof npm === 'object' ) &&
100+ links && (
101+ < div className = "space-y-1.5" >
102+ < div className = "text-xs text-gray-600 dark:text-gray-400" > Links</ div >
103+ < div className = "flex flex-col items-start gap-0.5 sm:flex-row sm:items-center sm:gap-1.5" >
104+ { links ?. map ( ( { title, url } , idx ) => (
105+ < Fragment key = { url } >
106+ < Link href = { url } className = "flex items-center gap-1.5" >
107+ < GrowingUnderline className = "font-medium" data-umami-event = "project-link" >
108+ { title }
109+ </ GrowingUnderline >
110+ </ Link >
111+ { idx !== links . length - 1 && (
112+ < span className = "hidden text-gray-400 dark:text-gray-500 md:inline" > |</ span >
113+ ) }
114+ </ Fragment >
115+ ) ) }
61116 </ div >
62117 </ div >
63- </ div >
64- ) : links ? (
118+ ) }
119+
120+ { /* Stack */ }
121+ { builtWith && builtWith . length > 0 && (
65122 < div className = "space-y-1.5" >
66- < div className = "text-xs text-gray-600 dark:text-gray-400" > Links</ div >
67- < div className = "flex flex-col items-start gap-0.5 sm:flex-row sm:items-center sm:gap-1.5" >
68- { links ?. map ( ( { title, url } , idx ) => (
69- < Fragment key = { url } >
70- < Link href = { url } className = "flex items-center gap-1.5" >
71- < GrowingUnderline className = "font-medium" data-umami-event = "project-link" >
72- { title }
73- </ GrowingUnderline >
74- </ Link >
75- { idx !== links . length - 1 && (
76- < span className = "hidden text-gray-400 dark:text-gray-500 md:inline" > |</ span >
77- ) }
78- </ Fragment >
79- ) ) }
123+ < div className = "text-xs text-gray-600 dark:text-gray-400" > Stack</ div >
124+ < div className = "flex h-6 flex-wrap items-center gap-1.5" >
125+ { builtWith ?. map ( ( tool ) => {
126+ return (
127+ < Brand
128+ key = { tool }
129+ name = { tool as keyof typeof BrandsMap }
130+ iconClassName = { clsx ( tool === 'Pygame' ? 'h-4' : 'h-4 w-4' ) }
131+ />
132+ )
133+ } ) }
80134 </ div >
81135 </ div >
82- ) : null }
83- < div className = "space-y-1.5" >
84- < div className = "text-xs text-gray-600 dark:text-gray-400" > Stack</ div >
85- < div className = "flex h-6 flex-wrap items-center gap-1.5" >
86- { builtWith ?. map ( ( tool ) => {
87- return (
88- < Brand
89- key = { tool }
90- name = { tool as keyof typeof BrandsMap }
91- iconClassName = { clsx ( tool === 'Pygame' ? 'h-4' : 'h-4 w-4' ) }
92- />
93- )
94- } ) }
95- </ div >
96- </ div >
136+ ) }
137+
138+ { /* Language */ }
97139 { lang && (
98140 < div className = "space-y-1.5" >
99141 < div className = "text-xs text-gray-600 dark:text-gray-400" > Language</ div >
0 commit comments