feat: implement projects page

feat/projects-page
Arik Chakma 1 month ago
parent dae737fa02
commit 88bad8fc6a
  1. 150
      src/components/Projects/ProjectsPage.tsx
  2. 17
      src/components/Projects/ProjectsPageHeader.tsx
  3. 19
      src/lib/project.ts
  4. 42
      src/pages/projects/index.astro

@ -0,0 +1,150 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { cn } from '../../lib/classname.ts';
import { Filter, X } from 'lucide-react';
import {
deleteUrlParam,
getUrlParams,
setUrlParams,
} from '../../lib/browser.ts';
import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx';
import {
projectDifficulties,
type ProjectFileType,
} from '../../lib/project.ts';
import { ProjectCard } from './ProjectCard.tsx';
type ProjectsPageProps = {
roadmapsProjects: {
id: string;
title: string;
projects: ProjectFileType[];
}[];
userCounts: Record<string, number>;
};
export function ProjectsPage(props: ProjectsPageProps) {
const { roadmapsProjects, userCounts } = props;
const allUniqueProjectIds = new Set<string>(
roadmapsProjects.flatMap((group) =>
group.projects.map((project) => project.id),
),
);
const allUniqueProjects = useMemo(
() =>
Array.from(allUniqueProjectIds)
.map((id) =>
roadmapsProjects
.flatMap((group) => group.projects)
.find((project) => project.id === id),
)
.filter(Boolean) as ProjectFileType[],
[allUniqueProjectIds],
);
const [activeGroup, setActiveGroup] = useState<string>('');
const [visibleProjects, setVisibleProjects] =
useState<ProjectFileType[]>(allUniqueProjects);
const [isFilterOpen, setIsFilterOpen] = useState(false);
useEffect(() => {
const { g } = getUrlParams() as { g: string };
if (!g) {
return;
}
setActiveGroup(g);
const group = roadmapsProjects.find((group) => group.id === g);
if (!group) {
return;
}
setVisibleProjects(group.projects);
}, []);
const sortedVisibleProjects = useMemo(
() =>
visibleProjects.sort((a, b) => {
const projectADifficulty = a?.frontmatter.difficulty || 'beginner';
const projectBDifficulty = b?.frontmatter.difficulty || 'beginner';
return (
projectDifficulties.indexOf(projectADifficulty) -
projectDifficulties.indexOf(projectBDifficulty)
);
}),
[visibleProjects],
);
return (
<div className="border-t bg-gray-100">
<button
onClick={() => {
setIsFilterOpen(!isFilterOpen);
}}
id="filter-button"
className={cn(
'-mt-1 flex w-full items-center justify-center bg-gray-300 py-2 text-sm text-black focus:shadow-none focus:outline-0 sm:hidden',
{
'mb-3': !isFilterOpen,
},
)}
>
{!isFilterOpen && <Filter size={13} className="mr-1" />}
{isFilterOpen && <X size={13} className="mr-1" />}
Categories
</button>
<div className="container relative flex flex-col gap-4 sm:flex-row">
<div
className={cn(
'hidden w-full flex-col from-gray-100 sm:w-[160px] sm:shrink-0 sm:border-r sm:bg-gradient-to-l sm:pt-6',
{
'hidden sm:flex': !isFilterOpen,
'z-50 flex': isFilterOpen,
},
)}
>
<div className="absolute top-0 -mx-4 w-full bg-white pb-0 shadow-xl sm:sticky sm:top-10 sm:mx-0 sm:bg-transparent sm:pb-20 sm:shadow-none">
<div className="grid grid-cols-1">
<CategoryFilterButton
onClick={() => {
setActiveGroup('');
setVisibleProjects(allUniqueProjects);
setIsFilterOpen(false);
deleteUrlParam('g');
}}
category={'All Projects'}
selected={activeGroup === ''}
/>
{roadmapsProjects.map((group) => (
<CategoryFilterButton
key={group.id}
onClick={() => {
setActiveGroup(group.id);
setIsFilterOpen(false);
document?.getElementById('filter-button')?.scrollIntoView();
setVisibleProjects(group.projects);
setUrlParams({ g: group.id });
}}
category={group.title}
selected={activeGroup === group.id}
/>
))}
</div>
</div>
</div>
<div className="flex flex-grow flex-col gap-6 pb-20 pt-2 sm:pt-6">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
{sortedVisibleProjects.map((project) => (
<ProjectCard
key={project.id}
project={project}
userCount={userCounts[project.id] || 0}
/>
))}
</div>
</div>
</div>
</div>
);
}

@ -0,0 +1,17 @@
import { isLoggedIn } from '../../lib/jwt.ts';
import { showLoginPopup } from '../../lib/popup.ts';
export function ProjectsPageHeader() {
return (
<div className="bg-white py-3 sm:py-12">
<div className="container">
<div className="flex flex-col items-start bg-white sm:items-center">
<h1 className="text-2xl font-bold sm:text-5xl">Project Ideas</h1>
<p className="mt-1 text-sm sm:my-3 sm:text-lg">
Browse the ever-growing list of projects ideas and solutions.
</p>
</div>
</div>
</div>
);
}

@ -93,3 +93,22 @@ export async function getProjectById(
id: projectPathToId(project.file),
};
}
export async function getRoadmapsProjects(): Promise<
Record<string, ProjectFileType[]>
> {
const projects = await getAllProjects();
const roadmapsProjects: Record<string, ProjectFileType[]> = {};
projects.forEach((project) => {
project.frontmatter.roadmapIds.forEach((roadmapId) => {
if (!roadmapsProjects[roadmapId]) {
roadmapsProjects[roadmapId] = [];
}
roadmapsProjects[roadmapId].push(project);
});
});
return roadmapsProjects;
}

@ -0,0 +1,42 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getRoadmapsProjects } from '../../lib/project';
import { getRoadmapsByIds } from '../../lib/roadmap';
import { ProjectsPageHeader } from '../../components/Projects/ProjectsPageHeader';
import { ProjectsPage } from '../../components/Projects/ProjectsPage';
import { projectApi } from '../../api/project';
const roadmapProjects = await getRoadmapsProjects();
const allRoadmapIds = Object.keys(roadmapProjects);
const allRoadmaps = await getRoadmapsByIds(allRoadmapIds);
const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projects = roadmapProjects[roadmap.id];
return {
id: roadmap.id,
title: roadmap.frontmatter.briefTitle,
projects,
};
});
const projectIds = allRoadmapIds
.map((id) => roadmapProjects[id])
.flat()
.map((project) => project.id);
const projectApiClient = projectApi(Astro);
const { response: userCounts } =
await projectApiClient.listProjectsUserCount(projectIds);
---
<BaseLayout
title='Project Ideas'
description='Explore project ideas to take you from beginner to advanced in different technologies'
permalink='/projects'
>
<ProjectsPageHeader client:load />
<ProjectsPage
roadmapsProjects={enrichedRoadmaps}
userCounts={userCounts || {}}
client:load
/>
</BaseLayout>
Loading…
Cancel
Save