Skip to content

Commit 7ccd7f0

Browse files
committed
feat: redesign projects page with modern card layout
1 parent b77067f commit 7ccd7f0

File tree

2 files changed

+75
-88
lines changed

2 files changed

+75
-88
lines changed

app/projects/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,23 @@ export default async function Projects() {
1717
description="Collections of my open-source side projects, along with some cool things I’ve built with colleagues at work. It’s a mix of passion projects and practical tools—some just for fun, others to solve real-world problems."
1818
className="border-b border-gray-200 dark:border-gray-700"
1919
/>
20-
<div className="py-5 md:py-10">
21-
<h3 className="mb-6 text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-3xl">
20+
21+
<div className="py-10">
22+
<h3 className="mb-8 text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-3xl">
2223
Work
2324
</h3>
24-
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
25+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
2526
{workProjects.map((pro) => (
2627
<ProjectCard key={pro.title} project={pro} />
2728
))}
2829
</div>
2930
</div>
30-
<div className="mt-6 border-t border-gray-200 py-5 dark:border-gray-700 md:mt-10 md:py-10">
31-
<h3 className="mb-6 text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:mb-8 md:text-3xl">
31+
32+
<div className="border-t border-gray-200 py-10 dark:border-gray-700">
33+
<h3 className="mb-8 text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-3xl">
3234
Side projects
3335
</h3>
34-
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
36+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
3537
{sideProjects.map((pro) => (
3638
<ProjectCard key={pro.title} project={pro} />
3739
))}

components/cards/project/index.tsx

Lines changed: 67 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,60 @@
11
'use client'
22

33
import clsx from 'clsx'
4-
import { Github, Download } from 'lucide-react'
4+
import { Github, Download, ExternalLink } from 'lucide-react'
55
import useSWR from 'swr'
6-
import type { BrandsMap } from '~/components/ui/brand'
7-
import { Brand } from '~/components/ui/brand'
8-
import { GradientBorder } from '~/components/effects/gradient-border'
9-
import { GrowingUnderline } from '~/components/effects/growing-underline'
6+
import { Brand, type BrandsMap } from '~/components/ui/brand'
107
import { Image } from '~/components/ui/image'
118
import { Link } from '~/components/ui/link'
12-
import { TiltedGridBackground } from '~/components/effects/tilted-grid-background'
139
import type { PROJECTS } from '~/data/projects'
1410
import type { GithubRepository, NpmPackage } from '~/types/data'
1511
import { fetcher } from '~/utils/misc'
1612

1713
function NpmStats({ npmPackageName, downloads }: { npmPackageName: string; downloads: number }) {
1814
return (
19-
<div className="space-y-1.5">
20-
<div className="text-xs text-gray-600 dark:text-gray-400">Downloads</div>
21-
<div className="flex items-center gap-2">
22-
<Link
23-
href={`https://www.npmjs.com/package/${npmPackageName}`}
24-
className="flex items-center gap-1.5"
25-
>
26-
<GrowingUnderline data-umami-event="project-npm-link">
27-
<div className="flex items-center space-x-1.5">
28-
<Download size={16} strokeWidth={1.5} />
29-
<span className="font-medium">{downloads}</span>
30-
</div>
31-
</GrowingUnderline>
32-
</Link>
33-
</div>
34-
</div>
15+
<Link
16+
href={`https://www.npmjs.com/package/${npmPackageName}`}
17+
className="flex items-center gap-1.5 text-xs text-gray-500 transition-colors hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400"
18+
>
19+
<Download size={14} strokeWidth={1.5} />
20+
<span>{downloads.toLocaleString()} downloads</span>
21+
</Link>
3522
)
3623
}
3724

3825
function GithubStats({ repository }: { repository: GithubRepository }) {
3926
return (
40-
<div className="space-y-1.5">
41-
<div className="text-xs text-gray-600 dark:text-gray-400">
42-
<span className="hidden sm:inline">Github stars</span>
43-
<span className="sm:hidden">Stars</span>
44-
</div>
45-
<div className="flex items-center gap-2">
46-
<Link href={repository.url} className="flex items-center gap-1.5">
47-
<GrowingUnderline data-umami-event="project-github-link">
48-
<div className="flex items-center space-x-1.5">
49-
<Github size={16} strokeWidth={1.5} />
50-
<span className="font-medium">{repository.stargazerCount}</span>
51-
</div>
52-
</GrowingUnderline>
53-
</Link>
54-
</div>
55-
</div>
27+
<Link
28+
href={repository.url}
29+
className="flex items-center gap-1.5 text-xs text-gray-500 transition-colors hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400"
30+
>
31+
<Github size={14} strokeWidth={1.5} />
32+
<span>{repository.stargazerCount.toLocaleString()} stars</span>
33+
</Link>
5634
)
5735
}
5836

5937
function Stack({ builtWith }: { builtWith: string[] }) {
6038
return (
61-
<div className="space-y-1.5">
62-
<div className="text-xs text-gray-600 dark:text-gray-400">Stack</div>
63-
<div className="flex h-6 flex-wrap items-center gap-1.5">
64-
{builtWith?.map((tool) => {
65-
return (
66-
<Brand
67-
key={tool}
68-
name={tool as keyof typeof BrandsMap}
69-
iconClassName={clsx(tool === 'Pygame' ? 'h-4' : 'h-4 w-4')}
70-
/>
71-
)
72-
})}
73-
</div>
39+
<div className="flex flex-wrap items-center gap-1.5">
40+
{builtWith?.map((tool) => (
41+
<div
42+
key={tool}
43+
className="flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300"
44+
>
45+
<Brand
46+
name={tool as keyof typeof BrandsMap}
47+
iconClassName={clsx(tool === 'Pygame' ? 'h-3 w-3' : 'h-3 w-3')}
48+
/>
49+
<span>{tool}</span>
50+
</div>
51+
))}
7452
</div>
7553
)
7654
}
7755

7856
export function ProjectCard({ project }: { project: (typeof PROJECTS)[0] }) {
79-
const { title, description, imgSrc, url, repo, npmPackageName, builtWith, links } = project
57+
const { title, description, imgSrc, url, repo, npmPackageName, builtWith } = project
8058

8159
const { data: repository } = useSWR<GithubRepository>(
8260
repo ? `/api/github?repo=${repo}` : null,
@@ -87,46 +65,53 @@ export function ProjectCard({ project }: { project: (typeof PROJECTS)[0] }) {
8765
npmPackageName ? `/api/npm?package=${npmPackageName}` : null,
8866
fetcher
8967
)
90-
const href = url || repository?.url
9168

92-
const propertyCount = (npmPackage ? 1 : 0) + (repository ? 1 : 0) + (builtWith.length > 0 ? 1 : 0)
69+
const href = url || repository?.url
9370

9471
return (
95-
<GradientBorder
96-
offset={28}
97-
className="flex flex-col rounded-[40px] p-6 [box-shadow:0_8px_32px_rgba(194,194,218,.3)] dark:bg-white/5 dark:shadow-none md:p-8"
98-
>
99-
<TiltedGridBackground className="inset-0 z-[-1] rounded-[40px]" />
100-
<div className="mb-6 flex items-center gap-4">
101-
<Image src={imgSrc} alt={title} width={100} height={100} className="h-15 w-15 shrink-0" />
102-
<div className="flex flex-col items-start gap-1 pt-1">
103-
<h2 className="text-[22px] font-bold leading-[30px]">
72+
<div className="group relative flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white transition-all hover:border-primary-500 hover:shadow-lg dark:border-gray-800 dark:bg-white/5 dark:hover:border-primary-500">
73+
<div className="relative aspect-[16/9] w-full overflow-hidden bg-gray-100 dark:bg-gray-800">
74+
<Image
75+
src={imgSrc}
76+
alt={title}
77+
fill
78+
className="object-cover transition-transform duration-500 group-hover:scale-105"
79+
/>
80+
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
81+
</div>
82+
83+
<div className="flex flex-1 flex-col p-5">
84+
<div className="mb-3 flex items-start justify-between gap-4">
85+
<h2 className="text-xl font-bold leading-tight text-gray-900 transition-colors group-hover:text-primary-600 dark:text-gray-100 dark:group-hover:text-primary-400">
10486
{href ? (
105-
<Link href={href} aria-label={`Link to ${title}`}>
106-
<GrowingUnderline data-umami-event="project-title-link">{title}</GrowingUnderline>
87+
<Link href={href} className="flex items-center gap-2">
88+
{title}
89+
<ExternalLink
90+
size={16}
91+
className="opacity-0 transition-opacity group-hover:opacity-100"
92+
/>
10793
</Link>
10894
) : (
10995
title
11096
)}
11197
</h2>
11298
</div>
99+
100+
<p className="mb-4 line-clamp-3 text-sm text-gray-600 dark:text-gray-400">{description}</p>
101+
102+
<div className="mt-auto flex flex-col gap-4">
103+
<Stack builtWith={builtWith} />
104+
105+
{(npmPackageName || repository) && (
106+
<div className="flex items-center gap-4 border-t border-gray-100 pt-3 dark:border-gray-700/50">
107+
{npmPackageName && (
108+
<NpmStats npmPackageName={npmPackageName} downloads={npmPackage?.downloads || 0} />
109+
)}
110+
{repository && <GithubStats repository={repository} />}
111+
</div>
112+
)}
113+
</div>
113114
</div>
114-
<p className="mb-16 line-clamp-3 grow text-lg">{description}</p>
115-
<div
116-
className={clsx('mt-auto flex gap-6 sm:gap-9 md:grid md:gap-0', {
117-
'md:grid-cols-1': propertyCount === 1,
118-
'md:grid-cols-2': propertyCount === 2,
119-
'md:grid-cols-3': propertyCount === 3,
120-
'md:grid-cols-4': propertyCount === 4,
121-
})}
122-
>
123-
{/* {projectProperty} */}
124-
{npmPackageName && (
125-
<NpmStats npmPackageName={npmPackageName} downloads={npmPackage?.downloads || 0} />
126-
)}
127-
{repository && <GithubStats repository={repository} />}
128-
{builtWith.length > 0 && <Stack builtWith={builtWith} />}
129-
</div>
130-
</GradientBorder>
115+
</div>
131116
)
132117
}

0 commit comments

Comments
 (0)