Compare commits
264 Commits
3.0
...
content/ty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f780487c71 | ||
|
|
a0ba51be1b | ||
|
|
7c206ed3f4 | ||
|
|
a530062bfc | ||
|
|
85fcad3c1e | ||
|
|
32c2199411 | ||
|
|
aabe43008a | ||
|
|
bf39bc3478 | ||
|
|
36e8abd38c | ||
|
|
0d2be7724f | ||
|
|
e38408f415 | ||
|
|
411387bda6 | ||
|
|
94ebd6cc89 | ||
|
|
71af9e3070 | ||
|
|
d59455425b | ||
|
|
f5295476f8 | ||
|
|
ea02c8835a | ||
|
|
e13733a503 | ||
|
|
6f0ad58764 | ||
|
|
f68c303ffa | ||
|
|
b2c79ff395 | ||
|
|
ff16ea542f | ||
|
|
e3adcdaba4 | ||
|
|
6783d7ea44 | ||
|
|
f06dfce7fb | ||
|
|
3df8db5fa5 | ||
|
|
5c92cdedd8 | ||
|
|
07b6d067c4 | ||
|
|
a7f9e7d735 | ||
|
|
3521525611 | ||
|
|
43260ff14f | ||
|
|
102ccc6a6b | ||
|
|
415dc2d8e8 | ||
|
|
e0e6168cfe | ||
|
|
dd7c0ec003 | ||
|
|
190c75cebe | ||
|
|
813a3d9b2b | ||
|
|
c2dda3bc35 | ||
|
|
4711ab9a6f | ||
|
|
5f2836a148 | ||
|
|
badb2c029d | ||
|
|
8a24b3e695 | ||
|
|
8b3f8ee6b8 | ||
|
|
f9db9bee95 | ||
|
|
e8d2bd00c6 | ||
|
|
f4e505113c | ||
|
|
f675f08d83 | ||
|
|
a12ec64af5 | ||
|
|
24512374e8 | ||
|
|
359f5d6a4d | ||
|
|
c7302d7484 | ||
|
|
6ab477df8d | ||
|
|
961d00e70e | ||
|
|
c1a53cf3cc | ||
|
|
1f485c21f7 | ||
|
|
e886d0bacb | ||
|
|
8a07f2f685 | ||
|
|
19ad916334 | ||
|
|
b30016b6f4 | ||
|
|
57395f769a | ||
|
|
b91c11b273 | ||
|
|
c026f9c928 | ||
|
|
aee51ee43e | ||
|
|
3b12130579 | ||
|
|
3dd9429338 | ||
|
|
0af54cd906 | ||
|
|
750e6e5a36 | ||
|
|
5b93bc42db | ||
|
|
8b32a3a831 | ||
|
|
a28204c908 | ||
|
|
4aca07e3d4 | ||
|
|
5c2562dadb | ||
|
|
e934dc60f4 | ||
|
|
ad4f35764d | ||
|
|
a715a85b46 | ||
|
|
f16a207e7c | ||
|
|
6582d65935 | ||
|
|
ab36350cdc | ||
|
|
3b05a615d8 | ||
|
|
9a2bc75646 | ||
|
|
d283ce7c67 | ||
|
|
59ed243fa7 | ||
|
|
ca35551e4f | ||
|
|
cab06b46da | ||
|
|
f5e980d8ec | ||
|
|
6187b1dc52 | ||
|
|
a3b8b5653a | ||
|
|
8f8e2f41d8 | ||
|
|
89a436a5b7 | ||
|
|
231e295f01 | ||
|
|
64e20e9fc1 | ||
|
|
621f841fbf | ||
|
|
c61afb15bc | ||
|
|
595f3680be | ||
|
|
ee65c56bf3 | ||
|
|
a2c339f2d5 | ||
|
|
a3031a2371 | ||
|
|
952169ec8e | ||
|
|
fbd82ce215 | ||
|
|
35f61e876e | ||
|
|
bb9878fdb7 | ||
|
|
ee843cc9e2 | ||
|
|
cbd79ef299 | ||
|
|
af9e266190 | ||
|
|
0cbd401071 | ||
|
|
0929d40bd0 | ||
|
|
927aa0a066 | ||
|
|
85eff7f894 | ||
|
|
11695f4b05 | ||
|
|
aebee9b3a3 | ||
|
|
6b52baf093 | ||
|
|
6922fd826f | ||
|
|
ec29e1836e | ||
|
|
dca9eb32cd | ||
|
|
4b681c6317 | ||
|
|
9c24ff23e3 | ||
|
|
cdc87a99e1 | ||
|
|
ea16e99598 | ||
|
|
ba86e8a6b1 | ||
|
|
5f23d4c7eb | ||
|
|
8264c4509f | ||
|
|
6c8aea98da | ||
|
|
64ccd02d53 | ||
|
|
f8c1c6278b | ||
|
|
4786265e04 | ||
|
|
8badf383b2 | ||
|
|
c4406b7649 | ||
|
|
1e878069bc | ||
|
|
8234de2b8c | ||
|
|
3466708ed4 | ||
|
|
b440fd9787 | ||
|
|
9bc73ab738 | ||
|
|
91c16a5e32 | ||
|
|
1768150fb1 | ||
|
|
4a1374c978 | ||
|
|
43df31b312 | ||
|
|
2037edb2da | ||
|
|
de8a4d4acf | ||
|
|
a67a27299e | ||
|
|
5d164198d4 | ||
|
|
a76b9d9ac0 | ||
|
|
6ed83349ba | ||
|
|
1b21550e48 | ||
|
|
c1d0ff7ea2 | ||
|
|
26125fc6d7 | ||
|
|
aff7d8eece | ||
|
|
fd939f198a | ||
|
|
18e4804a51 | ||
|
|
ed8bf11150 | ||
|
|
61f088d42a | ||
|
|
faee01b22d | ||
|
|
dc56ef6190 | ||
|
|
f393a23994 | ||
|
|
8e61330080 | ||
|
|
2c18529429 | ||
|
|
88ff836bfb | ||
|
|
66cb4f9a06 | ||
|
|
d9697b74fd | ||
|
|
863b7fa08b | ||
|
|
64078f9d1a | ||
|
|
5f8ead3d2f | ||
|
|
cb16abc8e1 | ||
|
|
52d00a0654 | ||
|
|
d5495f7280 | ||
|
|
564c9fdd4f | ||
|
|
e75df0ef9e | ||
|
|
642cbbf6d3 | ||
|
|
032602ad3b | ||
|
|
522f16957a | ||
|
|
1f3bf761cd | ||
|
|
f76f0ea1a6 | ||
|
|
a40457edc8 | ||
|
|
076db6dd0a | ||
|
|
d2b5e17ea2 | ||
|
|
3f027cbebb | ||
|
|
bd68dd4ab6 | ||
|
|
f681a0ab8a | ||
|
|
fbdf3a6942 | ||
|
|
af27626a1c | ||
|
|
86a1a4a078 | ||
|
|
44c908eca1 | ||
|
|
6ee7e4873f | ||
|
|
313e584711 | ||
|
|
58315932be | ||
|
|
20cbf7b432 | ||
|
|
0159292df2 | ||
|
|
f5c4ff59a7 | ||
|
|
bd9e2876e3 | ||
|
|
292d15ff19 | ||
|
|
b4ffb0e58e | ||
|
|
0f698b3647 | ||
|
|
75e840e26b | ||
|
|
b7544a5c44 | ||
|
|
f268ca934a | ||
|
|
5dfd43b1e4 | ||
|
|
4e44595948 | ||
|
|
a178eac1bb | ||
|
|
a6855d5c48 | ||
|
|
a2aff51deb | ||
|
|
abbc661858 | ||
|
|
f4cf194638 | ||
|
|
2ac1781118 | ||
|
|
f5adabcdc2 | ||
|
|
b90c6d8569 | ||
|
|
8120a544bf | ||
|
|
d512bbf95d | ||
|
|
f33ae82298 | ||
|
|
c359aefdac | ||
|
|
9dbb2d05c9 | ||
|
|
d1445d6c7c | ||
|
|
64edd70ec4 | ||
|
|
d7b201c7f7 | ||
|
|
577613132b | ||
|
|
00c118dc67 | ||
|
|
bb2cc12ed0 | ||
|
|
b34376ce3e | ||
|
|
9f4ffa211e | ||
|
|
4f9153fdb3 | ||
|
|
7a5068419c | ||
|
|
b30f7d2e96 | ||
|
|
40ddeef742 | ||
|
|
c1e8c0361b | ||
|
|
3613916570 | ||
|
|
5b4ed23946 | ||
|
|
9144002b89 | ||
|
|
303dbcbd7d | ||
|
|
0e45a409da | ||
|
|
280c2b8623 | ||
|
|
cbc53a66d8 | ||
|
|
9e010f10b9 | ||
|
|
6216c3051e | ||
|
|
de53dcf02d | ||
|
|
9b74a5fa62 | ||
|
|
274060a08f | ||
|
|
29dbc0a968 | ||
|
|
796f66698b | ||
|
|
726b9e6d66 | ||
|
|
b176d2c23e | ||
|
|
32761b5587 | ||
|
|
6f337f6b53 | ||
|
|
6d68542f25 | ||
|
|
cb08ead276 | ||
|
|
e9e68d1361 | ||
|
|
b1feefe2da | ||
|
|
30aef56202 | ||
|
|
e002e3c478 | ||
|
|
9492c61955 | ||
|
|
50b0309590 | ||
|
|
bc13c3bdad | ||
|
|
fed1803243 | ||
|
|
02883e50f0 | ||
|
|
f9911b6597 | ||
|
|
16ebf98164 | ||
|
|
451c36dca4 | ||
|
|
274eeece7a | ||
|
|
358c1440d8 | ||
|
|
362588cf1b | ||
|
|
228aea0b1a | ||
|
|
03da0ef24d | ||
|
|
0932f36953 | ||
|
|
10883454f5 | ||
|
|
45a7aad669 | ||
|
|
a5874bd057 | ||
|
|
16f48a1958 |
18
.eslintrc
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@next/next/no-img-element": [
|
||||
"off"
|
||||
],
|
||||
"react/display-name": [
|
||||
"off"
|
||||
],
|
||||
"react/jsx-no-target-blank": [
|
||||
"off"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
.github/images/banner.png
vendored
|
Before Width: | Height: | Size: 39 KiB |
1
.github/sponsors/doppler-logo.svg
vendored
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3473 1069"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#111;}.cls-3{fill-rule:evenodd;fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="658.73" y1="777.7" x2="341.45" y2="352.06" gradientTransform="matrix(1, 0, 0, -1, 0, 1070)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#33a9ff"/><stop offset="1" stop-color="#1673ff"/></linearGradient></defs><rect class="cls-1" width="3473" height="1069"/><path class="cls-2" d="M1054.06,633.32q4.93.45,11.23.9H1081q52.55,0,77.7-26.5,25.59-26.49,25.59-73.18,0-48.94-24.25-74.09t-76.79-25.14q-7.18,0-14.82.45-5.78,0-11,.51a3.73,3.73,0,0,0-3.33,3.74Zm202.54-98.78Q1256.6,575,1244,605t-35.92,49.84q-22.9,19.75-56.14,29.63t-74.55,9.88q-18.86,0-44-1.79A338.32,338.32,0,0,1,984,686.3V386.41a3.8,3.8,0,0,1,3.13-3.75,386.34,386.34,0,0,1,47.17-5.27q26.49-1.8,45.36-1.8,40,0,72.3,9,32.79,9,56.14,28.29T1244,462.25Q1256.61,492.33,1256.6,534.54Z"/><path class="cls-2" d="M1397.52,534.54q0,22.89,5.39,41.31a103.13,103.13,0,0,0,16.17,31.87,74.66,74.66,0,0,0,26.05,20.21q15.27,7.19,35,7.18,19.3,0,34.58-7.18a69.47,69.47,0,0,0,26-20.21A91.41,91.41,0,0,0,1557,575.85q5.83-18.42,5.84-41.31T1557,493.23q-5.39-18.86-16.17-31.88a67.73,67.73,0,0,0-26-20.65q-15.27-7.18-34.58-7.19-19.77,0-35,7.64a72.5,72.5,0,0,0-26.05,20.65q-10.33,13-16.17,31.88A145.23,145.23,0,0,0,1397.52,534.54Zm237.57,0q0,40-12.12,70.49-11.68,30.09-32.34,50.74a134.89,134.89,0,0,1-49.4,30.53,176.88,176.88,0,0,1-61.07,10.33A174.23,174.23,0,0,1,1420,686.3a140,140,0,0,1-49.4-30.53q-21.11-20.65-33.23-50.74-12.13-30.53-12.13-70.49t12.58-70q12.57-30.53,33.68-51.18a142,142,0,0,1,49.4-31A171.39,171.39,0,0,1,1480.16,372a174.08,174.08,0,0,1,60.18,10.33,136.87,136.87,0,0,1,49.4,31q21.1,20.65,33.23,51.18Q1635.09,494.58,1635.09,534.54Z"/><path class="cls-2" d="M1810.48,375.59q69.62,0,106.88,24.7,37.28,24.24,37.28,79.92,0,56.13-37.72,81.27-37.73,24.69-107.79,24.69H1791a3.83,3.83,0,0,0-3.83,3.83v96.51a3.83,3.83,0,0,1-3.83,3.83h-66.23V386.83a3.81,3.81,0,0,1,3.1-3.75,400.76,400.76,0,0,1,45.4-5.69Q1791.18,375.59,1810.48,375.59Zm4.49,59.72q-7.63,0-15.27.45-5,.3-9.06.62a3.8,3.8,0,0,0-3.51,3.8v86.28h22q36.38,0,54.79-9.88t18.42-36.82q0-13-4.94-21.55a32.47,32.47,0,0,0-13.48-13.47q-8.54-5.39-21.1-7.19A159.75,159.75,0,0,0,1815,435.31Z"/><path class="cls-2" d="M2123.07,375.59q69.61,0,106.89,24.7,37.27,24.24,37.27,79.92,0,56.13-37.72,81.27-37.73,24.69-107.78,24.69h-18.18a3.83,3.83,0,0,0-3.83,3.83v96.51a3.83,3.83,0,0,1-3.83,3.83h-66.23V386.82a3.8,3.8,0,0,1,3.1-3.74,400.76,400.76,0,0,1,45.4-5.69Q2103.77,375.59,2123.07,375.59Zm4.49,59.72q-7.64,0-15.27.45-5,.3-9.06.62a3.81,3.81,0,0,0-3.51,3.8v86.28h22q36.38,0,54.78-9.88t18.42-36.82q0-13-4.94-21.55a32.47,32.47,0,0,0-13.48-13.47q-8.52-5.39-21.1-7.19A159.75,159.75,0,0,0,2127.56,435.31Z"/><path class="cls-2" d="M2540.5,630.17a1.29,1.29,0,0,1,1.28,1.28v57.62a1.28,1.28,0,0,1-1.28,1.27H2342.25V401.41a3.83,3.83,0,0,1,2.81-3.69l67.25-18.54v251Z"/><path class="cls-2" d="M2618.88,690.34V383a3.84,3.84,0,0,1,3.83-3.83h215a1.28,1.28,0,0,1,1.23,1.64l-16.44,56.26a1.26,1.26,0,0,1-1.22.92H2692.77a3.83,3.83,0,0,0-3.83,3.83v57.24H2803.1a1.27,1.27,0,0,1,1.23,1.63l-16,54.92a1.28,1.28,0,0,1-1.22.92h-94.34a3.82,3.82,0,0,0-3.83,3.83v71.15h154.72a1.28,1.28,0,0,1,1.23,1.63l-16.34,56.27a1.27,1.27,0,0,1-1.22.92Z"/><path class="cls-2" d="M3006,375.59q70.07,0,107.34,25.15,37.28,24.69,37.27,77.22,0,32.79-15.27,53.44-14.82,20.19-43.11,31.87,9.43,11.68,19.76,26.94,10.34,14.82,20.21,31.43,10.32,16.17,19.76,34.13,8.93,16.58,16.65,32.75a1.28,1.28,0,0,1-1.16,1.82H3091.6a1.27,1.27,0,0,1-1.11-.65q-8.37-15-17.15-30.33-8.53-15.72-18-30.53-9-14.82-18-27.84a286.92,286.92,0,0,0-18-24.25h-30.76a3.83,3.83,0,0,0-3.82,3.83V686.51a3.83,3.83,0,0,1-3.83,3.83h-66.23V386.82a3.8,3.8,0,0,1,3.09-3.74,400,400,0,0,1,44.06-5.69Q2986.67,375.59,3006,375.59Zm4.05,59.72q-7.64,0-13.93.45-4,.3-7.71.61a3.82,3.82,0,0,0-3.51,3.81v80.89h19.76q39.51,0,56.58-9.88t17.07-33.67q0-22.9-17.52-32.33Q3043.71,435.31,3010,435.31Z"/><path class="cls-3" d="M307.26,310a1.79,1.79,0,0,0-1.5,2.75l89.7,140.38a14.19,14.19,0,0,0,12,6.58H528.38c39.92,0,64.87,35.28,64.72,74.79s-26.74,74.44-64.72,74.44H404.77a4.71,4.71,0,0,0-4.71,4.75V754.25a4.72,4.72,0,0,0,4.72,4.75H560.62C689.12,759,753,637.1,753.09,534.5S690,310,560.62,310ZM367,609.29H336.16C318.4,609.29,304,626,304,646.71V757.66a1.28,1.28,0,0,0,1.28,1.28h30.88c17.76,0,32.15-16.75,32.15-37.41v-111A1.27,1.27,0,0,0,367,609.29Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
59
.github/sponsors/oss-logo.svg
vendored
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 646.6 105.7" style="enable-background:new 0 0 646.6 105.7;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#104366;}
|
||||
.st1{fill:#4086C6;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M21.1,79.8c-6.6-3.5-11.7-8.4-15.5-14.6C1.9,59,0,52,0,44.3c0-7.8,1.9-14.7,5.6-20.9
|
||||
c3.7-6.2,8.9-11.1,15.5-14.6c6.6-3.5,14-5.3,22.2-5.3c8.2,0,15.6,1.8,22.1,5.3c6.6,3.5,11.7,8.4,15.5,14.6
|
||||
c3.8,6.2,5.6,13.2,5.6,20.9c0,7.8-1.9,14.7-5.6,20.9c-3.8,6.2-8.9,11.1-15.5,14.6c-6.5,3.5-13.9,5.3-22.1,5.3
|
||||
C35,85.1,27.6,83.4,21.1,79.8z M55.9,66.3c3.8-2.1,6.7-5.1,8.9-9c2.1-3.8,3.2-8.2,3.2-13.1c0-4.9-1.1-9.3-3.2-13.1
|
||||
c-2.1-3.8-5.1-6.8-8.9-9c-3.8-2.1-8-3.2-12.6-3.2c-4.7,0-8.9,1.1-12.6,3.2s-6.7,5.1-8.9,9c-2.1,3.8-3.2,8.2-3.2,13.1
|
||||
c0,4.9,1.1,9.3,3.2,13.1c2.1,3.8,5.1,6.8,8.9,9c3.8,2.1,8,3.2,12.6,3.2C47.9,69.5,52.1,68.5,55.9,66.3z"/>
|
||||
<path class="st0" d="M108.1,82.6c-5.8-1.7-10.5-3.9-14.1-6.6l6.2-13.8c3.4,2.5,7.4,4.5,12.1,6c4.7,1.5,9.3,2.3,14,2.3
|
||||
c5.2,0,9-0.8,11.5-2.3c2.5-1.5,3.7-3.6,3.7-6.2c0-1.9-0.7-3.4-2.2-4.7c-1.5-1.2-3.3-2.2-5.6-3c-2.3-0.8-5.4-1.6-9.3-2.5
|
||||
c-6-1.4-11-2.9-14.8-4.3c-3.8-1.4-7.1-3.7-9.9-6.9c-2.7-3.2-4.1-7.4-4.1-12.6c0-4.6,1.2-8.7,3.7-12.5c2.5-3.7,6.2-6.7,11.2-8.9
|
||||
c5-2.2,11.1-3.3,18.3-3.3c5,0,10,0.6,14.8,1.8c4.8,1.2,9,2.9,12.6,5.2l-5.6,13.9c-7.3-4.1-14.6-6.2-21.9-6.2
|
||||
c-5.1,0-8.9,0.8-11.3,2.5c-2.4,1.7-3.7,3.8-3.7,6.5c0,2.7,1.4,4.7,4.2,6c2.8,1.3,7.1,2.6,12.9,3.9c6,1.4,11,2.9,14.8,4.3
|
||||
c3.8,1.4,7.1,3.7,9.9,6.8c2.7,3.1,4.1,7.3,4.1,12.5c0,4.5-1.3,8.6-3.8,12.4c-2.5,3.7-6.3,6.7-11.3,8.9c-5,2.2-11.2,3.3-18.4,3.3
|
||||
C120,85.1,113.9,84.3,108.1,82.6z"/>
|
||||
<path class="st0" d="M180.1,82.6c-5.8-1.7-10.5-3.9-14.1-6.6l6.2-13.8c3.4,2.5,7.4,4.5,12.1,6c4.7,1.5,9.3,2.3,14,2.3
|
||||
c5.2,0,9-0.8,11.5-2.3c2.5-1.5,3.7-3.6,3.7-6.2c0-1.9-0.7-3.4-2.2-4.7c-1.5-1.2-3.3-2.2-5.6-3c-2.3-0.8-5.4-1.6-9.3-2.5
|
||||
c-6-1.4-10.9-2.9-14.8-4.3c-3.8-1.4-7.1-3.7-9.9-6.9c-2.7-3.2-4.1-7.4-4.1-12.6c0-4.6,1.2-8.7,3.7-12.5c2.5-3.7,6.2-6.7,11.2-8.9
|
||||
s11.1-3.3,18.3-3.3c5,0,10,0.6,14.8,1.8c4.8,1.2,9,2.9,12.6,5.2l-5.6,13.9c-7.3-4.1-14.6-6.2-21.9-6.2c-5.1,0-8.9,0.8-11.3,2.5
|
||||
c-2.4,1.7-3.7,3.8-3.7,6.5c0,2.7,1.4,4.7,4.2,6c2.8,1.3,7.1,2.6,12.9,3.9c6,1.4,10.9,2.9,14.8,4.3s7.1,3.7,9.9,6.8
|
||||
c2.7,3.1,4.1,7.3,4.1,12.5c0,4.5-1.3,8.6-3.8,12.4c-2.5,3.7-6.3,6.7-11.3,8.9c-5,2.2-11.2,3.3-18.4,3.3
|
||||
C192,85.1,186,84.3,180.1,82.6z"/>
|
||||
<path class="st1" d="M293.2,79.1c-6.2-3.5-11.1-8.2-14.7-14.3c-3.6-6.1-5.4-12.9-5.4-20.5c0-7.6,1.8-14.5,5.4-20.5
|
||||
c3.6-6.1,8.5-10.9,14.7-14.3c6.2-3.5,13.2-5.2,20.9-5.2c5.7,0,11,0.9,15.8,2.8c4.8,1.8,8.9,4.6,12.3,8.2l-3.6,3.7
|
||||
c-6.3-6.2-14.4-9.4-24.3-9.4c-6.6,0-12.6,1.5-18.1,4.5c-5.4,3-9.7,7.2-12.8,12.5s-4.6,11.2-4.6,17.8s1.5,12.5,4.6,17.8
|
||||
c3.1,5.3,7.3,9.5,12.8,12.5c5.4,3,11.4,4.5,18.1,4.5c9.8,0,17.9-3.2,24.3-9.5l3.6,3.7c-3.4,3.6-7.5,6.4-12.4,8.2
|
||||
c-4.9,1.9-10.1,2.8-15.7,2.8C306.3,84.3,299.4,82.6,293.2,79.1z"/>
|
||||
<path class="st1" d="M395.4,30c3.9,3.7,5.9,9.2,5.9,16.4v37.4h-5.4V73.3c-1.9,3.5-4.6,6.2-8.2,8.1c-3.6,1.9-7.9,2.9-13,2.9
|
||||
c-6.5,0-11.7-1.5-15.5-4.6c-3.8-3.1-5.7-7.1-5.7-12.2c0-4.9,1.8-8.9,5.2-11.9c3.5-3,9.1-4.6,16.8-4.6h20.2v-4.7
|
||||
c0-5.5-1.5-9.7-4.5-12.5c-3-2.9-7.3-4.3-13-4.3c-3.9,0-7.7,0.7-11.2,2c-3.6,1.4-6.6,3.2-9.1,5.4l-2.8-4.1c2.9-2.6,6.5-4.7,10.6-6.2
|
||||
c4.1-1.5,8.5-2.2,13-2.2C385.9,24.4,391.5,26.3,395.4,30z M387.9,76.2c3.4-2.3,6-5.5,7.7-9.8V55.3h-20.1c-5.8,0-10,1.1-12.6,3.2
|
||||
c-2.6,2.1-3.9,5-3.9,8.7c0,3.8,1.4,6.9,4.3,9.1c2.9,2.2,6.9,3.3,12.1,3.3C380.3,79.6,384.5,78.5,387.9,76.2z"/>
|
||||
<path class="st1" d="M469,28.2c4.4,2.6,7.9,6.1,10.4,10.6c2.5,4.5,3.8,9.7,3.8,15.5c0,5.8-1.3,11-3.8,15.5
|
||||
c-2.5,4.6-6,8.1-10.4,10.6c-4.4,2.5-9.4,3.8-14.9,3.8c-5.2,0-9.9-1.2-14.1-3.7c-4.2-2.4-7.5-5.9-9.8-10.2v35.3h-5.6V24.8h5.4v13.9
|
||||
c2.3-4.5,5.6-8,9.9-10.6c4.2-2.5,9-3.8,14.3-3.8C459.6,24.4,464.6,25.7,469,28.2z M465.9,76c3.6-2.1,6.5-5,8.5-8.8
|
||||
c2.1-3.8,3.1-8.1,3.1-12.9c0-4.8-1-9.1-3.1-12.9c-2.1-3.8-4.9-6.7-8.5-8.8c-3.6-2.1-7.7-3.2-12.2-3.2c-4.5,0-8.6,1.1-12.1,3.2
|
||||
c-3.6,2.1-6.4,5-8.5,8.8c-2.1,3.8-3.1,8.1-3.1,12.9c0,4.8,1,9.1,3.1,12.9c2.1,3.8,4.9,6.7,8.5,8.8c3.6,2.1,7.6,3.2,12.1,3.2
|
||||
C458.3,79.1,462.3,78.1,465.9,76z"/>
|
||||
<path class="st1" d="M500.3,9.2c-0.9-0.9-1.4-1.9-1.4-3.2c0-1.3,0.5-2.4,1.4-3.3c0.9-0.9,2-1.4,3.3-1.4c1.3,0,2.4,0.4,3.3,1.3
|
||||
c0.9,0.9,1.4,1.9,1.4,3.2c0,1.3-0.5,2.4-1.4,3.3c-0.9,0.9-2,1.4-3.3,1.4C502.3,10.5,501.2,10.1,500.3,9.2z M500.7,24.8h5.6v58.9
|
||||
h-5.6V24.8z"/>
|
||||
<path class="st1" d="M559.4,80c-1.4,1.4-3.2,2.4-5.4,3.1c-2.1,0.7-4.4,1.1-6.7,1.1c-5.1,0-9.1-1.4-11.9-4.2
|
||||
c-2.8-2.8-4.2-6.8-4.2-11.8V29.7h-10.8v-4.9h10.8V12h5.6v12.9h18.7v4.9H537v37.9c0,3.8,0.9,6.8,2.8,8.8c1.8,2,4.6,3,8.2,3
|
||||
c3.7,0,6.7-1.1,9.1-3.3L559.4,80z"/>
|
||||
<path class="st1" d="M611.8,30c3.9,3.7,5.9,9.2,5.9,16.4v37.4h-5.4V73.3c-1.9,3.5-4.6,6.2-8.2,8.1c-3.6,1.9-7.9,2.9-13,2.9
|
||||
c-6.5,0-11.7-1.5-15.5-4.6c-3.8-3.1-5.7-7.1-5.7-12.2c0-4.9,1.8-8.9,5.2-11.9c3.5-3,9.1-4.6,16.8-4.6H612v-4.7
|
||||
c0-5.5-1.5-9.7-4.5-12.5c-3-2.9-7.3-4.3-13-4.3c-3.9,0-7.7,0.7-11.2,2c-3.6,1.4-6.6,3.2-9.1,5.4l-2.8-4.1c2.9-2.6,6.5-4.7,10.6-6.2
|
||||
c4.1-1.5,8.5-2.2,13-2.2C602.3,24.4,607.9,26.3,611.8,30z M604.3,76.2c3.4-2.3,6-5.5,7.7-9.8V55.3h-20.1c-5.8,0-10,1.1-12.6,3.2
|
||||
c-2.6,2.1-3.9,5-3.9,8.7c0,3.8,1.4,6.9,4.3,9.1c2.9,2.2,6.9,3.3,12.1,3.3C596.7,79.6,600.9,78.5,604.3,76.2z"/>
|
||||
<path class="st1" d="M640.9,0h5.6v83.8h-5.6V0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
11
.github/sponsors/workos-logo-white-bg.svg
vendored
@@ -1,11 +0,0 @@
|
||||
<svg width="1354" height="420" viewBox="0 0 1354 420" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1354" height="420" rx="20" fill="white"/>
|
||||
<path d="M434.751 133.122H466.637L489.595 227.729C493.852 245.585 494.697 256.219 494.697 256.219H495.128C495.128 256.219 496.61 245.808 500.867 227.729L522.757 133.122H558.9L582.066 227.729C586.53 246.223 587.598 256.219 587.598 256.219H588.236C588.236 256.219 588.666 246.223 592.907 227.729L615.02 133.122H646.907L606.523 288.313H571.017L546.576 194.344C541.474 173.936 541.044 164.801 541.044 164.801H540.614C540.614 164.801 540.183 173.936 535.512 194.344L512.553 288.313H475.996L434.751 133.122Z" fill="black"/>
|
||||
<path d="M641.583 231.934C641.583 196.428 664.541 173.47 699.202 173.47C733.639 173.47 756.597 196.428 756.597 231.934C756.597 267.647 733.639 290.828 699.202 290.828C664.557 290.812 641.583 267.647 641.583 231.934ZM726.832 231.934C726.832 208.976 715.783 195.998 699.202 195.998C681.346 195.998 671.349 210.458 671.349 231.934C671.349 255.323 682.398 268.284 699.202 268.284C717.058 268.284 726.832 253.824 726.832 231.934Z" fill="black"/>
|
||||
<path d="M770.836 175.21H799.103V196.048H799.741C804.635 185.207 816.322 174.365 836.299 174.365C839.695 174.365 841.831 174.796 843.314 175.21V203.478H842.469C842.469 203.478 839.918 202.633 832.903 202.633C811.013 202.633 799.103 215.594 799.103 239.828V288.295H770.836V175.21Z" fill="black"/>
|
||||
<path d="M856.5 133.122H884.767V182.865C884.767 212.2 884.336 217.509 884.336 217.509H884.767L926.857 175.212H962.139L912.843 224.11L970.031 288.313H936.646L895.401 241.536L884.767 251.946V288.297H856.5V133.122Z" fill="black"/>
|
||||
<path d="M970.444 211.285C970.444 163.455 1000.21 131.569 1044.85 131.569C1089.49 131.569 1119.26 163.455 1119.26 211.285C1119.26 259.114 1089.49 291.001 1044.85 291.001C1000.21 291.001 970.444 259.114 970.444 211.285ZM1088.42 211.285C1088.42 178.761 1071 156.855 1044.84 156.855C1018.67 156.855 1001.26 178.761 1001.26 211.285C1001.26 243.809 1018.69 265.715 1044.84 265.715C1070.98 265.715 1088.42 243.809 1088.42 211.285Z" fill="black"/>
|
||||
<path d="M1130.08 236.656H1162.4C1162.4 254.943 1174.95 265.146 1194.08 265.146C1210.23 265.146 1221.29 257.063 1221.29 245.584C1221.29 232.622 1212.79 229.21 1185.79 223.901C1161.12 219.007 1134.98 210.716 1134.98 178.399C1134.98 151.408 1157.93 131 1193.01 131C1229.57 131 1252.11 150.132 1252.11 179.037H1219.79C1219.79 165.007 1208.95 156.286 1193.01 156.286C1176.86 156.286 1166.86 164.146 1166.86 175.625C1166.86 187.742 1173.88 192.413 1195.56 196.878C1227.65 203.685 1254.02 207.288 1254.02 243.001C1254.02 271.3 1229.36 290.432 1193.01 290.432C1156.02 290.432 1130.08 268.957 1130.08 236.656Z" fill="black"/>
|
||||
<path d="M100 210C100 214.824 101.269 219.647 103.723 223.793L148.231 300.878C152.8 308.747 159.739 315.178 168.369 318.055C185.377 323.724 202.977 316.447 211.354 301.893L222.1 283.278L179.708 210L224.47 132.408L235.216 113.792C238.431 108.208 242.747 103.638 247.824 100H243.17H178.777C166.677 100 155.508 106.431 149.5 116.923L103.723 196.208C101.269 200.354 100 205.177 100 210Z" fill="#6363F1"/>
|
||||
<path d="M353.847 210C353.847 205.177 352.578 200.353 350.124 196.207L305.024 118.107C296.647 103.638 279.047 96.3608 262.039 101.945C253.409 104.822 246.47 111.253 241.901 119.122L231.747 136.638L274.139 210L229.378 287.592L218.632 306.208C215.416 311.708 211.101 316.362 206.024 320H210.678H275.07C287.17 320 298.34 313.569 304.347 303.077L350.124 223.792C352.578 219.646 353.847 214.823 353.847 210Z" fill="#6363F1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
21
.github/workflows/aws-costs.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Sends Daily AWS Costs to Slack
|
||||
on:
|
||||
# Allow manual Run
|
||||
workflow_dispatch:
|
||||
# Run at 7:00 UTC every day
|
||||
schedule:
|
||||
- cron: "0 7 * * *"
|
||||
jobs:
|
||||
aws_costs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Costs
|
||||
env:
|
||||
AWS_KEY: ${{ secrets.COST_AWS_ACCESS_KEY }}
|
||||
AWS_SECRET: ${{ secrets.COST_AWS_SECRET_KEY }}
|
||||
AWS_REGION: ${{ secrets.COST_AWS_REGION }}
|
||||
SLACK_CHANNEL: ${{ secrets.SLACK_COST_CHANNEL }}
|
||||
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
|
||||
run: |
|
||||
npm install -g aws-cost-cli
|
||||
aws-cost -k $AWS_KEY -s $AWS_SECRET -r $AWS_REGION -S $SLACK_TOKEN -C $SLACK_CHANNEL
|
||||
13
.github/workflows/deploy.yml
vendored
@@ -3,11 +3,9 @@ on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
env:
|
||||
ROADMAP_GA_SECRET: ${{ secrets.GA_SECRET }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PAT: ${{ secrets.PAT }}
|
||||
CI: true
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -17,14 +15,19 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7.13.4
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
npm install
|
||||
pnpm install
|
||||
- name: Generate meta and build
|
||||
run: |
|
||||
npm run meta
|
||||
npm run build
|
||||
touch ./dist/.nojekyll
|
||||
echo 'roadmap.sh' > ./dist/CNAME
|
||||
- name: Deploy to GH Pages
|
||||
run: |
|
||||
git config user.email "kamranahmed.se@gmail.com"
|
||||
|
||||
38
.github/workflows/update-deps.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Update dependencies
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allow manual run
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # every sunday at midnight
|
||||
|
||||
jobs:
|
||||
upgrade-deps:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7.13.4
|
||||
- name: Upgrade dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
npm run upgrade
|
||||
pnpm install --lockfile-only
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
delete-branch: false
|
||||
branch: "update-deps"
|
||||
base: "master"
|
||||
labels: |
|
||||
dependencies
|
||||
automated pr
|
||||
reviewers: kamranahmedse
|
||||
commit-message: "chore: update dependencies to latest"
|
||||
title: "Upgrade dependencies to latest"
|
||||
body: |
|
||||
Updates all dependencies to latest versions.
|
||||
Please review the changes and merge if everything looks good.
|
||||
45
.gitignore
vendored
@@ -1,37 +1,26 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
out
|
||||
# build output
|
||||
dist/
|
||||
.output/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
yarn.lock
|
||||
node_modules/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
bin/developer-roadmap
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.idea
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
tests-examples
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2
|
||||
}
|
||||
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
astro.config.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
// https://astro.build/config
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import compress from 'astro-compress';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula',
|
||||
},
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
{
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
build: {
|
||||
format: 'file',
|
||||
},
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false,
|
||||
},
|
||||
}),
|
||||
sitemap({
|
||||
filter: shouldIndexPage,
|
||||
serialize: serializeSitemap,
|
||||
}),
|
||||
compress({
|
||||
css: false,
|
||||
js: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
144
bin/best-practice-content.cjs
Normal file
@@ -0,0 +1,144 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(__dirname, '../src/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');
|
||||
19
bin/compress-jsons.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsonsDir = path.join(process.cwd(), 'public/jsons');
|
||||
const childJsonDirs = fs.readdirSync(jsonsDir);
|
||||
|
||||
childJsonDirs.forEach((childJsonDir) => {
|
||||
const fullChildJsonDirPath = path.join(jsonsDir, childJsonDir);
|
||||
const jsonFiles = fs.readdirSync(fullChildJsonDirPath);
|
||||
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
|
||||
const jsonFilePath = path.join(fullChildJsonDirPath, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
});
|
||||
});
|
||||
28
bin/readme.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## CLI Tools
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
|
||||
Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
## `compress-jsons.cjs`
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-content [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
|
||||
- Remove all the groups from the roadmaps through the project editor. Select all and press `cmd+shift+g`
|
||||
- Identify the boxes that should be clickable and group them together with `cmd+shift+g`
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
|
||||
|
||||
163
bin/roadmap-content.cjs
Normal file
@@ -0,0 +1,163 @@
|
||||
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');
|
||||
44
bin/roadmap-links.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const roadmapId = process.argv[2];
|
||||
if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function readFiles(folderPath) {
|
||||
const stats = fs.lstatSync(folderPath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
return [folderPath];
|
||||
}
|
||||
|
||||
const folderContent = fs.readdirSync(folderPath);
|
||||
let files = [];
|
||||
|
||||
for (const file of folderContent) {
|
||||
const filePath = path.join(folderPath, file);
|
||||
|
||||
files = [...files, ...readFiles(filePath)];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = readFiles(fullPath);
|
||||
let allLinks = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileContent = fs.readFileSync(file, 'utf-8');
|
||||
const matches = [...fileContent.matchAll(/\[[^\]]+]\((https?:\/\/[^)]+)\)/g)];
|
||||
|
||||
allLinks = [...allLinks, ...matches.map((match) => match[1])];
|
||||
});
|
||||
|
||||
allLinks.map((link) => console.log(link));
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Box, Container, Flex, Heading, Image, Link, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type ContentPageHeaderProps = {
|
||||
formattedDate: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
author?: {
|
||||
name: string;
|
||||
twitter: string;
|
||||
picture: string;
|
||||
},
|
||||
subLink?: {
|
||||
text: string;
|
||||
url: string;
|
||||
}
|
||||
};
|
||||
|
||||
export function ContentPageHeader(props: ContentPageHeaderProps) {
|
||||
const { title, subtitle, author = null, formattedDate, subLink = null } = props;
|
||||
|
||||
return (
|
||||
<Box pt={['35px', '35px', '70px']} pb={['35px', '35px', '55px']} borderBottomWidth={1} mb='30px'>
|
||||
<Container maxW='container.md' position='relative' textAlign={['left', 'left', 'center']}>
|
||||
<Flex alignItems='center' justifyContent={['flex-start', 'flex-start', 'center']}
|
||||
fontSize={['12px', '12px', '14px']}>
|
||||
|
||||
{author?.name && (
|
||||
<>
|
||||
<Link
|
||||
d={['none', 'flex', 'flex']}
|
||||
target='_blank'
|
||||
href={`https://twitter.com/${author.twitter}`}
|
||||
alignItems='center'
|
||||
fontWeight={600}
|
||||
color='gray.500'
|
||||
>
|
||||
<Image alt={''} rounded={'full'} mr='7px' w='22px' src={author.picture} />
|
||||
{author.name}
|
||||
</Link>
|
||||
<Text d={['none', 'inline', 'inline']} mx='7px' color='gray.500' as='span'>·</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text color='gray.500' as='span'>{formattedDate}</Text>
|
||||
{subLink?.text && (
|
||||
<>
|
||||
<Text d={['none', 'none', 'inline']} mx='7px' color='gray.500' as='span'>·</Text>
|
||||
<Link d={['none', 'none', 'inline']} color='blue.500' fontWeight={500}
|
||||
href={subLink.url} target={'_blank'}>{subLink.text}</Link>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Heading as='h1' color='black' fontSize={['30px', '30px', '45px']} lineHeight={['40px', '40px', '53px']}
|
||||
fontWeight={700} my={['5px', '5px', '10px']}>{title}</Heading>
|
||||
<Text fontSize={['14px', '14px', '16px']} color='gray.700'>{subtitle}</Text>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Box, Flex, Heading, Image, Link } from '@chakra-ui/react';
|
||||
import { event } from '../lib/gtag';
|
||||
|
||||
function getPageSlug() {
|
||||
const pathname = (typeof window !== 'undefined' ? window : {} as any)?.location?.pathname || '';
|
||||
|
||||
return pathname?.replace(/\//g, '');
|
||||
}
|
||||
|
||||
export const CustomAd = () => {
|
||||
const slug = getPageSlug();
|
||||
|
||||
if (slug !== 'devops') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href='https://www.getambassador.io/edge-stack-guide-v4?utm_source=roadmap.sh&utm_medium=ebook&utm_campaign=edgestack-guide'
|
||||
id='custom-ad'
|
||||
pos='fixed'
|
||||
bottom='15px'
|
||||
right='20px'
|
||||
zIndex={999}
|
||||
display='flex'
|
||||
maxWidth='330px'
|
||||
bg='white'
|
||||
boxShadow='0 1px 4px 1px hsla(0, 0%, 0%, .1)'
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
rel="noopener sponsored"
|
||||
target={'_blank'}
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'SponsorClick',
|
||||
action: `Ambassador EBook Redirect`,
|
||||
label: `Clicked Ambassador EBook Link`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src='https://i.imgur.com/0bH1Vl6.png'
|
||||
alt='Custom Logo'
|
||||
height={['100px', '100px', '100px', 'auto']}
|
||||
width='130'
|
||||
style={{ maxWidth: '130px', border: 'none' }}
|
||||
/>
|
||||
<Flex as='span' flexDirection='column' justifyContent='space-between'>
|
||||
<Box as='span' p='10px'>
|
||||
<Heading as='span' fontSize='14px' mb='5px' display='block'>Free eBook</Heading>
|
||||
<Box display='block' as='span' fontSize='13px' lineHeight={1.5} fontWeight={500} color='gray.500'>
|
||||
Learn about API Gateways, Microservices, Load Balancing, and more with this free eBook.
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as='span'
|
||||
textAlign='center'
|
||||
fontWeight={600}
|
||||
fontSize='9px'
|
||||
letterSpacing='0.5px'
|
||||
textTransform='uppercase'
|
||||
padding='5px 10px'
|
||||
display={'block'}
|
||||
background='repeating-linear-gradient(-45deg, transparent, transparent 5px, hsla(0, 0%, 0%, .025) 5px, hsla(0, 0%, 0%, .025) 10px) hsla(203, 11%, 95%, .4)'
|
||||
>
|
||||
Partner Content
|
||||
</Box>
|
||||
</Flex>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Box, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
type DimmedMoreProps = {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function DimmedMore(props: DimmedMoreProps) {
|
||||
const { text, href } = props;
|
||||
|
||||
return (
|
||||
<Box position='relative' textAlign='center' bottom='20px'>
|
||||
<Box
|
||||
opacity={1}
|
||||
pointerEvents='none'
|
||||
position='absolute'
|
||||
bottom={0}
|
||||
height='200px'
|
||||
width='100%'
|
||||
background='linear-gradient(180deg, rgb(255 255 255 / 40%), white)'
|
||||
/>
|
||||
|
||||
<Link
|
||||
rounded='20px'
|
||||
display='inline'
|
||||
bg='green.600'
|
||||
color='white'
|
||||
p='7px 20px'
|
||||
href={href}
|
||||
fontWeight={800}
|
||||
fontSize='11px'
|
||||
textTransform='uppercase'
|
||||
my='25px'
|
||||
position='relative'
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
'& .forward-arrow': {
|
||||
transform: 'translateX(3px)'
|
||||
}
|
||||
}}>
|
||||
{text}
|
||||
<Text d='inline-block' as='span' transition='200ms' ml='4px' className='forward-arrow'>→</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Box, Container, Flex, Image, Link, SimpleGrid, Stack, Text } from '@chakra-ui/react';
|
||||
import siteConfig from '../content/site.json';
|
||||
import { CustomAd } from './custom-ad';
|
||||
import React from 'react';
|
||||
import { event } from '../lib/gtag';
|
||||
|
||||
function NavigationLinks() {
|
||||
return (
|
||||
<>
|
||||
<Stack isInline display={['none', 'none', 'flex']} justifyContent='center' color='gray.400' fontWeight={600}
|
||||
spacing='30px'>
|
||||
<Link _hover={{ color: 'white' }} href='/roadmaps'>Roadmaps</Link>
|
||||
<Link _hover={{ color: 'white' }} href='/guides'>Guides</Link>
|
||||
<Link _hover={{ color: 'white' }} href='/watch'>Videos</Link>
|
||||
<Link _hover={{ color: 'white' }} href='/about'>About</Link>
|
||||
<Link _hover={{ color: 'white' }} href={siteConfig.url.youtube} target='_blank'>YouTube</Link>
|
||||
</Stack>
|
||||
|
||||
<Stack display={['flex', 'flex', 'none']} color='gray.400' fontWeight={600} spacing={0}>
|
||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }}
|
||||
href='/roadmaps'>Roadmaps</Link>
|
||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }}
|
||||
href='/guides'>Guides</Link>
|
||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }}
|
||||
href='/watch'>Videos</Link>
|
||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }}
|
||||
href='/about'>About</Link>
|
||||
<Link py='7px' _hover={{ color: 'white' }} target='_blank'
|
||||
href={siteConfig.url.youtube}>YouTube</Link>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<Box bg='brand.hero' p={['25px 0', '25px 0', '40px 0']}>
|
||||
<Container maxW='container.md'>
|
||||
<NavigationLinks />
|
||||
|
||||
<SimpleGrid mt={['40px', '40px', '50px']} mb='40px' gap={['40px', '40px', '75px']} columns={[1, 1, 2, 2]}
|
||||
justifyContent='space-between'>
|
||||
<Box maxWidth={'550px'}>
|
||||
<Flex gap={0} alignItems='center' color='gray.400'>
|
||||
<Link d='flex' alignItems='center' fontWeight={600} _hover={{ textDecoration: 'none', color: 'white' }}
|
||||
href='/'>
|
||||
<Image alt='' h='25px' w='25px' src='/logo.svg' mr='6px' />
|
||||
roadmap.sh
|
||||
</Link>
|
||||
<Text as='span' mx='7px'>by</Text>
|
||||
<Link bg='blue.500' px='6px' py='2px' rounded='4px' color='white' fontWeight={600} fontSize='13px'
|
||||
_hover={{ textDecoration: 'none', bg: 'blue.600' }} href={siteConfig.url.twitter}
|
||||
target='_blank'>@kamranahmedse</Link>
|
||||
</Flex>
|
||||
|
||||
<Text my='15px' fontSize='14px' color='gray.500'>Community created roadmaps, articles, resources and
|
||||
journeys to help you choose your path and grow in your career.</Text>
|
||||
|
||||
<Text fontSize='14px' color='gray.500'>
|
||||
<Text as='span' mr='10px'>© roadmap.sh</Text>·
|
||||
<Link href='/about' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400'
|
||||
mx='10px'>FAQs</Link>·
|
||||
<Link href='/terms' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400'
|
||||
mx='10px'>Terms</Link>·
|
||||
<Link href='/privacy' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400'
|
||||
mx='10px'>Privacy</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box maxWidth={'550px'} textAlign={['left', 'left', 'right']}>
|
||||
<Link display='flex' justifyContent={['flex-start', 'flex-start', 'flex-end']} fontWeight={600}
|
||||
_hover={{ textDecoration: 'none', color: 'white' }}
|
||||
href='https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'>
|
||||
<Image alt='' w='195px' src='/tns.png' />
|
||||
</Link>
|
||||
|
||||
<Text my='15px' fontSize='14px' color='gray.500'>The leading DevOps resource for Kubernetes, cloud-native
|
||||
computing, and the latest in at-scale development, deployment, and management.</Text>
|
||||
|
||||
<Text fontSize='14px' color='gray.500'>
|
||||
<Link
|
||||
href='https://thenewstack.io/category/devops/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
_hover={{ textDecoration: 'none', color: 'white' }}
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'PartnerClick',
|
||||
action: `TNS Referral`,
|
||||
label: `TNS Referral - Footer`,
|
||||
});
|
||||
}}
|
||||
color='gray.400' mx='10px' ml={['0', '0', '10px']}>DevOps</Link>·
|
||||
<Link
|
||||
href='https://thenewstack.io/category/kubernetes/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank' _hover={{ textDecoration: 'none', color: 'white' }}
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'PartnerClick',
|
||||
action: `TNS Referral`,
|
||||
label: `TNS Referral - Footer`,
|
||||
});
|
||||
}}
|
||||
color='gray.400' mx='10px'>Kubernetes</Link>·
|
||||
<Link
|
||||
href='https://thenewstack.io/category/cloud-native/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank' _hover={{ textDecoration: 'none', color: 'white' }}
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'PartnerClick',
|
||||
action: `TNS Referral`,
|
||||
label: `TNS Referral - Footer`,
|
||||
});
|
||||
}}
|
||||
color='gray.400' mx='10px'>Cloud-Native</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
<CustomAd />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { HamburgerIcon } from '@chakra-ui/icons';
|
||||
import { Box, CloseButton, Container, Flex, IconButton, Link, Stack, Text } from '@chakra-ui/react';
|
||||
import RoadmapLogo from '../components/icons/roadmap.svg';
|
||||
|
||||
type MenuLinkProps = {
|
||||
text: string;
|
||||
link: string;
|
||||
target?: '_blank' | '_self' | '_parent' | '_top';
|
||||
isFancy?: boolean;
|
||||
};
|
||||
|
||||
function MenuLink(props: MenuLinkProps) {
|
||||
const { text, link, target = '_self', isFancy = false } = props;
|
||||
|
||||
const gradientProp = isFancy ? {
|
||||
bgGradient: 'linear(to-r, yellow.100, teal.100)',
|
||||
bgClip: 'text',
|
||||
_hover: {
|
||||
color: 'yellow.100'
|
||||
}
|
||||
} : {};
|
||||
|
||||
return <Link
|
||||
borderBottomWidth={0}
|
||||
borderBottomColor='gray.500'
|
||||
_hover={{ textDecoration: 'none', borderBottomColor: 'white' }}
|
||||
fontWeight={500}
|
||||
href={link}
|
||||
target={target}
|
||||
{...gradientProp}
|
||||
>
|
||||
{text}
|
||||
</Link>;
|
||||
}
|
||||
|
||||
function DesktopMenuLinks() {
|
||||
return (
|
||||
<Stack d={['none', 'flex', 'flex']} shouldWrapChildren isInline spacing='15px' alignItems='center' color='gray.50'
|
||||
fontSize='15px'>
|
||||
<MenuLink text={'Roadmaps'} link={'/roadmaps'} />
|
||||
<MenuLink text={'Guides'} link={'/guides'} />
|
||||
|
||||
<MenuLink
|
||||
target={'_blank'}
|
||||
text={'Hiring a DevRel'}
|
||||
isFancy
|
||||
link={'https://docs.google.com/forms/d/e/1FAIpQLSesFpPxgKx_8-L5hm7fw6NQpgGixrMGC4Cg3M8NHPQhFfSajQ/viewform'}
|
||||
/>
|
||||
|
||||
<Link ml='10px' bgGradient='linear(to-l, yellow.700, red.600)' p='7px 10px' rounded='4px'
|
||||
_hover={{ textDecoration: 'none', bgGradient: 'linear(to-l, red.800, yellow.700)' }}
|
||||
fontWeight={500} href={'/signup'}>Subscribe</Link>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileMenuLinks() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
rounded='5px'
|
||||
padding={0}
|
||||
aria-label={'Menu'}
|
||||
d={['block', 'none', 'none']}
|
||||
icon={<HamburgerIcon color='white' w='25px' height='25px' />}
|
||||
color='white'
|
||||
cursor='pointer'
|
||||
h='auto'
|
||||
bg='transparent'
|
||||
_hover={{ bg: 'transparent' }}
|
||||
_active={{ bg: 'transparent' }}
|
||||
_focus={{ bg: 'transparent' }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
<Stack color='gray.100'
|
||||
fontSize={['22px', '22px', '22px', '32px']}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
pos='fixed'
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
bg='gray.900'
|
||||
spacing='12px'
|
||||
zIndex={999}
|
||||
>
|
||||
<Link href='/roadmaps'>Roadmaps</Link>
|
||||
<Link href='/guides'>Guides</Link>
|
||||
<Link href='/watch'>Videos</Link>
|
||||
<Link href='/signup'>Subscribe</Link>
|
||||
<CloseButton onClick={() => setIsOpen(false)} pos='fixed' top='40px' right='15px' size='lg' />
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type GlobalHeaderProps = {
|
||||
variant?: 'transparent' | 'solid'
|
||||
};
|
||||
|
||||
export function GlobalHeader(props: GlobalHeaderProps) {
|
||||
const { variant = 'solid' } = props;
|
||||
|
||||
return (
|
||||
<Box bg={variant === 'solid' ? 'gray.900' : 'transparent'} p='20px 0'>
|
||||
<Container maxW='container.md'>
|
||||
<Flex justifyContent='space-between' alignItems='center'>
|
||||
<Box>
|
||||
<Link w='100%'
|
||||
d='flex'
|
||||
href='/'
|
||||
alignItems='center'
|
||||
color='white'
|
||||
fontWeight={600}
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
fontSize='18px'>
|
||||
<RoadmapLogo style={{ height: '30px', width: '30px', marginRight: '10px' }} />
|
||||
<Text d={['block', 'none', 'block']} as='span'>roadmap.sh</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
<DesktopMenuLinks />
|
||||
<MobileMenuLinks />
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Badge, Box, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { GuideType } from '../../lib/guide';
|
||||
|
||||
type GuideGridItemProps = {
|
||||
title: string;
|
||||
href: string;
|
||||
subtitle: string;
|
||||
date: string;
|
||||
isNew?: boolean;
|
||||
colorIndex?: number;
|
||||
type?: GuideType['type'];
|
||||
};
|
||||
|
||||
const bgColorList = [
|
||||
'gray.700',
|
||||
'purple.800'
|
||||
];
|
||||
|
||||
export function GuideGridItem(props: GuideGridItemProps) {
|
||||
const { title, subtitle, date, isNew = false, colorIndex = 0, href, type } = props;
|
||||
|
||||
return (
|
||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={href} shadow='xl' p='20px'
|
||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1}>
|
||||
<Text mb='10px' fontSize='13px' color='gray.400' textTransform='capitalize'>
|
||||
{isNew && <Badge colorScheme={'green'} mr='10px'>New</Badge>}
|
||||
{type} Guide
|
||||
</Text>
|
||||
<Heading color='white' mb={'6px'} fontSize='20px'>{title}</Heading>
|
||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import NextHead from 'next/head';
|
||||
import siteConfig from '../content/site.json';
|
||||
import { RoadmapType } from '../lib/roadmap';
|
||||
|
||||
type HelmetProps = {
|
||||
title?: string;
|
||||
keywords?: string[];
|
||||
canonical?: string;
|
||||
description?: string;
|
||||
noIndex?: boolean;
|
||||
roadmap?: RoadmapType;
|
||||
};
|
||||
|
||||
function getRichSnippetJson(roadmap: RoadmapType) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': `https://roadmap.sh/${roadmap.id}`,
|
||||
},
|
||||
headline: roadmap.seo.title,
|
||||
description: roadmap.seo.description,
|
||||
image: roadmap.jsonUrl
|
||||
? `https://roadmap.sh/roadmaps/${roadmap.id}.png`
|
||||
: undefined,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: 'Kamran Ahmed',
|
||||
url: 'https://twitter.com/kamranahmedse',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'roadmap.sh',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://roadmap.sh/brand-square.png',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Helmet = (props: HelmetProps) => {
|
||||
const { roadmap, title, canonical, description, keywords, noIndex = false } = props;
|
||||
|
||||
return (
|
||||
<NextHead>
|
||||
<meta charSet="UTF-8" />
|
||||
|
||||
<title>{title || siteConfig.title}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={description || siteConfig.description}
|
||||
/>
|
||||
|
||||
<meta name="author" content={siteConfig.author} />
|
||||
<meta
|
||||
name="keywords"
|
||||
content={keywords ? keywords.join(',') : siteConfig.keywords.join(',')}
|
||||
/>
|
||||
|
||||
{noIndex && <meta name="robots" content="noindex" /> }
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=yes, initial-scale=1.0, maximum-scale=3.0, minimum-scale=1.0"
|
||||
/>
|
||||
{canonical && <link rel="canonical" href={canonical} />}
|
||||
<meta httpEquiv="Content-Language" content="en" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content="@kamranahmedse" />
|
||||
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image" content="https://roadmap.sh/og-img.png" />
|
||||
<meta property="og:image:alt" content="roadmap.sh" />
|
||||
<meta property="og:site_name" content="roadmap.sh" />
|
||||
<meta property="og:title" content={title || siteConfig.title} />
|
||||
<meta property="og:description" content={description || siteConfig.description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://roadmap.sh" />
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/manifest/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#101010" />
|
||||
<meta name="theme-color" content="#848a9a" />
|
||||
|
||||
<link rel="manifest" href="/manifest/manifest.json" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/manifest/icon32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/manifest/icon16.png"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="/manifest/favicon.ico"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<link rel="icon" href="/manifest/favicon.ico" type="image/x-icon" />
|
||||
|
||||
{roadmap?.id && (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(getRichSnippetJson(roadmap)),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Global Site Tag (gtag.js) - Google Analytics */}
|
||||
{process.env.GA_SECRET && (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GA_SECRET}`}
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${process.env.GA_SECRET}');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</NextHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default Helmet;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { RoadmapType } from '../../lib/roadmap';
|
||||
import { SimpleGrid, Tag } from '@chakra-ui/react';
|
||||
import { HomeRoadmapItem } from '../roadmap/home-roadmap-item';
|
||||
|
||||
type FeaturedRoadmapsListProps = {
|
||||
roadmaps: RoadmapType[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const upcomingRoadmaps = [
|
||||
{
|
||||
type: 'Role Based',
|
||||
title: 'React Native',
|
||||
description: 'Step by step guide to become a React Native Developer',
|
||||
id: 'react-native'
|
||||
},
|
||||
{
|
||||
type: 'Role Based',
|
||||
title: 'Cyber Security',
|
||||
description: 'Step by step guide to become a Cyber Security Expert',
|
||||
id: 'cyber-security'
|
||||
},
|
||||
// {
|
||||
// type: 'Skill Based',
|
||||
// title: 'TypeScript',
|
||||
// description: 'Step by step guide to learn TypeScript in 2022',
|
||||
// id: 'typescript'
|
||||
// },
|
||||
// {
|
||||
// type: 'Skill Based',
|
||||
// title: 'Rust',
|
||||
// description: 'Step by step guide to learn Rust in 2022',
|
||||
// id: 'rust'
|
||||
// },
|
||||
];
|
||||
|
||||
export function FeaturedRoadmapsList(props: FeaturedRoadmapsListProps) {
|
||||
const { roadmaps, title } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tag bg='gray.400' mb={4}>{title}</Tag>
|
||||
<SimpleGrid columns={[1, 2, 3]} spacing={['10px', '10px', '15px']} mb='40px'>
|
||||
<>
|
||||
{roadmaps.map((roadmap: RoadmapType, counter: number) => (
|
||||
<HomeRoadmapItem
|
||||
isUpcoming={roadmap.isUpcoming}
|
||||
url={`/${roadmap.id}`}
|
||||
key={roadmap.id}
|
||||
colorIndex={counter}
|
||||
title={roadmap.featuredTitle === 'Software Design and Architecture' ? 'Software Design' : roadmap.featuredTitle}
|
||||
isCommunity={roadmap.isCommunity}
|
||||
isNew={roadmap.isNew}
|
||||
subtitle={roadmap.featuredDescription}
|
||||
/>
|
||||
))}
|
||||
{upcomingRoadmaps
|
||||
.filter(roadmap => roadmap.type === title)
|
||||
.map((roadmap, counter) => (
|
||||
<HomeRoadmapItem
|
||||
isUpcoming={true}
|
||||
url={`/upcoming?id=${roadmap.id}`}
|
||||
key={`upcoming-${roadmap.id}`}
|
||||
colorIndex={9}
|
||||
title={roadmap.title}
|
||||
subtitle={roadmap.description}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h137.25V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.27c-30.81 0-40.42 19.12-40.42 38.73V256h68.78l-11 71.69h-57.78V480H400a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48z"/></svg>
|
||||
|
Before Width: | Height: | Size: 507 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="29" height="29">
|
||||
<path d="M23.2 5H5.8a.8.8 0 0 0-.8.8V23.2c0 .44.35.8.8.8h9.3v-7.13h-2.38V13.9h2.38v-2.38c0-2.45 1.55-3.66 3.74-3.66 1.05 0 1.95.08 2.2.11v2.57h-1.5c-1.2 0-1.48.57-1.48 1.4v1.96h2.97l-.6 2.97h-2.37l.05 7.12h5.1a.8.8 0 0 0 .79-.8V5.8a.8.8 0 0 0-.8-.79"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 298 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 841 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM21.2 229.2H21c.1-.1.2-.3.3-.4 0 .1 0 .3-.1.4zm218 53.9V384h-31.4V281.3L128 128h37.3c52.5 98.3 49.2 101.2 59.3 125.6 12.3-27 5.8-24.4 60.6-125.6H320l-80.8 155.1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 515 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 474 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M283.2 345.5c2.7 2.7 2.7 6.8 0 9.2-24.5 24.5-93.8 24.6-118.4 0-2.7-2.4-2.7-6.5 0-9.2 2.4-2.4 6.5-2.4 8.9 0 18.7 19.2 81 19.6 100.5 0 2.4-2.3 6.6-2.3 9 0zm-91.3-53.8c0-14.9-11.9-26.8-26.5-26.8-14.9 0-26.8 11.9-26.8 26.8 0 14.6 11.9 26.5 26.8 26.5 14.6 0 26.5-11.9 26.5-26.5zm90.7-26.8c-14.6 0-26.5 11.9-26.5 26.8 0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-11.9 26.8-26.5 0-14.9-11.9-26.8-26.8-26.8zM448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-99.7 140.6c-10.1 0-19 4.2-25.6 10.7-24.1-16.7-56.5-27.4-92.5-28.6l18.7-84.2 59.5 13.4c0 14.6 11.9 26.5 26.5 26.5 14.9 0 26.8-12.2 26.8-26.8 0-14.6-11.9-26.8-26.8-26.8-10.4 0-19.3 6.2-23.8 14.9l-65.7-14.6c-3.3-.9-6.5 1.5-7.4 4.8l-20.5 92.8c-35.7 1.5-67.8 12.2-91.9 28.9-6.5-6.8-15.8-11-25.9-11-37.5 0-49.8 50.4-15.5 67.5-1.2 5.4-1.8 11-1.8 16.7 0 56.5 63.7 102.3 141.9 102.3 78.5 0 142.2-45.8 142.2-102.3 0-5.7-.6-11.6-2.1-17 33.6-17.2 21.2-67.2-16.1-67.2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="30" height="30" viewBox="0 0 283 283" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 39C0 17.4609 17.4609 0 39 0H244C265.539 0 283 17.4609 283 39V244C283 265.539 265.539 283 244 283H39C17.4609 283 0 265.539 0 244V39Z" fill="black"></path>
|
||||
<path d="M121.215 210.72C119.348 211.28 116.361 211.84 112.255 212.4C108.335 212.96 104.228 213.24 99.9347 213.24C95.828 213.24 92.0947 212.96 88.7347 212.4C85.5614 211.84 82.8547 210.72 80.6147 209.04C78.3747 207.36 76.6014 205.12 75.2947 202.32C74.1747 199.333 73.6147 195.507 73.6147 190.84V106.84C73.6147 102.547 74.3614 98.9067 75.8547 95.92C77.5347 92.7467 79.868 89.9467 82.8547 87.52C85.8414 85.0933 89.4814 82.9467 93.7747 81.08C98.2547 79.0267 103.015 77.2533 108.055 75.76C113.095 74.2667 118.321 73.1467 123.735 72.4C129.148 71.4667 134.561 71 139.975 71C148.935 71 156.028 72.7733 161.255 76.32C166.481 79.68 169.095 85.28 169.095 93.12C169.095 95.7333 168.721 98.3467 167.975 100.96C167.228 103.387 166.295 105.627 165.175 107.68C161.255 107.68 157.241 107.867 153.135 108.24C149.028 108.613 145.015 109.173 141.095 109.92C137.175 110.667 133.441 111.507 129.895 112.44C126.535 113.187 123.641 114.12 121.215 115.24V210.72ZM166.387 188.32C166.387 180.48 168.813 173.947 173.667 168.72C178.52 163.493 185.147 160.88 193.547 160.88C201.947 160.88 208.573 163.493 213.427 168.72C218.28 173.947 220.707 180.48 220.707 188.32C220.707 196.16 218.28 202.693 213.427 207.92C208.573 213.147 201.947 215.76 193.547 215.76C185.147 215.76 178.52 213.147 173.667 207.92C168.813 202.693 166.387 196.16 166.387 188.32Z" fill="white"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 18v-7h-9v-5h3v-6h-8v6h3v5h-9v7h-2v6h6v-6h-2v-5h7v5h-2v6h6v-6h-2v-5h7v5h-2v6h6v-6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 184 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-48.9 158.8c.2 2.8.2 5.7.2 8.5 0 86.7-66 186.6-186.6 186.6-37.2 0-71.7-10.8-100.7-29.4 5.3.6 10.4.8 15.8.8 30.7 0 58.9-10.4 81.4-28-28.8-.6-53-19.5-61.3-45.5 10.1 1.5 19.2 1.5 29.6-1.2-30-6.1-52.5-32.5-52.5-64.4v-.8c8.7 4.9 18.9 7.9 29.6 8.3a65.447 65.447 0 0 1-29.2-54.6c0-12.2 3.2-23.4 8.9-33.1 32.3 39.8 80.8 65.8 135.2 68.6-9.3-44.5 24-80.6 64-80.6 18.9 0 35.9 7.9 47.9 20.7 14.8-2.8 29-8.3 41.6-15.8-4.9 15.2-15.2 28-28.8 36.1 13.2-1.4 26-5.1 37.8-10.2-8.9 13.1-20.1 24.7-32.9 34z"/></svg>
|
||||
|
Before Width: | Height: | Size: 840 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="29" height="29" fill="currentColor">
|
||||
<path d="M22.05 7.54a4.47 4.47 0 0 0-3.3-1.46 4.53 4.53 0 0 0-4.53 4.53c0 .35.04.7.08 1.05A12.9 12.9 0 0 1 5 6.89a5.1 5.1 0 0 0-.65 2.26c.03 1.6.83 2.99 2.02 3.79a4.3 4.3 0 0 1-2.02-.57v.08a4.55 4.55 0 0 0 3.63 4.44c-.4.08-.8.13-1.21.16l-.81-.08a4.54 4.54 0 0 0 4.2 3.15 9.56 9.56 0 0 1-5.66 1.94l-1.05-.08c2 1.27 4.38 2.02 6.94 2.02 8.3 0 12.86-6.9 12.84-12.85.02-.24 0-.43 0-.65a8.68 8.68 0 0 0 2.26-2.34c-.82.38-1.7.62-2.6.72a4.37 4.37 0 0 0 1.95-2.51c-.84.53-1.81.9-2.83 1.13z"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 550 B |
@@ -1,21 +0,0 @@
|
||||
export function VideoIcon(props: any) {
|
||||
return (
|
||||
<svg
|
||||
stroke='currentColor'
|
||||
fill='currentColor'
|
||||
strokeWidth='0'
|
||||
viewBox='0 0 24 24'
|
||||
height='1em'
|
||||
width='1em'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path fill='none' d='M0 0h24v24H0z' />
|
||||
<path
|
||||
d='M3 3.993C3 3.445 3.445 3 3.993 3h16.014c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.994.994 0 0 1 3 20.007V3.993zm7.622 4.422a.4.4 0 0 0-.622.332v6.506a.4.4 0 0 0 .622.332l4.879-3.252a.4.4 0 0 0 0-.666l-4.88-3.252z'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill='currentColor'>
|
||||
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 369 B |
@@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Badge, Flex, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
type LinksListItemProps = {
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badgeText?: string;
|
||||
target?: string;
|
||||
icon?: React.ReactChild;
|
||||
hideSubtitleOnMobile?: boolean;
|
||||
};
|
||||
|
||||
export function LinksListItem(props: LinksListItemProps) {
|
||||
const { title, subtitle, badgeText, icon, hideSubtitleOnMobile = false, href, target } = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
target={target || '_self'}
|
||||
href={href}
|
||||
fontSize={['14px', '14px', '15px']}
|
||||
py='9px'
|
||||
d='flex'
|
||||
flexDirection={['column', 'row', 'row']}
|
||||
fontWeight={500}
|
||||
color='gray.600'
|
||||
alignItems={['flex-start', 'center']}
|
||||
justifyContent={'space-between'}
|
||||
sx={{
|
||||
'@media (hover: none)': {
|
||||
'&:hover': {
|
||||
'& .list-item-title': {
|
||||
transform: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
color: 'blue.400',
|
||||
'& .list-item-title': {
|
||||
transform: 'translateX(10px)'
|
||||
}
|
||||
}}
|
||||
isTruncated
|
||||
maxWidth='100%'
|
||||
>
|
||||
<Flex alignItems='center' className='list-item-title' transition={'200ms'}>
|
||||
{icon}
|
||||
<Text maxWidth={'345px'} isTruncated as='span'>{title}</Text>
|
||||
{badgeText &&
|
||||
<Badge pos='relative' top='1px' variant='subtle' colorScheme='green' ml='10px'>{badgeText}</Badge>}
|
||||
</Flex>
|
||||
<Text d={[hideSubtitleOnMobile ? 'none' : 'inline', 'inline']} mt={['3px', 0]} as='span'
|
||||
fontSize={['11px', '11px', '12px']} color='gray.500'>{subtitle}</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StackDivider, VStack } from '@chakra-ui/react';
|
||||
|
||||
type LinksListProps = {
|
||||
children: React.ReactNode
|
||||
};
|
||||
|
||||
export function LinksList(props: LinksListProps) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<VStack
|
||||
rounded='5px'
|
||||
divider={<StackDivider borderColor='gray.200' />}
|
||||
spacing={0}
|
||||
align='stretch'
|
||||
>
|
||||
{children}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import MdxComponents from './mdx-components';
|
||||
import { roadmapTheme } from '../../styles/theme';
|
||||
|
||||
type MdRendererType = {
|
||||
children: React.ReactNode
|
||||
};
|
||||
|
||||
export default function MdRenderer(props: MdRendererType) {
|
||||
return (
|
||||
<ChakraProvider theme={roadmapTheme} resetCSS>
|
||||
<MDXProvider components={MdxComponents}>
|
||||
{props.children}
|
||||
</MDXProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type EnrichedLinkProps = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Link = styled.a`
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
`;
|
||||
|
||||
const EnrichedLink = (props: EnrichedLinkProps) => {
|
||||
// Is external URL or is a media URL
|
||||
const isExternalUrl = /(^http(s)?:\/\/)|(\.(png|svg|jpeg|jpg)$)/.test(
|
||||
props.href
|
||||
);
|
||||
|
||||
const linkProps: Record<string, string> = {
|
||||
target: '_self',
|
||||
...(isExternalUrl
|
||||
? {
|
||||
rel: 'nofollow',
|
||||
target: '_blank',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={props.href} {...linkProps}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnrichedLink;
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link, Text, Badge } from '@chakra-ui/react';
|
||||
|
||||
type BadgeLinkType = {
|
||||
target: string;
|
||||
badgeText: string;
|
||||
href: string;
|
||||
colorScheme?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function BadgeLink(props: BadgeLinkType) {
|
||||
const {
|
||||
target = '_blank',
|
||||
colorScheme = 'purple',
|
||||
badgeText,
|
||||
href,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
// Is external URL or is a media URL
|
||||
const isExternalUrl = /(^http(s)?:\/\/)|(\.(png|svg|jpeg|jpg)$)/.test(
|
||||
props.href
|
||||
);
|
||||
|
||||
const linkProps: Record<string, string> = {
|
||||
...(isExternalUrl
|
||||
? {
|
||||
rel: 'nofollow',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Text mb={'0px'}>
|
||||
<Link
|
||||
fontSize="14px"
|
||||
color="blue.700"
|
||||
fontWeight={500}
|
||||
textDecoration="none"
|
||||
href={href}
|
||||
target={target}
|
||||
_hover={{ textDecoration: 'none', color: 'purple.400' }}
|
||||
{...linkProps}
|
||||
>
|
||||
<Badge fontSize="11px" mr="7px" colorScheme={colorScheme}>
|
||||
{badgeText}
|
||||
</Badge>
|
||||
{children}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const BlockQuote = styled.blockquote`
|
||||
padding: 16px 20px;
|
||||
position: relative;
|
||||
background: #e8e8e8;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p + h4 {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
& + p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default BlockQuote;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Code as ChakraCode } from '@chakra-ui/react';
|
||||
|
||||
type CodeType = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Code(props: CodeType) {
|
||||
return <ChakraCode bg='blue.500'>{props.children}</ChakraCode>;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Box, Flex, Heading, Text } from '@chakra-ui/react';
|
||||
import TreeIcon from '../../icons/tree.svg';
|
||||
|
||||
type DedicatedRoadmapProps = {
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export function DedicatedRoadmap(props: DedicatedRoadmapProps) {
|
||||
const { href, title, description } = props;
|
||||
|
||||
return (
|
||||
<Flex as={'a'} target='_blank' href={ href } p={5} px={5} mt={6} rounded='md' alignItems='center' _hover={{ bg: 'yellow.400'}} bg='yellow.300'>
|
||||
<Box d={['none', 'none', 'none', 'block', 'block']} mr={4} height='32px' w='32px' as={TreeIcon} color='gray.900' />
|
||||
<Box as='span'>
|
||||
<Heading fontSize='lg' as={'h4'} mb='2px' color='gray.900'>{ title }</Heading>
|
||||
<Text color='gray.700' as='span' fontSize='md'>{ description }</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import LinkIcon from 'components/icons/link.svg';
|
||||
|
||||
const linkify = (Component: React.FunctionComponent<any>) => {
|
||||
return function EnrichedHeading(props: { children: string }): React.ReactNode {
|
||||
const text = props.children;
|
||||
const id = text?.toLowerCase && text
|
||||
.toLowerCase()
|
||||
.replace(/[^\x00-\x7F]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[?!]/g, '');
|
||||
|
||||
return (
|
||||
<Component id={id}>
|
||||
<HeaderLink href={`#${id}`}>
|
||||
<LinkIcon />
|
||||
</HeaderLink>
|
||||
{props.children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const HeaderLink = styled.a`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -25px;
|
||||
width: 25px;
|
||||
display: none;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const H1 = styled.h1`
|
||||
position: relative;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0 10px !important;
|
||||
|
||||
&:hover ${HeaderLink} {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
const H2 = styled(H1).attrs({ as: 'h2' })`
|
||||
font-size: 30px;
|
||||
`;
|
||||
|
||||
const H3 = styled(H1).attrs({ as: 'h3' })`
|
||||
margin: 22px 0 8px;
|
||||
font-size: 28px;
|
||||
`;
|
||||
|
||||
const H4 = styled(H1).attrs({ as: 'h4' })`
|
||||
margin: 18px 0 8px;
|
||||
font-size: 24px;
|
||||
`;
|
||||
|
||||
const H5 = styled(H1).attrs({ as: 'h5' })`
|
||||
margin: 14px 0 8px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const H6 = styled(H1).attrs({ as: 'h6' })`
|
||||
margin: 12px 0 8px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const Headings = {
|
||||
h1: H1,
|
||||
h2: H2,
|
||||
h3: H3,
|
||||
h4: H4,
|
||||
h5: H5,
|
||||
h6: H6
|
||||
};
|
||||
|
||||
export default Headings;
|
||||
@@ -1,47 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type IFrameProps = {
|
||||
title: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
const AspectRatioBox = styled.div`
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:before {
|
||||
height: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
padding-bottom: 50%;
|
||||
}
|
||||
|
||||
& > iframe {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function IFrame(props: IFrameProps) {
|
||||
return (
|
||||
<AspectRatioBox>
|
||||
<iframe
|
||||
frameBorder={0}
|
||||
title={props.title}
|
||||
src={props.src}
|
||||
allow={'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'}
|
||||
allowFullScreen
|
||||
/>
|
||||
</AspectRatioBox>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Img = styled.img`
|
||||
max-width: 100%;
|
||||
margin: 25px auto;
|
||||
display: block;
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Code } from '@chakra-ui/react';
|
||||
import { P } from './p';
|
||||
import Headings from './heading';
|
||||
import { Pre } from './pre';
|
||||
import BlockQuote from './blockquote';
|
||||
import { Table } from './table';
|
||||
import IFrame from './iframe';
|
||||
import { Img } from './img';
|
||||
import EnrichedLink from './a';
|
||||
import { BadgeLink } from './badge-link';
|
||||
import { Li, Ul } from './ul';
|
||||
import PremiumBlock from './premium-block';
|
||||
import { ResourceGroupTitle } from './resource-group-title';
|
||||
import { DedicatedRoadmap } from './dedicated-roadmap';
|
||||
|
||||
const MdxComponents = {
|
||||
p: P,
|
||||
...Headings,
|
||||
pre: Pre,
|
||||
blockquote: BlockQuote,
|
||||
a: EnrichedLink,
|
||||
DedicatedRoadmap,
|
||||
table: Table,
|
||||
iframe: IFrame,
|
||||
img: Img,
|
||||
code: Code,
|
||||
BadgeLink: BadgeLink,
|
||||
ResourceGroupTitle: ResourceGroupTitle,
|
||||
PremiumBlock: PremiumBlock,
|
||||
ul: Ul,
|
||||
li: Li
|
||||
};
|
||||
|
||||
export default MdxComponents;
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type EnrichedTextType = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const P = styled.p`
|
||||
line-height: 27px;
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
margin-bottom: 18px;
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Pre = styled.pre`
|
||||
margin: 25px -25px 25px -25px !important;
|
||||
padding: 20px 25px !important;
|
||||
border-radius: 10px;
|
||||
line-height: 1.5 !important;
|
||||
|
||||
code {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Heading, Text } from '@chakra-ui/react';
|
||||
import { LockIcon } from '@chakra-ui/icons';
|
||||
|
||||
type PremiumBlockProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function PremiumBlock(props: PremiumBlockProps) {
|
||||
return (
|
||||
<Box p='40px' textAlign='center' rounded='5px' mb='18px' bg='gray.50' borderWidth={1}>
|
||||
<LockIcon color='gray.300' height='45px' w='45px' mb='18px' />
|
||||
<Heading as='h3' fontSize='30px' mb='10px'>{props.title}</Heading>
|
||||
<Text mb='18px'>{props.description}</Text>
|
||||
<Button colorScheme='green'>Become a Member</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
|
||||
type ResourceGroupTitleProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ResourceGroupTitle(props: ResourceGroupTitleProps) {
|
||||
const { children } = props;
|
||||
|
||||
return <Heading mt='20px' color='gray.800' fontSize='14px' pb='5px' borderBottomWidth={1} textTransform='uppercase' as="h2" mb={'10px'}>{children}</Heading>;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Table = styled.table`
|
||||
border-collapse: separate;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
margin: 20px 0;
|
||||
|
||||
th {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
background: #FAFAFA;
|
||||
text-transform: uppercase;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #EAEAEA;
|
||||
}
|
||||
`;
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { UnorderedList } from '@chakra-ui/react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Ul = styled.ul`
|
||||
margin-left: 40px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
ul {
|
||||
margin-top: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Li = styled.li`
|
||||
margin-bottom: 7px;
|
||||
`;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Box, Container, Heading, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
export function OpensourceBanner() {
|
||||
return (
|
||||
<Box bg='white' borderTopWidth={1} py={['45px', '45px', '70px']} textAlign='center'>
|
||||
<Container maxW='container.sm'>
|
||||
<Heading fontSize={['25px', '25px', '35px']} mb={['10px', '10px', '20px']}>Open Source</Heading>
|
||||
<Text lineHeight='26px' fontSize={['15px', '15px', '16px']} mb='20px'>The project is OpenSource,
|
||||
<Link
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories'
|
||||
target='_blank'
|
||||
borderBottomWidth={1}
|
||||
fontWeight={600}
|
||||
>6th most starred project on GitHub</Link> and is visited by hundreds of thousands of
|
||||
developers every month.</Text>
|
||||
<iframe
|
||||
src='https://ghbtns.com/github-btn.html?user=kamranahmedse&repo=developer-roadmap&type=star&count=true&size=large'
|
||||
frameBorder='0'
|
||||
scrolling='0'
|
||||
width='170'
|
||||
height='30'
|
||||
style={{ margin: 'auto' }}
|
||||
title='GitHub'
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Box, Container, Heading, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type PageHeaderProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children?: React.ReactNode;
|
||||
beforeTitle?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function PageHeader(props: PageHeaderProps) {
|
||||
const { title, subtitle, children, beforeTitle = null } = props;
|
||||
|
||||
return (
|
||||
<Box pt={['25px', '20px', '45px']} pb={['20px', '15px', '30px']} borderBottomWidth={1} mb='30px'>
|
||||
<Container maxW='container.md' position='relative'>
|
||||
{beforeTitle}
|
||||
<Heading
|
||||
as='h1'
|
||||
color='black'
|
||||
fontSize={['28px', '33px', '40px']}
|
||||
fontWeight={700}
|
||||
mb={['2px', '2px', '5px']}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
<Text fontSize={['13px', '14px', '15px']}>{subtitle}</Text>
|
||||
</Container>
|
||||
|
||||
{children && (
|
||||
<Container maxW='container.md'>
|
||||
{children}
|
||||
</Container>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
type PageWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageWrapper(props: PageWrapperProps) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<Box bgColor='brand.bg' bgImage='url(/bg.jpg)' bgRepeat='no-repeat' bgSize='100%' w='100%' minH='100vh'>
|
||||
{ children }
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Badge, Box, Button, Container, Link, Stack, Text } from '@chakra-ui/react';
|
||||
import { RoadmapType } from '../lib/roadmap';
|
||||
|
||||
type RelatedRoadmapsProps = {
|
||||
roadmaps: RoadmapType[];
|
||||
};
|
||||
|
||||
const colorsList = [
|
||||
'gray.700',
|
||||
'purple.700',
|
||||
'blue.700',
|
||||
'red.700',
|
||||
'green.700',
|
||||
'teal.700',
|
||||
'yellow.700',
|
||||
'cyan.700',
|
||||
'pink.700'
|
||||
];
|
||||
|
||||
const roadmapTitleMapping: Record<string, string> = {
|
||||
"Software Design and Architecture": "Software Design",
|
||||
}
|
||||
|
||||
export function RelatedRoadmaps(props: RelatedRoadmapsProps) {
|
||||
const { roadmaps } = props;
|
||||
if (!roadmaps.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box borderTopWidth={1} bgColor='gray.50' pb='35px' pt='5px'>
|
||||
<Container maxW='container.md'>
|
||||
<Box display='flex' position='relative' top='-23px' alignItems='center' justifyContent='space-between'>
|
||||
<Text textAlign='center' borderWidth={1} bg='white' p='4px' fontWeight='bold' rounded='md' px={'15px'}>
|
||||
Related Roadmaps
|
||||
</Text>
|
||||
|
||||
<Button as={Link} variant='outline' bg='white' size='sm' _hover={{ textDecoration: 'none', bg: 'gray.100' }}
|
||||
href='/'>
|
||||
<Text as='span' display={['inline', 'none', 'none']}>More →</Text>
|
||||
<Text as='span' display={['none', 'inline', 'inline']}>All Roadmaps →</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Stack spacing='5px'>
|
||||
{roadmaps.map((roadmap, counter) => (
|
||||
<Link
|
||||
href={`/${roadmap.id}`}
|
||||
key={roadmap.id}
|
||||
borderWidth={1}
|
||||
borderColor='blue.100'
|
||||
py='7px'
|
||||
px='14px'
|
||||
rounded='md'
|
||||
bg='white'
|
||||
textDecoration={'none'}
|
||||
_hover={{ bg: 'gray.100', borderColor: 'blue.200' }}
|
||||
bgGradient='linear(to-r, white, gray.50)'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
flexDir={['column', 'row', 'row']}
|
||||
>
|
||||
<Text
|
||||
color={colorsList[counter]}
|
||||
as='span'
|
||||
fontWeight='bold'
|
||||
display={['inline-block']}
|
||||
minWidth='150px'
|
||||
mr='10px'
|
||||
>
|
||||
{roadmapTitleMapping[roadmap.featuredTitle] || roadmap.featuredTitle}
|
||||
</Text>
|
||||
<Text as='span' display={['block', 'inline']} isTruncated maxWidth='100%' fontSize={['sm', 'sm', 'md']} color='gray.700'>{roadmap.featuredDescription}</Text>
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { RemoveScroll } from 'react-remove-scroll';
|
||||
import { RoadmapType } from '../../lib/roadmap';
|
||||
import RoadmapGroup from '../../pages/[roadmap]/[group]';
|
||||
import { CheckIcon, CloseIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||
import { queryGroupElementsById } from '../../lib/renderer';
|
||||
|
||||
type ContentDrawerProps = {
|
||||
roadmap: RoadmapType;
|
||||
groupId: string;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function markTopicDone(groupId: string) {
|
||||
localStorage.setItem(groupId, 'done');
|
||||
|
||||
queryGroupElementsById(groupId).forEach((item) =>
|
||||
item?.classList?.add('done')
|
||||
);
|
||||
}
|
||||
|
||||
export function markTopicPending(groupId: string) {
|
||||
localStorage.removeItem(groupId);
|
||||
|
||||
queryGroupElementsById(groupId).forEach((item) =>
|
||||
item?.classList?.remove('done')
|
||||
);
|
||||
}
|
||||
|
||||
export function isTopicDone(groupId: string) {
|
||||
return localStorage.getItem(groupId) === 'done';
|
||||
}
|
||||
|
||||
export function ContentDrawer(props: ContentDrawerProps) {
|
||||
const { roadmap, groupId, onClose = () => null } = props;
|
||||
if (!groupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDone = isTopicDone(groupId);
|
||||
|
||||
return (
|
||||
<Box zIndex={99999} pos="relative">
|
||||
<Box
|
||||
onClick={onClose}
|
||||
pos="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="black"
|
||||
opacity={0.4}
|
||||
/>
|
||||
<RemoveScroll allowPinchZoom>
|
||||
<Box
|
||||
p="0px 30px 30px"
|
||||
position="fixed"
|
||||
w={['100%', '60%', '40%']}
|
||||
bg="white"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
borderLeftWidth={'1px'}
|
||||
overflowY="scroll"
|
||||
>
|
||||
<Flex
|
||||
mt="20px"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
zIndex={1}
|
||||
>
|
||||
{!isDone && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
markTopicDone(groupId);
|
||||
onClose();
|
||||
}}
|
||||
colorScheme="green"
|
||||
leftIcon={<CheckIcon />}
|
||||
size="xs"
|
||||
iconSpacing={0}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
d={['block', 'none', 'none', 'block']}
|
||||
ml="10px"
|
||||
>
|
||||
Mark as Done
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{isDone && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
markTopicPending(groupId);
|
||||
onClose();
|
||||
}}
|
||||
colorScheme="red"
|
||||
leftIcon={<RepeatIcon />}
|
||||
size="xs"
|
||||
iconSpacing={0}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
d={['block', 'none', 'none', 'block']}
|
||||
ml="10px"
|
||||
>
|
||||
Mark as Pending
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={onClose}
|
||||
colorScheme="yellow"
|
||||
ml="5px"
|
||||
leftIcon={<CloseIcon width="8px" />}
|
||||
iconSpacing={0}
|
||||
size="xs"
|
||||
>
|
||||
<Text as="span" d={['none', 'none', 'none', 'block']} ml="10px">
|
||||
Close
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<RoadmapGroup isOutlet roadmap={roadmap} group={groupId} />
|
||||
</Box>
|
||||
</RemoveScroll>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Divider, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
type EditContentPageLinkProps = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function EditContentPageLink(props: EditContentPageLinkProps) {
|
||||
const { href } = props;
|
||||
|
||||
return (
|
||||
<Box my='30px'>
|
||||
<Divider mb="15px" orientation="horizontal" />
|
||||
<Text
|
||||
lineHeight="23px"
|
||||
fontWeight={500}
|
||||
fontSize="14px"
|
||||
color="gray.500"
|
||||
mb="10px"
|
||||
>
|
||||
This page is a work in progress. Help us by writing a small
|
||||
introduction to the topic and suggesting a few links to read more
|
||||
about this topic.
|
||||
</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
py="20px"
|
||||
as={Link}
|
||||
href={href}
|
||||
target="_blank"
|
||||
isFullWidth
|
||||
colorScheme={'gray'}
|
||||
_hover={{ textDecoration: 'none', bg: 'gray.200' }}
|
||||
>
|
||||
Edit this Page
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Badge, Box, Flex, Heading, Link, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
|
||||
type RoadmapGridItemProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
isCommunity?: boolean;
|
||||
isUpcoming?: boolean;
|
||||
colorIndex?: number;
|
||||
isNew?: boolean;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const bgColorList = [
|
||||
'red.100',
|
||||
'yellow.100',
|
||||
'green.200',
|
||||
'teal.200',
|
||||
'blue.200',
|
||||
'red.200',
|
||||
'gray.200',
|
||||
'teal.200',
|
||||
'yellow.100',
|
||||
'green.200',
|
||||
'red.200'
|
||||
];
|
||||
|
||||
export function HomeRoadmapItem(props: RoadmapGridItemProps) {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
isCommunity,
|
||||
isNew,
|
||||
colorIndex = 0,
|
||||
url,
|
||||
isUpcoming
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
position='relative'
|
||||
as={Link}
|
||||
href={url}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: 'rgba(255,255,255,.10)'
|
||||
}}
|
||||
sx={{
|
||||
// On mobile devices, don't change the scale
|
||||
'@media (hover: none)': {
|
||||
'&:hover': {
|
||||
bg: 'rgba(255,255,255,.05)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
flex={1}
|
||||
shadow='2xl'
|
||||
className={'home-roadmap-item'}
|
||||
bg={'rgba(255,255,255,.05)'}
|
||||
color='white'
|
||||
p='15px'
|
||||
rounded='10px'
|
||||
pos='relative'
|
||||
>
|
||||
{isCommunity && (
|
||||
<Tooltip label={'Community contribution'} hasArrow placement='top'>
|
||||
<InfoIcon opacity={0.5} position='absolute' top='10px' right='10px' />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
fontSize={['17px', '17px', '22px']}
|
||||
color={bgColorList[colorIndex]}
|
||||
mb='5px'
|
||||
d='flex'
|
||||
alignItems='center'
|
||||
>
|
||||
{title}
|
||||
|
||||
{ isNew && <Badge position='absolute' bottom={0} right={0} colorScheme='yellow' ml='10px'>New</Badge> }
|
||||
</Heading>
|
||||
<Text color='gray.200' fontSize={['13px']}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
|
||||
{isUpcoming && (
|
||||
<Flex
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
pos='absolute'
|
||||
left={0}
|
||||
right={0}
|
||||
top={0}
|
||||
bottom={0}
|
||||
rounded='10px'
|
||||
>
|
||||
<Text
|
||||
color='white'
|
||||
bg='gray.600'
|
||||
zIndex={1}
|
||||
fontWeight={600}
|
||||
p={'5px 10px'}
|
||||
rounded='10px'
|
||||
>
|
||||
Upcoming
|
||||
</Text>
|
||||
<Box
|
||||
bg={'black'}
|
||||
pos='absolute'
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
rounded={'10px'}
|
||||
opacity={0.5}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Badge, Link, Text } from '@chakra-ui/react';
|
||||
import siteConfig from '../../content/site.json';
|
||||
import { event } from '../../lib/gtag';
|
||||
import React from 'react';
|
||||
|
||||
export function NewAlertBanner() {
|
||||
return (
|
||||
<Text
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
color: 'blue.700',
|
||||
'& .new-badge': { bg: 'blue.700' },
|
||||
}}
|
||||
as={Link}
|
||||
href={siteConfig.url.youtube}
|
||||
d="block"
|
||||
target="_blank"
|
||||
color="red.700"
|
||||
fontSize="sm"
|
||||
mb="10px"
|
||||
fontWeight={500}
|
||||
onClick={() =>
|
||||
event({
|
||||
category: 'Subscription',
|
||||
action: 'Clicked the YouTube banner',
|
||||
label: 'YouTube Alert on Roadmap',
|
||||
})
|
||||
}
|
||||
>
|
||||
<Badge
|
||||
transition={'all 300ms'}
|
||||
className="new-badge"
|
||||
mr="7px"
|
||||
colorScheme="red"
|
||||
variant="solid"
|
||||
>
|
||||
New
|
||||
</Badge>
|
||||
<Text textDecoration="underline" as="span" d={['none', 'inline']}>
|
||||
Roadmap topics to be covered on our YouTube Channel
|
||||
</Text>
|
||||
<Text textDecoration="underline" as="span" d={['inline', 'none']}>
|
||||
Topic videos being made on YouTube
|
||||
</Text>
|
||||
<Text as="span" ml="5px">
|
||||
»
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { RoadmapType } from '../../lib/roadmap';
|
||||
import { Container, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import siteConfig from '../../content/site.json';
|
||||
|
||||
type RoadmapProps = {
|
||||
roadmap: RoadmapType;
|
||||
};
|
||||
|
||||
export function RoadmapError(props: RoadmapProps) {
|
||||
const { roadmap } = props;
|
||||
|
||||
return (
|
||||
<Container
|
||||
bg={'red.600'}
|
||||
maxW={'container.md'}
|
||||
position="relative"
|
||||
mt="50px"
|
||||
p='20px'
|
||||
rounded='5px'
|
||||
color='white'
|
||||
>
|
||||
<Heading mb='4px' size='md'>Oops! There's an error</Heading>
|
||||
<Text>Try refreshing or <Link target='_blank' fontWeight={700} textDecoration={'underline'} fontSize='14px' href={siteConfig.url.issue}>report a bug</Link> and use the <Link fontWeight={700} textDecoration={'underline'} href={`/roadmaps/${roadmap.id}.png`}>non-interactive version</Link></Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Badge, Box, Flex, Heading, Link, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
|
||||
type RoadmapGridItemProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
href: string;
|
||||
isCommunity?: boolean;
|
||||
isUpcoming?: boolean;
|
||||
colorIndex?: number;
|
||||
};
|
||||
|
||||
const bgColorList = [
|
||||
'gray.900',
|
||||
'purple.900',
|
||||
'blue.900',
|
||||
'red.900',
|
||||
'green.900',
|
||||
'teal.900',
|
||||
'yellow.900',
|
||||
'cyan.900',
|
||||
'pink.900',
|
||||
|
||||
'gray.800',
|
||||
'purple.800',
|
||||
'blue.800',
|
||||
'red.800',
|
||||
'green.800',
|
||||
'teal.800',
|
||||
'yellow.800',
|
||||
'cyan.800',
|
||||
'pink.800',
|
||||
|
||||
'gray.700',
|
||||
'purple.700',
|
||||
'blue.700',
|
||||
'red.700',
|
||||
'green.700',
|
||||
'teal.700',
|
||||
'yellow.700',
|
||||
'cyan.700',
|
||||
'pink.700',
|
||||
|
||||
'gray.600',
|
||||
'purple.600',
|
||||
'blue.600',
|
||||
'red.600',
|
||||
'green.600',
|
||||
'teal.600',
|
||||
'yellow.600',
|
||||
'cyan.600',
|
||||
'pink.600'
|
||||
];
|
||||
|
||||
export function RoadmapGridItem(props: RoadmapGridItemProps) {
|
||||
const { title, subtitle, isCommunity = false, isUpcoming = false, colorIndex = 0, href = '/' } = props;
|
||||
|
||||
return (
|
||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={href} shadow='xl' p='20px'
|
||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1} pos='relative'>
|
||||
|
||||
{isCommunity && (
|
||||
<Tooltip label={'Community contribution'} hasArrow placement='top'>
|
||||
<InfoIcon opacity={0.5} color='gray.100' position='absolute' top='10px' right='10px' />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Heading color='white' mb={'6px'} fontSize='20px'>{title}</Heading>
|
||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text>
|
||||
|
||||
{isUpcoming && (
|
||||
<Flex
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
pos='absolute'
|
||||
left={0}
|
||||
right={0}
|
||||
top={0}
|
||||
bottom={0}
|
||||
rounded='10px'
|
||||
>
|
||||
<Text color='white' bg='yellow.900' zIndex={1} fontWeight={600} p={'5px 10px'}
|
||||
rounded='10px'>Upcoming</Text>
|
||||
<Box bg={'black'} pos='absolute' top={0} left={0} right={0} bottom={0} rounded={'10px'} opacity={0.5} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Container, Spinner } from '@chakra-ui/react';
|
||||
|
||||
export function RoadmapLoader() {
|
||||
return (
|
||||
<Container
|
||||
maxW={'container.md'}
|
||||
position="relative"
|
||||
mt="60px"
|
||||
textAlign="center"
|
||||
>
|
||||
<Spinner
|
||||
thickness="7px"
|
||||
speed="0.65s"
|
||||
emptyColor="gray.200"
|
||||
color="gray.500"
|
||||
size="xl"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import siteConfig from '../../content/site.json';
|
||||
import { isInteractiveRoadmap, RoadmapType } from '../../lib/roadmap';
|
||||
import { NewAlertBanner } from './new-alert-banner';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Heading,
|
||||
Input,
|
||||
Link,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
Stack,
|
||||
Text,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { AtSignIcon, ChatIcon, DownloadIcon } from '@chakra-ui/icons';
|
||||
import React from 'react';
|
||||
import { SIGNUP_EMAIL_INPUT_NAME, SIGNUP_FORM_ACTION } from '../../pages/signup';
|
||||
import { event } from '../../lib/gtag';
|
||||
import { TNSAlert } from './tns-alert';
|
||||
|
||||
type RoadmapPageHeaderType = {
|
||||
roadmap: RoadmapType;
|
||||
};
|
||||
|
||||
function RoadmapDownloader({ roadmapTitle }: { roadmapTitle: string }) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const initialRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
event({
|
||||
category: 'Subscription',
|
||||
action: `Clicked Download ${roadmapTitle} Roadmap`,
|
||||
label: `Download ${roadmapTitle} Roadmap Button`
|
||||
});
|
||||
onOpen();
|
||||
}}
|
||||
size='xs'
|
||||
py='14px'
|
||||
px='10px'
|
||||
leftIcon={<DownloadIcon />}
|
||||
display={['none', 'flex']}
|
||||
colorScheme='yellow'
|
||||
variant='solid'
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
|
||||
<Modal initialFocusRef={initialRef} closeOnOverlayClick={true} isOpen={isOpen} onClose={onClose} isCentered motionPreset='none'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p={6}>
|
||||
<Heading mb='5px' fontSize='2xl'>Download Roadmap</Heading>
|
||||
<Text fontSize={'md'} color='gray.700'>Enter your email below to receive the download link.</Text>
|
||||
<form action={SIGNUP_FORM_ACTION} method='post' target='_blank' onSubmit={() => {
|
||||
event({
|
||||
category: 'Subscription',
|
||||
action: `Submitted Download ${roadmapTitle} Roadmap Email`,
|
||||
label: `PDF / Subscribe ${roadmapTitle} Roadmap`
|
||||
});
|
||||
|
||||
onClose();
|
||||
}}>
|
||||
<Input required ref={initialRef} size='md' my='10px' type='email' placeholder='Email address' name={SIGNUP_EMAIL_INPUT_NAME} />
|
||||
<Button type='submit' colorScheme='green' size='md' width={'full'}>Send Link</Button>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RoadmapSubscriber({ roadmapTitle }: { roadmapTitle: string }) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const initialRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
event({
|
||||
category: 'Subscription',
|
||||
action: `Clicked Subscribe ${roadmapTitle} Roadmap`,
|
||||
label: `Subscribe ${roadmapTitle} Roadmap Button`
|
||||
});
|
||||
onOpen();
|
||||
}}
|
||||
size='xs'
|
||||
py='14px'
|
||||
px='10px'
|
||||
leftIcon={<AtSignIcon />}
|
||||
display={'flex'}
|
||||
colorScheme='yellow'
|
||||
variant='solid'
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
<Modal initialFocusRef={initialRef} closeOnOverlayClick={true} isOpen={isOpen} onClose={onClose} isCentered motionPreset='none'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p={6}>
|
||||
<Heading mb='5px' fontSize='2xl'>Subscribe</Heading>
|
||||
<Text fontSize={'md'} color='gray.700'>Enter your email below to receive updates to this roadmap.</Text>
|
||||
<form action={SIGNUP_FORM_ACTION} method='post' target='_blank' onSubmit={() => {
|
||||
event({
|
||||
category: 'Subscription',
|
||||
action: `Submitted Subscribe ${roadmapTitle} Roadmap Email`,
|
||||
label: `Email / Subscribe ${roadmapTitle} Roadmap`
|
||||
});
|
||||
|
||||
onClose();
|
||||
}}>
|
||||
<Input required ref={initialRef} size='md' my='10px' type='email' placeholder='Email address' name={SIGNUP_EMAIL_INPUT_NAME} />
|
||||
<Button type='submit' colorScheme='green' size='md' width={'full'}>Subscribe</Button>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoadmapPageHeader(props: RoadmapPageHeaderType) {
|
||||
const { roadmap } = props;
|
||||
|
||||
const hasTNSAlert = ['frontend', 'backend', 'devops'].includes(roadmap.id);
|
||||
|
||||
return (
|
||||
<Box
|
||||
pt={['25px', '20px', '45px']}
|
||||
pb={['20px', '15px', '30px']}
|
||||
borderBottomWidth={1}
|
||||
mb='50px'
|
||||
>
|
||||
<Container maxW='container.md' position='relative'>
|
||||
<NewAlertBanner />
|
||||
<Heading
|
||||
as='h1'
|
||||
color='black'
|
||||
fontSize={['28px', '33px', '40px']}
|
||||
fontWeight={700}
|
||||
mb={['2px', '2px', '5px']}
|
||||
>
|
||||
{roadmap.title}
|
||||
</Heading>
|
||||
<Text fontSize={['13px', '14px', '15px']}>{roadmap.description}</Text>
|
||||
<Flex justifyContent='space-between' alignItems={'center'} mt='20px'>
|
||||
<Stack isInline flex={1}>
|
||||
<Button
|
||||
display={['flex', 'flex']}
|
||||
as={Link}
|
||||
href={'/roadmaps'}
|
||||
size='xs'
|
||||
py='14px'
|
||||
px='10px'
|
||||
colorScheme='teal'
|
||||
variant='solid'
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
>
|
||||
←
|
||||
<Text as='span' display={['none', 'inline']} ml='5px'>
|
||||
All Roadmaps
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
<RoadmapDownloader roadmapTitle={roadmap.featuredTitle} />
|
||||
<RoadmapSubscriber roadmapTitle={roadmap.featuredTitle} />
|
||||
|
||||
<Box flex={1} justifyContent='flex-end' display='flex'>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`${siteConfig.url.issue}?title=[Suggestion] ${roadmap.title}`}
|
||||
target='_blank'
|
||||
size='xs'
|
||||
py='14px'
|
||||
px='10px'
|
||||
colorScheme='green'
|
||||
leftIcon={<ChatIcon />}
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
>
|
||||
Suggest Changes
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Flex>
|
||||
{isInteractiveRoadmap(roadmap.id) && (
|
||||
<Box mt='30px' mb={hasTNSAlert ? ['-53px', '-48px', '-63px'] : ['-37px', '-32px', '-47px']} borderWidth={1} rounded='3px'>
|
||||
{ hasTNSAlert && <TNSAlert roadmapName={roadmap.featuredTitle} />}
|
||||
<Text
|
||||
fontWeight={500}
|
||||
fontSize='14px'
|
||||
bg='white'
|
||||
p='5px 7px'
|
||||
rounded='3px'
|
||||
>
|
||||
<Badge pos='relative' top={'-1px'} mr='6px' colorScheme='yellow'>
|
||||
New
|
||||
</Badge>
|
||||
Resources are here, try clicking any nodes.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Box, Link, Text } from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import React from 'react';
|
||||
import { event } from '../../lib/gtag';
|
||||
|
||||
type TNSAlertProps = {
|
||||
roadmapName: string;
|
||||
};
|
||||
|
||||
export function TNSAlert(props: TNSAlertProps) {
|
||||
const { roadmapName } = props;
|
||||
|
||||
return (
|
||||
<Text
|
||||
fontWeight={500}
|
||||
fontSize='14px'
|
||||
bg='gray.100'
|
||||
p='5px 7px'
|
||||
rounded='2px 2px 0 0'
|
||||
borderBottomWidth={1}
|
||||
>
|
||||
<Box as='span' display={['none', 'none', 'inline']}>Get the latest {roadmapName} news from our sister site
|
||||
<Link
|
||||
href={'https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Banner'}
|
||||
target='_blank' textDecoration='underline'
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'PartnerClick',
|
||||
action: `TNS Referral`,
|
||||
label: `TNS Referral - ${roadmapName}`,
|
||||
});
|
||||
}}
|
||||
fontWeight={600}>TheNewStack.io <ExternalLinkIcon />
|
||||
</Link>
|
||||
</Box>
|
||||
<Box as='span' display={['inline', 'inline', 'none']}>Get latest {roadmapName} news on
|
||||
<Link
|
||||
href={'https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Banner'}
|
||||
target='_blank' textDecoration='underline'
|
||||
onClick={() => {
|
||||
event({
|
||||
category: 'PartnerClick',
|
||||
action: `TNS Referral`,
|
||||
label: `TNS Referral - ${roadmapName}`,
|
||||
});
|
||||
}}
|
||||
fontWeight={600}>TheNewStack.io <ExternalLinkIcon />
|
||||
</Link>
|
||||
</Box>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Box, Flex, Link } from '@chakra-ui/react';
|
||||
import HackerNewsIcon from 'components/icons/hackernews-square.svg';
|
||||
import FacebookIcon from 'components/icons/facebook-square.svg';
|
||||
import TwitterIcon from 'components/icons/twitter-square.svg';
|
||||
import RedditIcon from 'components/icons/reddit-square.svg';
|
||||
import { Icon } from '@chakra-ui/icons';
|
||||
import { getFacebookShareUrl, getHnShareUrl, getRedditShareUrl, getTwitterShareUrl } from '../lib/url';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ShareIconProps = {
|
||||
text: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function ShareIcons(props: ShareIconProps) {
|
||||
const { text, url } = props;
|
||||
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setOffset(window.scrollY);
|
||||
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
if (offset <= 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pos='absolute' left={'-15px'} top={'190px'} height='100%' display={['none', 'none', 'none', 'block']}>
|
||||
<Flex pos='sticky' top='100px' flexDir='column'>
|
||||
<Link target='_blank' color='gray.500' href={getTwitterShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={TwitterIcon} /></Link>
|
||||
<Link target='_blank' color='gray.500' href={getFacebookShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={FacebookIcon} /></Link>
|
||||
<Link target='_blank' color='gray.500' href={getHnShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={HackerNewsIcon} /></Link>
|
||||
<Link target='_blank' color='gray.500' href={getRedditShareUrl({ text, url })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={RedditIcon} /></Link>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Flex, Link, Text } from '@chakra-ui/react';
|
||||
import YouTubeLogo from '../components/icons/youtube.svg';
|
||||
import siteConfig from '../content/site.json';
|
||||
import { event } from '../lib/gtag';
|
||||
|
||||
export function StickyBanner() {
|
||||
return (
|
||||
<Flex as={Link}
|
||||
href={siteConfig.url.youtube}
|
||||
bg={'yellow.200'}
|
||||
color='gray.900'
|
||||
alignItems='center'
|
||||
position='sticky'
|
||||
top={0}
|
||||
zIndex={999}
|
||||
justifyContent='center'
|
||||
py='8px'
|
||||
_hover={{ textDecoration: 'none', bg: 'yellow.400' }}
|
||||
target='_blank'
|
||||
onClick={() => event({
|
||||
category: 'Subscription',
|
||||
action: 'Clicked the YouTube banner',
|
||||
label: 'Sticky YouTube banner on Top'
|
||||
})}
|
||||
>
|
||||
<YouTubeLogo style={{ height: '20px', display: 'inline-block', marginRight: '7px' }} />
|
||||
<Text as='span' fontWeight={500} fontSize='14px'>
|
||||
<Text as='span'>We now have a YouTube Channel. <Text as='span' d={['none', 'inline']}>Subscribe for the video
|
||||
content.</Text></Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Box, Button, Container, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { event } from '../lib/gtag';
|
||||
|
||||
export function TeamsBanner() {
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Box bg='teal.500' borderTopWidth={1} py={['45px', '45px', '70px']} textAlign='center'>
|
||||
<Container maxW='container.sm'>
|
||||
<Heading as='h4' color={'white'} fontSize={['25px', '25px', '35px']} mb={['10px', '10px', '20px']}>Roadmaps for Teams</Heading>
|
||||
<Text lineHeight='26px' color={'white'} fontSize={['15px', '15px', '18px']} mb='20px'>We are working on a solution for teams. Help us shape the platform!</Text>
|
||||
<Button onClick={() => {
|
||||
event({
|
||||
category: 'UpcomingFeatureClick',
|
||||
action: `Teams Form Redirect`,
|
||||
label: `Click Teams Footer Link`
|
||||
});
|
||||
}} target={'_blank'} as={Link} href='https://forms.gle/6X2matbCmjmvYGGt6' _hover={{textDecoration: 'none', bg: 'gray.300'}}>Take a Survey</Button>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Badge, Box, Heading, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
type VideoGridItemProps = {
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
date: string;
|
||||
target?: string;
|
||||
isNew?: boolean;
|
||||
colorIndex?: number;
|
||||
};
|
||||
|
||||
const bgColorList = [
|
||||
'gray.900',
|
||||
'purple.900',
|
||||
'blue.900',
|
||||
'red.900',
|
||||
'green.900',
|
||||
'teal.900',
|
||||
'yellow.900',
|
||||
'cyan.900',
|
||||
'pink.900',
|
||||
|
||||
'gray.800',
|
||||
'purple.800',
|
||||
'blue.800',
|
||||
'red.800',
|
||||
'green.800',
|
||||
'teal.800',
|
||||
'yellow.800',
|
||||
'cyan.800',
|
||||
'pink.800',
|
||||
|
||||
'gray.700',
|
||||
'purple.700',
|
||||
'blue.700',
|
||||
'red.700',
|
||||
'green.700',
|
||||
'teal.700',
|
||||
'yellow.700',
|
||||
'cyan.700',
|
||||
'pink.700',
|
||||
|
||||
'gray.600',
|
||||
'purple.600',
|
||||
'blue.600',
|
||||
'red.600',
|
||||
'green.600',
|
||||
'teal.600',
|
||||
'yellow.600',
|
||||
'cyan.600',
|
||||
'pink.600'
|
||||
];
|
||||
|
||||
export function VideoGridItem(props: VideoGridItemProps) {
|
||||
const { title, subtitle, date, isNew = false, colorIndex = 0, href, target } = props;
|
||||
|
||||
return (
|
||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={ href } target={target || '_self'} shadow='xl' p='20px'
|
||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1}>
|
||||
<Text mb='7px' fontSize='12px' color='gray.400'>
|
||||
{isNew && <Badge colorScheme={'green'} mr='10px'>New</Badge>}
|
||||
{date}
|
||||
</Text>
|
||||
<Heading color='white' mb={'6px'} fontSize='20px' lineHeight={'28px'}>{title}</Heading>
|
||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
[
|
||||
{
|
||||
"username": "kamranahmedse",
|
||||
"name": "Kamran Ahmed",
|
||||
"twitter": "kamranahmedse",
|
||||
"picture": "/authors/kamranahmedse.jpeg",
|
||||
"bio": "Lead engineer at Tajawal. Lover of all things web and opensource. Created roadmap.sh to help the confused ones."
|
||||
},
|
||||
{
|
||||
"username": "jesse",
|
||||
"name": "Jesse Li",
|
||||
"twitter": "__jesse_li",
|
||||
"picture": "/authors/jesse.png",
|
||||
"bio": "Software engineer."
|
||||
},
|
||||
{
|
||||
"username": "dmytrobol",
|
||||
"name": "Dmytro Bolkachov",
|
||||
"twitter": "dmytrobol",
|
||||
"picture": "/authors/dmytrobol.png",
|
||||
"bio": "JavaScript Lad, Movie buff and coder interested in everything web related"
|
||||
},
|
||||
{
|
||||
"username": "spekulatius",
|
||||
"name": "Peter Thaleikis",
|
||||
"twitter": "spekulatius1984",
|
||||
"picture": "/authors/spekulatius.jpg",
|
||||
"bio": "Developer building side-projects for fun, lover of the web and open source"
|
||||
},
|
||||
{
|
||||
"username": "ebrahimbharmal007",
|
||||
"name": "Ebrahim Bharmal",
|
||||
"twitter": "BharmalEbrahim",
|
||||
"picture": "/authors/ebrahimbharmal007.png",
|
||||
"bio": "Love building projects using tools completely new to me. Python forever. Senior at University of Texas at Arlington (2021)"
|
||||
},
|
||||
{
|
||||
"username": "lesovsky",
|
||||
"name": "Alexey Lesovsky",
|
||||
"bio": "Linux system administrator and PostgreSQL DBA at DataEgret.",
|
||||
"picture": "/authors/lesovsky.jpeg"
|
||||
},
|
||||
{
|
||||
"username": "danielgruesso",
|
||||
"name": "Daniel Gruesso",
|
||||
"bio": "Product manager working on blockchain and smart contracts developer tools",
|
||||
"picture": "/authors/danielgruesso.jpg"
|
||||
}
|
||||
]
|
||||
@@ -1,314 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "session-based-authentication",
|
||||
"title": "Session Based Authentication",
|
||||
"description": "Learn what is Session Based Authentication and how to implement it in Node.js",
|
||||
"isNew": true,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2022-11-01T19:59:14.191Z",
|
||||
"createdAt": "2022-11-01T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "http-basic-authentication",
|
||||
"title": "HTTP Basic Authentication",
|
||||
"description": "Learn what is HTTP Basic Authentication and how to implement it in Node.js",
|
||||
"isNew": true,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2022-10-03T19:59:14.191Z",
|
||||
"createdAt": "2022-10-03T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "basics-of-authentication",
|
||||
"title": "Basics of Authentication",
|
||||
"description": "Learn the basics of Authentication and Authorization",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2022-09-21T19:59:14.191Z",
|
||||
"createdAt": "2022-09-21T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "avoid-render-blocking-javascript-with-async-defer",
|
||||
"title": "Async and Defer Script Loading",
|
||||
"description": "Learn how to avoid render blocking JavaScript using async and defer scripts.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-09-10T19:59:14.191Z",
|
||||
"createdAt": "2021-09-10T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "what-are-web-vitals",
|
||||
"title": "What are Web Vitals?",
|
||||
"description": "Learn what are the core web vitals and how to measure them.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-09-05T19:59:14.191Z",
|
||||
"createdAt": "2021-09-05T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "what-is-sli-slo-sla",
|
||||
"title": "SLIs, SLOs and SLAs",
|
||||
"description": "Learn what are different indicators for performance identification of any service.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-08-31T19:59:14.191Z",
|
||||
"createdAt": "2021-08-31T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "ci-cd",
|
||||
"title": "What is CI and CD?",
|
||||
"description": "Learn the basics of CI/CD and how to implement that with GitHub Actions.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-07-09T19:59:14.191Z",
|
||||
"createdAt": "2021-07-09T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "sso",
|
||||
"title": "SSO — Single Sign On",
|
||||
"description": "Learn the basics of SAML and understand how does Single Sign On work.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-07-01T19:59:14.191Z",
|
||||
"createdAt": "2021-07-01T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "oauth",
|
||||
"title": "OAuth — Open Authorization",
|
||||
"description": "Learn and understand what is OAuth and how it works",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-06-28T19:59:14.191Z",
|
||||
"createdAt": "2021-06-28T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "jwt-authentication",
|
||||
"title": "JWT Authentication",
|
||||
"description": "Understand what is JWT authentication and how is it implemented",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-06-20T19:59:14.191Z",
|
||||
"createdAt": "2021-06-20T19:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "token-authentication",
|
||||
"title": "Token Based Authentication",
|
||||
"description": "Understand what is token based authentication and how it is implemented",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-06-02T20:59:14.191Z",
|
||||
"createdAt": "2021-06-02T20:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "session-authentication",
|
||||
"title": "Session Based Authentication",
|
||||
"description": "Understand what is session based authentication and how it is implemented",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-05-26T20:59:14.191Z",
|
||||
"createdAt": "2021-05-26T20:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "basic-authentication",
|
||||
"title": "Basic Authentication",
|
||||
"description": "Understand what is basic authentication and how it is implemented",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-05-19T20:59:14.191Z",
|
||||
"createdAt": "2021-05-19T20:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "character-encodings",
|
||||
"title": "Character Encodings",
|
||||
"description": "Covers the basics of character encodings and explains ASCII vs Unicode",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-05-14T20:59:14.191Z",
|
||||
"createdAt": "2021-05-14T20:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "unfamiliar-codebase",
|
||||
"title": "Unfamiliar Codebase",
|
||||
"description": "Tips on getting familiar with an unfamiliar codebase",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-05-04T20:59:14.191Z",
|
||||
"createdAt": "2021-05-04T20:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "why-build-it-and-they-will-come-wont-work-anymore",
|
||||
"title": "Build it and they will come?",
|
||||
"description": "Why “build it and they will come” alone won’t work anymore",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "spekulatius",
|
||||
"updatedAt": "2021-05-04T12:59:14.191Z",
|
||||
"createdAt": "2021-05-04T12:59:14.191Z"
|
||||
},
|
||||
{
|
||||
"id": "dhcp-in-one-picture",
|
||||
"title": "DHCP in One Picture",
|
||||
"description": "Here is what happens when a new device joins the network.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-04-28T15:48:21.191Z",
|
||||
"createdAt": "2021-04-28T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "ssl-tls-https-ssh",
|
||||
"title": "SSL vs TLS vs SSH",
|
||||
"description": "Quick tidbit on the differences between SSL, TLS, HTTPS and SSH",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-04-22T15:48:21.191Z",
|
||||
"createdAt": "2021-04-22T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "asymptotic-notation",
|
||||
"title": "Asymptotic Notation",
|
||||
"description": "Learn the basics of measuring the time and space complexity of algorithms",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-04-03T15:48:21.191Z",
|
||||
"createdAt": "2021-04-03T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "big-o-notation",
|
||||
"title": "Big-O Notation",
|
||||
"description": "Easy to understand explanation of Big-O notation without any fancy terms",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-03-15T15:48:21.191Z",
|
||||
"createdAt": "2021-03-15T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "random-numbers",
|
||||
"title": "Random Numbers: Are they?",
|
||||
"description": "Learn how they are generated and why they may not be truly random.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-03-14T15:48:21.191Z",
|
||||
"createdAt": "2021-03-14T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "scaling-databases",
|
||||
"title": "Scaling Databases",
|
||||
"description": "Learn the ups and downs of different database scaling strategies",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-02-18T15:48:21.191Z",
|
||||
"createdAt": "2021-02-18T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "what-is-internet",
|
||||
"title": "How does the internet work?",
|
||||
"description": "Learn the basics of internet and everything involved with this short video series",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2021-02-29T15:48:21.191Z",
|
||||
"createdAt": "2021-02-29T15:48:21.191Z"
|
||||
},
|
||||
{
|
||||
"id": "torrent-client",
|
||||
"title": "Building a BitTorrent Client",
|
||||
"description": "Learn everything you need to know about BitTorrent by writing a client in Go",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "jesse",
|
||||
"updatedAt": "2021-01-17T15:48:21.191Z",
|
||||
"createdAt": "2021-01-17T15:48:21.191Z",
|
||||
"canonical": "https://blog.jse.li/posts/torrent/"
|
||||
},
|
||||
{
|
||||
"id": "levels-of-seniority",
|
||||
"title": "Levels of Seniority",
|
||||
"description": "How to Step Up as a Junior, Mid Level or a Senior Developer?",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2020-12-03T12:13:00.860Z",
|
||||
"createdAt": "2020-12-03T12:13:00.860Z"
|
||||
},
|
||||
{
|
||||
"id": "design-patterns-for-humans",
|
||||
"title": "Design Patterns for Humans",
|
||||
"description": "A language agnostic, ultra-simplified explanation to design patterns",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2019-10-09T12:00:00.860Z",
|
||||
"createdAt": "2019-01-23T17:00:00.860Z"
|
||||
},
|
||||
{
|
||||
"id": "journey-to-http2",
|
||||
"title": "Journey to HTTP/2",
|
||||
"description": "The evolution of HTTP. How it all started and where we stand today",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"createdAt": "2018-12-04T12:00:00.860Z",
|
||||
"updatedAt": "2018-12-04T12:00:00.860Z",
|
||||
"isDraft": true
|
||||
},
|
||||
{
|
||||
"id": "dns-in-one-picture",
|
||||
"title": "DNS in One Picture",
|
||||
"description": "Quick illustrative guide on how a website is found on the internet.",
|
||||
"isNew": false,
|
||||
"type": "visual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"updatedAt": "2018-12-04T12:00:00.860Z",
|
||||
"createdAt": "2018-12-04T17:00:00.860Z"
|
||||
},
|
||||
{
|
||||
"id": "http-caching",
|
||||
"title": "HTTP Caching",
|
||||
"description": "Everything you need to know about web caching",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"createdAt": "2018-11-29T17:00:00.860Z",
|
||||
"updatedAt": "2018-11-29T17:00:00.860Z"
|
||||
},
|
||||
{
|
||||
"id": "history-of-javascript",
|
||||
"title": "Brief History of JavaScript",
|
||||
"description": "How JavaScript was introduced and evolved over the years",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "kamranahmedse",
|
||||
"createdAt": "2017-10-28T17:00:00.860Z",
|
||||
"updatedAt": "2017-10-28T17:00:00.860Z"
|
||||
},
|
||||
{
|
||||
"id": "proxy-servers",
|
||||
"title": "Proxy Servers",
|
||||
"description": "How do proxy servers work and what are forward and reverse proxies?",
|
||||
"isNew": false,
|
||||
"type": "textual",
|
||||
"authorUsername": "ebrahimbharmal007",
|
||||
"createdAt": "2017-10-24T17:00:00.860Z",
|
||||
"updatedAt": "2017-10-24T17:00:00.860Z"
|
||||
}
|
||||
]
|
||||
@@ -1,4 +0,0 @@
|
||||
Asymptotic notation is the standard way of measuring the time and space that an algorithm will consume as the input grows. In one of my last guides, I covered "Big-O notation" and a lot of you asked for a similar one for Asymptotic notation. You can find the [previous guide here](/guides/big-o-notation).
|
||||
|
||||
[](/guides/asymptotic-notation.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/avoid-render-blocking-javascript-with-async-defer.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/basic-authentication.png)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
Our last video series was about data structures. We looked at the most common data structures, their use cases, pros and cons, and the different operations you could perform on each data structure.
|
||||
|
||||
Today, we are kicking off a similar series for Authentication strategies where we will discuss everything you need to know about authentication and authentication strategies.
|
||||
|
||||
In this guide today will be talking about what authentication is, and we will cover some terminology that will help us later in the series. You can watch the video below or continue reading this guide.
|
||||
|
||||
<iframe src="https://www.youtube.com/embed/Mcyt9SrZT6g" title="Basics of Authentication" />
|
||||
|
||||
## What is Authentication?
|
||||
|
||||
Authentication is the process of verifying someone's identity. A real-world example of that would be when you board a plane, the airline worker checks your passport to verify your identity, so the airport worker authenticates you.
|
||||
|
||||
If we talk about computers, when you log in to any website, you usually authenticate yourself by entering your username and password, which is then checked by the website to ensure that you are who you claim to be. There are two things you should keep in mind:
|
||||
|
||||
- Authentication is not only for the persons
|
||||
- And username and password are not the only way to authenticate.
|
||||
|
||||
Some other examples are:
|
||||
|
||||
- When you open a website in the browser. If the website uses HTTP, TLS is used to authenticate the server and avoid the fake loading of websites.
|
||||
|
||||
- There might be server-to-server communication on the website. The server may need to authenticate the incoming request to avoid malicious usage.
|
||||
|
||||
## How does Authentication Work?
|
||||
|
||||
On a high level, we have the following factors used for authentication.
|
||||
|
||||
- **Username and Password**
|
||||
- **Security Codes, Pin Codes, or Security Questions** — An example would be the pin code you enter at an ATM to withdraw cash.
|
||||
- **Hard Tokens and Soft Tokens** — Hard tokens are the special hardware devices that you attach to your device to authenticate yourself. Soft tokens, unlike hard tokens, don't have any authentication-specific device; we must verify the possession of a device that was used to set up the identity. For example, you may receive an OTP to log in to your account on a website.
|
||||
- **Biometric Authentication** — In biometric authentication, we authenticate using biometrics such as iris, facial, or voice recognition.
|
||||
|
||||
We can categorize the factors above into three different types.
|
||||
|
||||
- Username / Password and Security codes rely on the person's knowledge: we can group them under the **Knowledge Factor**.
|
||||
|
||||
- In hard and soft tokens, we authenticate by checking the possession of hardware, so this would be a **Possession Factor**.
|
||||
|
||||
- And in biometrics, we test the person's inherent qualities, i.e., iris, face, or voice, so this would be a **Qualities** factor.
|
||||
|
||||
This brings us to our next topic: Multi-factor Authentication and Two-Factor Authentication.
|
||||
|
||||
## Multifactor Authentication
|
||||
|
||||
Multifactor authentication is the type of authentication in which we rely on more than one factor to authenticate a user.
|
||||
|
||||
For example, if we pick up username/password from the **knowledge factor**. And we pick soft tokens from the **possession factor**, and we say that for a user to authenticate, they must enter their credentials and an OTP, which will be sent to their mobile phone, so this would be an example of multifactor authentication.
|
||||
|
||||
In multifactor authentication, since we rely on more than one factor, this way of authentication is much more secure than single-factor authentication.
|
||||
|
||||
One important thing to note here is that the factors you pick for authentication, they must differ. So, for example, if we pick up a username/password and security question or security codes, it is still not true multifactor authentication because we still rely on the knowledge factor. The factors have to be different from each other.
|
||||
|
||||
### Two-Factor Authentication
|
||||
|
||||
Two-factor authentication is similar to multifactor authentication. The only difference is that there are precisely two factors in 2FA. In MFA, we can have 2, 3, 4, or any authentication factors; 2FA has exactly two factors. We can say that 2FA is always MFA, because there are more than one factors. MFA is not always 2FA because there may be more than two factors involved.
|
||||
|
||||
Next we have the difference between authentication and authorization. This comes up a lot in the interviews, and beginners often confuse them.
|
||||
|
||||
### What is Authentication
|
||||
Authentication is the process of verifying the identity. For example, when you enter your credentials at a login screen, the application here identifies you through your credentials. So this is what the authentication is, the process of verifying the identity.
|
||||
|
||||
In case of an authentication failure, for example, if you enter an invalid username and password, the HTTP response code is "Unauthorized" 401.
|
||||
|
||||
### What is Authorization
|
||||
|
||||
Authorization is the process of checking permission. Once the user has logged in, i.e., the user has been authenticated, the process of reviewing the permission to see if the user can perform the relevant operation or not is called authorization.
|
||||
|
||||
And in case of authorization failure, i.e., if the user tries to perform an operation they are not allowed to perform, the HTTP response code is forbidden 403.
|
||||
|
||||
## Authentication Strategies
|
||||
|
||||
Given below is the list of common authentication strategies:
|
||||
|
||||
- Basics of Authentication
|
||||
- Session Based Authentication
|
||||
- Token-Based Authentication
|
||||
- JWT Authentication
|
||||
- OAuth - Open Authorization
|
||||
- Single Sign On (SSO)
|
||||
|
||||
In this series of illustrated videos and textual guides, we will be going through each of the strategies discussing what they are, how they are implemented, the pros and cons and so on.
|
||||
|
||||
So stay tuned, and I will see you in the next one.
|
||||
@@ -1,4 +0,0 @@
|
||||
Big-O notation is the mathematical notation that helps analyse the algorithms to get an idea about how they might perform as the input grows. The image below explains Big-O in a simple way without using any fancy terminology.
|
||||
|
||||
[](/guides/big-o-notation.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/character-encodings.png)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
The image below details the differences between the continuous integration and continuous delivery. Also, here is the [accompanying video on implementing that with GitHub actions](https://www.youtube.com/watch?v=nyKZTKQS_EQ).
|
||||
|
||||
[](/guides/ci-cd.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/dhcp.png)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
DNS or Domain Name System is one of the fundamental blocks of the internet. As a developer, you should have at-least the basic understanding of how it works. This article is a brief introduction to what is DNS and how it works.
|
||||
|
||||
DNS at its simplest is like a phonebook on your mobile phone. Whenever you have to call one of your contacts, you can either dial their number from your memory or use their name which will then be used by your mobile phone to search their number in your phone book to call them. Every time you make a new friend, or your existing friend gets a mobile phone, you have to memorize their phone number or save it in your phonebook to be able to call them later on. DNS or Domain Name System, in a similar fashion, is a mechanism that allows you to browse websites on the internet. Just like your mobile phone does not know how to call without knowing the phone number, your browser does not know how to open a website just by the domain name; it needs to know the IP Address for the website to open. You can either type the IP Address to open, or provide the domain name and press enter which will then be used by your browser to find the IP address by going through several hoops. The picture below is the illustration of how your browser finds a website on the internet.
|
||||
|
||||
[](https://i.imgur.com/z9rwm5A.png)
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/jwt-authentication.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/oauth.png)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
One of my favorite pastimes is going through the history of my favorite projects to learn how they grew over time or how certain features were implemented.
|
||||
|
||||
The image below describes how I do that in WebStorm.
|
||||
|
||||
[](/guides/project-history.png)
|
||||
@@ -1,4 +0,0 @@
|
||||
Random numbers are everywhere from computer games to lottery systems, graphics software, statistical sampling, computer simulation and cryptography. Graphic below is a quick explanation to how the random numbers are generated and why they may not be truly random.
|
||||
|
||||
[](/guides/random-numbers.png)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
The chart below aims to give you a really basic understanding of how the capability of a DBMS is increased to handle a growing amount of load.
|
||||
|
||||
[](/guides/scaling-databases.svg)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/session-authentication.png)
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
HTTP is the internet protocol that standardizes how clients and servers interact with each other. When you open a website, among other things, HTTP is the protocol that helps load the website in the browser.
|
||||
|
||||
## HTTP is Stateless
|
||||
HTTP is a stateless protocol which means that each request made from the client to the server is treated as a standalone request; neither the client nor the server keeps track of the subsequent requests. Sessions allow you to change that; with sessions, the server has a way to associate some information with the client so that when the same client requests the server, it can retrieve that information.
|
||||
|
||||
In this guide, we will learn what is Session-Based Authentication and how to implement it in Node.js. We also have a separate [visual guide on Session-Based Authentication](/guides/session-authentication) as well that explains the topic visually.
|
||||
|
||||
## What is Session-Based Authentication?
|
||||
Session-based authentication is a stateful authentication technique where we use sessions to keep track of the authenticated user. Here is how Session Based Authentication works:
|
||||
|
||||
* User submits the login request for authentication.
|
||||
* Server validates the credentials. If the credentials are valid, the server initiates a session and stores some information about the client. This information can be stored in memory, file system, or database. The server also generates a unique identifier that it can later use to retrieve this session information from the storage. Server sends this unique session identifier to the client.
|
||||
* Client saves the session id in a cookie and this cookie is sent to the server in each request made after the authentication.
|
||||
* Server, upon receiving a request, checks if the session id is present in the request and uses this session id to get information about the client.
|
||||
|
||||
And that is how session-based authentication works.
|
||||
|
||||
## Session-Based Authentication in Node.js
|
||||
Now that we know what session-based authentication is, let's see how we can implement session-based authentication in Node.js.
|
||||
|
||||
Please note that, for the sake of simplicity, I have intentionally kept the project strictly relevant to the Session Based Authentication and have left out a lot of details that a production-ready application may require. Also, if you don't want to follow along, project [codebase can be found on GitHub](https://github.com/kamranahmedse/node-session-auth-example).
|
||||
|
||||
First things first, create an empty directory that will be holding our application.
|
||||
|
||||
```shell
|
||||
mkdir session-auth-example
|
||||
```
|
||||
Now run the following command to setup a sample `package.json` file:
|
||||
```shell
|
||||
npm init -y
|
||||
```
|
||||
Next, we need to install the dependencies:
|
||||
```shell
|
||||
npm install express express-session
|
||||
```
|
||||
`Express` is the application framework, and `express-session` is the package that helps work with sessions easily.
|
||||
|
||||
### Setting up the server
|
||||
Now create an `index.js` file at the root of the project with the following content:
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const sessions = require('express-session');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(sessions({
|
||||
secret: "some secret",
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 // 24 hours
|
||||
},
|
||||
resave: true,
|
||||
saveUninitialized: false,
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({extended: true}));
|
||||
|
||||
// @todo register routes
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log(`Server Running at port 3000`);
|
||||
});
|
||||
```
|
||||
The important piece to note here is the `express-session` middleware registration which automatically handles the session initialization, cooking parsing and session data retrieval, and so on. In our example here, we are passing the following configuration options:
|
||||
* `secret`: This is used to sign the session ID cookie. Using a secret that cannot be guessed will reduce the ability to hijack a session.
|
||||
* `cookie`: Object containing the configuration for session id cookie.
|
||||
* `resave`: Forces the session to be saved back to the session store, even if the session data was never modified during the request.
|
||||
* `saveUninitialized`: Forces an "uninitialized" session to be saved to the store, i.e., saves a session to the store even if the session was not initiated.
|
||||
|
||||
Another important option is `store` which we can configure to change how/where the session data is stored on the server. By default, this data is stored in the memory, i.e., `MemoryStore`.
|
||||
|
||||
Look at the [express-session documentation](https://github.com/expressjs/session) to learn more about the available options.
|
||||
|
||||
### Creating Handlers
|
||||
Create a directory called the `handlers` at the project's root. This is the directory where we will be placing all the route-handling functions.
|
||||
|
||||
Now let's create the homepage route, which will show the welcome message and a link to log out for the logged-in users and redirect to the login screen for the logged-out users. Create a file at `handlers/home.js` with the following content.
|
||||
|
||||
```javascript
|
||||
module.exports = function HomeHandler(req, res) {
|
||||
if (!req.session.userid) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/HTML')
|
||||
res.write(`
|
||||
<h1>Welcome back ${req.session.userid}</h1>
|
||||
<a href="/logout">Logout</a>
|
||||
`);
|
||||
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
At the top of this function, you will notice the check `req.session.userid`. `req.session` is automatically populated using the session cookie by the `express-session` middleware that we registered earlier. `req.session.userid` is one of the data fields that we will set to store the `userid` of the logged in user.
|
||||
|
||||
Next, we need to register this handler with a route. Open the `index.js` file at the root of the project and register the following route:
|
||||
|
||||
```javascript
|
||||
const HomeHandler = require('./handlers/home.js');
|
||||
|
||||
app.get('/', HomeHandler);
|
||||
```
|
||||
|
||||
Next, we have the login page, redirecting the user to the home screen if the user is logged in or showing the login form. Create a file at `handlers/login.js` with the following content:
|
||||
|
||||
```javascript
|
||||
module.exports = function LoginHandler(req, res) {
|
||||
if (req.session.userid) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/HTML')
|
||||
res.write(`
|
||||
<h1>Login</h1>
|
||||
<form method="post" action="/process-login">
|
||||
<input type="text" name="username" placeholder="Username" /> <br>
|
||||
<input type="password" name="password" placeholder="Password" /> <br>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
res.end();
|
||||
}
|
||||
```
|
||||
Again, at the top of the function, we are simply checking if we have `userid` in the session (which means the user is logged in). If the user is logged in, we redirect them to the homepage; if not, we show the login screen. In the login form, we have the method of `post`, and we submit the form to `/process-login`. Please note that, for the sake of simplicity, we have a simple HTML string returned in the response, but in a real-world application, you will probably have a separate view file.
|
||||
|
||||
Let's first register this page and then implement `/process-login` endpoint. Open the `index.js` file from the root of the project and register the following route:
|
||||
|
||||
```javascript
|
||||
const LoginHandler = require('./handlers/login.js');
|
||||
|
||||
app.get('/login', LoginHandler);
|
||||
```
|
||||
|
||||
Next, we have to implement the functionality to process the login form submissions. Create a file at `handlers/process-login.js` with the following content:
|
||||
|
||||
```javascript
|
||||
module.exports = function processLogin(req, res) {
|
||||
if (req.body.username !== 'admin' || req.body.password !== 'admin') {
|
||||
return res.send('Invalid username or password);
|
||||
}
|
||||
|
||||
req.session.userid = req.body.username;
|
||||
|
||||
res.redirect('/');
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we are simply checking that the username and password should both be `admin` and `admin` for a user to authenticate successfully. Upon finding valid credentials, we set the `userid` in the session by updating `req.session.userid`. Similarly, you can set any data in the session. For example, if we wanted to store the user role, we would do the following:
|
||||
|
||||
```javascript
|
||||
req.session.role = 'admin'
|
||||
```
|
||||
|
||||
And later access this value out of the session anywhere in the subsequent requests.
|
||||
|
||||
Register this route in the `index.js` file at the root of the project:
|
||||
|
||||
```javascript
|
||||
const ProcessLoginHandler = require('./handlers/process-login.js');
|
||||
|
||||
app.post('/process-login', ProcessLoginHandler);
|
||||
```
|
||||
|
||||
Finally, we have the logout functionality. Create a file at `handlers/logout.js` with the following content:
|
||||
|
||||
```javascript
|
||||
module.exports = function Logout(req, res) {
|
||||
req.session.destroy();
|
||||
res.redirect('/');
|
||||
}
|
||||
```
|
||||
|
||||
We reset the session by calling `req.session.destroy()` and then redirecting the user to the homepage. Register the logout handler in the `index.js` file using the following:
|
||||
|
||||
```javascript
|
||||
const LogoutHandler = require('./handlers/logout.js');
|
||||
|
||||
app.get('/logout', LogoutHandler);
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
Open the `package.json` file and register the `start` script as follows:
|
||||
|
||||
```javascript
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
```
|
||||
|
||||
Now you can start the application by running the following command:
|
||||
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
Now, if you open up your browser and visit the project at `http://localhost:3000` you will be able to see the Session-Based Authentication in action.
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/ssl-tls-https-ssh.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/sso.png)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Threads and Concurrency
|
||||
|
||||
A thread is an execution context in which the instructions to the CPU can be scheduled and executed independently of the parent process. Concurrency is the concept of multiple threads in a shared memory space being computed simultaneously (or intermittently executed in succession to provide that illusion). Concurrency allows multiple processes to execute at once and can apply to programming languages as well as operating systems.
|
||||
|
||||
<ResourceGroupTitle>Free Content</ResourceGroupTitle>
|
||||
<BadgeLink badgeText='Watch' href='https://www.youtube.com/watch?v=olYdb0DdGtM'>Threading Tutorial #1 - Concurrency, Threading and Parallelism</BadgeLink>
|
||||
<BadgeLink colorScheme='yellow' badgeText='Read' href='https://medium.com/@akhandmishra/operating-system-threads-and-concurrency-aec2036b90f8'>Operating System: Threads and Concurrency</BadgeLink>
|
||||
<BadgeLink colorScheme='yellow' badgeText='Read' href='https://web.mit.edu/6.005/www/fa14/classes/17-concurrency/'>Reading 17: Concurrency</BadgeLink>
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/token-authentication.png)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/unfamiliar-codebase.png)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
> **Roadmap is not ready yet**. Please check back later or [subscribe to get notified](/signup).
|
||||
|
||||
While we prepare the roadmap, follow this simple advice to learn anything
|
||||
|
||||
> Just **pick a project and start working on it**, you will learn all that you need along the way.
|
||||
|
||||
**→** [All Roadmaps](/roadmaps) • [Programming guides](/guides) • [Subscribe](/signup)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/web-vitals.png)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
Since the explosive growth of web-based applications, every developer stands to benefit from understanding how the Internet works. Through this article and its accompanying introductory series of short videos about the Internet from [code.org](https://code.org), you will learn the basics of the Internet and how it works. After going through this article, you will be able to answer the following questions:
|
||||
|
||||
* What is the Internet?
|
||||
* How does the information move on the internet?
|
||||
* How do the networks talk to each other and the protocols involved?
|
||||
* What's the relationship between packets, routers, and reliability?
|
||||
* HTTP and the HTML – How are you viewing this webpage in your browser?
|
||||
* How is the information transfer on the internet made secure?
|
||||
* What is cybersecurity and what are some common internet crimes?
|
||||
|
||||
## What is the Internet?
|
||||
|
||||
The Internet is a global network of computers connected to each other which communicate through a standardized set of protocols.
|
||||
|
||||
In the video below, Vint Cerf, one of the "fathers of the internet," explains the history of how the Internet works and how no one person or organization is really in charge of it.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/Dxcc6ycZ73M" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
|
||||
## Wires, Cables, and Wi-Fi
|
||||
|
||||
Information on the Internet moves from one computer to another in the form of bits over various mediums, including Ethernet cables, fiber optic cables, and wireless signals (i.e., radio waves).
|
||||
|
||||
In the video linked below, you will learn about the different mediums for data transfer on the Internet and the pros and cons for each.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ZhEf7e4kopM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## IP Addresses and DNS
|
||||
|
||||
Now that you know about the physical medium for the data transfer over the internet, it's time to learn about the protocols involved. How does the information traverse from one computer to another in this massive global network of computers?
|
||||
|
||||
In the video below, you will get a brief introduction to IP, DNS, and how these protocols make the Internet work.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/5o8CwafCxnU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## Packets, Routing, and Reliability
|
||||
|
||||
Information transfer on the Internet from one computer to another does not need to follow a fixed path; in fact, it may change paths during the transfer. This information transfer is done in the form of packets and these packets may follow different routes depending on certain factors.
|
||||
|
||||
In this video, you will learn about how the packets of information are routed from one computer to another to reach the destination.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/AYdF7b3nMto" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## HTTP and HTML
|
||||
|
||||
HTTP is the standard protocol by which webpages are transferred over the Internet. The video below is a brief introduction to HTTP and how web browsers load websites for you.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/kBXQZMmiA4s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## Encryption and Public Keys
|
||||
|
||||
Cryptography is what keeps our communication secure on the Internet. In this short video, you will learn the basics of cryptography, SSL/TLS, and how they help make the communication on the Internet secure.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ZghMPWGXexs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## Cybersecurity and Crime
|
||||
|
||||
Cybersecurity refers to the protective measures against criminal activity accomplished through using a network, technological devices, and the internet.In this video, you will learn about the basics of cybersecurity and common cybercrimes.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/AuYNXgO_f3Y" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/).
|
||||
@@ -1,2 +0,0 @@
|
||||
[](/guides/sli-slo-sla.jpeg)
|
||||
|
||||