From 9355c1e77037adff095526e37c876eea53d3234d Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Mon, 5 Aug 2024 21:01:50 +0100 Subject: [PATCH] Projects listing and filtering --- src/components/Badge.tsx | 14 +-- src/components/Projects/ProjectCard.tsx | 35 ++++-- src/components/Projects/ProjectsList.tsx | 133 ++++++++++++++--------- src/components/RoadmapHeader.astro | 4 +- src/data/projects/stock-cli.md | 36 ++++++ src/data/projects/task-tracker-cli.md | 36 ++++++ src/lib/link-group.ts | 2 +- src/lib/project.ts | 82 ++++++++++++++ src/pages/[roadmapId]/index.astro | 3 + src/pages/[roadmapId]/projects.astro | 6 +- 10 files changed, 283 insertions(+), 68 deletions(-) create mode 100644 src/data/projects/stock-cli.md create mode 100644 src/data/projects/task-tracker-cli.md create mode 100644 src/lib/project.ts diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 835f726f6..e3c69646d 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -7,17 +7,17 @@ export function Badge(type: BadgeProps) { const { variant, text } = type; const colors = { - blue: 'bg-blue-100 text-blue-700', - green: 'bg-green-100 text-green-700', - red: 'bg-red-100 text-red-700', - yellow: 'bg-yellow-100 text-yellow-700', - grey: 'bg-gray-100 text-gray-700', - white: 'bg-white text-black', + blue: 'bg-blue-100 text-blue-700 border-blue-200', + green: 'bg-green-100 text-green-700 border-green-200', + red: 'bg-red-100 text-red-700 border-red-200', + yellow: 'bg-yellow-100 text-yellow-700 border-yellow-200', + grey: 'bg-gray-100 text-gray-700 border-gray-200', + white: 'bg-white text-black border-gray-200', }; return ( {text} diff --git a/src/components/Projects/ProjectCard.tsx b/src/components/Projects/ProjectCard.tsx index 488f8c1ce..3d3ced104 100644 --- a/src/components/Projects/ProjectCard.tsx +++ b/src/components/Projects/ProjectCard.tsx @@ -1,19 +1,38 @@ import { Badge } from '../Badge.tsx'; +import type { + ProjectDifficultyType, + ProjectFileType, +} from '../../lib/project.ts'; + +type ProjectCardProps = { + project: ProjectFileType; +}; + +const badgeVariants: Record = { + beginner: 'yellow', + intermediate: 'green', + advanced: 'red', +}; + +export function ProjectCard(props: ProjectCardProps) { + const { project } = props; + + const { frontmatter, id } = project; -export function ProjectCard() { return ( - - - - Bank Application - - Create a simple CLI to collect and calculate the taxes. + + + {frontmatter.title} + {frontmatter.description} ); } diff --git a/src/components/Projects/ProjectsList.tsx b/src/components/Projects/ProjectsList.tsx index 15ea3de66..3d786ca70 100644 --- a/src/components/Projects/ProjectsList.tsx +++ b/src/components/Projects/ProjectsList.tsx @@ -1,10 +1,20 @@ import { ProjectCard } from './ProjectCard.tsx'; -import { Diff, HeartHandshake } from 'lucide-react'; +import { HeartHandshake, Trash2 } from 'lucide-react'; import { cn } from '../../lib/classname.ts'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import { + projectDifficulties, + type ProjectDifficultyType, + type ProjectFileType, +} from '../../lib/project.ts'; +import { + deleteUrlParam, + getUrlParams, + setUrlParams, +} from '../../lib/browser.ts'; type DifficultyButtonProps = { - difficulty: 'beginner' | 'intermediate' | 'senior'; + difficulty: ProjectDifficultyType; isActive?: boolean; onClick?: () => void; }; @@ -28,36 +38,65 @@ function DifficultyButton(props: DifficultyButtonProps) { ); } -export function ProjectsList() { +type ProjectsListProps = { + projects: ProjectFileType[]; +}; + +export function ProjectsList(props: ProjectsListProps) { + const { projects } = props; + + const { difficulty: urlDifficulty } = getUrlParams(); const [difficulty, setDifficulty] = useState< - 'beginner' | 'intermediate' | 'senior' - >(); + ProjectDifficultyType | undefined + >(urlDifficulty); + + const projectsByDifficulty: Map = + useMemo(() => { + const result = new Map(); + + for (const project of projects) { + const difficulty = project.frontmatter.difficulty; + + if (!result.has(difficulty)) { + result.set(difficulty, []); + } + + result.get(difficulty)?.push(project); + } + + return result; + }, [projects]); + + const matchingProjects = difficulty + ? projectsByDifficulty.get(difficulty) || [] + : projects; return (
-
- { - setDifficulty('beginner'); - }} - difficulty={'beginner'} - isActive={difficulty === 'beginner'} - /> - { - setDifficulty('intermediate'); - }} - difficulty={'intermediate'} - isActive={difficulty === 'intermediate'} - /> - { - setDifficulty('senior'); - }} - difficulty={'senior'} - isActive={difficulty === 'senior'} - /> +
+ {projectDifficulties.map((projectDifficulty) => ( + { + setDifficulty(projectDifficulty); + setUrlParams({ difficulty: projectDifficulty }); + }} + difficulty={projectDifficulty} + isActive={projectDifficulty === difficulty} + /> + ))} + {difficulty && ( + + )}
- - - - - - - - - - - - - - - - - - - - - + {matchingProjects.length === 0 && ( +
+

No matching projects found.

+
+ )} + + {matchingProjects + .sort((project) => { + return project.frontmatter.difficulty === 'beginner' + ? -1 + : project.frontmatter.difficulty === 'intermediate' + ? 0 + : 1; + }) + .map((matchingProject) => ( + + ))}
); diff --git a/src/components/RoadmapHeader.astro b/src/components/RoadmapHeader.astro index 1e433b88b..338b9bf43 100644 --- a/src/components/RoadmapHeader.astro +++ b/src/components/RoadmapHeader.astro @@ -21,6 +21,7 @@ export interface Props { roadmapId: string; isUpcoming?: boolean; hasSearch?: boolean; + projectCount?: number; question?: RoadmapFrontmatter['question']; hasTopics?: boolean; isForkable?: boolean; @@ -35,6 +36,7 @@ const { isUpcoming = false, note, hasTopics = false, + projectCount = 0, question, activeTab = 'roadmap', } = Astro.props; @@ -123,7 +125,7 @@ const hasTnsBanner = !!tnsBannerLink; icon={FolderKanbanIcon} text='Projects' isActive={activeTab === 'projects'} - badgeText='soon' + badgeText={projectCount > 0 ? '' : 'soon'} />
diff --git a/src/data/projects/stock-cli.md b/src/data/projects/stock-cli.md new file mode 100644 index 000000000..d6c14b694 --- /dev/null +++ b/src/data/projects/stock-cli.md @@ -0,0 +1,36 @@ +--- +title: 'Stock CLI' +description: 'Build a command line interface (CLI) to track your tasks and manage your to-do list.' +isNew: false +difficulty: 'intermediate' +nature: 'API' +skills: + - 'Programming Language' + - 'CLI' + - 'Filesystem' + - 'Logic Building' +seo: + title: 'Task Tracker CLI' + description: 'Build a command line interface (CLI) to track your tasks and manage your to-do list.' + keywords: + - 'task tracker cli' + - 'backend project idea' +roadmapIds: + - 'backend' +--- + +# Task Tracker CLI + +Build a command line interface (CLI) to track your tasks and manage your to-do list. + +## Description + +You are required to build a command line interface (CLI) application that allows users to manage their tasks and to-do list. The application should be able to perform the following operations: + +- Add a new task +- List all tasks +- Mark a task as done +- Delete a task +- Update a task +- Search for a task +- Filter tasks by status (done, pending) diff --git a/src/data/projects/task-tracker-cli.md b/src/data/projects/task-tracker-cli.md new file mode 100644 index 000000000..743e8ebab --- /dev/null +++ b/src/data/projects/task-tracker-cli.md @@ -0,0 +1,36 @@ +--- +title: 'Task Tracker CLI' +description: 'Build a command line interface (CLI) to track your tasks and manage your to-do list.' +isNew: false +difficulty: 'beginner' +nature: 'API' +skills: + - 'Programming Language' + - 'CLI' + - 'Filesystem' + - 'Logic Building' +seo: + title: 'Task Tracker CLI' + description: 'Build a command line interface (CLI) to track your tasks and manage your to-do list.' + keywords: + - 'task tracker cli' + - 'backend project idea' +roadmapIds: + - 'backend' +--- + +# Task Tracker CLI + +Build a command line interface (CLI) to track your tasks and manage your to-do list. + +## Description + +You are required to build a command line interface (CLI) application that allows users to manage their tasks and to-do list. The application should be able to perform the following operations: + +- Add a new task +- List all tasks +- Mark a task as done +- Delete a task +- Update a task +- Search for a task +- Filter tasks by status (done, pending) diff --git a/src/lib/link-group.ts b/src/lib/link-group.ts index 9ed56cf6e..dc307cac1 100644 --- a/src/lib/link-group.ts +++ b/src/lib/link-group.ts @@ -25,7 +25,7 @@ function linkGroupPathToId(filePath: string): string { * @returns Promisifed linkGroup files */ export async function getAllLinkGroups(): Promise { - const linkGroups = await import.meta.glob( + const linkGroups = import.meta.glob( '/src/data/link-groups/*.md', { eager: true, diff --git a/src/lib/project.ts b/src/lib/project.ts new file mode 100644 index 000000000..83d05aee2 --- /dev/null +++ b/src/lib/project.ts @@ -0,0 +1,82 @@ +import type { MarkdownFileType } from './file'; + +export const projectDifficulties = ['beginner', 'intermediate', 'advanced'] as const; +export type ProjectDifficultyType = (typeof projectDifficulties)[number]; + +export interface ProjectFrontmatter { + title: string; + description: string; + isNew: boolean; + difficulty: ProjectDifficultyType; + nature: string; + skills: string[]; + seo: { + title: string; + description: string; + keywords: string[]; + }; + roadmapIds: string[]; +} + +export type ProjectFileType = MarkdownFileType & { + id: string; +}; + +/** + * Generates id from the given project file + * @param filePath Markdown file path + * + * @returns unique project identifier + */ +function projectPathToId(filePath: string): string { + const fileName = filePath.split('/').pop() || ''; + + return fileName.replace('.md', ''); +} + +export async function getProjectsByRoadmapId( + roadmapId: string, +): Promise { + const projects = await getAllProjects(); + + return projects.filter((project) => + project.frontmatter?.roadmapIds?.includes(roadmapId), + ); +} + +let tempProjects: ProjectFileType[] = []; + +/** + * Gets all the projects sorted by the publishing date + * @returns Promisifed project files + */ +export async function getAllProjects(): Promise { + if (tempProjects.length) { + return tempProjects; + } + + const projects = import.meta.glob( + '/src/data/projects/*.md', + { + eager: true, + }, + ); + + tempProjects = Object.values(projects).map((projectFile) => ({ + ...projectFile, + id: projectPathToId(projectFile.file), + })); + + return tempProjects; +} + +export async function getProjectById( + groupId: string, +): Promise { + const project = await import(`../data/projects/${groupId}.md`); + + return { + ...project, + id: projectPathToId(project.file), + }; +} diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index fbfc41773..4aa157968 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -17,6 +17,7 @@ import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; import RoadmapNote from '../../components/RoadmapNote.astro'; import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion'; import ResourceProgressStats from '../../components/ResourceProgressStats.astro'; +import { getProjectsByRoadmapId } from '../../lib/project'; export async function getStaticPaths() { const roadmapIds = await getRoadmapIds(); @@ -68,6 +69,7 @@ const ogImageUrl = const question = roadmapData?.question; const note = roadmapData.note; +const projects = await getProjectsByRoadmapId(roadmapId); ---
diff --git a/src/pages/[roadmapId]/projects.astro b/src/pages/[roadmapId]/projects.astro index e882d8a8c..4df325588 100644 --- a/src/pages/[roadmapId]/projects.astro +++ b/src/pages/[roadmapId]/projects.astro @@ -11,6 +11,7 @@ import ShareIcons from '../../components/ShareIcons/ShareIcons.astro'; import { TopicDetail } from '../../components/TopicDetail/TopicDetail'; import { UserProgressModal } from '../../components/UserProgress/UserProgressModal'; import BaseLayout from '../../layouts/BaseLayout.astro'; +import { getProjectsByRoadmapId } from '../../lib/project'; import { generateArticleSchema, generateFAQSchema, @@ -64,7 +65,7 @@ const nounTitle = descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle; const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`; -const projects = ['a']; +const projects = await getProjectsByRoadmapId(roadmapId); ---
{projects.length === 0 && } - {projects.length > 0 && } + {projects.length > 0 && }