diff --git a/src/components/Projects/ProjectsPage.tsx b/src/components/Projects/ProjectsPage.tsx new file mode 100644 index 000000000..d1304c32d --- /dev/null +++ b/src/components/Projects/ProjectsPage.tsx @@ -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; +}; + +export function ProjectsPage(props: ProjectsPageProps) { + const { roadmapsProjects, userCounts } = props; + const allUniqueProjectIds = new Set( + 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(''); + const [visibleProjects, setVisibleProjects] = + useState(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 ( +
+ +
+
+
+
+ { + setActiveGroup(''); + setVisibleProjects(allUniqueProjects); + setIsFilterOpen(false); + deleteUrlParam('g'); + }} + category={'All Projects'} + selected={activeGroup === ''} + /> + + {roadmapsProjects.map((group) => ( + { + setActiveGroup(group.id); + setIsFilterOpen(false); + document?.getElementById('filter-button')?.scrollIntoView(); + setVisibleProjects(group.projects); + setUrlParams({ g: group.id }); + }} + category={group.title} + selected={activeGroup === group.id} + /> + ))} +
+
+
+
+
+ {sortedVisibleProjects.map((project) => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/components/Projects/ProjectsPageHeader.tsx b/src/components/Projects/ProjectsPageHeader.tsx new file mode 100644 index 000000000..76f30f948 --- /dev/null +++ b/src/components/Projects/ProjectsPageHeader.tsx @@ -0,0 +1,17 @@ +import { isLoggedIn } from '../../lib/jwt.ts'; +import { showLoginPopup } from '../../lib/popup.ts'; + +export function ProjectsPageHeader() { + return ( +
+
+
+

Project Ideas

+

+ Browse the ever-growing list of projects ideas and solutions. +

+
+
+
+ ); +} diff --git a/src/lib/project.ts b/src/lib/project.ts index 8bd37bc24..011f829f7 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -93,3 +93,22 @@ export async function getProjectById( id: projectPathToId(project.file), }; } + +export async function getRoadmapsProjects(): Promise< + Record +> { + const projects = await getAllProjects(); + const roadmapsProjects: Record = {}; + + projects.forEach((project) => { + project.frontmatter.roadmapIds.forEach((roadmapId) => { + if (!roadmapsProjects[roadmapId]) { + roadmapsProjects[roadmapId] = []; + } + + roadmapsProjects[roadmapId].push(project); + }); + }); + + return roadmapsProjects; +} diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro new file mode 100644 index 000000000..61acb6d81 --- /dev/null +++ b/src/pages/projects/index.astro @@ -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); +--- + + + + +