Compare commits
219 Commits
content/sp
...
aws-best-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df3922678a | ||
|
|
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 | ||
|
|
f5e980d8ec | ||
|
|
6187b1dc52 | ||
|
|
a3b8b5653a | ||
|
|
8f8e2f41d8 | ||
|
|
89a436a5b7 | ||
|
|
231e295f01 | ||
|
|
64e20e9fc1 | ||
|
|
621f841fbf | ||
|
|
c61afb15bc | ||
|
|
595f3680be | ||
|
|
ee65c56bf3 | ||
|
|
a2c339f2d5 | ||
|
|
a3031a2371 | ||
|
|
952169ec8e | ||
|
|
fbd82ce215 | ||
|
|
35f61e876e | ||
|
|
bb9878fdb7 | ||
|
|
ee843cc9e2 | ||
|
|
cbd79ef299 | ||
|
|
af9e266190 | ||
|
|
0cbd401071 | ||
|
|
0929d40bd0 | ||
|
|
927aa0a066 | ||
|
|
85eff7f894 | ||
|
|
11695f4b05 | ||
|
|
aebee9b3a3 | ||
|
|
6b52baf093 | ||
|
|
6922fd826f | ||
|
|
ec29e1836e | ||
|
|
dca9eb32cd | ||
|
|
4b681c6317 | ||
|
|
9c24ff23e3 | ||
|
|
cdc87a99e1 | ||
|
|
ea16e99598 | ||
|
|
ba86e8a6b1 | ||
|
|
5f23d4c7eb |
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 */3 * * *' # every 3 hours
|
||||
|
||||
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.
|
||||
4
.gitignore
vendored
@@ -20,3 +20,7 @@ pnpm-debug.log*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
tests-examples
|
||||
@@ -9,6 +9,9 @@ import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula',
|
||||
},
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
@@ -18,6 +21,9 @@ export default defineConfig({
|
||||
],
|
||||
],
|
||||
},
|
||||
build: {
|
||||
format: 'file',
|
||||
},
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
|
||||
144
bin/best-practice-content.cjs
Normal file
@@ -0,0 +1,144 @@
|
||||
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');
|
||||
@@ -2,13 +2,18 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsonsDir = path.join(process.cwd(), 'public/jsons');
|
||||
const jsonFiles = fs.readdirSync(jsonsDir);
|
||||
const childJsonDirs = fs.readdirSync(jsonsDir);
|
||||
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
childJsonDirs.forEach((childJsonDir) => {
|
||||
const fullChildJsonDirPath = path.join(jsonsDir, childJsonDir);
|
||||
const jsonFiles = fs.readdirSync(fullChildJsonDirPath);
|
||||
|
||||
const jsonFilePath = path.join(jsonsDir, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
const jsonFilePath = path.join(fullChildJsonDirPath, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
});
|
||||
});
|
||||
|
||||
28
bin/readme.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## 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
|
||||
|
||||
## `roadmap-content.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-content [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`.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
@@ -82,7 +82,7 @@ function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(__dirname, `../public/jsons/${roadmapId}`));
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
118
bin/update-sponsors.cjs
Normal file
@@ -0,0 +1,118 @@
|
||||
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 populateRoadmapAds({
|
||||
roadmapUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
}) {
|
||||
const isConfiguredActive = isActive.toLowerCase() === 'yes';
|
||||
|
||||
const currentDate = new Date();
|
||||
const isDateInRange = currentDate >= new Date(startDate) && currentDate <= new Date(endDate);
|
||||
const shouldShowAd = isConfiguredActive && isDateInRange;
|
||||
|
||||
// get id from the roadmap URL
|
||||
const roadmapId = roadmapUrl
|
||||
.split('/')
|
||||
.pop()
|
||||
.replace(/\?.+?$/, '');
|
||||
|
||||
const roadmapFilePath = path.join(__dirname, '../src/data/roadmaps', `${roadmapId}/${roadmapId}.md`);
|
||||
|
||||
if (!fs.existsSync(roadmapFilePath)) {
|
||||
console.error(`Roadmap file not found: ${roadmapFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updating roadmap: ${roadmapId}`);
|
||||
const roadmapFileContent = fs.readFileSync(roadmapFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = roadmapFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = roadmapFileContent.replace(frontMatterRegex, ``).trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
if (shouldShowAd) {
|
||||
const frontmatterValues = Object.entries(frontmatterObj);
|
||||
const roadmapLabel = frontmatterObj.briefTitle;
|
||||
|
||||
// Insert sponsor data at 10 index i.e. after
|
||||
// roadmap dimensions in the fronmatter
|
||||
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(roadmapFilePath, newContent, 'utf8');
|
||||
}
|
||||
|
||||
fetch(sheetUrl)
|
||||
.then((res) => res.json())
|
||||
.then((rawData) => {
|
||||
const rows = rawData.values;
|
||||
|
||||
rows.map((row) => {
|
||||
// prettier-ignore
|
||||
const [
|
||||
roadmapUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
] = row;
|
||||
|
||||
populateRoadmapAds({
|
||||
roadmapUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,10 @@
|
||||
First of all thank you for considering to contribute. Please look at the details below:
|
||||
|
||||
- [Contribution](#contribution)
|
||||
- [New Roadmaps](#new-roadmaps)
|
||||
- [Existing Roadmaps](#existing-roadmaps)
|
||||
- [Adding Content](#adding-content)
|
||||
- [Guidelines](#guidelines)
|
||||
- [New Roadmaps](#new-roadmaps)
|
||||
- [Existing Roadmaps](#existing-roadmaps)
|
||||
- [Adding Content](#adding-content)
|
||||
- [Guidelines](#guidelines)
|
||||
|
||||
## New Roadmaps
|
||||
|
||||
@@ -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/roadmap-astro/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.
|
||||
|
||||
33
package.json
@@ -13,25 +13,28 @@
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs"
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-content": "node bin/best-practice-content.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"astro": "^1.8.0",
|
||||
"astro-compress": "^1.1.24",
|
||||
"astro-critters": "^1.1.24",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"@astrojs/sitemap": "^1.1.0",
|
||||
"@astrojs/tailwind": "^3.0.1",
|
||||
"astro": "^2.0.17",
|
||||
"astro-compress": "^1.1.35",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.7.10",
|
||||
"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": {
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"gh-pages": "^4.0.0",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-astro": "^0.7.0"
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-astro": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
108
playwright.config.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: {
|
||||
// ...devices['Desktop Firefox'],
|
||||
// },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: {
|
||||
// ...devices['Desktop Safari'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1357
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 |
BIN
public/images/partners/ambassador-img.png
Normal file
|
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 |
BIN
public/images/system-design.png
Normal file
|
After Width: | Height: | Size: 832 KiB |
1
public/jsons/best-practices/api-security.json
Normal file
4468
public/jsons/best-practices/aws.json
Normal file
1
public/jsons/best-practices/frontend-performance.json
Normal file
1
public/jsons/roadmaps/backend.json
Normal file
1
public/jsons/roadmaps/blockchain.json
Normal file
1
public/jsons/roadmaps/cyber-security.json
Normal file
1
public/jsons/roadmaps/flutter.json
Normal file
1
public/jsons/roadmaps/frontend-beginner.json
Normal file
1
public/jsons/roadmaps/frontend.json
Normal file
1
public/jsons/roadmaps/kubernetes.json
Normal file
1
public/jsons/roadmaps/software-design-architecture.json
Normal file
1
public/jsons/roadmaps/system-design.json
Normal file
1
public/jsons/roadmaps/typescript.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/system-design.pdf
Normal file
BIN
public/pdfs/roadmaps/typescript.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/typescript.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
@@ -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,14 @@ 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)
|
||||
|
||||
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)
|
||||
|
||||

|
||||
|
||||
|
||||
40
sitemap.mjs
@@ -2,26 +2,33 @@ 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 +39,17 @@ 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/')
|
||||
) {
|
||||
if (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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
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,11 +3,7 @@ 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'
|
||||
method='POST'
|
||||
@@ -15,6 +11,8 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
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>
|
||||
|
||||
@@ -1,3 +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'>
|
||||
<div class='text-sm sm:text-base leading-relaxed text-left p-2 sm:p-4 text-md text-gray-800 border-t border-t-gray-300 bg-gray-100 rounded-bl-md rounded-br-md [&>p:not(:last-child)]:mb-3 [&>p>a]:underline [&>p>a]:text-blue-500'>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,13 +1,42 @@
|
||||
---
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import Answer from './Answer.astro';
|
||||
import Question from './Question.astro';
|
||||
|
||||
export type FAQType = {
|
||||
question: string;
|
||||
answer: string[];
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
faqs: FAQType[];
|
||||
}
|
||||
|
||||
const { faqs } = Astro.props;
|
||||
|
||||
if (faqs.length === 0) {
|
||||
return '';
|
||||
}
|
||||
---
|
||||
|
||||
<div class='border-t bg-gray-100'>
|
||||
<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>
|
||||
<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 class='flex flex-col gap-1 pb-14'>
|
||||
{
|
||||
faqs.map((faq, questionIndex) => (
|
||||
<Question isActive={questionIndex === 0} question={faq.question}>
|
||||
<Answer>
|
||||
{faq.answer.map((answer) => (
|
||||
<p set:html={markdownToHtml(answer)} />
|
||||
))}
|
||||
</Answer>
|
||||
</Question>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||