feat: official project

This commit is contained in:
Arik Chakma authored and Kamran Ahmed committed 2025-09-02 17:54:54 +01:00
1 parent 2d18cefd55
commit bb0419bf8a
14 files changed
+244 -286

No files matched your search

@@ -3,21 +3,16 @@ import { useToast } from '../../hooks/use-toast';
import { httpGet, httpPost } from '../../lib/http';
import { LoadingSolutions } from './LoadingSolutions';
import { EmptySolutions } from './EmptySolutions';
import { ThumbsDown, ThumbsUp } from 'lucide-react';
import { getRelativeTimeString } from '../../lib/date';
import { Pagination } from '../Pagination/Pagination';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { pageProgressMessage } from '../../stores/page';
import { LeavingRoadmapWarningModal } from './LeavingRoadmapWarningModal';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { VoteButton } from './VoteButton.tsx';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { SelectLanguages } from './SelectLanguages.tsx';
import type { ProjectFrontmatter } from '../../lib/project.ts';
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
import { SortProjects } from './SortProjects.tsx';
import { ProjectSolutionRow } from './ProjectSolutionRow';
import type { OfficialProjectDocument } from '../../queries/official-project.ts';
export interface ProjectStatusDocument {
_id?: string;
@@ -69,12 +64,12 @@ type PageState = {
};
type ListProjectSolutionsProps = {
project: ProjectFrontmatter;
project: OfficialProjectDocument;
projectId: string;
};
export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const { projectId, project: projectData } = props;
const { projectId, project } = props;
const toast = useToast();
const [pageState, setPageState] = useState<PageState>({
@@ -226,7 +221,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
<div className="relative mb-5 hidden items-center justify-between sm:flex">
<div>
<h1 className="mb-1 text-xl font-semibold">
{projectData.title} Solutions
{project.title} Solutions
</h1>
<p className="text-sm text-gray-500">
Solutions submitted by the community
+14 -17
View File
@@ -1,20 +1,19 @@
import { Badge } from '../Badge.tsx';
import type {
ProjectDifficultyType,
ProjectFileType,
} from '../../lib/project.ts';
import { Users } from 'lucide-react';
import { formatCommaNumber } from '../../lib/number.ts';
import { cn } from '../../lib/classname.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import type {
AllowedOfficialProjectDifficulty,
OfficialProjectDocument,
} from '../../queries/official-project.ts';
type ProjectCardProps = {
project: ProjectFileType;
project: OfficialProjectDocument;
userCount?: number;
status?: 'completed' | 'started' | 'none';
};
const badgeVariants: Record<ProjectDifficultyType, string> = {
const badgeVariants: Record<AllowedOfficialProjectDifficulty, string> = {
beginner: 'yellow',
intermediate: 'green',
advanced: 'blue',
@@ -22,26 +21,24 @@ const badgeVariants: Record<ProjectDifficultyType, string> = {
export function ProjectCard(props: ProjectCardProps) {
const { project, userCount = 0, status } = props;
const { frontmatter, id } = project;
const { difficulty, title, description, slug, topics = [] } = project;
const isLoadingStatus = status === undefined;
const userStartedCount = status !== 'none' && userCount === 0 ? userCount + 1 : userCount;
const userStartedCount =
status !== 'none' && userCount === 0 ? userCount + 1 : userCount;
return (
<a
href={`/projects/${id}`}
href={`/projects/${slug}`}
className="flex flex-col rounded-md border bg-white p-3 transition-colors hover:border-gray-300 hover:bg-gray-50"
>
<span className="flex justify-between gap-1.5">
<Badge
variant={badgeVariants[frontmatter.difficulty] as any}
text={frontmatter.difficulty}
/>
<Badge variant={'grey'} text={frontmatter.nature} />
<Badge variant={badgeVariants[difficulty] as any} text={difficulty} />
{topics?.map((topic) => <Badge variant={'grey'} text={topic} />)}
</span>
<span className="my-3 flex min-h-[100px] flex-col">
<span className="mb-1 font-medium">{frontmatter.title}</span>
<span className="text-sm text-gray-500">{frontmatter.description}</span>
<span className="mb-1 font-medium">{title}</span>
<span className="text-sm text-gray-500">{description}</span>
</span>
<span className="flex min-h-[22px] items-center justify-between gap-2 text-xs text-gray-400">
{isLoadingStatus ? (
@@ -0,0 +1,25 @@
import { guideRenderer } from '../../lib/guide-renderer';
import type { OfficialProjectDocument } from '../../queries/official-project';
type ProjectContentProps = {
project: OfficialProjectDocument;
};
export function ProjectContent(props: ProjectContentProps) {
const { project } = props;
const isContentString = typeof project?.content === 'string';
return (
<div
className="prose prose-h2:mb-3 prose-h2:mt-5 prose-h3:mb-1 prose-h3:mt-5 prose-p:mb-2 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-pre:my-3 prose-ul:my-3.5 prose-hr:my-5 prose-li:[&>p]:m-0 max-w-full [&>ul>li]:my-1"
{...(isContentString
? {
dangerouslySetInnerHTML: { __html: project?.content },
}
: {
children: guideRenderer.render(project?.content),
})}
/>
);
}
+35 -28
View File
@@ -2,11 +2,7 @@ import { ProjectCard } from './ProjectCard.tsx';
import { HeartHandshake, Trash2 } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
import { useEffect, useMemo, useState } from 'react';
import {
projectDifficulties,
type ProjectDifficultyType,
type ProjectFileType,
} from '../../lib/project.ts';
import {
deleteUrlParam,
getUrlParams,
@@ -14,9 +10,14 @@ import {
} from '../../lib/browser.ts';
import { httpPost } from '../../lib/http.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import {
allowedOfficialProjectDifficulty,
type AllowedOfficialProjectDifficulty,
type OfficialProjectDocument,
} from '../../queries/official-project.ts';
type DifficultyButtonProps = {
difficulty: ProjectDifficultyType;
difficulty: AllowedOfficialProjectDifficulty;
isActive?: boolean;
onClick?: () => void;
};
@@ -46,7 +47,7 @@ export type ListProjectStatusesResponse = Record<
>;
type ProjectsListProps = {
projects: ProjectFileType[];
projects: OfficialProjectDocument[];
userCounts: Record<string, number>;
};
@@ -55,7 +56,7 @@ export function ProjectsList(props: ProjectsListProps) {
const { difficulty: urlDifficulty } = getUrlParams();
const [difficulty, setDifficulty] = useState<
ProjectDifficultyType | undefined
AllowedOfficialProjectDifficulty | undefined
>(urlDifficulty);
const [projectStatuses, setProjectStatuses] =
useState<ListProjectStatusesResponse>();
@@ -66,7 +67,7 @@ export function ProjectsList(props: ProjectsListProps) {
return;
}
const projectIds = projects.map((project) => project.id);
const projectIds = projects.map((project) => project.slug);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-statuses`,
{
@@ -82,22 +83,27 @@ export function ProjectsList(props: ProjectsListProps) {
setProjectStatuses(response);
};
const projectsByDifficulty: Map<ProjectDifficultyType, ProjectFileType[]> =
useMemo(() => {
const result = new Map<ProjectDifficultyType, ProjectFileType[]>();
const projectsByDifficulty: Map<
AllowedOfficialProjectDifficulty,
OfficialProjectDocument[]
> = useMemo(() => {
const result = new Map<
AllowedOfficialProjectDifficulty,
OfficialProjectDocument[]
>();
for (const project of projects) {
const difficulty = project.frontmatter.difficulty;
for (const project of projects) {
const difficulty = project.difficulty;
if (!result.has(difficulty)) {
result.set(difficulty, []);
}
result.get(difficulty)?.push(project);
if (!result.has(difficulty)) {
result.set(difficulty, []);
}
return result;
}, [projects]);
result.get(difficulty)?.push(project);
}
return result;
}, [projects]);
const matchingProjects = difficulty
? projectsByDifficulty.get(difficulty) || []
@@ -111,7 +117,7 @@ export function ProjectsList(props: ProjectsListProps) {
<div className="flex flex-col">
<div className="my-2.5 flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{projectDifficulties.map((projectDifficulty) => (
{allowedOfficialProjectDifficulty.map((projectDifficulty) => (
<DifficultyButton
key={projectDifficulty}
onClick={() => {
@@ -122,6 +128,7 @@ export function ProjectsList(props: ProjectsListProps) {
isActive={projectDifficulty === difficulty}
/>
))}
{difficulty && (
<button
onClick={() => {
@@ -155,25 +162,25 @@ export function ProjectsList(props: ProjectsListProps) {
{matchingProjects
.sort((project) => {
return project.frontmatter.difficulty === 'beginner'
return project.difficulty === 'beginner'
? -1
: project.frontmatter.difficulty === 'intermediate'
: project.difficulty === 'intermediate'
? 0
: 1;
})
.sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort;
return a.order - b.order;
})
.map((matchingProject) => {
const count = userCounts[matchingProject?.id] || 0;
const count = userCounts[matchingProject?.slug] || 0;
return (
<ProjectCard
key={matchingProject.id}
key={matchingProject.slug}
project={matchingProject}
userCount={count}
status={
projectStatuses
? (projectStatuses?.[matchingProject.id] || 'none')
? projectStatuses?.[matchingProject.slug] || 'none'
: undefined
}
/>
+17 -17
View File
@@ -7,16 +7,16 @@ import {
setUrlParams,
} from '../../lib/browser.ts';
import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx';
import {
projectDifficulties,
type ProjectFileType,
} from '../../lib/project.ts';
import { ProjectCard } from './ProjectCard.tsx';
import {
allowedOfficialProjectDifficulty,
type OfficialProjectDocument,
} from '../../queries/official-project.ts';
type ProjectGroup = {
id: string;
title: string;
projects: ProjectFileType[];
projects: OfficialProjectDocument[];
};
type ProjectsPageProps = {
@@ -28,7 +28,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
const { roadmapsProjects, userCounts } = props;
const allUniqueProjectIds = new Set<string>(
roadmapsProjects.flatMap((group) =>
group.projects.map((project) => project.id),
group.projects.map((project) => project.slug),
),
);
const allUniqueProjects = useMemo(
@@ -37,15 +37,15 @@ export function ProjectsPage(props: ProjectsPageProps) {
.map((id) =>
roadmapsProjects
.flatMap((group) => group.projects)
.find((project) => project.id === id),
.find((project) => project.slug === id),
)
.filter(Boolean) as ProjectFileType[],
.filter(Boolean) as OfficialProjectDocument[],
[allUniqueProjectIds],
);
const [activeGroup, setActiveGroup] = useState<string>('');
const [visibleProjects, setVisibleProjects] =
useState<ProjectFileType[]>(allUniqueProjects);
useState<OfficialProjectDocument[]>(allUniqueProjects);
const [isFilterOpen, setIsFilterOpen] = useState(false);
@@ -67,11 +67,11 @@ export function ProjectsPage(props: ProjectsPageProps) {
const sortedVisibleProjects = useMemo(
() =>
visibleProjects.sort((a, b) => {
const projectADifficulty = a?.frontmatter.difficulty || 'beginner';
const projectBDifficulty = b?.frontmatter.difficulty || 'beginner';
const projectADifficulty = a?.difficulty || 'beginner';
const projectBDifficulty = b?.difficulty || 'beginner';
return (
projectDifficulties.indexOf(projectADifficulty) -
projectDifficulties.indexOf(projectBDifficulty)
allowedOfficialProjectDifficulty.indexOf(projectADifficulty) -
allowedOfficialProjectDifficulty.indexOf(projectBDifficulty)
);
}),
[visibleProjects],
@@ -111,7 +111,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
{isFilterOpen && <X size={13} className="mr-1" />}
Categories
</button>
<div className="container relative flex flex-col gap-4 sm:flex-row">
<div className="relative container 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-linear-to-l sm:pt-6',
@@ -171,7 +171,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
</div>
</div>
</div>
<div className="flex grow flex-col pb-20 pt-2 sm:pt-6">
<div className="flex grow flex-col pt-2 pb-20 sm:pt-6">
<div className="mb-4 flex items-center justify-between text-sm text-gray-500">
<h3 className={'flex items-center'}>
<Box size={15} className="mr-1" strokeWidth={2} />
@@ -187,9 +187,9 @@ export function ProjectsPage(props: ProjectsPageProps) {
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
{sortedVisibleProjects.map((project) => (
<ProjectCard
key={project.id}
key={project.slug}
project={project}
userCount={userCounts[project.id] || 0}
userCount={userCounts[project.slug] || 0}
status={'none'}
/>
))}