feat: implement projects page (#7067)
parent
8c615084d3
commit
e3b6bacbc4
4 changed files with 228 additions and 0 deletions
@ -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> |
||||
); |
||||
} |
@ -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…
Reference in new issue