Compare commits
349 Commits
content/fl
...
feat/cmd-k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ad9dd5a3 | ||
|
|
bb23f57204 | ||
|
|
80dac2f7f7 | ||
|
|
c4f3c3529d | ||
|
|
f57b61a920 | ||
|
|
af05d795e3 | ||
|
|
44e5138b32 | ||
|
|
4d7cada25c | ||
|
|
b707aa02d8 | ||
|
|
c9c4516aa7 | ||
|
|
83057d65cd | ||
|
|
b886f20570 | ||
|
|
dacd2d898b | ||
|
|
a2490efa80 | ||
|
|
e087b79ade | ||
|
|
10b1a8cb07 | ||
|
|
f2c06462fa | ||
|
|
ad7ba44a2e | ||
|
|
7a72c96e79 | ||
|
|
d955044a3b | ||
|
|
b86fafd538 | ||
|
|
c52a4e6638 | ||
|
|
9d66da6bf9 | ||
|
|
4b76d0b7aa | ||
|
|
626026eebc | ||
|
|
fdd12acb8e | ||
|
|
02015826ff | ||
|
|
5d07ce32d8 | ||
|
|
3967b16d25 | ||
|
|
f325183691 | ||
|
|
a029850531 | ||
|
|
d3d2ae5889 | ||
|
|
4a049b2a7a | ||
|
|
fd349f2da8 | ||
|
|
f338bd5ecb | ||
|
|
a3470cd844 | ||
|
|
f4635d794f | ||
|
|
426fe44dc8 | ||
|
|
4ed39cec1a | ||
|
|
b1f0844546 | ||
|
|
88aa7e4024 | ||
|
|
471f6348f1 | ||
|
|
9dfb70c941 | ||
|
|
6fa7e0d1c0 | ||
|
|
5ccfa654ec | ||
|
|
1c67068eab | ||
|
|
f5ff2a0823 | ||
|
|
58503f67f3 | ||
|
|
5dd0479caf | ||
|
|
7441f1a203 | ||
|
|
4d3ebb0ac6 | ||
|
|
47d5716238 | ||
|
|
94d888a61e | ||
|
|
ddd43a1514 | ||
|
|
2cf94f981b | ||
|
|
f1973f63c2 | ||
|
|
dfb67e17d5 | ||
|
|
48239772f6 | ||
|
|
1cea9d0e13 | ||
|
|
6591c36ef4 | ||
|
|
41de9c47b0 | ||
|
|
0ba1a8a1d1 | ||
|
|
6fcb153244 | ||
|
|
7a8d97b1cd | ||
|
|
9e37076d0d | ||
|
|
f8e5661353 | ||
|
|
4d4cda6cac | ||
|
|
8b528f39f2 | ||
|
|
e1a04b4a20 | ||
|
|
f0e8ffe565 | ||
|
|
f9c1e64235 | ||
|
|
0174c9156b | ||
|
|
2ee81e6ff3 | ||
|
|
42ab5a3e9e | ||
|
|
e9fa663410 | ||
|
|
2d17a267be | ||
|
|
40371cdded | ||
|
|
6bb315a2fc | ||
|
|
fc2c9a1439 | ||
|
|
b50935ecd6 | ||
|
|
9b73d60c5d | ||
|
|
504ee8cf5e | ||
|
|
057bbddd9f | ||
|
|
4063979c2a | ||
|
|
da391fe9ed | ||
|
|
953ca9257c | ||
|
|
396bedd319 | ||
|
|
e05269f117 | ||
|
|
77c7ca8835 | ||
|
|
e877f5c610 | ||
|
|
42e1a79697 | ||
|
|
ce32cdc8a4 | ||
|
|
e2a0bd23c0 | ||
|
|
98f0ebde8b | ||
|
|
bc018f8b39 | ||
|
|
03bd478aaa | ||
|
|
67a8582c22 | ||
|
|
7533575df9 | ||
|
|
34fcd74d93 | ||
|
|
1558feb734 | ||
|
|
bc4d9f9e2f | ||
|
|
4142c7b51e | ||
|
|
e36a749223 | ||
|
|
e69d9b4238 | ||
|
|
3132a39816 | ||
|
|
03f9fa51ff | ||
|
|
e2062aefe9 | ||
|
|
855ba7bbfb | ||
|
|
ad71b6398d | ||
|
|
0ea0629104 | ||
|
|
8b2f12fcdd | ||
|
|
e66bff74bf | ||
|
|
58ea34bb49 | ||
|
|
275c2c3c88 | ||
|
|
f13c29adad | ||
|
|
ec9f836a1f | ||
|
|
589d157be5 | ||
|
|
a2719bc771 | ||
|
|
c5645299aa | ||
|
|
6aac3f296c | ||
|
|
137635f11a | ||
|
|
8487d2f443 | ||
|
|
a7bee1fea7 | ||
|
|
43292de507 | ||
|
|
bee30defb5 | ||
|
|
52649a2d3c | ||
|
|
be47ac6573 | ||
|
|
24ce27090e | ||
|
|
8dd0225720 | ||
|
|
9893e9f0a3 | ||
|
|
caf1cd0269 | ||
|
|
d21c1f6d0d | ||
|
|
9d38cf7650 | ||
|
|
d232d3bbd0 | ||
|
|
366d893df9 | ||
|
|
ff27561765 | ||
|
|
133642e05f | ||
|
|
7434ff71eb | ||
|
|
d081ecf5b3 | ||
|
|
d8a039690b | ||
|
|
56f0df549d | ||
|
|
b042161e29 | ||
|
|
db273210fd | ||
|
|
9370e262c0 | ||
|
|
e0f9bc8456 | ||
|
|
af211ab129 | ||
|
|
eab1fb31b2 | ||
|
|
c36fd71ec1 | ||
|
|
7d0e35d7ae | ||
|
|
59d881a77b | ||
|
|
b57e3ecc75 | ||
|
|
cd5c0c10a2 | ||
|
|
10a5e4c0ae | ||
|
|
432983631d | ||
|
|
29189062b9 | ||
|
|
84138d5049 | ||
|
|
c28ac4b078 | ||
|
|
66bdbd7458 | ||
|
|
907f820778 | ||
|
|
ec0a8a99ef | ||
|
|
a7a342c8e7 | ||
|
|
e0e26580fa | ||
|
|
f9d96d415f | ||
|
|
e1c22932be | ||
|
|
9dae1b3595 | ||
|
|
cdb642c8d4 | ||
|
|
71b43af862 | ||
|
|
69dccb3fcc | ||
|
|
ca6ddb4654 | ||
|
|
0ac6fc70ff | ||
|
|
5ddb021898 | ||
|
|
d227603a59 | ||
|
|
e0d70950ac | ||
|
|
148bfd8736 | ||
|
|
648985cefc | ||
|
|
9321ac6aa1 | ||
|
|
170ab3a6cf | ||
|
|
708fa31998 | ||
|
|
f83a1a6c3b | ||
|
|
dc1d7ef226 | ||
|
|
808bd40cce | ||
|
|
45ce59b10d | ||
|
|
e3f41ec0e3 | ||
|
|
4f821d0f1d | ||
|
|
ec1283a5dc | ||
|
|
4da909d358 | ||
|
|
0beb9ad239 | ||
|
|
c6213dde92 | ||
|
|
3d655965f6 | ||
|
|
c5c2ee3b2c | ||
|
|
cad0813eb6 | ||
|
|
f9c1e6e0a2 | ||
|
|
d3578756d4 | ||
|
|
5fe506324a | ||
|
|
bc007dcc9b | ||
|
|
42a5d5bba6 | ||
|
|
f908c5371d | ||
|
|
78964b9f65 | ||
|
|
9f11de60ed | ||
|
|
4ba28b702b | ||
|
|
45c6fc873f | ||
|
|
23b7c21502 | ||
|
|
d474d07ebb | ||
|
|
7d08572d78 | ||
|
|
17fc85f893 | ||
|
|
5ccba8d7c0 | ||
|
|
fefbb4f833 | ||
|
|
bcbc9c9d54 | ||
|
|
73d1d0e389 | ||
|
|
14856560c3 | ||
|
|
b097395c07 | ||
|
|
929be729e5 | ||
|
|
1eb8fab15e | ||
|
|
51233c8011 | ||
|
|
0081e9059c | ||
|
|
4e66148777 | ||
|
|
18e430be0b | ||
|
|
e39d0d93e0 | ||
|
|
9260dc36b5 | ||
|
|
f5f846ed73 | ||
|
|
7518d60013 | ||
|
|
59d9674d75 | ||
|
|
16fb03086e | ||
|
|
c0f46c5eed | ||
|
|
b5b8b92791 | ||
|
|
8f90dac32e | ||
|
|
e9f3a616d1 | ||
|
|
c28ed87247 | ||
|
|
53fb6313db | ||
|
|
cc933b238d | ||
|
|
48fba932b4 | ||
|
|
9961259ffb | ||
|
|
cab2054c1d | ||
|
|
d34affb420 | ||
|
|
ee4f0980bc | ||
|
|
37fdd010a8 | ||
|
|
aa04c51a12 | ||
|
|
7993f12d12 | ||
|
|
1a3265295c | ||
|
|
238245431b | ||
|
|
48c04055d5 | ||
|
|
596b8f56ac | ||
|
|
45267693e2 | ||
|
|
f932df8627 | ||
|
|
8dcf4b00c4 | ||
|
|
cb32a9610d | ||
|
|
01c090f62d | ||
|
|
60b1edcab9 | ||
|
|
d08887060f | ||
|
|
24a6c4930e | ||
|
|
e57b889f73 | ||
|
|
c5d14d2543 | ||
|
|
4f0b08ea93 | ||
|
|
47e2dbdd12 | ||
|
|
f1ad70acd9 | ||
|
|
ac230bbf29 | ||
|
|
d0861711ac | ||
|
|
74b2dda7f7 | ||
|
|
2b49fa3182 | ||
|
|
e2a8240e35 | ||
|
|
a7f45c0af1 | ||
|
|
77a6270bd7 | ||
|
|
64d3ad662c | ||
|
|
c8b8e12b64 | ||
|
|
8f94a5887e | ||
|
|
00b6217e63 | ||
|
|
1a0d7463eb | ||
|
|
983ee44632 | ||
|
|
f393cb186e | ||
|
|
70edfb0ac2 | ||
|
|
ed07d34d64 | ||
|
|
831521ae10 | ||
|
|
e29289f0dc | ||
|
|
0fd3eb0cc6 | ||
|
|
f58a77010b | ||
|
|
6303e31c0e | ||
|
|
dfc2d39427 | ||
|
|
5e75026424 | ||
|
|
7a4c077a90 | ||
|
|
e45c49a404 | ||
|
|
b6a0255f12 | ||
|
|
b741a0e1ee | ||
|
|
8200993af4 | ||
|
|
5c1d803892 | ||
|
|
dcf0f94af9 | ||
|
|
4ad8886aa0 | ||
|
|
a1143cd6cb | ||
|
|
f130c706da | ||
|
|
8068face54 | ||
|
|
39866117a6 | ||
|
|
df7aa17f86 | ||
|
|
ee6572660b | ||
|
|
9875a2d6f7 | ||
|
|
5b180e2597 | ||
|
|
6f05972493 | ||
|
|
4bd182e4d0 | ||
|
|
68d319cacb | ||
|
|
3e76df8d2a | ||
|
|
9d69477947 | ||
|
|
e0ead47fb1 | ||
|
|
253c88542f | ||
|
|
5bca9834fb | ||
|
|
dde6e3d3df | ||
|
|
6fe8fee25f | ||
|
|
f3622a1b1c | ||
|
|
a92bda38f4 | ||
|
|
b194d167be | ||
|
|
ec04b582a6 | ||
|
|
f55159a12b | ||
|
|
938c7796d1 | ||
|
|
e04bd9db05 | ||
|
|
7c837d14da | ||
|
|
cc05587d9e | ||
|
|
2172014d6e | ||
|
|
98d43e76b7 | ||
|
|
7665970813 | ||
|
|
d96e5890b9 | ||
|
|
659bd93094 | ||
|
|
a4dddfb19b | ||
|
|
12a4be2227 | ||
|
|
9edcb35acb | ||
|
|
1df4e4b836 | ||
|
|
49e78cf1c0 | ||
|
|
a4a29b4efa | ||
|
|
3e49e7f91d | ||
|
|
7627bc73b5 | ||
|
|
26eaa40dc1 | ||
|
|
45a0b53d5f | ||
|
|
7bac3c3444 | ||
|
|
62905bda7a | ||
|
|
179bf366cc | ||
|
|
59d47c5b1e | ||
|
|
d23ea8e577 | ||
|
|
07f001f8be | ||
|
|
754a91acef | ||
|
|
16c550211b | ||
|
|
a56710c43d | ||
|
|
00f94e031e | ||
|
|
d1556c85df | ||
|
|
1885d6d304 | ||
|
|
3b8c8316b3 | ||
|
|
034fd16a1f | ||
|
|
aa9bf2f263 | ||
|
|
6a5df98f4f | ||
|
|
ea02c8835a | ||
|
|
e13733a503 | ||
|
|
6f0ad58764 | ||
|
|
f68c303ffa | ||
|
|
b2c79ff395 |
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
PUBLIC_API_URL=http://api.roadmap.sh
|
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
|
||||
2
.github/workflows/deploy.yml
vendored
@@ -3,6 +3,8 @@ on:
|
||||
push:
|
||||
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
|
||||
|
||||
5
.gitignore
vendored
@@ -5,7 +5,7 @@ dist/
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
bin/developer-roadmap
|
||||
scripts/developer-roadmap
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
@@ -23,4 +23,5 @@ pnpm-debug.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
tests-examples
|
||||
tests-examples
|
||||
*.csv
|
||||
|
||||
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
app-dist
|
||||
dist
|
||||
.idea
|
||||
.github
|
||||
public
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
18
.prettierrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
options: {
|
||||
parser: 'astro',
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-astro'),
|
||||
require('prettier-plugin-tailwindcss'),
|
||||
],
|
||||
};
|
||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// https://astro.build/config
|
||||
import preact from '@astrojs/preact';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import compress from 'astro-compress';
|
||||
@@ -6,8 +7,9 @@ import { defineConfig } from 'astro/config';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
site: 'https://roadmap.sh/',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula',
|
||||
@@ -17,6 +19,24 @@ export default defineConfig({
|
||||
rehypeExternalLinks,
|
||||
{
|
||||
target: '_blank',
|
||||
rel: function (element) {
|
||||
const href = element.properties.href;
|
||||
const whiteListedStarts = [
|
||||
'/',
|
||||
'#',
|
||||
'mailto:',
|
||||
'https://github.com/kamranahmedse',
|
||||
'https://thenewstack.io',
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
];
|
||||
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return 'noopener noreferrer nofollow';
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -38,5 +58,6 @@ export default defineConfig({
|
||||
css: false,
|
||||
js: false,
|
||||
}),
|
||||
preact(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
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/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');
|
||||
@@ -1,28 +0,0 @@
|
||||
## 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`.
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the roadmaps
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(__dirname, `../public/jsons/roadmaps/${roadmapId}`));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param sortOrders Mapping from groupName to sort order
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, sortOrders, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(roadmapContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replace(/(^\d+?-)/g, '') // Remove sorting information
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
|
||||
// Attach sorting information to dirname
|
||||
// e.g. /roadmaps/100-frontend/content/internet
|
||||
// ———> /roadmaps/100-frontend/content/103-internet
|
||||
if (sortOrder) {
|
||||
parentDir = parentDir.replace(/(.+?)([^\/]+)?$/, `$1${sortOrder}-$2`);
|
||||
}
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
dirSortOrders,
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
@@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
@@ -23,7 +23,7 @@ For the existing roadmaps, please follow the details listed for the nature of co
|
||||
|
||||
## Adding Content
|
||||
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
|
||||
- Content must be in English.
|
||||
- Put a brief description about the topic on top of the file and the a list of links below with each link having title of the URL.
|
||||
|
||||
48
package.json
@@ -8,33 +8,47 @@
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier --write .",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"compress:jsons": "node scripts/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-content": "node bin/best-practice-content.cjs",
|
||||
"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",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"astro": "^1.9.2",
|
||||
"astro-compress": "^1.1.28",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.4"
|
||||
"@astrojs/preact": "^2.2.0",
|
||||
"@astrojs/sitemap": "^1.3.1",
|
||||
"@astrojs/tailwind": "^3.1.3",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.1",
|
||||
"@nanostores/preact": "^0.4.1",
|
||||
"astro": "^2.5.0",
|
||||
"astro-compress": "^1.1.43",
|
||||
"jose": "^4.14.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"nanostores": "^0.8.1",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.10.12",
|
||||
"preact": "^10.14.1",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@playwright/test": "^1.33.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^5.0.0",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-astro": "^0.7.2"
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.9.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const config: PlaywrightTestConfig = {
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: "http://localhost:3000",
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
3299
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 |
@@ -1,13 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 873 B |
BIN
public/guides/llms.png
Normal file
|
After Width: | Height: | Size: 691 KiB |
|
Before Width: | Height: | Size: 101 KiB |
BIN
public/images/default-avatar.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
BIN
public/images/features/in-progress.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/images/partners/ambassador-graphic-1.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/partners/ambassador-graphic-2.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/images/partners/apollo-event.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/images/partners/apollo-learning.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/partners/graphql-summit.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/images/partners/honeycomb-ebook.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
public/jsons/best-practices/api-security.json
Normal file
1
public/jsons/best-practices/aws.json
Normal file
1
public/jsons/best-practices/code-review.json
Normal file
1
public/jsons/roadmaps/cyber-security.json
Normal file
1
public/jsons/roadmaps/docker.json
Normal file
1
public/jsons/roadmaps/frontend-beginner.json
Normal file
4019
public/jsons/roadmaps/full-stack.json
Normal file
1
public/jsons/roadmaps/kubernetes.json
Normal file
1
public/jsons/roadmaps/mongodb.json
Normal file
1
public/jsons/roadmaps/postgresql-dba.json
Normal file
4119
public/jsons/roadmaps/prompt-engineering.json
Normal file
1
public/jsons/roadmaps/typescript.json
Normal file
1
public/jsons/roadmaps/ux-design.json
Normal file
BIN
public/pdfs/best-practices/api-security.pdf
Normal file
BIN
public/pdfs/best-practices/aws.pdf
Normal file
BIN
public/pdfs/best-practices/code-review.pdf
Normal file
BIN
public/pdfs/roadmaps/android.pdf
Normal file
BIN
public/pdfs/roadmaps/cyber-security.pdf
Normal file
BIN
public/pdfs/roadmaps/docker.pdf
Normal file
BIN
public/pdfs/roadmaps/full-stack.pdf
Normal file
BIN
public/pdfs/roadmaps/kubernetes.pdf
Normal file
BIN
public/pdfs/roadmaps/mongodb.pdf
Normal file
BIN
public/pdfs/roadmaps/postgresql-dba.pdf
Normal file
BIN
public/pdfs/roadmaps/prompt-engineering.pdf
Normal file
BIN
public/pdfs/roadmaps/typescript.pdf
Normal file
BIN
public/pdfs/roadmaps/ux-design.pdf
Normal file
BIN
public/roadmaps/cyber-security.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/roadmaps/docker.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
public/roadmaps/full-stack.png
Normal file
|
After Width: | Height: | Size: 345 KiB |
BIN
public/roadmaps/kubernetes.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
public/roadmaps/mongodb.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
public/roadmaps/typescript.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
public/roadmaps/ux-design.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
26
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/-Roadmaps%20-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="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" />
|
||||
</a>
|
||||
<a href="https://youtube.com/theroadmap?sub_confirmation=1">
|
||||
<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" />
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-Videos-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%9D%A4-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
<img src="https://img.shields.io/badge/%E2%9C%A8-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
@@ -33,11 +33,13 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend)
|
||||
- [Backend Roadmap](https://roadmap.sh/backend)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops)
|
||||
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
|
||||
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
|
||||
- [QA Roadmap](https://roadmap.sh/qa)
|
||||
- [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)
|
||||
@@ -54,6 +56,18 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Blockchain Roadmap](https://roadmap.sh/blockchain)
|
||||
- [ASP.NET Core Roadmap](https://roadmap.sh/aspnet-core)
|
||||
- [System Design Roadmap](https://roadmap.sh/system-design)
|
||||
- [Kubernetes Roadmap](https://roadmap.sh/kubernetes)
|
||||
- [Cyber Security Roadmap](https://roadmap.sh/cyber-security)
|
||||
- [MongoDB Roadmap](https://roadmap.sh/mongodb)
|
||||
- [UX Design Roadmap](https://roadmap.sh/ux-design)
|
||||
- [Docker Roadmap](https://roadmap.sh/docker)
|
||||
|
||||
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)
|
||||
|
||||

|
||||
|
||||
|
||||
173
scripts/best-practice-content.cjs
Normal file
@@ -0,0 +1,173 @@
|
||||
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);
|
||||
});
|
||||
155
scripts/best-practice-dirs.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
const allowedBestPracticeId = fs.readdirSync(BEST_PRACTICE_CONTENT_DIR);
|
||||
if (!bestPracticeId) {
|
||||
console.error('bestPractice is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedBestPracticeId.includes(bestPracticeId)) {
|
||||
console.error(`Invalid best practice key ${bestPracticeId}`);
|
||||
console.error(`Allowed keys are ${allowedBestPracticeId.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the best parctice content files
|
||||
const bestPracticeDirName = fs
|
||||
.readdirSync(BEST_PRACTICE_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === bestPracticeId);
|
||||
|
||||
if (!bestPracticeDirName) {
|
||||
console.error('Best practice directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bestPracticeDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName
|
||||
);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(
|
||||
`Best Practice content already exists @ ${bestPracticeContentDirPath}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
|
||||
// No directory for a group without control name
|
||||
if (
|
||||
!controlName ||
|
||||
controlName.startsWith('check:') ||
|
||||
controlName.startsWith('ext_link:')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlName.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/best-practices/${bestPracticeId}`
|
||||
));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
const dirTree = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(bestPracticeContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(path.join(parentDir, dirName), dirTree[dirName], filePaths);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(bestPracticeContentDirPath, dirTree);
|
||||
console.log('Created best practice content directory structure');
|
||||
129
scripts/page-data-agg.cjs
Normal file
@@ -0,0 +1,129 @@
|
||||
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);
|
||||
});
|
||||
37
scripts/readme.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## CLI Tools
|
||||
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
|
||||
Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
## `compress-jsons.cjs`
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `update-sponsors.cjs`
|
||||
|
||||
Updates the sponsor ads on each roadmap page with the latest sponsor information in the Excel sheet.
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
Currently, for any new roadmaps that we add, we do create the interactive roadmap but we end up leaving the content empty in the roadmap till we get time to fill it up manually.
|
||||
|
||||
This script populates all the content files with some minimal content from OpenAI so that the users visiting the website have something to read in the interactive roadmap till we get time to fill it up manually.
|
||||
|
||||
## `roadmap-dirs.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-dirs [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
|
||||
- Remove all the groups from the roadmaps through the project editor. Select all and press `cmd+shift+g`
|
||||
- Identify the boxes that should be clickable and group them together with `cmd+shift+g`
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
171
scripts/roadmap-content.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
const fs = require('fs');
|
||||
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];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ALL_ROADMAPS_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
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(ROADMAP_CONTENT_DIR, '') // Remove the content folder
|
||||
.replace(/\/\d+-/g, '/') // Remove ordering info `/101-ecosystem`
|
||||
.replace(/\/index\.md$/, '') // Make the `/index.md` to become the parent folder only
|
||||
.replace(/\.md$/, ''); // Remove `.md` from the end of file
|
||||
|
||||
fileList[fileUrl] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
function writeTopicContent(currTopicUrl) {
|
||||
const [parentTopic, childTopic] = currTopicUrl
|
||||
.replace(/^\d+-/g, '/')
|
||||
.replace(/:/g, '/')
|
||||
.replace(/^\//, '')
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.map((topic) => topic.replace(/-/g, ' '));
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
}
|
||||
|
||||
console.log(`Generating '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai
|
||||
.createChatCompletion({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.data.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function 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(ROADMAP_JSON_DIR, `${roadmapId}.json`));
|
||||
const groups = roadmapJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
!control.properties?.controlName?.startsWith('ext_link')
|
||||
);
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log('----------------------------------------');
|
||||
console.log('OPEN_AI_API_KEY not found. Skipping openai api calls...');
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
166
scripts/roadmap-dirs.cjs
Normal file
@@ -0,0 +1,166 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the roadmaps
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/roadmaps/${roadmapId}`
|
||||
));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param sortOrders Mapping from groupName to sort order
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, sortOrders, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(roadmapContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replace(/(^\d+?-)/g, '') // Remove sorting information
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
|
||||
// Attach sorting information to dirname
|
||||
// e.g. /roadmaps/100-frontend/content/internet
|
||||
// ———> /roadmaps/100-frontend/content/103-internet
|
||||
if (sortOrder) {
|
||||
parentDir = parentDir.replace(/(.+?)([^\/]+)?$/, `$1${sortOrder}-$2`);
|
||||
}
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
dirSortOrders,
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
@@ -6,7 +6,7 @@ if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
const fullPath = path.join(__dirname, `../src/data/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
167
scripts/update-sponsors.cjs
Normal file
@@ -0,0 +1,167 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const apiKey = process.env.SPONSOR_SHEET_API_KEY;
|
||||
const sheetId = process.env.SPONSOR_SHEET_ID;
|
||||
|
||||
if (!apiKey || !sheetId) {
|
||||
console.error('Missing API key or sheet ID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sheetRange = 'A3:I1001';
|
||||
const sheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${sheetRange}?key=${apiKey}`;
|
||||
|
||||
function removeAllSponsors(baseContentDir) {
|
||||
console.log('------------------------');
|
||||
console.log('Removing sponsors from: ', baseContentDir);
|
||||
console.log('------------------------');
|
||||
const dataDirPath = path.join(__dirname, '../src/data');
|
||||
const contentDirPath = path.join(dataDirPath, baseContentDir);
|
||||
|
||||
const contentDir = fs.readdirSync(contentDirPath);
|
||||
contentDir.forEach((content) => {
|
||||
console.log('Removing sponsors from: ', content);
|
||||
|
||||
const pageFilePath = path.join(contentDirPath, content, `${content}.md`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsors;
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
});
|
||||
}
|
||||
|
||||
function addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
}) {
|
||||
const urlPart = pageUrl
|
||||
.replace('https://roadmap.sh/', '')
|
||||
.replace(/\?.+?$/, '');
|
||||
|
||||
const parentDir = urlPart.startsWith('best-practices/')
|
||||
? 'best-practices'
|
||||
: 'roadmaps';
|
||||
const pageId = urlPart.replace(`${parentDir}/`, '');
|
||||
|
||||
const pageFilePath = path.join(
|
||||
__dirname,
|
||||
`../src/data/${parentDir}`,
|
||||
`${pageId}/${pageId}.md`
|
||||
);
|
||||
|
||||
if (!fs.existsSync(pageFilePath)) {
|
||||
console.error(`Page file not found: ${pageFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updating page: ${urlPart}`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
const sponsors = frontmatterObj.sponsors || [];
|
||||
|
||||
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]);
|
||||
|
||||
frontmatterObj = Object.fromEntries(frontmatterValues);
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
}
|
||||
|
||||
// Remove sponsors from all roadmaps
|
||||
removeAllSponsors('roadmaps');
|
||||
removeAllSponsors('best-practices');
|
||||
|
||||
console.log('------------------------');
|
||||
console.log('Adding sponsors');
|
||||
console.log('------------------------');
|
||||
fetch(sheetUrl)
|
||||
.then((res) => res.json())
|
||||
.then((rawData) => {
|
||||
const rows = rawData.values;
|
||||
|
||||
rows.map((row) => {
|
||||
// prettier-ignore
|
||||
const [
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
] = row;
|
||||
|
||||
const isConfiguredActive = isActive?.toLowerCase() === 'yes';
|
||||
const currentDate = new Date();
|
||||
const isDateInRange =
|
||||
currentDate >= new Date(startDate) && currentDate <= new Date(endDate);
|
||||
|
||||
if (!isConfiguredActive || !isDateInRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
});
|
||||
});
|
||||
});
|
||||
23
sitemap.mjs
@@ -2,20 +2,21 @@ 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'));
|
||||
}
|
||||
|
||||
async function getBestPracticesIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/best-practices'));
|
||||
return fs.readdir(path.join(process.cwd(), 'src/data/best-practices'));
|
||||
}
|
||||
|
||||
export function shouldIndexPage(page) {
|
||||
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/g',
|
||||
].includes(pageUrl);
|
||||
}
|
||||
|
||||
export async function serializeSitemap(item) {
|
||||
@@ -26,8 +27,13 @@ export async function serializeSitemap(item) {
|
||||
'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}`),
|
||||
...(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
|
||||
@@ -43,7 +49,10 @@ export async function serializeSitemap(item) {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Stylesheet Complexity
|
||||
|
||||
> Analyzing your stylesheets can help you to flag issues, redundancies and duplicate CSS selectors.
|
||||
|
||||
Sometimes you may have redundancies or validation errors in your CSS, analysing your CSS files and removed these complexities can help you to speed up your CSS files (because your browser will read them faster).
|
||||
|
||||
Your CSS should be organized, using a CSS preprocessor can help you with that. Some online tools listed below can also help you analysing and correct your code.
|
||||
|
||||
- [TestMyCSS | Optimize and Check CSS Performance](http://www.testmycss.com/)
|
||||
- [CSS Stats](https://cssstats.com/)
|
||||
- [macbre/analyze-css: CSS selectors complexity and performance analyzer](https://github.com/macbre/analyze-css)
|
||||
- [Project Wallace](https://www.projectwallace.com/) is like CSS Stats but stores stats over time so you can track your changes
|
||||
@@ -1,9 +0,0 @@
|
||||
# Avoid Inline CSS
|
||||
|
||||
> Avoid using embed or inline CSS inside your `<body>` (Not valid for HTTP/2)
|
||||
|
||||
One of the first reason it's because it's a good practice to separate content from design. It also helps you have a more maintainable code and keep your site accessible. But regarding performance, it's simply because it decreases the file-size of your HTML pages and the load time.
|
||||
|
||||
Always use external stylesheets or embed CSS in your `<head>` (and follow the others CSS performance rules)
|
||||
|
||||
- [Observe CSS Best Practices: Avoid CSS Inline Styles](https://www.lifewire.com/avoid-inline-styles-for-css-3466846)
|
||||
@@ -1,5 +0,0 @@
|
||||
# Page Speed Insights
|
||||
|
||||
Page Speed Insights is a free tool from Google that analyzes the performance of a web page and provides suggestions for improvements.
|
||||
|
||||
- [Page Speed Insights](https://pagespeed.web.dev/)
|
||||
@@ -1,90 +0,0 @@
|
||||
# Recommended Guides
|
||||
|
||||
> Optimize the critical rendering path:
|
||||
|
||||
* [Critical CSS? Not So Fast!](https://csswizardry.com/2022/09/critical-css-not-so-fast/)
|
||||
* [Priority Hints - What Your Browser Doesn’t Know (Yet)](https://www.etsy.com/codeascraft/priority-hints-what-your-browser-doesnt-know-yet)
|
||||
* [Optimizing resource loading with Priority Hints](https://web.dev/priority-hints/)
|
||||
* [Chrome Resource Priorities and Scheduling](https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit?usp=sharing)
|
||||
* [How To Optimize CSS for Peak Site Performance](https://kinsta.com/blog/optimize-css/)
|
||||
* [Eliminate render blocking CSS to improve start render time](https://www.jeffreyknox.dev/blog/eliminate-render-blocking-css-to-improve-start-render-time/)
|
||||
* [Small Bundles, Fast Pages: What To Do With Too Much JavaScript](https://calibreapp.com/blog/bundle-size-optimization)
|
||||
* [How to Eliminate Render-Blocking Resources: a Deep Dive](https://sia.codes/posts/render-blocking-resources/)
|
||||
* [The Critical Request: How to Prioritise Requests to Improve Speed](https://calibreapp.com/blog/critical-request)
|
||||
* [How to Improve CSS Performance](https://calibreapp.com/blog/css-performance)
|
||||
* [The Simplest Way to Load CSS Asynchronously](https://www.filamentgroup.com/lab/load-css-simpler/)
|
||||
* [CSS audit](https://css-tricks.com/a-quick-css-audit-and-general-notes-about-design-systems/)
|
||||
* [Measuring the Critical Rendering Path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp)
|
||||
* [Inlining or Caching? Both Please!](https://www.filamentgroup.com/lab/inlining-cache.html)
|
||||
* [CSS and Network Performance](https://csswizardry.com/2018/11/css-and-network-performance/)
|
||||
* [Analyzing Critical Rendering Path Performance](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp)
|
||||
* [Front-End Performance Checklist](https://github.com/thedaviddias/Front-End-Performance-Checklist)
|
||||
* [The PRPL Pattern](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)
|
||||
* [Now You See Me: How To Defer, Lazy-Load And Act With IntersectionObserver](https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/)
|
||||
* [Optimising the front end for the browser](https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572)
|
||||
* [Prefer DEFER Over ASYNC](https://calendar.perfplanet.com/2016/prefer-defer-over-async/)
|
||||
* [A comprehensive guide to font loading strategies](https://www.zachleat.com/web/comprehensive-webfonts/)
|
||||
* [Understanding the critical rendering path, rendering pages in 1 second](https://medium.com/@luisvieira_gmr/understanding-the-critical-rendering-path-rendering-pages-in-1-second-735c6e45b47a)
|
||||
* [More Weight Doesn’t Mean More Wait](https://www.filamentgroup.com/lab/weight-wait.html)
|
||||
|
||||
> JavaScript Rendering Performance
|
||||
|
||||
* [Five Data-Loading Patterns To Boost Web Performance](https://www.smashingmagazine.com/2022/09/data-loading-patterns-improve-frontend-performance/)
|
||||
* [Optimize long tasks](https://web.dev/optimize-long-tasks/)
|
||||
* [The impact of removing jQuery on our web performance](https://insidegovuk.blog.gov.uk/2022/08/15/the-impact-of-removing-jquery-on-our-web-performance/)
|
||||
* [Profiling & Optimizing the runtime performance with the DevTools Performance tab](iamtk.co/profiling-and-optimizing-the-runtime-performance-with-the-devtools-performance-tab)
|
||||
* [Don't fight the browser preload scanner](https://web.dev/preload-scanner/)
|
||||
* [The Web Performance impact of jQuery](https://twitter.com/TheRealNooshu/status/1509487050122276864)
|
||||
* [Have Single-Page Apps Ruined the Web? | Transitional Apps](https://www.youtube.com/watch?v=860d8usGC0o)
|
||||
* [Improve how you architect webapps](https://www.patterns.dev/)
|
||||
* [Nuxt SSR Optimizing Tips](https://vueschool.io/articles/vuejs-tutorials/nuxt-ssr-optimizing-tips/, Filip Rakowski
|
||||
* [GPU accelerated JavaScript](https://gpu.rocks/#/)
|
||||
* [Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp)
|
||||
* [Astro: Astro is a fresh but familiar approach to building websites. Astro combines decades of proven performance best practices with the DX improvements of the component-oriented era. Use your favorite JavaScript framework and automatically ship the bare-minimum amount of JavaScript—by default.](https://docs.astro.build/getting-started/)
|
||||
* [Minimising Layout and Layout thrashing for 60 FPS](https://www.charistheo.io/blog/2021/09/dom-reflow-and-layout-thrashing/)
|
||||
* [Does shadow DOM improve style performance?](https://nolanlawson.com/2021/08/15/does-shadow-dom-improve-style-performance/)
|
||||
* [Debugging memory leaks - HTTP 203](https://www.youtube.com/watch?v=YDU_3WdfkxA)
|
||||
* [Explore JavaScript Dependencies With Lighthouse Treemap](https://sia.codes/posts/lighthouse-treemap/)
|
||||
* [The real cost of Javascript dependencies (and the state of JS package quality)](https://medium.com/voodoo-engineering/the-real-cost-of-javascript-dependencies-and-the-state-of-js-package-quality-a8dacd74c0ec)
|
||||
* [The State Of Web Workers In 2021](https://www.smashingmagazine.com/2021/06/web-workers-2021/)
|
||||
* [Techniques for developing high-performance animations](https://web.dev/animations/)
|
||||
* [Building a Faster Web Experience with the postTask Scheduler](https://medium.com/airbnb-engineering/building-a-faster-web-experience-with-the-posttask-scheduler-276b83454e91), Callie (Airbnb Engineering & Data Science)
|
||||
* [Don’t attach tooltips to document.body – Learn how the browser works – Debug forced reflow](https://atfzl.com/don-t-attach-tooltips-to-document-body)
|
||||
* [How to Create and Fix Memory Leaks With Chrome DevTools](https://betterprogramming.pub/build-me-an-angular-app-with-memory-leaks-please-36302184e658)
|
||||
* [JavaScript performance beyond bundle size](https://nolanlawson.com/2021/02/23/javascript-performance-beyond-bundle-size/)
|
||||
* [The Import On Interaction Pattern](https://addyosmani.com/blog/import-on-interaction/)
|
||||
* [The “Live DOM” Is Not “Slow”, “Bad”, Or “Wrong”. Web Developers Are.](https://levelup.gitconnected.com/the-live-dom-is-not-slow-bad-or-wrong-web-developers-are-2bf86c3b9e2e)
|
||||
* [Prevent layout shifts with CSS grid stacks](https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/)
|
||||
* [content-visibility: the new CSS property that boosts your rendering performance](https://web.dev/content-visibility/)
|
||||
* [Preact vs React - Updating React at Etsy](https://github.com/mq2thez/blog/blob/main/upgrade-react-etsy/preact-vs-react.md)
|
||||
* [The Cost of Javascript Frameworks](https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/)
|
||||
* [Fixing memory leaks in web applications](https://nolanlawson.com/2020/02/19/fixing-memory-leaks-in-web-applications/)
|
||||
* [How to load polyfills only when needed](https://3perf.com/blog/polyfills/)
|
||||
* [Responsible JavaScript: Part III - Third parties](https://alistapart.com/article/responsible-javascript-part-3/)
|
||||
* [The cost of JavaScript in 2019](https://v8.dev/blog/cost-of-javascript-2019)
|
||||
* [When should you be using Web Workers?](https://dassur.ma/things/when-workers/)
|
||||
* [Responsible Javascript: Part II - Code Bundle](https://alistapart.com/article/responsible-javascript-part-2/)
|
||||
* [Faster script loading with BinaryAST?](https://blog.cloudflare.com/binary-ast/)
|
||||
* [Svelte 3: Rethinking reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity)
|
||||
* [Responsible Javascript: Part I - Web platform over frameworks](https://alistapart.com/article/responsible-javascript-part-1/)
|
||||
* [JavaScript Loading Priorities in Chrome](https://addyosmani.com/blog/script-priorities/)
|
||||
* [Idle Until Urgent](https://philipwalton.com/articles/idle-until-urgent/)
|
||||
* [Browser painting and considerations for web performance](https://css-tricks.com/browser-painting-and-considerations-for-web-performance/)
|
||||
* [The Cost Of JavaScript In 2018](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) ([Video](https://www.youtube.com/watch?v=i5R7giitymk))
|
||||
* [Examining Web Worker Performance](https://www.loxodrome.io/post/web-worker-performance/)
|
||||
* [Front-End Performance Checklist](https://github.com/thedaviddias/Front-End-Performance-Checklist)
|
||||
* [jankfree](http://jankfree.org/)
|
||||
* [What forces layout/reflow?](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)
|
||||
* [Using requestIdleCallback](https://developers.google.com/web/updates/2015/08/using-requestidlecallback)
|
||||
* [Optimize Javascript Execution](https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution)
|
||||
* [Why Web Developers Need to Care about Interactivity](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/)
|
||||
* [Improving Performance with the Paint Timing API](https://www.sitepen.com/blog/2017/10/06/improving-performance-with-the-paint-timing-api)
|
||||
* [Deploying ES2015+ Code in Production Today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)
|
||||
* [Performant Web Animations and Interactions: Achieving 60 FPS](https://blog.algolia.com/performant-web-animations/)
|
||||
* [JavaScript Start-up Performance](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)
|
||||
* [Performant Parallaxing](https://developers.google.com/web/updates/2016/12/performant-parallaxing)
|
||||
* [The Anatomy of a Frame](https://aerotwist.com/blog/the-anatomy-of-a-frame/)
|
||||
* [The future of loading CSS](https://jakearchibald.com/2016/link-in-body/)
|
||||
* [4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them](https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/)
|
||||
* [The cost of frameworks](https://aerotwist.com/blog/the-cost-of-frameworks/)
|
||||
* [FLIP Your Animations](https://aerotwist.com/blog/flip-your-animations/)
|
||||
@@ -1,6 +0,0 @@
|
||||
# Use CDN
|
||||
|
||||
Use a CDN to serve your static assets. This will reduce the load on your server and improve the performance of your site.
|
||||
|
||||
- [10 Tips to Optimize CDN Performance - CDN Planet](https://www.cdnplanet.com/blog/10-tips-optimize-cdn-performance/)
|
||||
- [HTTP Caching | Web Fundamentals | Google Developers](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Use Service Workers
|
||||
|
||||
You are using Service Workers in your PWA to cache data or execute possible heavy tasks without impacting the user experience of your application.
|
||||
|
||||
- [Service Workers: an Introduction | Web Fundamentals | Google Developers](https://developers.google.com/web/fundamentals/primers/service-workers/)
|
||||
- [Measuring the Real-world Performance Impact of Service Workers | Web | Google Developers](https://developers.google.com/web/showcase/2016/service-worker-perf)
|
||||
- [What Are Service Workers and How They Help Improve Performance](https://www.keycdn.com/blog/service-workers/)
|
||||
- [How does a service worker work? - YouTube](https://www.youtube.com/watch?v=__xAtWgfzvc)
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
jsonUrl: "/jsons/best-practices/frontend-performance.json"
|
||||
pdfUrl: "/pdfs/best-practices/frontend-performance.pdf"
|
||||
order: 1
|
||||
featuredTitle: "Frontend Performance"
|
||||
featuredDescription: "Frontend Performance Best Practices"
|
||||
isNew: true
|
||||
isUpcoming: false
|
||||
title: "Frontend Performance"
|
||||
description: "Detailed list of best practices to improve your frontend performance"
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1270.89
|
||||
schema:
|
||||
headline: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
imageUrl: "https://roadmap.sh/best-practices/frontend-performance.png"
|
||||
datePublished: "2023-01-23"
|
||||
dateModified: "2023-01-23"
|
||||
seo:
|
||||
title: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
keywords:
|
||||
- "frontend performance"
|
||||
- "frontend performance best practices"
|
||||
- "frontend performance checklist"
|
||||
- "frontend checklist"
|
||||
- "make performant frontends"
|
||||
---
|
||||
@@ -1,41 +1,17 @@
|
||||
---
|
||||
---
|
||||
|
||||
<script src='./analytics.js'></script>
|
||||
<script src='./analytics.ts'></script>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-139582634-1'
|
||||
></script>
|
||||
<script is:inline>
|
||||
// @ts-nocheck
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
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,32 +1,32 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: any;
|
||||
fireEvent: (props: GAEventType) => void;
|
||||
fireEvent: (props: {
|
||||
action: string;
|
||||
category: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
}) => 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: GAEventType) => {
|
||||
window.fireEvent = (props) => {
|
||||
const { action, category, label, value } = props;
|
||||
if (!window.gtag) {
|
||||
console.warn('Missing GTAG - Analytics disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Analytics event fired', props);
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
|
||||
38
src/components/AstroIcon.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { parse } from 'node-html-parser';
|
||||
import type { Attributes } from 'node-html-parser/dist/nodes/html';
|
||||
|
||||
export interface Props {
|
||||
icon: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
async function getSVG(name: string) {
|
||||
const filepath = `/src/icons/${name}.svg`;
|
||||
|
||||
const files = import.meta.glob<string>('/src/icons/**/*.svg', {
|
||||
eager: true,
|
||||
as: 'raw',
|
||||
});
|
||||
|
||||
if (!(filepath in files)) {
|
||||
throw new Error(`${filepath} not found`);
|
||||
}
|
||||
|
||||
const root = parse(files[filepath]);
|
||||
|
||||
const svg = root.querySelector('svg');
|
||||
|
||||
return {
|
||||
attributes: svg?.attributes,
|
||||
innerHTML: svg?.innerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
const { icon, ...attributes } = Astro.props as Props;
|
||||
const { attributes: baseAttributes, innerHTML } = await getSVG(icon);
|
||||
|
||||
const svgAttributes = { ...baseAttributes, ...attributes };
|
||||
---
|
||||
|
||||
<svg {...svgAttributes} set:html={innerHTML}></svg>
|
||||
5
src/components/AuthenticationFlow/Divider.astro
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class='flex w-full items-center gap-2 py-6 text-sm text-slate-600'>
|
||||
<div class='h-px w-full bg-slate-200'></div>
|
||||
OR
|
||||
<div class='h-px w-full bg-slate-200'></div>
|
||||
</div>
|
||||
100
src/components/AuthenticationFlow/EmailLoginForm.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import type { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
|
||||
const EmailLoginForm: FunctionComponent<{}> = () => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleFormSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-login`,
|
||||
{
|
||||
email,
|
||||
password,
|
||||
}
|
||||
);
|
||||
|
||||
// Log the user in and reload the page
|
||||
if (response?.token) {
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token);
|
||||
window.location.reload();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo use proper types
|
||||
if ((error as any).type === 'user_not_verified') {
|
||||
window.location.href = `/verification-pending?email=${encodeURIComponent(
|
||||
email
|
||||
)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setError(error?.message || 'Something went wrong. Please try again later.');
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="w-full" onSubmit={handleFormSubmit}>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Email Address"
|
||||
value={email}
|
||||
onInput={(e) => setEmail(String((e.target as any).value))}
|
||||
/>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onInput={(e) => setPassword(String((e.target as any).value))}
|
||||
/>
|
||||
|
||||
<p class="mb-3 mt-2 text-sm text-gray-500">
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-blue-800 hover:text-blue-600"
|
||||
>
|
||||
Reset your password?
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<p className="mb-2 rounded-md bg-red-100 p-2 text-red-800">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailLoginForm;
|
||||
103
src/components/AuthenticationFlow/EmailSignupForm.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
|
||||
const EmailSignupForm: FunctionComponent = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ status: 'ok' }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-register`,
|
||||
{
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || response?.status !== 'ok') {
|
||||
setIsLoading(false);
|
||||
setError(
|
||||
error?.message || 'Something went wrong. Please try again later.'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `/verification-pending?email=${encodeURIComponent(
|
||||
email
|
||||
)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex w-full flex-col gap-2" onSubmit={onSubmit}>
|
||||
<label htmlFor="name" className="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
min={3}
|
||||
max={50}
|
||||
required
|
||||
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Full Name"
|
||||
value={name}
|
||||
onInput={(e) => setName(String((e.target as any).value))}
|
||||
/>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Email Address"
|
||||
value={email}
|
||||
onInput={(e) => setEmail(String((e.target as any).value))}
|
||||
/>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
min={6}
|
||||
max={50}
|
||||
required
|
||||
className="block w-full rounded-lg border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onInput={(e) => setPassword(String((e.target as any).value))}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="rounded-lg bg-red-100 p-2 text-red-700">{error}.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Continue to Verify Email'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailSignupForm;
|
||||
64
src/components/AuthenticationFlow/ForgotPasswordForm.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-forgot-password`,
|
||||
{
|
||||
email,
|
||||
}
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setEmail('');
|
||||
setSuccess('Check your email for a link to reset your password.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class="w-full">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
required
|
||||
placeholder="Email Address"
|
||||
value={email}
|
||||
onInput={(e) => setEmail((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="mt-2 rounded-lg bg-red-100 p-2 text-sm text-red-700">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<p className="mt-2 rounded-lg bg-green-100 p-2 text-sm text-green-700">
|
||||
{success}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="mt-3 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
116
src/components/AuthenticationFlow/GitHubButton.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import GitHubIcon from '../../icons/github.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
|
||||
type GitHubButtonProps = {};
|
||||
|
||||
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
|
||||
const GITHUB_LAST_PAGE = 'githubLastPage';
|
||||
|
||||
export function GitHubButton(props: GitHubButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const icon = isLoading ? SpinnerIcon : GitHubIcon;
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const provider = urlParams.get('provider');
|
||||
|
||||
if (!code || !state || provider !== 'github') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
}`
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
const errMessage = error?.message || 'Something went wrong.';
|
||||
setError(errMessage);
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let redirectUrl = '/';
|
||||
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
|
||||
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
|
||||
|
||||
// If the social redirect is there and less than 30 seconds old
|
||||
// redirect to the page that user was on before they clicked the github login button
|
||||
if (gitHubRedirectAt && lastPageBeforeGithub) {
|
||||
const socialRedirectAtTime = parseInt(gitHubRedirectAt, 10);
|
||||
const now = Date.now();
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGithub;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token);
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const { response, error } = await httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`
|
||||
);
|
||||
|
||||
if (error || !response?.loginUrl) {
|
||||
setError(
|
||||
error?.message || 'Something went wrong. Please try again later.'
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// For non authentication pages, we want to redirect back to the page
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
localStorage.setItem(GITHUB_REDIRECT_AT, Date.now().toString());
|
||||
localStorage.setItem(GITHUB_LAST_PAGE, window.location.pathname);
|
||||
}
|
||||
|
||||
window.location.href = response.loginUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={icon}
|
||||
alt="GitHub"
|
||||
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
{error && (
|
||||
<p className="mb-2 mt-1 text-sm font-medium text-red-600">{error}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
116
src/components/AuthenticationFlow/GoogleButton.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
import GoogleIcon from '../../icons/google.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
|
||||
type GoogleButtonProps = {};
|
||||
|
||||
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
|
||||
const GOOGLE_LAST_PAGE = 'googleLastPage';
|
||||
|
||||
export function GoogleButton(props: GoogleButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const icon = isLoading ? SpinnerIcon : GoogleIcon;
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const provider = urlParams.get('provider');
|
||||
|
||||
if (!code || !state || provider !== 'google') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
}`
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let redirectUrl = '/';
|
||||
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
|
||||
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
|
||||
|
||||
// If the social redirect is there and less than 30 seconds old
|
||||
// redirect to the page that user was on before they clicked the github login button
|
||||
if (googleRedirectAt && lastPageBeforeGoogle) {
|
||||
const socialRedirectAtTime = parseInt(googleRedirectAt, 10);
|
||||
const now = Date.now();
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGoogle;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token);
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.loginUrl) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// For non authentication pages, we want to redirect back to the page
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
localStorage.setItem(GOOGLE_REDIRECT_AT, Date.now().toString());
|
||||
localStorage.setItem(GOOGLE_LAST_PAGE, window.location.pathname);
|
||||
}
|
||||
|
||||
window.location.href = response.loginUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={isLoading}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={icon}
|
||||
alt="Google"
|
||||
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
Continue with Google
|
||||
</button>
|
||||
{error && (
|
||||
<p className="mb-2 mt-1 text-sm font-medium text-red-600">{error}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
src/components/AuthenticationFlow/LoginPopup.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import Popup from '../Popup/Popup.astro';
|
||||
import EmailLoginForm from './EmailLoginForm';
|
||||
import Divider from './Divider.astro';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
import { GoogleButton } from './GoogleButton';
|
||||
---
|
||||
|
||||
<Popup id='login-popup' title='' subtitle=''>
|
||||
<div class='text-center'>
|
||||
<h2 class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
|
||||
Login to your account
|
||||
</h2>
|
||||
<p class='mt-2 text-sm leading-4 text-slate-600'>
|
||||
You must be logged in to perform this action.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='mt-7 flex flex-col gap-2'>
|
||||
<GitHubButton client:load />
|
||||
<GoogleButton client:load />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<EmailLoginForm client:load />
|
||||
|
||||
<div class='mt-6 text-center text-sm text-slate-600'>
|
||||
Don't have an account?{' '}
|
||||
<a href='/signup' class='font-medium text-[#4285f4]'>Sign up</a>
|
||||
</div>
|
||||
</Popup>
|
||||
97
src/components/AuthenticationFlow/ResetPasswordForm.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import Cookies from 'js-cookie';
|
||||
import {TOKEN_COOKIE_NAME} from "../../lib/jwt";
|
||||
|
||||
export default function ResetPasswordForm() {
|
||||
const [code, setCode] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
|
||||
if (!code) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
setCode(code);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
setIsLoading(false);
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-reset-forgotten-password`,
|
||||
{
|
||||
newPassword: password,
|
||||
confirmPassword: passwordConfirm,
|
||||
code,
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.message) {
|
||||
setIsLoading(false);
|
||||
setError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response?.token) {
|
||||
setIsLoading(false);
|
||||
setError('Something went wrong. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = response.token;
|
||||
Cookies.set(TOKEN_COOKIE_NAME, token);
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="mx-auto w-full" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="password"
|
||||
className="mb-2 mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
required
|
||||
minLength={6}
|
||||
placeholder="New Password"
|
||||
value={password}
|
||||
onInput={(e) => setPassword((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
required
|
||||
minLength={6}
|
||||
placeholder="Confirm New Password"
|
||||
value={passwordConfirm}
|
||||
onInput={(e) =>
|
||||
setPasswordConfirm((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="mt-2 inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Reset Password'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
79
src/components/AuthenticationFlow/TriggerVerifyAccount.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import ErrorIcon from '../../icons/error.svg';
|
||||
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpPost } from '../../lib/http';
|
||||
|
||||
export function TriggerVerifyAccount() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const triggerVerify = (code: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
httpPost<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
|
||||
{
|
||||
code,
|
||||
}
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong. Please try again.');
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Cookies.set(TOKEN_COOKIE_NAME, response.token);
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError('Something went wrong. Please try again.');
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code')!;
|
||||
|
||||
if (!code) {
|
||||
setIsLoading(false);
|
||||
setError('Something went wrong. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
triggerVerify(code);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
{isLoading && (
|
||||
<img
|
||||
alt={'Please wait.'}
|
||||
src={SpinnerIcon}
|
||||
class={'mx-auto h-16 w-16 animate-spin'}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<img
|
||||
alt={'Please wait.'}
|
||||
src={ErrorIcon}
|
||||
className={'mx-auto h-16 w-16'}
|
||||
/>
|
||||
)}
|
||||
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl">
|
||||
Verifying your account
|
||||
</h2>
|
||||
<div className="text-sm sm:text-base">
|
||||
{isLoading && <p>Please wait while we verify your account..</p>}
|
||||
{error && <p class="text-red-700">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import VerifyLetterIcon from '../../icons/verify-letter.svg';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
|
||||
export function VerificationEmailMessage() {
|
||||
const [email, setEmail] = useState('..');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEmailResent, setIsEmailResent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
setEmail(urlParams.get('email')!);
|
||||
}, []);
|
||||
|
||||
const resendVerificationEmail = () => {
|
||||
httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-send-verification-email`, {
|
||||
email,
|
||||
})
|
||||
.then(({ response, error }) => {
|
||||
if (error) {
|
||||
setIsEmailResent(false);
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEmailResent(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsEmailResent(false);
|
||||
setIsLoading(false);
|
||||
setError('Something went wrong. Please try again later.');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<img
|
||||
alt="Verify Email"
|
||||
src={VerifyLetterIcon}
|
||||
class="mx-auto mb-4 h-20 w-40 sm:h-40"
|
||||
/>
|
||||
<h2 class="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl">
|
||||
Verify your email address
|
||||
</h2>
|
||||
<div class="text-sm sm:text-base">
|
||||
<p>
|
||||
We have sent you an email at{' '}
|
||||
<span className="font-bold">{email}</span>. Please click the link to
|
||||
verify your account. This link will expire shortly, so please verify
|
||||
soon!
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
{!isEmailResent && (
|
||||
<>
|
||||
{isLoading && <p className="text-gray-400">Sending the email ..</p>}
|
||||
{!isLoading && !error && (
|
||||
<p>
|
||||
Please make sure to check your spam folder. If you still don't
|
||||
have the email click to{' '}
|
||||
<button
|
||||
disabled={!email}
|
||||
className="inline text-blue-700"
|
||||
onClick={resendVerificationEmail}
|
||||
>
|
||||
resend verification email.
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && <p class="text-red-700">{error}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isEmailResent && (
|
||||
<p class="text-green-700">Verification email has been sent!</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
src/components/Authenticator/Authenticator.astro
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
---
|
||||
|
||||
<script src='./authenticator.ts'></script>
|
||||
79
src/components/Authenticator/authenticator.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
|
||||
function easeInElement(el: Element) {
|
||||
el.classList.add('opacity-0', 'transition-opacity', 'duration-300');
|
||||
el.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('opacity-0');
|
||||
});
|
||||
}
|
||||
|
||||
function showHideAuthElements(hideOrShow: 'hide' | 'show' = 'hide') {
|
||||
document.querySelectorAll('[data-auth-required]').forEach((el) => {
|
||||
if (hideOrShow === 'hide') {
|
||||
el.classList.add('hidden');
|
||||
} else {
|
||||
easeInElement(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showHideGuestElements(hideOrShow: 'hide' | 'show' = 'hide') {
|
||||
document.querySelectorAll('[data-guest-required]').forEach((el) => {
|
||||
if (hideOrShow === 'hide') {
|
||||
el.classList.add('hidden');
|
||||
} else {
|
||||
easeInElement(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prepares the UI for the user who is logged in
|
||||
function handleGuest() {
|
||||
const authenticatedRoutes = [
|
||||
'/settings/update-profile',
|
||||
'/settings/update-password',
|
||||
];
|
||||
|
||||
showHideAuthElements('hide');
|
||||
showHideGuestElements('show');
|
||||
|
||||
// If the user is on an authenticated route, redirect them to the home page
|
||||
if (authenticatedRoutes.includes(window.location.pathname)) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Prepares the UI for the user who is logged out
|
||||
function handleAuthenticated() {
|
||||
const guestRoutes = [
|
||||
'/login',
|
||||
'/signup',
|
||||
'/verify-account',
|
||||
'/verification-pending',
|
||||
'/reset-password',
|
||||
'/forgot-password',
|
||||
];
|
||||
|
||||
showHideGuestElements('hide');
|
||||
showHideAuthElements('show');
|
||||
|
||||
// If the user is on a guest route, redirect them to the home page
|
||||
if (guestRoutes.includes(window.location.pathname)) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAuthRequired() {
|
||||
const token = Cookies.get(TOKEN_COOKIE_NAME);
|
||||
if (token) {
|
||||
handleAuthenticated();
|
||||
} else {
|
||||
handleGuest();
|
||||
}
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
handleAuthRequired();
|
||||
}, 0);
|
||||