mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-12 17:51:53 +08:00
Compare commits
3 Commits
94d4e06415
...
feat/offic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71dd89b062 | ||
|
|
1c29af4826 | ||
|
|
1e6d546712 |
@@ -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
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
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 { 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 = {
|
||||
beginner: 'yellow',
|
||||
intermediate: 'green',
|
||||
advanced: 'blue',
|
||||
};
|
||||
} as const;
|
||||
|
||||
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]} text={difficulty} />
|
||||
{topics?.map((topic, index) => (
|
||||
<Badge key={`${topic}-${index}`} 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 ? (
|
||||
|
||||
25
src/components/Projects/ProjectContent.tsx
Normal file
25
src/components/Projects/ProjectContent.tsx
Normal file
@@ -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),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import {
|
||||
officialRoadmapDetails,
|
||||
type OfficialRoadmapDocument,
|
||||
} from '../queries/official-roadmap';
|
||||
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;
|
||||
sort: number;
|
||||
difficulty: ProjectDifficultyType;
|
||||
nature: string;
|
||||
skills: string[];
|
||||
seo: {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
ogImageUrl: string;
|
||||
};
|
||||
hasNoSubmission: boolean;
|
||||
roadmapIds: string[];
|
||||
}
|
||||
|
||||
export type ProjectFileType = MarkdownFileType<ProjectFrontmatter> & {
|
||||
id: string;
|
||||
roadmaps: OfficialRoadmapDocument[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<ProjectFileType[]> {
|
||||
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<ProjectFileType[]> {
|
||||
if (tempProjects.length) {
|
||||
return tempProjects;
|
||||
}
|
||||
|
||||
const projects = import.meta.glob<ProjectFileType>(
|
||||
'/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<ProjectFileType> {
|
||||
const project = await import(`../data/projects/${groupId}.md`);
|
||||
const roadmapIds = project.frontmatter.roadmapIds || [];
|
||||
const roadmaps = await Promise.all(
|
||||
roadmapIds.map((roadmapId: string) => officialRoadmapDetails(roadmapId)),
|
||||
);
|
||||
|
||||
return {
|
||||
...project,
|
||||
roadmaps: roadmaps,
|
||||
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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { getProjectsByRoadmapId } from '../../lib/project';
|
||||
import { listOfficialProjects } from '../../queries/official-project';
|
||||
import { officialRoadmapDetails } from '../../queries/official-roadmap';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -42,7 +42,7 @@ const nounTitle =
|
||||
descriptionNoun[roadmapData.title.card] || roadmapData.title.card;
|
||||
const seoDescription = `Seeking ${nounTitle.toLowerCase()} courses to enhance your skills? Explore our top free and paid courses to help you become a ${nounTitle} expert!`;
|
||||
|
||||
const projects = await getProjectsByRoadmapId(roadmapId);
|
||||
const projects = await listOfficialProjects({ roadmapId });
|
||||
const courses = roadmapData?.courses || [];
|
||||
---
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion';
|
||||
import ResourceProgressStats from '../../components/ResourceProgressStats.astro';
|
||||
import { getProjectsByRoadmapId } from '../../lib/project';
|
||||
import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification';
|
||||
import { officialRoadmapDetails } from '../../queries/official-roadmap';
|
||||
import { DateTime } from 'luxon';
|
||||
import { listOfficialProjects } from '../../queries/official-project';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -73,7 +73,7 @@ if (faqs.length) {
|
||||
jsonLdSchema.push(generateFAQSchema(faqs));
|
||||
}
|
||||
|
||||
const projects = await getProjectsByRoadmapId(roadmapId);
|
||||
const projects = await listOfficialProjects({ roadmapId });
|
||||
const courses = roadmapData.courses || [];
|
||||
---
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
import { EmptyProjects } from '../../components/Projects/EmptyProjects';
|
||||
import { ProjectsList } from '../../components/Projects/ProjectsList';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getProjectsByRoadmapId } from '../../lib/project';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { projectApi } from '../../api/project';
|
||||
import { officialRoadmapDetails } from '../../queries/official-roadmap';
|
||||
import { listOfficialProjects } from '../../queries/official-project';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -46,8 +46,8 @@ const nounTitle =
|
||||
descriptionNoun[roadmapData?.title.card] || roadmapData.title.card;
|
||||
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 = await getProjectsByRoadmapId(roadmapId);
|
||||
const projectIds = projects.map((project) => project.id);
|
||||
const projects = await listOfficialProjects({ roadmapId });
|
||||
const projectIds = projects.map((project) => project.slug);
|
||||
|
||||
const projectApiClient = projectApi(Astro);
|
||||
const { response: userCounts } =
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import { getAllBestPractices } from '../lib/best-practice';
|
||||
import { getAllVideos } from '../lib/video';
|
||||
import { getAllQuestionGroups } from '../lib/question-group';
|
||||
import { getAllProjects } from '../lib/project';
|
||||
import {
|
||||
listOfficialAuthors,
|
||||
listOfficialGuides,
|
||||
} from '../queries/official-guide';
|
||||
import { listOfficialRoadmaps } from '../queries/official-roadmap';
|
||||
|
||||
// Add utility to fetch beginner roadmap file IDs
|
||||
function getBeginnerRoadmapIds() {
|
||||
const files = import.meta.glob('/src/data/roadmaps/*/*-beginner.json', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
return Object.keys(files).map((filePath) => {
|
||||
const fileName = filePath.split('/').pop() || '';
|
||||
return fileName.replace('.json', '');
|
||||
});
|
||||
}
|
||||
import {
|
||||
listOfficialBeginnerRoadmaps,
|
||||
listOfficialRoadmaps,
|
||||
} from '../queries/official-roadmap';
|
||||
import { listOfficialProjects } from '../queries/official-project';
|
||||
|
||||
export async function GET() {
|
||||
const guides = await listOfficialGuides();
|
||||
const authors = await listOfficialAuthors();
|
||||
const videos = await getAllVideos();
|
||||
const questionGroups = await getAllQuestionGroups();
|
||||
const roadmaps = await listOfficialRoadmaps();
|
||||
const mainRoadmaps = await listOfficialRoadmaps();
|
||||
const beginnerRoadmaps = await listOfficialBeginnerRoadmaps();
|
||||
|
||||
const bestPractices = await getAllBestPractices();
|
||||
const projects = await getAllProjects();
|
||||
const projects = await listOfficialProjects();
|
||||
|
||||
const roadmaps = [...mainRoadmaps, ...beginnerRoadmaps];
|
||||
// Transform main roadmaps into page objects first so that we can reuse their meta for beginner variants
|
||||
const roadmapPages = roadmaps
|
||||
.map((roadmap) => {
|
||||
@@ -114,11 +107,11 @@ export async function GET() {
|
||||
group: 'Videos',
|
||||
})),
|
||||
...projects.map((project) => ({
|
||||
id: project.id,
|
||||
url: `/projects/${project.id}`,
|
||||
title: project.frontmatter.title,
|
||||
description: project.frontmatter.description,
|
||||
shortTitle: project.frontmatter.title,
|
||||
id: project.slug,
|
||||
url: `/projects/${project.slug}`,
|
||||
title: project.title,
|
||||
description: project.description,
|
||||
shortTitle: project.title,
|
||||
group: 'Projects',
|
||||
})),
|
||||
]),
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import { Badge } from '../../../components/Badge';
|
||||
import {
|
||||
getAllProjects,
|
||||
getProjectById,
|
||||
type ProjectFrontmatter,
|
||||
} from '../../../lib/project';
|
||||
import AstroIcon from '../../../components/AstroIcon.astro';
|
||||
import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper';
|
||||
import { ProjectTrackingActions } from '../../../components/Projects/StatusStepper/ProjectTrackingActions';
|
||||
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
|
||||
import { officialProjectDetails } from '../../../queries/official-project';
|
||||
import { ProjectContent } from '../../../components/Projects/ProjectContent';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -19,31 +14,22 @@ interface Params extends Record<string, string | undefined> {
|
||||
|
||||
const { projectId } = Astro.params as Params;
|
||||
|
||||
const project = await getProjectById(projectId);
|
||||
const project = await officialProjectDetails(projectId);
|
||||
if (!project) {
|
||||
Astro.response.status = 404;
|
||||
Astro.response.statusText = 'Not found';
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
const projectData = project.frontmatter as ProjectFrontmatter;
|
||||
|
||||
let jsonLdSchema: any[] = [];
|
||||
|
||||
const ogImageUrl = projectData?.seo?.ogImageUrl || '/img/og-img.png';
|
||||
const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/projects/${projectId}.md`;
|
||||
|
||||
const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
|
||||
const parentRoadmapId = project?.roadmapIds?.[0] || '';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
permalink={`/projects/${projectId}`}
|
||||
title={projectData?.seo?.title}
|
||||
briefTitle={projectData.title}
|
||||
ogImageUrl={ogImageUrl}
|
||||
description={projectData.seo.description}
|
||||
keywords={projectData.seo.keywords}
|
||||
jsonLd={jsonLdSchema}
|
||||
title={project?.seo?.title || project?.title}
|
||||
briefTitle={project?.title}
|
||||
description={project?.seo?.description || project?.description}
|
||||
keywords={project?.seo?.keywords || []}
|
||||
resourceId={projectId}
|
||||
>
|
||||
<div class='bg-gray-50'>
|
||||
@@ -52,7 +38,6 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
|
||||
parentRoadmapId={parentRoadmapId}
|
||||
projectId={projectId}
|
||||
activeTab='details'
|
||||
hasNoSubmission={projectData?.hasNoSubmission}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -62,55 +47,27 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
|
||||
<div class='mb-4 hidden items-center justify-between sm:flex'>
|
||||
<div class='flex flex-row flex-wrap gap-1.5'>
|
||||
{
|
||||
projectData.skills.map((skill) => (
|
||||
project?.skills.map((skill) => (
|
||||
<Badge variant='green' text={skill} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Badge variant='yellow' text={projectData.difficulty} />
|
||||
<Badge variant='yellow' text={project?.difficulty} />
|
||||
</div>
|
||||
<div class='my-2 flex items-center justify-between gap-2 sm:my-7'>
|
||||
<div class=''>
|
||||
<h1 class='mb-1 text-xl font-semibold sm:mb-2 sm:text-3xl'>
|
||||
{projectData.title}
|
||||
{project?.title}
|
||||
</h1>
|
||||
<p class='text-sm text-balance text-gray-500'>
|
||||
{projectData.description}
|
||||
{project?.description}
|
||||
</p>
|
||||
</div>
|
||||
{
|
||||
projectData?.hasNoSubmission && (
|
||||
<ProjectTrackingActions projectId={projectId} client:load />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
!projectData?.hasNoSubmission && (
|
||||
<ProjectStepper projectId={projectId} client:load />
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
class='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 max-w-full [&>ul>li]:my-1'
|
||||
>
|
||||
<project.Content />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class='mt-5 flex flex-wrap items-center justify-center rounded-lg p-2.5 text-sm'
|
||||
>
|
||||
<AstroIcon class='mr-2 inline-block h-5 w-5' icon='github' />
|
||||
Found a mistake?
|
||||
<a
|
||||
class='ml-1 underline underline-offset-2'
|
||||
href={githubUrl}
|
||||
target='_blank'
|
||||
>
|
||||
Help us improve.
|
||||
</a>
|
||||
</div>
|
||||
<ProjectStepper projectId={projectId} client:load />
|
||||
<ProjectContent project={project} client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import {
|
||||
getAllProjects,
|
||||
getProjectById,
|
||||
type ProjectFrontmatter,
|
||||
} from '../../../lib/project';
|
||||
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
|
||||
import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions';
|
||||
import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal';
|
||||
import { officialProjectDetails } from '../../../queries/official-project';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -17,31 +13,22 @@ interface Params extends Record<string, string | undefined> {
|
||||
|
||||
const { projectId } = Astro.params as Params;
|
||||
|
||||
const project = await getProjectById(projectId);
|
||||
const project = await officialProjectDetails(projectId);
|
||||
if (!project) {
|
||||
Astro.response.status = 404;
|
||||
Astro.response.statusText = 'Not found';
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
const projectData = project.frontmatter as ProjectFrontmatter;
|
||||
|
||||
let jsonLdSchema: any[] = [];
|
||||
|
||||
const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
|
||||
|
||||
const ogImageUrl = projectData?.seo?.ogImageUrl || '/img/og-img.png';
|
||||
const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/projects/${projectId}.md`;
|
||||
const parentRoadmapId = project?.roadmapIds?.[0] || '';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
permalink={`/projects/${projectId}/solutions`}
|
||||
title={projectData?.seo?.title}
|
||||
briefTitle={projectData.title}
|
||||
ogImageUrl={ogImageUrl}
|
||||
description={projectData.seo.description}
|
||||
keywords={projectData.seo.keywords}
|
||||
jsonLd={jsonLdSchema}
|
||||
title={project?.seo?.title || project.title}
|
||||
briefTitle={project.title}
|
||||
description={project.seo.description || project.description}
|
||||
keywords={project.seo.keywords}
|
||||
resourceId={projectId}
|
||||
>
|
||||
<div class='bg-gray-50'>
|
||||
@@ -53,15 +40,15 @@ const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/maste
|
||||
/>
|
||||
|
||||
<ListProjectSolutions
|
||||
project={projectData}
|
||||
project={project}
|
||||
projectId={projectId}
|
||||
client:load
|
||||
/>
|
||||
|
||||
<ProjectSolutionModal
|
||||
projectId={projectId}
|
||||
projectTitle={projectData.title}
|
||||
projectDescription={projectData.description}
|
||||
projectTitle={project.title}
|
||||
projectDescription={project.description}
|
||||
client:only='react'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getRoadmapsProjects } from '../../lib/project';
|
||||
import { ProjectsPageHeader } from '../../components/Projects/ProjectsPageHeader';
|
||||
import { ProjectsPage } from '../../components/Projects/ProjectsPage';
|
||||
import { projectApi } from '../../api/project';
|
||||
import { listOfficialRoadmaps } from '../../queries/official-roadmap';
|
||||
import { getRoadmapsProjects } from '../../queries/official-project';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -18,7 +18,7 @@ const allRoadmaps = roadmaps.filter((roadmap) =>
|
||||
|
||||
const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
|
||||
const projects = (roadmapProjects[roadmap.slug] || []).sort((a, b) => {
|
||||
return a.frontmatter.sort - b.frontmatter.sort;
|
||||
return a.order - b.order;
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -31,7 +31,7 @@ const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
|
||||
const projectIds = allRoadmapIds
|
||||
.map((id) => roadmapProjects[id])
|
||||
.flat()
|
||||
.map((project) => project.id);
|
||||
.map((project) => project.slug);
|
||||
const projectApiClient = projectApi(Astro);
|
||||
const { response: userCounts } =
|
||||
await projectApiClient.listProjectsUserCount(projectIds);
|
||||
|
||||
108
src/queries/official-project.ts
Normal file
108
src/queries/official-project.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { FetchError, httpGet } from '../lib/query-http';
|
||||
|
||||
export const allowedOfficialProjectDifficulty = [
|
||||
'beginner',
|
||||
'intermediate',
|
||||
'advanced',
|
||||
] as const;
|
||||
export type AllowedOfficialProjectDifficulty =
|
||||
(typeof allowedOfficialProjectDifficulty)[number];
|
||||
|
||||
export const allowedOfficialProjectStatus = ['draft', 'published'] as const;
|
||||
export type AllowedOfficialProjectStatus =
|
||||
(typeof allowedOfficialProjectStatus)[number];
|
||||
|
||||
export interface OfficialProjectDocument {
|
||||
_id: string;
|
||||
|
||||
order: number;
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
difficulty: AllowedOfficialProjectDifficulty;
|
||||
topics: string[];
|
||||
|
||||
status: AllowedOfficialProjectStatus;
|
||||
publishedAt?: Date;
|
||||
|
||||
content: any;
|
||||
|
||||
seo: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
};
|
||||
|
||||
skills: string[];
|
||||
roadmapIds: string[];
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export async function officialProjectDetails(projectSlug: string) {
|
||||
try {
|
||||
const project = await httpGet<OfficialProjectDocument>(
|
||||
`/v1-official-project/${projectSlug}`,
|
||||
);
|
||||
|
||||
return project;
|
||||
} catch (error) {
|
||||
if (FetchError.isFetchError(error) && error.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
type ListOfficialProjectsQuery = {
|
||||
roadmapId?: string;
|
||||
};
|
||||
|
||||
export async function listOfficialProjects(
|
||||
query: ListOfficialProjectsQuery = {},
|
||||
) {
|
||||
try {
|
||||
const projects = await httpGet<OfficialProjectDocument[]>(
|
||||
`/v1-list-official-projects`,
|
||||
query,
|
||||
);
|
||||
|
||||
return projects;
|
||||
} catch (error) {
|
||||
if (FetchError.isFetchError(error) && error.status === 404) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewProject(createdAt: Date) {
|
||||
return (
|
||||
createdAt &&
|
||||
DateTime.now().diff(DateTime.fromJSDate(new Date(createdAt)), 'days').days <
|
||||
45
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRoadmapsProjects(): Promise<
|
||||
Record<string, OfficialProjectDocument[]>
|
||||
> {
|
||||
const projects = await listOfficialProjects();
|
||||
const roadmapsProjects: Record<string, OfficialProjectDocument[]> = {};
|
||||
|
||||
projects.forEach((project) => {
|
||||
project.roadmapIds.forEach((roadmapId) => {
|
||||
if (!roadmapsProjects[roadmapId]) {
|
||||
roadmapsProjects[roadmapId] = [];
|
||||
}
|
||||
|
||||
roadmapsProjects[roadmapId].push(project);
|
||||
});
|
||||
});
|
||||
|
||||
return roadmapsProjects;
|
||||
}
|
||||
@@ -128,6 +128,22 @@ export async function listOfficialRoadmaps() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function listOfficialBeginnerRoadmaps() {
|
||||
try {
|
||||
const roadmaps = await httpGet<OfficialRoadmapDocument[]>(
|
||||
`/v1-list-official-beginner-roadmaps`,
|
||||
);
|
||||
|
||||
return roadmaps;
|
||||
} catch (error) {
|
||||
if (FetchError.isFetchError(error) && error.status === 404) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewRoadmap(createdAt: Date) {
|
||||
return (
|
||||
createdAt &&
|
||||
|
||||
Reference in New Issue
Block a user