Compare commits

..

4 Commits

Author SHA1 Message Date
Arik Chakma
1af42a8060 fix: roadmap chat url 2025-08-12 18:38:45 +06:00
Arik Chakma
f93c60a1a3 refactor: floating and topic ai 2025-08-11 18:37:48 +06:00
Arik Chakma
c7f45bc91f Merge branch 'master' into feat/web-migration 2025-08-11 10:38:12 +06:00
Arik Chakma
266491207e chore: add short title 2025-08-04 20:45:22 +06:00
545 changed files with 3277 additions and 16414 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1755042938009
"lastUpdateCheck": 1753810743067
}
}

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

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

View File

@@ -1,67 +0,0 @@
name: Sync Content to Repo
on:
workflow_dispatch:
inputs:
roadmap_slug:
description: "The ID of the roadmap to sync"
required: true
default: "__default__"
jobs:
sync-content:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm@v9
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Setup Node.js Version 20 (LTS)
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Dependencies and Sync Content
run: |
echo "Installing Dependencies"
pnpm install
echo "Syncing Content to Repo"
npm run sync:content-to-repo -- --roadmap-slug=${{ inputs.roadmap_slug }} --secret=${{ secrets.GH_SYNC_SECRET }}
- name: Check for changes
id: verify-changed-files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Create PR
if: steps.verify-changed-files.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
delete-branch: false
branch: "chore/sync-content-to-repo-${{ inputs.roadmap_slug }}"
base: "master"
labels: |
dependencies
automated pr
reviewers: arikchakma
commit-message: "chore: sync content to repo"
title: "Sync Content to Repo - Automated"
body: |
## Sync Content to Repo
> [!IMPORTANT]
> This PR Syncs the Content to the Repo for the Roadmap: ${{ inputs.roadmap_slug }}
>
> Commit: ${{ github.sha }}
> Workflow Path: ${{ github.workflow_ref }}
**Please Review the Changes and Merge the PR if everything is fine.**

View File

@@ -1,66 +0,0 @@
name: Sync on Roadmap Changes
on:
push:
branches:
- master
paths:
- 'src/data/roadmaps/**'
jobs:
sync-on-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Fetch previous commit to compare changes
- name: Setup pnpm@v9
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Setup Node.js Version 20 (LTS)
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Get changed files
id: changed-files
run: |
echo "Getting changed files in /src/data/roadmaps/"
# Get changed files between HEAD and previous commit
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- src/data/roadmaps/)
if [ -z "$CHANGED_FILES" ]; then
echo "No changes found in roadmaps directory"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES"
# Convert to space-separated list for the script
CHANGED_FILES_LIST=$(echo "$CHANGED_FILES" | tr '\n' ' ')
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "changed_files=$CHANGED_FILES_LIST" >> $GITHUB_OUTPUT
- name: Install Dependencies
if: steps.changed-files.outputs.has_changes == 'true'
run: |
echo "Installing Dependencies"
pnpm install
- name: Run sync script with changed files
if: steps.changed-files.outputs.has_changes == 'true'
run: |
echo "Running sync script for changed roadmap files"
echo "Changed files: ${{ steps.changed-files.outputs.changed_files }}"
# Run your script with the changed file paths
npm run sync:repo-to-database -- --files="${{ steps.changed-files.outputs.changed_files }}" --secret=${{ secrets.TOPIC_CONTENT_SYNC_SECRET }}

View File

@@ -29,8 +29,6 @@
"compress:images": "tsx ./scripts/compress-images.ts",
"generate:roadmap-content-json": "tsx ./scripts/editor-roadmap-content-json.ts",
"migrate:editor-roadmaps": "tsx ./scripts/migrate-editor-roadmap.ts",
"sync:content-to-repo": "tsx ./scripts/sync-content-to-repo.ts",
"sync:repo-to-database": "tsx ./scripts/sync-repo-to-database.ts",
"test:e2e": "playwright test"
},
"dependencies": {

View File

@@ -725,7 +725,7 @@
},
"2vQPmVNk1QpMM-15RKG8b": {
"title": "Metrics",
"description": "In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every service in AWS.\n\nLearn more from the following resources:",
"description": "In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every serviece in AWS.\n\nLearn more from the following resources:",
"links": [
{
"title": "CloudWatch Metrics",

View File

@@ -153,11 +153,6 @@
"title": "Underlying Technologies - Medium",
"url": "https://medium.com/@furkan.turkal/how-does-docker-actually-work-the-hard-way-a-technical-deep-diving-c5b8ea2f0422",
"type": "article"
},
{
"title": "Containers - Namespaces, Cgroups and Overlay Filesystem",
"url": "https://www.youtube.com/watch?v=wJdDWc6zO4U",
"type": "video"
}
]
},

View File

@@ -107,7 +107,7 @@
"links": [
{
"title": "SQL Operators: 6 Different Types",
"url": "https://dataengineeracademy.com/blog/sql-operators-6-different-types-code-examples/",
"url": "https://www.dataquest.io/blog/sql-operators/",
"type": "article"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

View File

@@ -36,7 +36,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Backend Roadmap](https://roadmap.sh/backend) / [Backend Beginner Roadmap](https://roadmap.sh/backend?r=backend-beginner)
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
- [Git and GitHub](https://roadmap.sh/git-github) / [Git and GitHub Beginner](https://roadmap.sh/git-github?r=git-github-beginner)
- [Git and GitHub](https://roadmap.sh/git-github)
- [API Design Roadmap](https://roadmap.sh/api-design)
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
- [Data Structures and Algorithms Roadmap](https://roadmap.sh/datastructures-and-algorithms)
@@ -47,8 +47,6 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Linux Roadmap](https://roadmap.sh/linux)
- [Terraform Roadmap](https://roadmap.sh/terraform)
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)
- [Data Engineer Roadmap](https://roadmap.sh/data-engineer)
- [Machine Learning Roadmap](https://roadmap.sh/machine-learning)
- [MLOps Roadmap](https://roadmap.sh/mlops)
- [Product Manager Roadmap](https://roadmap.sh/product-manager)
- [Engineering Manager Roadmap](https://roadmap.sh/engineering-manager)

View File

@@ -1,110 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { slugify } from '../src/lib/slugger';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const roadmapSlug = args?.[0]?.replace('--roadmap-slug=', '');
const secret = args?.[1]?.replace('--secret=', '');
if (!secret) {
throw new Error('Secret is required');
}
if (!roadmapSlug || roadmapSlug === '__default__') {
throw new Error('Roadmap slug is required');
}
console.log(`🚀 Starting ${roadmapSlug}`);
export const allowedOfficialRoadmapTopicResourceType = [
'roadmap',
'official',
'opensource',
'article',
'course',
'podcast',
'video',
'book',
'feed',
] as const;
export type AllowedOfficialRoadmapTopicResourceType =
(typeof allowedOfficialRoadmapTopicResourceType)[number];
export type OfficialRoadmapTopicResource = {
_id?: string;
type: AllowedOfficialRoadmapTopicResourceType;
title: string;
url: string;
};
export interface OfficialRoadmapTopicContentDocument {
_id?: string;
roadmapSlug: string;
nodeId: string;
title: string;
description: string;
resources: OfficialRoadmapTopicResource[];
createdAt: Date;
updatedAt: Date;
}
export async function roadmapTopics(
roadmapId: string,
secret: string,
): Promise<OfficialRoadmapTopicContentDocument[]> {
const path = `https://roadmap.sh/api/v1-list-official-roadmap-topics/${roadmapId}?secret=${secret}`;
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to fetch roadmap topics: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`Failed to fetch roadmap topics: ${data.error}`);
}
return data;
}
// Directory containing the roadmaps
const ROADMAP_CONTENT_DIR = path.join(
__dirname,
'../src/data/roadmaps',
roadmapSlug,
);
const allTopics = await roadmapTopics(roadmapSlug, secret);
for (const topic of allTopics) {
const { title, nodeId } = topic;
const topicSlug = `${slugify(title)}@${nodeId}.md`;
const topicPath = path.join(ROADMAP_CONTENT_DIR, topicSlug);
const topicDir = path.dirname(topicPath);
const topicDirExists = await fs
.stat(topicDir)
.then(() => true)
.catch(() => false);
if (!topicDirExists) {
await fs.mkdir(topicDir, { recursive: true });
}
const topicContent = prepareTopicContent(topic);
await fs.writeFile(topicPath, topicContent);
console.log(`✅ Synced ${topicSlug}`);
}
function prepareTopicContent(topic: OfficialRoadmapTopicContentDocument) {
const { description, resources = [] } = topic;
const content = `${description}
Visit the following resources to learn more:
${resources.map((resource) => `- [@${resource.type}@${resource.title}](${resource.url})`).join('\n')}
`.trim();
return content;
}

View File

@@ -1,218 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { slugify } from '../src/lib/slugger';
import type { OfficialRoadmapDocument } from '../src/queries/official-roadmap';
import { parse } from 'node-html-parser';
import { markdownToHtml } from '../src/lib/markdown';
import { htmlToMarkdown } from '../src/lib/html';
import {
allowedOfficialRoadmapTopicResourceType,
type AllowedOfficialRoadmapTopicResourceType,
type OfficialRoadmapTopicContentDocument,
type OfficialRoadmapTopicResource,
} from './sync-content-to-repo';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const allFiles = args?.[0]?.replace('--files=', '');
const secret = args?.[1]?.replace('--secret=', '');
if (!secret) {
throw new Error('Secret is required');
}
let roadmapJsonCache: Map<string, OfficialRoadmapDocument> = new Map();
export async function fetchRoadmapJson(
roadmapId: string,
): Promise<OfficialRoadmapDocument> {
if (roadmapJsonCache.has(roadmapId)) {
return roadmapJsonCache.get(roadmapId)!;
}
const response = await fetch(
`https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`,
);
if (!response.ok) {
throw new Error(`Failed to fetch roadmap json: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`Failed to fetch roadmap json: ${data.error}`);
}
roadmapJsonCache.set(roadmapId, data);
return data;
}
export async function syncContentToDatabase(
topics: Omit<
OfficialRoadmapTopicContentDocument,
'createdAt' | 'updatedAt' | '_id'
>[],
) {
const response = await fetch(
`https://roadmap.sh/api/v1-sync-official-roadmap-topics`,
{
method: 'POST',
body: JSON.stringify({
topics,
secret,
}),
},
);
if (!response.ok) {
throw new Error(
`Failed to sync content to database: ${response.statusText}`,
);
}
return response.json();
}
const files = allFiles.split(' ');
console.log(`🚀 Starting ${files.length} files`);
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
try {
const topics: Omit<
OfficialRoadmapTopicContentDocument,
'createdAt' | 'updatedAt' | '_id'
>[] = [];
for (const file of files) {
const isContentFile = file.endsWith('.md') && file.includes('content/');
if (!isContentFile) {
console.log(`🚨 Skipping ${file} because it is not a content file`);
continue;
}
const pathParts = file.replace('src/data/roadmaps/', '').split('/');
const roadmapSlug = pathParts?.[0];
if (!roadmapSlug) {
console.error(`🚨 Roadmap slug is required: ${file}`);
continue;
}
const nodeSlug = pathParts?.[2]?.replace('.md', '');
if (!nodeSlug) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const nodeId = nodeSlug.split('@')?.[1];
if (!nodeId) {
console.error(`🚨 Node id is required: ${file}`);
continue;
}
const roadmap = await fetchRoadmapJson(roadmapSlug);
const node = roadmap.nodes.find((node) => node.id === nodeId);
if (!node) {
console.error(`🚨 Node not found: ${file}`);
continue;
}
const filePath = path.join(
ROADMAP_CONTENT_DIR,
roadmapSlug,
'content',
`${nodeSlug}.md`,
);
const content = await fs.readFile(filePath, 'utf8');
const html = markdownToHtml(content, false);
const rootHtml = parse(html);
let ulWithLinks: HTMLElement | undefined;
rootHtml.querySelectorAll('ul').forEach((ul) => {
const listWithJustLinks = Array.from(ul.querySelectorAll('li')).filter(
(li) => {
const link = li.querySelector('a');
return link && link.textContent?.trim() === li.textContent?.trim();
},
);
if (listWithJustLinks.length > 0) {
// @ts-expect-error - TODO: fix this
ulWithLinks = ul;
}
});
const listLinks: Omit<OfficialRoadmapTopicResource, '_id'>[] =
ulWithLinks !== undefined
? Array.from(ulWithLinks.querySelectorAll('li > a'))
.map((link) => {
const typePattern = /@([a-z.]+)@/;
let linkText = link.textContent || '';
const linkHref = link.getAttribute('href') || '';
let linkType = linkText.match(typePattern)?.[1] || 'article';
linkType = allowedOfficialRoadmapTopicResourceType.includes(
linkType as any,
)
? linkType
: 'article';
linkText = linkText.replace(typePattern, '');
return {
title: linkText,
url: linkHref,
type: linkType as AllowedOfficialRoadmapTopicResourceType,
};
})
.sort((a, b) => {
const order = [
'official',
'opensource',
'article',
'video',
'feed',
];
return order.indexOf(a.type) - order.indexOf(b.type);
})
: [];
const title = rootHtml.querySelector('h1');
ulWithLinks?.remove();
title?.remove();
const allParagraphs = rootHtml.querySelectorAll('p');
if (listLinks.length > 0 && allParagraphs.length > 0) {
// to remove the view more see more from the description
const lastParagraph = allParagraphs[allParagraphs.length - 1];
lastParagraph?.remove();
}
const htmlStringWithoutLinks = rootHtml.toString();
const description = htmlToMarkdown(htmlStringWithoutLinks);
const updatedDescription = `# ${title?.textContent}
${description}`.trim();
const label = node?.data?.label as string;
if (!label) {
console.error(`🚨 Label is required: ${file}`);
continue;
}
topics.push({
roadmapSlug,
nodeId,
title: label,
description: updatedDescription,
resources: listLinks,
});
}
await syncContentToDatabase(topics);
} catch (error) {
console.error(error);
process.exit(1);
}

View File

@@ -10,9 +10,9 @@ import { DashboardTabButton } from './DashboardTabButton';
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
import { TeamDashboard } from './TeamDashboard';
import type { QuestionGroupType } from '../../lib/question-group';
import type { GuideFileType } from '../../lib/guide';
import type { VideoFileType } from '../../lib/video';
import { cn } from '../../lib/classname';
import type { OfficialGuideDocument } from '../../queries/official-guide';
type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[];
@@ -20,7 +20,7 @@ type DashboardPageProps = {
builtInBestPractices?: BuiltInRoadmap[];
isTeamPage?: boolean;
questionGroups?: QuestionGroupType[];
guides?: OfficialGuideDocument[];
guides?: GuideFileType[];
videos?: VideoFileType[];
};
@@ -30,6 +30,7 @@ export function DashboardPage(props: DashboardPageProps) {
builtInBestPractices,
builtInSkillRoadmaps,
isTeamPage = false,
questionGroups,
guides,
videos,
} = props;
@@ -131,6 +132,7 @@ export function DashboardPage(props: DashboardPageProps) {
builtInRoleRoadmaps={builtInRoleRoadmaps}
builtInSkillRoadmaps={builtInSkillRoadmaps}
builtInBestPractices={builtInBestPractices}
questionGroups={questionGroups}
guides={guides}
videos={videos}
/>

View File

@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user.ts';
import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname.ts';
import type { GuideFileType } from '../../lib/guide';
import { httpGet } from '../../lib/http';
import type { QuestionGroupType } from '../../lib/question-group';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
@@ -20,7 +21,6 @@ import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import { useIsPaidUser } from '../../queries/billing.ts';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal.tsx';
import type { OfficialGuideDocument } from '../../queries/official-guide.ts';
const projectGroups = [
{
@@ -66,7 +66,7 @@ type PersonalDashboardProps = {
builtInSkillRoadmaps?: BuiltInRoadmap[];
builtInBestPractices?: BuiltInRoadmap[];
questionGroups?: QuestionGroupType[];
guides?: OfficialGuideDocument[];
guides?: GuideFileType[];
videos?: VideoFileType[];
};
@@ -193,6 +193,7 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
builtInRoleRoadmaps = [],
builtInBestPractices = [],
builtInSkillRoadmaps = [],
questionGroups = [],
guides = [],
videos = [],
} = props;
@@ -465,15 +466,40 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
</div>
</div>
</div>
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Questions
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{questionGroups.map((questionGroup) => {
return (
<HeroRoadmap
percentageDone={0}
key={questionGroup.id}
resourceId={questionGroup.id}
resourceType="roadmap"
resourceTitle={questionGroup.frontmatter.briefTitle}
url={`/questions/${questionGroup.id}`}
allowFavorite={false}
isNew={questionGroup.frontmatter.isNew}
/>
);
})}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-5 bg-gray-50 px-4 py-5 sm:gap-16 sm:px-0 sm:py-16">
<FeaturedGuideList
heading="Guides"
guides={guides.slice(0, 15)}
questions={guides
.filter((guide) => guide.roadmapId === 'questions')
.slice(0, 15)}
guides={guides}
questions={questionGroups
.filter((questionGroup) => questionGroup.frontmatter.authorId)
.slice(0, 7)}
/>
<FeaturedVideoList heading="Videos" videos={videos} />
</div>

View File

@@ -1,18 +1,22 @@
import type { OfficialGuideDocument } from '../../queries/official-guide';
import type { GuideFileType } from '../../lib/guide';
import type { QuestionGroupType } from '../../lib/question-group';
import { GuideListItem } from './GuideListItem';
export interface FeaturedGuidesProps {
heading: string;
guides: OfficialGuideDocument[];
questions: OfficialGuideDocument[];
guides: GuideFileType[];
questions: QuestionGroupType[];
}
export function FeaturedGuideList(props: FeaturedGuidesProps) {
const { heading, guides, questions = [] } = props;
const sortedGuides = [...guides, ...questions].sort((a, b) => {
const aDate = new Date(a.publishedAt ?? new Date());
const bDate = new Date(b.publishedAt ?? new Date());
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
...guides,
...questions,
].sort((a, b) => {
const aDate = new Date(a.frontmatter.date as string);
const bDate = new Date(b.frontmatter.date as string);
return bDate.getTime() - aDate.getTime();
});
@@ -23,7 +27,7 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
<div className="mt-3 sm:my-5">
{sortedGuides.map((guide) => (
<GuideListItem key={guide._id} guide={guide} />
<GuideListItem key={guide.id} guide={guide} />
))}
</div>
@@ -44,4 +48,4 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
</div>
</div>
);
}
}

View File

@@ -1,46 +1,52 @@
import { DateTime } from 'luxon';
import {
getOfficialGuideHref,
type OfficialGuideDocument,
} from '../../queries/official-guide';
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
import { type QuestionGroupType } from '../../lib/question-group';
import dayjs from 'dayjs';
export interface GuideListItemProps {
guide: OfficialGuideDocument;
guide: GuideFileType | QuestionGroupType;
}
function isQuestionGroupType(
guide: GuideFileType | QuestionGroupType,
): guide is QuestionGroupType {
return (guide as QuestionGroupType).questions !== undefined;
}
export function GuideListItem(props: GuideListItemProps) {
const { guide } = props;
const { title, slug, publishedAt, roadmapId } = guide;
const { frontmatter, id } = guide;
let guideType = 'Textual';
if (roadmapId === 'questions') {
guideType = 'Question';
let pageUrl = '';
let guideType = '';
if (isQuestionGroupType(guide)) {
pageUrl = `/questions/${id}`;
guideType = 'Questions';
} else {
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
guideType = (frontmatter as GuideFrontmatter).type;
}
const publishedAtDate = publishedAt
? DateTime.fromJSDate(new Date(publishedAt))
: null;
const isNew =
publishedAtDate && DateTime.now().diff(publishedAtDate, 'days').days < 15;
const publishedAtMonth = publishedAtDate
? publishedAtDate.toFormat('MMMM')
: '';
// Check if article is within the last 15 days
const isNew = frontmatter.date
? dayjs().diff(dayjs(frontmatter.date), 'day') < 15
: false;
return (
<a
className="text-md group flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={getOfficialGuideHref(slug, roadmapId)}
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={pageUrl}
>
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
{title}
{frontmatter.title}
{isNew && (
<span className="ml-2.5 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase">
New
<span className="hidden sm:inline">
&nbsp;&middot;&nbsp;
{publishedAtMonth}
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
</span>
</span>
)}

View File

@@ -0,0 +1,72 @@
---
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
import MarkdownFile from '../MarkdownFile.astro';
import { TableOfContent } from '../TableOfContent/TableOfContent';
import { RelatedGuides } from './RelatedGuides';
interface Props {
guide: GuideFileType;
}
const { guide } = Astro.props;
const allHeadings = guide.getHeadings();
const tableOfContent = getGuideTableOfContent(allHeadings);
const showTableOfContent = tableOfContent.length > 0;
const showRelatedGuides =
guide?.relatedGuides && Object.keys(guide?.relatedGuides).length > 0;
const { frontmatter: guideFrontmatter, author } = guide;
---
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
{
(showTableOfContent || showRelatedGuides) && (
<div class='sticky top-0 lg:relative bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
<RelatedGuides
relatedTitle={guideFrontmatter?.relatedTitle}
relatedGuides={guide?.relatedGuides || {}}
client:load
/>
<TableOfContent toc={tableOfContent} client:load />
</div>
)
}
<div
class:list={[
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
{
'lg:border-r': showTableOfContent,
},
]}
>
<MarkdownFile>
<h1 class='mb-3 text-balance text-4xl font-bold'>
{guideFrontmatter.title}
</h1>
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a
href={`/authors/${author.id}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
</a>
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`}
target='_blank'
>
Improve this Guide
</a>
</p>
<guide.Content />
</MarkdownFile>
</div>
</article>

View File

@@ -1,60 +0,0 @@
import { cn } from '../../lib/classname';
import { guideRenderer } from '../../lib/guide-renderer';
import type { OfficialGuideResponse } from '../../queries/official-guide';
import { TableOfContent } from '../TableOfContent/TableOfContent';
import { RelatedGuides } from './RelatedGuides';
type GuideContentProps = {
guide: OfficialGuideResponse;
};
export function GuideContent(props: GuideContentProps) {
const { guide } = props;
const content = guideRenderer.render(guide.content);
const tableOfContents = guideRenderer.tableOfContents(guide.content);
const showTableOfContent = tableOfContents.length > 0;
const hasRelatedGuides =
guide.relatedGuides && guide.relatedGuides.length > 0;
return (
<article className="lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]">
{(showTableOfContent || hasRelatedGuides) && (
<div className="sticky top-0 bg-linear-to-r from-gray-50 py-0 lg:relative lg:col-start-3 lg:col-end-4 lg:row-start-1">
{hasRelatedGuides && (
<RelatedGuides relatedGuides={guide?.relatedGuides || []} />
)}
{showTableOfContent && <TableOfContent toc={tableOfContents} />}
</div>
)}
<div
className={cn(
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
showTableOfContent && 'lg:border-r',
)}
>
<div className="prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h4:text-2xl prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 sm:prose-h2:scroll-mt-10 sm:prose-h3:scroll-mt-10 prose-li:[&>p]:m-0 container">
<h1 className="mb-3 text-4xl font-bold text-balance">
{guide.title}
</h1>
<p className="my-0 mb-6 flex items-center justify-start text-sm text-gray-400">
<a
href={`/authors/${guide.author?.slug}`}
className="inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline"
>
<img
alt={guide.author?.name}
src={guide.author?.avatar}
className="mr-2 mb-0 inline h-5 w-5 rounded-full"
/>
{guide.author?.name}
</a>
</p>
{content}
</div>
</div>
</article>
);
}

View File

@@ -1,11 +1,10 @@
import { ChevronDown } from 'lucide-react';
import { useState } from 'react';
import type { OfficialGuideDocument } from '../../queries/official-guide';
import { cn } from '../../lib/classname';
type RelatedGuidesProps = {
relatedTitle?: string;
relatedGuides: Pick<OfficialGuideDocument, 'title' | 'slug' | 'roadmapId'>[];
relatedGuides: Record<string, string>;
};
export function RelatedGuides(props: RelatedGuidesProps) {
@@ -13,7 +12,14 @@ export function RelatedGuides(props: RelatedGuidesProps) {
const [isOpen, setIsOpen] = useState(false);
if (relatedGuides.length === 0) {
const relatedGuidesArray = Object.entries(relatedGuides).map(
([title, url]) => ({
title,
url,
}),
);
if (relatedGuidesArray.length === 0) {
return null;
}
@@ -41,32 +47,23 @@ export function RelatedGuides(props: RelatedGuidesProps) {
isOpen && 'block',
)}
>
{relatedGuides.map((relatedGuide) => {
const { roadmapId, slug, title } = relatedGuide;
const href = roadmapId ? `/${roadmapId}/${slug}` : `/guides/${slug}`;
{relatedGuidesArray.map((relatedGuide) => (
<li key={relatedGuide.url}>
<a
href={relatedGuide.url}
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1"
onClick={() => {
if (!isOpen) {
return;
}
const className = cn(
'text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1',
);
return (
<li key={slug}>
<a
href={href}
className={className}
onClick={() => {
if (!isOpen) {
return;
}
setIsOpen(false);
}}
>
{title}
</a>
</li>
);
})}
setIsOpen(false);
}}
>
{relatedGuide.title}
</a>
</li>
))}
</ol>
</div>
);

View File

@@ -1,6 +1,8 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { guideRenderer, type QuestionType } from '../../lib/guide-renderer';
import { cn } from '../../lib/classname';
import type { QuestionType } from '../../lib/question-group';
import { markdownToHtml } from '../../lib/markdown';
import Prism from 'prismjs';
import './PrismAtom.css';
type QuestionCardProps = {
question: QuestionType;
@@ -18,6 +20,8 @@ export function QuestionCard(props: QuestionCardProps) {
// width if the answer is visible and the question height is less than
// the answer height
if (isAnswerVisible) {
Prism.highlightAll();
const answerHeight = answerRef.current?.clientHeight || 0;
const questionHeight = questionRef.current?.clientHeight || 0;
@@ -65,7 +69,7 @@ export function QuestionCard(props: QuestionCardProps) {
</div>
<div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-3 sm:py-8">
<p className="px-4 text-xl leading-snug! font-semibold text-black sm:text-3xl">
<p className="px-4 text-xl font-semibold leading-snug! text-black sm:text-3xl">
{question.question}
</p>
</div>
@@ -84,15 +88,27 @@ export function QuestionCard(props: QuestionCardProps) {
<div
ref={answerRef}
className={cn(
'absolute right-0 left-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl',
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full',
)}
className={`absolute left-0 right-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl ${
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full'
}`}
>
<div className="qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 prose-li:[&>p]:mb-0 sm:prose-p:mb-4 mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center px-4 py-0 text-left text-sm sm:px-5 sm:text-lg">
{guideRenderer.render(question.answer)}
</div>
{!question.isLongAnswer && (
<div
className={`mx-auto flex max-w-[600px] grow flex-col items-center justify-center py-0 px-5 text-center text-base [&>p]:leading-relaxed sm:text-xl`}
dangerouslySetInnerHTML={{
__html: markdownToHtml(question.answer, false),
}}
/>
)}
{question.isLongAnswer && (
<div
className={`qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
dangerouslySetInnerHTML={{
__html: markdownToHtml(question.answer, false),
}}
/>
)}
<div className="mt-7 text-center">
<button
onClick={() => {

View File

@@ -0,0 +1,182 @@
---
import { getGuideTableOfContent, type HeadingGroupType } from '../../lib/guide';
import { markdownToHtml } from '../../lib/markdown';
import {
type QuestionGroupType,
type QuestionType,
} from '../../lib/question-group';
import { slugify } from '../../lib/slugger';
import { RelatedGuides } from '../Guide/RelatedGuides';
import MarkdownFile from '../MarkdownFile.astro';
import { TableOfContent } from '../TableOfContent/TableOfContent';
import { QuestionsList } from './QuestionsList';
interface Props {
questionGroup: QuestionGroupType;
}
const { questionGroup } = Astro.props;
const { frontmatter: guideFrontmatter, author } = questionGroup;
// Group questions by topics
const questionsGroupedByTopics = questionGroup.questions.reduce(
(acc, question) => {
question.topics?.forEach((topic) => {
acc[topic] = [...(acc[topic] || []), question];
});
return acc;
},
{} as Record<string, QuestionType[]>,
);
// Get all unique topics in the order they appear in the questions array
const topicsInOrder: string[] = [];
questionGroup.questions.forEach((question) => {
question.topics?.forEach((topic) => {
if (!topicsInOrder.includes(topic)) {
topicsInOrder.push(topic);
}
});
});
const allHeadings = questionGroup.getHeadings();
let tableOfContent: HeadingGroupType[] = [
...getGuideTableOfContent(allHeadings),
{
depth: 2,
children: [],
slug: 'test-with-flashcards',
text: 'Test yourself with Flashcards',
},
{
depth: 2,
children: topicsInOrder.map((topic) => {
let topicText = topic;
let topicSlug = slugify(topic);
if (topic.toLowerCase() === 'beginners') {
topicText = 'Beginner Level';
topicSlug = 'beginner-level';
} else if (topic.toLowerCase() === 'intermediate') {
topicText = 'Intermediate Level';
topicSlug = 'intermediate-level';
} else if (topic.toLowerCase() === 'advanced') {
topicText = 'Advanced Level';
topicSlug = 'advanced-level';
}
return {
depth: 2,
children: [],
slug: topicSlug,
text: topicText,
};
}),
slug: 'questions-list',
text: 'Questions List',
},
];
const showTableOfContent = tableOfContent.length > 0;
---
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
{
showTableOfContent && (
<div class='bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
<RelatedGuides
relatedTitle={guideFrontmatter?.relatedTitle}
relatedGuides={questionGroup?.relatedGuides || {}}
client:load
/>
<TableOfContent toc={tableOfContent} client:load />
</div>
)
}
<div
class:list={[
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
{
'lg:border-r': showTableOfContent,
},
]}
>
<MarkdownFile>
<h1 class='mb-3 text-4xl font-bold text-balance'>
{guideFrontmatter.title}
</h1>
{
author && (
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a
href={`/authors/${author?.id}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
class='mr-2 mb-0 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
</a>
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.id}`}
target='_blank'
>
Improve this Guide
</a>
</p>
)
}
<questionGroup.Content />
<h2 id='test-with-flashcards'>Test yourself with Flashcards</h2>
<p>
You can either use these flashcards or jump to the questions list
section below to see them in a list format.
</p>
<div class='mx-0 sm:-mb-32'>
<QuestionsList
groupId={questionGroup.id}
questions={questionGroup.questions}
client:load
/>
</div>
<h2 id='questions-list'>Questions List</h2>
<p>
If you prefer to see the questions in a list format, you can find them
below.
</p>
{
topicsInOrder.map((questionLevel) => (
<div class='mb-5'>
<h3 id={slugify(questionLevel)} class='mb-0 capitalize'>
{questionLevel.toLowerCase() === 'beginners' ? 'Beginner Level' :
questionLevel.toLowerCase() === 'intermediate' ? 'Intermediate Level' :
questionLevel.toLowerCase() === 'advanced' ? 'Advanced Level' :
questionLevel}
</h3>
{questionsGroupedByTopics[questionLevel].map((q) => (
<div class='mb-5'>
<h4>{q.question}</h4>
<div set:html={markdownToHtml(q.answer, false)} />
</div>
))}
</div>
))
}
{
questionGroup.ending && (
<div class='mb-5'>
<div set:html={markdownToHtml(questionGroup.ending, false)} />
</div>
)
}
</MarkdownFile>
</div>
</article>

View File

@@ -1,10 +1,10 @@
import { useRef, useState } from 'react';
import type { QuestionType } from '../../lib/guide-renderer';
import { QuestionsProgress } from './QuestionsProgress';
import { cn } from '../../lib/classname';
import { QuestionFinished } from './QuestionFinished';
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react';
import { QuestionCard } from './QuestionCard';
import { CheckCircleIcon, SkipForwardIcon, SparklesIcon } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import type { QuestionType } from '../../lib/question-group';
import { QuestionFinished } from './QuestionFinished';
import { Confetti } from '../Confetti';
type UserQuestionProgress = {
@@ -16,12 +16,12 @@ type UserQuestionProgress = {
export type QuestionProgressType = keyof UserQuestionProgress;
type QuestionsListProps = {
groupId: string;
questions: QuestionType[];
className?: string;
};
export function QuestionsList(props: QuestionsListProps) {
const { questions, className } = props;
const { questions } = props;
const [showConfetti, setShowConfetti] = useState(false);
const [currQuestionIndex, setCurrQuestionIndex] = useState(0);
@@ -73,7 +73,7 @@ export function QuestionsList(props: QuestionsListProps) {
const hasFinished = hasProgress && currQuestionIndex === -1;
return (
<div className={cn('mb-0 gap-3 text-center sm:mb-40', className)}>
<div className="mb-0 gap-3 text-center sm:mb-40">
<QuestionsProgress
knowCount={knowCount}
didNotKnowCount={dontKnowCount}
@@ -139,10 +139,9 @@ export function QuestionsList(props: QuestionsListProps) {
</div>
<div
className={cn(
'flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3',
hasFinished ? 'opacity-0' : 'opacity-100',
)}
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
hasFinished ? 'opacity-0' : 'opacity-100'
}`}
>
<button
disabled={!currQuestion}
@@ -153,7 +152,7 @@ export function QuestionsList(props: QuestionsListProps) {
}}
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<CheckCircleIcon className="mr-1 h-4 text-current" />
<CheckCircle className="mr-1 h-4 text-current" />
Already Know that
</button>
<button
@@ -163,7 +162,7 @@ export function QuestionsList(props: QuestionsListProps) {
disabled={!currQuestion}
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<SparklesIcon className="mr-1 h-4 text-current" />
<Sparkles className="mr-1 h-4 text-current" />
Didn't Know that
</button>
<button
@@ -174,7 +173,7 @@ export function QuestionsList(props: QuestionsListProps) {
data-next-question="skip"
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<SkipForwardIcon className="mr-1 h-4" />
<SkipForward className="mr-1 h-4" />
Skip Question
</button>
</div>

View File

@@ -378,11 +378,6 @@ const groups: GroupType[] = [
{
group: 'Machine Learning',
roadmaps: [
{
title: 'Machine Learning',
link: '/machine-learning',
type: 'role',
},
{
title: 'AI and Data Scientist',
link: '/ai-data-scientist',
@@ -408,11 +403,6 @@ const groups: GroupType[] = [
link: '/data-analyst',
type: 'role',
},
{
title: 'Data Engineer',
link: '/data-engineer',
type: 'role',
},
{
title: 'MLOps',
link: '/mlops',
@@ -607,7 +597,7 @@ export function RoadmapsPage() {
{isFilterOpen && <X size={13} className="mr-1" />}
Categories
</button>
<div className="relative container flex flex-col gap-4 sm:flex-row">
<div className="container relative flex flex-col gap-4 sm:flex-row">
<div
className={cn(
'hidden w-full flex-col from-gray-100 sm:w-[180px] sm:border-r sm:bg-linear-to-l sm:pt-6',
@@ -645,10 +635,10 @@ export function RoadmapsPage() {
</div>
</div>
</div>
<div className="flex grow flex-col gap-6 pt-2 pb-20 sm:pt-8">
<div className="flex grow flex-col gap-6 pb-20 pt-2 sm:pt-8">
{visibleGroups.map((group) => (
<div key={`${group.group}-${group.roadmaps.length}`}>
<h2 className="mb-2 text-xs tracking-wide text-gray-400 uppercase">
<h2 className="mb-2 text-xs uppercase tracking-wide text-gray-400">
{group.group}
</h2>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import type { HeadingGroupType } from '../../lib/guide-renderer';
import type { HeadingGroupType } from '../../lib/guide';
import { ChevronDown } from 'lucide-react';
import { cn } from '../../lib/classname';
@@ -23,7 +23,7 @@ export function TableOfContent(props: TableOfContentProps) {
return (
<div
className={cn(
'relative min-w-[250px] px-5 pt-0 max-lg:max-w-full max-lg:min-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
'relative min-w-[250px] px-5 pt-0 max-lg:min-w-full max-lg:max-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
{
'top-0 lg:sticky!': totalRows <= 20,
},
@@ -68,7 +68,7 @@ export function TableOfContent(props: TableOfContentProps) {
</a>
{heading.children.length > 0 && (
<ol className="my-0 mt-1 ml-4 space-y-0 max-lg:mt-0 max-lg:ml-0 max-lg:list-none">
<ol className="my-0 ml-4 mt-1 space-y-0 max-lg:ml-0 max-lg:mt-0 max-lg:list-none">
{heading.children.map((children) => {
return (
<li key={children.slug}>

View File

@@ -17,7 +17,7 @@ const links = [
isHighlighted: true,
},
{
link: '/ai/roadmap',
link: '/ai?format=roadmap',
label: 'AI Roadmaps',
description: 'Generate roadmaps with AI',
Icon: Sparkles,

View File

@@ -197,7 +197,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
return;
}
}}
href={`/ai/course/search?term=${subject}&difficulty=beginner&src=topic`}
href={`/ai/course?term=${subject}&difficulty=beginner&src=topic`}
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black"
>
{subject}
@@ -208,20 +208,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
{roadmapTreeMapping?.subjects?.length === 0 && (
<a
target="_blank"
onClick={(e) => {
if (!isLoggedIn()) {
e.preventDefault();
onLogin();
return;
}
if (isLimitExceeded) {
e.preventDefault();
onUpgrade();
return;
}
}}
href={`/ai/course/search?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
href={`/ai/course?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black [&>svg:last-child]:hidden"
>
{nodeTextParts.slice(-2).map((text, index) => {

View File

@@ -14,5 +14,4 @@ function App() {
<LazyComponent />
</Suspense>
);
}
```
}

View File

@@ -1,4 +1,4 @@
Raw string literals in Go are enclosed in backticks (\`) and preserve all formatting exactly as written. This is different from interpreted string literals, which process escape sequences like \n. This distinction is particularly useful when you need to process data exactly as it is written.
Raw string literals in Go are enclosed in backticks (`` ` ``) and preserve all formatting exactly as written. This is different from interpreted string literals, which process escape sequences like \n. This distinction is particularly useful when you need to process data exactly as it is written.
Consider a scenario where you need to embed an HTML template directly into your Go code. With raw string literals, you can include the HTML exactly as written without worrying about escaping characters or preserving the formatting. For example:
@@ -11,7 +11,7 @@ htmlTemplate := `<!DOCTYPE html>
<body>
<h1>Hello, World!</h1>
</body>
</html>` // backtick here ends the raw string literal
</html>`
```
In this case, the raw string literal enclosed in backticks preserves newlines, tabs, and any other whitespace exactly as you write them.

View File

@@ -19,5 +19,4 @@ const styleP = document.getElementById("styleP");
styleP.style.color = "red";
styleP.style.border = "3px solid black";
console.log(styleP.style);
```
console.log(styleP.style);

View File

@@ -2,14 +2,21 @@ Function scope refers to the scope of variables defined within a function. You c
```javascript
function myStudyPlan() {
var studyPlanOne = "Top JavaScript interview questions for web developers";
let studyPlanTwo = "Top JavaScript interview questions for web developers";
const studyPlanThree = "Top JavaScript interview questions for web developers";
console.log(studyPlanOne);
console.log(studyPlanTwo);
console.log(studyPlanThree);
var studyPlanOne = "Top JavaScript interview questions for web developers";
let studyPlanTwo = "Top JavaScript interview questions for web developers";
const studyPlanThree = "Top JavaScript interview questions for web developers";
console.log(studyPlanOne);
console.log(studyPlanTwo);
console.log(studyPlanThree);
}
myStudyPlan(); // Calls the function
```
myStudyPlan(); // Calls the function

View File

@@ -13,5 +13,4 @@ Immediately invoked function expressions, or IIFEs, run as soon as they're creat
console.log(
"roadmap.sh helps prepare for JavaScript job interview questions"
);
})();
```
})();

View File

@@ -24,5 +24,4 @@ export function studyJs(course) {
import { studyJs } from './app.js';
console.log(studyJs("roadmap.sh")); // Read the JavaScript guide on, roadmap.sh
```
console.log(studyJs("roadmap.sh")); // Read the JavaScript guide on, roadmap.sh

View File

@@ -30,5 +30,4 @@ console.log(courseNumber ); // code won't run
}
strictExample(); // ReferenceError
```
strictExample(); // ReferenceError

View File

@@ -1,32 +0,0 @@
You use **WHERE** for filtering rows before applying any grouping or aggregation.
The code snippet below illustrates the use of **WHERE**. It filters the `Users` table for rows where the `Age` is greater than 18.
```sql
SELECT * FROM Users
WHERE Age > 18;
```
The result of the query is similar to the table below.
| userId | firstName | lastName | age |
| ------ | --------- | -------- | --- |
| 1 | John | Doe | 30 |
| 2 | Jane | Don | 31 |
| 3 | Will | Liam | 25 |
| 4 | Wade | Great | 32 |
| 5 | Peter | Smith | 27 |
On the other hand, you use **HAVING** to filter groups after performing grouping and aggregation. You apply it to the result of aggregate functions, and it is mostly used with the **GROUP BY** clause.
```sql
SELECT FirstName, Age FROM Users
GROUP BY FirstName, Age
HAVING Age > 30;
```
The code above selects the `FirstName` and `Age` columns, then groups by the `FirstName` and `Age`, and finally gets entries with age greater than 30. The result of the query looks like this:
| firstName | age |
| --------- | --- |
| Wade | 32 |
| Jane | 31 |

View File

@@ -5,5 +5,5 @@ Code-generation agents take a plain language request, understand the goal, and t
Visit the following resources to learn more:
- [@article@Multi-Agent-based Code Generation](https://arxiv.org/abs/2312.13010)
- [@article@From Prompt to Production: GitHub Blog](https://github.blog/ai-and-ml/github-copilot/from-prompt-to-production-building-a-landing-page-with-copilot-agent-mode/)
- [@official@GitHub Copilot](https://github.com/features/copilot)
- [@article@From Prompt to Production: Github Blog](https://github.blog/ai-and-ml/github-copilot/from-prompt-to-production-building-a-landing-page-with-copilot-agent-mode/)
- [@official@Github Copilot](https://github.com/features/copilot)

View File

@@ -1,7 +1,7 @@
---
jsonUrl: '/jsons/roadmaps/ai-data-scientist.json'
pdfUrl: '/pdfs/roadmaps/ai-data-scientist.pdf'
order: 4.5
order: 5
renderer: 'editor'
briefTitle: 'AI and Data Scientist'
briefDescription: 'Step by step guide to becoming an AI and Data Scientist in 2025'

View File

@@ -8,7 +8,7 @@ briefDescription: 'Step by step guide to becoming an AI Engineer in 2025'
title: 'AI Engineer'
description: 'Step by step guide to becoming an AI Engineer in 2025'
hasTopics: true
isNew: false
isNew: true
dimensions:
width: 968
height: 3200

View File

@@ -1,7 +1,7 @@
---
pdfUrl: '/pdfs/roadmaps/android.pdf'
renderer: 'editor'
order: 4.7
order: 5
briefTitle: 'Android'
briefDescription: 'Step by step guide to becoming an Android Developer in 2025'
title: 'Android Developer'

View File

@@ -4,6 +4,6 @@
Visit the following resources to learn more:
- [@roadmap@Git and GitHub Roadmap](https://roadmap.sh/git-github)
- [@roadmap@Git and Github Roadmap](https://roadmap.sh/git-github)
- [@official@Git](https://git-scm.com/)
- [@official@Git Documentation](https://git-scm.com/docs)

View File

@@ -4,6 +4,6 @@
Visit the following resources to learn more:
- [@roadmap@Git and GitHub Roadmap](https://roadmap.sh/git-github)
- [@roadmap@Git and Github Roadmap](https://roadmap.sh/git-github)
- [@official@GitHub](https://github.com/)
- [@official@GitHub Documentation](https://docs.github.com/)
- [@official@Github Documentation](https://docs.github.com/)

View File

@@ -1,8 +1,8 @@
# GitLab
`GitLab` is a web-based DevOps lifecycle tool which provides a Git-repository manager, along with continuous integration and deployment pipeline features, using an open-source license, developed by GitLab Inc. Users can manage and create their software projects and repositories, and collaborate on these projects with other members. `GitLab` also allows users to view analytics and open issues of their project. It stands next to other version control tools like `GitHub` and `Bitbucket`, but comes with its own set of additional features and nuances. For Android development, `GitLab` can be particularly useful owing to its continuous integration and deployment system which can automate large parts of the app testing and deployment.
`Gitlab` is a web-based DevOps lifecycle tool which provides a Git-repository manager, along with continuous integration and deployment pipeline features, using an open-source license, developed by GitLab Inc. Users can manage and create their software projects and repositories, and collaborate on these projects with other members. `Gitlab` also allows users to view analytics and open issues of their project. It stands next to other version control tools like `GitHub` and `Bitbucket`, but comes with its own set of additional features and nuances. For Android development, `Gitlab` can be particularly useful owing to its continuous integration and deployment system which can automate large parts of the app testing and deployment.
Visit the following resources to learn more:
- [@official@GitLab](https://about.gitlab.com/)
- [@official@GitLab Documentation](https://docs.gitlab.com/)
- [@official@Gitlab](https://about.gitlab.com/)
- [@official@Gitlab Documentation](https://docs.gitlab.com/)

View File

@@ -1,4 +1,4 @@
# GitHub Actions
# Github Actions
GitHub Actions is a powerful and flexible automation platform that enables developers to create custom workflows for their software development lifecycle (SDLC) directly in their GitHub repository. It allows developers to automate various tasks, such as building, testing, and deploying code, directly from their GitHub repository.

View File

@@ -1,6 +1,6 @@
# Metrics
In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every service in AWS.
In Amazon CloudWatch, **metrics** are fundamental concepts that you work with. A metric is the fundamental concept in CloudWatch and represents a time-ordered set of data points that are published to CloudWatch. Think of a metric as a variable to monitor, and the data points as representing the values of that variable over time. Metrics are uniquely defined by a name, a namespace, and zero or more dimensions up to 30 dimensions per metric. Every data point must have a timestamp. You can retrieve statistics about those data points as an ordered set of time-series data. CloudWatch provides metrics for every serviece in AWS.
Learn more from the following resources:

View File

@@ -6,5 +6,5 @@ Visit the following resources to learn more:
- [@official@GitLab](https://gitlab.com/)
- [@official@GitLab Documentation](https://docs.gitlab.com/)
- [@video@What is GitLab and Why Use It?](https://www.youtube.com/watch?v=bnF7f1zGpo4)
- [@video@What is Gitlab and Why Use It?](https://www.youtube.com/watch?v=bnF7f1zGpo4)
- [@feed@Explore top posts about GitLab](https://app.daily.dev/tags/gitlab?ref=roadmapsh)

View File

@@ -6,5 +6,5 @@ Visit the following resources to learn more:
- [@article@Solr Website](https://solr.apache.org/)
- [@article@Solr Documentation](https://solr.apache.org/resources.html#documentation)
- [@opensource@Solr on GitHub](https://github.com/apache/solr)
- [@opensource@Solr on Github](https://github.com/apache/solr)
- [@video@Apache Solr vs Elasticsearch Differences](https://www.youtube.com/watch?v=MMWBdSdbu5k)

View File

@@ -4,7 +4,7 @@ GitHub is a provider of Internet hosting for software development and version co
Visit the following resources to learn more:
- [@roadmap@Visit Dedicated GitHub Roadmap](https://roadmap.sh/git-github)
- [@roadmap@Visit Dedicated Github Roadmap](https://roadmap.sh/git-github)
- [@official@GitHub](https://github.com)
- [@official@GitHub Documentation](https://docs.github.com/en/get-started/quickstart)
- [@video@What is GitHub?](https://www.youtube.com/watch?v=w3jLJU7DT5E)

View File

@@ -6,7 +6,7 @@ briefTitle: 'Cloudflare'
briefDescription: 'Learn to deploy your applications on Cloudflare'
title: 'Cloudflare'
description: 'Learn to deploy your applications on Cloudflare'
isNew: false
isNew: true
hasTopics: true
renderer: editor
dimensions:

View File

@@ -5,6 +5,6 @@ CI/CD (Continuous Integration/Continuous Deployment) pipelines automate the proc
Visit the following resources to learn more:
- [@official@Automate your workflow - Github Actions](https://github.com/features/actions)
- [@official@CI/CD Pipelines - GitLab](https://docs.gitlab.com/ee/ci/pipelines/)
- [@official@CI/CD Pipelines - Gitlab](https://docs.gitlab.com/ee/ci/pipelines/)
- [@official@Continuous Integration and Delivery - CircleCI](https://circleci.com/)
- [@official@Simple, Flexible, Trustworthy CI/CD Tools - Travis CI](https://www.travis-ci.com/)

View File

@@ -5,4 +5,4 @@
Learn more from the following resources:
- [@official@memdump](https://www.kali.org/tools/memdump/)
- [@opensource@memdump - GitHub](https://github.com/tchebb/memdump)
- [@opensource@memdump - Github](https://github.com/tchebb/memdump)

Some files were not shown because too many files have changed in this diff Show More