mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-17 04:11:42 +08:00
Compare commits
4 Commits
feat/sync-
...
feat/web-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af42a8060 | ||
|
|
f93c60a1a3 | ||
|
|
c7f45bc91f | ||
|
|
266491207e |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1755042938009
|
||||
"lastUpdateCheck": 1753810743067
|
||||
}
|
||||
}
|
||||
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@@ -1 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
67
.github/workflows/sync-content-to-repo.yml
vendored
67
.github/workflows/sync-content-to-repo.yml
vendored
@@ -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.**
|
||||
66
.github/workflows/sync-repo-to-database.yml
vendored
66
.github/workflows/sync-repo-to-database.yml
vendored
@@ -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 }}
|
||||
@@ -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": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 |
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
·
|
||||
{publishedAtMonth}
|
||||
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
72
src/components/Guide/GuideContent.astro
Normal file
72
src/components/Guide/GuideContent.astro
Normal 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'>·</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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
182
src/components/Questions/QuestionGuide.astro
Normal file
182
src/components/Questions/QuestionGuide.astro
Normal 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'>·</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -14,5 +14,4 @@ function App() {
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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"
|
||||
);
|
||||
})();
|
||||
```
|
||||
})();
|
||||
@@ -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
|
||||
@@ -30,5 +30,4 @@ console.log(courseNumber ); // code won't run
|
||||
|
||||
}
|
||||
|
||||
strictExample(); // ReferenceError
|
||||
```
|
||||
strictExample(); // ReferenceError
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
# A/B Testing
|
||||
@@ -1 +0,0 @@
|
||||
# Amazon EC2 ( Compute)
|
||||
@@ -1 +0,0 @@
|
||||
# Amazon RDS (Database)
|
||||
@@ -1 +0,0 @@
|
||||
# Amazon RDS (Database)
|
||||
@@ -1 +0,0 @@
|
||||
# Amazon Redshift
|
||||
@@ -1 +0,0 @@
|
||||
# Apache Airflow
|
||||
@@ -1 +0,0 @@
|
||||
# Apache Hadoop YARN
|
||||
@@ -1 +0,0 @@
|
||||
# Apache Kafka
|
||||
@@ -1 +0,0 @@
|
||||
# Apache Spark
|
||||
@@ -1 +0,0 @@
|
||||
# APIs
|
||||
@@ -1 +0,0 @@
|
||||
# ArgoCD
|
||||
@@ -1 +0,0 @@
|
||||
# Async vs Sync Communication
|
||||
@@ -1 +0,0 @@
|
||||
# Aurora DB
|
||||
@@ -1 +0,0 @@
|
||||
# Authentication vs Authorization
|
||||
@@ -1 +0,0 @@
|
||||
# AWS CDK
|
||||
@@ -1 +0,0 @@
|
||||
# AWS EKS
|
||||
@@ -1 +0,0 @@
|
||||
# AWS SNS
|
||||
@@ -1 +0,0 @@
|
||||
# AWS SQS
|
||||
@@ -1 +0,0 @@
|
||||
# Azure Blob Storage
|
||||
@@ -1 +0,0 @@
|
||||
# Azure SQL Database
|
||||
@@ -1 +0,0 @@
|
||||
# Azure Virtual Machines
|
||||
@@ -1 +0,0 @@
|
||||
# Batch
|
||||
@@ -1 +0,0 @@
|
||||
# Best Practices
|
||||
@@ -1 +0,0 @@
|
||||
# Big Data Tools
|
||||
@@ -1 +0,0 @@
|
||||
# BigTable
|
||||
@@ -1 +0,0 @@
|
||||
# Business Intelligence
|
||||
@@ -1 +0,0 @@
|
||||
# CAP Theorem
|
||||
@@ -1 +0,0 @@
|
||||
# Cassandra
|
||||
@@ -1 +0,0 @@
|
||||
# Census
|
||||
@@ -1 +0,0 @@
|
||||
# Choosing the Right Technologies
|
||||
@@ -1 +0,0 @@
|
||||
# CI/CD
|
||||
@@ -1 +0,0 @@
|
||||
# Circle CI
|
||||
@@ -1 +0,0 @@
|
||||
# Cloud Architectures
|
||||
@@ -1 +0,0 @@
|
||||
# Cloud Computing
|
||||
@@ -1 +0,0 @@
|
||||
# Cloud SQL (Database)
|
||||
@@ -1 +0,0 @@
|
||||
# Cluster Computing Basics
|
||||
@@ -1 +0,0 @@
|
||||
# Cluster Management Tools
|
||||
@@ -1 +0,0 @@
|
||||
# Column
|
||||
@@ -1 +0,0 @@
|
||||
# Compute Engine (Compute)
|
||||
@@ -1 +0,0 @@
|
||||
# Containers & Orchestration
|
||||
@@ -1 +0,0 @@
|
||||
# CosmosDB
|
||||
@@ -1 +0,0 @@
|
||||
# CouchDB
|
||||
@@ -1 +0,0 @@
|
||||
# Data Analytics
|
||||
@@ -1 +0,0 @@
|
||||
# Data Collection Considerations
|
||||
@@ -1 +0,0 @@
|
||||
# Data Engineering Lifecycle
|
||||
@@ -1 +0,0 @@
|
||||
# Data Engineering Lifecycle
|
||||
@@ -1 +0,0 @@
|
||||
# Data Engineering vs Data Science
|
||||
@@ -1 +0,0 @@
|
||||
# Data Fabric
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user