Compare commits
284 Commits
content/gr
...
fix/guide-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e8e5e19b | ||
|
|
9893e9f0a3 | ||
|
|
caf1cd0269 | ||
|
|
d21c1f6d0d | ||
|
|
9d38cf7650 | ||
|
|
d232d3bbd0 | ||
|
|
366d893df9 | ||
|
|
ff27561765 | ||
|
|
133642e05f | ||
|
|
7434ff71eb | ||
|
|
d081ecf5b3 | ||
|
|
d8a039690b | ||
|
|
56f0df549d | ||
|
|
b042161e29 | ||
|
|
db273210fd | ||
|
|
9370e262c0 | ||
|
|
e0f9bc8456 | ||
|
|
af211ab129 | ||
|
|
eab1fb31b2 | ||
|
|
c36fd71ec1 | ||
|
|
7d0e35d7ae | ||
|
|
59d881a77b | ||
|
|
b57e3ecc75 | ||
|
|
cd5c0c10a2 | ||
|
|
10a5e4c0ae | ||
|
|
432983631d | ||
|
|
29189062b9 | ||
|
|
84138d5049 | ||
|
|
c28ac4b078 | ||
|
|
66bdbd7458 | ||
|
|
907f820778 | ||
|
|
ec0a8a99ef | ||
|
|
a7a342c8e7 | ||
|
|
e0e26580fa | ||
|
|
f9d96d415f | ||
|
|
e1c22932be | ||
|
|
9dae1b3595 | ||
|
|
cdb642c8d4 | ||
|
|
71b43af862 | ||
|
|
69dccb3fcc | ||
|
|
ca6ddb4654 | ||
|
|
0ac6fc70ff | ||
|
|
5ddb021898 | ||
|
|
d227603a59 | ||
|
|
e0d70950ac | ||
|
|
148bfd8736 | ||
|
|
648985cefc | ||
|
|
9321ac6aa1 | ||
|
|
170ab3a6cf | ||
|
|
708fa31998 | ||
|
|
f83a1a6c3b | ||
|
|
dc1d7ef226 | ||
|
|
808bd40cce | ||
|
|
45ce59b10d | ||
|
|
e3f41ec0e3 | ||
|
|
4f821d0f1d | ||
|
|
ec1283a5dc | ||
|
|
4da909d358 | ||
|
|
0beb9ad239 | ||
|
|
c6213dde92 | ||
|
|
3d655965f6 | ||
|
|
c5c2ee3b2c | ||
|
|
cad0813eb6 | ||
|
|
f9c1e6e0a2 | ||
|
|
d3578756d4 | ||
|
|
5fe506324a | ||
|
|
bc007dcc9b | ||
|
|
42a5d5bba6 | ||
|
|
f908c5371d | ||
|
|
78964b9f65 | ||
|
|
9f11de60ed | ||
|
|
4ba28b702b | ||
|
|
45c6fc873f | ||
|
|
23b7c21502 | ||
|
|
d474d07ebb | ||
|
|
7d08572d78 | ||
|
|
17fc85f893 | ||
|
|
5ccba8d7c0 | ||
|
|
fefbb4f833 | ||
|
|
bcbc9c9d54 | ||
|
|
73d1d0e389 | ||
|
|
14856560c3 | ||
|
|
b097395c07 | ||
|
|
929be729e5 | ||
|
|
1eb8fab15e | ||
|
|
51233c8011 | ||
|
|
0081e9059c | ||
|
|
4e66148777 | ||
|
|
18e430be0b | ||
|
|
e39d0d93e0 | ||
|
|
9260dc36b5 | ||
|
|
f5f846ed73 | ||
|
|
7518d60013 | ||
|
|
59d9674d75 | ||
|
|
16fb03086e | ||
|
|
c0f46c5eed | ||
|
|
b5b8b92791 | ||
|
|
8f90dac32e | ||
|
|
e9f3a616d1 | ||
|
|
c28ed87247 | ||
|
|
53fb6313db | ||
|
|
cc933b238d | ||
|
|
48fba932b4 | ||
|
|
9961259ffb | ||
|
|
cab2054c1d | ||
|
|
d34affb420 | ||
|
|
ee4f0980bc | ||
|
|
37fdd010a8 | ||
|
|
aa04c51a12 | ||
|
|
7993f12d12 | ||
|
|
1a3265295c | ||
|
|
238245431b | ||
|
|
48c04055d5 | ||
|
|
596b8f56ac | ||
|
|
45267693e2 | ||
|
|
f932df8627 | ||
|
|
8dcf4b00c4 | ||
|
|
cb32a9610d | ||
|
|
01c090f62d | ||
|
|
60b1edcab9 | ||
|
|
d08887060f | ||
|
|
24a6c4930e | ||
|
|
e57b889f73 | ||
|
|
c5d14d2543 | ||
|
|
4f0b08ea93 | ||
|
|
47e2dbdd12 | ||
|
|
f1ad70acd9 | ||
|
|
ac230bbf29 | ||
|
|
d0861711ac | ||
|
|
74b2dda7f7 | ||
|
|
2b49fa3182 | ||
|
|
e2a8240e35 | ||
|
|
a7f45c0af1 | ||
|
|
77a6270bd7 | ||
|
|
64d3ad662c | ||
|
|
c8b8e12b64 | ||
|
|
8f94a5887e | ||
|
|
00b6217e63 | ||
|
|
1a0d7463eb | ||
|
|
983ee44632 | ||
|
|
f393cb186e | ||
|
|
70edfb0ac2 | ||
|
|
ed07d34d64 | ||
|
|
831521ae10 | ||
|
|
e29289f0dc | ||
|
|
0fd3eb0cc6 | ||
|
|
f58a77010b | ||
|
|
6303e31c0e | ||
|
|
dfc2d39427 | ||
|
|
5e75026424 | ||
|
|
7a4c077a90 | ||
|
|
e45c49a404 | ||
|
|
b6a0255f12 | ||
|
|
b741a0e1ee | ||
|
|
8200993af4 | ||
|
|
5c1d803892 | ||
|
|
dcf0f94af9 | ||
|
|
4ad8886aa0 | ||
|
|
a1143cd6cb | ||
|
|
f130c706da | ||
|
|
8068face54 | ||
|
|
39866117a6 | ||
|
|
df7aa17f86 | ||
|
|
ee6572660b | ||
|
|
9875a2d6f7 | ||
|
|
5b180e2597 | ||
|
|
6f05972493 | ||
|
|
4bd182e4d0 | ||
|
|
68d319cacb | ||
|
|
3e76df8d2a | ||
|
|
9d69477947 | ||
|
|
e0ead47fb1 | ||
|
|
253c88542f | ||
|
|
5bca9834fb | ||
|
|
dde6e3d3df | ||
|
|
6fe8fee25f | ||
|
|
f3622a1b1c | ||
|
|
a92bda38f4 | ||
|
|
b194d167be | ||
|
|
ec04b582a6 | ||
|
|
f55159a12b | ||
|
|
938c7796d1 | ||
|
|
e04bd9db05 | ||
|
|
7c837d14da | ||
|
|
cc05587d9e | ||
|
|
2172014d6e | ||
|
|
98d43e76b7 | ||
|
|
7665970813 | ||
|
|
d96e5890b9 | ||
|
|
659bd93094 | ||
|
|
a4dddfb19b | ||
|
|
12a4be2227 | ||
|
|
9edcb35acb | ||
|
|
1df4e4b836 | ||
|
|
49e78cf1c0 | ||
|
|
a4a29b4efa | ||
|
|
3e49e7f91d | ||
|
|
7627bc73b5 | ||
|
|
26eaa40dc1 | ||
|
|
45a0b53d5f | ||
|
|
7bac3c3444 | ||
|
|
62905bda7a | ||
|
|
179bf366cc | ||
|
|
59d47c5b1e | ||
|
|
d23ea8e577 | ||
|
|
07f001f8be | ||
|
|
754a91acef | ||
|
|
16c550211b | ||
|
|
a56710c43d | ||
|
|
00f94e031e | ||
|
|
d1556c85df | ||
|
|
1885d6d304 | ||
|
|
3b8c8316b3 | ||
|
|
034fd16a1f | ||
|
|
aa9bf2f263 | ||
|
|
6a5df98f4f | ||
|
|
ea02c8835a | ||
|
|
e13733a503 | ||
|
|
6f0ad58764 | ||
|
|
f68c303ffa | ||
|
|
b2c79ff395 | ||
|
|
ff16ea542f | ||
|
|
e3adcdaba4 | ||
|
|
6783d7ea44 | ||
|
|
f06dfce7fb | ||
|
|
3df8db5fa5 | ||
|
|
5c92cdedd8 | ||
|
|
07b6d067c4 | ||
|
|
a7f9e7d735 | ||
|
|
3521525611 | ||
|
|
43260ff14f | ||
|
|
102ccc6a6b | ||
|
|
415dc2d8e8 | ||
|
|
e0e6168cfe | ||
|
|
dd7c0ec003 | ||
|
|
190c75cebe | ||
|
|
813a3d9b2b | ||
|
|
c2dda3bc35 | ||
|
|
4711ab9a6f | ||
|
|
5f2836a148 | ||
|
|
badb2c029d | ||
|
|
8a24b3e695 | ||
|
|
8b3f8ee6b8 | ||
|
|
f9db9bee95 | ||
|
|
e8d2bd00c6 | ||
|
|
f4e505113c | ||
|
|
f675f08d83 | ||
|
|
a12ec64af5 | ||
|
|
24512374e8 | ||
|
|
359f5d6a4d | ||
|
|
c7302d7484 | ||
|
|
6ab477df8d | ||
|
|
961d00e70e | ||
|
|
c1a53cf3cc | ||
|
|
1f485c21f7 | ||
|
|
e886d0bacb | ||
|
|
8a07f2f685 | ||
|
|
19ad916334 | ||
|
|
b30016b6f4 | ||
|
|
57395f769a | ||
|
|
b91c11b273 | ||
|
|
c026f9c928 | ||
|
|
aee51ee43e | ||
|
|
3b12130579 | ||
|
|
3dd9429338 | ||
|
|
0af54cd906 | ||
|
|
750e6e5a36 | ||
|
|
5b93bc42db | ||
|
|
8b32a3a831 | ||
|
|
a28204c908 | ||
|
|
4aca07e3d4 | ||
|
|
5c2562dadb | ||
|
|
e934dc60f4 | ||
|
|
ad4f35764d | ||
|
|
a715a85b46 | ||
|
|
f16a207e7c | ||
|
|
6582d65935 | ||
|
|
ab36350cdc | ||
|
|
3b05a615d8 | ||
|
|
9a2bc75646 | ||
|
|
d283ce7c67 | ||
|
|
59ed243fa7 | ||
|
|
ca35551e4f | ||
|
|
cab06b46da |
21
.github/workflows/aws-costs.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Sends Daily AWS Costs to Slack
|
||||
on:
|
||||
# Allow manual Run
|
||||
workflow_dispatch:
|
||||
# Run at 7:00 UTC every day
|
||||
schedule:
|
||||
- cron: "0 7 * * *"
|
||||
jobs:
|
||||
aws_costs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Costs
|
||||
env:
|
||||
AWS_KEY: ${{ secrets.COST_AWS_ACCESS_KEY }}
|
||||
AWS_SECRET: ${{ secrets.COST_AWS_SECRET_KEY }}
|
||||
AWS_REGION: ${{ secrets.COST_AWS_REGION }}
|
||||
SLACK_CHANNEL: ${{ secrets.SLACK_COST_CHANNEL }}
|
||||
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
|
||||
run: |
|
||||
npm install -g aws-cost-cli
|
||||
aws-cost -k $AWS_KEY -s $AWS_SECRET -r $AWS_REGION -S $SLACK_TOKEN -C $SLACK_CHANNEL
|
||||
43
.github/workflows/update-sponsors.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update Sponsors
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allow manual run
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # run daily at 00:00 UTC
|
||||
|
||||
env:
|
||||
SPONSOR_SHEET_API_KEY: ${{ secrets.SPONSOR_SHEET_API_KEY }}
|
||||
SPONSOR_SHEET_ID: ${{ secrets.SPONSOR_SHEET_ID }}
|
||||
|
||||
jobs:
|
||||
update-sponsors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7.13.4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Update sponsors
|
||||
run: |
|
||||
node bin/update-sponsors.cjs
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
delete-branch: false
|
||||
branch: 'update-sponsors'
|
||||
base: 'master'
|
||||
labels: |
|
||||
sponsors
|
||||
automated pr
|
||||
reviewers: kamranahmedse
|
||||
commit-message: 'chore: update sponsors'
|
||||
title: 'Update Sponsor Banners'
|
||||
body: |
|
||||
Updates sponsor banners.
|
||||
Please review the changes and merge if everything looks good.
|
||||
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
app-dist
|
||||
dist
|
||||
.idea
|
||||
.github
|
||||
public
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
18
.prettierrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
options: {
|
||||
parser: 'astro',
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-astro'),
|
||||
require('prettier-plugin-tailwindcss'),
|
||||
],
|
||||
};
|
||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,41 @@ import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
site: 'https://roadmap.sh/',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula'
|
||||
theme: 'dracula',
|
||||
},
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
{
|
||||
target: '_blank',
|
||||
rel: function (element) {
|
||||
const href = element.properties.href;
|
||||
const whiteListedStarts = [
|
||||
'/',
|
||||
'#',
|
||||
'mailto:',
|
||||
'https://github.com/kamranahmedse',
|
||||
'https://thenewstack.io',
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
];
|
||||
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return 'noopener noreferrer nofollow';
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
build: {
|
||||
format: 'file',
|
||||
},
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
|
||||
155
bin/best-practice-dirs.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
const allowedBestPracticeId = fs.readdirSync(BEST_PRACTICE_CONTENT_DIR);
|
||||
if (!bestPracticeId) {
|
||||
console.error('bestPractice is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedBestPracticeId.includes(bestPracticeId)) {
|
||||
console.error(`Invalid best practice key ${bestPracticeId}`);
|
||||
console.error(`Allowed keys are ${allowedBestPracticeId.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the best parctice content files
|
||||
const bestPracticeDirName = fs
|
||||
.readdirSync(BEST_PRACTICE_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === bestPracticeId);
|
||||
|
||||
if (!bestPracticeDirName) {
|
||||
console.error('Best practice directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bestPracticeDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName
|
||||
);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(
|
||||
`Best Practice content already exists @ ${bestPracticeContentDirPath}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
|
||||
// No directory for a group without control name
|
||||
if (
|
||||
!controlName ||
|
||||
controlName.startsWith('check:') ||
|
||||
controlName.startsWith('ext_link:')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlName.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/best-practices/${bestPracticeId}`
|
||||
));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
const dirTree = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(bestPracticeContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
// 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], filePaths);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(bestPracticeContentDirPath, dirTree);
|
||||
console.log('Created best practice content directory structure');
|
||||
37
bin/readme.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## CLI Tools
|
||||
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
|
||||
Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
## `compress-jsons.cjs`
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `update-sponsors.cjs`
|
||||
|
||||
Updates the sponsor ads on each roadmap page with the latest sponsor information in the Excel sheet.
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
Currently, for any new roadmaps that we add, we do create the interactive roadmap but we end up leaving the content empty in the roadmap till we get time to fill it up manually.
|
||||
|
||||
This script populates all the content files with some minimal content from OpenAI so that the users visiting the website have something to read in the interactive roadmap till we get time to fill it up manually.
|
||||
|
||||
## `roadmap-dirs.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-dirs [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
|
||||
- Remove all the groups from the roadmaps through the project editor. Select all and press `cmd+shift+g`
|
||||
- Identify the boxes that should be clickable and group them together with `cmd+shift+g`
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
@@ -1,12 +1,13 @@
|
||||
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 OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY;
|
||||
const ALL_ROADMAPS_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const ROADMAP_JSON_DIR = path.join(__dirname, '../public/jsons/roadmaps');
|
||||
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
const allowedRoadmapIds = fs.readdirSync(ALL_ROADMAPS_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
@@ -18,146 +19,144 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
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/roadmaps/${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);
|
||||
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
/**
|
||||
* @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;
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
// @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+-/, ':');
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(folderPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
if (stats.isDirectory()) {
|
||||
getFilesInFolder(filePath, fileList);
|
||||
} else if (stats.isFile()) {
|
||||
const fileUrl = filePath
|
||||
.replace(ROADMAP_CONTENT_DIR, '') // Remove the content folder
|
||||
.replace(/\/\d+-/g, '/') // Remove ordering info `/101-ecosystem`
|
||||
.replace(/\/index\.md$/, '') // Make the `/index.md` to become the parent folder only
|
||||
.replace(/\.md$/, ''); // Remove `.md` from the end of file
|
||||
|
||||
// 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
|
||||
);
|
||||
fileList[fileUrl] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
return fileList;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
function writeTopicContent(currTopicUrl) {
|
||||
const [parentTopic, childTopic] = currTopicUrl
|
||||
.replace(/^\d+-/g, '/')
|
||||
.replace(/:/g, '/')
|
||||
.replace(/^\//, '')
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.map((topic) => topic.replace(/-/g, ' '));
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
}
|
||||
|
||||
console.log(`Genearting '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai
|
||||
.createChatCompletion({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.data.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
|
||||
|
||||
const roadmapJson = require(path.join(ROADMAP_JSON_DIR, `${roadmapId}.json`));
|
||||
const groups = roadmapJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
!control.properties?.controlName?.startsWith('ext_link')
|
||||
);
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log('----------------------------------------');
|
||||
console.log('OPEN_AI_API_KEY not found. Skipping openai api calls...');
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
|
||||
for (let group of groups) {
|
||||
const topicId = group?.properties?.controlName;
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = topicId?.replace(/^\d+-/g, '/')?.replace(/:/g, '/');
|
||||
if (!currTopicUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentFilePath = topicUrlToPathMapping[currTopicUrl];
|
||||
|
||||
if (!contentFilePath) {
|
||||
console.log(`Missing file for: ${currTopicUrl}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFileContent = fs.readFileSync(contentFilePath, 'utf8');
|
||||
const isFileEmpty = currentFileContent.replace(/^#.+/, ``).trim() === '';
|
||||
|
||||
if (!isFileEmpty) {
|
||||
console.log(`Ignoring ${topicId}. Not empty.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let newFileContent = `# ${topicTitle}`;
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
continue;
|
||||
}
|
||||
|
||||
const topicContent = await writeTopicContent(currTopicUrl);
|
||||
newFileContent += `\n\n${topicContent}`;
|
||||
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
|
||||
// console.log(currentFileContent);
|
||||
// console.log(currTopicUrl);
|
||||
// console.log(topicTitle);
|
||||
// console.log(topicUrlToPathMapping[currTopicUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
console.log('Done');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
166
bin/roadmap-dirs.cjs
Normal file
@@ -0,0 +1,166 @@
|
||||
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/data/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/roadmaps/${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');
|
||||
@@ -6,7 +6,7 @@ if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
const fullPath = path.join(__dirname, `../src/data/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
|
||||
171
bin/update-sponsors.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const apiKey = process.env.SPONSOR_SHEET_API_KEY;
|
||||
const sheetId = process.env.SPONSOR_SHEET_ID;
|
||||
|
||||
if (!apiKey || !sheetId) {
|
||||
console.error('Missing API key or sheet ID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sheetRange = 'A3:I1001';
|
||||
const sheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${sheetRange}?key=${apiKey}`;
|
||||
|
||||
function removeAllSponsors(baseContentDir) {
|
||||
console.log('------------------------');
|
||||
console.log('Removing sponsors from: ', baseContentDir);
|
||||
console.log('------------------------');
|
||||
const dataDirPath = path.join(__dirname, '../src/data');
|
||||
const contentDirPath = path.join(dataDirPath, baseContentDir);
|
||||
|
||||
const contentDir = fs.readdirSync(contentDirPath);
|
||||
contentDir.forEach((content) => {
|
||||
console.log('Removing sponsor from: ', content);
|
||||
|
||||
const pageFilePath = path.join(contentDirPath, content, `${content}.md`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
});
|
||||
}
|
||||
|
||||
function addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
}) {
|
||||
const urlPart = pageUrl
|
||||
.replace('https://roadmap.sh/', '')
|
||||
.replace(/\?.+?$/, '');
|
||||
|
||||
const parentDir = urlPart.startsWith('best-practices/')
|
||||
? 'best-practices'
|
||||
: 'roadmaps';
|
||||
const pageId = urlPart.replace(`${parentDir}/`, '');
|
||||
|
||||
const pageFilePath = path.join(
|
||||
__dirname,
|
||||
`../src/data/${parentDir}`,
|
||||
`${pageId}/${pageId}.md`
|
||||
);
|
||||
|
||||
if (!fs.existsSync(pageFilePath)) {
|
||||
console.error(`Page file not found: ${pageFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updating page: ${urlPart}`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const frontmatterValues = Object.entries(frontmatterObj);
|
||||
const roadmapLabel = frontmatterObj.briefTitle;
|
||||
|
||||
// Insert sponsor data at 10 index i.e. after
|
||||
// roadmap dimensions in the frontmatter
|
||||
frontmatterValues.splice(10, 0, [
|
||||
'sponsor',
|
||||
{
|
||||
url: redirectUrl,
|
||||
title: adTitle,
|
||||
imageUrl,
|
||||
description: adDescription,
|
||||
event: {
|
||||
category: 'SponsorClick',
|
||||
action: `${company} Redirect`,
|
||||
label: `${roadmapLabel} / ${company} Link`,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
frontmatterObj = Object.fromEntries(frontmatterValues);
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
}
|
||||
|
||||
// Remove sponsors from all roadmaps
|
||||
removeAllSponsors('roadmaps');
|
||||
removeAllSponsors('best-practices');
|
||||
|
||||
console.log('------------------------');
|
||||
console.log('Adding sponsors');
|
||||
console.log('------------------------');
|
||||
fetch(sheetUrl)
|
||||
.then((res) => res.json())
|
||||
.then((rawData) => {
|
||||
const rows = rawData.values;
|
||||
|
||||
rows.map((row) => {
|
||||
// prettier-ignore
|
||||
const [
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
] = row;
|
||||
|
||||
const isConfiguredActive = isActive?.toLowerCase() === 'yes';
|
||||
const currentDate = new Date();
|
||||
const isDateInRange =
|
||||
currentDate >= new Date(startDate) && currentDate <= new Date(endDate);
|
||||
|
||||
if (!isConfiguredActive || !isDateInRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
@@ -23,7 +23,7 @@ For the existing roadmaps, please follow the details listed for the nature of co
|
||||
|
||||
## Adding Content
|
||||
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
|
||||
- Content must be in English.
|
||||
- Put a brief description about the topic on top of the file and the a list of links below with each link having title of the URL.
|
||||
|
||||
31
package.json
@@ -8,32 +8,37 @@
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier --write .",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node bin/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-dirs": "node bin/best-practice-dirs.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"astro": "^1.9.2",
|
||||
"astro-compress": "^1.1.27",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"@astrojs/sitemap": "^1.2.1",
|
||||
"@astrojs/tailwind": "^3.1.1",
|
||||
"astro": "^2.1.7",
|
||||
"astro-compress": "^1.1.35",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.8.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.1",
|
||||
"tailwindcss": "^3.2.4"
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"gh-pages": "^4.0.0",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-astro": "^0.7.2"
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const config: PlaywrightTestConfig = {
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: "http://localhost:3000",
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
5898
pnpm-lock.yaml
generated
BIN
public/best-practices/api-security.png
Normal file
|
After Width: | Height: | Size: 505 KiB |
BIN
public/best-practices/aws.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
public/best-practices/frontend-performance.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/partners/kubecampus.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
public/jsons/best-practices/api-security.json
Normal file
1
public/jsons/best-practices/aws.json
Normal file
1
public/jsons/best-practices/frontend-performance.json
Normal file
1
public/jsons/roadmaps/cyber-security.json
Normal file
1
public/jsons/roadmaps/frontend-beginner.json
Normal file
1
public/jsons/roadmaps/kubernetes.json
Normal file
1
public/jsons/roadmaps/mongodb.json
Normal file
1
public/jsons/roadmaps/typescript.json
Normal file
1
public/jsons/roadmaps/ux-design.json
Normal file
BIN
public/pdfs/best-practices/api-security.pdf
Normal file
BIN
public/pdfs/best-practices/aws.pdf
Normal file
BIN
public/pdfs/best-practices/frontend-performance.pdf
Normal file
BIN
public/pdfs/roadmaps/cyber-security.pdf
Normal file
BIN
public/pdfs/roadmaps/kubernetes.pdf
Normal file
BIN
public/pdfs/roadmaps/mongodb.pdf
Normal file
BIN
public/pdfs/roadmaps/typescript.pdf
Normal file
BIN
public/pdfs/roadmaps/ux-design.pdf
Normal file
BIN
public/roadmaps/cyber-security.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/roadmaps/kubernetes.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
public/roadmaps/mongodb.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
public/roadmaps/typescript.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
public/roadmaps/ux-design.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
12
readme.md
@@ -38,6 +38,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
|
||||
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
|
||||
- [JavaScript Roadmap](https://roadmap.sh/javascript)
|
||||
- [TypeScript Roadmap](https://roadmap.sh/typescript)
|
||||
- [React Roadmap](https://roadmap.sh/react)
|
||||
- [Vue Roadmap](https://roadmap.sh/vue)
|
||||
- [Angular Roadmap](https://roadmap.sh/angular)
|
||||
@@ -53,6 +54,17 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [DBA Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [Blockchain Roadmap](https://roadmap.sh/blockchain)
|
||||
- [ASP.NET Core Roadmap](https://roadmap.sh/aspnet-core)
|
||||
- [System Design Roadmap](https://roadmap.sh/system-design)
|
||||
- [Kubernetes Roadmap](https://roadmap.sh/kubernetes)
|
||||
- [Cyber Security Roadmap](https://roadmap.sh/cyber-security)
|
||||
- [MongoDB Roadmap](https://roadmap.sh/mongodb)
|
||||
- [UX Design Roadmap](https://roadmap.sh/ux-design)
|
||||
|
||||
We have also added a new form of visual content covering best practices:
|
||||
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
|
||||
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
|
||||
|
||||

|
||||
|
||||
|
||||
44
sitemap.mjs
@@ -2,26 +2,38 @@ import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
async function getRoadmapIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/roadmaps'));
|
||||
return fs.readdir(path.join(process.cwd(), 'src/data/roadmaps'));
|
||||
}
|
||||
|
||||
export function shouldIndexPage(page) {
|
||||
async function getBestPracticesIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/data/best-practices'));
|
||||
}
|
||||
|
||||
export function shouldIndexPage(pageUrl) {
|
||||
return ![
|
||||
'https://roadmap.sh/404/',
|
||||
'https://roadmap.sh/terms/',
|
||||
'https://roadmap.sh/privacy/',
|
||||
'https://roadmap.sh/pdfs/',
|
||||
].includes(page);
|
||||
'https://roadmap.sh/404',
|
||||
'https://roadmap.sh/terms',
|
||||
'https://roadmap.sh/privacy',
|
||||
'https://roadmap.sh/pdfs',
|
||||
'https://roadmap.sh/g',
|
||||
].includes(pageUrl);
|
||||
}
|
||||
|
||||
export async function serializeSitemap(item) {
|
||||
const highPriorityPages = [
|
||||
'https://roadmap.sh/',
|
||||
'https://roadmap.sh/about/',
|
||||
'https://roadmap.sh/roadmaps/',
|
||||
'https://roadmap.sh/guides/',
|
||||
'https://roadmap.sh/videos/',
|
||||
...(await getRoadmapIds()).map((id) => `https://roadmap.sh/${id}/`),
|
||||
'https://roadmap.sh',
|
||||
'https://roadmap.sh/about',
|
||||
'https://roadmap.sh/roadmaps',
|
||||
'https://roadmap.sh/best-practices',
|
||||
'https://roadmap.sh/guides',
|
||||
'https://roadmap.sh/videos',
|
||||
...(await getRoadmapIds()).flatMap((id) => [
|
||||
`https://roadmap.sh/${id}`,
|
||||
`https://roadmap.sh/${id}/topics`,
|
||||
]),
|
||||
...(await getBestPracticesIds()).map(
|
||||
(id) => `https://roadmap.sh/best-practices/${id}`
|
||||
),
|
||||
];
|
||||
|
||||
// Roadmaps and other high priority pages
|
||||
@@ -32,22 +44,20 @@ export async function serializeSitemap(item) {
|
||||
// @ts-ignore
|
||||
changefreq: 'monthly',
|
||||
priority: 1,
|
||||
lastmod: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Guide and video pages
|
||||
if (
|
||||
item.url.startsWith('https://roadmap.sh/guides/') ||
|
||||
item.url.startsWith('https://roadmap.sh/videos/')
|
||||
item.url.startsWith('https://roadmap.sh/guides') ||
|
||||
item.url.startsWith('https://roadmap.sh/videos')
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
// @ts-ignore
|
||||
changefreq: 'monthly',
|
||||
priority: 0.9,
|
||||
lastmod: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// To selectively enable/disable debug logs
|
||||
__DEBUG__: boolean;
|
||||
gtag: any;
|
||||
fireEvent: (props: GAEventType) => void;
|
||||
}
|
||||
@@ -27,6 +29,10 @@ window.fireEvent = (props: GAEventType) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.__DEBUG__) {
|
||||
console.log('Analytics event fired', props);
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
|
||||
87
src/components/BestPracticeHeader.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
import BestPracticeHint from './BestPracticeHint.astro';
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import Icon from './Icon.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
bestPracticeId: string;
|
||||
isUpcoming?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, bestPracticeId, isUpcoming = false } = Astro.props;
|
||||
const isBestPracticeReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<div class='mt-0 mb-3 sm:mb-6'>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class='flex justify-between'>
|
||||
<div class='flex gap-1 sm:gap-2'>
|
||||
<a
|
||||
href='/best-practices'
|
||||
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 Best Practices'
|
||||
>
|
||||
←<span class='hidden sm:inline'> All Best Practices</span>
|
||||
</a>
|
||||
|
||||
{
|
||||
isBestPracticeReady && (
|
||||
<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 Best Practice'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Best Practice Popup'
|
||||
>
|
||||
<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'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Subscribe Best Practice Popup'
|
||||
>
|
||||
<Icon icon='email' />
|
||||
<span class='ml-2'>Subscribe</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
isBestPracticeReady && (
|
||||
<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'
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<BestPracticeHint bestPracticeId={bestPracticeId} />
|
||||
</div>
|
||||
</div>
|
||||
20
src/components/BestPracticeHint.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
export interface Props {
|
||||
bestPracticeId: string;
|
||||
}
|
||||
---
|
||||
|
||||
<div class='mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0 sm:-mb-[65px]'>
|
||||
<!-- Desktop: Roadmap Resources - Alert -->
|
||||
<div class='hidden sm:flex justify-between px-2 bg-white items-center rounded-md p-1.5'>
|
||||
<p class='text-sm'>
|
||||
<span class='text-yellow-900 bg-yellow-200 py-0.5 px-1 text-xs rounded-sm font-medium uppercase mr-0.5'>Tip</span>
|
||||
Click the best practices for details and resources
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile - Roadmap resources alert -->
|
||||
<p class='block sm:hidden text-sm border border-yellow-500 text-yellow-700 rounded-md py-1.5 px-2 bg-white relative'>
|
||||
Click the best practices for details and resources
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import type { BreadcrumbItem } from '../lib/topic';
|
||||
import type { BreadcrumbItem } from '../lib/roadmap-topic';
|
||||
|
||||
export interface Props {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
@@ -10,28 +10,35 @@ const { breadcrumbs, roadmapId } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='py-7 pb-6'>
|
||||
<!-- Desktop breadcrums -->
|
||||
<p class='text-gray-500 container hidden sm:block'>
|
||||
{ breadcrumbs.map((breadcrumb, counter) => {
|
||||
<!-- Desktop breadcrums -->
|
||||
<p class='text-gray-500 container hidden sm:block'>
|
||||
{
|
||||
breadcrumbs.map((breadcrumb, counter) => {
|
||||
const isLast = counter === breadcrumbs.length - 1;
|
||||
|
||||
|
||||
if (!isLast) {
|
||||
return (
|
||||
<>
|
||||
<a class='hover:text-gray-800' href={`${breadcrumb.url}/`}>{ breadcrumb.title }</a>
|
||||
<a class='hover:text-gray-800' href={`${breadcrumb.url}`}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
<span> · </span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <span class='text-gray-400'>{ breadcrumb.title }</span>
|
||||
})}
|
||||
</p>
|
||||
|
||||
<!-- Mobile breadcrums -->
|
||||
<p class='container block sm:hidden'>
|
||||
<a class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600' href={`/${roadmapId}/`}>
|
||||
← Back to Topics List
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
return <span class='text-gray-400'>{breadcrumb.title}</span>;
|
||||
})
|
||||
}
|
||||
</p>
|
||||
|
||||
<!-- Mobile breadcrums -->
|
||||
<p class='container block sm:hidden'>
|
||||
<a
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
href={`/${roadmapId}`}
|
||||
>
|
||||
← Back to Topics List
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,16 @@ import Popup from './Popup/Popup.astro';
|
||||
import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
---
|
||||
|
||||
<Popup
|
||||
id='download-popup'
|
||||
title='Download Roadmap'
|
||||
subtitle='Enter your email below to receive the download link.'
|
||||
>
|
||||
<Popup id='download-popup' title='Download' subtitle='Enter your email below to receive the download link.'>
|
||||
<form
|
||||
action='https://newsletter.roadmap.sh/subscribe'
|
||||
action='https://news.roadmap.sh/subscribe'
|
||||
method='POST'
|
||||
accept-charset='utf-8'
|
||||
target='_blank'
|
||||
captcha-form
|
||||
>
|
||||
<input type='hidden' name='gdpr' value='true' />
|
||||
|
||||
<input
|
||||
type='email'
|
||||
name='email'
|
||||
@@ -42,13 +40,11 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
</Popup>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[submit-download-form]')
|
||||
?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
document.querySelector('[submit-download-form]')?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,13 @@ if (faqs.length === 0) {
|
||||
}
|
||||
---
|
||||
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='border-t bg-gray-100 mt-8'>
|
||||
<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'>
|
||||
<div class='flex flex-col gap-1 pb-14'>
|
||||
{
|
||||
faqs.map((faq, questionIndex) => (
|
||||
<Question isActive={questionIndex === 0} question={faq.question}>
|
||||
|
||||
@@ -18,7 +18,7 @@ const { heading, guides } = Astro.props;
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/guides/'
|
||||
href='/guides'
|
||||
class='hidden sm:inline transition-colors py-2 px-3 text-xs font-medium rounded-full bg-gradient-to-r from-slate-600 to-black hover:from-blue-600 hover:to-blue-800 text-white'
|
||||
>
|
||||
View All Guides →
|
||||
@@ -26,7 +26,7 @@ const { heading, guides } = Astro.props;
|
||||
|
||||
<div class='block sm:hidden mt-3'>
|
||||
<a
|
||||
href='/guides/'
|
||||
href='/guides'
|
||||
class='text-sm font-regular block p-2 border border-black text-black rounded-md text-center hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Guides →
|
||||
|
||||
50
src/components/FeaturedItems/FeaturedItem.astro
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
export interface FeaturedItemType {
|
||||
isUpcoming?: boolean;
|
||||
isNew?: boolean;
|
||||
url: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Props extends FeaturedItemType {}
|
||||
|
||||
const { isUpcoming = false, isNew = false, text, url } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'group border border-slate-800 bg-slate-900 p-2.5 sm:p-3.5 block no-underline rounded-lg relative text-slate-400 font-regular text-md hover:border-slate-600 hover:text-slate-100',
|
||||
{
|
||||
'opacity-50': isUpcoming,
|
||||
},
|
||||
]}
|
||||
href={url}
|
||||
>
|
||||
<span class='text-slate-400'>
|
||||
{text}
|
||||
</span>
|
||||
|
||||
{
|
||||
isNew && (
|
||||
<span class='absolute bottom-1.5 right-2 text-xs font-medium rounded-br rounded-tl text-purple-300 flex items-center'>
|
||||
<span class='flex h-2 w-2 mr-1.5'>
|
||||
<span class='animate-ping absolute inline-flex h-2 w-2 rounded-full bg-purple-400 opacity-75' />
|
||||
<span class='relative inline-flex rounded-full h-2 w-2 bg-purple-500' />
|
||||
</span>
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
isUpcoming && (
|
||||
<span class='absolute bottom-1.5 right-2 text-xs font-medium rounded-br rounded-tl text-slate-500 flex items-center'>
|
||||
<span class='flex h-2 w-2 mr-1.5'>
|
||||
<span class='animate-ping absolute inline-flex h-2 w-2 rounded-full bg-slate-500 opacity-75' />
|
||||
<span class='relative inline-flex rounded-full h-2 w-2 bg-slate-600' />
|
||||
</span>
|
||||
Upcoming
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
33
src/components/FeaturedItems/FeaturedItems.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import FeaturedItem, { FeaturedItemType } from './FeaturedItem.astro';
|
||||
|
||||
export interface Props {
|
||||
featuredItems: FeaturedItemType[];
|
||||
heading: string;
|
||||
}
|
||||
|
||||
const { featuredItems, heading } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='py-4 sm:py-14 border-b border-b-[#1e293c] relative'>
|
||||
<div class='container'>
|
||||
<h2 class='hidden sm:flex absolute rounded-lg -top-[17px] left-1/2 -translate-x-1/2 bg-slate-900 py-1 px-3 border border-[#1e293c] text-md text-slate-400 font-regular'>
|
||||
{heading}
|
||||
</h2>
|
||||
|
||||
<ul class='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2'>
|
||||
{
|
||||
featuredItems.map((featuredItem) => (
|
||||
<li>
|
||||
<FeaturedItem
|
||||
text={featuredItem.text}
|
||||
url={featuredItem.url}
|
||||
isNew={featuredItem.isNew}
|
||||
isUpcoming={featuredItem.isUpcoming}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFileType;
|
||||
}
|
||||
|
||||
const { roadmap } = Astro.props;
|
||||
const frontmatter = roadmap.frontmatter;
|
||||
|
||||
let roadmapTitle = frontmatter.featuredTitle;
|
||||
|
||||
// Lighthouse considers "Go" as a non-descriptive text such as "Submit" etc.
|
||||
// Adding "Roadmap" as a postfix to make it not complain ¯\_(ツ)_/¯
|
||||
if (roadmapTitle === 'Go') {
|
||||
roadmapTitle = 'Go Roadmap';
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'group border border-slate-800 bg-slate-900 p-2.5 sm:p-3.5 block no-underline rounded-lg relative text-slate-400 font-regular text-md hover:border-slate-600 hover:text-slate-100',
|
||||
{
|
||||
'opacity-50': roadmap.frontmatter.isUpcoming,
|
||||
},
|
||||
]}
|
||||
href={`/${roadmap.id}/`}
|
||||
>
|
||||
<span class="text-slate-400">
|
||||
{roadmapTitle}
|
||||
</span>
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class="absolute bottom-1.5 right-2 text-xs font-medium rounded-br rounded-tl text-purple-300 flex items-center">
|
||||
<span class="flex h-2 w-2 mr-1.5">
|
||||
<span class="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-purple-400 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-purple-500" />
|
||||
</span>
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
frontmatter.isUpcoming && (
|
||||
<span class="absolute bottom-1.5 right-2 text-xs font-medium rounded-br rounded-tl text-slate-500 flex items-center">
|
||||
<span class="flex h-2 w-2 mr-1.5">
|
||||
<span class="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-slate-500 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-slate-600" />
|
||||
</span>
|
||||
Upcoming
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../../lib/roadmap';
|
||||
import FeaturedRoadmapItem from './FeaturedRoadmapItem.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmaps: RoadmapFileType[];
|
||||
heading: string;
|
||||
}
|
||||
|
||||
const { roadmaps, heading } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="py-4 sm:py-14 border-b border-b-slate-900 relative">
|
||||
<div class="container">
|
||||
<h2
|
||||
class="hidden sm:flex absolute rounded-lg -top-[17px] left-1/2 -translate-x-1/2 bg-slate-900 py-1 px-3 border border-slate-900 text-md text-slate-400 font-regular"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{
|
||||
roadmaps.map((roadmap) => (
|
||||
<li>
|
||||
<FeaturedRoadmapItem roadmap={roadmap} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ const { heading, videos } = Astro.props;
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/videos/'
|
||||
href='/videos'
|
||||
class='hidden sm:inline transition-colors py-2 px-3 text-xs font-medium rounded-full bg-gradient-to-r from-slate-600 to-black hover:from-blue-600 hover:to-blue-800 text-white'
|
||||
>
|
||||
View All Videos →
|
||||
@@ -26,7 +26,7 @@ const { heading, videos } = Astro.props;
|
||||
|
||||
<div class='block sm:hidden mt-3'>
|
||||
<a
|
||||
href='/videos/'
|
||||
href='/videos'
|
||||
class='text-sm font-regular block p-2 border border-black text-black rounded-md text-center hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Videos →
|
||||
|
||||
@@ -4,24 +4,22 @@ import Icon from './Icon.astro';
|
||||
|
||||
<div class='py-6 sm:py-16 pb-10 bg-slate-900 text-white'>
|
||||
<div class='container'>
|
||||
<p
|
||||
class='text-gray-400 font-medium flex flex-col sm:flex-row gap-0 sm:gap-4 mb-8 sm:mb-16 justify-center'
|
||||
>
|
||||
<p class='text-gray-400 font-medium flex flex-col sm:flex-row gap-0 sm:gap-4 mb-8 sm:mb-16 justify-center'>
|
||||
<a
|
||||
class='transition-colors px-2 py-1.5 border-b border-b-gray-700 sm:border-b-0 sm:py-0 sm:px-0 hover:text-white'
|
||||
href='/roadmaps/'>Roadmaps</a
|
||||
href='/roadmaps'>Roadmaps</a
|
||||
>
|
||||
<a
|
||||
class='transition-colors px-2 py-1.5 border-b border-b-gray-700 sm:border-b-0 sm:py-0 sm:px-0 hover:text-white'
|
||||
href='/guides/'>Guides</a
|
||||
href='/guides'>Guides</a
|
||||
>
|
||||
<a
|
||||
class='transition-colors px-2 py-1.5 border-b border-b-gray-700 sm:border-b-0 sm:py-0 sm:px-0 hover:text-white'
|
||||
href='/videos/'>Videos</a
|
||||
href='/videos'>Videos</a
|
||||
>
|
||||
<a
|
||||
class='transition-colors px-2 py-1.5 border-b border-b-gray-700 sm:border-b-0 sm:py-0 sm:px-0 hover:text-white'
|
||||
href='/about/'>About</a
|
||||
href='/about'>About</a
|
||||
>
|
||||
<a
|
||||
class='transition-colors px-2 py-1.5 sm:border-b-0 sm:py-0 sm:px-0 hover:text-white'
|
||||
@@ -51,18 +49,18 @@ import Icon from './Icon.astro';
|
||||
</a>
|
||||
</p>
|
||||
<p class='text-slate-300/60 my-4'>
|
||||
Community created roadmaps, articles, resources and journeys to help
|
||||
you choose your path and grow in your career.
|
||||
Community created roadmaps, articles, resources and journeys to help you choose your path and grow in your
|
||||
career.
|
||||
</p>
|
||||
<div class='text-gray-400 text-sm'>
|
||||
<p>
|
||||
© roadmap.sh
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a href='/about/' class='hover:text-white'>FAQs</a>
|
||||
<a href='/about' class='hover:text-white'>FAQs</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a href='/terms/' class='hover:text-white'>Terms</a>
|
||||
<a href='/terms' class='hover:text-white'>Terms</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a href='/privacy/' class='hover:text-white'>Privacy</a>
|
||||
<a href='/privacy' class='hover:text-white'>Privacy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,8 +76,8 @@ import Icon from './Icon.astro';
|
||||
/>
|
||||
</a>
|
||||
<p class='text-slate-300/60 my-4'>
|
||||
The leading DevOps resource for Kubernetes, cloud-native computing,
|
||||
and the latest in at-scale development, deployment, and management.
|
||||
The leading DevOps resource for Kubernetes, cloud-native computing, and the latest in at-scale development,
|
||||
deployment, and management.
|
||||
</p>
|
||||
<div class='text-gray-400 text-sm'>
|
||||
<p>
|
||||
|
||||
30
src/components/FrameRenderer/FrameRenderer.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import Loader from '../Loader.astro';
|
||||
import './FrameRenderer.css';
|
||||
|
||||
export interface Props {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceId: string;
|
||||
jsonUrl: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { resourceId, resourceType, jsonUrl, dimensions = null } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
id='resource-svg-wrap'
|
||||
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null}
|
||||
data-resource-type={resourceType}
|
||||
data-resource-id={resourceId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
<div id='resource-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./renderer.js'></script>
|
||||
94
src/components/FrameRenderer/FrameRenderer.css
Normal file
@@ -0,0 +1,94 @@
|
||||
svg text tspan {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #1e1e3f;
|
||||
color: #9efeff;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
svg .clickable-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(65,53,214)'] {
|
||||
fill: #232381;
|
||||
stroke: #232381;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,0)'] {
|
||||
fill: #d6d700;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,229,153)'] {
|
||||
fill: #f3c950;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(153,153,153)'] {
|
||||
fill: #646464;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,255)'] {
|
||||
fill: #d7d7d7;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,221)'] {
|
||||
fill: #e5e5be;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,217,102)'] {
|
||||
fill: #d9b443;
|
||||
}
|
||||
|
||||
svg .done rect {
|
||||
fill: #cbcbcb !important;
|
||||
}
|
||||
|
||||
svg .done text {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
svg .clickable-group.done[data-group-id^='check:'] rect {
|
||||
fill: gray !important;
|
||||
stroke: gray;
|
||||
}
|
||||
|
||||
.clickable-group rect {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/************************************
|
||||
Aspect ratio implementation
|
||||
*************************************/
|
||||
[style*='--aspect-ratio'] > :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio'] > img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@supports (--custom: property) {
|
||||
[style*='--aspect-ratio'] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio']::before {
|
||||
content: '';
|
||||
display: block;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
padding-bottom: calc(100% / (var(--aspect-ratio)));
|
||||
}
|
||||
|
||||
[style*='--aspect-ratio'] > :first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
182
src/components/FrameRenderer/renderer.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
|
||||
export class Renderer {
|
||||
constructor() {
|
||||
this.resourceId = '';
|
||||
this.resourceType = '';
|
||||
this.jsonUrl = '';
|
||||
this.loaderHTML = null;
|
||||
|
||||
this.containerId = 'resource-svg-wrap';
|
||||
this.loaderId = 'resource-loader';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
this.jsonToSvg = this.jsonToSvg.bind(this);
|
||||
this.handleSvgClick = this.handleSvgClick.bind(this);
|
||||
this.prepareConfig = this.prepareConfig.bind(this);
|
||||
this.switchRoadmap = this.switchRoadmap.bind(this);
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get containerEl() {
|
||||
return document.getElementById(this.containerId);
|
||||
}
|
||||
|
||||
prepareConfig() {
|
||||
if (!this.containerEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clone it so we can use it later
|
||||
this.loaderHTML = this.loaderEl.innerHTML;
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.resourceType = dataset.resourceType;
|
||||
this.resourceId = dataset.resourceId;
|
||||
this.jsonUrl = dataset.jsonUrl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { string } jsonUrl
|
||||
* @returns {Promise<SVGElement>}
|
||||
*/
|
||||
jsonToSvg(jsonUrl) {
|
||||
if (!jsonUrl) {
|
||||
console.error('jsonUrl not defined in frontmatter');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.containerEl.innerHTML = this.loaderHTML;
|
||||
|
||||
return fetch(jsonUrl)
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
})
|
||||
.then((svg) => {
|
||||
this.containerEl.replaceChildren(svg);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = `
|
||||
<strong>There was an error.</strong><br>
|
||||
|
||||
Try loading the page again. or submit an issue on GitHub with following:<br><br>
|
||||
|
||||
${error.message} <br /> ${error.stack}
|
||||
`;
|
||||
|
||||
this.containerEl.innerHTML = `<div class="error py-5 text-center text-red-600 mx-auto">${message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
onDOMLoaded() {
|
||||
if (!this.prepareConfig()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const roadmapType = urlParams.get('r');
|
||||
|
||||
if (roadmapType) {
|
||||
this.switchRoadmap(`/jsons/roadmaps/${roadmapType}.json`);
|
||||
} else {
|
||||
this.jsonToSvg(this.jsonUrl);
|
||||
}
|
||||
}
|
||||
|
||||
switchRoadmap(newJsonUrl) {
|
||||
const newJsonFileSlug = newJsonUrl.split('/').pop().replace('.json', '');
|
||||
|
||||
// Update the URL and attach the new roadmap type
|
||||
if (window?.history?.pushState) {
|
||||
const url = new URL(window.location);
|
||||
const type = this.resourceType[0]; // r for roadmap, b for best-practices
|
||||
|
||||
url.searchParams.delete(type);
|
||||
url.searchParams.set(type, newJsonFileSlug);
|
||||
|
||||
window.history.pushState(null, '', url.toString());
|
||||
}
|
||||
|
||||
const pageType = this.resourceType.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
window.fireEvent({
|
||||
// RoadmapClick, BestPracticesClick, etc
|
||||
category: `${pageType.replace('-', '')}Click`,
|
||||
// roadmap/frontend/switch-version
|
||||
action: `${this.resourceId}/switch-version`,
|
||||
// roadmap/frontend/switch-version
|
||||
label: `${newJsonFileSlug}`,
|
||||
});
|
||||
|
||||
this.jsonToSvg(newJsonUrl).then(() => {
|
||||
this.containerEl.setAttribute('style', '');
|
||||
});
|
||||
}
|
||||
|
||||
handleSvgClick(e) {
|
||||
const targetGroup = e.target.closest('g') || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (/^ext_link/.test(groupId)) {
|
||||
window.open(`https://${groupId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^json:/.test(groupId)) {
|
||||
// e.g. /roadmaps/frontend-beginner.json
|
||||
const newJsonUrl = groupId.replace('json:', '');
|
||||
|
||||
this.switchRoadmap(newJsonUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^check:/.test(groupId)) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`${this.resourceType}.topic.toggle`, {
|
||||
detail: {
|
||||
topicId: groupId.replace('check:', ''),
|
||||
resourceType: this.resourceType,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove sorting prefix from groupId
|
||||
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`${this.resourceType}.topic.click`, {
|
||||
detail: {
|
||||
topicId: normalizedGroupId,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||
window.addEventListener('click', this.handleSvgClick);
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = new Renderer();
|
||||
renderer.init();
|
||||
32
src/components/GridItem.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
const { url, title, description, isNew } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={url}
|
||||
class='bg-gradient-to-r from-slate-900 to-amber-900 hover:from-stone-900 hover:to-stone-900 hover:bg-gray-100 flex flex-col p-2.5 sm:p-5 rounded-md sm:rounded-lg border border-gray-200 relative h-full'
|
||||
>
|
||||
<span
|
||||
class='font-regular sm:font-medium text-md sm:text-xl hover:text-gray-50 text-gray-200 sm:text-gray-100 mb-0 sm:mb-1.5'
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class='text-sm leading-normal text-gray-400 hidden sm:block'>{description}</span>
|
||||
|
||||
{
|
||||
isNew && (
|
||||
<span class='absolute bottom-1 right-1 bg-yellow-300 text-yellow-900 text-xs font-medium px-1 sm:px-1.5 py-0.5 rounded-sm uppercase'>
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import type { RoadmapFileType } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFileType;
|
||||
}
|
||||
|
||||
const { roadmap } = Astro.props;
|
||||
const frontmatter = roadmap.frontmatter;
|
||||
---
|
||||
|
||||
<a
|
||||
href={`/${roadmap.id}/`}
|
||||
class="bg-gradient-to-r from-slate-900 to-amber-900 hover:from-stone-900 hover:to-stone-900 hover:bg-gray-100 flex flex-col p-2.5 sm:p-5 rounded-md sm:rounded-lg border border-gray-200 relative h-full"
|
||||
>
|
||||
<span
|
||||
class="font-regular sm:font-medium text-md sm:text-xl hover:text-gray-50 text-gray-200 sm:text-gray-100 mb-0 sm:mb-1.5"
|
||||
>{frontmatter.title}</span
|
||||
>
|
||||
<span class="text-sm leading-normal text-gray-400 hidden sm:block"
|
||||
>{frontmatter.description}</span
|
||||
>
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class="absolute bottom-1 right-1 bg-yellow-300 text-yellow-900 text-xs font-medium px-1 sm:px-1.5 py-0.5 rounded-sm uppercase">
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
@@ -32,14 +32,14 @@ const { author } = frontmatter;
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
class='text-blue-400 hover:text-blue-500 hover:underline'
|
||||
href={`https://github.com/kamranahmedse/roadmap.sh/tree/master/src/guides/${guide.id}.md`}
|
||||
href={`https://github.com/kamranahmedse/roadmap.sh/tree/master/src/data/guides/${guide.id}.md`}
|
||||
target='_blank'>Improve this Guide</a
|
||||
>
|
||||
</p>
|
||||
<h1 class='text-2xl sm:text-5xl my-0 sm:my-3.5 font-bold'>
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
<p class='hidden sm:block text-gray-400 text-md'>
|
||||
<p class='hidden sm:block text-gray-400 text-xl'>
|
||||
{frontmatter.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const { frontmatter, id } = guide;
|
||||
class:list={[
|
||||
"block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b",
|
||||
]}
|
||||
href={`/guides/${id}/`}
|
||||
href={`/guides/${id}`}
|
||||
>
|
||||
<span class="group-hover:translate-x-2 transition-transform">
|
||||
{frontmatter.title}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
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;
|
||||
description: string;
|
||||
jsonUrl: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } = Astro.props;
|
||||
---
|
||||
|
||||
<link
|
||||
rel='preload'
|
||||
href='/fonts/balsamiq.woff2'
|
||||
as='font'
|
||||
type='font/woff2'
|
||||
crossorigin
|
||||
slot='after-header'
|
||||
/>
|
||||
|
||||
<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 roadmapId={roadmapId} />
|
||||
<div
|
||||
id='roadmap-svg'
|
||||
style={dimensions
|
||||
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
|
||||
: null}
|
||||
data-roadmap-id={roadmapId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./roadmap.js'></script>
|
||||
@@ -1,86 +0,0 @@
|
||||
svg text tspan {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #1e1e3f;
|
||||
color: #9efeff;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
svg .clickable-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(65,53,214)'] {
|
||||
fill: #232381;
|
||||
stroke: #232381;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,0)'] {
|
||||
fill: #d6d700;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,229,153)'] {
|
||||
fill: #f3c950;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(153,153,153)'] {
|
||||
fill: #646464;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,255)'] {
|
||||
fill: #d7d7d7;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,255,221)'] {
|
||||
fill: #e5e5be;
|
||||
}
|
||||
|
||||
svg .clickable-group:hover > [fill='rgb(255,217,102)'] {
|
||||
fill: #d9b443;
|
||||
}
|
||||
|
||||
svg .done rect {
|
||||
fill: #cbcbcb !important;
|
||||
}
|
||||
|
||||
svg .done text {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/************************************
|
||||
Aspect ratio implementation
|
||||
*************************************/
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"] > img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@supports (--custom:property) {
|
||||
[style*="--aspect-ratio"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"]::before {
|
||||
content: "";
|
||||
display: block;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
padding-bottom: calc(100% / (var(--aspect-ratio)));
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Topic } from './topic';
|
||||
import { Sharer } from './sharer';
|
||||
|
||||
/**
|
||||
* @typedef {{ roadmapId: string, jsonUrl: string }} RoadmapConfig
|
||||
*/
|
||||
|
||||
export class Roadmap {
|
||||
/**
|
||||
* @param {RoadmapConfig} config
|
||||
*/
|
||||
constructor() {
|
||||
this.roadmapId = '';
|
||||
this.jsonUrl = '';
|
||||
|
||||
this.containerId = 'roadmap-svg';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
this.fetchRoadmapSvg = this.fetchRoadmapSvg.bind(this);
|
||||
this.handleRoadmapClick = this.handleRoadmapClick.bind(this);
|
||||
this.prepareConfig = this.prepareConfig.bind(this);
|
||||
}
|
||||
|
||||
get containerEl() {
|
||||
return document.getElementById(this.containerId);
|
||||
}
|
||||
|
||||
prepareConfig() {
|
||||
if (!this.containerEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.roadmapId = dataset.roadmapId;
|
||||
this.jsonUrl = dataset.jsonUrl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { string } jsonUrl
|
||||
* @returns {Promise<SVGElement>}
|
||||
*/
|
||||
fetchRoadmapSvg(jsonUrl) {
|
||||
if (!jsonUrl) {
|
||||
console.error('jsonUrl not defined in frontmatter');
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetch(jsonUrl)
|
||||
.then(function (res) {
|
||||
return res.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
return wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDOMLoaded() {
|
||||
if (!this.prepareConfig()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchRoadmapSvg(this.jsonUrl)
|
||||
.then((svg) => {
|
||||
document.getElementById(this.containerId).replaceChildren(svg);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
handleRoadmapClick(e) {
|
||||
const targetGroup = e.target.closest('g') || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('topic.click', {
|
||||
detail: {
|
||||
topicId: groupId,
|
||||
roadmapId: this.roadmapId,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||
window.addEventListener('click', this.handleRoadmapClick);
|
||||
}
|
||||
}
|
||||
|
||||
const roadmap = new Roadmap();
|
||||
roadmap.init();
|
||||
|
||||
// Initialize the topic loader
|
||||
const topic = new Topic();
|
||||
topic.init();
|
||||
|
||||
// Handles the share icons on the roadmap page
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
||||