Skip to content

Commit d1eeb01

Browse files
authored
feat(projects): add client-side category filters (#72)
1 parent ad336e9 commit d1eeb01

2 files changed

Lines changed: 88 additions & 16 deletions

File tree

src/components/ProjectCard.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ interface Props {
1313
image?: ImageMetadata;
1414
stars?: number;
1515
downloads?: number;
16+
category?: string;
1617
}
1718
18-
const { title, slug, description, techStack, repoUrl, image, stars, downloads } = Astro.props;
19+
const { title, slug, description, techStack, repoUrl, image, stars, downloads, category } = Astro.props;
1920
---
2021

21-
<article class="bg-background-2 rounded-lg overflow-hidden hover:bg-background-3 transition-colors relative flex flex-col">
22+
<article class="project-card bg-background-2 rounded-lg overflow-hidden hover:bg-background-3 transition-colors relative flex flex-col" data-category={category}>
2223
{image && (
2324
<a href={`/projects/${slug}/`} class="block aspect-video overflow-hidden">
2425
<Image

src/pages/projects/[...page].astro

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import type { GetStaticPaths } from 'astro';
33
import BaseLayout from '../../layouts/BaseLayout.astro';
44
import ProjectCard from '../../components/ProjectCard.astro';
5-
import Pagination from '../../components/Pagination.astro';
65
import { getProjectsWithStats, getProjectSlug } from '../../lib/projects';
76
8-
export const getStaticPaths = (async ({ paginate }) => {
7+
export const getStaticPaths = (async () => {
98
const allProjects = await getProjectsWithStats();
109
// Sort by GitHub stars (highest first), using live stats when available
1110
const sortedProjects = allProjects.sort((a, b) => {
@@ -14,10 +13,32 @@ export const getStaticPaths = (async ({ paginate }) => {
1413
return bStars - aStars;
1514
});
1615
17-
return paginate(sortedProjects, { pageSize: 9 });
16+
return [{ params: { page: undefined }, props: { projects: sortedProjects } }];
1817
}) satisfies GetStaticPaths;
1918
20-
const { page } = Astro.props;
19+
const { projects } = Astro.props;
20+
21+
// Category labels for filter buttons
22+
const categoryLabels: Record<string, string> = {
23+
'vs-extension': 'VS Extensions',
24+
'vscode-extension': 'VS Code Extensions',
25+
'github-action': 'GitHub Actions',
26+
'cli-tool': 'CLI Tools',
27+
'nuget-package': 'NuGet Packages',
28+
'desktop-app': 'Desktop Apps',
29+
'documentation': 'Documentation',
30+
};
31+
32+
// Get unique categories from projects, sorted by count (descending)
33+
const categoryCounts = projects.reduce((acc, project) => {
34+
const cat = project.data.category;
35+
acc[cat] = (acc[cat] || 0) + 1;
36+
return acc;
37+
}, {} as Record<string, number>);
38+
39+
const categories = Object.entries(categoryCounts)
40+
.sort((a, b) => b[1] - a[1])
41+
.map(([cat]) => cat);
2142
---
2243

2344
<BaseLayout title="Open Source Projects" description="Open source projects by Calvin Allen" image="/images/projects-og.png">
@@ -28,8 +49,28 @@ const { page } = Astro.props;
2849
A collection of open source projects I've created and maintain, sorted by GitHub stars.
2950
</p>
3051

31-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
32-
{page.data.map(project => {
52+
<!-- Filter Buttons -->
53+
<div class="flex flex-wrap gap-2 mb-8" id="filter-buttons">
54+
<button
55+
type="button"
56+
class="filter-btn px-4 py-2 rounded-full text-sm font-medium transition-colors bg-primary text-white"
57+
data-category="all"
58+
>
59+
All ({projects.length})
60+
</button>
61+
{categories.map(cat => (
62+
<button
63+
type="button"
64+
class="filter-btn px-4 py-2 rounded-full text-sm font-medium transition-colors bg-background-2 text-text-muted hover:bg-background-3"
65+
data-category={cat}
66+
>
67+
{categoryLabels[cat] || cat} ({categoryCounts[cat]})
68+
</button>
69+
))}
70+
</div>
71+
72+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="projects-grid">
73+
{projects.map(project => {
3374
const stars = project.githubStats?.stars ?? project.data.stars;
3475
const downloads = project.marketplaceStats?.downloads ?? project.marketplaceStats?.installs ?? project.data.downloads;
3576
return (
@@ -42,18 +83,48 @@ const { page } = Astro.props;
4283
image={project.resolvedImage}
4384
stars={stars}
4485
downloads={downloads}
86+
category={project.data.category}
4587
/>
4688
);
4789
})}
4890
</div>
49-
50-
{page.lastPage > 1 && (
51-
<Pagination
52-
currentPage={page.currentPage}
53-
totalPages={page.lastPage}
54-
baseUrl="/projects/"
55-
/>
56-
)}
5791
</div>
5892
</section>
5993
</BaseLayout>
94+
95+
<script>
96+
function initProjectFilters() {
97+
const filterButtons = document.querySelectorAll('.filter-btn');
98+
const projectCards = document.querySelectorAll('.project-card');
99+
100+
filterButtons.forEach(button => {
101+
button.addEventListener('click', () => {
102+
const category = (button as HTMLElement).dataset.category;
103+
104+
// Update active button styling
105+
filterButtons.forEach(btn => {
106+
btn.classList.remove('bg-primary', 'text-white');
107+
btn.classList.add('bg-background-2', 'text-text-muted', 'hover:bg-background-3');
108+
});
109+
button.classList.remove('bg-background-2', 'text-text-muted', 'hover:bg-background-3');
110+
button.classList.add('bg-primary', 'text-white');
111+
112+
// Filter project cards
113+
projectCards.forEach(card => {
114+
const cardCategory = (card as HTMLElement).dataset.category;
115+
if (category === 'all' || cardCategory === category) {
116+
(card as HTMLElement).style.display = '';
117+
} else {
118+
(card as HTMLElement).style.display = 'none';
119+
}
120+
});
121+
});
122+
});
123+
}
124+
125+
// Run on page load
126+
document.addEventListener('DOMContentLoaded', initProjectFilters);
127+
128+
// Also run on Astro page transitions (View Transitions API)
129+
document.addEventListener('astro:page-load', initProjectFilters);
130+
</script>

0 commit comments

Comments
 (0)