1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-02 13:52:46 +02:00

Revert "Revert "feat: official roadmap meta"" (#9096)

* Revert "Revert "chore: update roadmap json endpoint""

This reverts commit 8dbe1468ed.

* Revert "Revert "feat: roadmap main page""

This reverts commit bb13bf38a8.

* Revert "Revert "chore: replace roadmap listing""

This reverts commit 80dfd5b206.

* Revert "Revert "feat: roadmap courses""

This reverts commit a89c2d454f.

* Revert "Revert "fix: course length""

This reverts commit d1cf7cca99.

* Revert "Revert "feat: roadmap with courses""

This reverts commit 9c32f9d469.

* Revert "Revert "chore: disable pre-render for roadmaps""

This reverts commit cef4c29f10.
This commit is contained in:
Kamran Ahmed
2025-09-01 20:22:54 +01:00
committed by GitHub
parent 931e1b4a31
commit 2d18cefd55
35 changed files with 681 additions and 705 deletions

1
.astro/types.d.ts vendored
View File

@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -23,7 +23,12 @@ type EditorRoadmapProps = {
};
export function EditorRoadmap(props: EditorRoadmapProps) {
const { resourceId, resourceType = 'roadmap', dimensions, hasChat = true } = props;
const {
resourceId,
resourceType = 'roadmap',
dimensions,
hasChat = true,
} = props;
const [hasSwitchedRoadmap, setHasSwitchedRoadmap] = useState(false);
const [isLoading, setIsLoading] = useState(true);

View File

@@ -1,3 +0,0 @@
<div class='text-sm sm:text-base leading-relaxed text-left p-2 sm:p-4 text-md text-gray-800 border-t border-t-gray-300 bg-gray-100 rounded-bl-md rounded-br-md [&>p:not(:last-child)]:mb-3 [&>p>a]:underline [&>p>a]:text-blue-700'>
<slot />
</div>

View File

@@ -1,42 +0,0 @@
---
import { markdownToHtml } from '../../lib/markdown';
import Answer from './Answer.astro';
import Question from './Question.astro';
export type FAQType = {
question: string;
answer: string[];
};
export interface Props {
faqs: FAQType[];
}
const { faqs } = Astro.props;
if (faqs.length === 0) {
return '';
}
---
<div class='border-t bg-gray-100 mt-8'>
<div class='container'>
<div class='flex justify-between relative -top-5'>
<h2 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>Frequently Asked Questions</h2>
</div>
<div class='flex flex-col gap-1 pb-14'>
{
faqs.map((faq, questionIndex) => (
<Question isActive={questionIndex === 0} question={faq.question}>
<Answer>
{faq.answer.map((answer) => (
<p set:html={markdownToHtml(answer)} />
))}
</Answer>
</Question>
))
}
</div>
</div>
</div>

View File

@@ -0,0 +1,61 @@
import { useState } from 'react';
import type { OfficialRoadmapQuestion } from '../../queries/official-roadmap';
import { Question } from './Question';
import { guideRenderer } from '../../lib/guide-renderer';
type FAQsProps = {
faqs: OfficialRoadmapQuestion[];
};
export function FAQs(props: FAQsProps) {
const { faqs } = props;
if (faqs.length === 0) {
return null;
}
const [activeQuestionIndex, setActiveQuestionIndex] = useState(0);
return (
<div className="mt-8 border-t bg-gray-100">
<div className="container">
<div className="relative -top-5 flex justify-between">
<h2 className="rounded-md border bg-white px-3 py-1 text-sm font-medium sm:text-base">
Frequently Asked Questions
</h2>
</div>
<div className="flex flex-col gap-1 pb-14">
{faqs.map((faq, questionIndex) => {
const isTextDescription =
typeof faq?.description === 'string' &&
faq?.description?.length > 0;
return (
<Question
key={faq._id}
isActive={questionIndex === activeQuestionIndex}
question={faq.title}
onClick={() => setActiveQuestionIndex(questionIndex)}
>
<div
className="text-md rounded-br-md rounded-bl-md border-t border-t-gray-300 bg-gray-100 p-2 text-left text-sm leading-relaxed text-gray-800 sm:p-4 sm:text-base [&>p:not(:last-child)]:mb-3 [&>p>a]:text-blue-700 [&>p>a]:underline"
{...(isTextDescription
? {
dangerouslySetInnerHTML: {
__html: faq.description,
},
}
: {})}
>
{!isTextDescription
? guideRenderer.render(faq.description)
: null}
</div>
</Question>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -1,42 +0,0 @@
---
import Icon from '../AstroIcon.astro';
export interface Props {
question: string;
isActive?: boolean;
}
const { question, isActive = false } = Astro.props;
---
<div
class='faq-item bg-white border rounded-md hover:bg-gray-50 border-gray-300'
>
<button
faq-question
class='flex flex-row justify-between items-center p-2 sm:p-3 w-full'
>
<span class='text-sm sm:text-base text-left font-medium'>{question}</span>
<Icon icon='down' class='h-6 hidden sm:block text-gray-400' />
</button>
<div class:list={['answer', { hidden: !isActive }]} faq-answer>
<slot />
</div>
</div>
<script>
document.querySelectorAll('[faq-question]').forEach((el) => {
el.addEventListener('click', () => {
// Hide any other visible answers
document.querySelectorAll('[faq-answer]').forEach((element) => {
element.classList.add('hidden');
});
// Show the current answer
const answer = el.nextElementSibling;
if (answer) {
answer.classList.remove('hidden');
}
});
});
</script>

View File

@@ -0,0 +1,29 @@
import { cn } from '../../lib/classname';
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
type QuestionProps = {
question: string;
isActive?: boolean;
children: React.ReactNode;
onClick?: () => void;
};
export function Question(props: QuestionProps) {
const { question, isActive = false, children, onClick } = props;
return (
<div className="faq-item rounded-md border border-gray-300 bg-white hover:bg-gray-50">
<button
className="flex w-full flex-row items-center justify-between p-2 sm:p-3"
onClick={onClick}
>
<span className="text-left text-sm font-medium sm:text-base">
{question}
</span>
<ChevronDownIcon className="hidden h-3.5 stroke-[3] text-gray-400 sm:block" />
</button>
{isActive && <div className={cn('answer')}>{children}</div>}
</div>
);
}

View File

@@ -1,6 +1,4 @@
---
import type { RoadmapFileType } from '../lib/roadmap';
export interface Props {
url: string;
title: string;
@@ -27,7 +25,7 @@ const { url, title, description, isNew } = Astro.props;
{
isNew && (
<span class='flex items-center gap-1.5 absolute bottom-1.5 right-1 rounded-xs text-xs font-semibold uppercase text-purple-500 sm:px-1.5'>
<span class='absolute right-1 bottom-1.5 flex items-center gap-1.5 rounded-xs text-xs font-semibold text-purple-500 uppercase sm:px-1.5'>
<span class='relative flex h-2 w-2'>
<span class='absolute inline-flex h-full w-full animate-ping rounded-full bg-purple-400 opacity-75' />
<span class='relative inline-flex h-2 w-2 rounded-full bg-purple-500' />

View File

@@ -1,69 +1,22 @@
---
import { getQuestionGroupsByIds } from '../lib/question-group';
import { getRoadmapsByIds, type RoadmapFrontmatter } from '../lib/roadmap';
import { Map, Clipboard } from 'lucide-react';
import { Map } from 'lucide-react';
import { listOfficialRoadmaps } from '../queries/official-roadmap';
export interface Props {
roadmap: RoadmapFrontmatter;
relatedRoadmaps: string[];
}
const { roadmap } = Astro.props;
const { relatedRoadmaps } = Astro.props;
const relatedRoadmaps = roadmap.relatedRoadmaps || [];
const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
const relatedQuestions = roadmap.relatedQuestions || [];
const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
const allRoadmaps = await listOfficialRoadmaps();
const relatedRoadmapsDetails = allRoadmaps.filter((roadmap) =>
relatedRoadmaps.includes(roadmap.slug),
);
---
{
relatedQuestionDetails.length > 0 && (
<div class='border-t bg-gray-100 pb-3'>
<div class='container'>
<div class='relative -top-5 flex justify-between'>
<span class='text-md flex items-center rounded-md border bg-white px-3 py-1 font-medium'>
<Clipboard className='mr-1.5 text-black' size='17px' />
Test your Knowledge
</span>
<a
href='/questions'
class='text-md rounded-md border bg-white px-3 py-1 font-medium hover:bg-gray-50'
>
<span class='hidden sm:inline'>All Quizzes &rarr;</span>
<span class='inline sm:hidden'>More &rarr;</span>
</a>
</div>
<div class='flex flex-col gap-1 pb-8'>
{relatedQuestionDetails.map((relatedQuestionGroup) => (
<a
href={`/questions/${relatedQuestionGroup.id}`}
class='flex flex-col gap-0.5 rounded-md border bg-white px-3.5 py-2 hover:bg-gray-50 sm:flex-row sm:gap-0'
>
<span class='inline-block min-w-[150px] font-medium'>
{relatedQuestionGroup.title}
</span>
<span class='text-gray-500'>
{relatedQuestionGroup.description}
</span>
</a>
))}
</div>
</div>
</div>
)
}
{
relatedRoadmaps.length && (
<div
class:list={[
'border-t bg-gray-100',
{
'mt-0': !relatedQuestionDetails.length,
},
]}
>
<div class:list={['border-t bg-gray-100']}>
<div class='container'>
<div class='relative -top-5 flex justify-between'>
<span class='text-md flex items-center rounded-md border bg-white px-3 py-1 font-medium'>
@@ -80,17 +33,15 @@ const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
</div>
<div class='flex flex-col gap-1 pb-8'>
{relatedRoadmapDetails.map((relatedRoadmap) => (
{relatedRoadmapsDetails.map((relatedRoadmap) => (
<a
href={`/${relatedRoadmap.id}`}
href={`/${relatedRoadmap.slug}`}
class='flex flex-col gap-0.5 rounded-md border bg-white px-3.5 py-2 hover:bg-gray-50 sm:flex-row sm:gap-0'
>
<span class='inline-block min-w-[195px] font-medium'>
{relatedRoadmap.frontmatter.briefTitle}
</span>
<span class='text-gray-500'>
{relatedRoadmap.frontmatter.briefDescription}
{relatedRoadmap.title.card}
</span>
<span class='text-gray-500'>{relatedRoadmap.description}</span>
</a>
))}
</div>

View File

@@ -5,9 +5,7 @@ import {
Bot,
FolderKanbanIcon,
MapIcon,
MessageCircle,
} from 'lucide-react';
import { type RoadmapFrontmatter } from '../lib/roadmap';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import { DownloadRoadmapButton } from './DownloadRoadmapButton';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
@@ -20,20 +18,16 @@ import { PersonalizedRoadmap } from './PersonalizedRoadmap/PersonalizedRoadmap';
export interface Props {
title: string;
description: string;
note?: string;
partner?: {
description: string;
link: string;
linkText: string;
};
roadmapId: string;
isUpcoming?: boolean;
hasSearch?: boolean;
projectCount?: number;
coursesCount?: number;
hasAIChat?: boolean;
question?: RoadmapFrontmatter['question'];
hasTopics?: boolean;
isForkable?: boolean;
activeTab?: 'roadmap' | 'projects' | 'courses';
}
@@ -43,12 +37,8 @@ const {
description,
roadmapId,
partner,
isUpcoming = false,
note,
hasTopics = false,
hasAIChat = false,
projectCount = 0,
question,
activeTab = 'roadmap',
coursesCount = 0,
} = Astro.props;

View File

@@ -10,10 +10,12 @@ import { useOutsideClick } from '../hooks/use-outside-click';
import { markdownToHtml } from '../lib/markdown';
import { cn } from '../lib/classname';
import { useScrollPosition } from '../hooks/use-scroll-position';
import type { JSONContent } from '@tiptap/core';
import { guideRenderer } from '../lib/guide-renderer';
type RoadmapTitleQuestionProps = {
question: string;
answer: string;
answer: JSONContent;
roadmapId?: string;
};
@@ -38,24 +40,24 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
'rounded-0 -mx-4 sm:mx-0': isAnswerVisible,
// @FIXME:
// The line below is to keep the question hidden on mobile devices except for
// the frontend roadmap. This is because we did not use to have the question
// the frontend roadmap. This is because we did not use to have the question
// on mobile devices before and we don't want to cause any SEO issues. It will
// be enabled on other roadmaps in the future.
},
)}
>
{isAnswerVisible && (
<div className="fixed left-0 right-0 top-0 z-100 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"></div>
<div className="fixed top-0 right-0 left-0 z-100 h-full items-center justify-center overflow-x-hidden overflow-y-auto overscroll-contain bg-black/50"></div>
)}
<h2
className="z-50 flex cursor-pointer select-none items-center px-2 py-2 text-sm font-medium"
className="z-50 flex cursor-pointer items-center px-2 py-2 text-sm font-medium select-none"
aria-expanded={isAnswerVisible ? 'true' : 'false'}
onClick={(e) => {
e.preventDefault();
setIsAnswerVisible(!isAnswerVisible);
}}
>
<span className="flex grow select-none items-center">
<span className="flex grow items-center select-none">
<Info className="mr-1.5 inline-block h-4 w-4" strokeWidth={2.5} />
{question}
</span>
@@ -65,7 +67,7 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
</h2>
<div
className={`absolute left-0 right-0 top-0 z-100 mt-0 border bg-white ${
className={`absolute top-0 right-0 left-0 z-100 mt-0 border bg-white ${
isAnswerVisible ? 'rounded-0 block sm:rounded-md' : 'hidden'
}`}
ref={ref}
@@ -73,7 +75,7 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
{isAnswerVisible && (
<h2
className={cn(
'sticky top-0 flex cursor-pointer select-none items-center rounded-t-md border-b bg-white px-[7px] py-[9px] text-base font-medium',
'sticky top-0 flex cursor-pointer items-center rounded-t-md border-b bg-white px-[7px] py-[9px] text-base font-medium select-none',
)}
onClick={() => {
setIsAnswerVisible(false);
@@ -95,9 +97,11 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
</h2>
)}
<div
className="bg-gray-100 p-3 text-base [&>h2]:mb-2 [&>h2]:mt-5 [&>h2]:text-[17px] [&>h2]:font-medium [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>p]:mb-3 [&>p]:font-normal [&>p]:leading-relaxed [&>p]:text-gray-800 [&>ul>li]:mb-2 [&>ul>li]:font-normal"
dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }}
></div>
className="bg-gray-100 p-3 text-base [&>h2]:mt-5 [&>h2]:mb-2 [&>h2]:text-[17px] [&>h2]:font-medium [&>p]:mb-3 [&>p]:leading-relaxed [&>p]:font-normal [&>p]:text-gray-800 [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>ul>li]:mb-2 [&>ul>li]:font-normal"
// dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }}
>
{guideRenderer.render(answer)}
</div>
</div>
</div>
);

View File

@@ -1,5 +1,5 @@
---
import type { FAQType } from '../../../components/FAQs/FAQs.astro';
import type { FAQType } from '../../../components/FAQs/FAQs';
export const faqs: FAQType[] = [
{
@@ -11,13 +11,13 @@ export const faqs: FAQType[] = [
{
question: 'What is reinforcement learning?',
answer: [
'[Reinforcement learning](https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292) (RL) is a type of machine learning where an agent learns to make decisions by interacting with an environment. Unlike traditional supervised learning, RL does not rely on labeled data. Instead, the agent learns by taking actions and receiving feedback in the form of rewards or penalties. Over time, it aims to maximize cumulative rewards by refining its strategy based on past experiences. RL is often used in areas like robotics, game AI, and autonomous systems, where the goal is to develop intelligent behaviors through trial and error.',
'[Reinforcement learning](https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292) (RL) is a type of machine learning where an agent learns to make decisions by interacting with an environment. Unlike traditional supervised learning, RL does not rely on labeled data. Instead, the agent learns by taking actions and receiving feedback in the form of rewards or penalties. Over time, it aims to maximize cumulative rewards by refining its strategy based on past experiences. RL is often used in areas like robotics, game AI, and autonomous systems, where the goal is to develop intelligent behaviors through trial and error.',
],
},
{
question: 'Do AI Engineers need a degree?',
answer: [
'While a degree in computer science, data science, or a related field can provide a solid foundation for becoming an AI engineer, it is not strictly necessary. Many successful AI engineers are self-taught or have gained expertise through online courses, certifications, and hands-on projects.'
'While a degree in computer science, data science, or a related field can provide a solid foundation for becoming an AI engineer, it is not strictly necessary. Many successful AI engineers are self-taught or have gained expertise through online courses, certifications, and hands-on projects.',
],
},
];

View File

@@ -1,5 +1,5 @@
---
import type { FAQType } from '../../../components/FAQs/FAQs.astro';
import type { FAQType } from '../../../components/FAQs/FAQs';
export const faqs: FAQType[] = [
{

View File

@@ -1,50 +1,50 @@
---
import type { FAQType } from '../../../components/FAQs/FAQs.astro';
import type { FAQType } from '../../../components/FAQs/FAQs';
export const faqs: FAQType[] = [
{
question: 'Is Frontend Development really coding?',
answer: [
"Do frontend developers really code? The answer is yes, absolutely.",
"The fact that frontend developers are full-time developers who produce an output that is visually appealing (thanks to the designs provided by others) sometimes confuses others, making them believe that frontend developers aren\t really coding. However, that couldn\t be further from the truth.",
"As a frontend developer, you\ll be coding all the time.",
"While in some companies, the frontend developer is also a skilled designer or UX engineer, those are not the typical profiles. As a frontend dev, your learning focus should be coding-related (i.e coding best practices, software design patterns, frontend architecture, etc).",
'Do frontend developers really code? The answer is yes, absolutely.',
'The fact that frontend developers are full-time developers who produce an output that is visually appealing (thanks to the designs provided by others) sometimes confuses others, making them believe that frontend developers aren\t really coding. However, that couldn\t be further from the truth.',
'As a frontend developer, you\ll be coding all the time.',
'While in some companies, the frontend developer is also a skilled designer or UX engineer, those are not the typical profiles. As a frontend dev, your learning focus should be coding-related (i.e coding best practices, software design patterns, frontend architecture, etc).',
],
},
{
question: 'Is Frontend Developer a good career?',
answer: [
"As the web space and technology in general continue to evolve, the role of frontend developers becomes more relevant. In the end, providing a web version of an application has quite a lot of benefits, making it the obvious version (unless there is a specific requirement forcing for native development) for most systems, meaning that frontend developers have the potential to be involved in all types of projects and companies.",
"This renders the frontend developer career one of the most versatile and in-demand paths in the web tech industry.",
'As the web space and technology in general continue to evolve, the role of frontend developers becomes more relevant. In the end, providing a web version of an application has quite a lot of benefits, making it the obvious version (unless there is a specific requirement forcing for native development) for most systems, meaning that frontend developers have the potential to be involved in all types of projects and companies.',
'This renders the frontend developer career one of the most versatile and in-demand paths in the web tech industry.',
],
},
{
question: 'How to prepare for a frontend developer interview?',
answer: [
"If you\re looking to prepare for a frontend developer interview, make sure you\ve tackled the fundamentals of the technologies you\ll work with.",
"And while that is one of the most impactful things you can do for your frontend career, consider focusing on the following points as well:",
"1. **Master the Fundamentals**: Ensure a solid understanding of HTML, CSS, and JavaScript, as they are crucial for the role of frontend developer.",
"2. **Practice Coding**: Improve your skills through mini-projects or coding challenges on platforms like LeetCode, especially those focused on front-end development. Using these skills to create a [web developer portfolio](https://roadmap.sh/frontend/web-developer-portfolio) can help you in many ways.",
"3. **Learn Modern Frameworks**: Familiarize yourself with popular frameworks like React, Angular, or Vue.js.",
"4. **Know Your Tools**: Be comfortable with version control systems, testing, and build tools, which are vital for all types of development, including frontend.",
"5. **Understand UI/UX Principles**: Learn about accessibility, responsive design, and intuitive interfaces to stand out.",
"6. **Research the Company**: Show interest by learning about the company\s business and products.",
"7. **Enhance Communication Skills**: Good communication skills are essential for success in any role, so make sure to work on them.",
'If you\re looking to prepare for a frontend developer interview, make sure you\ve tackled the fundamentals of the technologies you\ll work with.',
'And while that is one of the most impactful things you can do for your frontend career, consider focusing on the following points as well:',
'1. **Master the Fundamentals**: Ensure a solid understanding of HTML, CSS, and JavaScript, as they are crucial for the role of frontend developer.',
'2. **Practice Coding**: Improve your skills through mini-projects or coding challenges on platforms like LeetCode, especially those focused on front-end development. Using these skills to create a [web developer portfolio](https://roadmap.sh/frontend/web-developer-portfolio) can help you in many ways.',
'3. **Learn Modern Frameworks**: Familiarize yourself with popular frameworks like React, Angular, or Vue.js.',
'4. **Know Your Tools**: Be comfortable with version control systems, testing, and build tools, which are vital for all types of development, including frontend.',
'5. **Understand UI/UX Principles**: Learn about accessibility, responsive design, and intuitive interfaces to stand out.',
'6. **Research the Company**: Show interest by learning about the company\s business and products.',
'7. **Enhance Communication Skills**: Good communication skills are essential for success in any role, so make sure to work on them.',
],
},
{
question: 'How is Frontend Development different from Backend Development?',
answer: [
"The main difference between frontend development and backend development is that frontend development focuses on the UI, while [backend development](https://roadmap.sh/backend) focuses more on the server-side logic.",
"You see, frontend development works with the user interface and user experience, dealing with the design, layout, and interactivity of a website or application using HTML, CSS, and JavaScript (or TypeScript).",
"On the other hand, backend development handles the server-side logic, databases, and application functionality, ensuring data is processed and served correctly. The tech stack for backend development is much bigger with more options, such as Python, Java, or Node.js.",
"Both options are equally interesting and challenging, so it\s not really a question of backend vs frontend, but instead of understanding where you feel more comfortable and what type of systems you enjoy creating.",
'The main difference between frontend development and backend development is that frontend development focuses on the UI, while [backend development](https://roadmap.sh/backend) focuses more on the server-side logic.',
'You see, frontend development works with the user interface and user experience, dealing with the design, layout, and interactivity of a website or application using HTML, CSS, and JavaScript (or TypeScript).',
'On the other hand, backend development handles the server-side logic, databases, and application functionality, ensuring data is processed and served correctly. The tech stack for backend development is much bigger with more options, such as Python, Java, or Node.js.',
'Both options are equally interesting and challenging, so it\s not really a question of backend vs frontend, but instead of understanding where you feel more comfortable and what type of systems you enjoy creating.',
],
},
{
question: 'What are the job titles of a Frontend Developer?',
answer: [
"Front-end developers are also known as front-end engineers, front-end web developers, JavaScript Developers, HTML/CSS Developer, front-end web designers, and front-end web architects.",
'Front-end developers are also known as front-end engineers, front-end web developers, JavaScript Developers, HTML/CSS Developer, front-end web designers, and front-end web architects.',
"Each of these roles mostly encompasses the same front-end development skills but requires different levels of expertise in different [front-end development skills](https://roadmap.sh/frontend/developer-skills). It's better to look at the job description to get an idea about the job requirements.",
],
},
@@ -57,16 +57,16 @@ export const faqs: FAQType[] = [
{
question: 'How long does it take to become a Frontend Developer?',
answer: [
"The amount of time it takes to become a frontend developer can vary depending on several factors, such as your learning pace, previous experience, and the amount of time you are able to dedicate to learning.",
'The amount of time it takes to become a frontend developer can vary depending on several factors, such as your learning pace, previous experience, and the amount of time you are able to dedicate to learning.',
"However, to give you a rough idea, if you are a complete beginner, it could take you anywhere from 3 to 6 months to get a job as an entry-level frontend developer. If you are already familiar with some of the frontend technologies, it could take you anywhere from 1 to 3 months. What's important is to practice as much as you can while you are learning i.e., by building as many projects as you can. You should also participate in online communities and ask for feedback from more experienced developers to accelerate your learning process.",
],
},
{
question: 'What are the Frontend Developer salaries?',
answer: [
"Frontend developer salaries can vary depending on factors such as location, experience, and company size. According to data from Glassdoor, the average base salary for a frontend developer in the United States is around $80,000 per year. However, this number can vary greatly depending on location, with the highest-paying cities such as San Francisco, Seattle, and New York having an average salary of $110,000 to $130,000.",
'Frontend developer salaries can vary depending on factors such as location, experience, and company size. According to data from Glassdoor, the average base salary for a frontend developer in the United States is around $80,000 per year. However, this number can vary greatly depending on location, with the highest-paying cities such as San Francisco, Seattle, and New York having an average salary of $110,000 to $130,000.',
"It's important to keep in mind that these are just averages, and salaries can vary greatly depending on factors such as experience level, specific skills, and the company you work for. With more experience and specific skills, you can expect to earn more.",
"It is worth looking at a range of resources, including salary surveys and job boards, to get a general understanding of the current market in your location and experience level. Also, try reaching out to other professionals in the field and understanding their experience and salary ranges.",
'It is worth looking at a range of resources, including salary surveys and job boards, to get a general understanding of the current market in your location and experience level. Also, try reaching out to other professionals in the field and understanding their experience and salary ranges.',
],
},
{
@@ -79,8 +79,8 @@ export const faqs: FAQType[] = [
{
question: 'What are Frontend Frameworks?',
answer: [
"[Frontend frameworks](https://roadmap.sh/frontend/frameworks) are collections of tools and libraries that help developers build web applications more efficiently. They provide a structure for the code, making it easier to build and maintain complex applications.",
"Some popular frontend frameworks include React, Angular, and Vue.js. These frameworks provide a set of tools and libraries that help developers build user interfaces, manage state, and interact with APIs.",
'[Frontend frameworks](https://roadmap.sh/frontend/frameworks) are collections of tools and libraries that help developers build web applications more efficiently. They provide a structure for the code, making it easier to build and maintain complex applications.',
'Some popular frontend frameworks include React, Angular, and Vue.js. These frameworks provide a set of tools and libraries that help developers build user interfaces, manage state, and interact with APIs.',
],
},
];

View File

@@ -1,5 +1,5 @@
---
import type { FAQType } from '../../../components/FAQs/FAQs.astro';
import type { FAQType } from '../../../components/FAQs/FAQs';
export const faqs: FAQType[] = [
{

View File

@@ -30,10 +30,12 @@ export interface MarkType {
attrs?: Record<string, any> | undefined;
}
export type GuideRendererOptions = {};
export class GuideRenderer {
private marksOrder = ['underline', 'bold', 'italic', 'textStyle', 'link'];
render(content: JSONContent): JSX.Element[] {
render(content: JSONContent) {
const nodes = content.content || [];
const jsxNodes = nodes
.map((node, index) => {

View File

@@ -1,4 +1,5 @@
import type { FAQType } from '../components/FAQs/FAQs.astro';
import type { OfficialRoadmapQuestion } from '../queries/official-roadmap';
import { renderMarkdownFromJson } from './markdown-renderer';
type ArticleSchemaProps = {
url: string;
@@ -41,16 +42,16 @@ export function generateArticleSchema(article: ArticleSchemaProps) {
};
}
export function generateFAQSchema(faqs: FAQType[]) {
export function generateFAQSchema(faqs: OfficialRoadmapQuestion[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
name: faq.title,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer.join(' '),
text: renderMarkdownFromJson(faq.description, { join: ' ' }),
},
})),
};

View File

@@ -0,0 +1,134 @@
import type { JSONContent } from '@tiptap/core';
export type MarkdownRendererOptions = {
join?: string;
};
export class MarkdownRenderer {
private marksOrder = ['underline', 'bold', 'italic', 'textStyle', 'link'];
render(content: JSONContent, options: MarkdownRendererOptions = {}): string {
const nodes = content.content || [];
return nodes
.map((node) => this.renderNode(node))
.join(options?.join || '\n\n');
}
private renderNode(node: JSONContent): string {
const type = node.type || '';
if (type in this) {
// @ts-expect-error dynamic lookup
return this[type]?.(node);
}
console.warn(`Node type "${type}" is not supported.`);
return '';
}
private getText(node: JSONContent): string {
if (node.type === 'text') return node.text || '';
if (node.content)
return node.content.map((child) => this.getText(child)).join('');
return '';
}
private content(node: JSONContent): string {
return (node.content || []).map((child) => this.renderNode(child)).join('');
}
private renderMark(node: JSONContent): string {
let text = node.text || '';
let marks = node.marks || [];
marks.sort(
(a, b) =>
this.marksOrder.indexOf(a.type) - this.marksOrder.indexOf(b.type),
);
return marks.reduce((acc, mark) => {
if (mark.type === 'bold') return `**${acc}**`;
if (mark.type === 'italic') return `*${acc}*`;
if (mark.type === 'underline') return `_${acc}_`; // fallback since markdown has no underline
if (mark.type === 'code') return `\`${acc}\``;
if (mark.type === 'link') return `[${acc}](${mark.attrs?.href})`;
return acc;
}, text);
}
// ---- Nodes ----
private paragraph(node: JSONContent): string {
return this.content(node);
}
private text(node: JSONContent): string {
return node.marks ? this.renderMark(node) : node.text || '';
}
private heading(node: JSONContent): string {
const level = node.attrs?.level || 1;
const prefix = '#'.repeat(level);
return `${prefix} ${this.content(node)}`;
}
private bulletList(node: JSONContent): string {
return (node.content || [])
.map((child) => `- ${this.renderNode(child)}`)
.join('\n');
}
private orderedList(node: JSONContent): string {
return (node.content || [])
.map((child, i) => `${i + 1}. ${this.renderNode(child)}`)
.join('\n');
}
private listItem(node: JSONContent): string {
return this.content(node);
}
private blockquote(node: JSONContent): string {
return this.content(node)
.split('\n')
.map((line) => `> ${line}`)
.join('\n');
}
private codeBlock(node: JSONContent): string {
const code = this.getText(node);
const language = node.attrs?.language || '';
return `\`\`\`${language}\n${code}\n\`\`\``;
}
private horizontalRule(): string {
return `---`;
}
private image(node: JSONContent): string {
const { src, alt } = node.attrs || {};
return `![${alt || ''}](${src})`;
}
private table(node: JSONContent): string {
const rows = (node.content || []).filter((n) => n.type === 'tableRow');
return rows.map((row) => this.renderNode(row)).join('\n');
}
private tableRow(node: JSONContent): string {
return `| ${this.content(node)} |`;
}
private tableHeader(node: JSONContent): string {
return this.content(node);
}
private tableCell(node: JSONContent): string {
return this.content(node);
}
}
export function renderMarkdownFromJson(
json: JSONContent,
options: MarkdownRendererOptions = {},
) {
return new MarkdownRenderer().render(json, options);
}

View File

@@ -1,5 +1,8 @@
import {
officialRoadmapDetails,
type OfficialRoadmapDocument,
} from '../queries/official-roadmap';
import type { MarkdownFileType } from './file';
import { getRoadmapById, type RoadmapFileType } from './roadmap';
export const projectDifficulties = [
'beginner',
@@ -28,7 +31,7 @@ export interface ProjectFrontmatter {
export type ProjectFileType = MarkdownFileType<ProjectFrontmatter> & {
id: string;
roadmaps: RoadmapFileType[];
roadmaps: OfficialRoadmapDocument[];
};
/**
@@ -85,7 +88,7 @@ export async function getProjectById(
const project = await import(`../data/projects/${groupId}.md`);
const roadmapIds = project.frontmatter.roadmapIds || [];
const roadmaps = await Promise.all(
roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)),
roadmapIds.map((roadmapId: string) => officialRoadmapDetails(roadmapId)),
);
return {

View File

@@ -1,19 +1,7 @@
import type { PageType } from '../components/CommandMenu/CommandMenu';
import type { MarkdownFileType } from './file';
import { httpGet } from './http';
import type { ResourceType } from './resource-progress';
export function resourceTitleFromId(id: string): string {
if (id === 'devops') {
return 'DevOps';
}
return id
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
export type AllowedRoadmapRenderer = 'balsamiq' | 'editor';
export interface RoadmapFrontmatter {
@@ -76,99 +64,6 @@ export interface RoadmapFrontmatter {
renderer?: AllowedRoadmapRenderer;
}
export type RoadmapFileType = MarkdownFileType<RoadmapFrontmatter> & {
id: string;
};
function roadmapPathToId(filePath: string): string {
const fileName = filePath.split('/').pop() || '';
return fileName.replace('.md', '');
}
/**
* Gets the IDs of all the roadmaps available on the website
*
* @returns string[] Array of roadmap IDs
*/
export async function getRoadmapIds() {
const roadmapFiles = import.meta.glob<RoadmapFileType>(
'/src/data/roadmaps/*/*.md',
{
eager: true,
},
);
return Object.keys(roadmapFiles).map(roadmapPathToId);
}
/**
* Gets the roadmap files which have the given tag assigned
*
* @param tag Tag assigned to roadmap
* @returns Promisified RoadmapFileType[]
*/
export async function getRoadmapsByTag(
tag: string,
): Promise<RoadmapFileType[]> {
const roadmapFilesMap = import.meta.glob<RoadmapFileType>(
'/src/data/roadmaps/*/*.md',
{
eager: true,
},
);
const roadmapFiles: RoadmapFileType[] = Object.values(roadmapFilesMap);
const filteredRoadmaps = roadmapFiles
.filter((roadmapFile) => roadmapFile.frontmatter.tags?.includes(tag))
.map((roadmapFile) => ({
...roadmapFile,
id: roadmapPathToId(roadmapFile.file),
}));
return filteredRoadmaps.sort(
(a, b) => a.frontmatter.order - b.frontmatter.order,
);
}
export async function getRoadmapById(id: string): Promise<RoadmapFileType> {
const roadmapFilesMap: Record<string, RoadmapFileType> =
import.meta.glob<RoadmapFileType>('/src/data/roadmaps/*/*.md', {
eager: true,
});
const roadmapFile = Object.values(roadmapFilesMap).find((roadmapFile) => {
return roadmapPathToId(roadmapFile.file) === id;
});
if (!roadmapFile) {
throw new Error(`Roadmap with ID ${id} not found`);
}
return {
...roadmapFile,
id: roadmapPathToId(roadmapFile.file),
};
}
export async function getRoadmapsByIds(
ids: string[],
): Promise<RoadmapFileType[]> {
if (!ids?.length) {
return [];
}
return Promise.all(ids.map((id) => getRoadmapById(id)));
}
export async function getRoadmapFaqsById(roadmapId: string): Promise<string[]> {
const { faqs } = await import(
`../data/roadmaps/${roadmapId}/faqs.astro`
).catch(() => ({}));
return faqs || [];
}
export async function getResourceMeta(
resourceType: ResourceType,
resourceId: string,

View File

@@ -1,10 +1,13 @@
---
import Icon from '../components/AstroIcon.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getRoadmapIds } from '../lib/roadmap';
import { listOfficialRoadmaps } from '../queries/official-roadmap';
const roadmapIds = await getRoadmapIds();
const legacyRoadmapUrls = [...roadmapIds.map((id) => `/${id}/`), '/roadmaps/'];
const roadmapIds = await listOfficialRoadmaps();
const legacyRoadmapUrls = [
...roadmapIds.map((roadmap) => `/${roadmap.slug}/`),
'/roadmaps/',
];
---
<BaseLayout title='Page not found' permalink={'/404'} noIndex={true}>
@@ -18,20 +21,26 @@ const legacyRoadmapUrls = [...roadmapIds.map((id) => `/${id}/`), '/roadmaps/'];
</script>
<div class='bg-gray-100'>
<div class='py-10 md:py-32 container flex flex-col md:flex-row items-center justify-center gap-7'>
<div
class='container flex flex-col items-center justify-center gap-7 py-10 md:flex-row md:py-32'
>
<Icon icon='construction' class='hidden md:block' />
<div class='text-left md:text-left'>
<h1
class='font-extrabold text-transparent text-2xl leading-normal md:text-5xl md:leading-normal bg-clip-text bg-linear-to-t from-black to-gray-600'
class='bg-linear-to-t from-black to-gray-600 bg-clip-text text-2xl leading-normal font-extrabold text-transparent md:text-5xl md:leading-normal'
>
Page not found!
</h1>
<p class='text-md md:text-xl mb-2'>Sorry, we couldn't find the page you are looking for.</p>
<p class='text-md mb-2 md:text-xl'>
Sorry, we couldn't find the page you are looking for.
</p>
<p>
<a class='underline text-blue-700' href='/'>Homepage</a> &middot; <a
<a class='text-blue-700 underline' href='/'>Homepage</a> &middot; <a
href='/roadmaps'
class='underline text-blue-700'>Roadmaps</a
> &middot; <a href='/best-practices' class='underline text-blue-700'>Best Practices</a>
class='text-blue-700 underline'>Roadmaps</a
> &middot; <a href='/best-practices' class='text-blue-700 underline'
>Best Practices</a
>
</p>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import path from 'node:path';
import fs from 'node:fs';
import matter from 'gray-matter';
import { fileURLToPath } from 'node:url';
import type { OfficialRoadmapDocument } from '../queries/official-roadmap';
export const prerender = false;
@@ -10,9 +11,6 @@ export const prerender = false;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// hack to make it work. TODO: Fix
const projectRoot = path.resolve(__dirname, '../..').replace(/dist$/, '');
type RoadmapJson = {
_id: string;
title: string;
@@ -30,10 +28,16 @@ type RoadmapJson = {
export async function fetchRoadmapJson(
roadmapId: string,
): Promise<RoadmapJson> {
const response = await fetch(
`https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`,
): Promise<OfficialRoadmapDocument> {
const isDev = import.meta.env.DEV;
const baseUrl = new URL(
isDev ? 'http://localhost:8080' : 'https://roadmap.sh',
);
baseUrl.pathname = isDev
? `/v1-official-roadmap/${roadmapId}`
: `/api/v1-official-roadmap/${roadmapId}`;
const response = await fetch(String(baseUrl));
if (!response.ok) {
throw new Error(`Failed to fetch roadmap json: ${response.statusText}`);
@@ -56,48 +60,7 @@ export const GET: APIRoute = async function ({ params, request, props }) {
});
}
// Construct the path to the markdown file
let roadmapFilePath = path.join(
projectRoot,
'src',
'data',
'roadmaps',
roadmapId,
`${roadmapId}.md`,
);
let roadmapJsonPath = path.join(
projectRoot,
'src',
'data',
'roadmaps',
roadmapId,
`${roadmapId}.json`,
);
if (!fs.existsSync(roadmapFilePath)) {
return new Response(JSON.stringify({ message: 'Roadmap not found' }), {
status: 404,
});
}
// Read and parse the markdown file
const fileContent = fs.readFileSync(roadmapFilePath, 'utf-8');
const { data: frontmatter, content } = matter(fileContent);
if (frontmatter.renderer !== 'editor') {
const roadmapJson = JSON.parse(fs.readFileSync(roadmapJsonPath, 'utf-8'));
return new Response(JSON.stringify(roadmapJson), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
}
const roadmapJson = await fetchRoadmapJson(roadmapId);
return new Response(JSON.stringify(roadmapJson), {
status: 200,
headers: {

View File

@@ -1,43 +0,0 @@
---
import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification';
import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat';
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
import { AITutorLayout } from '../../components/AITutor/AITutorLayout';
import { getRoadmapById, getRoadmapIds } from '../../lib/roadmap';
type Props = {
roadmapId: string;
};
export const prerender = false;
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
const { roadmapId } = Astro.params as Props;
const roadmapDetail = await getRoadmapById(roadmapId);
const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`;
const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle;
---
<SkeletonLayout
title={`${roadmapBriefTitle} AI Mentor`}
description=`Learn anything ${roadmapBriefTitle} with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.`
canonicalUrl={canonicalUrl}
>
<AITutorLayout
activeTab='chat'
wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden'
client:load
>
<RoadmapAIChat roadmapId={roadmapId} client:load />
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</SkeletonLayout>

View File

@@ -2,35 +2,26 @@
import RoadmapHeader from '../../components/RoadmapHeader.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import CourseStep from '../../components/courses/CourseStep.astro';
import Milestone from '../../components/courses/Milestone.astro';
import { getProjectsByRoadmapId } from '../../lib/project';
import { officialRoadmapDetails } from '../../queries/official-roadmap';
export const prerender = true;
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const roadmapFile = await import(
`../../data/roadmaps/${roadmapId}/${roadmapId}.md`
);
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
const roadmapData = await officialRoadmapDetails(roadmapId);
if (!roadmapData) {
Astro.response.status = 404;
Astro.response.statusText = 'Not found';
return Astro.rewrite('/404');
}
// update og for projects
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
roadmapData?.openGraph?.image ||
getOpenGraphImageUrl({
group: 'roadmap',
resourceId: roadmapId,
@@ -43,23 +34,23 @@ const descriptionNoun: Record<string, string> = {
'Product Manager': 'Product Management',
};
const title = `${roadmapData.briefTitle} Courses`;
const description = `Premium courses to help you master ${descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle}`;
const title = `${roadmapData?.title.card} Courses`;
const description = `Premium courses to help you master ${descriptionNoun[roadmapData?.title.card] || roadmapData?.title.card}`;
const seoTitle = `${roadmapData.briefTitle} Courses`;
const seoTitle = `${roadmapData?.title.card} Courses`;
const nounTitle =
descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle;
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 courses = roadmapData.courses || [];
const courses = roadmapData?.courses || [];
---
<BaseLayout
permalink={`/${roadmapId}`}
title={seoTitle}
description={seoDescription}
briefTitle={roadmapData.briefTitle}
briefTitle={roadmapData.title.card}
ogImageUrl={ogImageUrl}
keywords={roadmapData.seo.keywords}
resourceId={roadmapId}
@@ -70,13 +61,9 @@ const courses = roadmapData.courses || [];
<RoadmapHeader
title={title}
description={description}
note={roadmapData.note}
partner={roadmapData.partner}
roadmapId={roadmapId}
hasTopics={roadmapData.hasTopics}
isUpcoming={roadmapData.isUpcoming}
isForkable={roadmapData.isForkable}
question={roadmapData.question}
isForkable={true}
coursesCount={courses.length}
projectCount={projects.length}
activeTab='courses'

View File

@@ -1,7 +1,6 @@
---
import { EditorRoadmap } from '../../components/EditorRoadmap/EditorRoadmap';
import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
import { FAQs } from '../../components/FAQs/FAQs';
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
import RoadmapHeader from '../../components/RoadmapHeader.astro';
import { ShareIcons } from '../../components/ShareIcons/ShareIcons';
@@ -13,82 +12,83 @@ import {
generateFAQSchema,
} from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
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';
import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification';
import { officialRoadmapDetails } from '../../queries/official-roadmap';
import { DateTime } from 'luxon';
export const prerender = true;
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const roadmapFile = await import(
`../../data/roadmaps/${roadmapId}/${roadmapId}.md`
);
const { faqs: roadmapFAQs = [] } = await import(
`../../data/roadmaps/${roadmapId}/faqs.astro`
);
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
const roadmapData = await officialRoadmapDetails(roadmapId);
if (!roadmapData) {
Astro.response.status = 404;
Astro.response.statusText = 'Not found';
return Astro.rewrite('/404');
}
let jsonLdSchema = [];
if (roadmapData.schema) {
const roadmapSchema = roadmapData.schema;
jsonLdSchema.push(
generateArticleSchema({
url: `https://roadmap.sh/${roadmapId}`,
headline: roadmapSchema.headline,
description: roadmapSchema.description,
datePublished: roadmapSchema.datePublished,
dateModified: roadmapSchema.dateModified,
imageUrl: roadmapSchema.imageUrl,
}),
);
}
const datePublished = DateTime.fromJSDate(
new Date(roadmapData?.createdAt),
).toFormat('yyyy-MM-dd');
const dateModified = DateTime.fromJSDate(
new Date(roadmapData?.updatedAt),
).toFormat('yyyy-MM-dd');
if (roadmapFAQs.length) {
jsonLdSchema.push(generateFAQSchema(roadmapFAQs as unknown as FAQType[]));
}
const baseUrl = import.meta.env.DEV
? `http://localhost:8080`
: `https://roadmap.sh`;
jsonLdSchema.push(
generateArticleSchema({
url: `https://roadmap.sh/${roadmapId}`,
headline: roadmapData?.seo?.title || roadmapData?.title?.page,
description: roadmapData?.description,
datePublished,
dateModified,
imageUrl: `${baseUrl}/roadmaps/${roadmapId}.png`,
}),
);
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
roadmapData?.openGraph?.image ||
getOpenGraphImageUrl({
group: 'roadmap',
resourceId: roadmapId,
});
const question = roadmapData?.question;
const note = roadmapData.note;
const question = roadmapData?.questions?.find(
(question) => question.type === 'main',
);
const faqs =
roadmapData?.questions?.filter((question) => question.type === 'faq') || [];
if (faqs.length) {
jsonLdSchema.push(generateFAQSchema(faqs));
}
const projects = await getProjectsByRoadmapId(roadmapId);
const courses = roadmapData.courses || [];
---
<BaseLayout
permalink={`/${roadmapId}`}
title={roadmapData?.seo?.title}
briefTitle={roadmapData.briefTitle}
title={roadmapData?.seo?.title || roadmapData?.title.page}
briefTitle={roadmapData.title.card}
ogImageUrl={ogImageUrl}
description={roadmapData.seo.description}
keywords={roadmapData.seo.keywords}
noIndex={roadmapData.isUpcoming}
noIndex={false}
jsonLd={jsonLdSchema}
resourceId={roadmapId}
resourceType='roadmap'
>
<!-- Preload the font being used in the renderer -->
<link
rel='preload'
href='/fonts/balsamiq.woff2'
@@ -101,25 +101,21 @@ const courses = roadmapData.courses || [];
<TopicDetail
resourceId={roadmapId}
resourceType='roadmap'
renderer={roadmapData.renderer}
renderer='editor'
client:idle
canSubmitContribution={true}
/>
<div class='bg-gray-50'>
<RoadmapHeader
title={roadmapData.title}
title={roadmapData.title.page}
description={roadmapData.description}
note={roadmapData.note}
partner={roadmapData.partner}
roadmapId={roadmapId}
hasTopics={roadmapData.hasTopics}
isUpcoming={roadmapData.isUpcoming}
isForkable={roadmapData.isForkable}
question={roadmapData.question}
isForkable={true}
projectCount={projects.length}
coursesCount={courses.length}
hasAIChat={roadmapData.renderer === 'editor'}
hasAIChat={true}
/>
<div class='container mt-2.5'>
@@ -142,33 +138,23 @@ const courses = roadmapData.courses || [];
<ShareIcons
resourceId={roadmapId}
resourceType='roadmap'
description={roadmapData.briefDescription}
description={roadmapData.description}
pageUrl={`https://roadmap.sh/${roadmapId}`}
client:load
/>
{
roadmapData?.renderer === 'editor' ? (
<EditorRoadmap
resourceId={roadmapId}
resourceType='roadmap'
dimensions={roadmapData.dimensions!}
client:load
/>
) : (
<FrameRenderer
resourceType={'roadmap'}
resourceId={roadmapId}
dimensions={roadmapData.dimensions}
/>
)
}
<EditorRoadmap
resourceId={roadmapId}
resourceType='roadmap'
dimensions={roadmapData.dimensions!}
client:load
/>
</div>
<UserProgressModal
resourceId={roadmapId}
resourceType='roadmap'
renderer={roadmapData?.renderer}
renderer='editor'
client:only='react'
/>
@@ -180,9 +166,8 @@ const courses = roadmapData.courses || [];
)
}
<FAQs faqs={roadmapFAQs as unknown as FAQType[]} />
<RelatedRoadmaps roadmap={roadmapData} />
<FAQs faqs={faqs} client:load />
<RelatedRoadmaps relatedRoadmaps={roadmapData?.relatedRoadmaps || []} />
</div>
<CheckSubscriptionVerification client:load />

View File

@@ -5,33 +5,26 @@ import { ProjectsList } from '../../components/Projects/ProjectsList';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getProjectsByRoadmapId } from '../../lib/project';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import { projectApi } from '../../api/project';
import { officialRoadmapDetails } from '../../queries/official-roadmap';
export const prerender = true;
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const roadmapFile = await import(
`../../data/roadmaps/${roadmapId}/${roadmapId}.md`
);
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
const roadmapData = await officialRoadmapDetails(roadmapId);
if (!roadmapData) {
Astro.response.status = 404;
Astro.response.statusText = 'Not found';
return Astro.rewrite('/404');
}
// update og for projects
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
roadmapData?.openGraph?.image ||
getOpenGraphImageUrl({
group: 'roadmap',
resourceId: roadmapId,
@@ -44,13 +37,13 @@ const descriptionNoun: Record<string, string> = {
'Product Manager': 'Product Management',
};
const title = `${roadmapData.briefTitle} Projects`;
const description = `Project ideas to take you from beginner to advanced in ${descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle}`;
const title = `${roadmapData.title.card} Projects`;
const description = `Project ideas to take you from beginner to advanced in ${descriptionNoun[roadmapData.title.card] || roadmapData.title.card}`;
// `Seeking backend projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`
const seoTitle = `${roadmapData.briefTitle} Projects`;
const seoTitle = `${roadmapData.title.card} Projects`;
const nounTitle =
descriptionNoun[roadmapData?.briefTitle] || roadmapData.briefTitle;
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);
@@ -65,7 +58,7 @@ const { response: userCounts } =
permalink={`/${roadmapId}/projects`}
title={seoTitle}
description={seoDescription}
briefTitle={roadmapData.briefTitle}
briefTitle={roadmapData.title.card}
ogImageUrl={ogImageUrl}
keywords={roadmapData.seo.keywords}
noIndex={projects.length === 0}
@@ -76,13 +69,9 @@ const { response: userCounts } =
<RoadmapHeader
title={title}
description={description}
note={roadmapData.note}
partner={roadmapData.partner}
roadmapId={roadmapId}
hasTopics={roadmapData.hasTopics}
isUpcoming={roadmapData.isUpcoming}
isForkable={roadmapData.isForkable}
question={roadmapData.question}
isForkable={true}
activeTab='projects'
projectCount={projects.length}
coursesCount={roadmapData.courses?.length || 0}

View File

@@ -1,32 +1,25 @@
---
import { EditorRoadmap } from '../../components/EditorRoadmap/EditorRoadmap';
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import { officialRoadmapDetails } from '../../queries/official-roadmap';
export const prerender = true;
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const roadmapFile = await import(
`../../data/roadmaps/${roadmapId}/${roadmapId}.md`
);
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
const roadmapData = await officialRoadmapDetails(roadmapId);
if (!roadmapData) {
Astro.response.status = 404;
Astro.response.statusText = 'Not found';
return Astro.rewrite('/404');
}
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
roadmapData?.openGraph?.image ||
getOpenGraphImageUrl({
group: 'roadmap',
resourceId: roadmapId,
@@ -35,8 +28,8 @@ const ogImageUrl =
<SkeletonLayout
permalink={`/${roadmapId}`}
title={roadmapData?.seo?.title}
briefTitle={roadmapData.briefTitle}
title={roadmapData?.seo?.title || roadmapData.title.page}
briefTitle={roadmapData.title.card}
ogImageUrl={ogImageUrl}
description={roadmapData.seo.description}
keywords={roadmapData.seo.keywords}
@@ -44,23 +37,13 @@ const ogImageUrl =
resourceType='roadmap'
noIndex={true}
>
<div class='container relative max-w-[1000px]!'>
{
roadmapData?.renderer === 'editor' ? (
<EditorRoadmap
resourceId={roadmapId}
resourceType='roadmap'
dimensions={roadmapData.dimensions!}
client:load
hasChat={false}
/>
) : (
<FrameRenderer
resourceType={'roadmap'}
resourceId={roadmapId}
dimensions={roadmapData.dimensions}
/>
)
}
<div class='relative container max-w-[1000px]!'>
<EditorRoadmap
resourceId={roadmapId}
resourceType='roadmap'
dimensions={roadmapData.dimensions!}
client:load
hasChat={false}
/>
</div>
</SkeletonLayout>

View File

@@ -1,56 +1,53 @@
---
import { DateTime } from 'luxon';
import { DashboardPage } from '../components/Dashboard/DashboardPage';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getAllBestPractices } from '../lib/best-practice';
import { getAllQuestionGroups } from '../lib/question-group';
import { getRoadmapsByTag } from '../lib/roadmap';
import { getAllVideos } from '../lib/video';
import { listOfficialGuides } from '../queries/official-guide';
import {
isNewRoadmap,
listOfficialRoadmaps,
} from '../queries/official-roadmap';
import type { BuiltInRoadmap } from '../components/Dashboard/PersonalDashboard';
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
const roadmaps = await listOfficialRoadmaps();
const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
const bestPractices = await getAllBestPractices();
const questionGroups = await getAllQuestionGroups();
const guides = await listOfficialGuides();
const videos = await getAllVideos();
const enrichedRoleRoadmaps = roleRoadmaps
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
.map((roadmap) => {
const { frontmatter } = roadmap;
return {
id: roadmap.id,
url: `/${roadmap.id}`,
title: frontmatter.briefTitle,
description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer,
isNew: frontmatter.isNew,
metadata: {
tags: frontmatter.tags,
},
};
});
const enrichedSkillRoadmaps = skillRoadmaps
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
.map((roadmap) => {
const { frontmatter } = roadmap;
return {
id: roadmap.id,
url: `/${roadmap.id}`,
title:
frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle,
description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer,
isNew: frontmatter.isNew,
metadata: {
tags: frontmatter.tags,
},
};
});
const enrichedRoleRoadmaps: BuiltInRoadmap[] = roleRoadmaps.map((roadmap) => {
return {
id: roadmap.slug,
url: `/${roadmap.slug}`,
title: roadmap.title.card,
description: roadmap.description,
relatedRoadmapIds: roadmap.relatedRoadmaps,
renderer: 'editor',
isNew: isNewRoadmap(roadmap.createdAt),
metadata: {
tags: ['role-roadmap'],
},
};
});
const enrichedSkillRoadmaps: BuiltInRoadmap[] = skillRoadmaps.map((roadmap) => {
return {
id: roadmap.slug,
url: `/${roadmap.slug}`,
title: roadmap.title.card === 'Go' ? 'Go Roadmap' : roadmap.title.card,
description: roadmap.description,
relatedRoadmapIds: roadmap.relatedRoadmaps,
renderer: 'editor',
isNew: isNewRoadmap(roadmap.createdAt),
metadata: {
tags: ['skill-roadmap'],
},
};
});
const enrichedBestPractices = bestPractices.map((bestPractice) => {
const { frontmatter } = bestPractice;

View File

@@ -1,4 +1,5 @@
---
import { DateTime } from 'luxon';
import ChangelogBanner from '../components/ChangelogBanner.astro';
import { FeaturedGuideList } from '../components/FeaturedGuides/FeaturedGuideList';
import FeaturedItems from '../components/FeaturedItems/FeaturedItems.astro';
@@ -6,12 +7,16 @@ import { FeaturedVideoList } from '../components/FeaturedVideos/FeaturedVideoLis
import HeroSection from '../components/HeroSection/HeroSection.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getAllBestPractices } from '../lib/best-practice';
import { getRoadmapsByTag } from '../lib/roadmap';
import { getAllVideos } from '../lib/video';
import { listOfficialGuides } from '../queries/official-guide';
import {
isNewRoadmap,
listOfficialRoadmaps,
} from '../queries/official-roadmap';
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
const roadmaps = await listOfficialRoadmaps();
const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
const bestPractices = await getAllBestPractices();
export const projectGroups = [
@@ -47,33 +52,32 @@ const videos = await getAllVideos();
<FeaturedItems
heading='Role-based Roadmaps'
featuredItems={roleRoadmaps
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
.map((roadmapItem) => ({
text: roadmapItem.frontmatter.briefTitle,
url: `/${roadmapItem.id}`,
isNew: roadmapItem.frontmatter.isNew,
isUpcoming: roadmapItem.frontmatter.isUpcoming,
}))}
featuredItems={roleRoadmaps.map((roadmapItem) => {
const isNew = isNewRoadmap(roadmapItem.createdAt);
return {
text: roadmapItem.title.card,
url: `/${roadmapItem.slug}`,
isNew,
};
})}
showCreateRoadmap={true}
/>
<FeaturedItems
heading='Skill-based Roadmaps'
featuredItems={skillRoadmaps
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
.map((roadmapItem) => ({
featuredItems={skillRoadmaps.map((roadmapItem) => {
const isNew = isNewRoadmap(roadmapItem.createdAt);
return {
text:
roadmapItem.frontmatter.briefTitle === 'Go'
roadmapItem.title.card === 'Go'
? 'Go Roadmap'
: roadmapItem.frontmatter.briefTitle.replace(
'Software Design',
'Design',
),
url: `/${roadmapItem.id}`,
isNew: roadmapItem.frontmatter.isNew,
isUpcoming: roadmapItem.frontmatter.isUpcoming,
}))}
: roadmapItem.title.card.replace('Software Design', 'Design'),
url: `/${roadmapItem.slug}`,
isNew,
};
})}
showCreateRoadmap={true}
/>

View File

@@ -1,5 +1,4 @@
import { getAllBestPractices } from '../lib/best-practice';
import { getRoadmapsByTag } from '../lib/roadmap';
import { getAllVideos } from '../lib/video';
import { getAllQuestionGroups } from '../lib/question-group';
import { getAllProjects } from '../lib/project';
@@ -7,6 +6,7 @@ import {
listOfficialAuthors,
listOfficialGuides,
} from '../queries/official-guide';
import { listOfficialRoadmaps } from '../queries/official-roadmap';
// Add utility to fetch beginner roadmap file IDs
function getBeginnerRoadmapIds() {
@@ -25,40 +25,49 @@ export async function GET() {
const authors = await listOfficialAuthors();
const videos = await getAllVideos();
const questionGroups = await getAllQuestionGroups();
const roadmaps = await getRoadmapsByTag('roadmap');
const roadmaps = await listOfficialRoadmaps();
const bestPractices = await getAllBestPractices();
const projects = await getAllProjects();
// Transform main roadmaps into page objects first so that we can reuse their meta for beginner variants
const roadmapPages = roadmaps.map((roadmap) => ({
id: roadmap.id,
url: `/${roadmap.id}`,
title: roadmap.frontmatter.briefTitle,
shortTitle: roadmap.frontmatter.title,
description: roadmap.frontmatter.briefDescription,
group: 'Roadmaps',
metadata: {
tags: roadmap.frontmatter.tags,
},
renderer: roadmap?.frontmatter?.renderer || 'balsamiq',
}));
const roadmapPages = roadmaps
.map((roadmap) => {
const isBeginner = roadmap.slug.endsWith('-beginner');
if (!isBeginner) {
return {
id: roadmap.slug,
url: `/${roadmap.slug}`,
title: roadmap.title.card,
shortTitle: roadmap.title.card,
description: roadmap.description,
group: 'Roadmaps',
metadata: {
tags:
roadmap.type === 'role' ? ['role-roadmap'] : ['skill-roadmap'],
},
renderer: 'editor',
};
}
// Generate beginner roadmap page objects
const beginnerRoadmapPages = getBeginnerRoadmapIds()
.map((beginnerId) => {
const parentId = beginnerId.replace('-beginner', '');
const parentMeta = roadmapPages.find((page) => page.id === parentId);
const parentSlug = roadmap.slug.replace('-beginner', '');
const parentMeta = roadmaps.find((r) => r.slug === parentSlug);
if (!parentMeta) {
return null;
}
return {
...parentMeta,
id: beginnerId,
url: `/${parentId}?r=${beginnerId}`,
title: `${parentMeta.title} Beginner`,
shortTitle: `${parentMeta.shortTitle} Beginner`,
id: roadmap.slug,
url: `/${parentSlug}?r=${roadmap.slug}`,
title: `${parentMeta.title.page} Beginner`,
shortTitle: `${parentMeta.title.page} Beginner`,
description: parentMeta.description,
group: 'Roadmaps',
metadata: {
tags: ['beginner-roadmap'],
},
renderer: 'editor',
};
})
.filter(Boolean);
@@ -66,7 +75,6 @@ export async function GET() {
return new Response(
JSON.stringify([
...roadmapPages,
...beginnerRoadmapPages,
...bestPractices.map((bestPractice) => ({
id: bestPractice.id,
url: `/best-practices/${bestPractice.id}`,

View File

@@ -11,17 +11,7 @@ import { ProjectStepper } from '../../../components/Projects/StatusStepper/Proje
import { ProjectTrackingActions } from '../../../components/Projects/StatusStepper/ProjectTrackingActions';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
export const prerender = true;
export async function getStaticPaths() {
const projects = await getAllProjects();
return projects
.map((project) => project.id)
.map((projectId) => ({
params: { projectId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
projectId: string;
@@ -30,6 +20,12 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params;
const project = await getProjectById(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[] = [];

View File

@@ -9,18 +9,7 @@ import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions';
import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal';
export const prerender = true;
export async function getStaticPaths() {
const projects = await getAllProjects();
return projects
.filter((project) => !(project?.frontmatter?.hasNoSubmission || false))
.map((project) => project.id)
.map((projectId) => ({
params: { projectId },
}));
}
export const prerender = false;
interface Params extends Record<string, string | undefined> {
projectId: string;
@@ -29,6 +18,12 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params;
const project = await getProjectById(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[] = [];

View File

@@ -1,23 +1,29 @@
---
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';
import { listOfficialRoadmaps } from '../../queries/official-roadmap';
export const prerender = false;
const roadmapProjects = await getRoadmapsProjects();
const allRoadmapIds = Object.keys(roadmapProjects);
const allRoadmaps = await getRoadmapsByIds(allRoadmapIds);
const roadmaps = await listOfficialRoadmaps();
const allRoadmaps = roadmaps.filter((roadmap) =>
allRoadmapIds.includes(roadmap.slug),
);
const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projects = (roadmapProjects[roadmap.id] || []).sort((a, b) => {
const projects = (roadmapProjects[roadmap.slug] || []).sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort;
});
return {
id: roadmap.id,
title: roadmap.frontmatter.briefTitle,
id: roadmap.slug,
title: roadmap.title.card,
projects,
};
});
@@ -42,5 +48,5 @@ const { response: userCounts } =
userCounts={userCounts || {}}
client:load
/>
<div slot="changelog-banner" />
<div slot='changelog-banner'></div>
</BaseLayout>

View File

@@ -1,10 +1,7 @@
---
import { RoadmapsPage } from '../components/Roadmaps/RoadmapsPage';
import { RoadmapsPageHeader } from '../components/Roadmaps/RoadmapsPageHeader';
import GridItem from '../components/GridItem.astro';
import SimplePageHeader from '../components/SimplePageHeader.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getRoadmapsByTag } from '../lib/roadmap';
import ChangelogBanner from '../components/ChangelogBanner.astro';
---

View File

@@ -1,24 +1,137 @@
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
import { FetchError, httpGet } from '../lib/query-http';
import type { Node, Edge } from '@roadmapsh/editor';
import { DateTime } from 'luxon';
export interface OfficialRoadmapDocument {
export const allowedOfficialRoadmapType = ['skill', 'role'] as const;
export type AllowedOfficialRoadmapType =
(typeof allowedOfficialRoadmapType)[number];
export const allowedOfficialRoadmapQuestionType = ['faq', 'main'] as const;
export type AllowedOfficialRoadmapQuestionType =
(typeof allowedOfficialRoadmapQuestionType)[number];
export type OfficialRoadmapQuestion = {
_id: string;
type: AllowedOfficialRoadmapQuestionType;
title: string;
// Tiptap JSON Content
description: any;
};
export type OfficialRoadmapCourse = {
_id: string;
title: string;
description?: string;
description: string;
link: string;
instructor: {
name: string;
image: string;
title: string;
};
features: string[];
};
export interface OfficialRoadmapDocument {
_id?: string;
order: number;
title: {
card: string;
page: string;
};
description: string;
slug: string;
nodes: Node[];
edges: Edge[];
nodes: any[];
edges: any[];
draft: {
nodes: any[];
edges: any[];
};
seo: {
title: string;
description: string;
keywords: string[];
};
openGraph?: {
image?: string;
};
partner?: {
description: string;
linkText: string;
link: string;
};
type: AllowedOfficialRoadmapType;
dimensions?: {
height: number;
width: number;
};
questions?: OfficialRoadmapQuestion[];
relatedRoadmaps?: string[];
courses?: string[];
createdAt: Date;
updatedAt: Date;
}
export type OfficialRoadmapWithCourses = Omit<
OfficialRoadmapDocument,
'courses'
> & {
courses: OfficialRoadmapCourse[];
};
export function officialRoadmapOptions(slug: string) {
return queryOptions({
queryKey: ['official-roadmap', slug],
queryFn: () => {
return httpGet<OfficialRoadmapDocument>(`/v1-official-roadmap/${slug}`);
return httpGet<OfficialRoadmapWithCourses>(
`/v1-official-roadmap/${slug}`,
);
},
});
}
export async function officialRoadmapDetails(roadmapSlug: string) {
try {
const roadmap = await httpGet<OfficialRoadmapWithCourses>(
`/v1-official-roadmap/${roadmapSlug}`,
);
return roadmap;
} catch (error) {
if (FetchError.isFetchError(error) && error.status === 404) {
return null;
}
throw error;
}
}
export async function listOfficialRoadmaps() {
try {
const roadmaps = await httpGet<OfficialRoadmapDocument[]>(
`/v1-list-official-roadmaps`,
);
return roadmaps;
} catch (error) {
if (FetchError.isFetchError(error) && error.status === 404) {
return [];
}
throw error;
}
}
export function isNewRoadmap(createdAt: Date) {
return (
createdAt &&
DateTime.now().diff(DateTime.fromJSDate(new Date(createdAt)), 'days').days <
45
);
}