Compare commits
699 Commits
content/fl
...
chore/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47926c496d | ||
|
|
34d0cde165 | ||
|
|
03ba0c384b | ||
|
|
bbe8125fc1 | ||
|
|
0c64223ec1 | ||
|
|
3a022926de | ||
|
|
93a6ae3f81 | ||
|
|
42b3595367 | ||
|
|
39278cc97b | ||
|
|
c83d20d63c | ||
|
|
6e8770c8c4 | ||
|
|
3457f7495a | ||
|
|
07acb17459 | ||
|
|
77cd0ecf26 | ||
|
|
eccc0302f2 | ||
|
|
7274d8a54e | ||
|
|
8d19be6232 | ||
|
|
e0828d11bf | ||
|
|
9e7a37d079 | ||
|
|
76f1592615 | ||
|
|
80e80e7d9b | ||
|
|
8692f05f14 | ||
|
|
e5705bd6cc | ||
|
|
f52e6df410 | ||
|
|
c4db994753 | ||
|
|
7bfd3934f8 | ||
|
|
32dac79565 | ||
|
|
ceb51a18df | ||
|
|
c21f217425 | ||
|
|
9299326dc2 | ||
|
|
fbe597706a | ||
|
|
c7b6257c74 | ||
|
|
dbe6f8589d | ||
|
|
9139c8eaf8 | ||
|
|
05451a0f07 | ||
|
|
36d4d8e449 | ||
|
|
fa8551dd31 | ||
|
|
7cbf8eb72a | ||
|
|
e739662d49 | ||
|
|
e26fa35470 | ||
|
|
37e92fd084 | ||
|
|
0aef3efda9 | ||
|
|
7187da853b | ||
|
|
b81dba9f8b | ||
|
|
bf0fd62bff | ||
|
|
67e6043cbc | ||
|
|
9d169219ce | ||
|
|
8eb6a0f857 | ||
|
|
9c2d3bd2d8 | ||
|
|
d6de73d7d4 | ||
|
|
8899654937 | ||
|
|
d64cb4116a | ||
|
|
f428849daa | ||
|
|
83143f4438 | ||
|
|
8adc6cb7b4 | ||
|
|
d12eccb6aa | ||
|
|
93a1dedd8f | ||
|
|
027a4a947a | ||
|
|
67fd8d3d47 | ||
|
|
e42532ad7c | ||
|
|
944a35a905 | ||
|
|
9f620866cb | ||
|
|
4d74e9c47c | ||
|
|
f1a37deab2 | ||
|
|
f36dd4b964 | ||
|
|
80c842412a | ||
|
|
219ef68001 | ||
|
|
ab8a551a96 | ||
|
|
467382879d | ||
|
|
258f6cd0f0 | ||
|
|
2c3bf1ebbc | ||
|
|
1113b698be | ||
|
|
eefcc6866b | ||
|
|
34185ac8fb | ||
|
|
c1e85ce422 | ||
|
|
6ed270112d | ||
|
|
df4c457dd4 | ||
|
|
8a5bc21206 | ||
|
|
2d3a89bd56 | ||
|
|
d39dad7275 | ||
|
|
37107c495f | ||
|
|
2a910ddde4 | ||
|
|
11d7e7d431 | ||
|
|
991de00891 | ||
|
|
7747582e70 | ||
|
|
28550ec84c | ||
|
|
8246b48f59 | ||
|
|
455a70c64c | ||
|
|
f0f797a996 | ||
|
|
037763770d | ||
|
|
8d4299c899 | ||
|
|
534ed126d4 | ||
|
|
0fa6ecd3ce | ||
|
|
7dfb630cb5 | ||
|
|
13e1aacd3b | ||
|
|
9ad5143588 | ||
|
|
9e867d5f4e | ||
|
|
f3b186d525 | ||
|
|
5f9a50804b | ||
|
|
486603aff7 | ||
|
|
feec4b7576 | ||
|
|
f64f7b973e | ||
|
|
31f941e262 | ||
|
|
09d312ee46 | ||
|
|
a92e8f1b1a | ||
|
|
bca66f7c0b | ||
|
|
b743a31610 | ||
|
|
b1dc116cae | ||
|
|
fae57224a8 | ||
|
|
c8ffea31d9 | ||
|
|
fc3b2a4015 | ||
|
|
f70272763f | ||
|
|
a15c2a3ca7 | ||
|
|
550555c0c5 | ||
|
|
6e201a8c29 | ||
|
|
dd139170d1 | ||
|
|
66412327fa | ||
|
|
7736271ba0 | ||
|
|
4236c8495a | ||
|
|
6c930716fc | ||
|
|
522b00612a | ||
|
|
e36ff7bdd6 | ||
|
|
d168731cbd | ||
|
|
715daf499f | ||
|
|
6f5449e4b9 | ||
|
|
7f3690d5b8 | ||
|
|
1046dc9171 | ||
|
|
28f672d989 | ||
|
|
3f5ddfa346 | ||
|
|
f1f4e99dab | ||
|
|
67f3917a8d | ||
|
|
02988fac2c | ||
|
|
d21deb0725 | ||
|
|
2a5d316e58 | ||
|
|
557a01b4d0 | ||
|
|
680dbee6eb | ||
|
|
b525c5efb4 | ||
|
|
347141f93b | ||
|
|
9a6e8b1635 | ||
|
|
8a42d0346b | ||
|
|
0e0a3f17ae | ||
|
|
9298f76a68 | ||
|
|
e85ff79dbe | ||
|
|
7a19b7887a | ||
|
|
08e7efa637 | ||
|
|
cc348c0c96 | ||
|
|
5a059c151f | ||
|
|
4063b71345 | ||
|
|
129ef9ccd8 | ||
|
|
d60e4fcfa4 | ||
|
|
310c6d4c55 | ||
|
|
fffccbe5b5 | ||
|
|
9685f1e952 | ||
|
|
ef53c2dd5f | ||
|
|
7e0f7a32af | ||
|
|
cdea68e754 | ||
|
|
90069e4ef4 | ||
|
|
8dbaa60b58 | ||
|
|
19b38dec4c | ||
|
|
9c246984d1 | ||
|
|
ff0e10c16c | ||
|
|
ec165d4a78 | ||
|
|
afe718ee09 | ||
|
|
4aca01a98d | ||
|
|
140282f1ff | ||
|
|
4d38d19e4f | ||
|
|
5e39417a64 | ||
|
|
03ec7ebcd9 | ||
|
|
fbb6def555 | ||
|
|
ae9e30eb73 | ||
|
|
9e89c6946b | ||
|
|
6ff83d0797 | ||
|
|
5ff131ae29 | ||
|
|
e80f88ef2c | ||
|
|
cff01c151b | ||
|
|
6ca85a41a2 | ||
|
|
1630b493b1 | ||
|
|
518ece3cab | ||
|
|
aba2fd1d35 | ||
|
|
fcd68568c2 | ||
|
|
1b5e9ffe0d | ||
|
|
b3c3e44ba2 | ||
|
|
67b49d3f87 | ||
|
|
0d3e1d31bb | ||
|
|
28a27a1c65 | ||
|
|
8c3ea21ef1 | ||
|
|
417596db36 | ||
|
|
28240162b3 | ||
|
|
6dca357782 | ||
|
|
d1fe06a4e9 | ||
|
|
97cba5681b | ||
|
|
715d2ba62b | ||
|
|
32673c21fb | ||
|
|
f0c47705cb | ||
|
|
612b91e05f | ||
|
|
b4cce42844 | ||
|
|
2c2d57ecab | ||
|
|
d05374ca68 | ||
|
|
b5c02a9aff | ||
|
|
1e3568a1c4 | ||
|
|
bdeebbc9cc | ||
|
|
510e6fd273 | ||
|
|
2ca98bbb10 | ||
|
|
49cff0c22c | ||
|
|
943bf41dc5 | ||
|
|
6c9ba75906 | ||
|
|
70976ee42a | ||
|
|
5848698abf | ||
|
|
29dd1eb21f | ||
|
|
ebe6d3c6e4 | ||
|
|
425bfea265 | ||
|
|
c58efe8d00 | ||
|
|
955d04e532 | ||
|
|
0031a9c6ba | ||
|
|
8fb778337d | ||
|
|
a48d39a863 | ||
|
|
36b2a8f2d7 | ||
|
|
00e9d44ba9 | ||
|
|
62b068a94a | ||
|
|
af926002e9 | ||
|
|
0612f9c44f | ||
|
|
fbf545c2ed | ||
|
|
c7ef97cb4f | ||
|
|
564f48540e | ||
|
|
52e729d212 | ||
|
|
bdfa7606dd | ||
|
|
056e0e8e3a | ||
|
|
879ba258b2 | ||
|
|
3d62d2689f | ||
|
|
3b7a9ca5cd | ||
|
|
ac892d2868 | ||
|
|
19bde7bb2f | ||
|
|
419b1872b8 | ||
|
|
bbeb4ee279 | ||
|
|
f2ca7d9140 | ||
|
|
70b95c6ad1 | ||
|
|
5a3f621093 | ||
|
|
631eb380fc | ||
|
|
cb9778ba15 | ||
|
|
38106a8199 | ||
|
|
226e94857b | ||
|
|
f94c701657 | ||
|
|
259109cc38 | ||
|
|
e120df30e3 | ||
|
|
43f351a943 | ||
|
|
502b8e20d5 | ||
|
|
ff5858f965 | ||
|
|
8b8ef52d98 | ||
|
|
7032bc0726 | ||
|
|
ba65dec596 | ||
|
|
78cf88fbd9 | ||
|
|
93e16d899a | ||
|
|
14060bda94 | ||
|
|
45b729d708 | ||
|
|
9023ea6298 | ||
|
|
d29176cf98 | ||
|
|
55989d8480 | ||
|
|
9c936974c7 | ||
|
|
311b4683d0 | ||
|
|
bf61697154 | ||
|
|
52818f1e34 | ||
|
|
174ea05a92 | ||
|
|
dcb4e06fea | ||
|
|
62eb6a4a01 | ||
|
|
f643f3bd9a | ||
|
|
972370e0e6 | ||
|
|
a6feb72339 | ||
|
|
c751706631 | ||
|
|
8900324234 | ||
|
|
f1b880d898 | ||
|
|
9a285d7470 | ||
|
|
15259560e0 | ||
|
|
d8afa166aa | ||
|
|
d39791257e | ||
|
|
06b7005782 | ||
|
|
bc6c933440 | ||
|
|
b965a89db3 | ||
|
|
9b82e327e2 | ||
|
|
5808125d92 | ||
|
|
f49fe258aa | ||
|
|
08df9e8c33 | ||
|
|
56e388edd8 | ||
|
|
ded75c7af1 | ||
|
|
557c426078 | ||
|
|
d61a83a0a3 | ||
|
|
7500c6c1cb | ||
|
|
b51076dd0a | ||
|
|
8010bfc832 | ||
|
|
0f80f26d17 | ||
|
|
40d25c43f4 | ||
|
|
686a7382ab | ||
|
|
88401bd7b1 | ||
|
|
1d97467c05 | ||
|
|
2388fa148b | ||
|
|
d574fccbc8 | ||
|
|
89cc55a1eb | ||
|
|
8c75354235 | ||
|
|
9eb9dc8cd8 | ||
|
|
afa28bddd3 | ||
|
|
5cf0e76765 | ||
|
|
16b3f8ff49 | ||
|
|
d2055e0f6d | ||
|
|
4010157baf | ||
|
|
75c7e83264 | ||
|
|
8ca22e0dcc | ||
|
|
2b8d1f470c | ||
|
|
c4d9651e95 | ||
|
|
813c0ebd93 | ||
|
|
e376942c8d | ||
|
|
6d91c11856 | ||
|
|
1d47b1fb7b | ||
|
|
54a9e586bf | ||
|
|
b58c2a1356 | ||
|
|
dec5e58063 | ||
|
|
b0a4130229 | ||
|
|
a06e992b8a | ||
|
|
6e1072bea9 | ||
|
|
1f9eb18bfb | ||
|
|
603bd2b107 | ||
|
|
0163d9e4f9 | ||
|
|
910579f463 | ||
|
|
d6a28a312a | ||
|
|
267a4a7be5 | ||
|
|
59111a1a90 | ||
|
|
9f5d1aef74 | ||
|
|
36eed57ec2 | ||
|
|
cc054bb24b | ||
|
|
056256015d | ||
|
|
dd5f3795ec | ||
|
|
8c29d43bef | ||
|
|
aa32258aa1 | ||
|
|
d2394aca77 | ||
|
|
6804535fe4 | ||
|
|
3852e7d96f | ||
|
|
eb852caee8 | ||
|
|
1414693e33 | ||
|
|
fbdb7e77c3 | ||
|
|
c72658938f | ||
|
|
718c582a8c | ||
|
|
12f385dffd | ||
|
|
35f500d218 | ||
|
|
44949709d1 | ||
|
|
476557db80 | ||
|
|
f7625a8250 | ||
|
|
c06c236da5 | ||
|
|
24c262282e | ||
|
|
876330522d | ||
|
|
f1c771e95c | ||
|
|
d3668b25e9 | ||
|
|
b0493c370c | ||
|
|
e67caa0ffe | ||
|
|
82a44ddfef | ||
|
|
205fe6cc23 | ||
|
|
591cac8bfa | ||
|
|
42debdeab0 | ||
|
|
0555452bf2 | ||
|
|
cc7f9d94bb | ||
|
|
51d986b86f | ||
|
|
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 | ||
|
|
b6c8260faf | ||
|
|
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 | ||
|
|
03f69c02c1 | ||
|
|
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
|
||||
25
.github/ISSUE_TEMPLATE/01-suggest-changes.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: "✍️ Suggest Changes"
|
||||
description: Help us improve the roadmaps by suggesting changes
|
||||
labels: [suggestion]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help us improve the roadmaps with your suggestions.
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: Roadmap URL
|
||||
description: Please provide the URL of the roadmap you are suggesting changes to.
|
||||
placeholder: https://roadmap.sh
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: roadmap-suggestions
|
||||
attributes:
|
||||
label: Suggestions
|
||||
description: What changes would you like to suggest?
|
||||
placeholder: Enter your suggestions here.
|
||||
validations:
|
||||
required: true
|
||||
42
.github/ISSUE_TEMPLATE/02-bug-report.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: "🐛 Bug Report"
|
||||
description: Report an issue or possible bug
|
||||
labels: [bug]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: What is the URL where the issue is happening
|
||||
placeholder: https://roadmap.sh
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Output from browser console (if any)
|
||||
description: Please copy and paste any relevant log output.
|
||||
- type: checkboxes
|
||||
id: will-pr
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this issue.
|
||||
required: false
|
||||
12
.github/ISSUE_TEMPLATE/03-feature-suggestion.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: "✨ Feature Suggestion"
|
||||
description: Is there a feature you'd like to see on Roadmap.sh? Let us know!
|
||||
labels: [feature request]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: Please provide a detailed description of the feature you are suggesting and how it would help you/others.
|
||||
validations:
|
||||
required: true
|
||||
37
.github/ISSUE_TEMPLATE/04-roadmap-contribution.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: "🙏 Submit a Roadmap"
|
||||
description: Help us launch a new roadmap with your expertise.
|
||||
labels: [roadmap contribution]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to submit a roadmap! Please fill out the information below and we'll get back to you as soon as we can.
|
||||
- type: input
|
||||
id: roadmap-title
|
||||
attributes:
|
||||
label: What is the title of the roadmap you are submitting?
|
||||
placeholder: e.g. Roadmap to learn Data Science
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: Is this roadmap prepared by you or someone else?
|
||||
options:
|
||||
- I prepared this roadmap
|
||||
- I found this roadmap online (please provide a link below)
|
||||
- type: textarea
|
||||
id: roadmap-description
|
||||
attributes:
|
||||
label: Roadmap Items
|
||||
description: Please submit a nested list of items which we can convert into the visual. Here is an [example of roadmap items list.](https://gist.github.com/kamranahmedse/98758d2c73799b3a6ce17385e4c548a5).
|
||||
placeholder: |
|
||||
- Item 1
|
||||
- Subitem 1
|
||||
- Subitem 2
|
||||
- Item 2
|
||||
- Subitem 1
|
||||
- Subitem 2
|
||||
validations:
|
||||
required: true
|
||||
12
.github/ISSUE_TEMPLATE/05-something-else.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: "🤷♂️ Something else"
|
||||
description: If none of the above templates fit your needs, please use this template to submit your issue.
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-description
|
||||
attributes:
|
||||
label: Detailed Description
|
||||
description: Please provide a detailed description of the issue.
|
||||
validations:
|
||||
required: true
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ✋ Roadmap Request
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Please do not open issues with roadmap requests, hop onto the discord server for that.
|
||||
- name: 📝 Typo or Grammatical Mistake
|
||||
url: https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data
|
||||
about: Please submit a pull request instead of reporting it as an issue.
|
||||
- name: 💬 Chat on Discord
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Join the community on our Discord server.
|
||||
- name: 🤝 Guidance
|
||||
url: https://discord.gg/cJpEt5Qbwa
|
||||
about: Join the community in our Discord server.
|
||||
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
|
||||
|
||||
7
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.idea
|
||||
|
||||
# build output
|
||||
dist/
|
||||
.output/
|
||||
@@ -5,7 +7,7 @@ dist/
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
bin/developer-roadmap
|
||||
scripts/developer-roadmap
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
@@ -23,4 +25,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,13 +1,16 @@
|
||||
// https://astro.build/config
|
||||
import preact from '@astrojs/preact';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import compress from 'astro-compress';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
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 +20,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';
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -25,6 +46,22 @@ export default defineConfig({
|
||||
format: 'file',
|
||||
},
|
||||
integrations: [
|
||||
{
|
||||
name: 'client-authenticated',
|
||||
hooks: {
|
||||
'astro:config:setup'(options) {
|
||||
options.addClientDirective({
|
||||
name: 'authenticated',
|
||||
entrypoint: fileURLToPath(
|
||||
new URL(
|
||||
'./src/directives/client-authenticated.mjs',
|
||||
import.meta.url
|
||||
)
|
||||
),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false,
|
||||
@@ -38,5 +75,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,19 +0,0 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsonsDir = path.join(process.cwd(), 'public/jsons');
|
||||
const childJsonDirs = fs.readdirSync(jsonsDir);
|
||||
|
||||
childJsonDirs.forEach((childJsonDir) => {
|
||||
const fullChildJsonDirPath = path.join(jsonsDir, childJsonDir);
|
||||
const jsonFiles = fs.readdirSync(fullChildJsonDirPath);
|
||||
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
|
||||
const jsonFilePath = path.join(fullChildJsonDirPath, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -16,14 +16,14 @@ For new roadmaps, submit a roadmap by providing [a textual roadmap similar to th
|
||||
|
||||
For the existing roadmaps, please follow the details listed for the nature of contribution:
|
||||
|
||||
- **Fixing Typos** — Make your changes in the [roadmap JSON file](https://github.com/kamranahmedse/developer-roadmap/tree/master/public/jsons)
|
||||
- **Fixing Typos** — Make your changes in the [roadmap JSON file](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/roadmaps)
|
||||
- **Adding or Removing Nodes** — Please open an issue with your suggestion.
|
||||
|
||||
**Note:** Please note that our goal is not to have the biggest list of items. Our goal is to list items or skills most relevant today.
|
||||
|
||||
## 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.
|
||||
|
||||
49
package.json
@@ -8,33 +8,48 @@
|
||||
"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.1",
|
||||
"@astrojs/sitemap": "^1.3.3",
|
||||
"@astrojs/tailwind": "^3.1.3",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.1",
|
||||
"@nanostores/preact": "^0.5.0",
|
||||
"astro": "^2.6.6",
|
||||
"astro-compress": "^1.1.47",
|
||||
"jose": "^4.14.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"nanostores": "^0.9.2",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.10.12",
|
||||
"preact": "^10.15.1",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@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.3.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.10.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,
|
||||
},
|
||||
};
|
||||
|
||||
4804
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
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/apollo-workshop.png
Normal file
|
After Width: | Height: | Size: 92 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 |
BIN
public/og-images/sql-roadmap.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
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/code-review.pdf
Normal file
BIN
public/pdfs/roadmaps/cpp.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/react-native.pdf
Normal file
BIN
public/pdfs/roadmaps/sql.pdf
Normal file
BIN
public/pdfs/roadmaps/typescript.pdf
Normal file
BIN
public/pdfs/roadmaps/ux-design.pdf
Normal file
BIN
public/roadmaps/cpp.png
Normal file
|
After Width: | Height: | Size: 773 KiB |
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/sql.png
Normal file
|
After Width: | Height: | Size: 550 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 |
42
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>
|
||||
@@ -30,15 +30,19 @@ Roadmaps are now interactive, you can click the nodes to read more about the top
|
||||
|
||||
Here is the list of available roadmaps with more being actively worked upon.
|
||||
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend)
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend) / [Frontend Beginner Roadmap](https://roadmap.sh/frontend?r=frontend-beginner)
|
||||
- [Backend Roadmap](https://roadmap.sh/backend)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
|
||||
- [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)
|
||||
- [C++ Roadmap](https://roadmap.sh/cpp)
|
||||
- [React Roadmap](https://roadmap.sh/react)
|
||||
- [React Native Roadmap](https://roadmap.sh/react-native)
|
||||
- [Vue Roadmap](https://roadmap.sh/vue)
|
||||
- [Angular Roadmap](https://roadmap.sh/angular)
|
||||
- [Node.js Roadmap](https://roadmap.sh/nodejs)
|
||||
@@ -50,10 +54,24 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Java Roadmap](https://roadmap.sh/java)
|
||||
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
|
||||
- [Design System Roadmap](https://roadmap.sh/design-system)
|
||||
- [DBA Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [PostgreSQL Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [SQL Roadmap](https://roadmap.sh/sql)
|
||||
- [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)
|
||||
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
|
||||
|
||||
We have also added a new form of visual content covering best practices:
|
||||
|
||||
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
|
||||
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
|
||||
|
||||

|
||||
|
||||
@@ -77,6 +95,12 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Note: use the `depth` parameter to reduce the clone size and speed up the clone.
|
||||
|
||||
```sh
|
||||
git clone --depth=1 https://github.com/kamranahmedse/developer-roadmap.git
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
> Have a look at [contribution docs](./contributing.md) for how to update any of the roadmaps
|
||||
|
||||
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`.
|
||||
174
scripts/roadmap-content.cjs
Normal file
@@ -0,0 +1,174 @@
|
||||
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 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 with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
|
||||
}
|
||||
|
||||
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(
|
||||
ALL_ROADMAPS_DIR,
|
||||
`${roadmapId}/${roadmapId}`
|
||||
));
|
||||
|
||||
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);
|
||||
});
|
||||
167
scripts/roadmap-dirs.cjs
Normal file
@@ -0,0 +1,167 @@
|
||||
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,
|
||||
`../src/data/roadmaps/${roadmapId}/${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,7 +0,0 @@
|
||||
# Enable Compression
|
||||
|
||||
Use a compression method such as Gzip or Brotli to reduce the size of your JavaScript files. With a smaller sizes file, users will be able to download the asset faster, resulting in improved performance.
|
||||
|
||||
- [Check GZIP compression](https://checkgzipcompression.com/)
|
||||
- [Check Brotli Compression](https://tools.keycdn.com/brotli-test)
|
||||
- [Can I use... Brotli](https://caniuse.com/#feat=brotli)
|
||||
@@ -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"
|
||||
---
|
||||
170
src/components/AccountSidebar.astro
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
import AstroIcon from './AstroIcon.astro';
|
||||
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
|
||||
|
||||
export interface Props {
|
||||
activePageId: string;
|
||||
activePageTitle: string;
|
||||
hasDesktopSidebar?: boolean;
|
||||
}
|
||||
|
||||
const { hasDesktopSidebar = true, activePageId, activePageTitle } = Astro.props;
|
||||
|
||||
const sidebarLinks = [
|
||||
{
|
||||
href: '/account',
|
||||
title: 'Activity',
|
||||
id: 'activity',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'analytics',
|
||||
classes: 'h-3 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/road-card',
|
||||
title: 'Card',
|
||||
id: 'road-card',
|
||||
isNew: true,
|
||||
icon: {
|
||||
glyph: 'badge',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/update-profile',
|
||||
title: 'Profile',
|
||||
id: 'profile',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'user',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
href: '/account/settings',
|
||||
title: 'Settings',
|
||||
id: 'settings',
|
||||
isNew: false,
|
||||
icon: {
|
||||
glyph: 'cog',
|
||||
classes: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class='relative mb-5 block border-b p-4 shadow-inner md:hidden'>
|
||||
<button
|
||||
class='flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-sm font-medium text-gray-900'
|
||||
id='settings-menu'
|
||||
>
|
||||
{activePageTitle}
|
||||
<AstroIcon icon='dropdown' />
|
||||
</button>
|
||||
<ul
|
||||
id='settings-menu-dropdown'
|
||||
class='absolute left-0 right-0 z-10 mt-1 hidden space-y-1.5 bg-white p-2 shadow-lg'
|
||||
>
|
||||
<!--<li>-->
|
||||
<!-- <a-->
|
||||
<!-- href='/team'-->
|
||||
<!-- class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${-->
|
||||
<!-- activePageId === 'team' ? 'bg-slate-100' : ''-->
|
||||
<!-- }`}-->
|
||||
<!-- >-->
|
||||
<!-- <AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />-->
|
||||
<!-- Teams-->
|
||||
<!-- </a>-->
|
||||
<!--</li>-->
|
||||
{
|
||||
sidebarLinks.map((sidebarLink) => {
|
||||
const isActive = activePageId === sidebarLink.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={sidebarLink.href}
|
||||
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
|
||||
isActive ? 'bg-slate-100' : ''
|
||||
}`}
|
||||
>
|
||||
<AstroIcon
|
||||
icon={sidebarLink.icon.glyph}
|
||||
class={`${sidebarLink.icon.classes} mr-2`}
|
||||
/>
|
||||
{sidebarLink.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class='container flex min-h-screen items-stretch'>
|
||||
<!-- Start Desktop Sidebar -->
|
||||
{
|
||||
hasDesktopSidebar && (
|
||||
<aside class='hidden w-[195px] shrink-0 border-r border-slate-200 py-10 md:block'>
|
||||
<TeamDropdown client:load />
|
||||
|
||||
<nav>
|
||||
<ul class='space-y-1'>
|
||||
{sidebarLinks.map((sidebarLink) => {
|
||||
const isActive = activePageId === sidebarLink.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={sidebarLink.href}
|
||||
class={`font-regular flex w-full items-center border-r-2 px-2 py-1.5 text-sm ${
|
||||
isActive
|
||||
? 'border-r-black bg-gray-100 text-black'
|
||||
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span class='flex flex-grow items-center'>
|
||||
<AstroIcon
|
||||
icon={sidebarLink.icon.glyph}
|
||||
class={`${sidebarLink.icon.classes} mr-2`}
|
||||
/>
|
||||
{sidebarLink.title}
|
||||
</span>
|
||||
|
||||
{sidebarLink.isNew && !isActive && (
|
||||
<span class='relative mr-1 flex items-center'>
|
||||
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
|
||||
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
<!-- /End Desktop Sidebar -->
|
||||
|
||||
<div class:list={['grow px-0 py-0 md:py-10', { 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar }]}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const menuButton = document.getElementById('settings-menu');
|
||||
const menuDropdown = document.getElementById('settings-menu-dropdown');
|
||||
|
||||
menuButton?.addEventListener('click', () => {
|
||||
menuDropdown?.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menuButton?.contains(e.target as Node)) {
|
||||
menuDropdown?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
56
src/components/Activity/ActivityCounters.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
type ActivityCountersType = {
|
||||
done: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
learning: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
streak: {
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ActivityCounterType = {
|
||||
text: string;
|
||||
count: string;
|
||||
};
|
||||
|
||||
function ActivityCounter(props: ActivityCounterType) {
|
||||
const { text, count } = props;
|
||||
|
||||
return (
|
||||
<div class="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
|
||||
<h2 class="text-base sm:text-5xl font-bold">
|
||||
{count}
|
||||
</h2>
|
||||
<p class="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivityCounters(props: ActivityCountersType) {
|
||||
const { done, learning, streak } = props;
|
||||
|
||||
return (
|
||||
<div class="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
|
||||
<div class="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
|
||||
<ActivityCounter
|
||||
text={'Topics Completed'}
|
||||
count={`${done?.total || 0}`}
|
||||
/>
|
||||
|
||||
<ActivityCounter
|
||||
text={'Currently Learning'}
|
||||
count={`${learning?.total || 0}`}
|
||||
/>
|
||||
|
||||
<ActivityCounter
|
||||
text={'Visit Streak'}
|
||||
count={`${streak?.count || 0}d`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
src/components/Activity/ActivityPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { ActivityCounters } from './ActivityCounters';
|
||||
import { ResourceProgress } from './ResourceProgress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { EmptyActivity } from './EmptyActivity';
|
||||
|
||||
export type ActivityResponse = {
|
||||
done: {
|
||||
today: number;
|
||||
total: number;
|
||||
};
|
||||
learning: {
|
||||
today: number;
|
||||
total: number;
|
||||
roadmaps: {
|
||||
title: string;
|
||||
id: string;
|
||||
learning: number;
|
||||
done: number;
|
||||
total: number;
|
||||
skipped: number;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
bestPractices: {
|
||||
title: string;
|
||||
id: string;
|
||||
learning: number;
|
||||
done: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
};
|
||||
streak: {
|
||||
count: number;
|
||||
firstVisitAt: Date | null;
|
||||
lastVisitAt: Date | null;
|
||||
};
|
||||
activity: {
|
||||
type: 'done' | 'learning' | 'pending' | 'skipped';
|
||||
createdAt: Date;
|
||||
metadata: {
|
||||
resourceId?: string;
|
||||
resourceType?: 'roadmap' | 'best-practice';
|
||||
topicId?: string;
|
||||
topicLabel?: string;
|
||||
resourceTitle?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export function ActivityPage() {
|
||||
const [activity, setActivity] = useState<ActivityResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
async function loadActivity() {
|
||||
const { error, response } = await httpGet<ActivityResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`
|
||||
);
|
||||
|
||||
if (!response || error) {
|
||||
console.error('Error loading activity');
|
||||
console.error(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setActivity(response);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const learningRoadmaps = activity?.learning.roadmaps || [];
|
||||
const learningBestPractices = activity?.learning.bestPractices || [];
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActivityCounters
|
||||
done={activity?.done || { today: 0, total: 0 }}
|
||||
learning={activity?.learning || { today: 0, total: 0 }}
|
||||
streak={activity?.streak || { count: 0 }}
|
||||
/>
|
||||
|
||||
<div class="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
|
||||
{learningRoadmaps.length === 0 &&
|
||||
learningBestPractices.length === 0 && <EmptyActivity />}
|
||||
|
||||
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
|
||||
<>
|
||||
<h2 class="mb-3 text-xs uppercase text-gray-400">
|
||||
Continue Following
|
||||
</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{learningRoadmaps
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.map((roadmap) => (
|
||||
<ResourceProgress
|
||||
doneCount={roadmap.done || 0}
|
||||
learningCount={roadmap.learning || 0}
|
||||
totalCount={roadmap.total || 0}
|
||||
skippedCount={roadmap.skipped || 0}
|
||||
resourceId={roadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.title}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{learningBestPractices
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.map((bestPractice) => (
|
||||
<ResourceProgress
|
||||
doneCount={bestPractice.done || 0}
|
||||
totalCount={bestPractice.total || 0}
|
||||
learningCount={bestPractice.learning || 0}
|
||||
resourceId={bestPractice.id}
|
||||
skippedCount={bestPractice.skipped || 0}
|
||||
resourceType={'best-practice'}
|
||||
title={bestPractice.title}
|
||||
updatedAt={bestPractice.updatedAt}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
src/components/Activity/EmptyActivity.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
|
||||
export function EmptyActivity() {
|
||||
return (
|
||||
<div class="rounded-md">
|
||||
<div class="flex flex-col items-center p-7 text-center">
|
||||
<img
|
||||
alt="no roadmaps"
|
||||
src={RoadmapIcon}
|
||||
class="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
|
||||
/>
|
||||
<h2 class="text-lg sm:text-xl font-bold">No Progress</h2>
|
||||
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
|
||||
Progress will appear here as you start tracking your{' '}
|
||||
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline">
|
||||
Roadmaps
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="/best-practices" class="mt-4 text-blue-500 hover:underline">
|
||||
Best Practices
|
||||
</a>{' '}
|
||||
progress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
src/components/Activity/ResourceProgress.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useState } from 'preact/hooks';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
|
||||
type ResourceProgressType = {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceId: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
totalCount: number;
|
||||
doneCount: number;
|
||||
learningCount: number;
|
||||
skippedCount: number;
|
||||
onCleared?: () => void;
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
export function ResourceProgress(props: ResourceProgressType) {
|
||||
const { showClearButton = true } = props;
|
||||
const toast = useToast();
|
||||
const [isClearing, setIsClearing] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
const {
|
||||
updatedAt,
|
||||
resourceType,
|
||||
resourceId,
|
||||
title,
|
||||
totalCount,
|
||||
learningCount,
|
||||
doneCount,
|
||||
skippedCount,
|
||||
onCleared,
|
||||
} = props;
|
||||
|
||||
async function clearProgress() {
|
||||
setIsClearing(true);
|
||||
const { error, response } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-clear-resource-progress`,
|
||||
{
|
||||
resourceId,
|
||||
resourceType,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error('Error clearing progress. Please try again.');
|
||||
console.error(error);
|
||||
setIsClearing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(`${resourceType}-${resourceId}-favorite`);
|
||||
localStorage.removeItem(`${resourceType}-${resourceId}-progress`);
|
||||
|
||||
setIsClearing(false);
|
||||
setIsConfirming(false);
|
||||
if (onCleared) {
|
||||
onCleared();
|
||||
}
|
||||
}
|
||||
|
||||
const url =
|
||||
resourceType === 'roadmap'
|
||||
? `/${resourceId}`
|
||||
: `/best-practices/${resourceId}`;
|
||||
|
||||
const totalMarked = doneCount + skippedCount;
|
||||
const progressPercentage = Math.round((totalMarked / totalCount) * 100);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
href={url}
|
||||
className="group relative flex cursor-pointer items-center rounded-t-md border p-3 text-gray-600 hover:border-gray-300 hover:text-black"
|
||||
>
|
||||
<span
|
||||
className={`absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10`}
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
></span>
|
||||
<span className="relative flex-1 cursor-pointer truncate">
|
||||
{title}
|
||||
</span>
|
||||
<span className="ml-1 cursor-pointer text-sm text-gray-400">
|
||||
{getRelativeTimeString(updatedAt)}
|
||||
</span>
|
||||
</a>
|
||||
<p className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
|
||||
<span className="hidden flex-1 gap-1 sm:flex">
|
||||
{doneCount > 0 && (
|
||||
<>
|
||||
<span>{doneCount} done</span> •
|
||||
</>
|
||||
)}
|
||||
{learningCount > 0 && (
|
||||
<>
|
||||
<span>{learningCount} in progress</span> •
|
||||
</>
|
||||
)}
|
||||
{skippedCount > 0 && (
|
||||
<>
|
||||
<span>{skippedCount} skipped</span> •
|
||||
</>
|
||||
)}
|
||||
<span>{totalCount} total</span>
|
||||
</span>
|
||||
{showClearButton && (
|
||||
<>
|
||||
{!isConfirming && (
|
||||
<button
|
||||
className="text-red-500 hover:text-red-800"
|
||||
onClick={() => setIsConfirming(true)}
|
||||
disabled={isClearing}
|
||||
>
|
||||
{!isClearing && (
|
||||
<>
|
||||
Clear Progress <span>×</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isClearing && 'Processing...'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isConfirming && (
|
||||
<span>
|
||||
Are you sure?{' '}
|
||||
<button
|
||||
onClick={clearProgress}
|
||||
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
|
||||
>
|
||||
Yes
|
||||
</button>{' '}
|
||||
<button
|
||||
onClick={() => setIsConfirming(false)}
|
||||
className="text-red-500 underline hover:text-red-800"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
174
src/components/AddTeamRoadmap.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { useOutsideClick } from '../hooks/use-outside-click';
|
||||
import { OptionType, SearchSelector } from './SearchSelector';
|
||||
import type { PageType } from './CommandMenu/CommandMenu';
|
||||
import { CheckIcon } from './ReactIcons/CheckIcon';
|
||||
import { httpPut } from '../lib/http';
|
||||
import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector';
|
||||
import { Spinner } from './ReactIcons/Spinner';
|
||||
|
||||
type AddTeamRoadmapProps = {
|
||||
teamId: string;
|
||||
allRoadmaps: PageType[];
|
||||
availableRoadmaps: PageType[];
|
||||
onClose: () => void;
|
||||
onMakeChanges: (roadmapId: string) => void;
|
||||
setResourceConfigs: (config: TeamResourceConfig) => void;
|
||||
};
|
||||
|
||||
export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
|
||||
const {
|
||||
teamId,
|
||||
onMakeChanges,
|
||||
onClose,
|
||||
allRoadmaps,
|
||||
availableRoadmaps,
|
||||
setResourceConfigs,
|
||||
} = props;
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedRoadmap, setSelectedRoadmap] = useState<string>('');
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
async function addTeamResource(roadmapId: string) {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { error, response } = await httpPut<TeamResourceConfig>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-update-team-resource-config/${teamId}`,
|
||||
{
|
||||
teamId: teamId,
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
removed: [],
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setError(error?.message || 'Error adding roadmap');
|
||||
return;
|
||||
}
|
||||
|
||||
setResourceConfigs(response);
|
||||
}
|
||||
|
||||
useOutsideClick(popupBodyEl, () => {
|
||||
onClose();
|
||||
});
|
||||
|
||||
const selectedRoadmapTitle = allRoadmaps.find(
|
||||
(roadmap) => roadmap.id === selectedRoadmap
|
||||
)?.title;
|
||||
|
||||
return (
|
||||
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
|
||||
<div class="relative h-full w-full max-w-md p-4 md:h-auto">
|
||||
<div
|
||||
ref={popupBodyEl}
|
||||
class="popup-body relative rounded-lg bg-white p-4 shadow"
|
||||
>
|
||||
{isLoading && (
|
||||
<>
|
||||
<div class="flex items-center justify-center gap-2 py-8">
|
||||
<Spinner isDualRing={false} className="h-4 w-4" />
|
||||
<h2 className="font-medium">Loading...</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && selectedRoadmap && (
|
||||
<div className={'text-center'}>
|
||||
<CheckIcon additionalClasses="h-10 w-10 mx-auto opacity-20 mb-3 mt-4" />
|
||||
<h3 class="mb-1.5 text-2xl font-medium">
|
||||
{selectedRoadmapTitle} Added
|
||||
</h3>
|
||||
<p className="mb-4 text-sm leading-none text-gray-400">
|
||||
<button
|
||||
onClick={() => onMakeChanges(selectedRoadmap)}
|
||||
className="underline underline-offset-2 hover:text-gray-900"
|
||||
>
|
||||
Click here
|
||||
</button>{' '}
|
||||
to make changes to the roadmap.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRoadmap('');
|
||||
setError('');
|
||||
setIsLoading(false);
|
||||
}}
|
||||
type="button"
|
||||
class="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
|
||||
>
|
||||
+ Add More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<>
|
||||
<h3 class="mb-1.5 text-2xl font-medium">Error</h3>
|
||||
<p className="mb-3 text-sm leading-none text-red-400">{error}</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && !selectedRoadmap && (
|
||||
<>
|
||||
<h3 class="mb-1.5 text-2xl font-medium">Add Roadmap</h3>
|
||||
<p className="mb-3 text-sm leading-none text-gray-400">
|
||||
Search and add a roadmap
|
||||
</p>
|
||||
|
||||
<SearchSelector
|
||||
options={availableRoadmaps.map((roadmap) => ({
|
||||
value: roadmap.id,
|
||||
label: roadmap.title,
|
||||
}))}
|
||||
onSelect={(option: OptionType) => {
|
||||
const roadmapId = option.value;
|
||||
addTeamResource(roadmapId).finally(() => {
|
||||
setIsLoading(false);
|
||||
setSelectedRoadmap(roadmapId);
|
||||
});
|
||||
}}
|
||||
inputClassName="mt-2 mb-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
|
||||
placeholder={'Search for roadmap'}
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||