Compare commits
146 Commits
chore/java
...
feat/accou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912300f15f | ||
|
|
4e2deb13bd | ||
|
|
b04e105ff7 | ||
|
|
c13ca84190 | ||
|
|
a544cfea8d | ||
|
|
0da417735e | ||
|
|
7dbb00a306 | ||
|
|
ae72680a5b | ||
|
|
3825968106 | ||
|
|
20db0baec5 | ||
|
|
78e4c38c97 | ||
|
|
f83cd701e9 | ||
|
|
f17673b6e7 | ||
|
|
a730c81886 | ||
|
|
a9d70f665b | ||
|
|
2c06225c3c | ||
|
|
0018627afe | ||
|
|
7177638b49 | ||
|
|
38b1cf4b33 | ||
|
|
a0764aa7be | ||
|
|
f1fc4ef2bc | ||
|
|
a4256db687 | ||
|
|
33d7257ed2 | ||
|
|
e849eeeca5 | ||
|
|
44ce3a3c98 | ||
|
|
3072e42b0a | ||
|
|
725aa2b092 | ||
|
|
9bcf421590 | ||
|
|
91ce20776c | ||
|
|
dbe99df82c | ||
|
|
7a2e283281 | ||
|
|
a5866608e5 | ||
|
|
cdc7f081a3 | ||
|
|
8b285cc600 | ||
|
|
82334bbcae | ||
|
|
ec45a565ea | ||
|
|
aece5a4eea | ||
|
|
332c71c16e | ||
|
|
0389cd8f51 | ||
|
|
768e7ab67d | ||
|
|
3cd8468c8f | ||
|
|
1950404b20 | ||
|
|
81be1d8802 | ||
|
|
d29aeaf000 | ||
|
|
3dfaad8704 | ||
|
|
1ea66824d0 | ||
|
|
9c8cd71ed2 | ||
|
|
6b4be3f0ab | ||
|
|
983476eb37 | ||
|
|
22174954ab | ||
|
|
f71613eec0 | ||
|
|
1a2d58689e | ||
|
|
6a0403ec25 | ||
|
|
bf9e767083 | ||
|
|
54ac3ee2cf | ||
|
|
50ae6f9cda | ||
|
|
36aaf0dfaf | ||
|
|
52219d4b27 | ||
|
|
ee55b0192b | ||
|
|
729751804b | ||
|
|
46a6d8c0c5 | ||
|
|
ced7cf8135 | ||
|
|
b72c5b203b | ||
|
|
0fc72f2dcf | ||
|
|
765efd4eb6 | ||
|
|
21be8e526f | ||
|
|
f82c637bd2 | ||
|
|
05c9bbec8b | ||
|
|
11d7618230 | ||
|
|
9ca74a5750 | ||
|
|
9d8889a492 | ||
|
|
0fa4533168 | ||
|
|
931dac64ff | ||
|
|
423df1a225 | ||
|
|
61fe3a16bc | ||
|
|
7dc2d9169b | ||
|
|
580ee114c9 | ||
|
|
f8c4023d9a | ||
|
|
277a3aca06 | ||
|
|
fa9f87b310 | ||
|
|
173e37af38 | ||
|
|
e41cc2f4e6 | ||
|
|
6061377f70 | ||
|
|
1c55d2f91c | ||
|
|
bb9a6431df | ||
|
|
024940cf5c | ||
|
|
094d350855 | ||
|
|
6ac01491cb | ||
|
|
4ef6eb0e20 | ||
|
|
3a589f94a7 | ||
|
|
9e71ce5cef | ||
|
|
ac94b85c51 | ||
|
|
97ad05e373 | ||
|
|
41d6b51089 | ||
|
|
055d6e0023 | ||
|
|
47288bfc0e | ||
|
|
c7fd995a86 | ||
|
|
6bf02c5687 | ||
|
|
448c5cda5f | ||
|
|
ecb9de8a40 | ||
|
|
9a145cb785 | ||
|
|
f5658e1980 | ||
|
|
fc054e20e7 | ||
|
|
884bd5d66e | ||
|
|
e40be008ba | ||
|
|
2c415a30b0 | ||
|
|
b69ec5617d | ||
|
|
ca7b9d9744 | ||
|
|
6964c06d91 | ||
|
|
854779fcc8 | ||
|
|
e1f6ac68f9 | ||
|
|
8d923c4135 | ||
|
|
754ea69bc6 | ||
|
|
edd93b1d1d | ||
|
|
1c0eee6929 | ||
|
|
0185bf179a | ||
|
|
df44dad61f | ||
|
|
c5cda201ef | ||
|
|
f1de53c191 | ||
|
|
eaec8d70f7 | ||
|
|
5e690b854c | ||
|
|
cbb322b66f | ||
|
|
da9c897765 | ||
|
|
4c525f8dc0 | ||
|
|
00133369af | ||
|
|
5cdc443261 | ||
|
|
f354130f35 | ||
|
|
b2934993b0 | ||
|
|
b5d47349a1 | ||
|
|
8eadfb6640 | ||
|
|
407d70d462 | ||
|
|
d338480802 | ||
|
|
a4efe04a61 | ||
|
|
0688968c22 | ||
|
|
1d9cb45733 | ||
|
|
351e1e4509 | ||
|
|
410308bf7f | ||
|
|
e6054e247c | ||
|
|
174803dc2a | ||
|
|
77de8fdd3d | ||
|
|
678363f77d | ||
|
|
b424404bfd | ||
|
|
8102f60ebc | ||
|
|
130d460b94 | ||
|
|
52d110dffe | ||
|
|
5bd3d90d3c |
@@ -1,2 +1 @@
|
||||
PUBLIC_API_URL=http://api.roadmap.sh
|
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/01-suggest-changes.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: "✍️ Suggest Changes"
|
||||
description: Help us improve the roadmaps by suggesting changes
|
||||
labels: [suggestion]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help us improve the roadmaps with your suggestions.
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: Roadmap URL
|
||||
description: Please provide the URL of the roadmap you are suggesting changes to.
|
||||
placeholder: https://roadmap.sh
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: roadmap-suggestions
|
||||
attributes:
|
||||
label: Suggestions
|
||||
description: What changes would you like to suggest?
|
||||
placeholder: Enter your suggestions here.
|
||||
validations:
|
||||
required: true
|
||||
42
.github/ISSUE_TEMPLATE/02-bug-report.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: "🐛 Bug Report"
|
||||
description: Report an issue or possible bug
|
||||
labels: [bug]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: What is the URL where the issue is happening
|
||||
placeholder: https://roadmap.sh
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Output from browser console (if any)
|
||||
description: Please copy and paste any relevant log output.
|
||||
- type: checkboxes
|
||||
id: will-pr
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this issue.
|
||||
required: false
|
||||
12
.github/ISSUE_TEMPLATE/03-feature-suggestion.yml
vendored
@@ -1,12 +0,0 @@
|
||||
name: "✨ Feature Suggestion"
|
||||
description: Is there a feature you'd like to see on Roadmap.sh? Let us know!
|
||||
labels: [feature request]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: Please provide a detailed description of the feature you are suggesting and how it would help you/others.
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,37 +0,0 @@
|
||||
name: "🙏 Submit a Roadmap"
|
||||
description: Help us launch a new roadmap with your expertise.
|
||||
labels: [roadmap contribution]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to submit a roadmap! Please fill out the information below and we'll get back to you as soon as we can.
|
||||
- type: input
|
||||
id: roadmap-title
|
||||
attributes:
|
||||
label: What is the title of the roadmap you are submitting?
|
||||
placeholder: e.g. Roadmap to learn Data Science
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: Is this roadmap prepared by you or someone else?
|
||||
options:
|
||||
- I prepared this roadmap
|
||||
- I found this roadmap online (please provide a link below)
|
||||
- type: textarea
|
||||
id: roadmap-description
|
||||
attributes:
|
||||
label: Roadmap Items
|
||||
description: Please submit a nested list of items which we can convert into the visual. Here is an [example of roadmap items list.](https://gist.github.com/kamranahmedse/98758d2c73799b3a6ce17385e4c548a5).
|
||||
placeholder: |
|
||||
- Item 1
|
||||
- Subitem 1
|
||||
- Subitem 2
|
||||
- Item 2
|
||||
- Subitem 1
|
||||
- Subitem 2
|
||||
validations:
|
||||
required: true
|
||||
12
.github/ISSUE_TEMPLATE/05-something-else.yml
vendored
@@ -1,12 +0,0 @@
|
||||
name: "🤷♂️ Something else"
|
||||
description: If none of the above templates fit your needs, please use this template to submit your issue.
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-description
|
||||
attributes:
|
||||
label: Detailed Description
|
||||
description: Please provide a detailed description of the issue.
|
||||
validations:
|
||||
required: true
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ✋ Roadmap Request
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Please do not open issues with roadmap requests, hop onto the discord server for that.
|
||||
- name: 📝 Typo or Grammatical Mistake
|
||||
url: https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data
|
||||
about: Please submit a pull request instead of reporting it as an issue.
|
||||
- name: 💬 Chat on Discord
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Join the community on our Discord server.
|
||||
- name: 🤝 Guidance
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Join the community in our Discord server.
|
||||
1
.github/workflows/deploy.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
branches: [ master ]
|
||||
env:
|
||||
PUBLIC_API_URL: "https://api.roadmap.sh"
|
||||
PUBLIC_AVATAR_BASE_URL: "https://dodrc8eu8m09s.cloudfront.net/avatars"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PAT: ${{ secrets.PAT }}
|
||||
CI: true
|
||||
|
||||
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
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
.idea
|
||||
|
||||
# build output
|
||||
dist/
|
||||
.output/
|
||||
@@ -7,7 +5,7 @@ dist/
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
scripts/developer-roadmap
|
||||
bin/developer-roadmap
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
@@ -25,5 +23,4 @@ pnpm-debug.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
tests-examples
|
||||
*.csv
|
||||
tests-examples
|
||||
@@ -1,13 +1,11 @@
|
||||
// https://astro.build/config
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import compress from 'astro-compress';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import compress from 'astro-compress';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
|
||||
import react from '@astrojs/react';
|
||||
import preact from '@astrojs/preact';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -32,9 +30,11 @@ export default defineConfig({
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
];
|
||||
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return 'noopener noreferrer nofollow';
|
||||
},
|
||||
},
|
||||
@@ -55,10 +55,9 @@ export default defineConfig({
|
||||
serialize: serializeSitemap,
|
||||
}),
|
||||
compress({
|
||||
HTML: false,
|
||||
CSS: false,
|
||||
JavaScript: false,
|
||||
css: false,
|
||||
js: false,
|
||||
}),
|
||||
react(),
|
||||
preact(),
|
||||
],
|
||||
});
|
||||
|
||||
19
bin/compress-jsons.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsonsDir = path.join(process.cwd(), 'public/jsons');
|
||||
const childJsonDirs = fs.readdirSync(jsonsDir);
|
||||
|
||||
childJsonDirs.forEach((childJsonDir) => {
|
||||
const fullChildJsonDirPath = path.join(jsonsDir, childJsonDir);
|
||||
const jsonFiles = fs.readdirSync(fullChildJsonDirPath);
|
||||
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
|
||||
const jsonFilePath = path.join(fullChildJsonDirPath, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
|
||||
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];
|
||||
|
||||
@@ -60,12 +61,12 @@ function writeTopicContent(currTopicUrl) {
|
||||
|
||||
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 with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
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 with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
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(`Generating '${childTopic || parentTopic}'...`);
|
||||
console.log(`Genearting '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai
|
||||
@@ -89,60 +90,10 @@ function writeTopicContent(currTopicUrl) {
|
||||
});
|
||||
}
|
||||
|
||||
async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let newFileContent = `# ${topicTitle}`;
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log(`Writing ${topicId}..`);
|
||||
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
|
||||
|
||||
const roadmapJson = require(path.join(
|
||||
ALL_ROADMAPS_DIR,
|
||||
`${roadmapId}/${roadmapId}`
|
||||
));
|
||||
|
||||
const roadmapJson = require(path.join(ROADMAP_JSON_DIR, `${roadmapId}.json`));
|
||||
const groups = roadmapJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
@@ -155,13 +106,50 @@ async function run() {
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
|
||||
const writePromises = [];
|
||||
for (let group of groups) {
|
||||
writePromises.push(writeFileForGroup(group, topicUrlToPathMapping));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
console.log('Waiting for all files to be written...');
|
||||
await Promise.all(writePromises);
|
||||
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()
|
||||
@@ -53,12 +53,12 @@ function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || (!sortOrder && !controlName.startsWith('check:'))) {
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '').replace(/^check:/, '');
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
@@ -84,9 +84,8 @@ function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
|
||||
const roadmap = require(path.join(
|
||||
__dirname,
|
||||
`../src/data/roadmaps/${roadmapId}/${roadmapId}`
|
||||
`../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
|
||||
@@ -22,7 +22,7 @@ function removeAllSponsors(baseContentDir) {
|
||||
|
||||
const contentDir = fs.readdirSync(contentDirPath);
|
||||
contentDir.forEach((content) => {
|
||||
console.log('Removing sponsors from: ', content);
|
||||
console.log('Removing sponsor from: ', content);
|
||||
|
||||
const pageFilePath = path.join(contentDirPath, content, `${content}.md`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
@@ -35,7 +35,7 @@ function removeAllSponsors(baseContentDir) {
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsors;
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
@@ -87,23 +87,27 @@ function addPageSponsor({
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
const sponsors = frontmatterObj.sponsors || [];
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const frontmatterValues = Object.entries(frontmatterObj);
|
||||
const roadmapLabel = frontmatterObj.briefTitle;
|
||||
|
||||
sponsors.push({
|
||||
url: redirectUrl,
|
||||
title: adTitle,
|
||||
imageUrl,
|
||||
description: adDescription,
|
||||
page: roadmapLabel,
|
||||
company,
|
||||
});
|
||||
|
||||
// Insert sponsor data at 10 index i.e. after
|
||||
// roadmap dimensions in the frontmatter
|
||||
frontmatterValues.splice(10, 0, ['sponsors', sponsors]);
|
||||
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);
|
||||
|
||||
@@ -16,7 +16,7 @@ For new roadmaps, submit a roadmap by providing [a textual roadmap similar to th
|
||||
|
||||
For the existing roadmaps, please follow the details listed for the nature of contribution:
|
||||
|
||||
- **Fixing Typos** — Make your changes in the [roadmap JSON file](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/roadmaps)
|
||||
- **Fixing Typos** — Make your changes in the [roadmap JSON file](https://github.com/kamranahmedse/developer-roadmap/tree/master/public/jsons)
|
||||
- **Adding or Removing Nodes** — Please open an issue with your suggestion.
|
||||
|
||||
**Note:** Please note that our goal is not to have the biggest list of items. Our goal is to list items or skills most relevant today.
|
||||
@@ -30,12 +30,11 @@ Find [the content directory inside the relevant roadmap](https://github.com/kamr
|
||||
|
||||
## Guidelines
|
||||
|
||||
- <p><strong>Adding everything available out there is not the goal!</strong><br />
|
||||
- <p><strong>Adding everything available out there is not the goal!</strong><br />
|
||||
The roadmaps represent the skillset most valuable today, i.e., if you were to enter any of the listed fields today, what would you learn?! There might be things that are of-course being used today but prioritize the things that are most in demand today, e.g., agreed that lots of people are using angular.js today but you wouldn't want to learn that instead of React, Angular, or Vue. Use your critical thinking to filter out non-essential stuff. Give honest arguments for why the resource should be included.</p>
|
||||
- <p><strong>Do not add things you have not evaluated personally!</strong><br />
|
||||
- <p><strong>Do not add things you have not evaluated personally!</strong><br />
|
||||
Use your critical thinking to filter out non-essential stuff. Give honest arguments for why the resource should be included. Have you read this book? Can you give a short article?</p>
|
||||
- <p><strong>Create a Single PR for Content Additions</strong></p>
|
||||
|
||||
If you are planning to contribute by adding content to the roadmaps, I recommend you to clone the repository, add content to the [content directory of the roadmap](./src/data/roadmaps/) and create a single PR to make it easier for me to review and merge the PR.
|
||||
If you are planning to contribute by adding content to the roadmaps, I recommend you to clone the repository, add content to the [content directory of the roadmap](./content/roadmaps/) and create a single PR to make it easier for me to review and merge the PR.
|
||||
- Write meaningful commit messages
|
||||
- Look at the existing issues/pull requests before opening new ones
|
||||
|
||||
61
package.json
@@ -4,60 +4,49 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev --port 3000",
|
||||
"dev": "astro dev",
|
||||
"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 scripts/compress-jsons.cjs",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node scripts/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node scripts/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node scripts/roadmap-content.cjs",
|
||||
"best-practice-dirs": "node scripts/best-practice-dirs.cjs",
|
||||
"best-practice-content": "node scripts/best-practice-content.cjs",
|
||||
"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/react": "^3.0.0",
|
||||
"@astrojs/sitemap": "^1.3.3",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@astrojs/preact": "^2.1.0",
|
||||
"@astrojs/sitemap": "^1.2.2",
|
||||
"@astrojs/tailwind": "^3.1.1",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.1",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"astro": "^3.0.5",
|
||||
"astro-compress": "^2.0.8",
|
||||
"dracula-prism": "^2.1.13",
|
||||
"jose": "^4.14.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"nanostores": "^0.9.2",
|
||||
"@nanostores/preact": "^0.3.1",
|
||||
"astro": "^2.2.3",
|
||||
"astro-compress": "^1.1.35",
|
||||
"jose": "^4.13.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"nanostores": "^0.7.4",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.10.12",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.0.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwindcss": "^3.3.3"
|
||||
"npm-check-updates": "^16.10.8",
|
||||
"preact": "^10.13.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.5",
|
||||
"tailwindcss": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"openai": "^3.3.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0"
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
4326
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
13
public/favicon.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 691 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="63" height="24" viewBox="0 0 63 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="63" height="24" rx="7" fill="#563AFF"/>
|
||||
<path d="M27.2629 16.7273H25.2856L28.2984 8H30.6763L33.6848 16.7273H31.7075L29.5214 9.99432H29.4533L27.2629 16.7273ZM27.1393 13.2969H31.8098V14.7372H27.1393V13.2969Z" fill="white"/>
|
||||
<path d="M37.829 16.7273H34.7352V8H37.8545C38.7324 8 39.4881 8.17472 40.1216 8.52415C40.7551 8.87074 41.2423 9.36932 41.5832 10.0199C41.927 10.6705 42.0989 11.4489 42.0989 12.3551C42.0989 13.2642 41.927 14.0455 41.5832 14.6989C41.2423 15.3523 40.7523 15.8537 40.1131 16.2031C39.4767 16.5526 38.7153 16.7273 37.829 16.7273ZM36.5804 15.1463H37.7523C38.2977 15.1463 38.7565 15.0497 39.1287 14.8565C39.5037 14.6605 39.7849 14.358 39.9724 13.9489C40.1628 13.5369 40.2579 13.0057 40.2579 12.3551C40.2579 11.7102 40.1628 11.1832 39.9724 10.7741C39.7849 10.3651 39.5051 10.0639 39.1329 9.87074C38.7608 9.67756 38.302 9.58097 37.7565 9.58097H36.5804V15.1463Z" fill="white"/>
|
||||
<path d="M46.5594 16.7273H43.4657V8H46.585C47.4628 8 48.2185 8.17472 48.8521 8.52415C49.4856 8.87074 49.9728 9.36932 50.3137 10.0199C50.6574 10.6705 50.8293 11.4489 50.8293 12.3551C50.8293 13.2642 50.6574 14.0455 50.3137 14.6989C49.9728 15.3523 49.4827 15.8537 48.8435 16.2031C48.2072 16.5526 47.4458 16.7273 46.5594 16.7273ZM45.3109 15.1463H46.4827C47.0282 15.1463 47.487 15.0497 47.8592 14.8565C48.2342 14.6605 48.5154 14.358 48.7029 13.9489C48.8932 13.5369 48.9884 13.0057 48.9884 12.3551C48.9884 11.7102 48.8932 11.1832 48.7029 10.7741C48.5154 10.3651 48.2356 10.0639 47.8634 9.87074C47.4913 9.67756 47.0324 9.58097 46.487 9.58097H45.3109V15.1463Z" fill="white"/>
|
||||
<path d="M10 12H18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 8V16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="89" height="24" viewBox="0 0 89 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="89" height="24" rx="7" fill="black"/>
|
||||
<path d="M23.8217 17V7.54545H27.5518C28.2659 7.54545 28.8752 7.67318 29.38 7.92862C29.8878 8.18099 30.274 8.53954 30.5387 9.00426C30.8065 9.46591 30.9403 10.0091 30.9403 10.6339C30.9403 11.2617 30.8049 11.8018 30.5341 12.2543C30.2633 12.7036 29.8709 13.0483 29.3569 13.2884C28.846 13.5284 28.2274 13.6484 27.5011 13.6484H25.0036V12.0419H27.1779C27.5595 12.0419 27.8765 11.9896 28.1289 11.8849C28.3813 11.7803 28.569 11.6233 28.6921 11.4141C28.8183 11.2048 28.8814 10.9447 28.8814 10.6339C28.8814 10.32 28.8183 10.0553 28.6921 9.83984C28.569 9.62441 28.3797 9.46129 28.1243 9.3505C27.8719 9.23662 27.5534 9.17969 27.1687 9.17969H25.8207V17H23.8217ZM28.9276 12.6974L31.2773 17H29.0707L26.7717 12.6974H28.9276ZM32.353 17V7.54545H38.7237V9.19354H34.3519V11.4464H38.396V13.0945H34.3519V15.3519H38.7422V17H32.353ZM40.3129 7.54545H42.7781L45.3818 13.8977H45.4926L48.0963 7.54545H50.5615V17H48.6226V10.8462H48.5441L46.0974 16.9538H44.7771L42.3303 10.8232H42.2519V17H40.3129V7.54545ZM60.8967 12.2727C60.8967 13.3037 60.7012 14.1809 60.3104 14.9041C59.9226 15.6274 59.3932 16.1798 58.7223 16.5614C58.0545 16.94 57.3035 17.1293 56.4695 17.1293C55.6293 17.1293 54.8752 16.9384 54.2074 16.5568C53.5395 16.1752 53.0117 15.6228 52.6239 14.8995C52.2362 14.1763 52.0423 13.3007 52.0423 12.2727C52.0423 11.2417 52.2362 10.3646 52.6239 9.64134C53.0117 8.91809 53.5395 8.36719 54.2074 7.98864C54.8752 7.60701 55.6293 7.41619 56.4695 7.41619C57.3035 7.41619 58.0545 7.60701 58.7223 7.98864C59.3932 8.36719 59.9226 8.91809 60.3104 9.64134C60.7012 10.3646 60.8967 11.2417 60.8967 12.2727ZM58.87 12.2727C58.87 11.6049 58.77 11.0417 58.57 10.5831C58.373 10.1245 58.0945 9.77675 57.7344 9.53977C57.3743 9.30279 56.9527 9.1843 56.4695 9.1843C55.9863 9.1843 55.5646 9.30279 55.2045 9.53977C54.8445 9.77675 54.5644 10.1245 54.3643 10.5831C54.1674 11.0417 54.0689 11.6049 54.0689 12.2727C54.0689 12.9406 54.1674 13.5038 54.3643 13.9624C54.5644 14.4209 54.8445 14.7687 55.2045 15.0057C55.5646 15.2427 55.9863 15.3612 56.4695 15.3612C56.9527 15.3612 57.3743 15.2427 57.7344 15.0057C58.0945 14.7687 58.373 14.4209 58.57 13.9624C58.77 13.5038 58.87 12.9406 58.87 12.2727ZM63.5523 7.54545L65.8374 14.7287H65.9252L68.2149 7.54545H70.4308L67.1716 17H64.5956L61.3318 7.54545H63.5523ZM71.5688 17V7.54545H77.9395V9.19354H73.5677V11.4464H77.6118V13.0945H73.5677V15.3519H77.958V17H71.5688Z" fill="white"/>
|
||||
<path d="M8 12L17 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 71 KiB |
BIN
public/images/partners/ambassador-img.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 92 KiB |
BIN
public/images/partners/devops-ebook.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 17 KiB |
BIN
public/images/partners/kubecampus.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
public/jsons/roadmaps/angular.json
Normal file
1
public/jsons/roadmaps/aspnet-core.json
Normal file
1
public/jsons/roadmaps/backend.json
Normal file
1
public/jsons/roadmaps/blockchain.json
Normal file
1
public/jsons/roadmaps/computer-science.json
Normal file
1
public/jsons/roadmaps/cyber-security.json
Normal file
1
public/jsons/roadmaps/design-system.json
Normal file
1
public/jsons/roadmaps/devops.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/golang.json
Normal file
1
public/jsons/roadmaps/graphql.json
Normal file
1
public/jsons/roadmaps/java.json
Normal file
1
public/jsons/roadmaps/javascript.json
Normal file
1
public/jsons/roadmaps/kubernetes.json
Normal file
1
public/jsons/roadmaps/mongodb.json
Normal file
1
public/jsons/roadmaps/nodejs.json
Normal file
1
public/jsons/roadmaps/python.json
Normal file
1
public/jsons/roadmaps/qa.json
Normal file
1
public/jsons/roadmaps/react.json
Normal file
1
public/jsons/roadmaps/software-architect.json
Normal file
1
public/jsons/roadmaps/software-design-architecture.json
Normal file
1
public/jsons/roadmaps/spring-boot.json
Normal file
1
public/jsons/roadmaps/system-design.json
Normal file
1
public/jsons/roadmaps/typescript.json
Normal file
1
public/jsons/roadmaps/ux-design.json
Normal file
1
public/jsons/roadmaps/vue.json
Normal file
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 773 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 550 KiB |
34
readme.md
@@ -4,16 +4,16 @@
|
||||
<p align="center">Community driven roadmaps, articles and resources for developers<p>
|
||||
<p align="center">
|
||||
<a href="https://roadmap.sh/roadmaps">
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-Roadmaps%20-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
</a>
|
||||
<a href="https://roadmap.sh/best-practices">
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-Best%20Practices-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="best practices" />
|
||||
<img src="https://img.shields.io/badge/-Roadmaps%20-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
</a>
|
||||
<a href="https://youtube.com/theroadmap?sub_confirmation=1">
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-Videos-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
|
||||
<img src="https://img.shields.io/badge/-Videos-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
|
||||
</a>
|
||||
<a href="https://github.com/kamranahmedse/developer-roadmap/tree/0471d44c8fae58b6a36a7c57bba12253916d0249/translations">
|
||||
<img src="https://img.shields.io/badge/-Translations-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
|
||||
</a>
|
||||
<a href="https://www.youtube.com/channel/UCA0H2KIWgWTwpTFjSxp0now?sub_confirmation=1">
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
<img src="https://img.shields.io/badge/%E2%9D%A4-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
@@ -30,33 +30,28 @@ Roadmaps are now interactive, you can click the nodes to read more about the top
|
||||
|
||||
Here is the list of available roadmaps with more being actively worked upon.
|
||||
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend) / [Frontend Beginner Roadmap](https://roadmap.sh/frontend?r=frontend-beginner)
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend)
|
||||
- [Backend Roadmap](https://roadmap.sh/backend)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
|
||||
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops)
|
||||
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
|
||||
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
|
||||
- [QA Roadmap](https://roadmap.sh/qa)
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [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)
|
||||
- [C++ Roadmap](https://roadmap.sh/cpp)
|
||||
- [React Roadmap](https://roadmap.sh/react)
|
||||
- [React Native Roadmap](https://roadmap.sh/react-native)
|
||||
- [Vue Roadmap](https://roadmap.sh/vue)
|
||||
- [Angular Roadmap](https://roadmap.sh/angular)
|
||||
- [Node.js Roadmap](https://roadmap.sh/nodejs)
|
||||
- [GraphQL Roadmap](https://roadmap.sh/graphql)
|
||||
- [Android Roadmap](https://roadmap.sh/android)
|
||||
- [Flutter Roadmap](https://roadmap.sh/flutter)
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [Go Roadmap](https://roadmap.sh/golang)
|
||||
- [Java Roadmap](https://roadmap.sh/java)
|
||||
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
|
||||
- [Design System Roadmap](https://roadmap.sh/design-system)
|
||||
- [PostgreSQL Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [SQL Roadmap](https://roadmap.sh/sql)
|
||||
- [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)
|
||||
@@ -64,12 +59,9 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Cyber Security Roadmap](https://roadmap.sh/cyber-security)
|
||||
- [MongoDB Roadmap](https://roadmap.sh/mongodb)
|
||||
- [UX Design Roadmap](https://roadmap.sh/ux-design)
|
||||
- [Docker Roadmap](https://roadmap.sh/docker)
|
||||
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
|
||||
|
||||
We have also added a new form of visual content covering best practices:
|
||||
|
||||
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
|
||||
- [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)
|
||||
@@ -96,12 +88,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Note: use the `depth` parameter to reduce the clone size and speed up the clone.
|
||||
|
||||
```sh
|
||||
git clone --depth=1 https://github.com/kamranahmedse/developer-roadmap.git
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
> Have a look at [contribution docs](./contributing.md) for how to update any of the roadmaps
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY;
|
||||
const ALL_BEST_PRACTICES_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const BEST_PRACTICE_JSON_DIR = path.join(
|
||||
__dirname,
|
||||
'../public/jsons/best-practices'
|
||||
);
|
||||
|
||||
const bestPracticeId = process.argv[2];
|
||||
const bestPracticeTitle = bestPracticeId.replace(/-/g, ' ');
|
||||
|
||||
const allowedBestPracticeIds = fs.readdirSync(ALL_BEST_PRACTICES_DIR);
|
||||
if (!bestPracticeId) {
|
||||
console.error('bestPracticeId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedBestPracticeIds.includes(bestPracticeId)) {
|
||||
console.error(`Invalid bestPractice key ${bestPracticeId}`);
|
||||
console.error(`Allowed keys are ${allowedBestPracticeIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
ALL_BEST_PRACTICES_DIR,
|
||||
bestPracticeId,
|
||||
'content'
|
||||
);
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(folderPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
getFilesInFolder(filePath, fileList);
|
||||
} else if (stats.isFile()) {
|
||||
const fileUrl = filePath
|
||||
.replace(BEST_PRACTICE_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
|
||||
|
||||
fileList[fileUrl] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
function writeTopicContent(topicTitle) {
|
||||
let prompt = `I am reading a guide that has best practices about "${bestPracticeTitle}". I want to know more about "${topicTitle}". Write me a brief introductory paragraph about this and some tips on how I make sure of this? Behave as if you are the author of the guide.`;
|
||||
|
||||
console.log(`Generating '${topicTitle}'...`);
|
||||
|
||||
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 writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
const topicId = group?.properties?.controlName;
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = `/${topicId}`;
|
||||
if (currTopicUrl.startsWith('/check:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentFilePath = topicUrlToPathMapping[currTopicUrl];
|
||||
|
||||
if (!contentFilePath) {
|
||||
console.log(`Missing file for: ${currTopicUrl}`);
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFileContent = fs.readFileSync(contentFilePath, 'utf8');
|
||||
const isFileEmpty = currentFileContent.replace(/^#.+/, ``).trim() === '';
|
||||
|
||||
if (!isFileEmpty) {
|
||||
console.log(`Ignoring ${topicId}. Not empty.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let newFileContent = `# ${topicTitle}`;
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log(`Writing ${topicId}..`);
|
||||
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
return;
|
||||
}
|
||||
|
||||
const topicContent = await writeTopicContent(topicTitle);
|
||||
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]);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(BEST_PRACTICE_CONTENT_DIR);
|
||||
|
||||
const bestPracticeJson = require(path.join(
|
||||
BEST_PRACTICE_JSON_DIR,
|
||||
`${bestPracticeId}.json`
|
||||
));
|
||||
const groups = bestPracticeJson?.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('----------------------------------------');
|
||||
}
|
||||
|
||||
const writePromises = [];
|
||||
for (let group of groups) {
|
||||
writePromises.push(writeFileForGroup(group, topicUrlToPathMapping));
|
||||
}
|
||||
|
||||
console.log('Waiting for all files to be written...');
|
||||
await Promise.all(writePromises);
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
console.log('Done');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
const csv = require('csv-parser');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const csvFilePath = path.join(__dirname, '../data.csv');
|
||||
|
||||
const results = {};
|
||||
const pageSummary = {};
|
||||
|
||||
fs.createReadStream(csvFilePath)
|
||||
.pipe(
|
||||
csv({
|
||||
separator: ',',
|
||||
mapHeaders: ({ header, index }) =>
|
||||
header.toLowerCase().replace(/ /g, '_'),
|
||||
mapValues: ({ header, index, value }) => {
|
||||
if (header === 'page') {
|
||||
return (
|
||||
value
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/`/g, '')
|
||||
.replace(/\?r=/g, '#r#')
|
||||
.replace(/\?.+?$/g, '')
|
||||
.replace(/#r#/g, '?r=')
|
||||
.replace(/\/$/g, '') || '/'
|
||||
);
|
||||
}
|
||||
|
||||
if (header !== 'month_of_year') {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
})
|
||||
)
|
||||
.on('data', (data) => {
|
||||
const { page, month_of_year, unique_pageviews, users } = data;
|
||||
const pageData = results[page] || {};
|
||||
const existingPageMonthData = pageData[month_of_year] || {};
|
||||
|
||||
const existingViews = existingPageMonthData.views || 0;
|
||||
const existingUsers = existingPageMonthData.users || 0;
|
||||
|
||||
const newViews = existingViews + unique_pageviews;
|
||||
const newUsers = existingUsers + users;
|
||||
|
||||
pageData[month_of_year] = {
|
||||
views: newViews,
|
||||
users: newUsers,
|
||||
};
|
||||
|
||||
results[page] = pageData;
|
||||
|
||||
pageSummary[page] = pageSummary[page] || { views: 0, users: 0 };
|
||||
pageSummary[page].views += unique_pageviews;
|
||||
pageSummary[page].users += users;
|
||||
})
|
||||
.on('end', () => {
|
||||
const csvHeader = [
|
||||
'Page',
|
||||
'Jan 2022',
|
||||
'Feb 2022',
|
||||
'Mar 2022',
|
||||
'Apr 2022',
|
||||
'May 2022',
|
||||
'Jun 2022',
|
||||
'Jul 2022',
|
||||
'Aug 2022',
|
||||
'Sep 2022',
|
||||
'Oct 2022',
|
||||
'Nov 2022',
|
||||
'Dec 2022',
|
||||
'Jan 2023',
|
||||
'Feb 2023',
|
||||
'Mar 2023',
|
||||
'Apr 2023',
|
||||
'May 2023',
|
||||
'Jun 2023',
|
||||
'Jul 2023',
|
||||
'Aug 2023',
|
||||
'Sep 2023',
|
||||
'Oct 2023',
|
||||
'Nov 2023',
|
||||
'Dec 2023',
|
||||
];
|
||||
|
||||
const csvRows = Object.keys(pageSummary)
|
||||
.filter(pageUrl => pageSummary[pageUrl].views > 10)
|
||||
.filter(pageUrl => !['/upcoming', '/pdfs', '/signup', '/login', '/@'].includes(pageUrl))
|
||||
.sort((pageA, pageB) => {
|
||||
const aViews = pageSummary[pageA].views;
|
||||
const bViews = pageSummary[pageB].views;
|
||||
|
||||
return bViews - aViews;
|
||||
})
|
||||
.map((pageUrl) => {
|
||||
const rawPageResult = results[pageUrl];
|
||||
const pageResultCsvRow = [];
|
||||
|
||||
csvHeader.forEach((csvHeaderItem) => {
|
||||
if (csvHeaderItem === 'Page') {
|
||||
pageResultCsvRow.push(pageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const csvHeaderItemAlt = csvHeaderItem
|
||||
.replace(/ /g, '_')
|
||||
.toLowerCase();
|
||||
|
||||
const result = rawPageResult[csvHeaderItem || csvHeaderItemAlt] || {};
|
||||
const views = result.views || 0;
|
||||
const users = result.users || 0;
|
||||
|
||||
pageResultCsvRow.push(users);
|
||||
});
|
||||
|
||||
return pageResultCsvRow;
|
||||
});
|
||||
|
||||
const finalCsvRows = [csvHeader, ...csvRows];
|
||||
const csvRowStrings = finalCsvRows.map((row) => {
|
||||
return row.join(',');
|
||||
});
|
||||
|
||||
const csvString = csvRowStrings.join('\n');
|
||||
fs.writeFileSync(path.join(__dirname, '../data-agg.csv'), csvString);
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
---
|
||||
import AstroIcon from './AstroIcon.astro';
|
||||
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
|
||||
import { SidebarFriendsCounter } from './Friends/SidebarFriendsCounter';
|
||||
|
||||
export interface Props {
|
||||
activePageId: string;
|
||||
activePageTitle: string;
|
||||
hasDesktopSidebar?: boolean;
|
||||
}
|
||||
|
||||
const { hasDesktopSidebar = true, activePageId, activePageTitle } = Astro.props;
|
||||
|
||||
const sidebarLinks = [
|
||||
{
|
||||
href: '/account',
|
||||
title: 'Activity',
|
||||
id: 'activity',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'analytics',
|
||||
classes: 'h-3 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/friends',
|
||||
title: 'Friends',
|
||||
id: 'friends',
|
||||
isNew: true,
|
||||
icon: {
|
||||
glyph: 'users',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/road-card',
|
||||
title: 'Card',
|
||||
id: 'road-card',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'badge',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/update-profile',
|
||||
title: 'Profile',
|
||||
id: 'profile',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'user',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/settings',
|
||||
title: 'Settings',
|
||||
id: 'settings',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'cog',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class='relative mb-5 block border-b p-4 shadow-inner md:hidden'>
|
||||
<button
|
||||
class='flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-sm font-medium text-gray-900'
|
||||
id='settings-menu'
|
||||
>
|
||||
{activePageTitle}
|
||||
<AstroIcon icon='dropdown' />
|
||||
</button>
|
||||
<ul
|
||||
id='settings-menu-dropdown'
|
||||
class='absolute left-0 right-0 z-10 mt-1 hidden space-y-1.5 bg-white p-2 shadow-lg'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href='/team'
|
||||
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
|
||||
activePageId === 'team' ? 'bg-slate-100' : ''
|
||||
}`}
|
||||
>
|
||||
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
|
||||
Teams
|
||||
</a>
|
||||
</li>
|
||||
{
|
||||
sidebarLinks.map((sidebarLink) => {
|
||||
const isActive = activePageId === sidebarLink.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={sidebarLink.href}
|
||||
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
|
||||
isActive ? 'bg-slate-100' : ''
|
||||
}`}
|
||||
>
|
||||
<AstroIcon
|
||||
icon={sidebarLink.icon.glyph}
|
||||
class={`${sidebarLink.icon.classes} mr-2`}
|
||||
/>
|
||||
{sidebarLink.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class='container flex min-h-screen items-stretch'>
|
||||
<!-- Start Desktop Sidebar -->
|
||||
{
|
||||
hasDesktopSidebar && (
|
||||
<aside class='hidden w-[195px] shrink-0 border-r border-slate-200 py-10 md:block'>
|
||||
<TeamDropdown client:load />
|
||||
|
||||
<nav>
|
||||
<ul class='space-y-1'>
|
||||
{sidebarLinks.map((sidebarLink) => {
|
||||
const isActive = activePageId === sidebarLink.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={sidebarLink.href}
|
||||
class={`font-regular flex w-full items-center border-r-2 px-2 py-1.5 text-sm ${
|
||||
isActive
|
||||
? 'border-r-black bg-gray-100 text-black'
|
||||
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span class='flex flex-grow items-center'>
|
||||
<AstroIcon
|
||||
icon={sidebarLink.icon.glyph}
|
||||
class={`${sidebarLink.icon.classes} mr-2`}
|
||||
/>
|
||||
{sidebarLink.title}
|
||||
</span>
|
||||
|
||||
{sidebarLink.isNew &&
|
||||
sidebarLink.id !== 'friends' &&
|
||||
!isActive && (
|
||||
<span class='relative mr-1 flex items-center'>
|
||||
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
|
||||
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{sidebarLink.id === 'friends' && (
|
||||
<SidebarFriendsCounter client:load />
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
<!-- /End Desktop Sidebar -->
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'grow px-0 py-0 md:py-10',
|
||||
{ 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar },
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const menuButton = document.getElementById('settings-menu');
|
||||
const menuDropdown = document.getElementById('settings-menu-dropdown');
|
||||
|
||||
menuButton?.addEventListener('click', () => {
|
||||
menuDropdown?.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menuButton?.contains(e.target as Node)) {
|
||||
menuDropdown?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,56 +0,0 @@
|
||||
type ActivityCountersType = {
|
||||
done: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
learning: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
streak: {
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ActivityCounterType = {
|
||||
text: string;
|
||||
count: string;
|
||||
};
|
||||
|
||||
function ActivityCounter(props: ActivityCounterType) {
|
||||
const { text, count } = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
|
||||
<h2 className="text-base sm:text-5xl font-bold">
|
||||
{count}
|
||||
</h2>
|
||||
<p className="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivityCounters(props: ActivityCountersType) {
|
||||
const { done, learning, streak } = props;
|
||||
|
||||
return (
|
||||
<div className="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
|
||||
<div className="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
|
||||
<ActivityCounter
|
||||
text={'Topics Completed'}
|
||||
count={`${done?.total || 0}`}
|
||||
/>
|
||||
|
||||
<ActivityCounter
|
||||
text={'Currently Learning'}
|
||||
count={`${learning?.total || 0}`}
|
||||
/>
|
||||
|
||||
<ActivityCounter
|
||||
text={'Visit Streak'}
|
||||
count={`${streak?.count || 0}d`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { ActivityCounters } from './ActivityCounters';
|
||||
import { ResourceProgress } from './ResourceProgress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { EmptyActivity } from './EmptyActivity';
|
||||
|
||||
export type ActivityResponse = {
|
||||
done: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
learning: {
|
||||
today: number;
|
||||
total: number;
|
||||
roadmaps: {
|
||||
title: string;
|
||||
id: string;
|
||||
learning: number;
|
||||
done: number;
|
||||
total: number;
|
||||
skipped: number;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
bestPractices: {
|
||||
title: string;
|
||||
id: string;
|
||||
learning: number;
|
||||
done: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
};
|
||||
streak: {
|
||||
count: number;
|
||||
firstVisitAt: Date | null;
|
||||
lastVisitAt: Date | null;
|
||||
};
|
||||
activity: {
|
||||
type: 'done' | 'learning' | 'pending' | 'skipped';
|
||||
createdAt: Date;
|
||||
metadata: {
|
||||
resourceId?: string;
|
||||
resourceType?: 'roadmap' | 'best-practice';
|
||||
topicId?: string;
|
||||
topicLabel?: string;
|
||||
resourceTitle?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export function ActivityPage() {
|
||||
const [activity, setActivity] = useState<ActivityResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
async function loadActivity() {
|
||||
const { error, response } = await httpGet<ActivityResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`
|
||||
);
|
||||
|
||||
if (!response || error) {
|
||||
console.error('Error loading activity');
|
||||
console.error(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setActivity(response);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const learningRoadmaps = activity?.learning.roadmaps || [];
|
||||
const learningBestPractices = activity?.learning.bestPractices || [];
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActivityCounters
|
||||
done={activity?.done || { today: 0, total: 0 }}
|
||||
learning={activity?.learning || { today: 0, total: 0 }}
|
||||
streak={activity?.streak || { count: 0 }}
|
||||
/>
|
||||
|
||||
<div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
|
||||
{learningRoadmaps.length === 0 &&
|
||||
learningBestPractices.length === 0 && <EmptyActivity />}
|
||||
|
||||
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
|
||||
<>
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">
|
||||
Continue Following
|
||||
</h2>
|
||||
<div className="flex flex-col gap-3">
|
||||
{learningRoadmaps
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.map((roadmap) => (
|
||||
<ResourceProgress
|
||||
key={roadmap.id}
|
||||
doneCount={roadmap.done || 0}
|
||||
learningCount={roadmap.learning || 0}
|
||||
totalCount={roadmap.total || 0}
|
||||
skippedCount={roadmap.skipped || 0}
|
||||
resourceId={roadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.title}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{learningBestPractices
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.map((bestPractice) => (
|
||||
<ResourceProgress
|
||||
doneCount={bestPractice.done || 0}
|
||||
totalCount={bestPractice.total || 0}
|
||||
learningCount={bestPractice.learning || 0}
|
||||
resourceId={bestPractice.id}
|
||||
skippedCount={bestPractice.skipped || 0}
|
||||
resourceType={'best-practice'}
|
||||
title={bestPractice.title}
|
||||
updatedAt={bestPractice.updatedAt}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
|
||||
export function EmptyActivity() {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center">
|
||||
<img
|
||||
alt="no roadmaps"
|
||||
src={RoadmapIcon.src}
|
||||
className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
|
||||
/>
|
||||
<h2 className="text-lg sm:text-xl font-bold">No Progress</h2>
|
||||
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
|
||||
Progress will appear here as you start tracking your{' '}
|
||||
<a href="/roadmaps" className="mt-4 text-blue-500 hover:underline">
|
||||
Roadmaps
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="/best-practices" className="mt-4 text-blue-500 hover:underline">
|
||||
Best Practices
|
||||
</a>{' '}
|
||||
progress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { ProgressShareButton } from '../UserProgress/ProgressShareButton';
|
||||
import { useState } from 'react';
|
||||
|
||||
type ResourceProgressType = {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceId: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
totalCount: number;
|
||||
doneCount: number;
|
||||
learningCount: number;
|
||||
skippedCount: number;
|
||||
onCleared?: () => void;
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
export function ResourceProgress(props: ResourceProgressType) {
|
||||
const { showClearButton = true } = props;
|
||||
const toast = useToast();
|
||||
const [isClearing, setIsClearing] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
const {
|
||||
updatedAt,
|
||||
resourceType,
|
||||
resourceId,
|
||||
title,
|
||||
totalCount,
|
||||
learningCount,
|
||||
doneCount,
|
||||
skippedCount,
|
||||
onCleared,
|
||||
} = props;
|
||||
|
||||
async function clearProgress() {
|
||||
setIsClearing(true);
|
||||
const { error, response } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-clear-resource-progress`,
|
||||
{
|
||||
resourceId,
|
||||
resourceType,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error('Error clearing progress. Please try again.');
|
||||
console.error(error);
|
||||
setIsClearing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(`${resourceType}-${resourceId}-favorite`);
|
||||
localStorage.removeItem(`${resourceType}-${resourceId}-progress`);
|
||||
|
||||
setIsClearing(false);
|
||||
setIsConfirming(false);
|
||||
if (onCleared) {
|
||||
onCleared();
|
||||
}
|
||||
}
|
||||
|
||||
const url =
|
||||
resourceType === 'roadmap'
|
||||
? `/${resourceId}`
|
||||
: `/best-practices/${resourceId}`;
|
||||
|
||||
const totalMarked = doneCount + skippedCount;
|
||||
const progressPercentage = Math.round((totalMarked / totalCount) * 100);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
href={url}
|
||||
className="group relative flex cursor-pointer items-center rounded-t-md border p-3 text-gray-600 hover:border-gray-300 hover:text-black"
|
||||
>
|
||||
<span
|
||||
className={`absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10`}
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
></span>
|
||||
<span className="relative flex-1 cursor-pointer truncate">
|
||||
{title}
|
||||
</span>
|
||||
<span className="ml-1 cursor-pointer text-sm text-gray-400">
|
||||
{getRelativeTimeString(updatedAt)}
|
||||
</span>
|
||||
</a>
|
||||
<div className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
|
||||
<span className="hidden flex-1 gap-1 sm:flex">
|
||||
{doneCount > 0 && (
|
||||
<>
|
||||
<span>{doneCount} done</span> •
|
||||
</>
|
||||
)}
|
||||
{learningCount > 0 && (
|
||||
<>
|
||||
<span>{learningCount} in progress</span> •
|
||||
</>
|
||||
)}
|
||||
{skippedCount > 0 && (
|
||||
<>
|
||||
<span>{skippedCount} skipped</span> •
|
||||
</>
|
||||
)}
|
||||
<span>{totalCount} total</span>
|
||||
</span>
|
||||
<div className="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-start">
|
||||
<ProgressShareButton
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
className="text-xs font-normal"
|
||||
shareIconClassName="w-2.5 h-2.5 stroke-2"
|
||||
checkIconClassName="w-2.5 h-2.5"
|
||||
/>
|
||||
<span className={'hidden sm:block'}>•</span>
|
||||
|
||||
{showClearButton && (
|
||||
<>
|
||||
{!isConfirming && (
|
||||
<button
|
||||
className="text-red-500 hover:text-red-800"
|
||||
onClick={() => setIsConfirming(true)}
|
||||
disabled={isClearing}
|
||||
>
|
||||
{!isClearing && (
|
||||
<>
|
||||
Clear Progress <span>×</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isClearing && 'Processing...'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isConfirming && (
|
||||
<span>
|
||||
Are you sure?{' '}
|
||||
<button
|
||||
onClick={clearProgress}
|
||||
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
|
||||
>
|
||||
Yes
|
||||
</button>{' '}
|
||||
<button
|
||||
onClick={() => setIsConfirming(false)}
|
||||
className="text-red-500 underline hover:text-red-800"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../hooks/use-outside-click';
|
||||
import { OptionType, SearchSelector } from './SearchSelector';
|
||||
import type { PageType } from './CommandMenu/CommandMenu';
|
||||
import { CheckIcon } from './ReactIcons/CheckIcon';
|
||||
import { httpPut } from '../lib/http';
|
||||
import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector';
|
||||
import { Spinner } from './ReactIcons/Spinner';
|
||||
|
||||
type AddTeamRoadmapProps = {
|
||||
teamId: string;
|
||||
allRoadmaps: PageType[];
|
||||
availableRoadmaps: PageType[];
|
||||
onClose: () => void;
|
||||
onMakeChanges: (roadmapId: string) => void;
|
||||
setResourceConfigs: (config: TeamResourceConfig) => void;
|
||||
};
|
||||
|
||||
export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
|
||||
const {
|
||||
teamId,
|
||||
onMakeChanges,
|
||||
onClose,
|
||||
allRoadmaps,
|
||||
availableRoadmaps,
|
||||
setResourceConfigs,
|
||||
} = props;
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedRoadmap, setSelectedRoadmap] = useState<string>('');
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
async function addTeamResource(roadmapId: string) {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { error, response } = await httpPut<TeamResourceConfig>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-update-team-resource-config/${teamId}`,
|
||||
{
|
||||
teamId: teamId,
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
removed: [],
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setError(error?.message || 'Error adding roadmap');
|
||||
return;
|
||||
}
|
||||
|
||||
setResourceConfigs(response);
|
||||
}
|
||||
|
||||
useOutsideClick(popupBodyEl, () => {
|
||||
onClose();
|
||||
});
|
||||
|
||||
const selectedRoadmapTitle = allRoadmaps.find(
|
||||
(roadmap) => roadmap.id === selectedRoadmap
|
||||
)?.title;
|
||||
|
||||
return (
|
||||
<div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
|
||||
<div className="relative h-full w-full max-w-md p-4 md:h-auto">
|
||||
<div
|
||||
ref={popupBodyEl}
|
||||
className="popup-body relative rounded-lg bg-white p-4 shadow"
|
||||
>
|
||||
{isLoading && (
|
||||
<>
|
||||
<div className="flex items-center justify-center gap-2 py-8">
|
||||
<Spinner isDualRing={false} className="h-4 w-4" />
|
||||
<h2 className="font-medium">Loading...</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && selectedRoadmap && (
|
||||
<div className={'text-center'}>
|
||||
<CheckIcon additionalClasses="h-10 w-10 mx-auto opacity-20 mb-3 mt-4" />
|
||||
<h3 className="mb-1.5 text-2xl font-medium">
|
||||
{selectedRoadmapTitle} Added
|
||||
</h3>
|
||||
<p className="mb-4 text-sm leading-none text-gray-400">
|
||||
<button
|
||||
onClick={() => onMakeChanges(selectedRoadmap)}
|
||||
className="underline underline-offset-2 hover:text-gray-900"
|
||||
>
|
||||
Click here
|
||||
</button>{' '}
|
||||
to make changes to the roadmap.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRoadmap('');
|
||||
setError('');
|
||||
setIsLoading(false);
|
||||
}}
|
||||
type="button"
|
||||
className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
|
||||
>
|
||||
+ Add More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<>
|
||||
<h3 className="mb-1.5 text-2xl font-medium">Error</h3>
|
||||
<p className="mb-3 text-sm leading-none text-red-400">{error}</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && !selectedRoadmap && (
|
||||
<>
|
||||
<h3 className="mb-1.5 text-2xl font-medium">Add Roadmap</h3>
|
||||
<p className="mb-3 text-sm leading-none text-gray-400">
|
||||
Search and add a roadmap
|
||||
</p>
|
||||
|
||||
<SearchSelector
|
||||
options={availableRoadmaps.map((roadmap) => ({
|
||||
value: roadmap.id,
|
||||
label: roadmap.title,
|
||||
}))}
|
||||
onSelect={(option: OptionType) => {
|
||||
const roadmapId = option.value;
|
||||
addTeamResource(roadmapId).finally(() => {
|
||||
setIsLoading(false);
|
||||
setSelectedRoadmap(roadmapId);
|
||||
});
|
||||
}}
|
||||
inputClassName="mt-2 mb-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
|
||||
placeholder={'Search for roadmap'}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,4 +14,29 @@
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-139582634-1');
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
let trackEl = e.target;
|
||||
if (!trackEl.getAttribute('ga-category')) {
|
||||
trackEl = trackEl.closest('[ga-category]');
|
||||
}
|
||||
|
||||
if (!trackEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = trackEl.getAttribute('ga-category');
|
||||
const action = trackEl.getAttribute('ga-action');
|
||||
const label = trackEl.getAttribute('ga-label');
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.fireEvent({
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// To selectively enable/disable debug logs
|
||||
__DEBUG__: boolean;
|
||||
gtag: any;
|
||||
fireEvent: (props: {
|
||||
action: string;
|
||||
category: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
}) => void;
|
||||
fireEvent: (props: GAEventType) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export type GAEventType = {
|
||||
action: string;
|
||||
category: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the event on google analytics
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||
* @param props Event properties
|
||||
* @returns void
|
||||
*/
|
||||
window.fireEvent = (props) => {
|
||||
window.fireEvent = (props: GAEventType) => {
|
||||
const { action, category, label, value } = props;
|
||||
if (!window.gtag) {
|
||||
console.warn('Missing GTAG - Analytics disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
if (window.__DEBUG__) {
|
||||
console.log('Analytics event fired', props);
|
||||
}
|
||||
|
||||
|
||||