mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-15 11:21:46 +08:00
Compare commits
108 Commits
4.0
...
content/as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc780e08c | ||
|
|
9bec0cb30d | ||
|
|
817c2f2bb2 | ||
|
|
25ea9aeb00 | ||
|
|
0274d9eddb | ||
|
|
8e181d26cb | ||
|
|
177f34404b | ||
|
|
85206085df | ||
|
|
6f9d586415 | ||
|
|
277d7815b9 | ||
|
|
45ed8d3ec1 | ||
|
|
fc37b42ff1 | ||
|
|
16de89c832 | ||
|
|
b5566c2984 | ||
|
|
c3bf2716d4 | ||
|
|
aaf4b61845 | ||
|
|
025e4895ca | ||
|
|
20d06d929e | ||
|
|
e74d3b5e2e | ||
|
|
efecc43523 | ||
|
|
fd40850d0c | ||
|
|
09718af33f | ||
|
|
3af67ec742 | ||
|
|
2a4985f8c4 | ||
|
|
74edb3d2fd | ||
|
|
6ce6ce36d7 | ||
|
|
001f0d31a3 | ||
|
|
0262e7486b | ||
|
|
27810a2c39 | ||
|
|
fa1ec112b1 | ||
|
|
71a2b3712c | ||
|
|
28a87feb92 | ||
|
|
0ca7d23cbd | ||
|
|
04d19d3d6d | ||
|
|
b0630a34ee | ||
|
|
a850e3785c | ||
|
|
5fc79810ff | ||
|
|
4f84f35bcf | ||
|
|
16b1ec96b1 | ||
|
|
04019d1b27 | ||
|
|
68f4b2ec11 | ||
|
|
fa0bcb7ef1 | ||
|
|
7ad3089d13 | ||
|
|
aa55a6dac0 | ||
|
|
44239647ed | ||
|
|
5d66bf786e | ||
|
|
fb098550b4 | ||
|
|
2793e2b13a | ||
|
|
ce3ff223aa | ||
|
|
167419b70a | ||
|
|
48d6e320bc | ||
|
|
e583e3499f | ||
|
|
76b1562c51 | ||
|
|
ba86e8a6b1 | ||
|
|
5f23d4c7eb | ||
|
|
8264c4509f | ||
|
|
6c8aea98da | ||
|
|
64ccd02d53 | ||
|
|
f8c1c6278b | ||
|
|
4786265e04 | ||
|
|
8badf383b2 | ||
|
|
c4406b7649 | ||
|
|
1e878069bc | ||
|
|
8234de2b8c | ||
|
|
3466708ed4 | ||
|
|
b440fd9787 | ||
|
|
9bc73ab738 | ||
|
|
91c16a5e32 | ||
|
|
1768150fb1 | ||
|
|
4a1374c978 | ||
|
|
43df31b312 | ||
|
|
2037edb2da | ||
|
|
de8a4d4acf | ||
|
|
a67a27299e | ||
|
|
5d164198d4 | ||
|
|
a76b9d9ac0 | ||
|
|
6ed83349ba | ||
|
|
1b21550e48 | ||
|
|
c1d0ff7ea2 | ||
|
|
26125fc6d7 | ||
|
|
aff7d8eece | ||
|
|
fd939f198a | ||
|
|
18e4804a51 | ||
|
|
ed8bf11150 | ||
|
|
61f088d42a | ||
|
|
faee01b22d | ||
|
|
dc56ef6190 | ||
|
|
f393a23994 | ||
|
|
8e61330080 | ||
|
|
2c18529429 | ||
|
|
88ff836bfb | ||
|
|
66cb4f9a06 | ||
|
|
d9697b74fd | ||
|
|
863b7fa08b | ||
|
|
64078f9d1a | ||
|
|
5f8ead3d2f | ||
|
|
cb16abc8e1 | ||
|
|
52d00a0654 | ||
|
|
d5495f7280 | ||
|
|
564c9fdd4f | ||
|
|
e75df0ef9e | ||
|
|
642cbbf6d3 | ||
|
|
032602ad3b | ||
|
|
522f16957a | ||
|
|
1f3bf761cd | ||
|
|
f76f0ea1a6 | ||
|
|
a40457edc8 | ||
|
|
076db6dd0a |
@@ -1,120 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 1 - Renames each readme.md to index.md
|
||||
// e.g.
|
||||
// before => roadmaps/frontend/content/internet/readme.md
|
||||
// after => roadmaps/frontend/content/internet/index.md
|
||||
//
|
||||
// 2 - Replaces the resource tags with short codes
|
||||
// e.g.
|
||||
// <ResourceGroupTitle>Free Content</ResourceGroupTitle>
|
||||
// <BadgeLink colorScheme='yellow' badgeText='Read' href='https://www.w3schools.com/css/'>W3Schools — Learn CSS</BadgeLink>
|
||||
//
|
||||
// {% resources %}
|
||||
// {% Blog "https://www.w3schools.com/css/", "W3Schools — Learn CSS" %}
|
||||
// {% endresources %}
|
||||
//
|
||||
// 3 - Removes the index.md file from within the content dir i.e. to avoid `/frontend` permalink for `/frontend/index.md`
|
||||
// Because we have the `/frontend` permalink serving the actual roadmap and not any content
|
||||
const roadmapsDir = path.join(__dirname, '../src/roadmaps');
|
||||
const roadmapDirs = fs.readdirSync(roadmapsDir);
|
||||
|
||||
roadmapDirs.forEach((roadmapDirName) => {
|
||||
const roadmapDirPath = path.join(roadmapsDir, roadmapDirName);
|
||||
const contentDirPath = path.join(roadmapDirPath, 'content');
|
||||
|
||||
console.log(`[Start] == Migrating ${roadmapDirName}`);
|
||||
|
||||
if (!fs.existsSync(contentDirPath)) {
|
||||
console.log(`Content dir not found ${roadmapDirName}/content`);
|
||||
return;
|
||||
}
|
||||
|
||||
function handleContentDir(parentDirPath) {
|
||||
const dirChildrenNames = fs.readdirSync(parentDirPath);
|
||||
|
||||
dirChildrenNames.forEach((dirChildName) => {
|
||||
let dirChildPath = path.join(parentDirPath, dirChildName);
|
||||
|
||||
// If directory, handle the children for it
|
||||
if (fs.lstatSync(dirChildPath).isDirectory()) {
|
||||
handleContentDir(dirChildPath);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 1 - Rename directories to remove the numbers
|
||||
//////////////////////////////////////////////////////////
|
||||
// let newDirChildPath = path.join(
|
||||
// path.dirname(dirChildPath),
|
||||
// path.basename(dirChildPath).replace(/^\d+-/, '')
|
||||
// );
|
||||
// fs.renameSync(dirChildPath, dirChildPath);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 1 - Rename readme.md to index.md
|
||||
//////////////////////////////////////////////////////////
|
||||
if (dirChildPath.endsWith('readme.md')) {
|
||||
const newFilePath = path.join(path.dirname(dirChildPath), `index.md`);
|
||||
|
||||
fs.renameSync(dirChildPath, newFilePath);
|
||||
dirChildPath = newFilePath;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 2 - Replace the resource tags with short codes
|
||||
//////////////////////////////////////////////////////////
|
||||
if (fs.lstatSync(dirChildPath).isFile()) {
|
||||
const fileContent = fs.readFileSync(dirChildPath, 'utf-8');
|
||||
|
||||
let resourceLinks = [...fileContent.matchAll(/<BadgeLink.+<\/BadgeLink>/g)].map(([fullMatch]) => {
|
||||
// const resourceType = fullMatch.match(/badgeText=["'](.+?)["']/)[1];
|
||||
const link = fullMatch.match(/href=["'](.+?)["']/)[1];
|
||||
const text = fullMatch.match(/>([^<]+)<\/BadgeLink>$/)[1];
|
||||
|
||||
return `- [${text.replaceAll(/['"]/g, '')}](${link})`;
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Replace the dedicated roadmap tag with the short code
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// prettier-ignore
|
||||
const dedicatedRegex = /<DedicatedRoadmap\s*href=['"](.+?)['"]\s*title=['"](.+?)['"]\s*description=['"].+?['"]\s*\/>/;
|
||||
const dedicatedMatches = fileContent.match(dedicatedRegex);
|
||||
|
||||
if (dedicatedMatches) {
|
||||
const [, href, title] = dedicatedMatches;
|
||||
|
||||
resourceLinks = [`- [Visit Dedicated ${title}](${href})`, ...resourceLinks];
|
||||
}
|
||||
|
||||
resourceLinks = ['Visit the following resources to learn more:\n', ...resourceLinks];
|
||||
resourceLinks = resourceLinks.join('\n');
|
||||
|
||||
let newFileContent = fileContent.replace(
|
||||
/<ResourceGroupTitle>([^<\/BadgeLink>]|\S|\s)+<\/BadgeLink>/,
|
||||
resourceLinks
|
||||
);
|
||||
|
||||
// In case if the resources were not wrapped in <ResourceGroupTitle>
|
||||
newFileContent = newFileContent.replace(
|
||||
/<BadgeLink([^<\/BadgeLink>]|\S|\s)+<\/BadgeLink>/,
|
||||
resourceLinks
|
||||
);
|
||||
|
||||
fs.writeFileSync(dirChildPath, newFileContent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleContentDir(contentDirPath);
|
||||
|
||||
// 3 - Removes the index.md file from within the content dir i.e. to avoid `/frontend` permalink for `/frontend/index.md`
|
||||
// Because we have the `/frontend` permalink serving the actual roadmap and not any content
|
||||
const contentRootFile = path.join(contentDirPath, '/index.md');
|
||||
if (fs.existsSync(contentRootFile)) {
|
||||
fs.rmSync(contentRootFile);
|
||||
}
|
||||
|
||||
console.log(` == Migrated ${roadmapDirName}`);
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
|
||||
const contentDirPath = path.join(__dirname, './developer-roadmap/content');
|
||||
const guides = require('./developer-roadmap/content/guides.json');
|
||||
const authors = require('./developer-roadmap/content/authors.json');
|
||||
|
||||
const guideImagesDirPath = path.join(__dirname, './developer-roadmap/public/guides');
|
||||
const newGuideImagesDirPath = path.join(__dirname, '../public/guides');
|
||||
|
||||
// Remove the guide images directory
|
||||
if (fs.existsSync(newGuideImagesDirPath)) {
|
||||
fs.rmSync(newGuideImagesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.cpSync(guideImagesDirPath, newGuideImagesDirPath, { recursive: true });
|
||||
|
||||
// Remove the old guides directory
|
||||
const newGuidesDirPath = path.join(__dirname, '../src/guides');
|
||||
if (fs.existsSync(newGuidesDirPath)) {
|
||||
fs.rmSync(newGuidesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newGuidesDirPath);
|
||||
|
||||
guides.forEach((guide) => {
|
||||
const { id: guideId } = guide;
|
||||
|
||||
const originalGuidePath = path.join(contentDirPath, 'guides', `${guideId}.md`);
|
||||
const newGuidePath = path.join(__dirname, `../src/guides/${guideId}.md`);
|
||||
|
||||
const guideWithoutFrontmatter = fs.readFileSync(originalGuidePath, 'utf8');
|
||||
fs.copyFileSync(originalGuidePath, newGuidePath);
|
||||
|
||||
const guideAuthor = authors.find((author) => author.username === guide.authorUsername);
|
||||
|
||||
const guideFrontMatter = yaml
|
||||
.stringify({
|
||||
title: guide.title,
|
||||
description: guide.description,
|
||||
author: {
|
||||
name: guideAuthor.name,
|
||||
url: `https://twitter.com/${guideAuthor.twitter}`,
|
||||
imageUrl: `${guideAuthor.picture}`,
|
||||
},
|
||||
seo: {
|
||||
title: `${guide.title} - roadmap.sh`,
|
||||
description: guide.description,
|
||||
},
|
||||
isNew: guide.isNew,
|
||||
type: guide.type,
|
||||
date: guide.createdAt.replace(/T.*/, ''),
|
||||
sitemap: {
|
||||
priority: 0.7,
|
||||
changefreq: 'weekly',
|
||||
},
|
||||
tags: ['guide', `${guide.type}-guide`, `guide-sitemap`],
|
||||
})
|
||||
.replace(/date: "(.+?)"/, 'date: $1');
|
||||
|
||||
const guideWithUpdatedUrls = guideWithoutFrontmatter
|
||||
.replace(/\[\!\[\]\((.+?\.png)\)\]\((.+?\.png)\)/g, '[]($2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.svg)\)/g, '[]($2)')
|
||||
.replace(/\/http/g, 'http')
|
||||
.replace(/]\(\/guides\/(.+?)\.png\)/g, '](/guides/$1.png)')
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const guideWithFrontmatter = `---\n${guideFrontMatter}---\n\n${guideWithUpdatedUrls}`;
|
||||
|
||||
console.log(`Writing guide ${guideId} to disk`);
|
||||
fs.writeFileSync(newGuidePath, guideWithFrontmatter);
|
||||
});
|
||||
|
||||
const oldAuthorAssetsPath = path.join(__dirname, 'developer-roadmap/public/authors');
|
||||
const newAuthorAssetsPath = path.join(__dirname, '../public/authors');
|
||||
|
||||
if (fs.existsSync(newAuthorAssetsPath)) {
|
||||
fs.rmSync(newAuthorAssetsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.cpSync(oldAuthorAssetsPath, newAuthorAssetsPath, { recursive: true });
|
||||
163
bin/roadmap-content.cjs
Normal file
163
bin/roadmap-content.cjs
Normal file
@@ -0,0 +1,163 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the roadmaps
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(__dirname, `../public/jsons/${roadmapId}`));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param sortOrders Mapping from groupName to sort order
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, sortOrders, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(roadmapContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replace(/(^\d+?-)/g, '') // Remove sorting information
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
|
||||
// Attach sorting information to dirname
|
||||
// e.g. /roadmaps/100-frontend/content/internet
|
||||
// ———> /roadmaps/100-frontend/content/103-internet
|
||||
if (sortOrder) {
|
||||
parentDir = parentDir.replace(/(.+?)([^\/]+)?$/, `$1${sortOrder}-$2`);
|
||||
}
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
dirSortOrders,
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
44
bin/roadmap-links.cjs
Normal file
44
bin/roadmap-links.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const roadmapId = process.argv[2];
|
||||
if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function readFiles(folderPath) {
|
||||
const stats = fs.lstatSync(folderPath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
return [folderPath];
|
||||
}
|
||||
|
||||
const folderContent = fs.readdirSync(folderPath);
|
||||
let files = [];
|
||||
|
||||
for (const file of folderContent) {
|
||||
const filePath = path.join(folderPath, file);
|
||||
|
||||
files = [...files, ...readFiles(filePath)];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = readFiles(fullPath);
|
||||
let allLinks = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileContent = fs.readFileSync(file, 'utf-8');
|
||||
const matches = [...fileContent.matchAll(/\[[^\]]+]\((https?:\/\/[^)]+)\)/g)];
|
||||
|
||||
allLinks = [...allLinks, ...matches.map((match) => match[1])];
|
||||
});
|
||||
|
||||
allLinks.map((link) => console.log(link));
|
||||
@@ -1,116 +0,0 @@
|
||||
module.exports = {
|
||||
angular: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2277.8,
|
||||
},
|
||||
},
|
||||
'aspnet-core': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2773.45,
|
||||
},
|
||||
},
|
||||
backend: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2840.4,
|
||||
},
|
||||
},
|
||||
blockchain: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2173.87,
|
||||
},
|
||||
},
|
||||
'computer-science': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 3009.05,
|
||||
},
|
||||
},
|
||||
'design-system': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2309.7,
|
||||
},
|
||||
},
|
||||
devops: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2527.46,
|
||||
},
|
||||
},
|
||||
flutter: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2042.2,
|
||||
},
|
||||
},
|
||||
frontend: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2734.48,
|
||||
},
|
||||
},
|
||||
golang: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1495.21,
|
||||
},
|
||||
},
|
||||
java: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1167.29,
|
||||
},
|
||||
},
|
||||
javascript: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2438.9,
|
||||
},
|
||||
},
|
||||
nodejs: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2474.06,
|
||||
},
|
||||
},
|
||||
python: {
|
||||
dimensions: {
|
||||
width: 992,
|
||||
height: 1259.03,
|
||||
},
|
||||
},
|
||||
qa: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2107.75,
|
||||
},
|
||||
},
|
||||
react: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1570.26,
|
||||
},
|
||||
},
|
||||
'software-architect': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1882.18,
|
||||
},
|
||||
},
|
||||
'software-design-architecture': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1764.66,
|
||||
},
|
||||
},
|
||||
vue: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1657.07,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
const roadmapMetas = require('./roadmap-metas.cjs');
|
||||
|
||||
const oldAssetsPath = path.join(__dirname, 'developer-roadmap/public');
|
||||
const newAssetsPath = path.join(__dirname, '../public/');
|
||||
|
||||
// Create JSONs dir
|
||||
const newJsonsPath = path.join(newAssetsPath, 'jsons');
|
||||
if (fs.existsSync(newJsonsPath)) {
|
||||
fs.rmSync(newJsonsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newJsonsPath);
|
||||
|
||||
// Create PDFs dir
|
||||
const newPdfsPath = path.join(newAssetsPath, 'pdfs');
|
||||
if (fs.existsSync(newPdfsPath)) {
|
||||
fs.rmSync(newPdfsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newPdfsPath);
|
||||
|
||||
const oldRoadmapsDirPath = path.join(__dirname, 'developer-roadmap/content/roadmaps');
|
||||
const newRoadmapsDirPath = path.join(__dirname, '../src/roadmaps');
|
||||
|
||||
if (fs.existsSync(newRoadmapsDirPath)) {
|
||||
fs.rmSync(newRoadmapsDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newRoadmapsDirPath);
|
||||
|
||||
const oldRoadmaps = fs
|
||||
.readdirSync(oldRoadmapsDirPath)
|
||||
.map((roadmapDirName) => path.join(oldRoadmapsDirPath, roadmapDirName));
|
||||
|
||||
const orderInfo = {};
|
||||
const typeCounter = {
|
||||
role: 1,
|
||||
tool: 1,
|
||||
};
|
||||
|
||||
// Calculate the sorting information for the roadmaps
|
||||
oldRoadmaps.forEach((oldRoadmapPath) => {
|
||||
const roadmapId = path.basename(oldRoadmapPath).replace(/\d+-/g, '').toLowerCase();
|
||||
const oldRoadmapMeta = require(path.join(oldRoadmapPath, 'meta.json'));
|
||||
|
||||
orderInfo[roadmapId] = typeCounter[oldRoadmapMeta.type];
|
||||
typeCounter[oldRoadmapMeta.type] += 1;
|
||||
});
|
||||
|
||||
// Iterate and create new roadmaps
|
||||
oldRoadmaps.forEach((oldRoadmapPath) => {
|
||||
const roadmapId = path.basename(oldRoadmapPath).replace(/\d+-/g, '').toLowerCase();
|
||||
|
||||
const metaToMerge = roadmapMetas[roadmapId] ?? {};
|
||||
const oldRoadmapMeta = require(path.join(oldRoadmapPath, 'meta.json'));
|
||||
const isTextual = oldRoadmapMeta?.landingPath?.endsWith('.md');
|
||||
|
||||
const hasContentDir = fs.existsSync(path.join(oldRoadmapPath, 'content'));
|
||||
|
||||
const roadmapFileContent = isTextual
|
||||
? fs.readFileSync(path.join(oldRoadmapPath, oldRoadmapMeta.landingPath), 'utf8')
|
||||
: '';
|
||||
|
||||
const roadmapFileContentWithUpdatedUrls = roadmapFileContent
|
||||
.replace(/\[\!\[\]\((.+?\.png)\)\]\((.+?\.png)\)/g, '[](/assets$2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.svg)\)/g, '[](/assets$2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.png)\)/g, '[](/assets$2)')
|
||||
.replace(/assetshttp\//g, 'http')
|
||||
.replace(/assetshttps:\/\//g, 'https://')
|
||||
.replace(/\/http/g, 'http')
|
||||
.replace(/]\(\/roadmaps\/(.+?)\.png\)/g, '](/assets/roadmaps/$1.png)')
|
||||
.replace(/]\(\/roadmaps\/(.+?)\.svg\)/g, '](/assets/roadmaps/$1.svg)')
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const hasJson = fs.existsSync(path.join(oldAssetsPath, `/project/${roadmapId}.json`));
|
||||
|
||||
const newRoadmapMeta = {
|
||||
...( hasJson ? { jsonUrl: `/jsons/${roadmapId}.json`} : {}),
|
||||
pdfUrl: `/pdfs/${roadmapId}.pdf`,
|
||||
order: orderInfo[roadmapId],
|
||||
featuredTitle:
|
||||
oldRoadmapMeta.featuredTitle === 'Software Design and Architecture'
|
||||
? 'Software Design'
|
||||
: oldRoadmapMeta.featuredTitle,
|
||||
featuredDescription: oldRoadmapMeta.featuredDescription,
|
||||
title: oldRoadmapMeta.title,
|
||||
description: oldRoadmapMeta.description,
|
||||
isNew: oldRoadmapMeta.isNew,
|
||||
hasTopics: hasContentDir,
|
||||
...metaToMerge,
|
||||
seo: oldRoadmapMeta.seo,
|
||||
relatedRoadmaps: oldRoadmapMeta.relatedRoadmaps,
|
||||
sitemap: {
|
||||
priority: 1,
|
||||
changefreq: 'monthly',
|
||||
},
|
||||
tags: ['roadmap', 'main-sitemap', `${oldRoadmapMeta.type === 'tool' ? 'skill' : oldRoadmapMeta.type}-roadmap`],
|
||||
};
|
||||
|
||||
const frontmatter = yaml.stringify(newRoadmapMeta);
|
||||
const newRoadmapDirPath = path.join(newRoadmapsDirPath, roadmapId);
|
||||
const newRoadmapFilePath = path.join(newRoadmapDirPath, `/${roadmapId}.md`);
|
||||
|
||||
fs.mkdirSync(newRoadmapDirPath);
|
||||
fs.writeFileSync(newRoadmapFilePath, `---\n${frontmatter}---\n\n${roadmapFileContentWithUpdatedUrls}`);
|
||||
|
||||
const jsonFile = path.join(oldAssetsPath, oldRoadmapMeta.jsonUrl || '/unknown');
|
||||
const pdfFile = path.join(oldAssetsPath, oldRoadmapMeta.pdfUrl || '/unknown');
|
||||
|
||||
if (fs.existsSync(jsonFile)) {
|
||||
fs.copyFileSync(jsonFile, path.join(newJsonsPath, `${roadmapId}.json`));
|
||||
}
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
fs.copyFileSync(pdfFile, path.join(newPdfsPath, `${roadmapId}.pdf`));
|
||||
}
|
||||
|
||||
// Copy the content directory
|
||||
const oldRoadmapContentDir = path.join(oldRoadmapPath, 'content');
|
||||
if (fs.existsSync(oldRoadmapContentDir)) {
|
||||
fs.cpSync(oldRoadmapContentDir, path.join(newRoadmapDirPath, 'content'), { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
const roadmapAssets = path.join(oldAssetsPath, 'roadmaps');
|
||||
if (fs.existsSync(roadmapAssets)) {
|
||||
fs.cpSync(roadmapAssets, path.join(newAssetsPath, 'roadmaps'), { recursive: true });
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Change working directory to the directory of this script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -d "./developer-roadmap" ]; then
|
||||
git clone --depth 1 -b master git@github.com:kamranahmedse/developer-roadmap.git
|
||||
fi
|
||||
|
||||
echo "Removing old directories"
|
||||
rm -rf ../src/videos
|
||||
rm -rf ../src/guides
|
||||
rm -rf ../src/roadmaps
|
||||
|
||||
rm -rf ../public/jsons
|
||||
rm -rf ../public/pdfs
|
||||
|
||||
echo "=== Migrating Roadmaps ==="
|
||||
node roadmap-migrator.cjs
|
||||
|
||||
echo "=== Migrating Content ==="
|
||||
node content-migrator.cjs
|
||||
|
||||
echo "=== Migrating Guides ==="
|
||||
node guide-migrator.cjs
|
||||
|
||||
echo "=== Migrating Videos ==="
|
||||
node video-migrator.cjs
|
||||
@@ -1,58 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
|
||||
const contentDirPath = path.join(__dirname, './developer-roadmap/content');
|
||||
const videos = require('./developer-roadmap/content/videos.json');
|
||||
|
||||
// Remove the old videos directory
|
||||
const newVideosDirPath = path.join(__dirname, '../src/videos');
|
||||
if (fs.existsSync(newVideosDirPath)) {
|
||||
fs.rmSync(newVideosDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newVideosDirPath);
|
||||
|
||||
videos.forEach((video) => {
|
||||
const { id: videoId } = video;
|
||||
|
||||
const originalVideoPath = path.join(
|
||||
contentDirPath,
|
||||
'videos',
|
||||
`${videoId}.md`
|
||||
);
|
||||
|
||||
const newVideoPath = path.join(__dirname, `../src/videos/${videoId}.md`);
|
||||
|
||||
const videoWithoutFrontmatter = fs.readFileSync(originalVideoPath, 'utf8');
|
||||
fs.copyFileSync(originalVideoPath, newVideoPath);
|
||||
|
||||
const videoFrontMatter = yaml
|
||||
.stringify({
|
||||
title: video.title,
|
||||
description: video.description,
|
||||
duration: video.duration,
|
||||
isNew: video.isNew,
|
||||
date: video.createdAt.replace(/T.*/, ''),
|
||||
author: {
|
||||
name: 'Kamran Ahmed',
|
||||
url: `https://twitter.com/kamranahmedse`,
|
||||
imageUrl: `/authors/kamranahmedse.jpeg`,
|
||||
},
|
||||
sitemap: {
|
||||
priority: 0.7,
|
||||
changefreq: 'weekly',
|
||||
},
|
||||
tags: ['video', `video-sitemap`],
|
||||
})
|
||||
.replace(/date: "(.+?)"/, 'date: $1');
|
||||
|
||||
const videoWithIframeClass = videoWithoutFrontmatter
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const videoWithFrontmatter = `---\n${videoFrontMatter}---\n\n${videoWithIframeClass}`;
|
||||
|
||||
console.log(`Writing video ${videoId} to disk`);
|
||||
fs.writeFileSync(newVideoPath, videoWithFrontmatter);
|
||||
});
|
||||
@@ -10,8 +10,10 @@
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"sync-content": "sh ./bin/sync-content.sh",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs"
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
@@ -20,6 +22,7 @@
|
||||
"astro-compress": "^1.1.24",
|
||||
"astro-critters": "^1.1.24",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.1",
|
||||
"tailwindcss": "^3.2.4"
|
||||
|
||||
1402
pnpm-lock.yaml
generated
1402
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 835 KiB |
BIN
public/images/devops-ebook.png
Normal file
BIN
public/images/devops-ebook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
File diff suppressed because one or more lines are too long
1
public/jsons/spring-boot.json
Normal file
1
public/jsons/spring-boot.json
Normal file
File diff suppressed because one or more lines are too long
BIN
public/pdfs/software-architect.pdf
Normal file
BIN
public/pdfs/software-architect.pdf
Normal file
Binary file not shown.
BIN
public/pdfs/spring-boot.pdf
Normal file
BIN
public/pdfs/spring-boot.pdf
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="public/brand.png" height="128">
|
||||
<img src="public/images/brand.png" height="128">
|
||||
<h2 align="center"><a href="https://roadmap.sh">roadmap.sh</a></h2>
|
||||
<p align="center">Community driven roadmaps, articles and resources for developers<p>
|
||||
<p align="center">
|
||||
@@ -48,6 +48,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [Go Roadmap](https://roadmap.sh/golang)
|
||||
- [Java Roadmap](https://roadmap.sh/java)
|
||||
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
|
||||
- [Design System Roadmap](https://roadmap.sh/design-system)
|
||||
- [DBA Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [Blockchain Roadmap](https://roadmap.sh/blockchain)
|
||||
@@ -2,7 +2,8 @@
|
||||
---
|
||||
|
||||
<script src='./analytics.js'></script>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-139582634-1'></script>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-139582634-1'
|
||||
></script>
|
||||
<script is:inline>
|
||||
// @ts-nocheck
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
@@ -12,4 +13,29 @@
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-139582634-1');
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
let trackEl = e.target;
|
||||
if (!trackEl.getAttribute('ga-category')) {
|
||||
trackEl = trackEl.closest('[ga-category]');
|
||||
}
|
||||
|
||||
if (!trackEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = trackEl.getAttribute('ga-category');
|
||||
const action = trackEl.getAttribute('ga-action');
|
||||
const label = trackEl.getAttribute('ga-label');
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.fireEvent({
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,8 +34,21 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
type='submit'
|
||||
name='submit'
|
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
|
||||
submit-download-form
|
||||
>
|
||||
Send Link
|
||||
</button>
|
||||
</form>
|
||||
</Popup>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[submit-download-form]')
|
||||
?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
3
src/components/FAQs/Answer.astro
Normal file
3
src/components/FAQs/Answer.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class='text-sm sm:text-base leading-relaxed text-left p-2 sm:p-4 text-md text-gray-800 border-t border-t-gray-300 bg-gray-100 rounded-bl-md rounded-br-md'>
|
||||
<slot />
|
||||
</div>
|
||||
13
src/components/FAQs/FAQs.astro
Normal file
13
src/components/FAQs/FAQs.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='container'>
|
||||
<div class='flex justify-between relative -top-5'>
|
||||
<h1 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
42
src/components/FAQs/Question.astro
Normal file
42
src/components/FAQs/Question.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import Icon from '../Icon.astro';
|
||||
|
||||
export interface Props {
|
||||
question: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const { question, isActive = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class='faq-item bg-white border rounded-md hover:bg-gray-50 border-gray-300'
|
||||
>
|
||||
<button
|
||||
faq-question
|
||||
class='flex flex-row justify-between items-center p-2 sm:p-3 w-full'
|
||||
>
|
||||
<span class='text-sm sm:text-base text-left font-medium'>{question}</span>
|
||||
<Icon icon='down' class='h-6 hidden sm:block text-gray-400' />
|
||||
</button>
|
||||
<div class:list={['answer', { hidden: !isActive }]} faq-answer>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('[faq-question]').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
// Hide any other visible answers
|
||||
document.querySelectorAll('[faq-answer]').forEach((element) => {
|
||||
element.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show the current answer
|
||||
const answer = el.nextElementSibling;
|
||||
if (answer) {
|
||||
answer.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -84,23 +84,29 @@ import Icon from './Icon.astro';
|
||||
<div class='text-gray-400 text-sm'>
|
||||
<p>
|
||||
<a
|
||||
href='https://thenewstack.io/category/devops/'
|
||||
href='https://thenewstack.io/category/devops?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>DevOps</a
|
||||
>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
href='https://thenewstack.io/category/kubernetes/'
|
||||
href='https://thenewstack.io/category/kubernetes?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>Kubernetes</a
|
||||
>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
href='https://thenewstack.io/category/cloud-native/'
|
||||
href='https://thenewstack.io/category/cloud-native?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>Cloud-Native</a
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,11 @@ const { author } = frontmatter;
|
||||
target='_blank'
|
||||
class='font-medium hover:text-gray-600 inline-flex items-center hover:underline'
|
||||
>
|
||||
<img src={author.imageUrl} class='w-5 h-5 inline mr-2 rounded-full' />
|
||||
<img
|
||||
alt={author.name}
|
||||
src={author.imageUrl}
|
||||
class='w-5 h-5 inline mr-2 rounded-full'
|
||||
/>
|
||||
{author.name}
|
||||
</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import DownloadPopup from "../DownloadPopup.astro";
|
||||
import Loader from "../Loader.astro";
|
||||
import ShareIcons from "../ShareIcons.astro";
|
||||
import SubscribePopup from "../SubscribePopup.astro";
|
||||
import TopicOverlay from "../TopicOverlay.astro";
|
||||
import "./InteractiveRoadmap.css";
|
||||
import DownloadPopup from '../DownloadPopup.astro';
|
||||
import Loader from '../Loader.astro';
|
||||
import ShareIcons from '../ShareIcons.astro';
|
||||
import SubscribePopup from '../SubscribePopup.astro';
|
||||
import TopicOverlay from '../TopicOverlay.astro';
|
||||
import './InteractiveRoadmap.css';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
@@ -16,31 +16,32 @@ export interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } =
|
||||
Astro.props;
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } = Astro.props;
|
||||
---
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/balsamiq.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
rel='preload'
|
||||
href='/fonts/balsamiq.woff2'
|
||||
as='font'
|
||||
type='font/woff2'
|
||||
crossorigin
|
||||
slot="after-header"
|
||||
slot='after-header'
|
||||
/>
|
||||
|
||||
<div class="bg-gray-50 py-4 sm:py-12">
|
||||
<div class="max-w-[1000px] container relative">
|
||||
<div class='bg-gray-50 py-4 sm:py-12'>
|
||||
<div class='max-w-[1000px] container relative'>
|
||||
<ShareIcons
|
||||
description={description}
|
||||
pageUrl={`https://roadmap.sh/${roadmapId}`}
|
||||
/>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
<TopicOverlay />
|
||||
<TopicOverlay roadmapId={roadmapId} />
|
||||
<div
|
||||
id="roadmap-svg"
|
||||
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null}
|
||||
id='roadmap-svg'
|
||||
style={dimensions
|
||||
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
|
||||
: null}
|
||||
data-roadmap-id={roadmapId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
@@ -49,4 +50,4 @@ const { roadmapId, jsonUrl, dimensions = null, description } =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./roadmap.js"></script>
|
||||
<script src='./roadmap.js'></script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { wireframeJSONToSVG } from "roadmap-renderer";
|
||||
import { Topic } from "./topic";
|
||||
import { Sharer } from "./sharer";
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Topic } from './topic';
|
||||
import { Sharer } from './sharer';
|
||||
|
||||
/**
|
||||
* @typedef {{ roadmapId: string, jsonUrl: string }} RoadmapConfig
|
||||
@@ -11,10 +11,10 @@ export class Roadmap {
|
||||
* @param {RoadmapConfig} config
|
||||
*/
|
||||
constructor() {
|
||||
this.roadmapId = "";
|
||||
this.jsonUrl = "";
|
||||
this.roadmapId = '';
|
||||
this.jsonUrl = '';
|
||||
|
||||
this.containerId = "roadmap-svg";
|
||||
this.containerId = 'roadmap-svg';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
@@ -28,10 +28,16 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
prepareConfig() {
|
||||
if (!this.containerEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.roadmapId = dataset.roadmapId;
|
||||
this.jsonUrl = dataset.jsonUrl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +46,7 @@ export class Roadmap {
|
||||
*/
|
||||
fetchRoadmapSvg(jsonUrl) {
|
||||
if (!jsonUrl) {
|
||||
console.error("jsonUrl not defined in frontmatter");
|
||||
console.error('jsonUrl not defined in frontmatter');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,13 +56,15 @@ export class Roadmap {
|
||||
})
|
||||
.then(function (json) {
|
||||
return wireframeJSONToSVG(json, {
|
||||
fontURL: "/fonts/balsamiq.woff2",
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDOMLoaded() {
|
||||
this.prepareConfig();
|
||||
if (!this.prepareConfig()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchRoadmapSvg(this.jsonUrl)
|
||||
.then((svg) => {
|
||||
@@ -66,8 +74,8 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
handleRoadmapClick(e) {
|
||||
const targetGroup = e.target.closest("g") || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : "";
|
||||
const targetGroup = e.target.closest('g') || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +83,7 @@ export class Roadmap {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("topic.click", {
|
||||
new CustomEvent('topic.click', {
|
||||
detail: {
|
||||
topicId: groupId,
|
||||
roadmapId: this.roadmapId,
|
||||
@@ -85,8 +93,8 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("DOMContentLoaded", this.onDOMLoaded);
|
||||
window.addEventListener("click", this.handleRoadmapClick);
|
||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||
window.addEventListener('click', this.handleRoadmapClick);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export class Sharer {
|
||||
this.init = this.init.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.shareIconsId = "page-share-icons";
|
||||
this.shareIconsId = 'page-share-icons';
|
||||
}
|
||||
|
||||
get shareIconsEl() {
|
||||
@@ -12,14 +12,18 @@ export class Sharer {
|
||||
|
||||
onScroll() {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
this.shareIconsEl.classList.add("hidden");
|
||||
this.shareIconsEl.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.shareIconsEl.classList.remove("hidden");
|
||||
this.shareIconsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
if (!this.shareIconsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,204 +1,219 @@
|
||||
export class Topic {
|
||||
constructor() {
|
||||
this.overlayId = 'topic-overlay';
|
||||
this.contentId = 'topic-content';
|
||||
this.loaderId = 'topic-loader';
|
||||
this.topicBodyId = 'topic-body';
|
||||
this.topicActionsId = 'topic-actions';
|
||||
this.markTopicDoneId = 'mark-topic-done';
|
||||
this.markTopicPendingId = 'mark-topic-pending';
|
||||
this.closeTopicId = 'close-topic';
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleTopicClick = this.handleTopicClick.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
this.populate = this.populate.bind(this);
|
||||
this.handleOverlayClick = this.handleOverlayClick.bind(this);
|
||||
this.markAsDone = this.markAsDone.bind(this);
|
||||
this.markAsPending = this.markAsPending.bind(this);
|
||||
this.queryRoadmapElementsByTopicId = this.queryRoadmapElementsByTopicId.bind(this);
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
constructor() {
|
||||
this.overlayId = 'topic-overlay';
|
||||
this.contentId = 'topic-content';
|
||||
this.loaderId = 'topic-loader';
|
||||
this.topicBodyId = 'topic-body';
|
||||
this.topicActionsId = 'topic-actions';
|
||||
this.markTopicDoneId = 'mark-topic-done';
|
||||
this.markTopicPendingId = 'mark-topic-pending';
|
||||
this.closeTopicId = 'close-topic';
|
||||
this.contributionTextId = 'contrib-meta';
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleTopicClick = this.handleTopicClick.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
this.populate = this.populate.bind(this);
|
||||
this.handleOverlayClick = this.handleOverlayClick.bind(this);
|
||||
this.markAsDone = this.markAsDone.bind(this);
|
||||
this.markAsPending = this.markAsPending.bind(this);
|
||||
this.queryRoadmapElementsByTopicId =
|
||||
this.queryRoadmapElementsByTopicId.bind(this);
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get markTopicDoneEl() {
|
||||
return document.getElementById(this.markTopicDoneId);
|
||||
}
|
||||
|
||||
get markTopicPendingEl() {
|
||||
return document.getElementById(this.markTopicPendingId);
|
||||
}
|
||||
|
||||
get topicActionsEl() {
|
||||
return document.getElementById(this.topicActionsId);
|
||||
}
|
||||
|
||||
get contributionTextEl() {
|
||||
return document.getElementById(this.contributionTextId);
|
||||
}
|
||||
|
||||
get contentEl() {
|
||||
return document.getElementById(this.contentId);
|
||||
}
|
||||
|
||||
get overlayEl() {
|
||||
return document.getElementById(this.overlayId);
|
||||
}
|
||||
|
||||
resetDOM(hideOverlay = false) {
|
||||
if (hideOverlay) {
|
||||
this.overlayEl.classList.add('hidden');
|
||||
} else {
|
||||
this.overlayEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get markTopicDoneEl() {
|
||||
return document.getElementById(this.markTopicDoneId);
|
||||
}
|
||||
|
||||
get markTopicPendingEl() {
|
||||
return document.getElementById(this.markTopicPendingId);
|
||||
}
|
||||
|
||||
get topicActionsEl() {
|
||||
return document.getElementById(this.topicActionsId);
|
||||
}
|
||||
|
||||
get contentEl() {
|
||||
return document.getElementById(this.contentId);
|
||||
}
|
||||
|
||||
get overlayEl() {
|
||||
return document.getElementById(this.overlayId);
|
||||
}
|
||||
|
||||
resetDOM(hideOverlay = false) {
|
||||
if (hideOverlay) {
|
||||
this.overlayEl.classList.add('hidden');
|
||||
} else {
|
||||
this.overlayEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||
this.contentEl.replaceChildren(''); // Remove content
|
||||
}
|
||||
|
||||
close() {
|
||||
this.resetDOM(true);
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | HTMLElement} html
|
||||
*/
|
||||
populate(html) {
|
||||
this.contentEl.replaceChildren(html);
|
||||
this.loaderEl.classList.add('hidden');
|
||||
this.topicActionsEl.classList.remove('hidden');
|
||||
|
||||
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
|
||||
const isDone = localStorage.getItem(normalizedGroup) === 'done';
|
||||
|
||||
if (isDone) {
|
||||
this.markTopicDoneEl.classList.add('hidden');
|
||||
this.markTopicPendingEl.classList.remove('hidden');
|
||||
} else {
|
||||
this.markTopicDoneEl.classList.remove('hidden');
|
||||
this.markTopicPendingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
fetchTopicHtml(roadmapId, topicId) {
|
||||
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/');
|
||||
const fullUrl = `/${roadmapId}/${topicPartial}/`;
|
||||
|
||||
return fetch(fullUrl)
|
||||
.then((res) => {
|
||||
return res.text();
|
||||
})
|
||||
.then((topicHtml) => {
|
||||
// It's full HTML with page body, head etc.
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
|
||||
|
||||
return node.getElementById('main-content');
|
||||
});
|
||||
}
|
||||
|
||||
handleTopicClick(e) {
|
||||
const { roadmapId, topicId } = e.detail;
|
||||
if (!topicId || !roadmapId) {
|
||||
console.log('Missing topic or roadmap: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRoadmapId = roadmapId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
if (/^ext_link/.test(topicId)) {
|
||||
window.open(`https://${topicId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDOM();
|
||||
this.fetchTopicHtml(roadmapId, topicId)
|
||||
.then((content) => {
|
||||
this.populate(content);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.populate('Error loading the content!');
|
||||
});
|
||||
}
|
||||
|
||||
queryRoadmapElementsByTopicId(topicId) {
|
||||
const elements = document.querySelectorAll(`[data-group-id$="-${topicId}"]`);
|
||||
const matchingElements = [];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
markAsDone(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
localStorage.setItem(updatedTopicId, 'done');
|
||||
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.add('done');
|
||||
});
|
||||
}
|
||||
|
||||
markAsPending(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
localStorage.removeItem(updatedTopicId);
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.remove('done');
|
||||
});
|
||||
}
|
||||
|
||||
handleOverlayClick(e) {
|
||||
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`);
|
||||
|
||||
if (!isClickedInsideTopic) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`);
|
||||
if (isClickedDone) {
|
||||
this.markAsDone(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedPending = e.target.id === this.markTopicPendingId || e.target.closest(`#${this.markTopicPendingId}`);
|
||||
if (isClickedPending) {
|
||||
this.markAsPending(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedClose = e.target.id === this.closeTopicId || e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('topic.click', this.handleTopicClick);
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 'escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||
this.contributionTextEl.classList.add('hidden'); // Hide contribution text
|
||||
this.contentEl.replaceChildren(''); // Remove content
|
||||
}
|
||||
|
||||
close() {
|
||||
this.resetDOM(true);
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | HTMLElement} html
|
||||
*/
|
||||
populate(html) {
|
||||
this.contentEl.replaceChildren(html);
|
||||
this.loaderEl.classList.add('hidden');
|
||||
this.topicActionsEl.classList.remove('hidden');
|
||||
this.contributionTextEl.classList.remove('hidden');
|
||||
|
||||
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
|
||||
const isDone = localStorage.getItem(normalizedGroup) === 'done';
|
||||
|
||||
if (isDone) {
|
||||
this.markTopicDoneEl.classList.add('hidden');
|
||||
this.markTopicPendingEl.classList.remove('hidden');
|
||||
} else {
|
||||
this.markTopicDoneEl.classList.remove('hidden');
|
||||
this.markTopicPendingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchTopicHtml(roadmapId, topicId) {
|
||||
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/');
|
||||
const fullUrl = `/${roadmapId}/${topicPartial}/`;
|
||||
|
||||
return fetch(fullUrl)
|
||||
.then((res) => {
|
||||
return res.text();
|
||||
})
|
||||
.then((topicHtml) => {
|
||||
// It's full HTML with page body, head etc.
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
|
||||
|
||||
return node.getElementById('main-content');
|
||||
});
|
||||
}
|
||||
|
||||
handleTopicClick(e) {
|
||||
const { roadmapId, topicId } = e.detail;
|
||||
if (!topicId || !roadmapId) {
|
||||
console.log('Missing topic or roadmap: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRoadmapId = roadmapId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
if (/^ext_link/.test(topicId)) {
|
||||
window.open(`https://${topicId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDOM();
|
||||
this.fetchTopicHtml(roadmapId, topicId)
|
||||
.then((content) => {
|
||||
this.populate(content);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.populate('Error loading the content!');
|
||||
});
|
||||
}
|
||||
|
||||
queryRoadmapElementsByTopicId(topicId) {
|
||||
const elements = document.querySelectorAll(
|
||||
`[data-group-id$="-${topicId}"]`
|
||||
);
|
||||
const matchingElements = [];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
markAsDone(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
localStorage.setItem(updatedTopicId, 'done');
|
||||
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.add('done');
|
||||
});
|
||||
}
|
||||
|
||||
markAsPending(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
localStorage.removeItem(updatedTopicId);
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.remove('done');
|
||||
});
|
||||
}
|
||||
|
||||
handleOverlayClick(e) {
|
||||
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`);
|
||||
|
||||
if (!isClickedInsideTopic) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickedDone =
|
||||
e.target.id === this.markTopicDoneId ||
|
||||
e.target.closest(`#${this.markTopicDoneId}`);
|
||||
if (isClickedDone) {
|
||||
this.markAsDone(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedPending =
|
||||
e.target.id === this.markTopicPendingId ||
|
||||
e.target.closest(`#${this.markTopicPendingId}`);
|
||||
if (isClickedPending) {
|
||||
this.markAsPending(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedClose =
|
||||
e.target.id === this.closeTopicId ||
|
||||
e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('topic.click', this.handleTopicClick);
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 'escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
---
|
||||
import "../styles/prism.css";
|
||||
import '../styles/prism.css';
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import ShareIcons from './ShareIcons.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { roadmapId, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="bg-gray-50 py-4 sm:py-10">
|
||||
<div class="container prose prose-headings:mt-4 prose-headings:mb-2 prose-p:mb-0.5">
|
||||
<div class='bg-gray-50 py-4 sm:py-10'>
|
||||
<div
|
||||
class='container prose prose-headings:mt-4 prose-headings:mb-2 prose-p:mb-0.5 relative prose-code:text-white'
|
||||
>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,24 +32,22 @@ import Icon from './Icon.astro';
|
||||
|
||||
<!-- Mobile Navigation Button -->
|
||||
<button
|
||||
id='show-mobile-navigation'
|
||||
class='text-gray-400 hover:text-gray-50 block sm:hidden cursor-pointer'
|
||||
aria-label='Menu'
|
||||
onclick="document.getElementById('mobile-navigation').classList.remove('hidden');"
|
||||
show-mobile-nav
|
||||
>
|
||||
<Icon icon='hamburger' />
|
||||
</button>
|
||||
|
||||
<!-- Mobile Navigation Items -->
|
||||
<div
|
||||
id='mobile-navigation'
|
||||
class='fixed top-0 bottom-0 left-0 right-0 z-40 bg-slate-900 items-center flex hidden'
|
||||
mobile-nav
|
||||
>
|
||||
<button
|
||||
id='close-mobile-navigation'
|
||||
close-mobile-nav
|
||||
class='text-gray-400 hover:text-gray-50 block cursor-pointer absolute top-6 right-6'
|
||||
aria-label='Close Menu'
|
||||
onclick="document.getElementById('mobile-navigation').classList.add('hidden');"
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
@@ -80,3 +78,15 @@ import Icon from './Icon.astro';
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('[show-mobile-nav]')?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('[close-mobile-nav]')
|
||||
?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@ const starCount = await getFormattedStars('kamranahmedse/developer-roadmap');
|
||||
|
||||
<div class='py-6 sm:py-16 border-b border-t text-left sm:text-center bg-white'>
|
||||
<div class='max-w-[600px] container'>
|
||||
<h1 class='text-3xl sm:text-5xl font-bold'>Open Source</h1>
|
||||
<h2 class='text-2xl sm:text-5xl font-bold'>Open Source</h2>
|
||||
<p class='text-gray-600 text-sm sm:text-lg leading-relaxed my-2.5 sm:my-5'>
|
||||
The project is OpenSource, <a
|
||||
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories'
|
||||
|
||||
@@ -21,8 +21,7 @@ const { id, title, subtitle } = Astro.props;
|
||||
<div class='relative bg-white rounded-lg shadow popup-body'>
|
||||
<button
|
||||
type='button'
|
||||
class='absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center'
|
||||
onclick='this.closest(".popup").classList.add("hidden")'
|
||||
class='absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center popup-close'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
<span class='sr-only'>Close popup</span>
|
||||
|
||||
@@ -31,8 +31,9 @@ export class Popup {
|
||||
const target = e.target;
|
||||
const popupBody = target.closest('.popup-body');
|
||||
const closestPopup = target.closest('.popup');
|
||||
const closeBtn = target.closest('.popup-close');
|
||||
|
||||
if (popupBody) {
|
||||
if (!closeBtn && popupBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const roadmapTitle =
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'mt-4 sm:mt-7 border rounded-md mb-0',
|
||||
'mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0',
|
||||
{
|
||||
'sm:-mb-[82px]': hasTNSBanner,
|
||||
'sm:-mb-[65px]': !hasTNSBanner,
|
||||
@@ -29,10 +29,12 @@ const roadmapTitle =
|
||||
<p class='text-sm'>
|
||||
Get the latest {roadmapTitle} news from our sister site{' '}
|
||||
<a
|
||||
href='https://thenewstack.io'
|
||||
href='https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
|
||||
target='_blank'
|
||||
class='font-semibold underline'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Roadmap` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Roadmap'
|
||||
>
|
||||
TheNewStack.io
|
||||
</a>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import ResourcesAlert from "./ResourcesAlert.astro";
|
||||
import TopicSearch from "./TopicSearch/TopicSearch.astro";
|
||||
import YouTubeAlert from "./YouTubeAlert.astro";
|
||||
import Icon from './Icon.astro';
|
||||
import ResourcesAlert from './ResourcesAlert.astro';
|
||||
import TopicSearch from './TopicSearch/TopicSearch.astro';
|
||||
import YouTubeAlert from './YouTubeAlert.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -22,48 +22,57 @@ const {
|
||||
hasTopics = false,
|
||||
} = Astro.props;
|
||||
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<div class="border-b">
|
||||
<div class="py-5 sm:py-12 container relative">
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<YouTubeAlert />
|
||||
|
||||
<div class="mt-0 mb-3 sm:mb-6 sm:mt-4">
|
||||
<h1 class="text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold">
|
||||
<div class='mt-0 mb-3 sm:mb-6 sm:mt-4'>
|
||||
<h1 class='text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold'>
|
||||
{title}
|
||||
</h1>
|
||||
<p class="text-gray-500 text-sm sm:text-lg">{description}</p>
|
||||
<p class='text-gray-500 text-sm sm:text-lg'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-1 sm:gap-2">
|
||||
<div class='flex justify-between'>
|
||||
<div class='flex gap-1 sm:gap-2'>
|
||||
{
|
||||
!hasSearch && (
|
||||
<>
|
||||
<a href='/roadmaps' class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600' aria-label="Back to All Roadmaps">
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
</a>
|
||||
<a
|
||||
href='/roadmaps/'
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
aria-label='Back to All Roadmaps'
|
||||
>
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
</a>
|
||||
|
||||
{isRoadmapReady && (
|
||||
<button
|
||||
data-popup="download-popup"
|
||||
class="inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500"
|
||||
aria-label="Download Roadmap"
|
||||
data-popup='download-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Download Roadmap'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Roadmap Popup'
|
||||
>
|
||||
<Icon icon="download" />
|
||||
<span class="hidden sm:inline ml-2">Download</span>
|
||||
<Icon icon='download' />
|
||||
<span class='hidden sm:inline ml-2'>Download</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-popup="subscribe-popup"
|
||||
class="inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500"
|
||||
aria-label="Subscribe for Updates"
|
||||
data-popup='subscribe-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Subscribe for Updates'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Subscribe Roadmap Popup'
|
||||
>
|
||||
<Icon icon="email" />
|
||||
<span class="ml-2">Subscribe</span>
|
||||
<Icon icon='email' />
|
||||
<span class='ml-2'>Subscribe</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
@@ -73,11 +82,11 @@ const isRoadmapReady = !isUpcoming;
|
||||
hasSearch && (
|
||||
<a
|
||||
href={`/${roadmapId}/`}
|
||||
class="bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600"
|
||||
aria-label="Back to Visual Roadmap"
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
aria-label='Back to Visual Roadmap'
|
||||
>
|
||||
←
|
||||
<span class="inline"> Visual Roadmap</span>
|
||||
<span class='inline'> Visual Roadmap</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -87,13 +96,13 @@ const isRoadmapReady = !isUpcoming;
|
||||
isRoadmapReady && (
|
||||
<a
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
|
||||
target="_blank"
|
||||
class="inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600"
|
||||
aria-label="Suggest Changes"
|
||||
target='_blank'
|
||||
class='inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600'
|
||||
aria-label='Suggest Changes'
|
||||
>
|
||||
<Icon icon="comment" class="h-3 w-3" />
|
||||
<span class="ml-2 hidden sm:inline">Suggest Changes</span>
|
||||
<span class="ml-2 inline sm:hidden">Suggest</span>
|
||||
<Icon icon='comment' class='h-3 w-3' />
|
||||
<span class='ml-2 hidden sm:inline'>Suggest Changes</span>
|
||||
<span class='ml-2 inline sm:hidden'>Suggest</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import type { GAEventType } from '../Analytics/analytics';
|
||||
import Icon from '../Icon.astro';
|
||||
|
||||
export type SponsorType = {
|
||||
url: string;
|
||||
@@ -25,10 +26,19 @@ const {
|
||||
id='sponsor-ad'
|
||||
target='_blank'
|
||||
rel='noopener sponsored'
|
||||
onclick={event ? `window.fireEvent(${JSON.stringify(event)})` : ''}
|
||||
ga-category={event?.category}
|
||||
ga-action={event?.action}
|
||||
ga-label={event?.label}
|
||||
class='fixed bottom-[15px] right-[20px] outline-transparent z-50 bg-white max-w-[330px] shadow-lg outline-0 hidden'
|
||||
>
|
||||
<img src={imageUrl} class='w-[100px] lg:w-[130px]' alt='Sponsor Banner' />
|
||||
<button
|
||||
class='absolute top-1.5 right-1.5 text-gray-300 hover:text-gray-800'
|
||||
aria-label='Close'
|
||||
close-sponsor
|
||||
>
|
||||
<Icon icon='close' class='h-4' />
|
||||
</button>
|
||||
<img src={imageUrl} class='h-[150px] lg:h-[169px]' alt='Sponsor Banner' />
|
||||
<span class='text-sm flex flex-col justify-between'>
|
||||
<span class='p-[10px]'>
|
||||
<span class='font-semibold mb-0.5 block'>{title}</span>
|
||||
@@ -37,3 +47,11 @@ const {
|
||||
<span class='sponsor-footer'>Partner Content</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<script>
|
||||
document.querySelector('[close-sponsor]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.getElementById('sponsor-ad')?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,9 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
type='submit'
|
||||
name='submit'
|
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
|
||||
ga-category='Subscription'
|
||||
ga-action='Submitted Popup Form'
|
||||
ga-label='Subscribe Roadmap Popup'
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
|
||||
@@ -1,31 +1,69 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import Loader from "./Loader.astro";
|
||||
import Icon from './Icon.astro';
|
||||
import Loader from './Loader.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
}
|
||||
|
||||
const { roadmapId } = Astro.props;
|
||||
const githubLink = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps/${roadmapId}/content`;
|
||||
---
|
||||
|
||||
<div id='topic-overlay' class='hidden'>
|
||||
<div class="fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]" tabindex="-1" id='topic-body'>
|
||||
<div id='topic-loader' class='hidden'>
|
||||
<Loader />
|
||||
</div>
|
||||
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button id='mark-topic-done' class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'>
|
||||
<Icon icon="check" /> <span class='ml-2'>Mark as Done</span>
|
||||
</button>
|
||||
|
||||
<button id='mark-topic-pending' class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'>
|
||||
<Icon icon="reset" /> <span class='ml-2'>Mark as Pending</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id='close-topic' class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center">
|
||||
<Icon icon="close" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id='topic-content' class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'></div>
|
||||
<div
|
||||
class='fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]'
|
||||
tabindex='-1'
|
||||
id='topic-body'
|
||||
>
|
||||
<div id='topic-loader' class='hidden'>
|
||||
<Loader />
|
||||
</div>
|
||||
<div class="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30"></div>
|
||||
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button
|
||||
id='mark-topic-done'
|
||||
class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='check' />
|
||||
<span class='ml-2'>Mark as Done</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id='mark-topic-pending'
|
||||
class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='reset' />
|
||||
<span class='ml-2'>Mark as Pending</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
id='close-topic'
|
||||
class='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id='topic-content'
|
||||
class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'
|
||||
>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id='contrib-meta'
|
||||
class='text-gray-400 text-sm border-t pt-3 mt-10 hidden'
|
||||
>
|
||||
We are still working on this page. You can contribute by submitting a
|
||||
brief description and a few links to learn more about this topic <a
|
||||
target='_blank'
|
||||
class='underline text-blue-700'
|
||||
href={githubLink}>on GitHub repository.</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30'>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,11 @@ const { author } = frontmatter;
|
||||
target='_blank'
|
||||
class='font-medium hover:text-gray-600 inline-flex items-center hover:underline'
|
||||
>
|
||||
<img src={author.imageUrl} class='w-5 h-5 inline mr-2 rounded-full' />
|
||||
<img
|
||||
alt={author.name}
|
||||
src={author.imageUrl}
|
||||
class='w-5 h-5 inline mr-2 rounded-full'
|
||||
/>
|
||||
{author.name}
|
||||
</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<div class='sticky top-0 border-b border-b-yellow-300 z-10 flex h-[37px]' id='sticky-youtube-banner'>
|
||||
<a href='https://youtube.com/theroadmap?sub_confirmation=1' target='_blank' class='flex bg-yellow-200 text-center flex-1 items-center justify-center text-sm hover:bg-yellow-300 outline-0 '>
|
||||
<Icon icon="youtube" class="mr-2" /> We now have a YouTube Channel. <span class='hidden sm:inline'>Subscribe for the video content.</span>
|
||||
</a>
|
||||
<button class='text-yellow-500 bg-yellow-200 hover:text-yellow-900 hover:bg-yellow-400 outline-0 px-2' onclick='this.parentElement.classList.add("hidden")' aria-label="Close">
|
||||
<Icon icon="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class='sticky top-0 border-b border-b-yellow-300 z-10 flex h-[37px]'
|
||||
youtube-banner
|
||||
>
|
||||
<a
|
||||
href='https://youtube.com/theroadmap?sub_confirmation=1'
|
||||
target='_blank'
|
||||
class='flex bg-yellow-200 text-center flex-1 items-center justify-center text-sm hover:bg-yellow-300 outline-0'
|
||||
>
|
||||
<Icon icon='youtube' class='mr-2' /> We now have a YouTube Channel. <span
|
||||
class='hidden sm:inline'>Subscribe for the video content.</span
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
class='text-yellow-500 bg-yellow-200 hover:text-yellow-900 hover:bg-yellow-400 outline-0 px-2'
|
||||
aria-label='Close'
|
||||
close-youtube-banner
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[close-youtube-banner]')
|
||||
?.addEventListener('click', () => {
|
||||
document.querySelector('[youtube-banner]').remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
8
src/env.d.ts
vendored
8
src/env.d.ts
vendored
@@ -1 +1,9 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
GITHUB_SHA: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Asymptotic notation is the standard way of measuring the time and space that an algorithm will consume as the input grows. In one of my last guides, I covered "Big-O notation" and a lot of you asked for a similar one for Asymptotic notation. You can find the [previous guide here](/guides/big-o-notation).
|
||||
|
||||
[](/guides/asymptotic-notation.png)
|
||||
[](/guides/asymptotic-notation.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/avoid-render-blocking-javascript-with-async-defer.png)
|
||||
[](/guides/avoid-render-blocking-javascript-with-async-defer.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/basic-authentication.png)
|
||||
[](/guides/basic-authentication.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Big-O notation is the mathematical notation that helps analyse the algorithms to get an idea about how they might perform as the input grows. The image below explains Big-O in a simple way without using any fancy terminology.
|
||||
|
||||
[](/guides/big-o-notation.png)
|
||||
[](/guides/big-o-notation.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/character-encodings.png)
|
||||
[](/guides/character-encodings.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
The image below details the differences between the continuous integration and continuous delivery. Also, here is the [accompanying video on implementing that with GitHub actions](https://www.youtube.com/watch?v=nyKZTKQS_EQ).
|
||||
|
||||
[](/guides/ci-cd.png)
|
||||
[](/guides/ci-cd.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/dhcp.png)
|
||||
[](/guides/dhcp.png)
|
||||
|
||||
|
||||
@@ -24,4 +24,4 @@ DNS or Domain Name System is one of the fundamental blocks of the internet. As a
|
||||
|
||||
DNS at its simplest is like a phonebook on your mobile phone. Whenever you have to call one of your contacts, you can either dial their number from your memory or use their name which will then be used by your mobile phone to search their number in your phone book to call them. Every time you make a new friend, or your existing friend gets a mobile phone, you have to memorize their phone number or save it in your phonebook to be able to call them later on. DNS or Domain Name System, in a similar fashion, is a mechanism that allows you to browse websites on the internet. Just like your mobile phone does not know how to call without knowing the phone number, your browser does not know how to open a website just by the domain name; it needs to know the IP Address for the website to open. You can either type the IP Address to open, or provide the domain name and press enter which will then be used by your browser to find the IP address by going through several hoops. The picture below is the illustration of how your browser finds a website on the internet.
|
||||
|
||||
[](https://i.imgur.com/z9rwm5A.png)
|
||||
[](https://i.imgur.com/z9rwm5A.png)
|
||||
|
||||
@@ -31,7 +31,7 @@ Given the name "Basic Authentication", you should not confuse Basic Authenticati
|
||||
|
||||
Because it is a part of the HTTP specifications, all the browsers have native support for "HTTP Basic Authentication". Given below is the screenshot from the implementation in Google Chrome.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## How does it Work?
|
||||
@@ -52,7 +52,7 @@ The browser might use Realm to cache the credential. In the future, when there i
|
||||
## Step 2
|
||||
Upon receiving the response from the server, the browser will notice the `WWW-Authenticate` header and will show the authentication popup.
|
||||
|
||||

|
||||

|
||||
|
||||
## Step 3
|
||||
After the user submits the credentials through this authentication popup, the browser will automatically encode the credentials using the `base64` encoding and send them in the `Authorization` header of the same request.
|
||||
|
||||
@@ -39,7 +39,7 @@ Before we get into further details, let me give you an overview of the terms tha
|
||||
- **Cache Validation** is the process of contacting the server to check the validity of the cached content and get it updated for when it is going to expire
|
||||
- **Cache Invalidation** is the process of removing any stale content available in the cache
|
||||
|
||||

|
||||

|
||||
|
||||
### Caching Locations
|
||||
|
||||
@@ -75,21 +75,30 @@ Although you can control the reverse proxy caches (since it is implemented by yo
|
||||
|
||||
So, how do we control the web cache? Whenever the server emits some response, it is accompanied by some HTTP headers to guide the caches on whether and how to cache this response. The content provider is the one that has to make sure to return proper HTTP headers to force the caches on how to cache the content.
|
||||
|
||||
- [Expires](#expires)
|
||||
- [Pragma](#pragma)
|
||||
- [Cache-Control](#cache-control)
|
||||
- [private](#private)
|
||||
- [public](#public)
|
||||
- [no-store](#no-store)
|
||||
- [no-cache](#no-cache)
|
||||
- [max-age: seconds](#max-age)
|
||||
- [s-maxage: seconds](#s-maxage)
|
||||
- [must-revalidate](#must-revalidate)
|
||||
- [proxy-revalidate](#proxy-revalidate)
|
||||
- [Mixing Values](#mixing-values)
|
||||
- [Introduction](#introduction)
|
||||
- [Caching Locations](#caching-locations)
|
||||
- [Browser Cache](#browser-cache)
|
||||
- [Proxy Cache](#proxy-cache)
|
||||
- [Reverse Proxy Cache](#reverse-proxy-cache)
|
||||
- [Caching Headers](#caching-headers)
|
||||
- [Expires](#expires)
|
||||
- [Pragma](#pragma)
|
||||
- [Cache-Control](#cache-control)
|
||||
- [private](#private)
|
||||
- [public](#public)
|
||||
- [no-store](#no-store)
|
||||
- [no-cache](#no-cache)
|
||||
- [max-age: seconds](#max-age-seconds)
|
||||
- [s-maxage: seconds](#s-maxage-seconds)
|
||||
- [must-revalidate](#must-revalidate)
|
||||
- [proxy-revalidate](#proxy-revalidate)
|
||||
- [Mixing Values](#mixing-values)
|
||||
- [Validators](#validators)
|
||||
- [ETag](#etag)
|
||||
- [Last-Modified](#last-modified)
|
||||
- [Where do I start?](#where-do-i-start)
|
||||
- [Utilizing Server](#utilizing-server)
|
||||
- [Caching Recommendations](#caching-recommendations)
|
||||
|
||||
#### Expires
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Three-way handshake in its simplest form is that all the `TCP` connections begin
|
||||
|
||||
Once the three-way handshake is completed, the data sharing between the client and server may begin. It should be noted that the client may start sending the application data as soon as it dispatches the last `ACK` packet but the server will still have to wait for the `ACK` packet to be received in order to fulfill the request.
|
||||
|
||||

|
||||

|
||||
|
||||
> Please note that there is a minor issue with the image, the last `ACK` packet sent by the client to end the handshake contains only `y+1` i.e. it should have been `ACK:y+1` instead of `ACK: x+1, y+1`
|
||||
|
||||
@@ -131,7 +131,7 @@ After merely 3 years of `HTTP/1.0`, the next version i.e. `HTTP/1.1` was release
|
||||
- New status codes
|
||||
- ..and more
|
||||
|
||||
I am not going to dwell about all the `HTTP/1.1` features in this post as it is a topic in itself and you can already find a lot about it. The one such document that I would recommend you to read is [Key differences between `HTTP/1.0` and HTTP/1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) and here is the link to [original RFC](https://tools.ietf.org/html/rfc2616) for the overachievers.
|
||||
I am not going to dwell about all the `HTTP/1.1` features in this post as it is a topic in itself and you can already find a lot about it. The one such document that I would recommend you to read is [Key differences between `HTTP/1.0` and HTTP/1.1](https://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) and here is the link to [original RFC](https://tools.ietf.org/html/rfc2616) for the overachievers.
|
||||
|
||||
`HTTP/1.1` was introduced in 1999 and it had been a standard for many years. Although, it improved alot over its predecessor; with the web changing everyday, it started to show its age. Loading a web page these days is more resource-intensive than it ever was. A simple webpage these days has to open more than 30 connections. Well `HTTP/1.1` has persistent connections, then why so many connections? you say! The reason is, in `HTTP/1.1` it can only have one outstanding connection at any moment of time. `HTTP/1.1` tried to fix this by introducing pipelining but it didn't completely address the issue because of the **head-of-line blocking** where a slow or heavy request may block the requests behind and once a request gets stuck in a pipeline, it will have to wait for the next requests to be fulfilled. To overcome these shortcomings of `HTTP/1.1`, the developers started implementing the workarounds, for example use of spritesheets, encoded images in CSS, single humongous CSS/Javascript files, [domain sharding](https://www.maxcdn.com/one/visual-glossary/domain-sharding-2/) etc.
|
||||
|
||||
@@ -162,7 +162,7 @@ By now, you must be convinced that why we needed another revision of the HTTP pr
|
||||
- Request Prioritization
|
||||
- Security
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
#### 1. Binary Protocol
|
||||
@@ -187,7 +187,7 @@ Since `HTTP/2` is now a binary protocol and as I said above that it uses frames
|
||||
|
||||
It was part of a separate RFC which was specifically aimed at optimizing the sent headers. The essence of it is that when we are constantly accessing the server from a same client there is alot of redundant data that we are sending in the headers over and over, and sometimes there might be cookies increasing the headers size which results in bandwidth usage and increased latency. To overcome this, `HTTP/2` introduced header compression.
|
||||
|
||||

|
||||

|
||||
|
||||
Unlike request and response, headers are not compressed in `gzip` or `compress` etc formats but there is a different mechanism in place for header compression which is literal values are encoded using Huffman code and a headers table is maintained by the client and server and both the client and server omit any repetitive headers (e.g. user agent etc) in the subsequent requests and reference them using the headers table maintained by both.
|
||||
|
||||
@@ -210,8 +210,8 @@ Without any priority information, server processes the requests asynchronously i
|
||||
|
||||
There was extensive discussion on whether security (through `TLS`) should be made mandatory for `HTTP/2` or not. In the end, it was decided not to make it mandatory. However, most vendors stated that they will only support `HTTP/2` when it is used over `TLS`. So, although `HTTP/2` doesn't require encryption by specs but it has kind of become mandatory by default anyway. With that out of the way, `HTTP/2` when implemented over `TLS` does impose some requirements i.e. `TLS` version `1.2` or higher must be used, there must be a certain level of minimum key sizes, ephemeral keys are required etc.
|
||||
|
||||
`HTTP/2` is here and it has already [surpassed SPDY in adaption](http://caniuse.com/#search=http2) which is gradually increasing. `HTTP/2` has alot to offer in terms of performance gain and it is about time we should start using it.
|
||||
`HTTP/2` is here and it has already [surpassed SPDY in adaption](https://caniuse.com/#search=http2) which is gradually increasing. `HTTP/2` has alot to offer in terms of performance gain and it is about time we should start using it.
|
||||
|
||||
For anyone interested in further details here is the [link to specs](https:/http2.github.iohttp2-spec) and a link [demonstrating the performance benefits of `HTTP/2`](http://www.http2demo.io/).
|
||||
For anyone interested in further details here is the [link to specs](https:/http2.github.iohttp2-spec) and a link [demonstrating the performance benefits of `HTTP/2`](https://www.http2demo.io/).
|
||||
|
||||
And that about wraps it up. Until next time! stay tuned.
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/jwt-authentication.png)
|
||||
[](/guides/jwt-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/oauth.png)
|
||||
[](/guides/oauth.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Random numbers are everywhere from computer games to lottery systems, graphics software, statistical sampling, computer simulation and cryptography. Graphic below is a quick explanation to how the random numbers are generated and why they may not be truly random.
|
||||
|
||||
[](/guides/random-numbers.png)
|
||||
[](/guides/random-numbers.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
The chart below aims to give you a really basic understanding of how the capability of a DBMS is increased to handle a growing amount of load.
|
||||
|
||||
[](/guides/scaling-databases.svg)
|
||||
[](/guides/scaling-databases.svg)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/session-authentication.png)
|
||||
[](/guides/session-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/ssl-tls-https-ssh.png)
|
||||
[](/guides/ssl-tls-https-ssh.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/sso.png)
|
||||
[](/guides/sso.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/token-authentication.png)
|
||||
[](/guides/token-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/unfamiliar-codebase.png)
|
||||
[](/guides/unfamiliar-codebase.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/web-vitals.png)
|
||||
[](/guides/web-vitals.png)
|
||||
|
||||
|
||||
@@ -81,4 +81,4 @@ Cybersecurity refers to the protective measures against criminal activity accomp
|
||||
|
||||
<iframe class="w-full aspect-video mb-5" width="100%" height="400" src="https://www.youtube.com/embed/AuYNXgO_f3Y" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/).
|
||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture/). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/).
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/sli-slo-sla.jpeg)
|
||||
[](/guides/sli-slo-sla.jpeg)
|
||||
|
||||
|
||||
4
src/icons/down.svg
Normal file
4
src/icons/down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m16.843 10.211c.108-.141.157-.3.157-.456 0-.389-.306-.755-.749-.755h-8.501c-.445 0-.75.367-.75.755 0 .157.05.316.159.457 1.203 1.554 3.252 4.199 4.258 5.498.142.184.36.29.592.29.23 0 .449-.107.591-.291 1.002-1.299 3.044-3.945 4.243-5.498z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -14,6 +14,7 @@ export interface Props {
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
noIndex?: boolean;
|
||||
permalink?: string;
|
||||
sponsor?: SponsorType;
|
||||
}
|
||||
|
||||
@@ -22,8 +23,16 @@ const {
|
||||
description = siteConfig.description,
|
||||
keywords = siteConfig.keywords,
|
||||
noIndex = false,
|
||||
permalink = '',
|
||||
sponsor,
|
||||
} = Astro.props;
|
||||
|
||||
// Remove trailing slashes to consider the page as canonical
|
||||
const currentPageAbsoluteUrl = `https://roadmap.sh${permalink}`;
|
||||
|
||||
const commitUrl = `https://github.com/kamranahmedse/developer-roadmap/commit/${
|
||||
import.meta.env.GITHUB_SHA
|
||||
}`;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -31,6 +40,7 @@ const {
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta name='generator' content={Astro.generator} />
|
||||
<meta name='commit' content={commitUrl} />
|
||||
<title>{title}</title>
|
||||
<meta name='description' content={description} />
|
||||
<meta name='author' content='Kamran Ahmed' />
|
||||
@@ -53,7 +63,9 @@ const {
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content='https://roadmap.sh' />
|
||||
<meta property='og:url' content={currentPageAbsoluteUrl} />
|
||||
|
||||
<link rel='canonical' href={currentPageAbsoluteUrl} />
|
||||
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
|
||||
@@ -2,6 +2,7 @@ const formatter = Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
});
|
||||
|
||||
const defaultStarCount = 224000;
|
||||
let starCount: number | undefined = undefined;
|
||||
|
||||
export async function countStars(
|
||||
@@ -15,10 +16,10 @@ export async function countStars(
|
||||
const repoData = await fetch(`https://api.github.com/repos/${repo}`);
|
||||
const json = await repoData.json();
|
||||
|
||||
starCount = json.stargazers_count * 1;
|
||||
starCount = json.stargazers_count * 1 || defaultStarCount;
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch stars', e);
|
||||
starCount = 224000;
|
||||
starCount = defaultStarCount;
|
||||
}
|
||||
|
||||
return starCount;
|
||||
@@ -27,11 +28,7 @@ export async function countStars(
|
||||
export async function getFormattedStars(
|
||||
repo = 'kamranahmedse/developer-roadmap'
|
||||
): Promise<string> {
|
||||
if (import.meta.env.DEV) {
|
||||
return '224k';
|
||||
}
|
||||
|
||||
const stars = await countStars(repo);
|
||||
const stars = import.meta.env.DEV ? defaultStarCount : await countStars(repo);
|
||||
|
||||
return formatter.format(stars);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Icon from '../components/Icon.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title='Page not found'>
|
||||
<BaseLayout title='Page not found' permalink={'/404/'}>
|
||||
<div class='bg-gray-100'>
|
||||
<div
|
||||
class='py-10 md:py-32 container flex flex-col md:flex-row items-center justify-center gap-7 '
|
||||
|
||||
@@ -15,16 +15,24 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { topicId } = Astro.params;
|
||||
const { file, breadcrumbs, roadmapId, roadmap, heading } = Astro.props as TopicFileType;
|
||||
const { file, breadcrumbs, roadmapId, roadmap, heading } =
|
||||
Astro.props as TopicFileType;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${heading} - roadmap.sh`} description={`Free resources to learn ${heading} in ${roadmap.featuredTitle}. Everything you need to know about ${heading} and how it realtes to ${roadmap.featuredTitle}.`} noIndex={true}>
|
||||
<BaseLayout
|
||||
title={`${heading} - roadmap.sh`}
|
||||
description={`Free resources to learn ${heading} in ${roadmap.featuredTitle}. Everything you need to know about ${heading} and how it realtes to ${roadmap.featuredTitle}.`}
|
||||
noIndex={true}
|
||||
permalink={`/${topicId}/`}
|
||||
>
|
||||
<RoadmapBanner roadmapId={roadmapId} roadmap={roadmap} />
|
||||
<div class="bg-gray-50">
|
||||
<div class='bg-gray-50'>
|
||||
<Breadcrumbs breadcrumbs={breadcrumbs} roadmapId={roadmapId} />
|
||||
|
||||
<div class="container pb-16 prose prose-p:mt-0 prose-h1:mb-4 prose-h2:mb-3 prose-h2:mt-0">
|
||||
<main id="main-content">
|
||||
<div
|
||||
class='container pb-16 prose prose-p:mt-0 prose-h1:mb-4 prose-h2:mb-3 prose-h2:mt-0'
|
||||
>
|
||||
<main id='main-content'>
|
||||
<file.Content />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import CaptchaScripts from '../../components/Captcha/CaptchaScripts.astro';
|
||||
import FAQs from '../../components/FAQs.astro';
|
||||
import InteractiveRoadmap from '../../components/InteractiveRoadmap/InteractiveRoadmap.astro';
|
||||
import MarkdownRoadmap from '../../components/MarkdownRoadmap.astro';
|
||||
import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
@@ -21,10 +22,12 @@ interface Params extends Record<string, string | undefined> {
|
||||
|
||||
const { roadmapId } = Astro.params as Params;
|
||||
const roadmapFile = await import(`../../roadmaps/${roadmapId}/${roadmapId}.md`);
|
||||
const questions = await import (`../../roadmaps/${roadmapId}/faqs.astro`);
|
||||
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
permalink={`/${roadmapId}/`}
|
||||
title={roadmapData?.seo?.title}
|
||||
description={roadmapData.seo.description}
|
||||
keywords={roadmapData.seo.keywords}
|
||||
@@ -52,7 +55,10 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
|
||||
{
|
||||
!roadmapData.isUpcoming && !roadmapData.jsonUrl && (
|
||||
<MarkdownRoadmap>
|
||||
<MarkdownRoadmap
|
||||
roadmapId={roadmapId}
|
||||
description={roadmapData.description}
|
||||
>
|
||||
<roadmapFile.Content />
|
||||
</MarkdownRoadmap>
|
||||
)
|
||||
@@ -60,5 +66,6 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
|
||||
{roadmapData.isUpcoming && <UpcomingRoadmap />}
|
||||
|
||||
<questions.default />
|
||||
<CaptchaScripts slot='after-footer' />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -22,7 +22,12 @@ const roadmapFile = await import(`../../roadmaps/${roadmapId}/${roadmapId}.md`);
|
||||
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${roadmapData.title} Topics`} description={roadmapData.seo.description} keywords={roadmapData.seo.keywords}>
|
||||
<BaseLayout
|
||||
title={`${roadmapData.title} Topics`}
|
||||
description={roadmapData.seo.description}
|
||||
keywords={roadmapData.seo.keywords}
|
||||
permalink={`/${roadmapId}/topics/`}
|
||||
>
|
||||
<RoadmapHeader
|
||||
description={roadmapData.description}
|
||||
title={`${roadmapData.featuredTitle} Topics`}
|
||||
@@ -31,8 +36,8 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
hasTopics={false}
|
||||
/>
|
||||
|
||||
<div class="bg-gray-50 pt-5 pb-8 sm:pt-10 sm:pb-16">
|
||||
<div class="container">
|
||||
<div class='bg-gray-50 pt-5 pb-8 sm:pt-10 sm:pb-16'>
|
||||
<div class='container'>
|
||||
{
|
||||
topics.map((topic) => {
|
||||
// Breadcrumbs have three additional items e.g.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title='About roadmap.sh'>
|
||||
<BaseLayout title='About roadmap.sh' permalink={'/about/'}>
|
||||
<div class='bg-white border-b pt-7 pb-7 sm:pt-12 sm:pb-10'>
|
||||
<div class='container'>
|
||||
<div class='flex items-center'>
|
||||
|
||||
@@ -22,7 +22,11 @@ const { guide } = Astro.props;
|
||||
const { frontmatter: guideData } = guide;
|
||||
---
|
||||
|
||||
<BaseLayout title={guideData.seo.title} description={guideData.seo.description}>
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/guides/${guideId}/`}
|
||||
>
|
||||
<GuideHeader guide={guide} />
|
||||
|
||||
<div class='bg-gray-50 py-5 sm:py-10'>
|
||||
|
||||
@@ -7,7 +7,11 @@ import { getAllGuides } from '../../lib/guide';
|
||||
const guides = await getAllGuides();
|
||||
---
|
||||
|
||||
<BaseLayout title='Guides - roadmap.sh' description={'Detailed guides on Software Engineering Topics'}>
|
||||
<BaseLayout
|
||||
title='Guides - roadmap.sh'
|
||||
description={'Detailed guides on Software Engineering Topics'}
|
||||
permalink={`/guides/`}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title='Guides'
|
||||
description='Succinct graphical explanations to engineering topics.'
|
||||
|
||||
@@ -13,7 +13,7 @@ const guides = await getAllGuides();
|
||||
const videos = await getAllVideos();
|
||||
---
|
||||
|
||||
<BaseLayout title='Developer Roadmaps' description={"Community driven roadmaps, articles and guides for developers to grow in their career."}>
|
||||
<BaseLayout title='Developer Roadmaps - roadmap.sh' description={"Community driven roadmaps, articles and guides for developers to grow in their career."} permalink={'/'}>
|
||||
<div class='bg-gradient-to-b from-slate-900 to-black'>
|
||||
<div class='border-b border-b-slate-900'>
|
||||
<div
|
||||
|
||||
@@ -15,6 +15,7 @@ Here is the list of PDF links for each of the roadmaps.
|
||||
* **QA Roadmap** - [Roadmap Link](https://roadmap.sh/qa) / [PDF Link](https://roadmap.sh/pdfs/qa.pdf)
|
||||
* **ASP.NET Core Roadmap** - [Roadmap Link](https://roadmap.sh/aspnet-core) / [PDF Link](https://roadmap.sh/pdfs/aspnet-core.pdf)
|
||||
* **Flutter Roadmap** - [Roadmap Link](https://roadmap.sh/flutter) / [PDF Link](https://roadmap.sh/pdfs/flutter.pdf)
|
||||
* **Go Roadmap** - [Roadmap Link](https://roadmap.sh/golang) / [PDF Link](https://roadmap.sh/pdfs/golang.pdf)
|
||||
* **Software Architect Roadmap** - [Roadmap Link](https://roadmap.sh/software-architect) / [PDF Link](https://roadmap.sh/pdfs/software-architect.pdf)
|
||||
* **Software Design and Architecture Roadmap** - [Roadmap Link](https://roadmap.sh/software-design-architecture) / [PDF Link](https://roadmap.sh/pdfs/software-design-architecture.pdf)
|
||||
* **JavaScript Roadmap** - [Roadmap Link](https://roadmap.sh/javascript) / [PDF Link](https://roadmap.sh/pdfs/javascript.pdf)
|
||||
@@ -25,6 +26,6 @@ Here is the list of PDF links for each of the roadmaps.
|
||||
* **Vue Roadmap** - [Roadmap Link](https://roadmap.sh/vue) / [PDF Link](https://roadmap.sh/pdfs/vue.pdf)
|
||||
* **Design System Roadmap** - [Roadmap Link](https://roadmap.sh/design-system) / [PDF Link](https://roadmap.sh/pdfs/design-system.pdf)
|
||||
* **Blockchain Roadmap** - [Roadmap Link](https://roadmap.sh/blockchain) / [PDF Link](https://roadmap.sh/pdfs/blockchain.pdf)
|
||||
* **Go Roadmap** - [Roadmap Link](https://roadmap.sh/golang) / [PDF Link](https://roadmap.sh/pdfs/go.pdf)
|
||||
* **Java Roadmap** - [Roadmap Link](https://roadmap.sh/java) / [PDF Link](https://roadmap.sh/pdfs/java.pdf)
|
||||
* **Spring Boot Roadmap** - [Roadmap Link](https://roadmap.sh/spring-boot) / [PDF Link](https://roadmap.sh/pdfs/spring-boot.pdf)
|
||||
* **Python Roadmap** - [Roadmap Link](https://roadmap.sh/python) / [PDF Link](https://roadmap.sh/pdfs/python.pdf)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
layout: ../layouts/MarkdownLayout.astro
|
||||
title: Privacy Policy - roadmap.sh
|
||||
noIndex: true
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
By using or accessing the Services in any manner, you acknowledge that you accept the practices and policies outlined in this Privacy Policy, and you hereby consent that we will collect, use, and share your information in the following ways. Remember that your use of roadmap.sh’s Services is at all times subject to the [Terms of Use](/terms), which incorporates this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to them in the Terms of Use.
|
||||
By using or accessing the Services in any manner, you acknowledge that you accept the practices and policies outlined in this Privacy Policy, and you hereby consent that we will collect, use, and share your information in the following ways. Remember that your use of roadmap.sh’s Services is at all times subject to the [Terms of Use](/terms/), which incorporates this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to them in the Terms of Use.
|
||||
|
||||
## What does this Privacy Policy cover?
|
||||
|
||||
|
||||
@@ -8,16 +8,20 @@ const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
|
||||
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
|
||||
---
|
||||
|
||||
<BaseLayout title="Developer Roadmaps" description={"Step by step guides and paths to learn different tools or technologies"}>
|
||||
<BaseLayout
|
||||
title='Developer Roadmaps'
|
||||
description={'Step by step guides and paths to learn different tools or technologies'}
|
||||
permalink={'/roadmaps/'}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title="Developer Roadmaps"
|
||||
description="Step by step guides and paths to learn different tools or technologies"
|
||||
title='Developer Roadmaps'
|
||||
description='Step by step guides and paths to learn different tools or technologies'
|
||||
showYouTubeAlert={true}
|
||||
/>
|
||||
|
||||
<div class="bg-gray-100 pt-4 pb-14 sm:pt-8 sm:pb-16">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-0.5 sm:gap-3">
|
||||
<div class='bg-gray-100 pt-4 pb-14 sm:pt-8 sm:pb-16'>
|
||||
<div class='container'>
|
||||
<div class='grid grid-cols-1 sm:grid-cols-2 gap-0.5 sm:gap-3'>
|
||||
{
|
||||
roleRoadmaps.map((roleRoadmap) => (
|
||||
<GridRoadmapItem roadmap={roleRoadmap} />
|
||||
|
||||
@@ -7,6 +7,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
<BaseLayout
|
||||
title='Signup - roadmap.sh'
|
||||
description='Register yourself to receive occasional emails about new roadmaps, updates, guides and videos'
|
||||
permalink={'/signup/'}
|
||||
noIndex={true}
|
||||
>
|
||||
<div class='container'>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: ../layouts/MarkdownLayout.astro
|
||||
title: Terms and Conditions - roadmap.sh
|
||||
noIndex: true
|
||||
---
|
||||
|
||||
# Terms of Service
|
||||
@@ -21,7 +22,7 @@ Except for changes by us as described here, no other amendment or modification o
|
||||
|
||||
## Do these terms cover privacy?
|
||||
|
||||
You can view the current roadmap.sh [Privacy Policy here](/privacy).
|
||||
You can view the current roadmap.sh [Privacy Policy here](/privacy/).
|
||||
|
||||
The Children’s Online Privacy Protection Act (“COPPA”) requires that online service providers obtain parental consent before they knowingly collect personally identifiable information online from children who are under 13. We do not knowingly collect or solicit personally identifiable information from children under 13. If you are a child under 13, please do not attempt to register for the Services or send any personal information about yourself to us. If we learn we have collected personal information from a child under 13, we will delete that information as quickly as possible. If you believe that a child under 13 may have provided us personal information, please contact us at kamranahmed.se@gmail.com.
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const { video } = Astro.props;
|
||||
<BaseLayout
|
||||
title={video.frontmatter.title}
|
||||
description={video.frontmatter.description}
|
||||
permalink={`/videos/${videoId}/`}
|
||||
>
|
||||
<VideoHeader video={video} />
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const videos = await getAllVideos();
|
||||
<BaseLayout
|
||||
title='Illustrated Videos - roadmap.sh'
|
||||
description={'Graphical video demonstrations on software engineering topics.'}
|
||||
permalink={`/videos/`}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title='Videos'
|
||||
|
||||
@@ -119,7 +119,7 @@ I would highly recommend watching [this free course](https://www.udacity.com/cou
|
||||
* [Developing Android Apps with Kotlin](https://www.udacity.com/course/developing-android-apps-with-kotlin--ud9012)
|
||||
* [Android Basics in Kotlin](https://developer.android.com/courses/android-basics-kotlin/course)
|
||||
* [Android Developer Guides](https://developer.android.com/guide)
|
||||
* [Raywenderlich](https://www.raywenderlich.com)
|
||||
* [Kodeco](https://www.kodeco.com)
|
||||
|
||||
## Wrap Up
|
||||
|
||||
|
||||
0
src/roadmaps/android/faqs.astro
Normal file
0
src/roadmaps/android/faqs.astro
Normal file
@@ -4,5 +4,5 @@ A schematic is a template-based code generator that supports complex logic. It i
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Angular Website](https://angular.io/guide/schematics#:~:text=A%20schematic%20is%20a%20template,collections%20and%20installed%20with%20npm.)
|
||||
- [Angular Website](https://angular.io/guide/schematics)
|
||||
- [Angular Blog](https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2?gi=ad9571373944)
|
||||
|
||||
@@ -4,4 +4,4 @@ Angular processes all data bindings once for each JavaScript event cycle, from t
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Angular.io Website](https://angular.io/guide/architecture-components#:~:text=Angular%20processes%20all%20data%20bindings,between%20parent%20and%20child%20components.)
|
||||
- [Angular.io Website](https://angular.io/guide/architecture-components)
|
||||
|
||||
0
src/roadmaps/angular/faqs.astro
Normal file
0
src/roadmaps/angular/faqs.astro
Normal file
@@ -5,4 +5,4 @@ C# is a modern coding language that was developed by Microsoft that focuses on a
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [C# official website?](https://learn.microsoft.com/en-us/dotnet/csharp//)
|
||||
- [The Beginners Guide to C#](https://www.w3schools.com/CS/index.php)
|
||||
- [The Beginners Guide to C#](https://www.w3schools.com/CS/index.php)
|
||||
@@ -1 +1,9 @@
|
||||
# Dotnet
|
||||
# .NET Framework
|
||||
|
||||
.NET (pronounced "dot net") is a software framework developed by Microsoft that can be used to create a wide range of applications, including Windows desktop and web applications, mobile apps, and gaming. The .NET Framework provides a large library of pre-built functionality, including collections, file input/output, and networking, that can be used by .NET applications. It also includes a Common Language Runtime (CLR) which manages the execution of code, providing features such as memory management, security, and exception handling.
|
||||
|
||||
For more information, visit the following link:
|
||||
|
||||
- [What is .NET?](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet)
|
||||
- [Intro to .NET](https://www.codecademy.com/article/what-is-net)
|
||||
- [An Overview of .NET](https://auth0.com/blog/what-is-dotnet-platform-overview/)
|
||||
@@ -5,3 +5,9 @@ C# (pronounced "C-sharp") is a general-purpose, object-oriented programming lang
|
||||
C# is a statically-typed language, which means that the type of a variable must be specified when it is declared, and that the type of a value cannot be changed after it has been assigned. C# also supports object-oriented programming, which means that it provides features such as encapsulation, inheritance, and polymorphism.
|
||||
|
||||
C# is a popular language for building .NET applications, and it is used by many large companies and organizations, including Microsoft, Dell, and IBM. It is a versatile language that can be used for a wide range of purposes, and it is well-suited for building scalable and maintainable software systems.
|
||||
|
||||
Visit the following links for more information:
|
||||
|
||||
- [C Sharp Basics](https://www.codecademy.com/catalog/language/c-sharp)
|
||||
- [Introduction to C#](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/tutorials/)
|
||||
- [Basics Of C#](https://www.c-sharpcorner.com/UploadFile/e9fdcd/basics-of-C-Sharp/)
|
||||
@@ -7,4 +7,4 @@ Visit the following resources to learn more:
|
||||
- [Learn Git on the command line](https://github.com/jlord/git-it-electron)
|
||||
- [Version Control System Introduction](https://www.youtube.com/watch?v=zbKdDsNNOhg)
|
||||
- [Git & GitHub Crash Course For Beginners](https://www.youtube.com/watch?v=SWYqp7iY_Tc)
|
||||
- [Learn Git in 20 Minutes](https://youtu.be/Y9XZQO1n_7c?t=21)
|
||||
- [Learn Git in 20 Minutes](https://youtu.be/Y9XZQO1n_7c?t=21)
|
||||
@@ -1 +1,16 @@
|
||||
# Azure devops services
|
||||
# Azure Devops Services
|
||||
|
||||
Azure DevOps Services is a collection of services provided by Microsoft that can be used to plan, build, test, and deploy .NET applications. These services can be used together or independently to support various aspects of the software development process. Some of the main services include:
|
||||
|
||||
- Azure Boards: Provides features for agile planning and tracking, such as backlogs, boards, and sprint planning.
|
||||
- Azure Repos: Provides source control management for Git and Team Foundation Version Control (TFVC) repositories.
|
||||
- Azure Artifacts: Provides package management for NuGet, npm, and Maven packages.
|
||||
- Azure Test Plans: Provides support for manual and exploratory testing, as well as continuous testing and testing in production.
|
||||
- Azure Pipeline: Provides a way to build, test, and deploy code automatically, with support for multiple languages and platforms, including .NET.
|
||||
|
||||
These services can be used to create a full-featured development environment that can be used to manage all aspects of a software development project, from planning and design to testing and deployment.
|
||||
|
||||
For more information, visit the following links:
|
||||
|
||||
- [Build, test, and deploy .NET Core apps](https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops&tabs=dotnetfive)
|
||||
- [Microsoft Azure DevOps for ASP .NET Core Web apps](https://techmindfactory.com/Microsoft-Azure-DevOps-for-ASP-.NET-Core-Web-apps/)
|
||||
@@ -5,7 +5,7 @@ As the name indicates, a **Data Structure** is a way of organizing the data in t
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [What are Data Structures?](https://www.geeksforgeeks.org/data-structures)
|
||||
- [ Data Structures and Algorithms](https://www.javatpoint.com/data-structure-tutorial)
|
||||
- [Data Structures and Algorithms](https://www.javatpoint.com/data-structure-tutorial)
|
||||
- [Data Structures Illustrated](https://www.youtube.com/watch?v=9rhT3P1MDHk&list=PLkZYeFmDuaN2-KUIv-mvbjfKszIGJ4FaY)
|
||||
- [C# resources](https://dev.to/adavidoaiei/fundamental-data-structures-and-algorithms-in-c-4ocf)
|
||||
- [Interview Questions about Data Structures](https://www.csharpstar.com/csharp-algorithms/)
|
||||
|
||||
@@ -1 +1,24 @@
|
||||
# General development skills
|
||||
# General development skills
|
||||
|
||||
There are several skills that are generally considered to be important for working with .NET and C#:
|
||||
|
||||
- Object-oriented programming: Understanding the concepts of classes, objects, inheritance, and polymorphism is essential for working with C# and the .NET Framework.
|
||||
|
||||
- C# language: A strong understanding of the C# language, including its syntax, keywords, and built-in classes and types, is necessary for writing efficient and maintainable code.
|
||||
|
||||
- .NET Framework: Familiarity with the .NET Framework, including the Common Language Runtime (CLR) and the Base Class Library (BCL), is important for understanding how C# code is executed and for utilizing the framework's many built-in features.
|
||||
|
||||
- Web & Software development: Knowledge of web development technologies such as HTML, CSS, JavaScript, and ASP.NET is important for creating web applications using C# and the .NET Framework and knowledge of software development methodologies such as Agile, Scrum, or Waterfall is also useful.
|
||||
|
||||
- Database: Familiarity with database concepts and technologies, such as SQL and ADO.NET, is important for working with data in C# applications.
|
||||
|
||||
- Cloud computing: Familiarity with cloud computing concepts and technologies, such as Azure, is becoming increasingly important for deploying and scaling C# applications.
|
||||
|
||||
- DevOps: Understanding of DevOps concepts and practices, such as continuous integration and continuous deployment, is necessary for automating and streamlining the software development process.
|
||||
|
||||
For more information, visit the following links:
|
||||
|
||||
- [A Step-by-Step Approach to Learn OOP](https://www.geeksforgeeks.org/a-step-by-step-approach-to-learn-object-oriented-programming/)
|
||||
- [Asp.net - Complete Tutorial](https://www.youtube.com/watch?v=kdPtNMb8tPw)
|
||||
- [Learn Cloud Computing](https://www.youtube.com/watch?v=eWwK2FKWp0g)
|
||||
- [DevOps Course for Beginners](https://www.youtube.com/watch?v=hQcFE0RD0cQ)
|
||||
@@ -5,3 +5,5 @@ A stored procedure is a pre-compiled collection of SQL statements that can be ex
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Stored Procedure Tutorial](https://www.w3schools.com/sql/sql_stored_procedures.asp)
|
||||
- [Stored Procedure in SQL: Benefits And How to Create It](https://www.simplilearn.com/tutorials/sql-tutorial/stored-procedure-in-sql)
|
||||
- [SQL Server stored procedures for beginners](https://www.sqlshack.com/sql-server-stored-procedures-for-beginners/)
|
||||
@@ -5,3 +5,5 @@ Database constraints are rules that are used to limit the data that can be store
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [SQL Constraints](https://www.w3schools.com/sql/sql_constraints.asp)
|
||||
- [Constraints of SQL](https://www.educative.io/courses/database-design-fundamentals/m7JnY9Xm6Qp)
|
||||
- [Constraints in DBMS](https://beginnersbook.com/2015/04/constraints-in-dbms/)
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
# Triggers
|
||||
# Triggers
|
||||
|
||||
Triggers are special type of stored procedures that are automatically executed in response to specific events that occur within a database. These events can include:
|
||||
|
||||
- Data modification events (INSERT, UPDATE, DELETE) on a specific table or view.
|
||||
- Data definition events (CREATE, ALTER, DROP) on specific database objects such as tables or views.
|
||||
- Logon events (CONNECT, DISCONNECT) that occur when a user connects to or disconnects from the database.
|
||||
|
||||
For more information, visit the following links:
|
||||
|
||||
- [Database Triggers](https://docs.oracle.com/cd/A57673_01/DOC/server/doc/SCN73/ch15.htm)
|
||||
- [Database Triggers: Examples & Overview](https://study.com/academy/lesson/database-triggers-examples-overview.html)
|
||||
- [What are Triggers in SQL?](https://www.edureka.co/blog/triggers-in-sql/)
|
||||
- [What is a SQL Trigger?](https://www.essentialsql.com/sql-trigger/)
|
||||
@@ -9,3 +9,5 @@ MVC is an architectural design pattern used for developing applications, specifi
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [MVC Official Documentation](https://learn.microsoft.com/en-us/aspnet/core/mvc/overview?WT.mc_id=dotnet-35129-website&view=aspnetcore-7.0)
|
||||
- [ASP.NET MVC Architecture](https://www.tutorialsteacher.com/mvc/mvc-architecture)
|
||||
- [MVC Framework - Introduction](https://www.tutorialspoint.com/mvc_framework/mvc_framework_introduction.htm)
|
||||
@@ -1 +1,9 @@
|
||||
# Rest
|
||||
# REST
|
||||
|
||||
REST (Representational State Transfer) is an architectural style for building web services. In the context of .NET, RESTful web services can be created using the ASP.NET Web API framework, which allows developers to create HTTP-based services that can be consumed by a wide range of clients, including web browsers and mobile devices. The Web API framework provides a set of tools and libraries for creating RESTful services, including routing, request/response handling, and support for a variety of data formats, such as JSON and XML.
|
||||
|
||||
For more information, visit the following resources:
|
||||
|
||||
- [What is REST Services?](http://www.codedigest.com/quick-start/16/what-is-rest-services-how-to-create-rest-services-in-aspnet)
|
||||
- [Restful API In ASP.NET: Introduction of REST & Web API](https://www.c-sharpcorner.com/UploadFile/4b0136/restful-api-in-Asp-Net-introduction-of-rest-web-api/)
|
||||
- [What are RESTful APIs](https://www.pragimtech.com/blog/blazor/what-are-restful-apis/)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user