mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 10:11:55 +08:00
Compare commits
152 Commits
feat/new-u
...
fix/sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75945c7c65 | ||
|
|
8b61bbfcbb | ||
|
|
2c39611b47 | ||
|
|
475cb85600 | ||
|
|
37de8700d5 | ||
|
|
9db05bddbd | ||
|
|
4bda81bf52 | ||
|
|
00c5254ea9 | ||
|
|
bc97fc4c03 | ||
|
|
f953b96d52 | ||
|
|
40793efe4e | ||
|
|
22a29605d8 | ||
|
|
cbfd4f7fcb | ||
|
|
992cf82e5c | ||
|
|
ea89ac864c | ||
|
|
35a4a93ca7 | ||
|
|
2544d4b12d | ||
|
|
d6ee7ef76a | ||
|
|
daa2c03643 | ||
|
|
92a61e7c45 | ||
|
|
e6ad9b29d6 | ||
|
|
ea040b7912 | ||
|
|
28e874bdcd | ||
|
|
4125c618d7 | ||
|
|
088615d13f | ||
|
|
b02d07917d | ||
|
|
9f1d44e542 | ||
|
|
587e8a197f | ||
|
|
5023e962b3 | ||
|
|
28c41b02c0 | ||
|
|
7a80313f1d | ||
|
|
6bc1233253 | ||
|
|
f82ca986c5 | ||
|
|
d45b08c5d3 | ||
|
|
d0c6ae1835 | ||
|
|
be2d3a0d0e | ||
|
|
c81c799e44 | ||
|
|
0bcf1b09bd | ||
|
|
f7f0270f75 | ||
|
|
8033ccbe6d | ||
|
|
694c208ee3 | ||
|
|
41c7388f63 | ||
|
|
2f2a9b2d32 | ||
|
|
083ec6c0d7 | ||
|
|
32690e98da | ||
|
|
ed8c2f3168 | ||
|
|
1a9f53150b | ||
|
|
1779eef91b | ||
|
|
d7b3f82d18 | ||
|
|
fb34a7176c | ||
|
|
c5316929ba | ||
|
|
1ef1818327 | ||
|
|
472c7f415b | ||
|
|
c43d294ab4 | ||
|
|
06747df054 | ||
|
|
2aeb2ad65e | ||
|
|
3fd6e859df | ||
|
|
6d85bbe488 | ||
|
|
5bb1252795 | ||
|
|
dfaf120314 | ||
|
|
be5ccbbc0b | ||
|
|
12950051d6 | ||
|
|
03cd25b6c0 | ||
|
|
829615ffec | ||
|
|
6a24436418 | ||
|
|
c726a1a342 | ||
|
|
7d7835ee9c | ||
|
|
17d30c0e8d | ||
|
|
62bd5c339f | ||
|
|
111f702b9b | ||
|
|
3dd115dce1 | ||
|
|
75925cb53a | ||
|
|
de5bed02f8 | ||
|
|
72c670570b | ||
|
|
a450b4ed5a | ||
|
|
66be61efa3 | ||
|
|
caddd0f93d | ||
|
|
cc32cbe79c | ||
|
|
f9d39db24a | ||
|
|
203bbc6eae | ||
|
|
31a852113f | ||
|
|
66119e935b | ||
|
|
3374fafe5b | ||
|
|
8ed47a2e71 | ||
|
|
932b513d98 | ||
|
|
e0ae5dd309 | ||
|
|
70bc2a1038 | ||
|
|
86c1120559 | ||
|
|
c3135e1470 | ||
|
|
23346ec007 | ||
|
|
c92a183ef8 | ||
|
|
ca7888aa37 | ||
|
|
33b36a7017 | ||
|
|
bff98e6448 | ||
|
|
5dd2bc439f | ||
|
|
24d10d212f | ||
|
|
4127f77aac | ||
|
|
dbebf593fc | ||
|
|
0e0b550f98 | ||
|
|
80741df13b | ||
|
|
14b6aea3b1 | ||
|
|
e0da1e4f0e | ||
|
|
5cc4b834d1 | ||
|
|
314eb5d7d2 | ||
|
|
ad2597f610 | ||
|
|
8d25eabe3a | ||
|
|
6186e12b05 | ||
|
|
158857c928 | ||
|
|
8e6959cc60 | ||
|
|
e351f653a1 | ||
|
|
83e315aef7 | ||
|
|
b15bdd5f78 | ||
|
|
0783330a70 | ||
|
|
7ec56fd1ff | ||
|
|
201632941a | ||
|
|
126501b40a | ||
|
|
34ba9162b2 | ||
|
|
e093eddabc | ||
|
|
1e4a4c96b8 | ||
|
|
0c3ea981cb | ||
|
|
5de9539af5 | ||
|
|
3722e5f3bd | ||
|
|
7f90b8a0b0 | ||
|
|
2bafd61f71 | ||
|
|
adc66cf97c | ||
|
|
c754a971c3 | ||
|
|
dbda69fc23 | ||
|
|
6f1087981c | ||
|
|
ec8a0917c1 | ||
|
|
60e5e38a55 | ||
|
|
5d77d36236 | ||
|
|
37b3140516 | ||
|
|
34b68478cc | ||
|
|
9bb86408c5 | ||
|
|
d07fd3d183 | ||
|
|
9aa363a01e | ||
|
|
9f2a33f078 | ||
|
|
91cfa88b3d | ||
|
|
d87ea1c972 | ||
|
|
dbf2353a41 | ||
|
|
8d78c17c77 | ||
|
|
698dbbd7d8 | ||
|
|
25216d4052 | ||
|
|
2be0d61a1e | ||
|
|
f3ee75e92d | ||
|
|
db0b5e77ad | ||
|
|
c239886049 | ||
|
|
aab03074f8 | ||
|
|
433302b910 | ||
|
|
a583937f5c | ||
|
|
412e3b5935 | ||
|
|
1ffa292c98 |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1737392387456
|
||||
"lastUpdateCheck": 1739229597159
|
||||
}
|
||||
}
|
||||
2
.github/workflows/cloudfront-fe-cache.yml
vendored
2
.github/workflows/cloudfront-fe-cache.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/roadmapsh/infra-ansible/actions/workflows/playbook.yml/dispatches \
|
||||
-d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront", "is_verbose": false } }'
|
||||
-d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront,cloudfront-course", "is_verbose": false } }'
|
||||
|
||||
16
.github/workflows/greetings.yml
vendored
16
.github/workflows/greetings.yml
vendored
@@ -15,19 +15,9 @@ jobs:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
🙌 Hello! Thank you for taking the time to file an issue.
|
||||
|
||||
If this is a bug report, please include any relevant logs or details that can help us debug the problem. Your help is greatly appreciated! 💡
|
||||
|
||||
We'll get back to you as soon as possible, kindly be patient for a response from a maintainer.
|
||||
pr-message: |
|
||||
🎉 Warm regards and welcome! Thank you for your first ever contribution to **Roadmap.sh**!
|
||||
Thank you for your first ever contribution to [roadmap.sh](https://roadmap.sh)! 🎉
|
||||
|
||||
We appreciate your effort and enthusiasm. Before diving in, we kindly ask you to take a moment to go through our [Contribution Guidelines](https://github.com/kamranahmedse/developer-roadmap/blob/master/contributing.md) 📘 to ensure your contribution aligns with the project's standards and goals.
|
||||
Please make sure to follow the [contribution guidelines](https://github.com/kamranahmedse/developer-roadmap/blob/master/contributing.md) when contributing to this project. Any PRs that don't follow the guidelines will be closed.
|
||||
|
||||
If you are fixing a bug, please reference the associated issue number in your pull request description. 🐛
|
||||
|
||||
If you're working on a new feature, feel free to check with the community on [discord](https://roadmap.sh/discord) to ensure the feature will be accepted. *Also, kindly refrain pinging the maintainer(s).* 🚀
|
||||
|
||||
Thanks for choosing to contribute, and for making this project better! 🌟
|
||||
Thanks for choosing to contribute, and for helping make this project better! 🌟
|
||||
@@ -34,6 +34,7 @@
|
||||
"@astrojs/sitemap": "^3.2.0",
|
||||
"@astrojs/tailwind": "^5.1.2",
|
||||
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
||||
"@microsoft/clarity": "^1.0.0",
|
||||
"@nanostores/react": "^0.8.0",
|
||||
"@napi-rs/image": "^1.9.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
@@ -72,14 +73,15 @@
|
||||
"satori-html": "^0.3.2",
|
||||
"sharp": "^0.33.5",
|
||||
"slugify": "^1.6.6",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"turndown": "^7.2.0",
|
||||
"unified": "^11.0.5",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/google": "^1.1.19",
|
||||
"@playwright/test": "^1.48.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -90,6 +92,7 @@
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"ai": "^4.1.51",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
||||
278
pnpm-lock.yaml
generated
278
pnpm-lock.yaml
generated
@@ -10,19 +10,22 @@ importers:
|
||||
dependencies:
|
||||
'@astrojs/node':
|
||||
specifier: ^8.3.4
|
||||
version: 8.3.4(astro@4.16.18)
|
||||
version: 8.3.4(astro@4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3))
|
||||
'@astrojs/react':
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
version: 3.6.3(@types/node@18.19.74)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@astrojs/sitemap':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.1
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.5(astro@4.16.18)(tailwindcss@3.4.17)
|
||||
version: 5.1.5(astro@4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3))(tailwindcss@3.4.17)
|
||||
'@fingerprintjs/fingerprintjs':
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.1
|
||||
'@microsoft/clarity':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
'@nanostores/react':
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.4(nanostores@0.11.3)(react@18.3.1)
|
||||
@@ -43,7 +46,7 @@ importers:
|
||||
version: 18.3.5(@types/react@18.3.18)
|
||||
astro:
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.18(typescript@5.7.3)
|
||||
version: 4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -109,10 +112,10 @@ importers:
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-tooltip:
|
||||
specifier: ^5.28.0
|
||||
version: 5.28.0(react-dom@18.3.1)(react@18.3.1)
|
||||
version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
reactflow:
|
||||
specifier: ^11.11.4
|
||||
version: 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
version: 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
rehype-external-links:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -145,7 +148,7 @@ importers:
|
||||
version: 3.4.17
|
||||
tiptap-markdown:
|
||||
specifier: ^0.8.10
|
||||
version: 0.8.10(@tiptap/core@2.11.3)
|
||||
version: 0.8.10(@tiptap/core@2.11.3(@tiptap/pm@2.11.3))
|
||||
turndown:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
@@ -156,6 +159,9 @@ importers:
|
||||
specifier: ^4.5.5
|
||||
version: 4.5.6(@types/react@18.3.18)(react@18.3.1)
|
||||
devDependencies:
|
||||
'@ai-sdk/google':
|
||||
specifier: ^1.1.19
|
||||
version: 1.1.19(zod@3.24.1)
|
||||
'@playwright/test':
|
||||
specifier: ^1.48.0
|
||||
version: 1.50.0
|
||||
@@ -186,6 +192,9 @@ importers:
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
ai:
|
||||
specifier: ^4.1.51
|
||||
version: 4.1.51(react@18.3.1)(zod@3.24.1)
|
||||
csv-parser:
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.0
|
||||
@@ -216,6 +225,46 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/google@1.1.19':
|
||||
resolution: {integrity: sha512-Q4l2iWAADUf1pGbXX60A2nnUqEtPLtLpXsbjr3hVcgI9M3q9BqUmSoGsoJ/AAwvZU3uarEb0IJuv+7zlitvCBw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.10':
|
||||
resolution: {integrity: sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.1.20':
|
||||
resolution: {integrity: sha512-4QOM9fR9SryaRraybckDjrhl1O6XejqELdKmrM5g9y9eLnWAfjwF+W1aN0knkSHzbbjMqN77sy9B9yL8EuJbDw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/ui-utils@1.1.16':
|
||||
resolution: {integrity: sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -789,6 +838,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@microsoft/clarity@1.0.0':
|
||||
resolution: {integrity: sha512-2QY6SmXnqRj6dWhNY8NYCN3e53j4zCFebH4wGnNhdGV1mqAsQwql2fT0w8TISxCvwwfVp8idsWLIdrRHOms1PQ==}
|
||||
|
||||
'@mixmark-io/domino@2.2.0':
|
||||
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
||||
|
||||
@@ -889,6 +941,10 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
@@ -1281,6 +1337,9 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/dom-to-image@2.6.7':
|
||||
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
|
||||
|
||||
@@ -1389,6 +1448,18 @@ packages:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
ai@4.1.51:
|
||||
resolution: {integrity: sha512-CuJgbi2Ktfv/7jjxvUhFOGZ8OFxWQ8a7ZF19lwJuVLauL4uWHLetm6R3iaafahJ8ZkueSbhR/Bnroy5apd1nCw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
@@ -1737,6 +1808,9 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
diff@5.2.0:
|
||||
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -1857,6 +1931,10 @@ packages:
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
eventsource-parser@3.0.0:
|
||||
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2171,11 +2249,19 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
@@ -2987,6 +3073,9 @@ packages:
|
||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -3115,6 +3204,11 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.3.2:
|
||||
resolution: {integrity: sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
@@ -3130,6 +3224,10 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
@@ -3407,6 +3505,43 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/google@1.1.19(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.10(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
eventsource-parser: 3.0.0
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.1.20(react@18.3.1)(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.1)
|
||||
swr: 2.3.2(react@18.3.1)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/ui-utils@1.1.16(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
zod-to-json-schema: 3.24.1(zod@3.24.1)
|
||||
optionalDependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@@ -3441,9 +3576,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/node@8.3.4(astro@4.16.18)':
|
||||
'@astrojs/node@8.3.4(astro@4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3))':
|
||||
dependencies:
|
||||
astro: 4.16.18(typescript@5.7.3)
|
||||
astro: 4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3)
|
||||
send: 0.19.1
|
||||
server-destroy: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3453,15 +3588,15 @@ snapshots:
|
||||
dependencies:
|
||||
prismjs: 1.29.0
|
||||
|
||||
'@astrojs/react@3.6.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@astrojs/react@3.6.3(@types/node@18.19.74)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
'@vitejs/plugin-react': 4.3.4(vite@5.4.14)
|
||||
'@vitejs/plugin-react': 4.3.4(vite@5.4.14(@types/node@18.19.74))
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
ultrahtml: 1.5.3
|
||||
vite: 5.4.14
|
||||
vite: 5.4.14(@types/node@18.19.74)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -3479,9 +3614,9 @@ snapshots:
|
||||
stream-replace-string: 2.0.0
|
||||
zod: 3.24.1
|
||||
|
||||
'@astrojs/tailwind@5.1.5(astro@4.16.18)(tailwindcss@3.4.17)':
|
||||
'@astrojs/tailwind@5.1.5(astro@4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3))(tailwindcss@3.4.17)':
|
||||
dependencies:
|
||||
astro: 4.16.18(typescript@5.7.3)
|
||||
astro: 4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3)
|
||||
autoprefixer: 10.4.20(postcss@8.5.1)
|
||||
postcss: 8.5.1
|
||||
postcss-load-config: 4.0.2(postcss@8.5.1)
|
||||
@@ -3904,6 +4039,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@microsoft/clarity@1.0.0': {}
|
||||
|
||||
'@mixmark-io/domino@2.2.0': {}
|
||||
|
||||
'@nanostores/react@0.8.4(nanostores@0.11.3)(react@18.3.1)':
|
||||
@@ -3983,6 +4120,8 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.18.0
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
@@ -3992,9 +4131,9 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.50.0
|
||||
|
||||
'@reactflow/background@11.3.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/background@11.3.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classcat: 5.0.5
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -4003,9 +4142,9 @@ snapshots:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@reactflow/controls@11.2.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/controls@11.2.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classcat: 5.0.5
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -4014,7 +4153,7 @@ snapshots:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@reactflow/core@11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/core@11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@types/d3': 7.4.3
|
||||
'@types/d3-drag': 3.0.7
|
||||
@@ -4031,9 +4170,9 @@ snapshots:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@reactflow/minimap@11.7.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/minimap@11.7.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-zoom': 3.0.8
|
||||
classcat: 5.0.5
|
||||
@@ -4046,9 +4185,9 @@ snapshots:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@reactflow/node-resizer@2.2.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/node-resizer@2.2.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classcat: 5.0.5
|
||||
d3-drag: 3.0.0
|
||||
d3-selection: 3.0.0
|
||||
@@ -4059,9 +4198,9 @@ snapshots:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@reactflow/node-toolbar@1.3.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)':
|
||||
'@reactflow/node-toolbar@1.3.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classcat: 5.0.5
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -4123,11 +4262,13 @@ snapshots:
|
||||
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
|
||||
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
|
||||
|
||||
'@rollup/pluginutils@5.1.4':
|
||||
'@rollup/pluginutils@5.1.4(rollup@4.32.0)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.2
|
||||
optionalDependencies:
|
||||
rollup: 4.32.0
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.32.0':
|
||||
optional: true
|
||||
@@ -4415,6 +4556,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/dom-to-image@2.6.7': {}
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
@@ -4503,14 +4646,14 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-react@4.3.4(vite@5.4.14)':
|
||||
'@vitejs/plugin-react@4.3.4(vite@5.4.14(@types/node@18.19.74))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.7
|
||||
'@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.2
|
||||
vite: 5.4.14
|
||||
vite: 5.4.14(@types/node@18.19.74)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4524,6 +4667,18 @@ snapshots:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
|
||||
ai@4.1.51(react@18.3.1)(zod@3.24.1):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
'@ai-sdk/react': 1.1.20(react@18.3.1)(zod@3.24.1)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.1)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
zod: 3.24.1
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -4559,7 +4714,7 @@ snapshots:
|
||||
|
||||
array-union@2.1.0: {}
|
||||
|
||||
astro@4.16.18(typescript@5.7.3):
|
||||
astro@4.16.18(@types/node@18.19.74)(rollup@4.32.0)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.10.3
|
||||
'@astrojs/internal-helpers': 0.4.1
|
||||
@@ -4569,7 +4724,7 @@ snapshots:
|
||||
'@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.7)
|
||||
'@babel/types': 7.26.7
|
||||
'@oslojs/encoding': 1.1.0
|
||||
'@rollup/pluginutils': 5.1.4
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.32.0)
|
||||
'@types/babel__core': 7.20.5
|
||||
'@types/cookie': 0.6.0
|
||||
acorn: 8.14.0
|
||||
@@ -4615,8 +4770,8 @@ snapshots:
|
||||
tsconfck: 3.1.4(typescript@5.7.3)
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
vite: 5.4.14
|
||||
vitefu: 1.0.5(vite@5.4.14)
|
||||
vite: 5.4.14(@types/node@18.19.74)
|
||||
vitefu: 1.0.5(vite@5.4.14(@types/node@18.19.74))
|
||||
which-pm: 3.0.0
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
@@ -4884,6 +5039,8 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff@5.2.0: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
@@ -5015,6 +5172,8 @@ snapshots:
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
eventsource-parser@3.0.0: {}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
@@ -5363,8 +5522,16 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
chalk: 5.4.1
|
||||
diff-match-patch: 1.0.5
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -5869,6 +6036,7 @@ snapshots:
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -5984,8 +6152,9 @@ snapshots:
|
||||
postcss-load-config@4.0.2(postcss@8.5.1):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
postcss: 8.5.1
|
||||
yaml: 2.7.0
|
||||
optionalDependencies:
|
||||
postcss: 8.5.1
|
||||
|
||||
postcss-nested@6.2.0(postcss@8.5.1):
|
||||
dependencies:
|
||||
@@ -6025,6 +6194,7 @@ snapshots:
|
||||
prettier-plugin-tailwindcss@0.6.11(prettier-plugin-astro@0.14.1)(prettier@3.4.2):
|
||||
dependencies:
|
||||
prettier: 3.4.2
|
||||
optionalDependencies:
|
||||
prettier-plugin-astro: 0.14.1
|
||||
|
||||
prettier@3.4.2: {}
|
||||
@@ -6178,7 +6348,7 @@ snapshots:
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-tooltip@5.28.0(react-dom@18.3.1)(react@18.3.1):
|
||||
react-tooltip@5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.13
|
||||
classnames: 2.5.1
|
||||
@@ -6189,14 +6359,14 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
reactflow@11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
|
||||
reactflow@11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@reactflow/background': 11.3.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/controls': 11.2.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/minimap': 11.7.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/node-resizer': 2.2.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/node-toolbar': 1.3.14(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@reactflow/background': 11.3.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@reactflow/controls': 11.2.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@reactflow/core': 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@reactflow/minimap': 11.7.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@reactflow/node-resizer': 2.2.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@reactflow/node-toolbar': 1.3.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
@@ -6414,6 +6584,8 @@ snapshots:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.6.3: {}
|
||||
@@ -6571,6 +6743,12 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.3.2(react@18.3.1):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.4.0(react@18.3.1)
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
@@ -6608,11 +6786,13 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tiptap-markdown@0.8.10(@tiptap/core@2.11.3):
|
||||
tiptap-markdown@0.8.10(@tiptap/core@2.11.3(@tiptap/pm@2.11.3)):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.3(@tiptap/pm@2.11.3)
|
||||
'@types/markdown-it': 13.0.9
|
||||
@@ -6639,7 +6819,7 @@ snapshots:
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsconfck@3.1.4(typescript@5.7.3):
|
||||
dependencies:
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
tslib@2.8.1: {}
|
||||
@@ -6753,17 +6933,18 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite@5.4.14:
|
||||
vite@5.4.14(@types/node@18.19.74):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.1
|
||||
rollup: 4.32.0
|
||||
optionalDependencies:
|
||||
'@types/node': 18.19.74
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitefu@1.0.5(vite@5.4.14):
|
||||
dependencies:
|
||||
vite: 5.4.14
|
||||
vitefu@1.0.5(vite@5.4.14(@types/node@18.19.74)):
|
||||
optionalDependencies:
|
||||
vite: 5.4.14(@types/node@18.19.74)
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
@@ -6835,8 +7016,9 @@ snapshots:
|
||||
|
||||
zustand@4.5.6(@types/react@18.3.18)(react@18.3.1):
|
||||
dependencies:
|
||||
use-sync-external-store: 1.4.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.4.0(react@18.3.1)
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
Binary file not shown.
BIN
public/pdfs/roadmaps/cloudflare.pdf
Normal file
BIN
public/pdfs/roadmaps/cloudflare.pdf
Normal file
Binary file not shown.
Binary file not shown.
@@ -42,7 +42,7 @@
|
||||
]
|
||||
},
|
||||
"mwPJh33MEUQ4Co_LiVEOb": {
|
||||
"title": "Differential Calculus",
|
||||
"title": "Differential Calculus ",
|
||||
"description": "",
|
||||
"links": [
|
||||
{
|
||||
@@ -330,17 +330,17 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Advantages and Disadvantages of AI",
|
||||
"url": "https://towardsdatascience.com/advantages-and-disadvantages-of-artificial-intelligence-182a5ef6588c",
|
||||
"url": "https://medium.com/@laners.org/advantages-and-disadvantages-of-artificial-intelligence-cd6e42819b20",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Reinforcement Learning 101",
|
||||
"url": "https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292",
|
||||
"url": "https://medium.com/towards-data-science/reinforcement-learning-101-e24b50e1d292",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Understanding AUC-ROC Curve",
|
||||
"url": "https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5",
|
||||
"url": "https://medium.com/towards-data-science/understanding-auc-roc-curve-68b2303cc9c5",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -458,8 +458,29 @@
|
||||
},
|
||||
"O7wjldZ3yTA2s_F-UnJw_": {
|
||||
"title": "Rate Limiting",
|
||||
"description": "Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.",
|
||||
"links": []
|
||||
"description": "Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Rate limit",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Rate_limit",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Throttle",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Throttle",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Debounce",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Debounce",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is rate limiting? | Rate limiting and bots",
|
||||
"url": "https://www.cloudflare.com/en-gb/learning/bots/what-is-rate-limiting/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"20KEgZH6cu_UokqWpV-9I": {
|
||||
"title": "Idempotency",
|
||||
@@ -569,6 +590,11 @@
|
||||
"title": "Caching REST API Response",
|
||||
"url": "https://restfulapi.net/caching/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "HTTP caching",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -709,7 +735,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "API Authorization Methods",
|
||||
"url": "https://konghq.com/blog/engineering/common-api-authentication-methods",
|
||||
"url": "https://www.pingidentity.com/en/resources/identity-fundamentals/authorization/authorization-methods.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
2875
public/roadmap-content/aspnet-core.json
Normal file
2875
public/roadmap-content/aspnet-core.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -629,7 +629,7 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "MySQL Full Course for free",
|
||||
"title": "MySQL Complete Course",
|
||||
"url": "https://www.youtube.com/watch?v=5OdVJbNCSso",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -1690,8 +1690,8 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Architectural Patterns in a nutshell",
|
||||
"url": "https://towardsdatascience.com/10-common-software-architectural-patterns-in-a-nutshell-a0b47a1e9013",
|
||||
"title": "10 Common Software Architectural Patterns in a nutshell",
|
||||
"url": "https://theiotacademy.medium.com/10-common-software-architectural-patterns-in-a-nutshell-1b1f6cf5036b",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
1643
public/roadmap-content/cloudflare.json
Normal file
1643
public/roadmap-content/cloudflare.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,12 +55,12 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Python Website",
|
||||
"title": "Python",
|
||||
"url": "https://www.python.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Python Getting Started",
|
||||
"title": "Getting Started with Python",
|
||||
"url": "https://www.python.org/about/gettingstarted/",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -182,6 +182,11 @@
|
||||
"title": "Rust",
|
||||
"description": "Rust is a modern systems programming language focusing on safety, speed, and concurrency. It accomplishes these goals by being memory safe without using garbage collection.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Visit Dedicated Rust Roadmap",
|
||||
"url": "https://roadmap.sh/rust",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "The Rust Programming Language - online book",
|
||||
"url": "https://doc.rust-lang.org/book/",
|
||||
@@ -213,6 +218,11 @@
|
||||
"title": "C++",
|
||||
"description": "C++ is a powerful general-purpose programming language. It can be used to develop operating systems, browsers, games, and so on. C++ supports different ways of programming like procedural, object-oriented, functional, and so on. This makes C++ powerful as well as flexible.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "C++ Roadmap",
|
||||
"url": "https://roadmap.sh/cpp",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Learn C++",
|
||||
"url": "https://learncpp.com/",
|
||||
@@ -238,11 +248,6 @@
|
||||
"url": "https://www.w3schools.com/cpp/default.asp",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "C++ Roadmap",
|
||||
"url": "https://roadmap.sh/cpp",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about C++ Programming",
|
||||
"url": "https://app.daily.dev/tags/c++?ref=roadmapsh",
|
||||
@@ -326,6 +331,11 @@
|
||||
"url": "https://techdevguide.withgoogle.com/paths/data-structures-and-algorithms/",
|
||||
"type": "course"
|
||||
},
|
||||
{
|
||||
"title": "Visit Dedicated DSA Roadmap",
|
||||
"url": "https://roadmap.sh/datastructures-and-algorithms",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Data Structures and Algorithms",
|
||||
"url": "https://www.javatpoint.com/data-structure-tutorial",
|
||||
@@ -645,6 +655,11 @@
|
||||
"title": "Complete Binary Tree - Programiz",
|
||||
"url": "https://www.programiz.com/dsa/complete-binary-tree",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Complete Binary Trees",
|
||||
"url": "https://www.wscubetech.com/resources/dsa/complete-binary-tree",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -771,8 +786,14 @@
|
||||
},
|
||||
"HZ1kk0TQ13FLC9t13BZl5": {
|
||||
"title": "Adjacency Matrix",
|
||||
"description": "",
|
||||
"links": []
|
||||
"description": "An adjacency matrix is a square matrix used to represent a finite graph. It is used to represent the connections between vertices in a graph. The matrix is filled with 0s and 1s, where a 1 represents a connection between two vertices and a 0 represents no connection.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Adjacency Matrix",
|
||||
"url": "https://en.wikipedia.org/wiki/Adjacency_matrix",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rTnKJcPniUtqvfOyC88N0": {
|
||||
"title": "Adjacency List",
|
||||
@@ -1017,8 +1038,14 @@
|
||||
},
|
||||
"7a6-AnBI-3tAU1dkOvPkx": {
|
||||
"title": "Common Algorithms",
|
||||
"description": "Here are some common algorithms that you should know. You can find more information about them in the [Algorithms](https://www.khanacademy.org/computing/computer-science/algorithms) section of the Computer Science course.\n\n* Sorting\n* Recursion\n* Searching\n* Cache Algorithms\n* Tree Algorithms\n* Graph Algorithms\n* Greedy Algorithms\n* Backtracking\n* Substring Search\n* Suffix Arrays\n* Dynamic Programming",
|
||||
"links": []
|
||||
"description": "Here are some common algorithms that you should know. You can find more information about them in the Algorithms section of the Computer Science course.\n\n* Sorting\n* Recursion\n* Searching\n* Cache Algorithms\n* Tree Algorithms\n* Graph Algorithms\n* Greedy Algorithms\n* Backtracking\n* Substring Search\n* Suffix Arrays\n* Dynamic Programming\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Top Algorithms and Data Structures",
|
||||
"url": "https://towardsdatascience.com/top-algorithms-and-data-structures-you-really-need-to-know-ab9a2a91c7b5",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"0_qNhprnXU3i8koW3XTdD": {
|
||||
"title": "Tail Recursion",
|
||||
@@ -1143,20 +1170,26 @@
|
||||
"XwyqBK9rgP1MMcJrdIzm5": {
|
||||
"title": "Linear Search",
|
||||
"description": "Linear search is a very simple algorithm that is used to search for a value in an array. It sequentially checks each element of the array until a match is found or until all the elements have been searched.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "Linear Search",
|
||||
"url": "https://www.programiz.com/dsa/linear-search",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"4wGBYFZpcdTt97WTbSazx": {
|
||||
"title": "Bubble Sort",
|
||||
"description": "Bubble sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order. The pass through the list is repeated until the list is sorted.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Bubble Sort",
|
||||
"url": "https://www.youtube.com/watch?v=P00xJgWzz2c&index=1&list=PL89B61F78B552C1AB",
|
||||
"type": "video"
|
||||
"title": "Bubble Sort Algorithm",
|
||||
"url": "https://www.programiz.com/dsa/bubble-sort",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Analyzing Bubble Sort",
|
||||
"url": "https://www.youtube.com/watch?v=ni_zk257Nqo&index=7&list=PL89B61F78B552C1AB",
|
||||
"title": "Bubble Sort",
|
||||
"url": "https://www.youtube.com/watch?v=P00xJgWzz2c&index=1&list=PL89B61F78B552C1AB",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
@@ -1375,7 +1408,13 @@
|
||||
"Yf5gOIe7oiL19MjEVcpdw": {
|
||||
"title": "Depth First Search",
|
||||
"description": "Depth first search is a graph traversal algorithm that starts at a root node and explores as far as possible along each branch before backtracking.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "Depth-first Search",
|
||||
"url": "https://en.wikipedia.org/wiki/Depth-first_search",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eY4nK2lPYsrR-a_8y2sao": {
|
||||
"title": "Bellman Ford's Algorithm",
|
||||
@@ -1442,8 +1481,13 @@
|
||||
},
|
||||
"aBjBHpq_OajgQjxdCobXD": {
|
||||
"title": "Finding Hamiltonian Paths",
|
||||
"description": "Hamiltonian paths are paths that visit every node in a graph exactly once. They are named after the famous mathematician [Hamilton](https://en.wikipedia.org/wiki/William_Rowan_Hamilton). Hamiltonian paths are a special case of [Hamiltonian cycles](https://en.wikipedia.org/wiki/Hamiltonian_cycle), which are cycles that visit every node in a graph exactly once.\n\nVisit the following resources to learn more:",
|
||||
"description": "Hamiltonian paths are paths that visit every node in a graph exactly once. They are named after the famous mathematician Hamilton. Hamiltonian paths are a special case of Hamiltonian cycles, which are cycles that visit every node in a graph exactly once.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Hamiltonian Cycles",
|
||||
"url": "https://en.wikipedia.org/wiki/Hamiltonian_cycle",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Hamiltonian Path",
|
||||
"url": "https://www.hackerearth.com/practice/algorithms/graphs/hamiltonian-path/tutorial/",
|
||||
@@ -1967,13 +2011,13 @@
|
||||
"description": "Class Diagrams are used to model the static structure of a system. They are used to show the classes, their attributes, operations (or methods), and the relationships between objects.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "UML Class Diagram Tutorial",
|
||||
"url": "https://www.youtube.com/watch?v=UI6lqHOVHic",
|
||||
"type": "video"
|
||||
"title": "Class Diagrams",
|
||||
"url": "https://www.visual-paradigm.com/guide/uml-unified-modeling-language/uml-class-diagram-tutorial/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "UML Class Diagram Tutorial",
|
||||
"url": "https://www.youtube.com/watch?v=3cmzqZzwNDM&list=PLfoY2ARMh0hC2FcJKP5voAKCpk6PZXSd5&index=2",
|
||||
"url": "https://www.youtube.com/watch?v=UI6lqHOVHic",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
@@ -2354,26 +2398,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2425,26 +2449,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2496,26 +2500,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2567,26 +2551,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2643,26 +2607,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2941,11 +2885,6 @@
|
||||
"url": "https://www.youtube.com/watch?v=svfnVhJOfMc&index=8&list=PLA5Lqm4uh9Bbq-E0ZnqTIa8LRaL77ica6",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "MIT 6.851 - Memory Hierarchy Models",
|
||||
"url": "https://www.youtube.com/watch?v=V3omVLzI0WE&index=7&list=PLUl4u3cNGP61hsJNdULdudlRL493b-XZf",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "B-Trees (playlist) in 26 minutes",
|
||||
"url": "https://www.youtube.com/playlist?list=PL9xmBV_5YoZNFPPv98DjTdD9X6UI9KMHz",
|
||||
@@ -3057,6 +2996,11 @@
|
||||
"title": "CDN",
|
||||
"description": "A CDN is a network of servers that are distributed geographically. The servers are connected to each other and to the internet. The servers are used to deliver content to users. The content is delivered to the user from the server that is closest to the user. This is done to reduce latency and improve the performance of the content delivery.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is a CDN?",
|
||||
"url": "https://www.cloudflare.com/learning/cdn/what-is-a-cdn/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Content Delivery Network (CDN) - System Design",
|
||||
"url": "https://dev.to/karanpratapsingh/system-design-the-complete-course-10fo#content-delivery-network-cdn",
|
||||
@@ -3163,6 +3107,11 @@
|
||||
"title": "GraphQL",
|
||||
"description": "GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Visit Dedicated GraphQL Roadmap",
|
||||
"url": "https://roadmap.sh/graphql",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Apollo GraphQL Tutorials",
|
||||
"url": "https://www.apollographql.com/tutorials/",
|
||||
@@ -3227,7 +3176,7 @@
|
||||
"description": "Long polling is a technique used to implement server push functionality over HTTP. It is a method of opening a request on the server and keeping it open until an event occurs, at which point the server responds. This is in contrast to a regular HTTP request, where the server responds immediately with whatever data is available at the time.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Long polling",
|
||||
"title": "Long Polling",
|
||||
"url": "https://javascript.info/long-polling",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -3252,7 +3201,18 @@
|
||||
"bVjI14VismTHNCyA0mEBP": {
|
||||
"title": "Web Sockets",
|
||||
"description": "Web sockets are a bidirectional communication protocol between a client and a server. They are used for real-time applications like chat, multiplayer games, and live data updates. Web sockets are also used to establish a connection between a server and a client. This connection is then used to send data in both directions.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "WebSockets",
|
||||
"url": "https://en.wikipedia.org/wiki/WebSocket",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Web Sockets API",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"JckRqZA8C6IqQLPpTCgf4": {
|
||||
"title": "SSE",
|
||||
@@ -3280,12 +3240,12 @@
|
||||
"description": "A database is a collection of useful data of one or more related organizations structured in a way to make data an asset to the organization. A database management system is a software designed to assist in maintaining and extracting large collections of data in a timely fashion.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Oracle: What is a Database?",
|
||||
"title": "What is a Database?",
|
||||
"url": "https://www.oracle.com/database/what-is-database/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Prisma.io: What are Databases?",
|
||||
"title": "What are Databases?",
|
||||
"url": "https://www.prisma.io/dataguide/intro/what-are-databases",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -3362,7 +3322,13 @@
|
||||
"ii1vF74u3yrFNlw_21b3B": {
|
||||
"title": "DDL",
|
||||
"description": "DDL or Data Definition Language actually consists of the SQL commands that can be used to define the database schema. It simply deals with descriptions of the database schema and is used to create and modify the structure of database objects in the database. DDL is a set of SQL commands used to create, modify, and delete database structures but not data. These commands are normally not used by a general user, who should be accessing the database via an application.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "DDL",
|
||||
"url": "https://en.wikipedia.org/wiki/Data_definition_language",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tcQSH-eAvJUZuePTDjAIb": {
|
||||
"title": "DML",
|
||||
@@ -3383,12 +3349,29 @@
|
||||
"05lkb3B86Won7Rkf-8DeD": {
|
||||
"title": "DQL",
|
||||
"description": "DQL statements are used for performing queries on the data within schema objects. The purpose of the DQL Command is to get some schema relation based on the query passed to it. We can define DQL as follows it is a component of SQL statement that allows getting data from the database and imposing order upon it. It includes the SELECT statement. This command allows getting the data out of the database to perform operations with it. When a SELECT is fired against a table or tables the result is compiled into a further temporary table, which is displayed or perhaps received by the program i.e. a front-end.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "Data Query Language",
|
||||
"url": "https://en.wikipedia.org/wiki/Data_query_language",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"4bUmfuP2qgcli8I2Vm9zh": {
|
||||
"title": "DCL",
|
||||
"description": "DCL includes commands such as GRANT and REVOKE which mainly deal with the rights, permissions, and other controls of the database system.\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "DCL",
|
||||
"url": "https://en.wikipedia.org/wiki/Data_Control_Language",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "DCL Commands",
|
||||
"url": "https://www.geeksforgeeks.org/sql-ddl-dql-dml-dcl-tcl-commands/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"_sm63rZNKoibVndeNgOpW": {
|
||||
"title": "Locking",
|
||||
@@ -3435,7 +3418,13 @@
|
||||
"q3nRhTYS5wg9tYnQe2sCF": {
|
||||
"title": "BASE",
|
||||
"description": "The rise in popularity of NoSQL databases provided a flexible and fluidity with ease to manipulate data and as a result, a new database model was designed, reflecting these properties. The acronym BASE is slightly more confusing than ACID but however, the words behind it suggest ways in which the BASE model is different and acronym BASE stands for:-\n\n* **B**asically **A**vailable\n* **S**oft state\n* **E**ventual consistency\n\nVisit the following resources to learn more:",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "BASE Model vs. ACID Model",
|
||||
"url": "https://www.geeksforgeeks.org/acid-model-vs-base-model-for-database/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uqfeiQ9K--QkGNwks4kjk": {
|
||||
"title": "CAP Theorem",
|
||||
@@ -3804,7 +3793,7 @@
|
||||
"description": "Public-key cryptography, or asymmetric cryptography, is the field of cryptographic systems that use pairs of related keys. Each key pair consists of a public key and a corresponding private key. Key pairs are generated with cryptographic algorithms based on mathematical problems termed one-way functions.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Public-key cryptography - Wikipedia",
|
||||
"title": "Public-key Cryptography",
|
||||
"url": "https://en.wikipedia.org/wiki/Public-key_cryptography",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -3877,7 +3866,12 @@
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "Wikipedia - OWASP",
|
||||
"title": "OWASP",
|
||||
"url": "https://owasp.org/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "OWASP - Wiki",
|
||||
"url": "https://en.wikipedia.org/wiki/OWASP",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -3926,7 +3920,7 @@
|
||||
},
|
||||
"1eglba39q426Nh0E0qcdj": {
|
||||
"title": "How CPU Executes Programs",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"description": "The CPU executes programs by repeatedly fetching instructions from memory, decoding them to understand the operation, and then executing those operations. This cycle, called the fetch-decode-execute cycle, continues for each instruction in the program, with the CPU using registers for temporary storage and a program counter to keep track of the next instruction. Modern CPUs use techniques like pipelining and caches to speed up this process, enabling them to execute complex programs efficiently.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Explore top posts about Computing",
|
||||
@@ -3934,7 +3928,7 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How CPU executes a program",
|
||||
"title": "How CPU Executes a Program",
|
||||
"url": "https://www.youtube.com/watch?v=XM4lGflQFvA",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -3964,8 +3958,13 @@
|
||||
},
|
||||
"AxiGqbteK7ZSXEUt_zckH": {
|
||||
"title": "Instructions and Programs",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"description": "Instructions are the most basic commands a CPU can understand, directing it to perform specific actions like adding numbers or moving data. A program, on the other hand, is a collection of these instructions, organized in a sequence to accomplish a particular task. Think of instructions as individual words and a program as a complete sentence or story; the CPU executes these instructions one by one, following the program's logic, to achieve the desired outcome.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Instruction and Programs",
|
||||
"url": "https://nerdfighteria.info/v/zltgXvg6r3k/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Instructions and Programs",
|
||||
"url": "https://youtu.be/zltgXvg6r3k",
|
||||
@@ -3975,8 +3974,13 @@
|
||||
},
|
||||
"DjTQjMbika4_yTzrBpcmB": {
|
||||
"title": "CPU Cache",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"description": "A CPU cache is a hardware cache used by the central processing unit of a computer to reduce the average cost to access data from the main memory. A cache is a smaller, faster memory, located closer to a processor core, which stores copies of the data from frequently used main memory locations.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is CPU Cache",
|
||||
"url": "https://www.howtogeek.com/854138/what-is-cpu-cache/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about Computing",
|
||||
"url": "https://app.daily.dev/tags/computing?ref=roadmapsh",
|
||||
@@ -4070,10 +4074,10 @@
|
||||
},
|
||||
"xUo5Ox_HTgGyeQMDIkVyK": {
|
||||
"title": "Concurrency in Multiple Cores",
|
||||
"description": "Visit the following resources to learn more:",
|
||||
"description": "Concurrency or Parallelism is simultaneous execution of processes on a multiple cores per CPU or multiple CPUs (on a single motherboard). Concurrency is when Parallelism is achieved on a single core/CPU by using scheduling algorithms that divides the CPU's time (time-slice).\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "What is the difference between multicore and concurrent programming?",
|
||||
"title": "Difference between Multi-core and concurrent Programming?",
|
||||
"url": "https://stackoverflow.com/questions/5372861/what-is-the-difference-between-multicore-and-concurrent-programming",
|
||||
"type": "article"
|
||||
},
|
||||
@@ -4081,12 +4085,17 @@
|
||||
"title": "Concurrency in Multicore systems",
|
||||
"url": "https://cs.stackexchange.com/questions/140793/concurrency-in-multiple-core",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Mastering Concurrency",
|
||||
"url": "https://www.harrisonclarke.com/blog/mastering-concurrency-a-guide-for-software-engineers",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Ge2nagN86ofa2y-yYR1lv": {
|
||||
"title": "Scheduling Algorithms",
|
||||
"description": "CPU Scheduling is the process of selecting a process from the ready queue and allocating the CPU to it. The selection of a process is based on a particular scheduling algorithm. The scheduling algorithm is chosen depending on the type of system and the requirements of the processes.\n\nHere is the list of some of the most commonly used scheduling algorithms:\n\n* **First Come First Serve (FCFS):** The process that arrives first is allocated the CPU first. It is a non-preemptive algorithm.\n* **Shortest Job First (SJF):** The process with the smallest execution time is allocated the CPU first. It is a non-preemptive algorithm.\n* **Shortest Remaining Time First (SRTF):** The process with the smallest remaining execution time is allocated the CPU first. It is a preemptive algorithm.\n* **Round Robin (RR):** The process is allocated the CPU for a fixed time slice. The time slice is usually 10 milliseconds. It is a preemptive algorithm.\n* **Priority Scheduling:** The process with the highest priority is allocated the CPU first. It is a preemptive algorithm.\n* **Multi-level Queue Scheduling:** The processes are divided into different queues based on their priority. The process with the highest priority is allocated the CPU first. It is a preemptive algorithm.\n* **Multi-level Feedback Queue Scheduling:** The processes are divided into different queues based on their priority. The process with the highest priority is allocated the CPU first. If a process is preempted, it is moved to the next queue. It is a preemptive algorithm.\n* **Highest Response Ratio Next(HRRN):** CPU is allotted to the next process which has the highest response ratio and not to the process having less burst time. It is a Non-Preemptive algorithm.\n* **Lottery Scheduling:** The process is allocated the CPU based on a lottery system. It is a preemptive algorithm.\n\nVisit the following resources to learn more :",
|
||||
"description": "CPU Scheduling is the process of selecting a process from the ready queue and allocating the CPU to it. The selection of a process is based on a particular scheduling algorithm. The scheduling algorithm is chosen depending on the type of system and the requirements of the processes.\n\nHere is the list of some of the most commonly used scheduling algorithms:\n\n* **First Come First Serve (FCFS):** The process that arrives first is allocated the CPU first. It is a non-preemptive algorithm.\n* **Shortest Job First (SJF):** The process with the smallest execution time is allocated the CPU first. It is a non-preemptive algorithm.\n* **Shortest Remaining Time First (SRTF):** The process with the smallest remaining execution time is allocated the CPU first. It is a preemptive algorithm.\n* **Round Robin (RR):** The process is allocated the CPU for a fixed time slice. The time slice is usually 10 milliseconds. It is a preemptive algorithm.\n* **Priority Scheduling:** The process with the highest priority is allocated the CPU first. It is a preemptive algorithm.\n* **Multi-level Queue Scheduling:** The processes are divided into different queues based on their priority. The process with the highest priority is allocated the CPU first. It is a preemptive algorithm.\n* **Multi-level Feedback Queue Scheduling:** The processes are divided into different queues based on their priority. The process with the highest priority is allocated the CPU first. If a process is preempted, it is moved to the next queue. It is a preemptive algorithm.\n* **Highest Response Ratio Next(HRRN):** CPU is allotted to the next process which has the highest response ratio and not to the process having less burst time. It is a Non-Preemptive algorithm.\n* **Lottery Scheduling:** The process is allocated the CPU based on a lottery system. It is a preemptive algorithm.\n\nVisit the following resources to learn more",
|
||||
"links": [
|
||||
{
|
||||
"title": "CPU Scheduling in Operating Systems - geeksforgeeks",
|
||||
@@ -4120,7 +4129,7 @@
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Video on Interrupts",
|
||||
"title": "Interrupts",
|
||||
"url": "https://youtu.be/iKlAWIKEyuw",
|
||||
"type": "video"
|
||||
}
|
||||
@@ -4222,26 +4231,6 @@
|
||||
"title": "NP Completeness IV",
|
||||
"url": "https://www.youtube.com/watch?v=NKLDp3Rch3M&list=PLFDnELG9dpVxQCxuD-9BSy2E7BWY3t5Sm&index=18",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 23 - NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=ItHp5laE1VE&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=23",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 24 - Satisfiability",
|
||||
"url": "https://www.youtube.com/watch?v=inaFJeCzGxU&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=24",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 25 - More NP-Completeness",
|
||||
"url": "https://www.youtube.com/watch?v=B-bhKxjZLlc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=25",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "CSE373 2020 - Lecture 26 - NP-Completeness Challenge",
|
||||
"url": "https://www.youtube.com/watch?v=_EzetTkG_Cc&list=PLOtl7M3yp-DX6ic0HGT0PUX_wiNmkWkXx&index=26",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"title": "Introduction to Data Analytics",
|
||||
"url": "https://www.coursera.org/learn/introduction-to-data-analytics",
|
||||
"type": "article"
|
||||
"type": "course"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -265,7 +265,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Replace Function",
|
||||
"url": "https://support.microsoft.com/en-us/office/replace-function-6acf209b-01b7-4078-b4b8-e0a4ef67d181",
|
||||
"url": "https://support.microsoft.com/en-us/office/replace-function-8d799074-2425-4a8a-84bc-82472868878a",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
@@ -670,7 +670,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Outliers",
|
||||
"url": "%5Bhttps://www.mathsisfun.com/data/outliers.html",
|
||||
"url": "https://www.mathsisfun.com/data/outliers.html",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3026,14 +3026,14 @@
|
||||
"title": "Envoy",
|
||||
"description": "Originally created at Lyft, Envoy is a high-performance data plane designed for service mesh architectures. Lyft open sourced it and donated it to the CNCF, where it is now one of the CNCF’s graduated open source projects. Envoy is a self contained process that is designed to run alongside every application server. All of the Envoys form a transparent communication mesh in which each application sends and receives messages to and from localhost and is unaware of the network topology.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Envoy Website",
|
||||
"url": "https://www.envoyproxy.io/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "envoyproxy/envoy",
|
||||
"url": "https://github.com/envoyproxy/envoy",
|
||||
"type": "opensource"
|
||||
},
|
||||
{
|
||||
"title": "Envoy",
|
||||
"url": "https://www.envoyproxy.io/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -162,8 +162,14 @@
|
||||
},
|
||||
"7PBmYoSmIgZT21a2Ip3_S": {
|
||||
"title": "Trust / Influence Building",
|
||||
"description": "Building trust and influence is crucial for any Engineering Manager. This involves establishing a solid reputation, delivering on promises and being an active listener to your team's ideas and issues. It's a manager's job to ensure there's an open, honest environment that promotes trust. Balancing delegation and taking charge, especially in difficult situations, is key to building influence.\n\nOne challenge in this area is building trust between team members of varying experiences and skills. Managers must not only show the team they're competent, but also that they value everyone's inputs. They can achieve this by promoting inclusivity and praising team contributions regularly.\n\nBeing patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.",
|
||||
"links": []
|
||||
"description": "Building trust and influence is crucial for any Engineering Manager. This involves establishing a solid reputation, delivering on promises and being an active listener to your team's ideas and issues. It's a manager's job to ensure there's an open, honest environment that promotes trust. Balancing delegation and taking charge, especially in difficult situations, is key to building influence.\n\nOne challenge in this area is building trust between team members of varying experiences and skills. Managers must not only show the team they're competent, but also that they value everyone's inputs. They can achieve this by promoting inclusivity and praising team contributions regularly.\n\nBeing patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Understanding The Trust Equation",
|
||||
"url": "https://trustedadvisor.com/why-trust-matters/understanding-trust/understanding-the-trust-equation",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"b3qoH_LuW-Gz4N8WdGnZs": {
|
||||
"title": "One-on-One Meetings",
|
||||
|
||||
@@ -364,8 +364,13 @@
|
||||
"description": "Joints in game development primarily refer to the connections between two objects, often used in the context of physics simulations and character animations. These might simulate the physics of real-world joints like hinges or springs. Developers can control various characteristics of joints such as their constraints, forces, and reactions. The different types come with various properties suitable for specific needs. For example, Fixed joints keep objects together, Hinge joints allow rotation around an axis, and Spring joints apply a force to keep objects apart.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Game Character Rigging Fundamentals",
|
||||
"url": "https://learn.unity.com/project/game-character-rigging-fundamentals",
|
||||
"title": "Introduction to joints",
|
||||
"url": "https://docs.unity3d.com/Manual/Joints.html",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Character Rigging for Video Games",
|
||||
"url": "https://game-ace.com/blog/character-rigging-for-video-games/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
@@ -599,16 +604,16 @@
|
||||
},
|
||||
"vWLKYK2KUzV1fO-vQunzW": {
|
||||
"title": "EPA",
|
||||
"description": "The **EPA**, also known as the _Environmental Protection Agency_, is not typically related to game development or the concept of intersection within this context. However, in game development, EPA might refer to an 'Event-driven Process chain Architecture' or some other game-specific acronym. In this domain, different terminologies and acronyms are often used to express complex architectures, designs, or functionalities. If you have encountered EPA in a game development context, it might be best to refer to the specific documentation or guide where it was described for a better understanding. Understanding the context is key to untangle the meaning of such abbreviations.\n\nVisit the following resources to learn more:",
|
||||
"description": "The **EPA** (Expanding Polytope Algorithm) is an iterative algorithm used for calculating the penetration depth between two shapes in collision detection. It is commonly used in physics engines and robotics. The algorithm takes the resulting simplex from a previously applied GJK algorithm, iteratively expanding the polytope towards the Minkowski Difference boundary until it finds the closest point to the origin. The vector from that point to the origin is the penetration vector and its magnitude is equal to the penetration depth between the two shapes.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Environmental Sustainability in Game Development",
|
||||
"url": "https://polydin.com/environmental-sustainability-in-game-development/",
|
||||
"title": "EPA: Collision response algorithm for 2D/3D - winter.dev",
|
||||
"url": "https://winter.dev/articles/epa-algorithm",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Gaming Sustainability - Microsoft Game Dev",
|
||||
"url": "https://learn.microsoft.com/en-us/gaming/sustainability/sustainability-overview",
|
||||
"title": "EPA (Expanding Polytope Algorithm) - dyn4j",
|
||||
"url": "https://dyn4j.org/2010/05/epa-expanding-polytope-algorithm/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -744,7 +749,7 @@
|
||||
},
|
||||
"7OffO2mBmfBKqPBTZ9ngI": {
|
||||
"title": "Godot",
|
||||
"description": "Godot is an open-source, multi-platform game engine that is known for being feature-rich and user-friendly. It is developed by hundreds of contributors from around the world and supports the creation of both 2D and 3D games. Godot uses its own scripting language, GDScript, which is similar to Python, but it also supports C# and visual scripting. It is equipped with a unique scene system and comes with a multitude of tools that can expedite the development process. Godot's design philosophy centers around flexibility, extensibility, and ease of use, providing a handy tool for both beginners and pros in game development.\n\nVisit the following resources to learn more:",
|
||||
"description": "Godot is an open-source, multi-platform game engine that is known for being feature-rich and user-friendly. It is developed by hundreds of contributors from around the world and supports the creation of both 2D and 3D games. Godot uses its own scripting language, GDScript, which is similar to Python, but it also supports C#. It is equipped with a unique scene system and comes with a multitude of tools that can expedite the development process. Godot's design philosophy centers around flexibility, extensibility, and ease of use, providing a handy tool for both beginners and pros in game development.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "godotengine/godot",
|
||||
@@ -763,7 +768,17 @@
|
||||
},
|
||||
{
|
||||
"title": "Godot in 100 Seconds",
|
||||
"url": "https://m.youtube.com/watch?v=QKgTZWbwD1U",
|
||||
"url": "https://www.youtube.com/watch?v=QKgTZWbwD1U",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "Tutorial - How to make a Video Game in Godot",
|
||||
"url": "https://www.youtube.com/watch?v=LOhfqjmasi0",
|
||||
"type": "video"
|
||||
},
|
||||
{
|
||||
"title": "Tutorial - How to make 3D Games in Godot",
|
||||
"url": "https://www.youtube.com/watch?v=ke5KpqcoiIU",
|
||||
"type": "video"
|
||||
}
|
||||
]
|
||||
@@ -882,8 +897,8 @@
|
||||
"description": "**C** and **C++ (commonly known as CPP)** are two of the most foundational high-level programming languages in computer science. **C** was developed in the 1970s and it is a procedural language, meaning it follows a step-by-step approach. Its fundamental principles include structured programming and lexical variable scope.\n\nOn the other hand, **C++** follows the paradigm of both procedural and object-oriented programming. It was developed as an extension to C to add the concept of \"classes\" - a core feature of object-oriented programming. C++ enhances C by introducing new features like function overloading, exception handling, and templates.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "The C Programming Language",
|
||||
"url": "https://www.iso.org/standard/74528.html",
|
||||
"title": "C Programming Language",
|
||||
"url": "https://en.wikipedia.org/wiki/C_%28programming_language%29",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1440,7 +1440,7 @@
|
||||
},
|
||||
"uxqJzQFRcALqatNRIWR0w": {
|
||||
"title": "Unstaged Changes",
|
||||
"description": "For changes that are not yet staged with `git add`, such as untracked new files or modified existing ones, use `git diff --unified`. This command compares your working directory against the latest committed version of each file. It's a useful tool for reviewing any local modifications before deciding whether to stage them for future commits.",
|
||||
"description": "For changes that are not yet staged with `git add`, such as untracked new files or modified existing ones , use `git diff`. This command compares your working directory (your current changes) against the staging area (changes already staged with `git add`). It’s a useful tool for reviewing local modifications before deciding whether to stage them for future commits.\n\nThe `--unified` option (or -U) controls the number of context lines shown in the diff output. By default, Git shows 3 lines of context around each change. For example, `git diff --unified=5` will display 5 lines of context around each change, making it easier to understand the surrounding code or content.",
|
||||
"links": [
|
||||
{
|
||||
"title": "What are unstaged changes in GitHub?",
|
||||
|
||||
1591
public/roadmap-content/java.json
Normal file
1591
public/roadmap-content/java.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1023,16 +1023,16 @@
|
||||
},
|
||||
"XteNExIZN3_g95_dPCopY": {
|
||||
"title": "Exitting / Exit Codes",
|
||||
"description": "`Exiting` is a way of terminating a Node.js process by using node.js process module.\n\nVisit the following resources to learn more:",
|
||||
"description": "Exiting is a way of terminating a Node.js process by using node.js process module.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Node.js Docs on exit",
|
||||
"url": "https://nodejs.org/docs/latest/api/process.html",
|
||||
"title": "Exit Documentation",
|
||||
"url": "https://nodejs.org/api/process.html#event-exit",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "How to Exit a Process in Node.js",
|
||||
"url": "https://www.knowledgehut.com/blog/web-development/node-js-process-exit",
|
||||
"url": "https://betterstack.com/community/questions/how-to-exit-in-node-js/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -319,8 +319,13 @@
|
||||
"description": "The Null Safe Operator is a handy feature in PHP which deals with an issue that often pops up when working with objects: trying to access properties or methods on an object that might be null. Instead of a fatal error, the PHP Null Safe Operator (indicated by ?->) allows null values to be returned safely, making your code more robust. Here's a quick example, consider $session?->user?->name. If $session or user is null, PHP will stop further execution and simply return null. This makes PHP more resilient when processing unpredictable data.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Null Safe Operator",
|
||||
"url": "https://www.php.net/manual/en/language.oop5.nullsafe.php",
|
||||
"title": "The Basics - Manual",
|
||||
"url": "https://www.php.net/manual/en/language.oop5.basic.php",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "PHP RFC: Nullsafe operator",
|
||||
"url": "https://wiki.php.net/rfc/nullsafe_operator",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -393,7 +398,7 @@
|
||||
},
|
||||
"RkNjYva8o_jXp9suz5YdG": {
|
||||
"title": "Named Arguments",
|
||||
"description": "Named arguments in PHP, introduced with PHP 8.0, allow you to specify the values of required parameters by their names, instead of their position in the function call, thus making your code more readable, reducing mistakes, and allowing for unimportant arguments to be skipped. Here's an array\\_fill() function using named arguments:\n\n <?php\n $a = array_fill(start_index: 0, num: 100, value: 50);\n \n\nIn this code snippet, the parameters are passed by their names ('start\\_index', 'num', 'value'), not by their order in the function definition.\n\nVisit the following resources to learn more:",
|
||||
"description": "Named arguments in PHP, introduced with PHP 8.0, allow you to specify the values of required parameters by their names, instead of their position in the function call, thus making your code more readable, reducing mistakes, and allowing for unimportant arguments to be skipped. Here's an array\\_fill() function using named arguments:\n\n <?php\n $a = array_fill(start_index: 0, count: 100, value: 50);\n \n\nIn this code snippet, the parameters are passed by their names ('start\\_index', 'count', 'value'), not by their order in the function definition.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Named Arguments",
|
||||
@@ -1015,12 +1020,12 @@
|
||||
"description": "Symfony is a set of PHP components and a framework for web projects. It aims to speed up the creation and maintenance of web applications and replace the recurring coding tasks. Symfony uses Composer, a PHP dependency manager, to manage its components. Below is an example of creating a new Symfony project:\n\n composer create-project symfony/website-skeleton myproject\n \n\nThis will download and install a new Symfony project in the 'myproject' directory. Symfony's components are reusable PHP libraries that will help you complete tasks, like routing, templating, or even creating form handling.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Symphony",
|
||||
"title": "Symfony",
|
||||
"url": "https://symfony.com/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Symphony Documentation",
|
||||
"title": "Symfony Documentation",
|
||||
"url": "https://symfony.com/doc/current/index.html",
|
||||
"type": "article"
|
||||
}
|
||||
|
||||
@@ -553,6 +553,11 @@
|
||||
"title": "Modules in Python",
|
||||
"url": "https://www.programiz.com/python-programming/modules",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Python Modules and Packages",
|
||||
"url": "https://realpython.com/python-modules-packages/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -647,7 +652,7 @@
|
||||
},
|
||||
"P_Di-XPSDITmU3xKQew8G": {
|
||||
"title": "Object Oriented Programming",
|
||||
"description": "In Python, object-oriented Programming (OOPs) is a programming paradigm that uses objects and classes in programming. It aims to implement real-world entities like inheritance, polymorphisms, encapsulation, etc. in the programming. The main concept of OOPs is to bind the data and the functions that work on that together as a single unit so that no other part of the code can access this data.\n\nVisit the following resources to learn more:",
|
||||
"description": "In Python, object-oriented Programming (OOPs) is a programming paradigm that uses objects and classes in programming. It aims to implement real-world entities like inheritance, polymorphism, encapsulation, etc., in programming. The main concept of OOPs is to bind the data and the functions that work on that together as a single unit so that no other part of the code can access this data.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Object Oriented Programming in Python",
|
||||
@@ -710,7 +715,7 @@
|
||||
},
|
||||
"zAS4YiEJ6VPsyABrkIG8i": {
|
||||
"title": "Methods, Dunder",
|
||||
"description": "A method in python is somewhat similar to a function, except it is associated with object/classes. Methods in python are very similar to functions except for two major differences.\n\n* The method is implicitly used for an object for which it is called.\n* The method is accessible to data that is contained within the class.\n\nDunder or magic methods in Python are the methods having two prefix and suffix underscores in the method name. Dunder here means “Double Under (Underscores)”. These are commonly used for operator overloading. Few examples for magic methods are: **`__init__`**, **`__add__`**, **`__len__`**, **`__repr__`** etc.\n\nVisit the following resources to learn more:",
|
||||
"description": "A method in python is somewhat similar to a function, except it is associated with object/classes. Methods in python are very similar to functions except for two major differences.\n\n* The method is implicitly used for an object for which it is called.\n* The method is accessible to data that is contained within the class.\n\nDunder or magic methods in Python are the methods that have two prefix and suffix underscores in the method name. Dunder here means “Double Under (Underscores)”. These are commonly used for operator overloading. Few examples for magic methods are: **`__init__`**, **`__add__`**, **`__len__`**, **`__repr__`** etc.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Method vs Function in Python",
|
||||
@@ -1309,7 +1314,7 @@
|
||||
"description": "An extremely fast Python linter and code formatter, written in Rust.",
|
||||
"links": [
|
||||
{
|
||||
"title": "ruff documentation",
|
||||
"title": "Ruff documentation",
|
||||
"url": "https://docs.astral.sh/ruff/",
|
||||
"type": "article"
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "RPOP Documentation",
|
||||
"url": "https://redis.io/docs/latest/commands/rpush/",
|
||||
"url": "https://redis.io/docs/latest/commands/rpop/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
@@ -954,8 +954,19 @@
|
||||
},
|
||||
"jrgaoDnt_RxTu79hk4hCD": {
|
||||
"title": "Atomicity in Redis",
|
||||
"description": "Atomicity in Redis refers to the property that ensures a set of operations is executed as a single, indivisible unit. This means that either all the operations are executed successfully or none of them are. Atomicity is crucial in Redis to maintain consistency, especially when multiple operations need to be performed together.\n\nLearn more from the following resources:\n\n* [@official@Atomicity with Lua](https://redis.io/learn/develop/java/spring/rate-limiting/fixed-window/reactive-lua) -[@article@Atomicity in Redis operations](https://lucaspin.medium.com/atomicity-in-redis-operations-a1d7bc9f4a90)",
|
||||
"links": []
|
||||
"description": "Atomicity in Redis refers to the property that ensures a set of operations is executed as a single, indivisible unit. This means that either all the operations are executed successfully or none of them are. Atomicity is crucial in Redis to maintain consistency, especially when multiple operations need to be performed together.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Atomicity with Lua",
|
||||
"url": "https://redis.io/learn/develop/java/spring/rate-limiting/fixed-window/reactive-lua",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Atomicity in Redis operations",
|
||||
"url": "https://lucaspin.medium.com/atomicity-in-redis-operations-a1d7bc9f4a90",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"LHlwjN3WHYUBUafzzwsWQ": {
|
||||
"title": "Pipelining",
|
||||
|
||||
@@ -257,7 +257,18 @@
|
||||
"E4H3hniIW6hKpH3Qr--N5": {
|
||||
"title": "C/C++",
|
||||
"description": "\"C\" and \"C++\", often written as \"C/CPP\", are two significantly prominent and similar programming languages widely used in server-side game development. \"C\" is a procedural language, which means that it follows a step-by-step procedure to solve a problem, while \"C++\" is both a procedural and object-oriented programming (OOP) language. This dual nature of \"C++\" allows it to handle more complex interrelated data and functions efficiently, which is a beneficial feature in game development. Moreover, \"C++\" is an extension of \"C\", meaning that any legal \"C\" program is also a valid \"C++\" program. Both languages offer a high degree of control over system resources and memory, making them an excellent choice for building fast and efficient server-side applications, such as multiplayer game servers.",
|
||||
"links": []
|
||||
"links": [
|
||||
{
|
||||
"title": "C Programming Language",
|
||||
"url": "https://en.wikipedia.org/wiki/C_%28programming_language%29",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "C++ Programming Language",
|
||||
"url": "https://en.wikipedia.org/wiki/C%2B%2B",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DuyUc9a-47Uz03yr4aeyg": {
|
||||
"title": "C#",
|
||||
|
||||
@@ -1326,16 +1326,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"LncTxPg-wx8loy55r5NmV": {
|
||||
"queu-based-load-leveling@LncTxPg-wx8loy55r5NmV.md": {
|
||||
"title": "Queu-based Load Leveling",
|
||||
"description": "Use a queue that acts as a buffer between a task and a service it invokes in order to smooth intermittent heavy loads that can cause the service to fail or the task to time out. This can help to minimize the impact of peaks in demand on availability and responsiveness for both the task and the service.\n\nLearn more from the following links:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Queue-Based Load Leveling pattern",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"links": []
|
||||
},
|
||||
"2ryzJhRDTo98gGgn9mAxR": {
|
||||
"title": "Publisher/Subscriber",
|
||||
@@ -1665,10 +1659,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"backends-for-frontend@n4It-lr7FFtSY83DcGydX.md": {
|
||||
"n4It-lr7FFtSY83DcGydX": {
|
||||
"title": "Backends for Frontend",
|
||||
"description": "",
|
||||
"links": []
|
||||
"description": "Create separate backend services to be consumed by specific frontend applications or interfaces. This pattern is useful when you want to avoid customizing a single backend for multiple interfaces. This pattern was first described by Sam Newman.\n\nTo learn more, visit the following links:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Backends for Frontends pattern",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Explore top posts about Frontend Development",
|
||||
"url": "https://app.daily.dev/tags/frontend?ref=roadmapsh",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"4hi7LvjLcv8eR6m-uk8XQ": {
|
||||
"title": "Anti-Corruption Layer",
|
||||
|
||||
@@ -800,11 +800,6 @@
|
||||
"title": "TypeScript Utility Types Guide",
|
||||
"url": "https://camchenry.com/blog/typescript-utility-types",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "TypeScript Utility Types: Key Concepts And Best Practices",
|
||||
"url": "https://marketsplash.com/tutorials/typescript/typescript-utility-types/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
BIN
public/roadmaps/cloudflare.png
Normal file
BIN
public/roadmaps/cloudflare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 512 KiB |
@@ -43,6 +43,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
|
||||
- [AI Engineer Roadmap](https://roadmap.sh/ai-engineer)
|
||||
- [AWS Roadmap](https://roadmap.sh/aws)
|
||||
- [Cloudflare Roadmap](https://roadmap.sh/cloudflare)
|
||||
- [Linux Roadmap](https://roadmap.sh/linux)
|
||||
- [Terraform Roadmap](https://roadmap.sh/terraform)
|
||||
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)
|
||||
|
||||
181
scripts/gemini-roadmap-content.ts
Normal file
181
scripts/gemini-roadmap-content.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Edge, Node } from 'reactflow';
|
||||
import matter from 'gray-matter';
|
||||
import type { RoadmapFrontmatter } from '../src/lib/roadmap';
|
||||
import { slugify } from '../src/lib/slugger';
|
||||
import { runPromisesInBatchSequentially } from '../src/lib/promise';
|
||||
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
// ERROR: `__dirname` is not defined in ES module scope
|
||||
// https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Usage: tsx ./scripts/editor-roadmap-content.ts <roadmapId>
|
||||
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||||
console.log('GEMINI_API_KEY:', GEMINI_API_KEY);
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const google = createGoogleGenerativeAI({
|
||||
apiKey: process.env.GEMINI_API_KEY,
|
||||
});
|
||||
|
||||
const allowedRoadmapIds = await fs.readdir(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('Roadmap Id is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapFrontmatterDir = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapId,
|
||||
`${roadmapId}.md`,
|
||||
);
|
||||
const roadmapFrontmatterRaw = await fs.readFile(roadmapFrontmatterDir, 'utf-8');
|
||||
const { data } = matter(roadmapFrontmatterRaw);
|
||||
|
||||
const roadmapFrontmatter = data as RoadmapFrontmatter;
|
||||
if (!roadmapFrontmatter) {
|
||||
console.error('Invalid roadmap frontmatter');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (roadmapFrontmatter.renderer !== 'editor') {
|
||||
console.error('Only Editor Rendered Roadmaps are allowed');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDir = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapId,
|
||||
`${roadmapId}.json`,
|
||||
);
|
||||
const roadmapContent = await fs.readFile(roadmapDir, 'utf-8');
|
||||
let { nodes, edges } = JSON.parse(roadmapContent) as {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
const enrichedNodes = nodes
|
||||
.filter(
|
||||
(node) =>
|
||||
node?.type &&
|
||||
['topic', 'subtopic'].includes(node.type) &&
|
||||
node.data?.label,
|
||||
)
|
||||
.map((node) => {
|
||||
// Because we only need the parent id and title for subtopics
|
||||
if (node.type !== 'subtopic') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const parentNodeId =
|
||||
edges.find((edge) => edge.target === node.id)?.source || '';
|
||||
const parentNode = nodes.find((n) => n.id === parentNodeId);
|
||||
|
||||
return {
|
||||
...node,
|
||||
parentId: parentNodeId,
|
||||
parentTitle: parentNode?.data?.label || '',
|
||||
};
|
||||
}) as (Node & { parentId?: string; parentTitle?: string })[];
|
||||
|
||||
const roadmapContentDir = path.join(ROADMAP_CONTENT_DIR, roadmapId, 'content');
|
||||
const stats = await fs.stat(roadmapContentDir).catch(() => null);
|
||||
if (!stats || !stats.isDirectory()) {
|
||||
await fs.mkdir(roadmapContentDir, { recursive: true });
|
||||
}
|
||||
|
||||
function writeTopicContent(
|
||||
roadmapTitle: string,
|
||||
childTopic: string,
|
||||
parentTopic?: string,
|
||||
) {
|
||||
const updatedTitle = roadmapTitle.replace('Roadmap', '').trim().replace('Developer', '');
|
||||
let prompt = `I will give you a topic and you need to write a brief introduction for that in "${roadmapTitle}". Your format should be as follows and be in strictly markdown format:
|
||||
|
||||
# (Put a heading for the topic without adding parent "Subtopic in Topic" or "Topic in Roadmap" or "Subtopic under XYZ" etc.)
|
||||
|
||||
(Briefly explain the topic in one paragraph using simple english. Don't start with explaining how important the topic is with regard to "${roadmapTitle}". Don't say something along the lines of "XYZ plays a crucial role in ${roadmapTitle}". Don't include anything saying "In the context of ${roadmapTitle}". Instead, start with a simple explanation of the topic itself. For example, if the topic is "React", you can start with "React is a JavaScript library for building user interfaces."".)
|
||||
`;
|
||||
|
||||
if (!parentTopic) {
|
||||
prompt += `First topic is: ${childTopic}`;
|
||||
} else {
|
||||
prompt += `First topic is: "${parentTopic} > ${childTopic}"`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
generateText({
|
||||
model: google('gemini-2.0-flash'),
|
||||
prompt: prompt,
|
||||
providerOptions: {
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.text;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function writeNodeContent(node: Node & { parentTitle?: string }) {
|
||||
const nodeDirPattern = `${slugify(node.data.label)}@${node.id}.md`;
|
||||
if (!roadmapContentFiles.includes(nodeDirPattern)) {
|
||||
console.log(`Missing file for: ${nodeDirPattern}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeDir = path.join(roadmapContentDir, nodeDirPattern);
|
||||
const nodeContent = await fs.readFile(nodeDir, 'utf-8');
|
||||
const isFileEmpty = !nodeContent.replace(`# ${node.data.label}`, '').trim();
|
||||
if (!isFileEmpty) {
|
||||
console.log(`❌ Ignoring ${nodeDirPattern}. Not empty.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = node.data.label;
|
||||
const parentTopic = node.parentTitle;
|
||||
|
||||
console.log(`⏳ Generating content for ${topic}...`);
|
||||
let newContentFile = '';
|
||||
if (GEMINI_API_KEY) {
|
||||
newContentFile = (await writeTopicContent(
|
||||
roadmapFrontmatter.title,
|
||||
topic,
|
||||
parentTopic,
|
||||
)) as string;
|
||||
} else {
|
||||
newContentFile = `# ${topic}`;
|
||||
}
|
||||
|
||||
await fs.writeFile(nodeDir, newContentFile, 'utf-8');
|
||||
console.log(`✅ Content generated for ${topic}`);
|
||||
}
|
||||
|
||||
let roadmapContentFiles = await fs.readdir(roadmapContentDir, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
if (!GEMINI_API_KEY) {
|
||||
console.log('----------------------------------------');
|
||||
console.log('GEMINI_API_KEY not found. Skipping gemini api calls...');
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
const promises = enrichedNodes.map((node) => () => writeNodeContent(node));
|
||||
await runPromisesInBatchSequentially(promises, 20);
|
||||
console.log('✅ All content generated');
|
||||
14
src/components/Analytics/Clarity.astro
Normal file
14
src/components/Analytics/Clarity.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
<script type='text/javascript'>
|
||||
(function (c, l, a, r, i, t, y) {
|
||||
c[a] =
|
||||
c[a] ||
|
||||
function () {
|
||||
(c[a].q = c[a].q || []).push(arguments);
|
||||
};
|
||||
t = l.createElement(r);
|
||||
t.async = 1;
|
||||
t.src = 'https://www.clarity.ms/tag/' + i;
|
||||
y = l.getElementsByTagName(r)[0];
|
||||
y.parentNode.insertBefore(t, y);
|
||||
})(window, document, 'clarity', 'script', 'qcw723i36o');
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@ declare global {
|
||||
category: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
callback?: () => void;
|
||||
}) => void;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +18,7 @@ declare global {
|
||||
* @returns void
|
||||
*/
|
||||
window.fireEvent = (props) => {
|
||||
const { action, category, label, value } = props;
|
||||
const { action, category, label, value, callback } = props;
|
||||
if (!window.gtag) {
|
||||
console.warn('Missing GTAG - Analytics disabled');
|
||||
return;
|
||||
@@ -25,11 +26,16 @@ window.fireEvent = (props) => {
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Analytics event fired', props);
|
||||
callback?.();
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
...(callback ? { event_callback: callback } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
export {};
|
||||
|
||||
@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useId, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { FIRST_LOGIN_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
|
||||
type EmailLoginFormProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -24,19 +24,22 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
|
||||
setIsDisabled?.(true);
|
||||
setError('');
|
||||
|
||||
const { response, error } = await httpPost<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-login`,
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
);
|
||||
const { response, error } = await httpPost<{
|
||||
token: string;
|
||||
isNewUser: boolean;
|
||||
}>(`${import.meta.env.PUBLIC_API_URL}/v1-login`, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Log the user in and reload the page
|
||||
if (response?.token) {
|
||||
setAuthToken(response.token);
|
||||
window.location.reload();
|
||||
|
||||
const currentLocation = window.location.href;
|
||||
const url = new URL(currentLocation, window.location.origin);
|
||||
url.searchParams.set(FIRST_LOGIN_PARAM, response?.isNewUser ? '1' : '0');
|
||||
window.location.href = url.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../../editor/utils/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
@@ -34,7 +38,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -51,7 +55,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const gitHubRedirectAt = localStorage.getItem(GITHUB_REDIRECT_AT);
|
||||
const lastPageBeforeGithub = localStorage.getItem(GITHUB_LAST_PAGE);
|
||||
|
||||
@@ -63,31 +67,37 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGithub;
|
||||
redirectUrl = new URL(lastPageBeforeGithub, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { COURSE_PURCHASE_PARAM } from '../../lib/jwt';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
@@ -9,6 +14,7 @@ import {
|
||||
getStoredUtmParams,
|
||||
triggerUtmRegistration,
|
||||
} from '../../lib/browser.ts';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type GoogleButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -37,14 +43,12 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
const utmParams = getStoredUtmParams();
|
||||
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong.');
|
||||
setIsLoading(false);
|
||||
@@ -55,7 +59,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const googleRedirectAt = localStorage.getItem(GOOGLE_REDIRECT_AT);
|
||||
const lastPageBeforeGoogle = localStorage.getItem(GOOGLE_LAST_PAGE);
|
||||
|
||||
@@ -67,22 +71,28 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeGoogle;
|
||||
redirectUrl = new URL(lastPageBeforeGoogle, window.location.origin);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
@@ -90,7 +100,8 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
COURSE_PURCHASE_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
@@ -34,7 +40,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
setIsLoading(true);
|
||||
setIsDisabled?.(true);
|
||||
httpGet<{ token: string }>(
|
||||
httpGet<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
|
||||
window.location.search
|
||||
}`,
|
||||
@@ -50,7 +56,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
|
||||
triggerUtmRegistration();
|
||||
|
||||
let redirectUrl = '/';
|
||||
let redirectUrl = new URL('/', window.location.origin);
|
||||
const linkedInRedirectAt = localStorage.getItem(LINKEDIN_REDIRECT_AT);
|
||||
const lastPageBeforeLinkedIn = localStorage.getItem(LINKEDIN_LAST_PAGE);
|
||||
|
||||
@@ -62,30 +68,39 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const timeSinceRedirect = now - socialRedirectAtTime;
|
||||
|
||||
if (timeSinceRedirect < 30 * 1000) {
|
||||
redirectUrl = lastPageBeforeLinkedIn;
|
||||
redirectUrl = new URL(
|
||||
lastPageBeforeLinkedIn,
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const authRedirectUrl = localStorage.getItem('authRedirect');
|
||||
if (authRedirectUrl) {
|
||||
localStorage.removeItem('authRedirect');
|
||||
redirectUrl = authRedirectUrl;
|
||||
redirectUrl = new URL(authRedirectUrl, window.location.origin);
|
||||
}
|
||||
|
||||
redirectUrl.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
if (
|
||||
redirectUrl.pathname.includes('/courses/sql') &&
|
||||
shouldTriggerPurchase
|
||||
) {
|
||||
redirectUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
|
||||
localStorage.removeItem(LINKEDIN_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
window.location.href = redirectUrl.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setError('Something went wrong. Please try again later.');
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import {
|
||||
FIRST_LOGIN_PARAM,
|
||||
TOKEN_COOKIE_NAME,
|
||||
setAuthToken,
|
||||
} from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
@@ -13,7 +17,7 @@ export function TriggerVerifyAccount() {
|
||||
const triggerVerify = (code: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
httpPost<{ token: string }>(
|
||||
httpPost<{ token: string; isNewUser: boolean }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
|
||||
{
|
||||
code,
|
||||
@@ -30,7 +34,13 @@ export function TriggerVerifyAccount() {
|
||||
triggerUtmRegistration();
|
||||
|
||||
setAuthToken(response.token);
|
||||
window.location.href = '/';
|
||||
|
||||
const url = new URL('/', window.location.origin);
|
||||
url.searchParams.set(
|
||||
FIRST_LOGIN_PARAM,
|
||||
response?.isNewUser ? '1' : '0',
|
||||
);
|
||||
window.location.href = url.toString();
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { getAllChangelogs } from '../lib/changelog';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import AstroIcon from './AstroIcon.astro';
|
||||
const allChangelogs = await getAllChangelogs();
|
||||
const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
---
|
||||
@@ -12,16 +12,18 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
<img
|
||||
src='/images/gifs/rocket.gif'
|
||||
alt='Rocket'
|
||||
class='mr-2 hidden sm:inline h-12 w-12'
|
||||
class='mr-2 hidden h-12 w-12 sm:inline'
|
||||
/>
|
||||
Actively Maintained
|
||||
</p>
|
||||
<p class='mt-1 mb-2 sm:my-2 text-sm leading-relaxed text-gray-600 sm:my-5 sm:text-lg'>
|
||||
<p
|
||||
class='mb-2 mt-1 text-sm leading-relaxed text-gray-600 sm:my-2 sm:my-5 sm:text-lg'
|
||||
>
|
||||
We are always improving our content, adding new resources and adding
|
||||
features to enhance your learning experience.
|
||||
</p>
|
||||
|
||||
<div class='relative mt-2 sm:mt-8 text-left'>
|
||||
<div class='relative mt-2 text-left sm:mt-8'>
|
||||
<div
|
||||
class='absolute inset-y-0 left-[120px] hidden w-px -translate-x-[0.5px] translate-x-[5.75px] bg-gray-300 sm:block'
|
||||
>
|
||||
@@ -36,13 +38,13 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
<li class='relative'>
|
||||
<a
|
||||
href={`/changelog#${changelog.id}`}
|
||||
class='flex flex-col sm:flex-row items-start sm:items-center'
|
||||
class='flex flex-col items-start sm:flex-row sm:items-center'
|
||||
>
|
||||
<span class='sm:w-[120px] flex-shrink-0 pr-0 sm:pr-4 text-right text-sm tracking-wide text-gray-400'>
|
||||
<span class='flex-shrink-0 pr-0 text-right text-sm tracking-wide text-gray-400 sm:w-[120px] sm:pr-4'>
|
||||
{formattedDate}
|
||||
</span>
|
||||
<span class='h-3 w-3 flex-shrink-0 rounded-full bg-gray-300 hidden sm:block' />
|
||||
<span class='text-balance sm:pl-8 text-base font-medium text-gray-900'>
|
||||
<span class='hidden h-3 w-3 flex-shrink-0 rounded-full bg-gray-300 sm:block' />
|
||||
<span class='text-balance text-base font-medium text-gray-900 sm:pl-8'>
|
||||
{changelog.frontmatter.title}
|
||||
</span>
|
||||
</a>
|
||||
@@ -52,13 +54,23 @@ const top10Changelogs = allChangelogs.slice(0, 10);
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class='mt-2 sm:mt-8 text-left sm:text-center'>
|
||||
<div
|
||||
class='mt-2 flex flex-col gap-2 sm:flex-row sm:mt-8 sm:items-center sm:justify-center'
|
||||
>
|
||||
<a
|
||||
href='/changelog'
|
||||
class='inline-block text-sm sm:text-base rounded-lg sm:rounded-full bg-gray-800 px-4 sm:px-6 py-2 text-white transition-colors hover:bg-gray-700'
|
||||
class='inline-block rounded-lg border border-black bg-black px-4 py-2 text-sm text-white transition-colors hover:bg-gray-700 sm:rounded-full sm:px-6 sm:text-base'
|
||||
>
|
||||
View Full Changelog
|
||||
</a>
|
||||
<button
|
||||
data-guest-required
|
||||
data-popup='login-popup'
|
||||
class='flex flex-row items-center gap-2 rounded-lg border border-black bg-white px-4 py-2 text-sm text-black transition-all hover:bg-black hover:text-white sm:rounded-full sm:pl-4 sm:pr-5 sm:text-base'
|
||||
>
|
||||
<AstroIcon icon='bell' class='h-5 w-5' />
|
||||
Subscribe for Notifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../../editor/utils/classname';
|
||||
import { useParams } from '../../hooks/use-params';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { $teamList } from '../../stores/team';
|
||||
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
|
||||
import { DashboardTab } from './DashboardTab';
|
||||
import { DashboardTabButton } from './DashboardTabButton';
|
||||
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
|
||||
import { TeamDashboard } from './TeamDashboard';
|
||||
import { getUser } from '../../lib/jwt';
|
||||
import { useParams } from '../../hooks/use-params';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
|
||||
type DashboardPageProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
isTeamPage?: boolean;
|
||||
questionGroups?: QuestionGroupType[];
|
||||
guides?: GuideFileType[];
|
||||
videos?: VideoFileType[];
|
||||
};
|
||||
|
||||
export function DashboardPage(props: DashboardPageProps) {
|
||||
@@ -23,6 +30,9 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
builtInBestPractices,
|
||||
builtInSkillRoadmaps,
|
||||
isTeamPage = false,
|
||||
questionGroups,
|
||||
guides,
|
||||
videos,
|
||||
} = props;
|
||||
|
||||
const currentUser = getUser();
|
||||
@@ -66,78 +76,79 @@ export function DashboardPage(props: DashboardPageProps) {
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pb-20 pt-8">
|
||||
<div className="container">
|
||||
<div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
|
||||
<DashboardTab
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId && !isTeamPage}
|
||||
href="/dashboard"
|
||||
avatar={userAvatar}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<>
|
||||
<DashboardTabSkeleton />
|
||||
<DashboardTabSkeleton />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<>
|
||||
{teamList.map((team) => {
|
||||
const { avatar } = team;
|
||||
const avatarUrl = avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<DashboardTab
|
||||
key={team._id}
|
||||
label={team.name}
|
||||
isActive={team._id === selectedTeamId}
|
||||
{...(team.status === 'invited'
|
||||
? {
|
||||
href: `/respond-invite?i=${team.memberId}`,
|
||||
}
|
||||
: {
|
||||
href: `/team?t=${team._id}`,
|
||||
})}
|
||||
avatar={avatarUrl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DashboardTab
|
||||
label="+ Create Team"
|
||||
isActive={false}
|
||||
href="/team/new"
|
||||
className="border border-dashed border-gray-300 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-gray-600 hover:text-black"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div
|
||||
className={cn('bg-slate-900', {
|
||||
'striped-loader-slate': isLoading,
|
||||
})}
|
||||
>
|
||||
<div className="bg-slate-800/30 py-5 min-h-[70px]">
|
||||
<div className="container flex flex-wrap items-center gap-1.5">
|
||||
{!isLoading && (
|
||||
<>
|
||||
<DashboardTabButton
|
||||
label="Personal"
|
||||
isActive={!selectedTeamId && !isTeamPage}
|
||||
href="/dashboard"
|
||||
avatar={userAvatar}
|
||||
/>
|
||||
{teamList.map((team) => {
|
||||
const { avatar } = team;
|
||||
const avatarUrl = avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
return (
|
||||
<DashboardTabButton
|
||||
key={team._id}
|
||||
label={team.name}
|
||||
isActive={team._id === selectedTeamId}
|
||||
{...(team.status === 'invited'
|
||||
? {
|
||||
href: `/respond-invite?i=${team.memberId}`,
|
||||
}
|
||||
: {
|
||||
href: `/team?t=${team._id}`,
|
||||
})}
|
||||
avatar={avatarUrl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DashboardTabButton
|
||||
label="+ Create Team"
|
||||
isActive={false}
|
||||
href="/team/new"
|
||||
className="border border-dashed border-slate-700 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-solid hover:border-slate-700 hover:text-gray-400"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{!selectedTeamId && !isTeamPage && (
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
/>
|
||||
<div className="bg-slate-900">
|
||||
<PersonalDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps}
|
||||
builtInBestPractices={builtInBestPractices}
|
||||
questionGroups={questionGroups}
|
||||
guides={guides}
|
||||
videos={videos}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(selectedTeamId || isTeamPage) && (
|
||||
<TeamDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||
teamId={selectedTeamId!}
|
||||
/>
|
||||
<div className="container">
|
||||
<TeamDashboard
|
||||
builtInRoleRoadmaps={builtInRoleRoadmaps!}
|
||||
builtInSkillRoadmaps={builtInSkillRoadmaps!}
|
||||
teamId={selectedTeamId!}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardTabSkeleton() {
|
||||
return (
|
||||
<div className="h-[30px] w-[114px] animate-pulse rounded-md border bg-white"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type DashboardTabProps = {
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
export function DashboardTab(props: DashboardTabProps) {
|
||||
export function DashboardTabButton(props: DashboardTabProps) {
|
||||
const { isActive, onClick, label, className, href, avatar, icon } = props;
|
||||
|
||||
const Slot = href ? 'a' : 'button';
|
||||
@@ -20,8 +20,10 @@ export function DashboardTab(props: DashboardTabProps) {
|
||||
<Slot
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border bg-white p-1.5 px-2 text-sm leading-none text-gray-600',
|
||||
isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '',
|
||||
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border border-slate-700 bg-slate-800 p-1.5 pl-2 pr-3 text-sm leading-none text-gray-400 transition-colors hover:bg-slate-700',
|
||||
isActive
|
||||
? 'border-slate-200 bg-slate-200 text-gray-900 hover:bg-slate-200'
|
||||
: '',
|
||||
className,
|
||||
)}
|
||||
{...(href ? { href } : {})}
|
||||
@@ -30,7 +32,7 @@ export function DashboardTab(props: DashboardTabProps) {
|
||||
<img
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
className="h-4 w-4 mr-0.5 rounded-full object-cover"
|
||||
className="mr-0.5 h-4 w-4 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{icon}
|
||||
@@ -1,23 +1,47 @@
|
||||
import { type JSXElementConstructor, useEffect, useState } from 'react';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getCurrentPeriod } from '../../lib/date';
|
||||
import { ListDashboardCustomProgress } from './ListDashboardCustomProgress';
|
||||
import { RecommendedRoadmaps } from './RecommendedRoadmaps';
|
||||
import { ProgressStack } from './ProgressStack';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
||||
import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx';
|
||||
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx';
|
||||
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx';
|
||||
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
|
||||
import {
|
||||
ChartColumn,
|
||||
CheckSquare,
|
||||
FolderGit2,
|
||||
SquarePen,
|
||||
Zap,
|
||||
type LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { AllowedProfileVisibility } from '../../api/user.ts';
|
||||
import { PencilIcon, type LucideIcon } from 'lucide-react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
import { $accountStreak, type StreakResponse } from '../../stores/streak';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { FeaturedGuideList } from '../FeaturedGuides/FeaturedGuideList';
|
||||
import { FeaturedVideoList } from '../FeaturedVideos/FeaturedVideoList';
|
||||
import {
|
||||
FavoriteRoadmaps,
|
||||
type AIRoadmapType,
|
||||
} from '../HeroSection/FavoriteRoadmaps.tsx';
|
||||
import { HeroRoadmap } from '../HeroSection/HeroRoadmap.tsx';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
|
||||
|
||||
const projectGroups = [
|
||||
{
|
||||
title: 'Frontend',
|
||||
id: 'frontend',
|
||||
},
|
||||
{
|
||||
title: 'Backend',
|
||||
id: 'backend',
|
||||
},
|
||||
{
|
||||
title: 'DevOps',
|
||||
id: 'devops',
|
||||
},
|
||||
];
|
||||
|
||||
type UserDashboardResponse = {
|
||||
name: string;
|
||||
@@ -28,11 +52,7 @@ type UserDashboardResponse = {
|
||||
profileVisibility: AllowedProfileVisibility;
|
||||
progresses: UserProgress[];
|
||||
projects: ProjectStatusDocument[];
|
||||
aiRoadmaps: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
}[];
|
||||
aiRoadmaps: AIRoadmapType[];
|
||||
topicDoneToday: number;
|
||||
};
|
||||
|
||||
@@ -42,6 +62,7 @@ export type BuiltInRoadmap = {
|
||||
title: string;
|
||||
description: string;
|
||||
isFavorite?: boolean;
|
||||
isNew?: boolean;
|
||||
relatedRoadmapIds?: string[];
|
||||
renderer?: AllowedRoadmapRenderer;
|
||||
metadata?: Record<string, any>;
|
||||
@@ -51,16 +72,162 @@ type PersonalDashboardProps = {
|
||||
builtInRoleRoadmaps?: BuiltInRoadmap[];
|
||||
builtInSkillRoadmaps?: BuiltInRoadmap[];
|
||||
builtInBestPractices?: BuiltInRoadmap[];
|
||||
questionGroups?: QuestionGroupType[];
|
||||
guides?: GuideFileType[];
|
||||
videos?: VideoFileType[];
|
||||
};
|
||||
|
||||
type DashboardStatItemProps = {
|
||||
icon: LucideIcon;
|
||||
iconClassName: string;
|
||||
value: number;
|
||||
label: string;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
function DashboardStatItem(props: DashboardStatItemProps) {
|
||||
const { icon: Icon, iconClassName, value, label, isLoading } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3',
|
||||
{
|
||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
||||
isLoading,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
className={cn(iconClassName, { 'text-transparent': isLoading })}
|
||||
/>
|
||||
<span>
|
||||
<span className="tabular-nums">{value}</span> {label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ProfileButtonProps = {
|
||||
isLoading: boolean;
|
||||
name?: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
function PersonalProfileButton(props: ProfileButtonProps) {
|
||||
const { isLoading, name, username, avatar } = props;
|
||||
|
||||
if (isLoading || !username) {
|
||||
return (
|
||||
<a
|
||||
href="/account/update-profile"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 font-medium outline-slate-700 hover:bg-slate-800 hover:outline-slate-400',
|
||||
{
|
||||
'striped-loader-slate striped-loader-slate-fast text-transparent':
|
||||
isLoading,
|
||||
'bg-blue-500/10 text-blue-500 hover:bg-blue-500/20': !isLoading,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" strokeWidth={2.5} />
|
||||
Set up your profile
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
<a
|
||||
href={`/u/${username}`}
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-300 transition-colors hover:bg-slate-800/70"
|
||||
>
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name || 'Profile'}
|
||||
className="h-5 w-5 rounded-full ring-1 ring-slate-700"
|
||||
/>
|
||||
<span className="font-medium">Visit Profile</span>
|
||||
</a>
|
||||
<a
|
||||
href="/account/update-profile"
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-400 transition-colors hover:bg-slate-800/70 hover:text-slate-300"
|
||||
title="Edit Profile"
|
||||
>
|
||||
<SquarePen className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DashboardStatsProps = {
|
||||
profile: ProfileButtonProps;
|
||||
accountStreak?: StreakResponse;
|
||||
topicsDoneToday?: number;
|
||||
finishedProjectsCount?: number;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
function DashboardStats(props: DashboardStatsProps) {
|
||||
const {
|
||||
accountStreak,
|
||||
topicsDoneToday = 0,
|
||||
finishedProjectsCount = 0,
|
||||
isLoading,
|
||||
profile,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="container mb-3 flex flex-col gap-4 pb-2 pt-6 text-sm text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<PersonalProfileButton
|
||||
isLoading={isLoading}
|
||||
name={profile.name}
|
||||
username={profile.username}
|
||||
avatar={profile.avatar}
|
||||
/>
|
||||
<div className="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<DashboardStatItem
|
||||
icon={Zap}
|
||||
iconClassName="text-yellow-500"
|
||||
value={accountStreak?.count || 0}
|
||||
label="day streak"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<DashboardStatItem
|
||||
icon={ChartColumn}
|
||||
iconClassName="text-green-500"
|
||||
value={topicsDoneToday}
|
||||
label="learnt today"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<DashboardStatItem
|
||||
icon={FolderGit2}
|
||||
iconClassName="text-blue-500"
|
||||
value={finishedProjectsCount}
|
||||
label="projects finished"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
const {
|
||||
builtInRoleRoadmaps = [],
|
||||
builtInBestPractices = [],
|
||||
builtInSkillRoadmaps = [],
|
||||
questionGroups = [],
|
||||
guides = [],
|
||||
videos = [],
|
||||
} = props;
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [personalDashboardDetails, setPersonalDashboardDetails] =
|
||||
useState<UserDashboardResponse>();
|
||||
@@ -138,7 +305,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||
}, []);
|
||||
|
||||
const learningRoadmapsToShow = (personalDashboardDetails?.progresses || [])
|
||||
const learningRoadmapsToShow: UserProgress[] = (
|
||||
personalDashboardDetails?.progresses || []
|
||||
)
|
||||
.filter((progress) => !progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
@@ -156,7 +325,10 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
});
|
||||
|
||||
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
|
||||
const customRoadmaps = (personalDashboardDetails?.progresses || [])
|
||||
|
||||
const customRoadmaps: UserProgress[] = (
|
||||
personalDashboardDetails?.progresses || []
|
||||
)
|
||||
.filter((progress) => progress.isCustomResource)
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
@@ -169,43 +341,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png';
|
||||
|
||||
const allRoadmapsAndBestPractices = [
|
||||
...builtInRoleRoadmaps,
|
||||
...builtInSkillRoadmaps,
|
||||
...builtInBestPractices,
|
||||
];
|
||||
|
||||
const relatedRoadmapIds = allRoadmapsAndBestPractices
|
||||
// take the ones that user is learning
|
||||
.filter((roadmap) =>
|
||||
learningRoadmapsToShow?.some(
|
||||
(learningRoadmap) => learningRoadmap.resourceId === roadmap.id,
|
||||
),
|
||||
)
|
||||
.flatMap((roadmap) => roadmap.relatedRoadmapIds)
|
||||
// remove the ones that user is already learning or has bookmarked
|
||||
.filter(
|
||||
(roadmapId) =>
|
||||
!learningRoadmapsToShow.some((lr) => lr.resourceId === roadmapId),
|
||||
);
|
||||
|
||||
const recommendedRoadmapIds = new Set(
|
||||
relatedRoadmapIds.length === 0
|
||||
? [
|
||||
'frontend',
|
||||
'backend',
|
||||
'devops',
|
||||
'ai-data-scientist',
|
||||
'full-stack',
|
||||
'api-design',
|
||||
]
|
||||
: relatedRoadmapIds,
|
||||
);
|
||||
|
||||
const recommendedRoadmaps = allRoadmapsAndBestPractices.filter((roadmap) =>
|
||||
recommendedRoadmapIds.has(roadmap.id),
|
||||
);
|
||||
|
||||
const enrichedProjects = personalDashboardDetails?.projects
|
||||
.map((project) => {
|
||||
const projectDetail = projectDetails.find(
|
||||
@@ -232,165 +367,200 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
|
||||
const { username } = personalDashboardDetails || {};
|
||||
|
||||
return (
|
||||
<section>
|
||||
{isLoading ? (
|
||||
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
|
||||
) : (
|
||||
<div className="flex flex-col items-start justify-between gap-1 sm:flex-row sm:items-center">
|
||||
<h2 className="text-lg font-medium">
|
||||
Hi {name}, good {getCurrentPeriod()}!
|
||||
</h2>
|
||||
<a
|
||||
href="/home"
|
||||
className="rounded-full bg-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black"
|
||||
>
|
||||
Visit Homepage
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
<DashboardCardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DashboardCard
|
||||
imgUrl={avatarLink}
|
||||
title={name!}
|
||||
description={
|
||||
username ? 'View your profile' : 'Setup your profile'
|
||||
}
|
||||
href={username ? `/u/${username}` : '/account/update-profile'}
|
||||
{...(username && {
|
||||
externalLinkIcon: PencilIcon,
|
||||
externalLinkHref: '/account/update-profile',
|
||||
externalLinkText: 'Edit',
|
||||
})}
|
||||
className={
|
||||
!username
|
||||
? 'border-dashed border-gray-500 bg-gray-100 hover:border-gray-500 hover:bg-gray-200'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
<DashboardCard
|
||||
icon={BookEmoji}
|
||||
title="Visit Roadmaps"
|
||||
description="Learn new skills"
|
||||
href="/roadmaps"
|
||||
/>
|
||||
|
||||
<DashboardCard
|
||||
icon={ConstructionEmoji}
|
||||
title="Build Projects"
|
||||
description="Practice what you learn"
|
||||
href="/projects"
|
||||
/>
|
||||
<DashboardCard
|
||||
icon={CheckEmoji}
|
||||
title="Best Practices"
|
||||
description="Do things the right way"
|
||||
href="/best-practices"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProgressStack
|
||||
progresses={learningRoadmapsToShow}
|
||||
projects={enrichedProjects || []}
|
||||
<div>
|
||||
<DashboardStats
|
||||
profile={{
|
||||
name,
|
||||
username,
|
||||
avatar: avatarLink,
|
||||
isLoading,
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
accountStreak={accountStreak}
|
||||
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0}
|
||||
topicsDoneToday={personalDashboardDetails?.topicDoneToday}
|
||||
finishedProjectsCount={
|
||||
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
|
||||
.length
|
||||
}
|
||||
/>
|
||||
|
||||
<ListDashboardCustomProgress
|
||||
progresses={customRoadmaps}
|
||||
<FavoriteRoadmaps
|
||||
progress={learningRoadmapsToShow}
|
||||
customRoadmaps={customRoadmaps}
|
||||
aiRoadmaps={aiGeneratedRoadmaps}
|
||||
projects={enrichedProjects || []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<DashboardAiRoadmaps
|
||||
roadmaps={aiGeneratedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<div className="bg-gradient-to-b from-slate-900 to-black pb-12">
|
||||
<div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2
|
||||
id="role-based-roadmaps"
|
||||
className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2"
|
||||
>
|
||||
Role Based Roadmaps
|
||||
</h2>
|
||||
|
||||
<RecommendedRoadmaps
|
||||
roadmaps={recommendedRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInRoleRoadmaps.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
type DashboardCardProps = {
|
||||
icon?: JSXElementConstructor<any>;
|
||||
imgUrl?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
externalLinkIcon?: LucideIcon;
|
||||
externalLinkText?: string;
|
||||
externalLinkHref?: string;
|
||||
className?: string;
|
||||
};
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
function DashboardCard(props: DashboardCardProps) {
|
||||
const {
|
||||
icon: Icon,
|
||||
imgUrl,
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
externalLinkHref,
|
||||
externalLinkIcon: ExternalLinkIcon,
|
||||
externalLinkText,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={cn('relative overflow-hidden', className)}>
|
||||
<a
|
||||
href={href}
|
||||
className="flex flex-col rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50"
|
||||
>
|
||||
{Icon && (
|
||||
<div className="px-4 pb-3 pt-4">
|
||||
<Icon className="size-6" />
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{imgUrl && (
|
||||
<div className="px-4 pb-1.5 pt-3.5">
|
||||
<img src={imgUrl} alt={title} className="size-8 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex grow flex-col justify-center gap-0.5 p-4">
|
||||
<h3 className="truncate font-medium text-black">{title}</h3>
|
||||
<p className="text-xs text-black">{description}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{externalLinkHref && (
|
||||
<a
|
||||
href={externalLinkHref}
|
||||
className="absolute right-1 top-1 flex items-center gap-1.5 rounded-md bg-gray-200 p-1 px-2 text-xs text-gray-600 hover:bg-gray-300 hover:text-black"
|
||||
>
|
||||
{ExternalLinkIcon && <ExternalLinkIcon className="size-3" />}
|
||||
{externalLinkText}
|
||||
</a>
|
||||
)}
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Skill Based Roadmaps
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInSkillRoadmaps.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Project Ideas
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{projectGroups.map((projectGroup) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
percentageDone={0}
|
||||
key={projectGroup.id}
|
||||
resourceId={projectGroup.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={projectGroup.title}
|
||||
url={`/${projectGroup.id}/projects`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Best Practices
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{builtInBestPractices.map((roadmap) => {
|
||||
const roadmapProgress = learningRoadmapsToShow.find(
|
||||
(lr) => lr.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
const percentageDone =
|
||||
(((roadmapProgress?.skipped || 0) +
|
||||
(roadmapProgress?.done || 0)) /
|
||||
(roadmapProgress?.total || 1)) *
|
||||
100;
|
||||
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={roadmap.id}
|
||||
resourceId={roadmap.id}
|
||||
resourceType="best-practice"
|
||||
resourceTitle={roadmap.title}
|
||||
isFavorite={roadmap.isFavorite}
|
||||
percentageDone={percentageDone}
|
||||
isNew={roadmap.isNew}
|
||||
url={`/best-practices/${roadmap.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
|
||||
<div className="container">
|
||||
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
|
||||
Questions
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
|
||||
{questionGroups.map((questionGroup) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
percentageDone={0}
|
||||
key={questionGroup.id}
|
||||
resourceId={questionGroup.id}
|
||||
resourceType="roadmap"
|
||||
resourceTitle={questionGroup.frontmatter.briefTitle}
|
||||
url={`/questions/${questionGroup.id}`}
|
||||
allowFavorite={false}
|
||||
isNew={questionGroup.frontmatter.isNew}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 bg-gray-50 px-4 py-5 sm:gap-16 sm:px-0 sm:py-16">
|
||||
<FeaturedGuideList
|
||||
heading="Guides"
|
||||
guides={guides}
|
||||
questions={questionGroups
|
||||
.filter((questionGroup) => questionGroup.frontmatter.authorId)
|
||||
.slice(0, 7)}
|
||||
/>
|
||||
<FeaturedVideoList heading="Videos" videos={videos} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardCardSkeleton() {
|
||||
return (
|
||||
<div className="h-[128px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,80 +6,24 @@ import { isMobileScreen } from '../lib/is-mobile.ts';
|
||||
type FeatureAnnouncementProps = {};
|
||||
|
||||
export function FeatureAnnouncement(props: FeatureAnnouncementProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const videoModal = (
|
||||
<Modal
|
||||
onClose={() => setIsPlaying(false)}
|
||||
bodyClassName={'h-auto overflow-hidden'}
|
||||
wrapperClassName={'md:max-w-3xl lg:max-w-4xl xl:max-w-5xl'}
|
||||
>
|
||||
<div className="text-balance bg-gradient-to-r from-gray-100 px-4 py-2 text-left text-sm md:py-3 lg:text-base">
|
||||
<span
|
||||
className="relative -top-px mr-1.5 rounded bg-blue-300 px-1.5 py-0.5 text-xs font-semibold uppercase text-gray-800"
|
||||
style={{ lineHeight: '1.5' }}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
Projects are live on the{' '}
|
||||
<a
|
||||
href={'/projects'}
|
||||
className="font-medium text-blue-500 underline underline-offset-2"
|
||||
>
|
||||
several of our roadmaps
|
||||
</a>
|
||||
<span className={'hidden md:inline'}>
|
||||
{' '}
|
||||
and are coming soon on the others
|
||||
</span>
|
||||
<PartyPopper className="relative -top-[3px] ml-2 inline-block h-5 w-5 text-blue-500 md:ml-1 md:h-6 md:w-6" />
|
||||
</div>
|
||||
<div
|
||||
className="iframe-container"
|
||||
style={{
|
||||
position: 'relative',
|
||||
paddingBottom: '56.25%',
|
||||
height: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/*https://www.youtube.com/embed/?playsinline=1&disablekb=1&&iv_load_policy=3&cc_load_policy=0&controls=0&rel=0&autoplay=1&mute=1&origin=https%3A%2F%2Fytch.xyz&widgetid=1*/}
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/9lS3slfJ0x0?start=31&autoplay=1&disablekb=1&rel=0&cc_load_policy=0&rel=0&autoplay=1&origin=https%3A%2F%2Froadmap.sh&widgetid=1&showinfo=0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{isPlaying && videoModal}
|
||||
<button
|
||||
<a
|
||||
className="rounded-md border border-dashed border-purple-600 px-3 py-1.5 text-purple-400 transition-colors hover:border-purple-400 hover:text-purple-200"
|
||||
onClick={() => {
|
||||
setIsPlaying(true);
|
||||
}}
|
||||
href="/courses/sql"
|
||||
>
|
||||
<span className="relative sm:-top-[1px] mr-1 text-xs font-semibold uppercase text-white">
|
||||
<PlayCircle className="inline-block h-4 w-4 relative -top-[2px] mr-1" />
|
||||
Watch
|
||||
<PartyPopper className="inline-block h-4 w-4 relative -top-[2px] mr-1" />
|
||||
Courses
|
||||
</span>{' '}
|
||||
<span className={'hidden sm:inline'}>
|
||||
Practice your skills with projects
|
||||
Our first paid course about SQL is now live!
|
||||
</span>
|
||||
<span className={'inline text-sm sm:hidden'}>
|
||||
Build projects to skill up
|
||||
Our SQL course is now live!
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
import type { GuideFileType } from '../lib/guide';
|
||||
import GuideListItem from './GuideListItem.astro';
|
||||
import { QuestionGroupType } from '../lib/question-group';
|
||||
|
||||
export interface Props {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
}
|
||||
|
||||
const { heading, guides, questions = [] } = Astro.props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date);
|
||||
const bDate = new Date(b.frontmatter.date);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
---
|
||||
|
||||
<div class='container'>
|
||||
<h2 class='block text-2xl font-bold sm:text-3xl'>{heading}</h2>
|
||||
|
||||
<div class='mt-3 sm:my-5'>
|
||||
{sortedGuides.map((guide) => <GuideListItem guide={guide} />)}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/guides'
|
||||
class='hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline'
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
|
||||
<div class='mt-3 block sm:hidden'>
|
||||
<a
|
||||
href='/guides'
|
||||
class='font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
51
src/components/FeaturedGuides/FeaturedGuideList.tsx
Normal file
51
src/components/FeaturedGuides/FeaturedGuideList.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import { GuideListItem } from './GuideListItem';
|
||||
|
||||
export interface FeaturedGuidesProps {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
}
|
||||
|
||||
export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
const { heading, guides, questions = [] } = props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date as string);
|
||||
const bDate = new Date(b.frontmatter.date as string);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{sortedGuides.map((guide) => (
|
||||
<GuideListItem key={guide.id} guide={guide} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/guides"
|
||||
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
|
||||
<div className="mt-3 block sm:hidden">
|
||||
<a
|
||||
href="/guides"
|
||||
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
|
||||
>
|
||||
View All Guides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/FeaturedGuides/GuideListItem.tsx
Normal file
57
src/components/FeaturedGuides/GuideListItem.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
|
||||
import { type QuestionGroupType } from '../../lib/question-group';
|
||||
|
||||
export interface GuideListItemProps {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
}
|
||||
|
||||
export function GuideListItem(props: GuideListItemProps) {
|
||||
const { guide } = props;
|
||||
const { frontmatter, id } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={pageUrl}
|
||||
>
|
||||
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
|
||||
{frontmatter.title}
|
||||
|
||||
{frontmatter.isNew && (
|
||||
<span className="ml-2.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{new Date(frontmatter.date || '').toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="hidden text-xs capitalize text-gray-500 sm:block">
|
||||
{guideType}
|
||||
</span>
|
||||
|
||||
<span className="block text-xs text-gray-400 sm:hidden"> »</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
import type { VideoFileType } from '../lib/video';
|
||||
import VideoListItem from './VideoListItem.astro';
|
||||
|
||||
export interface Props {
|
||||
heading: string;
|
||||
videos: VideoFileType[];
|
||||
}
|
||||
|
||||
const { heading, videos } = Astro.props;
|
||||
---
|
||||
|
||||
<div class='container'>
|
||||
<h2 class='text-2xl sm:text-3xl font-bold block'>{heading}</h2>
|
||||
|
||||
<div class='mt-3 sm:my-5'>
|
||||
{videos.map((video) => <VideoListItem video={video} />)}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href='/videos'
|
||||
class='hidden sm:inline transition-colors py-2 px-3 text-xs font-medium rounded-full bg-gradient-to-r from-slate-600 to-black hover:from-blue-600 hover:to-blue-800 text-white'
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
|
||||
<div class='block sm:hidden mt-3'>
|
||||
<a
|
||||
href='/videos'
|
||||
class='text-sm font-regular block p-2 border border-black text-black rounded-md text-center hover:bg-black hover:text-gray-50'
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
39
src/components/FeaturedVideos/FeaturedVideoList.tsx
Normal file
39
src/components/FeaturedVideos/FeaturedVideoList.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
import { VideoListItem } from './VideoListItem';
|
||||
|
||||
export interface FeaturedVideoListProps {
|
||||
heading: string;
|
||||
videos: VideoFileType[];
|
||||
}
|
||||
|
||||
export function FeaturedVideoList(props: FeaturedVideoListProps) {
|
||||
const { heading, videos } = props;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{videos.map((video) => (
|
||||
<VideoListItem key={video.id} video={video} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/videos"
|
||||
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
|
||||
<div className="mt-3 block sm:hidden">
|
||||
<a
|
||||
href="/videos"
|
||||
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
|
||||
>
|
||||
View All Videos →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/FeaturedVideos/VideoListItem.tsx
Normal file
38
src/components/FeaturedVideos/VideoListItem.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { VideoFileType } from '../../lib/video';
|
||||
|
||||
export interface VideoListItemProps {
|
||||
video: VideoFileType;
|
||||
}
|
||||
|
||||
export function VideoListItem(props: VideoListItemProps) {
|
||||
const { video } = props;
|
||||
const { frontmatter, id } = video;
|
||||
|
||||
return (
|
||||
<a
|
||||
className="block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b"
|
||||
href={`/videos/${id}`}
|
||||
>
|
||||
<span className="group-hover:translate-x-2 transition-transform">
|
||||
{frontmatter.title}
|
||||
|
||||
{frontmatter.isNew && (
|
||||
<span className="bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{new Date(frontmatter.date).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="capitalize text-gray-500 text-xs hidden sm:block">
|
||||
{frontmatter.duration}
|
||||
</span>
|
||||
|
||||
<span className="text-gray-400 text-xs block sm:hidden"> »</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
import type { GuideFileType, GuideFrontmatter } from '../lib/guide';
|
||||
import { type QuestionGroupType } from '../lib/question-group';
|
||||
|
||||
export interface Props {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
}
|
||||
|
||||
const { guide } = Astro.props;
|
||||
const { frontmatter, id } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
}
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600',
|
||||
]}
|
||||
href={pageUrl}
|
||||
>
|
||||
<span
|
||||
class='text-sm transition-transform group-hover:translate-x-2 md:text-base'
|
||||
>
|
||||
{frontmatter.title}
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class='ml-1.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900'>
|
||||
New
|
||||
<span class='hidden sm:inline'>
|
||||
·
|
||||
{new Date(frontmatter.date || '').toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<span class='hidden text-xs capitalize text-gray-500 sm:block'>
|
||||
{guideType}
|
||||
</span>
|
||||
|
||||
<span class='block text-xs text-gray-400 sm:hidden'> »</span>
|
||||
</a>
|
||||
@@ -1,164 +1,229 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EmptyProgress } from './EmptyProgress';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
||||
import {
|
||||
FolderKanban,
|
||||
MapIcon,
|
||||
Plus,
|
||||
Sparkle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Square,
|
||||
SquareCheckBig,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
|
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx';
|
||||
import { HeroProject } from './HeroProject';
|
||||
import { HeroRoadmap } from './HeroRoadmap';
|
||||
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx';
|
||||
import { HeroItemsGroup } from './HeroItemsGroup';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
|
||||
export type UserProgressResponse = {
|
||||
resourceId: string;
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceTitle: string;
|
||||
isFavorite: boolean;
|
||||
done: number;
|
||||
learning: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: Date;
|
||||
isCustomResource: boolean;
|
||||
roadmapSlug?: string;
|
||||
team?: {
|
||||
name: string;
|
||||
id: string;
|
||||
role: AllowedMemberRoles;
|
||||
};
|
||||
}[];
|
||||
export type AIRoadmapType = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
function renderProgress(progressList: UserProgressResponse) {
|
||||
progressList.forEach((progress) => {
|
||||
const href =
|
||||
progress.resourceType === 'best-practice'
|
||||
? `/best-practices/${progress.resourceId}`
|
||||
: `/${progress.resourceId}`;
|
||||
const element = document.querySelector(`a[href="${href}"]`);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
type FavoriteRoadmapsProps = {
|
||||
progress: UserProgress[];
|
||||
projects: (ProjectStatusDocument & {
|
||||
title: string;
|
||||
})[];
|
||||
customRoadmaps: UserProgress[];
|
||||
aiRoadmaps: AIRoadmapType[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mark-favorite', {
|
||||
detail: {
|
||||
resourceId: progress.resourceId,
|
||||
resourceType: progress.resourceType,
|
||||
isFavorite: progress.isFavorite,
|
||||
},
|
||||
}),
|
||||
);
|
||||
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
||||
const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
|
||||
|
||||
const totalDone = progress.done + progress.skipped;
|
||||
const percentageDone = (totalDone / progress.total) * 100;
|
||||
|
||||
const progressBar: HTMLElement | null =
|
||||
element.querySelector('[data-progress]');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percentageDone}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type ProgressResponse = UserProgressResponse;
|
||||
|
||||
export function FavoriteRoadmaps() {
|
||||
const isAuthenticated = isLoggedIn();
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [isPreparing, setIsPreparing] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [progress, setProgress] = useState<ProgressResponse>([]);
|
||||
const [containerOpacity, setContainerOpacity] = useState(0);
|
||||
|
||||
function showProgressContainer() {
|
||||
const heroEl = document.getElementById('hero-text')!;
|
||||
if (!heroEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
heroEl.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
heroEl.parentElement?.removeChild(heroEl);
|
||||
setIsPreparing(false);
|
||||
|
||||
setTimeout(() => {
|
||||
setContainerOpacity(100);
|
||||
}, 50);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function loadProgress() {
|
||||
setIsLoading(true);
|
||||
|
||||
const { response: progressList, error } = await httpGet<ProgressResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
|
||||
);
|
||||
|
||||
if (error || !progressList) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(progressList);
|
||||
setIsLoading(false);
|
||||
showProgressContainer();
|
||||
|
||||
// render progress on featured items
|
||||
renderProgress(progressList);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProgress().finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('refresh-favorites', loadProgress);
|
||||
return () => window.removeEventListener('refresh-favorites', loadProgress);
|
||||
}, []);
|
||||
|
||||
if (isPreparing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasProgress = progress?.length > 0;
|
||||
const customRoadmaps = progress?.filter(
|
||||
(p) => p.isCustomResource && !p.team?.name,
|
||||
const completedProjects = projects.filter(
|
||||
(project) => project.submittedAt && project.repositoryUrl,
|
||||
);
|
||||
const inProgressProjects = projects.filter(
|
||||
(project) => !project.submittedAt || !project.repositoryUrl,
|
||||
);
|
||||
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
|
||||
const teamRoadmaps: HeroTeamRoadmaps = progress
|
||||
?.filter((p) => p.isCustomResource && p.team?.name)
|
||||
.reduce((acc: HeroTeamRoadmaps, curr) => {
|
||||
const currTeam = curr.team!;
|
||||
if (!acc[currTeam.name]) {
|
||||
acc[currTeam.name] = [];
|
||||
}
|
||||
|
||||
acc[currTeam.name].push(curr);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
const projectsToShow = [
|
||||
...inProgressProjects,
|
||||
...(showCompleted ? completedProjects : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
|
||||
>
|
||||
<div
|
||||
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
|
||||
hasProgress && `border-t border-t-[#1e293c]`
|
||||
}`}
|
||||
<div className="flex flex-col">
|
||||
{isCreatingCustomRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
onClose={() => {
|
||||
setIsCreatingCustomRoadmap(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your progress and bookmarks"
|
||||
isEmpty={!isLoading && progress.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No bookmarks found
|
||||
<a
|
||||
href="#role-based-roadmaps"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Bookmark a roadmap
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="container min-h-full">
|
||||
{!isLoading && progress?.length == 0 && <EmptyProgress />}
|
||||
{hasProgress && (
|
||||
<HeroRoadmaps
|
||||
teamRoadmaps={teamRoadmaps}
|
||||
customRoadmaps={customRoadmaps}
|
||||
progress={defaultRoadmaps}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{progress.map((resource) => (
|
||||
<HeroRoadmap
|
||||
key={`${resource.resourceType}-${resource.resourceId}`}
|
||||
resourceId={resource.resourceId}
|
||||
resourceType={resource.resourceType}
|
||||
resourceTitle={resource.resourceTitle}
|
||||
isFavorite={resource.isFavorite}
|
||||
percentageDone={
|
||||
((resource.skipped + resource.done) / resource.total) * 100
|
||||
}
|
||||
url={
|
||||
resource.resourceType === 'roadmap'
|
||||
? `/${resource.resourceId}`
|
||||
: `/best-practices/${resource.resourceId}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your custom roadmaps"
|
||||
isEmpty={!isLoading && customRoadmaps.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No custom roadmaps found
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsCreatingCustomRoadmap(true);
|
||||
}}
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Create custom roadmap
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{customRoadmaps.map((customRoadmap) => (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
))}
|
||||
<CreateRoadmapButton />
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<Sparkle className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your AI roadmaps"
|
||||
isEmpty={!isLoading && aiRoadmaps.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No AI roadmaps found
|
||||
<a
|
||||
href="/ai"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Generate AI roadmap
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{aiRoadmaps.map((aiRoadmap) => (
|
||||
<HeroRoadmap
|
||||
key={aiRoadmap.id}
|
||||
resourceId={aiRoadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={aiRoadmap.title}
|
||||
url={`/ai/${aiRoadmap.slug}`}
|
||||
percentageDone={0}
|
||||
allowFavorite={false}
|
||||
isTrackable={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
<a
|
||||
href="/ai"
|
||||
className={
|
||||
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300'
|
||||
}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Generate New
|
||||
</a>
|
||||
</HeroItemsGroup>
|
||||
|
||||
<HeroItemsGroup
|
||||
icon={<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
isLoading={isLoading}
|
||||
title="Your active projects"
|
||||
isEmpty={!isLoading && projectsToShow.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No active projects found
|
||||
<a
|
||||
href="/projects"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
|
||||
Start a new project
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
rightContent={
|
||||
completedProjects.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowCompleted(!showCompleted)}
|
||||
className="hidden items-center gap-2 rounded-md text-xs text-slate-400 hover:text-slate-300 sm:flex"
|
||||
>
|
||||
{showCompleted ? (
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{completedProjects.length} Completed
|
||||
</button>
|
||||
)
|
||||
}
|
||||
className="border-b-0"
|
||||
>
|
||||
{projectsToShow.map((project) => (
|
||||
<HeroProject key={project._id} project={project} />
|
||||
))}
|
||||
|
||||
<a
|
||||
href="/projects"
|
||||
className="flex min-h-[80px] items-center justify-center gap-2 rounded-md border border-dashed border-slate-800 p-4 text-sm text-slate-400 hover:border-slate-600 hover:bg-slate-900/50 hover:text-slate-300"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Start a new project
|
||||
</a>
|
||||
</HeroItemsGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
78
src/components/HeroSection/HeroItemsGroup.tsx
Normal file
78
src/components/HeroSection/HeroItemsGroup.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { HeroTitle } from './HeroTitle';
|
||||
|
||||
type HeroItemsGroupProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
isEmpty?: boolean;
|
||||
emptyTitle?: ReactNode;
|
||||
title: string | ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function HeroItemsGroup(props: HeroItemsGroupProps) {
|
||||
const {
|
||||
icon,
|
||||
isLoading = false,
|
||||
isEmpty = false,
|
||||
emptyTitle,
|
||||
title,
|
||||
rightContent,
|
||||
children,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const storageKey = `hero-group-${title}-collapsed`;
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
function isCollapsedByStorage() {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
|
||||
return stored === 'true';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollapsed(isCollapsedByStorage());
|
||||
}, [isLoading]);
|
||||
|
||||
const isLoadingOrCollapsedOrEmpty = isLoading || isCollapsed || isEmpty;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b border-gray-800/50',
|
||||
{
|
||||
'py-4': !isLoadingOrCollapsedOrEmpty,
|
||||
'py-4 ': isLoadingOrCollapsedOrEmpty,
|
||||
'opacity-50 transition-opacity hover:opacity-100':
|
||||
isCollapsed && !isLoading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="container">
|
||||
<HeroTitle
|
||||
icon={icon}
|
||||
isLoading={isLoading}
|
||||
isEmpty={isEmpty}
|
||||
emptyTitle={emptyTitle}
|
||||
title={title}
|
||||
rightContent={rightContent}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={() => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
localStorage.setItem(storageKey, (!isCollapsed).toString());
|
||||
}}
|
||||
/>
|
||||
{!isLoadingOrCollapsedOrEmpty && (
|
||||
<div className="mt-4 grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/HeroSection/HeroProject.tsx
Normal file
52
src/components/HeroSection/HeroProject.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ThumbsUp } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
|
||||
|
||||
type HeroProjectProps = {
|
||||
project: ProjectStatusDocument & {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function HeroProject({ project }: HeroProjectProps) {
|
||||
return (
|
||||
<a
|
||||
href={`/projects/${project.projectId}`}
|
||||
className="group relative flex flex-col justify-between gap-2 rounded-md border border-slate-800 bg-slate-900 p-3.5 hover:border-slate-600"
|
||||
>
|
||||
<div className="relative z-10 flex items-start justify-between gap-2">
|
||||
<h3 className="truncate font-medium text-slate-300 group-hover:text-slate-100">
|
||||
{project.title}
|
||||
</h3>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -right-2 -top-2 flex flex-shrink-0 items-center gap-1 rounded-full text-xs uppercase tracking-wide',
|
||||
{
|
||||
'text-green-600/50': project.submittedAt && project.repositoryUrl,
|
||||
'text-yellow-600': !project.submittedAt || !project.repositoryUrl,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{project.submittedAt && project.repositoryUrl ? 'Done' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center gap-2 text-xs text-slate-400">
|
||||
{project.submittedAt && project.repositoryUrl && (
|
||||
<span className="flex items-center gap-1">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
{project.upvotes}
|
||||
</span>
|
||||
)}
|
||||
{project.startedAt && (
|
||||
<span>Started {getRelativeTimeString(project.startedAt)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-slate-800/50 via-transparent to-transparent" />
|
||||
{project.submittedAt && project.repositoryUrl && (
|
||||
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-green-950/20 via-transparent to-transparent" />
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
74
src/components/HeroSection/HeroRoadmap.tsx
Normal file
74
src/components/HeroSection/HeroRoadmap.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import type { ResourceType } from '../../lib/resource-progress.ts';
|
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
|
||||
|
||||
type ProgressRoadmapProps = {
|
||||
url: string;
|
||||
percentageDone: number;
|
||||
allowFavorite?: boolean;
|
||||
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
resourceTitle: string;
|
||||
isFavorite?: boolean;
|
||||
|
||||
isTrackable?: boolean;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
export function HeroRoadmap(props: ProgressRoadmapProps) {
|
||||
const {
|
||||
url,
|
||||
percentageDone,
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
isFavorite,
|
||||
allowFavorite = true,
|
||||
isTrackable = true,
|
||||
isNew = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className={cn(
|
||||
'relative flex flex-col overflow-hidden rounded-md border p-3 text-sm text-slate-400 hover:text-slate-300',
|
||||
{
|
||||
'border-slate-800 bg-slate-900 hover:border-slate-600': isTrackable,
|
||||
'border-slate-700/50 bg-slate-800/50 hover:border-slate-600/70':
|
||||
!isTrackable,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<span title={resourceTitle} className="relative z-20 truncate">
|
||||
{resourceTitle}
|
||||
</span>
|
||||
|
||||
{isTrackable && (
|
||||
<span
|
||||
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
||||
style={{ width: `${percentageDone}%` }}
|
||||
></span>
|
||||
)}
|
||||
|
||||
{allowFavorite && (
|
||||
<MarkFavorite
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
favorite={isFavorite}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isNew && (
|
||||
<span className="absolute bottom-1.5 right-2 flex items-center rounded-br rounded-tl text-xs font-medium text-purple-300">
|
||||
<span className="mr-1.5 flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-purple-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-purple-500" />
|
||||
</span>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import type { UserProgressResponse } from './FavoriteRoadmaps';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { MapIcon, Users2 } from 'lucide-react';
|
||||
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
|
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx';
|
||||
|
||||
type ProgressRoadmapProps = {
|
||||
url: string;
|
||||
percentageDone: number;
|
||||
allowFavorite?: boolean;
|
||||
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
resourceTitle: string;
|
||||
isFavorite?: boolean;
|
||||
};
|
||||
function HeroRoadmap(props: ProgressRoadmapProps) {
|
||||
const {
|
||||
url,
|
||||
percentageDone,
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceTitle,
|
||||
isFavorite,
|
||||
allowFavorite = true,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
|
||||
>
|
||||
<span className="relative z-20">{resourceTitle}</span>
|
||||
|
||||
<span
|
||||
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
|
||||
style={{ width: `${percentageDone}%` }}
|
||||
></span>
|
||||
|
||||
{allowFavorite && (
|
||||
<MarkFavorite
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
favorite={isFavorite}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
type ProgressTitleProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
title: string | ReactNode;
|
||||
};
|
||||
|
||||
export function HeroTitle(props: ProgressTitleProps) {
|
||||
const { isLoading = false, title, icon } = props;
|
||||
|
||||
return (
|
||||
<p className="mb-4 flex items-center text-sm text-gray-400">
|
||||
{!isLoading && icon}
|
||||
{isLoading && (
|
||||
<span className="mr-1.5">
|
||||
<Spinner />
|
||||
</span>
|
||||
)}
|
||||
{title}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
|
||||
|
||||
type ProgressListProps = {
|
||||
progress: UserProgressResponse;
|
||||
customRoadmaps: UserProgressResponse;
|
||||
teamRoadmaps?: HeroTeamRoadmaps;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export function HeroRoadmaps(props: ProgressListProps) {
|
||||
const {
|
||||
teamRoadmaps = {},
|
||||
progress,
|
||||
isLoading = false,
|
||||
customRoadmaps,
|
||||
} = props;
|
||||
|
||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
||||
const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState<string>();
|
||||
|
||||
return (
|
||||
<div className="relative pb-12 pt-4 sm:pt-7">
|
||||
<p className="mb-7 mt-2 text-sm">
|
||||
<FeatureAnnouncement />
|
||||
</p>
|
||||
{isCreatingRoadmap && (
|
||||
<CreateRoadmapModal
|
||||
teamId={creatingRoadmapTeamId}
|
||||
onClose={() => {
|
||||
setIsCreatingRoadmap(false);
|
||||
setCreatingRoadmapTeamId(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<HeroTitle
|
||||
icon={
|
||||
(<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />) as any
|
||||
}
|
||||
isLoading={isLoading}
|
||||
title="Your progress and favorite roadmaps."
|
||||
/>
|
||||
}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{progress.map((resource) => (
|
||||
<HeroRoadmap
|
||||
key={`${resource.resourceType}-${resource.resourceId}`}
|
||||
resourceId={resource.resourceId}
|
||||
resourceType={resource.resourceType}
|
||||
resourceTitle={resource.resourceTitle}
|
||||
isFavorite={resource.isFavorite}
|
||||
percentageDone={
|
||||
((resource.skipped + resource.done) / resource.total) * 100
|
||||
}
|
||||
url={
|
||||
resource.resourceType === 'roadmap'
|
||||
? `/${resource.resourceId}`
|
||||
: `/best-practices/${resource.resourceId}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
{
|
||||
<HeroTitle
|
||||
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
title="Your custom roadmaps"
|
||||
/>
|
||||
}
|
||||
|
||||
{customRoadmaps.length === 0 && (
|
||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
||||
You haven't created any custom roadmaps yet.{' '}
|
||||
<button
|
||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
||||
onClick={() => setIsCreatingRoadmap(true)}
|
||||
>
|
||||
Create one!
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{customRoadmaps.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{customRoadmaps.map((customRoadmap) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<CreateRoadmapButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Object.keys(teamRoadmaps).map((teamName) => {
|
||||
const currentTeam: UserProgressResponse[0]['team'] =
|
||||
teamRoadmaps?.[teamName]?.[0]?.team;
|
||||
const roadmapsList = teamRoadmaps[teamName].filter(
|
||||
(roadmap) => !!roadmap.resourceTitle,
|
||||
);
|
||||
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
|
||||
|
||||
return (
|
||||
<div className="mt-5" key={teamName}>
|
||||
{
|
||||
<HeroTitle
|
||||
icon={<Users2 className="mr-1.5 h-[14px] w-[14px]" />}
|
||||
title={
|
||||
<>
|
||||
Team{' '}
|
||||
<a
|
||||
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
|
||||
href={`/team/activity?t=${currentTeam?.id}`}
|
||||
>
|
||||
{teamName}
|
||||
</a>
|
||||
Roadmaps
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
{roadmapsList.length === 0 && (
|
||||
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
|
||||
Team does not have any roadmaps yet.{' '}
|
||||
{canManageTeam && (
|
||||
<button
|
||||
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
|
||||
onClick={() => {
|
||||
setCreatingRoadmapTeamId(currentTeam?.id);
|
||||
setIsCreatingRoadmap(true);
|
||||
}}
|
||||
>
|
||||
Create one!
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{roadmapsList.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{roadmapsList.map((customRoadmap) => {
|
||||
return (
|
||||
<HeroRoadmap
|
||||
key={customRoadmap.resourceId}
|
||||
resourceId={customRoadmap.resourceId}
|
||||
resourceType={'roadmap'}
|
||||
resourceTitle={customRoadmap.resourceTitle}
|
||||
percentageDone={
|
||||
((customRoadmap.skipped + customRoadmap.done) /
|
||||
customRoadmap.total) *
|
||||
100
|
||||
}
|
||||
url={`/r/${customRoadmap?.roadmapSlug}`}
|
||||
allowFavorite={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{canManageTeam && (
|
||||
<CreateRoadmapButton
|
||||
teamId={currentTeam?.id}
|
||||
text="Create Team Roadmap"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/components/HeroSection/HeroTitle.tsx
Normal file
71
src/components/HeroSection/HeroTitle.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { ChevronDown, ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type HeroTitleProps = {
|
||||
icon: any;
|
||||
isLoading?: boolean;
|
||||
title: string | ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
isCollapsed?: boolean;
|
||||
onToggleCollapse?: () => void;
|
||||
isEmpty?: boolean;
|
||||
emptyTitle?: ReactNode;
|
||||
};
|
||||
|
||||
export function HeroTitle(props: HeroTitleProps) {
|
||||
const {
|
||||
isLoading = false,
|
||||
title,
|
||||
icon,
|
||||
rightContent,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse,
|
||||
isEmpty = false,
|
||||
emptyTitle,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="flex items-center gap-0.5 text-sm text-gray-400">
|
||||
{!isLoading && icon}
|
||||
{isLoading && (
|
||||
<span className="mr-1.5">
|
||||
<Spinner />
|
||||
</span>
|
||||
)}
|
||||
{!isEmpty ? title : emptyTitle || title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isCollapsed && rightContent}
|
||||
|
||||
{!isLoading && !isEmpty && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className={cn(
|
||||
'ml-2 inline-flex items-center gap-1 rounded-md bg-slate-800 py-0.5 pl-1 pr-1.5 text-xs uppercase tracking-wider text-slate-400 hover:bg-slate-700',
|
||||
{
|
||||
'bg-slate-800 text-slate-500 hover:bg-slate-800 hover:text-slate-400':
|
||||
!isCollapsed,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isCollapsed && (
|
||||
<>
|
||||
<ChevronsUpDown className="h-3.5 w-3.5" /> Expand
|
||||
</>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<ChevronsDownUp className="h-3.5 w-3.5" /> Collapse
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const { class: className } = Astro.props;
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'container prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10',
|
||||
'container prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h4:text-2xl prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10',
|
||||
className,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -4,8 +4,11 @@ import Icon from '../AstroIcon.astro';
|
||||
import { NavigationDropdown } from '../NavigationDropdown';
|
||||
import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
|
||||
---
|
||||
|
||||
<CourseAnnouncement client:load />
|
||||
|
||||
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
||||
<nav class='container flex items-center justify-between'>
|
||||
<div class='flex items-center gap-5'>
|
||||
@@ -18,20 +21,10 @@ import { AccountDropdown } from './AccountDropdown';
|
||||
</a>
|
||||
|
||||
<a
|
||||
href='/teams'
|
||||
class='group relative inline text-blue-300 hover:text-white sm:hidden'
|
||||
href='/roadmaps'
|
||||
class='group relative inline text-gray-400 hover:text-white sm:hidden'
|
||||
>
|
||||
Teams
|
||||
|
||||
<span class='absolute -right-[11px] top-0'>
|
||||
<span class='relative flex h-2 w-2'>
|
||||
<span
|
||||
class='absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75'
|
||||
></span>
|
||||
<span class='relative inline-flex h-2 w-2 rounded-full bg-sky-500'
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
Roadmaps
|
||||
</a>
|
||||
|
||||
<!-- Desktop navigation items -->
|
||||
@@ -46,19 +39,9 @@ import { AccountDropdown } from './AccountDropdown';
|
||||
</a>
|
||||
<a
|
||||
href='/changelog'
|
||||
class='group relative ml-0.5 hidden text-blue-300 hover:text-white md:block'
|
||||
class='group relative ml-0.5 hidden text-gray-400 hover:text-white md:block'
|
||||
>
|
||||
Changelog
|
||||
|
||||
<span class='absolute -right-[11px] top-0'>
|
||||
<span class='relative flex h-2 w-2'>
|
||||
<span
|
||||
class='absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75'
|
||||
></span>
|
||||
<span class='relative inline-flex h-2 w-2 rounded-full bg-sky-500'
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ export function NavigationDropdown() {
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[90] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
{
|
||||
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||
$navigationDropdownOpen,
|
||||
|
||||
@@ -37,6 +37,9 @@ export function OnboardingNudge(props: OnboardingNudgeProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @TODO put it back once <CourseAnnouncement /> is removed
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -16,6 +16,8 @@ import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import { SelectLanguages } from './SelectLanguages.tsx';
|
||||
import type { ProjectFrontmatter } from '../../lib/project.ts';
|
||||
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
|
||||
import { SortProjects } from './SortProjects.tsx';
|
||||
import { ProjectSolutionRow } from './ProjectSolutionRow';
|
||||
|
||||
export interface ProjectStatusDocument {
|
||||
_id?: string;
|
||||
@@ -57,11 +59,13 @@ type ListProjectSolutionsResponse = {
|
||||
type QueryParams = {
|
||||
p?: string;
|
||||
l?: string;
|
||||
s?: string;
|
||||
};
|
||||
|
||||
type PageState = {
|
||||
currentPage: number;
|
||||
language: string;
|
||||
sort: string;
|
||||
};
|
||||
|
||||
type ListProjectSolutionsProps = {
|
||||
@@ -69,30 +73,6 @@ type ListProjectSolutionsProps = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const submittedAlternatives = [
|
||||
'submitted their solution',
|
||||
'got it done',
|
||||
'submitted their take',
|
||||
'finished the project',
|
||||
'submitted their work',
|
||||
'completed the project',
|
||||
'got it done',
|
||||
'delivered their project',
|
||||
'handed in their solution',
|
||||
'provided their deliverables',
|
||||
'submitted their approach',
|
||||
'sent in their project',
|
||||
'presented their take',
|
||||
'shared their completed task',
|
||||
'submitted their approach',
|
||||
'completed it',
|
||||
'finalized their solution',
|
||||
'delivered their approach',
|
||||
'turned in their project',
|
||||
'submitted their final draft',
|
||||
'delivered their solution',
|
||||
];
|
||||
|
||||
export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
const { projectId, project: projectData } = props;
|
||||
|
||||
@@ -100,6 +80,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
const [pageState, setPageState] = useState<PageState>({
|
||||
currentPage: 0,
|
||||
language: '',
|
||||
sort: 'rating',
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -108,12 +89,17 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
ListProjectSolutionsResponse['data'][number] | null
|
||||
>(null);
|
||||
|
||||
const loadSolutions = async (page = 1, language: string = '') => {
|
||||
const loadSolutions = async (
|
||||
page = 1,
|
||||
language: string = '',
|
||||
sort: string = 'rating',
|
||||
) => {
|
||||
const { response, error } = await httpGet<ListProjectSolutionsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
|
||||
{
|
||||
currPage: page,
|
||||
...(language ? { languages: language } : {}),
|
||||
sort,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,6 +164,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
setPageState({
|
||||
currentPage: +(queryParams.p || '1'),
|
||||
language: queryParams.l || '',
|
||||
sort: queryParams.s || 'rating',
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -187,17 +174,27 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState.currentPage !== 1 || pageState.language !== '') {
|
||||
if (
|
||||
pageState.currentPage !== 1 ||
|
||||
pageState.language !== '' ||
|
||||
pageState.sort !== 'rating'
|
||||
) {
|
||||
setUrlParams({
|
||||
p: String(pageState.currentPage),
|
||||
l: pageState.language,
|
||||
s: pageState.sort,
|
||||
});
|
||||
} else {
|
||||
deleteUrlParam('p');
|
||||
deleteUrlParam('l');
|
||||
deleteUrlParam('s');
|
||||
}
|
||||
|
||||
loadSolutions(pageState.currentPage, pageState.language).finally(() => {
|
||||
loadSolutions(
|
||||
pageState.currentPage,
|
||||
pageState.language,
|
||||
pageState.sort,
|
||||
).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [pageState]);
|
||||
@@ -216,6 +213,13 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
|
||||
const selectedLanguage = pageState.language;
|
||||
|
||||
const setSelectedLanguage = (language: string) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 overflow-hidden rounded-lg border bg-white p-3 sm:p-5">
|
||||
{leavingRoadmapModal}
|
||||
@@ -224,19 +228,32 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
<h1 className="mb-1 text-xl font-semibold">
|
||||
{projectData.title} Solutions
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">{projectData.description}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Solutions submitted by the community
|
||||
</p>
|
||||
</div>
|
||||
{!isLoading && (
|
||||
<SelectLanguages
|
||||
projectId={projectId}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={(language) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<SortProjects
|
||||
selectedSort={pageState.sort}
|
||||
onSelectSort={(sort) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
sort,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<SelectLanguages
|
||||
projectId={projectId}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={(language) => {
|
||||
setPageState((prev) => ({
|
||||
...prev,
|
||||
language: prev.language === language ? '' : language,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -245,73 +262,16 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
||||
) : (
|
||||
<>
|
||||
<div className="flex min-h-[500px] flex-col divide-y divide-gray-100">
|
||||
{solutions?.data.map((solution, counter) => {
|
||||
const avatar = solution.user.avatar || '';
|
||||
return (
|
||||
<div
|
||||
key={solution._id}
|
||||
className="flex flex-col gap-2 py-2 text-sm text-gray-500"
|
||||
>
|
||||
<div className="flex flex-col justify-between gap-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<img
|
||||
src={
|
||||
avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png'
|
||||
}
|
||||
alt={solution.user.name}
|
||||
className="mr-0.5 h-7 w-7 rounded-full"
|
||||
/>
|
||||
<span className="font-medium text-black">
|
||||
{solution.user.name}
|
||||
</span>
|
||||
<span className="hidden sm:inline">
|
||||
{submittedAlternatives[
|
||||
counter % submittedAlternatives.length
|
||||
] || 'submitted their solution'}
|
||||
</span>{' '}
|
||||
<span className="flex-grow text-right text-gray-400 sm:flex-grow-0 sm:text-left sm:font-medium sm:text-black">
|
||||
{getRelativeTimeString(solution?.submittedAt!)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span className="flex shrink-0 overflow-hidden rounded-full border">
|
||||
<VoteButton
|
||||
icon={ThumbsUp}
|
||||
isActive={solution?.voteType === 'upvote'}
|
||||
count={solution.upvotes || 0}
|
||||
onClick={() => {
|
||||
handleSubmitVote(solution._id!, 'upvote');
|
||||
}}
|
||||
/>
|
||||
|
||||
<VoteButton
|
||||
icon={ThumbsDown}
|
||||
isActive={solution?.voteType === 'downvote'}
|
||||
count={solution.downvotes || 0}
|
||||
hideCount={true}
|
||||
onClick={() => {
|
||||
handleSubmitVote(solution._id!, 'downvote');
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="ml-1 flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
|
||||
onClick={() => {
|
||||
setShowLeavingRoadmapModal(solution);
|
||||
}}
|
||||
>
|
||||
<GitHubIcon className="h-4 w-4 text-current" />
|
||||
Visit Solution
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{solutions?.data.map((solution, counter) => (
|
||||
<ProjectSolutionRow
|
||||
key={solution._id}
|
||||
solution={solution}
|
||||
counter={counter}
|
||||
onVote={handleSubmitVote}
|
||||
onVisitSolution={setShowLeavingRoadmapModal}
|
||||
onLanguageClick={setSelectedLanguage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(solutions?.totalPages || 0) > 1 && (
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { ArrowUpRight, ThumbsDown, ThumbsUp } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { ModalLoader } from '../UserProgress/ModalLoader';
|
||||
import { Modal } from '../Modal';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import {
|
||||
submittedAlternatives,
|
||||
type AllowedVoteType,
|
||||
} from './ListProjectSolutions';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { ArrowUpRight, ThumbsDown, ThumbsUp, Trophy } from 'lucide-react';
|
||||
import { VoteButton } from './VoteButton';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { Modal } from '../Modal';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import { ModalLoader } from '../UserProgress/ModalLoader';
|
||||
import { type AllowedVoteType } from './ListProjectSolutions';
|
||||
import { VoteButton } from './VoteButton';
|
||||
|
||||
type UserProjectSolutionResponse = {
|
||||
id?: string;
|
||||
@@ -135,8 +132,12 @@ export function ProjectSolutionModal(props: ProjectSolutionModalProps) {
|
||||
bodyClassName={'h-auto'}
|
||||
>
|
||||
<div className="relative p-6">
|
||||
<h1 className="text-2xl text-balance mb-1 font-bold text-gray-900">{projectTitle}</h1>
|
||||
<p className="text-sm text-balance text-gray-600">{projectDescription}</p>
|
||||
<h1 className="mb-1 text-balance text-2xl font-bold text-gray-900">
|
||||
{projectTitle}
|
||||
</h1>
|
||||
<p className="text-balance text-sm text-gray-600">
|
||||
{projectDescription}
|
||||
</p>
|
||||
|
||||
<div className="my-5 rounded-lg bg-gray-100 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -150,7 +151,9 @@ export function ProjectSolutionModal(props: ProjectSolutionModalProps) {
|
||||
className="h-12 w-12 rounded-full border-2 border-white shadow-md"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{solution?.user.name}'s Solution</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{solution?.user.name}'s Solution
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Submitted their solution{' '}
|
||||
{getRelativeTimeString(solution?.submittedAt!)}
|
||||
|
||||
137
src/components/Projects/ProjectSolutionRow.tsx
Normal file
137
src/components/Projects/ProjectSolutionRow.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ThumbsDown, ThumbsUp } from 'lucide-react';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { VoteButton } from './VoteButton';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
|
||||
import type {
|
||||
AllowedVoteType,
|
||||
ProjectStatusDocument,
|
||||
} from './ListProjectSolutions';
|
||||
|
||||
export const submittedAlternatives = [
|
||||
'submitted their solution',
|
||||
'got it done',
|
||||
'submitted their take',
|
||||
'finished the project',
|
||||
'submitted their work',
|
||||
'completed the project',
|
||||
'got it done',
|
||||
'delivered their project',
|
||||
'handed in their solution',
|
||||
'provided their deliverables',
|
||||
'submitted their approach',
|
||||
'sent in their project',
|
||||
'presented their take',
|
||||
'shared their completed task',
|
||||
'submitted their approach',
|
||||
'completed it',
|
||||
'finalized their solution',
|
||||
'delivered their approach',
|
||||
'turned in their project',
|
||||
'submitted their final draft',
|
||||
'delivered their solution',
|
||||
];
|
||||
|
||||
type ProjectSolutionRowProps = {
|
||||
solution: ProjectStatusDocument & {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
voteType?: AllowedVoteType | 'none';
|
||||
};
|
||||
counter: number;
|
||||
onVote: (solutionId: string, voteType: AllowedVoteType) => void;
|
||||
onVisitSolution: (solution: ProjectSolutionRowProps['solution']) => void;
|
||||
onLanguageClick?: (language: string) => void;
|
||||
};
|
||||
|
||||
export function ProjectSolutionRow(props: ProjectSolutionRowProps) {
|
||||
const { solution, counter, onVote, onVisitSolution, onLanguageClick } = props;
|
||||
|
||||
const avatar = solution.user.avatar || '';
|
||||
|
||||
return (
|
||||
<div className="group flex flex-col border-gray-100 px-3 py-2.5 text-sm hover:bg-gray-50/50 sm:flex-row sm:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<img
|
||||
src={
|
||||
avatar
|
||||
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
|
||||
: '/images/default-avatar.png'
|
||||
}
|
||||
alt={solution.user.name}
|
||||
className="h-7 w-7 flex-shrink-0 rounded-full sm:h-8 sm:w-8"
|
||||
/>
|
||||
<div className="min-w-0 flex-auto">
|
||||
<div className="flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5">
|
||||
<span className="max-w-[150px] truncate font-medium text-gray-900 sm:max-w-[180px]">
|
||||
{solution.user.name}
|
||||
</span>
|
||||
<span className="hidden truncate text-xs text-gray-500 sm:block sm:text-sm">
|
||||
{submittedAlternatives[counter % submittedAlternatives.length] ||
|
||||
'submitted their solution'}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title={new Date(solution?.submittedAt!).toLocaleString()}
|
||||
>
|
||||
· {getRelativeTimeString(solution?.submittedAt!)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 flex gap-1.5">
|
||||
<div className="flex gap-1">
|
||||
<span className="flex shrink-0 overflow-hidden rounded-full border">
|
||||
<VoteButton
|
||||
icon={ThumbsUp}
|
||||
isActive={solution?.voteType === 'upvote'}
|
||||
count={solution.upvotes || 0}
|
||||
onClick={() => {
|
||||
onVote(solution._id!, 'upvote');
|
||||
}}
|
||||
/>
|
||||
|
||||
<VoteButton
|
||||
icon={ThumbsDown}
|
||||
isActive={solution?.voteType === 'downvote'}
|
||||
count={solution.downvotes || 0}
|
||||
hideCount={true}
|
||||
onClick={() => {
|
||||
onVote(solution._id!, 'downvote');
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
|
||||
onClick={() => {
|
||||
onVisitSolution(solution);
|
||||
}}
|
||||
>
|
||||
<GitHubIcon className="h-3.5 w-3.5 text-current" />
|
||||
<span>Visit Solution</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 hidden sm:mt-0 sm:block sm:pl-4">
|
||||
{solution.languages && solution.languages.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{solution.languages.slice(0, 2).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => onLanguageClick?.(lang)}
|
||||
className="inline-flex items-center rounded-md border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-700 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900"
|
||||
>
|
||||
{lang}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { ChevronDown, X } from 'lucide-react';
|
||||
import { ChevronDown, Search, X } from 'lucide-react';
|
||||
|
||||
type SelectLanguagesProps = {
|
||||
projectId: string;
|
||||
@@ -14,10 +14,44 @@ export function SelectLanguages(props: SelectLanguagesProps) {
|
||||
const { projectId, onSelectLanguage, selectedLanguage } = props;
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLDivElement>(null);
|
||||
const toast = useToast();
|
||||
|
||||
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
|
||||
const filteredLanguages = distinctLanguages.filter((language) =>
|
||||
language.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
// Handle scrolling of highlighted option into view
|
||||
useEffect(() => {
|
||||
if (!isOpen || !optionsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = optionsRef.current.getElementsByTagName('button');
|
||||
const highlightedOption = options[highlightedIndex];
|
||||
if (!highlightedOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = optionsRef.current.getBoundingClientRect();
|
||||
const optionRect = highlightedOption.getBoundingClientRect();
|
||||
|
||||
const isAbove = optionRect.top < containerRect.top;
|
||||
const isBelow = optionRect.bottom > containerRect.bottom;
|
||||
|
||||
if (isAbove || isBelow) {
|
||||
highlightedOption.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
}, [highlightedIndex, isOpen]);
|
||||
|
||||
const loadDistinctLanguages = async () => {
|
||||
const { response, error } = await httpGet<string[]>(
|
||||
@@ -34,53 +68,124 @@ export function SelectLanguages(props: SelectLanguagesProps) {
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadDistinctLanguages().finally(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex">
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedLanguage || 'Select Language'}
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
{selectedLanguage && (
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev >= filteredLanguages.length - 1 ? 0 : prev + 1,
|
||||
);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev <= 0 ? filteredLanguages.length - 1 : prev - 1,
|
||||
);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (filteredLanguages[highlightedIndex]) {
|
||||
onSelectLanguage(filteredLanguages[highlightedIndex]);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-shrink-0">
|
||||
<div className="relative">
|
||||
<button
|
||||
className="ml-1 text-red-500 text-xs border border-red-500 rounded-md px-2 py-1"
|
||||
onClick={() => onSelectLanguage('')}
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Clear
|
||||
{selectedLanguage || 'Select Language'}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{selectedLanguage && (
|
||||
<button
|
||||
className="absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectLanguage('');
|
||||
}}
|
||||
>
|
||||
<X className="size-3" strokeWidth={2.5} />
|
||||
<span className="sr-only">Clear selection</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full z-10 w-full min-w-[200px] max-w-[200px] translate-y-1.5 overflow-hidden rounded-md border border-gray-300 bg-white p-1 shadow-lg"
|
||||
ref={dropdownRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{distinctLanguages.map((language) => {
|
||||
const isSelected = selectedLanguage === language;
|
||||
<div className="relative mb-1 px-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className="w-full rounded-md border border-gray-200 py-1.5 pl-9 pr-3 text-sm focus:border-gray-300 focus:outline-none"
|
||||
placeholder="Search languages..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setHighlightedIndex(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div ref={optionsRef} className="max-h-[200px] overflow-y-auto">
|
||||
{filteredLanguages.map((language, index) => {
|
||||
const isSelected = selectedLanguage === language;
|
||||
const isHighlighted = index === highlightedIndex;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
className="flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 aria-selected:bg-gray-100"
|
||||
onClick={() => {
|
||||
onSelectLanguage(language);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
{language}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
className={`flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 aria-selected:bg-gray-100 ${
|
||||
isHighlighted ? 'bg-gray-100' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelectLanguage(language);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(0);
|
||||
}}
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
{language}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredLanguages.length === 0 && (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
No languages found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
66
src/components/Projects/SortProjects.tsx
Normal file
66
src/components/Projects/SortProjects.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
type SortOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const sortOptions: SortOption[] = [
|
||||
{ label: 'Latest First', value: 'latest' },
|
||||
{ label: 'Oldest First', value: 'oldest' },
|
||||
{ label: 'Highest Rating', value: 'rating' },
|
||||
];
|
||||
|
||||
type SortProjectsProps = {
|
||||
selectedSort: string;
|
||||
onSelectSort: (sort: string) => void;
|
||||
};
|
||||
|
||||
export function SortProjects(props: SortProjectsProps) {
|
||||
const { selectedSort, onSelectSort } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
const selectedOption =
|
||||
sortOptions.find((option) => option.value === selectedSort) ||
|
||||
sortOptions[0];
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0" ref={dropdownRef}>
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedOption.label}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-10 mt-1.5 min-w-[150px] overflow-hidden rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
className={`flex w-full items-center px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 ${
|
||||
selectedSort === option.value ? 'bg-gray-100' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelectSort(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -287,28 +287,32 @@ export function ProjectStepper(props: ProjectStepperProps) {
|
||||
number={2}
|
||||
/>
|
||||
|
||||
<span className="text-gray-600 sm:hidden">·</span>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm sm:hidden',
|
||||
isCopied ? 'text-green-500' : 'text-gray-600',
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(projectSolutionUrl);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckIcon additionalClasses="h-3 w-3" />
|
||||
URL Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share className="h-3 w-3 stroke-[2.5px]" />
|
||||
Share your Solution
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{activeStep > 1 && (
|
||||
<>
|
||||
<span className="text-gray-600 sm:hidden">·</span>
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm sm:hidden',
|
||||
isCopied ? 'text-green-500' : 'text-gray-600',
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(projectSolutionUrl);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckIcon additionalClasses="h-3 w-3" />
|
||||
URL Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share className="h-3 w-3 stroke-[2.5px]" />
|
||||
Share your Solution
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<StepperStepSeparator isActive={activeStep > 1} />
|
||||
<MilestoneStep
|
||||
|
||||
@@ -103,7 +103,7 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
|
||||
{question.isLongAnswer && (
|
||||
<div
|
||||
className={`qa-answer prose prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:!mb-6 prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
className={`qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:!mb-6 prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
|
||||
@@ -149,6 +149,11 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{questionGroup.ending && (
|
||||
<div class='mb-5'>
|
||||
<div set:html={markdownToHtml(questionGroup.ending, false)} />
|
||||
</div>
|
||||
)}
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
20
src/components/ReactIcons/HackerNewsIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface HackerNewsIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HackerNewsIcon(props: HackerNewsIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
20
src/components/ReactIcons/RedditIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
interface RedditIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RedditIcon(props: RedditIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
fill="currentColor"
|
||||
className={cn('h-[26px] w-[26px]', className)}
|
||||
>
|
||||
<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.8a26.67 26.67 0 0 0-26.8 26.8c0 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.5a26.67 26.67 0 0 0-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.8s-11.9-26.8-26.8-26.8c-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>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function TwitterIcon(props: TwitterIconProps) {
|
||||
<rect width="23" height="23" rx="3" fill={boxColor} />
|
||||
<path
|
||||
d="M12.9285 10.3522L18.5135 4H17.1905L12.339 9.5144L8.467 4H4L9.8565 12.3395L4 19H5.323L10.443 13.1754L14.533 19H19M5.8005 4.97619H7.833L17.1895 18.0718H15.1565"
|
||||
fill='currentColor'
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
|
||||
import { SelectionButton } from './SelectionButton';
|
||||
import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage';
|
||||
|
||||
type RoadmapSelectProps = {
|
||||
selectedRoadmaps: string[];
|
||||
|
||||
@@ -64,7 +64,7 @@ export function RoadmapDropdownMenu() {
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-2xl transition-all duration-100',
|
||||
'pointer-events-none invisible absolute left-0 top-full z-[90] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-2xl transition-all duration-100',
|
||||
{
|
||||
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||
$roadmapsDropdownOpen,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { ShareRoadmapButton } from './ShareRoadmapButton';
|
||||
import { DownloadRoadmapButton } from './DownloadRoadmapButton';
|
||||
|
||||
import { CourseAnnouncement } from './SQLCourse/CourseAnnouncement';
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
@@ -33,7 +33,7 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative hidden rounded-b-[5px] border-t bg-white text-sm font-medium hover:bg-gray-50 sm:block',
|
||||
'relative block rounded-b-[5px] border-t bg-white text-sm font-medium hover:bg-gray-50 sm:block',
|
||||
{
|
||||
'rounded-0 -mx-4 sm:mx-0': isAnswerVisible,
|
||||
// @FIXME:
|
||||
@@ -41,7 +41,6 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
|
||||
// the frontend roadmap. This is because we did not use to have the question
|
||||
// on mobile devices before and we don't want to cause any SEO issues. It will
|
||||
// be enabled on other roadmaps in the future.
|
||||
block: roadmapId === 'frontend',
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -10,8 +10,27 @@ import {
|
||||
} from '../../lib/browser.ts';
|
||||
import { RoadmapCard } from './RoadmapCard.tsx';
|
||||
import { httpGet } from '../../lib/http.ts';
|
||||
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx';
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
|
||||
|
||||
export type UserProgressResponse = {
|
||||
resourceId: string;
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceTitle: string;
|
||||
isFavorite: boolean;
|
||||
done: number;
|
||||
learning: number;
|
||||
skipped: number;
|
||||
total: number;
|
||||
updatedAt: Date;
|
||||
isCustomResource: boolean;
|
||||
roadmapSlug?: string;
|
||||
team?: {
|
||||
name: string;
|
||||
id: string;
|
||||
role: AllowedMemberRoles;
|
||||
};
|
||||
}[];
|
||||
|
||||
const groupNames = [
|
||||
'Absolute Beginners',
|
||||
@@ -238,6 +257,12 @@ const groups: GroupType[] = [
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Cloudflare',
|
||||
link: '/cloudflare',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Linux',
|
||||
link: '/linux',
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
} from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { BuyButton, COURSE_SLUG } from './BuyButton';
|
||||
import { BuyButton, SQL_COURSE_SLUG } from './BuyButton';
|
||||
|
||||
export function AccountButton() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(COURSE_SLUG),
|
||||
courseProgressOptions(SQL_COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export function AccountButton() {
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`}
|
||||
href={`${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`}
|
||||
className={`${buttonClasses} animate-fade-in`}
|
||||
>
|
||||
Start Learning
|
||||
|
||||
41
src/components/SQLCourse/AuthorCredentials.tsx
Normal file
41
src/components/SQLCourse/AuthorCredentials.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Award } from 'lucide-react';
|
||||
|
||||
export function AuthorCredentials() {
|
||||
return (
|
||||
<div className="mx-auto mt-8 flex flex-col items-start gap-4 text-sm text-zinc-400 sm:flex-row sm:flex-wrap sm:items-center md:mt-12 md:justify-center md:gap-x-3 md:gap-y-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
|
||||
className="size-8 rounded-full object-cover mr-1.5"
|
||||
alt="Kamran Ahmed"
|
||||
/>
|
||||
<span>Course by</span>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
target="_blank"
|
||||
className="font-medium text-yellow-500 hover:text-yellow-400"
|
||||
>
|
||||
Kamran Ahmed
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden flex-wrap items-center gap-x-3 gap-y-2 sm:flex sm:justify-center">
|
||||
<a
|
||||
href="https://github.com/kamranahmedse"
|
||||
target="_blank"
|
||||
className="hidden items-center gap-1 sm:inline-flex text-yellow-500 hover:text-yellow-400"
|
||||
>
|
||||
<svg className="size-4 fill-zinc-400" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.291 2.747-1.022 2.747-1.022.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
|
||||
</svg>
|
||||
#2 most-starred on GitHub
|
||||
</a>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<svg className="size-3 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
|
||||
</svg>
|
||||
founder roadmap.sh
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/components/SQLCourse/AuthorQuoteMessage.tsx
Normal file
72
src/components/SQLCourse/AuthorQuoteMessage.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Award, QuoteIcon, Trophy } from 'lucide-react';
|
||||
|
||||
export function AuthorQuoteMessage() {
|
||||
return (
|
||||
<div className="mx-auto mt-14 max-w-2xl sm:mt-20">
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-yellow-500/10 via-yellow-400/5 to-yellow-300/10 p-8 sm:p-12">
|
||||
<div className="absolute right-0 top-0 -translate-y-1/2 translate-x-1/2">
|
||||
<div className="size-[500px] rounded-full bg-yellow-500/5 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col items-center text-center">
|
||||
<h2 className="mb-4 hidden text-2xl font-semibold text-yellow-500 md:block">
|
||||
From your Instructor
|
||||
</h2>
|
||||
|
||||
<div className="mt-4 hidden flex-wrap items-center justify-center gap-x-4 gap-y-2 text-sm text-zinc-400 md:flex">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-yellow-500/10 px-3 py-1">
|
||||
<Trophy className="size-4 text-yellow-500/80" />
|
||||
Multiple GitHub Star Awards
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-yellow-500/10 px-3 py-1">
|
||||
<svg className="size-4 fill-yellow-500/80" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.291 2.747-1.022 2.747-1.022.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
|
||||
</svg>
|
||||
#2 Most Starred Developer
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-yellow-500/10 px-3 py-1">
|
||||
<Award className="size-4 text-yellow-500/80" />
|
||||
Founder roadmap.sh
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-yellow-500/10 px-3 py-1">
|
||||
<Award className="size-4 text-yellow-500/80" />
|
||||
Google Developer Expert
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-0 md:mt-8">
|
||||
<p className="text-base leading-relaxed text-zinc-200 sm:text-xl">
|
||||
"As someone who has worked extensively with databases throughout
|
||||
my career, I know firsthand how crucial SQL skills are. I've
|
||||
created this course to share the practical knowledge that has
|
||||
helped me build and scale data systems at various companies."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center gap-4">
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
|
||||
alt="Kamran Ahmed"
|
||||
className="size-14 rounded-full ring-2 ring-yellow-500/20"
|
||||
/>
|
||||
<div className="text-left">
|
||||
<h3 className="font-medium text-yellow-500">Kamran Ahmed</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Founder roadmap.sh{' '}
|
||||
<span className="mx-1 hidden sm:inline">·</span>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-0.5 text-yellow-500/80 underline underline-offset-4 hover:text-yellow-500"
|
||||
>
|
||||
@kamrify
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { ArrowRightIcon, Play } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { COURSE_PURCHASE_PARAM, isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
COURSE_PURCHASE_PARAM,
|
||||
COURSE_PURCHASE_SUCCESS_PARAM,
|
||||
isLoggedIn,
|
||||
} from '../../lib/jwt';
|
||||
import { coursePriceOptions } from '../../queries/billing';
|
||||
import { courseProgressOptions } from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
@@ -10,8 +14,9 @@ import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpPost } from '../../lib/query-http';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { VideoModal } from '../VideoModal';
|
||||
|
||||
export const COURSE_SLUG = 'master-sql';
|
||||
export const SQL_COURSE_SLUG = 'sql';
|
||||
|
||||
type CreateCheckoutSessionBody = {
|
||||
courseId: string;
|
||||
@@ -31,15 +36,16 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
const { variant = 'main' } = props;
|
||||
|
||||
const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false);
|
||||
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const { data: coursePricing, isLoading: isLoadingCourse } = useQuery(
|
||||
coursePriceOptions({ courseSlug: COURSE_SLUG }),
|
||||
coursePriceOptions({ courseSlug: SQL_COURSE_SLUG }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(COURSE_SLUG),
|
||||
courseProgressOptions(SQL_COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
@@ -58,7 +64,25 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
toast.loading('Creating checkout session...');
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
if (!window.gtag) {
|
||||
window.location.href = data.checkoutUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_begin_checkout`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Checkout Started`,
|
||||
callback: () => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
},
|
||||
});
|
||||
|
||||
// Hacky way to make sure that we redirect in case
|
||||
// GA was blocked or not able to redirect the user.
|
||||
setTimeout(() => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
}, 3000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
@@ -77,6 +101,32 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = getUrlParams();
|
||||
const param = urlParams?.[COURSE_PURCHASE_SUCCESS_PARAM];
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
|
||||
const success = param === '1';
|
||||
|
||||
if (success) {
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_purchase_complete`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Purchase Completed`,
|
||||
});
|
||||
} else {
|
||||
window?.fireEvent({
|
||||
action: `${SQL_COURSE_SLUG}_purchase_canceled`,
|
||||
category: 'course',
|
||||
label: `${SQL_COURSE_SLUG} Course Purchase Canceled`,
|
||||
});
|
||||
}
|
||||
|
||||
deleteUrlParam(COURSE_PURCHASE_SUCCESS_PARAM);
|
||||
}, []);
|
||||
|
||||
const isLoadingPricing =
|
||||
isLoadingCourse || !coursePricing || isLoadingCourseProgress;
|
||||
const isAlreadyEnrolled = !!courseProgress?.enrolledAt;
|
||||
@@ -87,9 +137,9 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
}
|
||||
|
||||
createCheckoutSession({
|
||||
courseId: COURSE_SLUG,
|
||||
success: `/courses/sql?e=1`,
|
||||
cancel: `/courses/sql`,
|
||||
courseId: SQL_COURSE_SLUG,
|
||||
success: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=1`,
|
||||
cancel: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=0`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,7 +151,7 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt;
|
||||
if (hasEnrolled) {
|
||||
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`;
|
||||
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,6 +166,12 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
return (
|
||||
<div className="relative flex w-full flex-col items-center gap-2 md:w-auto">
|
||||
{courseLoginPopup}
|
||||
{isVideoModalOpen && (
|
||||
<VideoModal
|
||||
videoId="6S1CcF-ngeQ"
|
||||
onClose={() => setIsVideoModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
@@ -133,8 +189,7 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
{coursePricing?.isEligibleForDiscount && coursePricing?.flag} Buy
|
||||
now for{' '}
|
||||
Buy now for{' '}
|
||||
{coursePricing?.isEligibleForDiscount ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="hidden text-base line-through opacity-75 md:inline">
|
||||
@@ -151,14 +206,18 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!isLoadingPricing &&
|
||||
!isAlreadyEnrolled &&
|
||||
coursePricing?.isEligibleForDiscount && (
|
||||
<span className="absolute top-full translate-y-2.5 text-sm text-yellow-400">
|
||||
{coursePricing.regionalDiscountPercentage}% regional discount
|
||||
applied
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isLoadingPricing && (
|
||||
<span className="absolute top-full z-50 flex w-[300px] translate-y-3 flex-row items-center justify-center text-sm text-yellow-400">
|
||||
Lifetime access <span className="mx-2">·</span>{' '}
|
||||
<button
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="flex cursor-pointer flex-row items-center gap-1.5 underline underline-offset-4 hover:text-yellow-500"
|
||||
>
|
||||
<Play className="size-3 fill-current" /> Watch Video (3 min)
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -195,17 +254,15 @@ export function BuyButton(props: BuyButtonProps) {
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
{coursePricing?.flag && coursePricing.isEligibleForDiscount
|
||||
? coursePricing.flag
|
||||
: null}{' '}
|
||||
Buy Now ${coursePricing?.regionalPrice}
|
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!isAlreadyEnrolled && coursePricing?.isEligibleForDiscount && (
|
||||
|
||||
{!isLoadingPricing && !isAlreadyEnrolled && (
|
||||
<span className="top-full text-sm text-yellow-400">
|
||||
{coursePricing.regionalDiscountPercentage}% regional discount applied
|
||||
Lifetime access <span className="mx-1">·</span> Free updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
43
src/components/SQLCourse/CourseAnnouncement.tsx
Normal file
43
src/components/SQLCourse/CourseAnnouncement.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Database, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function CourseAnnouncement() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-[91]">
|
||||
<a
|
||||
href="/courses/sql"
|
||||
className="flex items-center bg-yellow-400 py-1.5"
|
||||
>
|
||||
<span className="container mx-auto flex items-center justify-start sm:justify-center gap-2 text-center sm:gap-4">
|
||||
<span className="flex items-center gap-1.5 text-xs font-medium text-black md:text-base">
|
||||
<Database className="hidden h-4 w-4 flex-shrink-0 text-black sm:block" />
|
||||
<span className="hidden sm:block">
|
||||
Master SQL with our new paid course
|
||||
</span>
|
||||
<span className="block sm:hidden">Announcing our SQL course</span>
|
||||
</span>
|
||||
<span className="items-center gap-1.5 rounded-full bg-black px-2 py-0.5 text-sm text-xs font-medium uppercase tracking-wide text-white hover:bg-zinc-800 sm:px-3 sm:py-1">
|
||||
<span className="mr-1.5 hidden sm:inline">Start Learning</span>
|
||||
<span className="mr-1.5 inline sm:hidden">Visit</span>
|
||||
<span className="">→</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 rounded-lg px-1.5 py-1.5 text-gray-500 hover:bg-yellow-500 hover:text-gray-700"
|
||||
onClick={(e) => {
|
||||
setIsVisible(false);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,9 @@ export function CourseAuthor() {
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4">
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<img
|
||||
src="https://github.com/kamranahmedse.png"
|
||||
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
|
||||
className="size-12 rounded-full bg-yellow-500/10 md:size-16"
|
||||
alt="Kamran Ahmed"
|
||||
/>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
|
||||
34
src/components/SQLCourse/PlatformDemo.tsx
Normal file
34
src/components/SQLCourse/PlatformDemo.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { useState } from 'react';
|
||||
import { Play } from 'lucide-react';
|
||||
import { VideoModal } from '../VideoModal';
|
||||
|
||||
export function PlatformDemo() {
|
||||
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVideoModalOpen && (
|
||||
<VideoModal
|
||||
videoId="6S1CcF-ngeQ"
|
||||
onClose={() => setIsVideoModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="relative mt-12 w-full max-w-5xl sm:mt-24">
|
||||
<img
|
||||
src="https://assets.roadmap.sh/guest/course-environment-87jg8.png"
|
||||
alt="Course Environment"
|
||||
className="w-full rounded-xl"
|
||||
/>
|
||||
<div
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="group absolute inset-0 flex cursor-pointer items-center justify-center rounded-xl bg-black/40 transition-all hover:bg-black/50"
|
||||
>
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-white/90 transition-transform group-hover:scale-105 lg:size-16">
|
||||
<Play className="ml-1 fill-current text-black lg:size-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
254
src/components/SQLCourse/ReviewsSection.tsx
Normal file
254
src/components/SQLCourse/ReviewsSection.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import { ChevronDownIcon, StarIcon, User2Icon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../../editor/utils/classname';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
|
||||
type Review = {
|
||||
name: string;
|
||||
role: string;
|
||||
rating: number;
|
||||
text: string | string[];
|
||||
avatarUrl?: string;
|
||||
isProminent?: boolean;
|
||||
isSecondaryProminent?: boolean;
|
||||
};
|
||||
|
||||
export function ReviewsSection() {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const reviews: Review[] = [
|
||||
{
|
||||
name: 'Robin Wieruch',
|
||||
role: 'Author - Multiple best-selling books',
|
||||
rating: 5,
|
||||
text: [
|
||||
'Kamran has been in the **educative space for a long time**, and it shows in the way he teaches SQL: clear, structured, and straight to the point.',
|
||||
"He breaks down SQL fundamentals in a way that's both **intuitive and practical**, helping you not just write queries, but truly understand how databases work.",
|
||||
"Even if you've used SQL before, this **course will fill in gaps you didn't even realize you had**. Get ready to level up your database skills!",
|
||||
],
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/robin.jpeg',
|
||||
isProminent: true,
|
||||
},
|
||||
{
|
||||
name: 'William Imoh',
|
||||
role: 'Founder and Data Enthusiast',
|
||||
rating: 5,
|
||||
text: [
|
||||
'I have been working with SQL and databases for a long time, I bought this course for the advanced chapters but ended up completing the entire course. I learned a lot of new things and it was **well worth the investment**.',
|
||||
'No matter your SQL experience, this course is **a must-have** if you want to level up your SQL and data analysis skills. Highly recommended!',
|
||||
],
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/william-imoh-sd2dk.jpg',
|
||||
isProminent: true,
|
||||
},
|
||||
{
|
||||
name: 'Tomáš Janků',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: "The course and it's interactivity is excellent and I'd honestly say it's **one of the best** on the SQL theme I've seen out there.",
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/tomas-janku-6bg89.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Gourav Khunger',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'This course was **absolutely brilliant!** The integrated database environment to practice what I learned was the best part.',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/gourav-h2f3a.png',
|
||||
},
|
||||
{
|
||||
name: 'Meabed',
|
||||
role: 'CTO',
|
||||
rating: 5,
|
||||
text: 'Kamran has **clearly put a lot of thought** into this course. The content, structure and exercises were all great.',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/meabed-fu83q.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Mohsin Aheer',
|
||||
role: 'Sr. Software Engineer',
|
||||
rating: 5,
|
||||
text: 'I already knew SQL but this course **taught me a bunch of new things.** Practical examples and challenges were great. Highly recommended!',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/mohsinaheer-szchu.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Reeve Tee',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'I found the course **highly comprehensive and incredibly valuable**. I would love to see more courses like this!',
|
||||
avatarUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Zeeshan',
|
||||
role: 'Sr. Software Engineer',
|
||||
rating: 5,
|
||||
text: 'Loved the teaching style and the way the course was structured. The **AI tutor was a great help** when I got stuck.',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/ziishaned-qjepj.png',
|
||||
},
|
||||
{
|
||||
name: 'Adnan Ahmed',
|
||||
role: 'Engineering Manager',
|
||||
rating: 5,
|
||||
text: 'Having the integrated IDE made a huge difference. Being able to **immediately practice** what I learned was **invaluable**.',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/idnan-fzps5.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Kalvin Chakma',
|
||||
role: 'Jr. Software Engineer',
|
||||
rating: 5,
|
||||
text: "Best SQL course I've taken. The progression from basic to advanced concepts is **well thought out**, and the challenges are **excellent**.",
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/kalvin-d65ol.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Faisal Ahsan',
|
||||
role: 'Software Engineer',
|
||||
rating: 5,
|
||||
text: 'The course and the learning experience was great. What I really liked was the **no-fluff explanations** and **practical examples**.',
|
||||
avatarUrl: 'https://assets.roadmap.sh/guest/faisal-q78p2.jpeg',
|
||||
},
|
||||
];
|
||||
|
||||
const prominentReviews = reviews.filter((r) => r.isProminent);
|
||||
const regularReviews = reviews.filter((r) => !r.isProminent);
|
||||
|
||||
return (
|
||||
<div className="relative max-w-5xl">
|
||||
<div
|
||||
className={cn('rounded-2xl pb-0 pt-24', {
|
||||
'pb-8': isExpanded,
|
||||
})}
|
||||
>
|
||||
{/* Prominent Reviews */}
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="grid grid-cols-1 gap-4 md:gap-6 md:grid-cols-2">
|
||||
{prominentReviews.map((review, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="review-testimonial relative overflow-hidden rounded-2xl bg-gradient-to-br from-yellow-500/10 via-yellow-500/5 to-transparent p-8 backdrop-blur [&_strong]:font-normal [&_strong]:text-yellow-300/70"
|
||||
>
|
||||
<div className="absolute -right-8 -top-8 h-32 w-32 rounded-full bg-yellow-500/5" />
|
||||
<div className="flex items-center gap-4">
|
||||
{review.avatarUrl && (
|
||||
<img
|
||||
src={review.avatarUrl}
|
||||
alt={review.name}
|
||||
className="h-16 w-16 rounded-full border-2 border-yellow-500/20 object-cover"
|
||||
/>
|
||||
)}
|
||||
{!review.avatarUrl && (
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-zinc-800">
|
||||
<User2Icon className="h-8 w-8 text-zinc-400" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-zinc-100">
|
||||
{review.name}
|
||||
</h3>
|
||||
<p className="text-sm text-yellow-500/70">{review.role}</p>
|
||||
<div className="mt-1 flex">
|
||||
{Array.from({ length: review.rating }).map((_, i) => (
|
||||
<StarIcon
|
||||
key={i}
|
||||
className="h-4 w-4 fill-yellow-500 text-yellow-500"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
{(typeof review.text === 'string'
|
||||
? [review.text]
|
||||
: review.text
|
||||
).map((text, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className="text-zinc-300"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(text),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3',
|
||||
isExpanded ? '' : 'max-h-[400px] overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{regularReviews.map((review, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'review-testimonial flex-shrink-0 break-inside-avoid-column rounded-xl p-6 backdrop-blur [&_strong]:font-normal [&_strong]:text-yellow-300/70',
|
||||
{
|
||||
'bg-gradient-to-br from-yellow-500/10 via-yellow-500/5 to-transparent':
|
||||
review.isSecondaryProminent,
|
||||
'bg-zinc-800/30': !review.isSecondaryProminent,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
{review.avatarUrl && (
|
||||
<img
|
||||
src={review.avatarUrl}
|
||||
alt={review.name}
|
||||
className="h-12 w-12 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{!review.avatarUrl && (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-zinc-800">
|
||||
<User2Icon className="h-6 w-6 text-zinc-400" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-100">{review.name}</h3>
|
||||
<p className="text-sm text-zinc-400">{review.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
{Array.from({ length: review.rating }).map((_, i) => (
|
||||
<StarIcon
|
||||
key={i}
|
||||
className="h-4 w-4 fill-yellow-500 text-yellow-500"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className="mt-4 text-zinc-300"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(review.text),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-0 left-0 right-0 h-40 bg-gradient-to-t from-[#121212] via-[#121212]/80 to-transparent',
|
||||
isExpanded ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn('absolute left-1/2 top-full -translate-x-1/2', {
|
||||
'-translate-y-1/2': !isExpanded,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 rounded-full bg-zinc-800 px-6 py-2 text-sm font-medium text-zinc-300 transition-all hover:bg-zinc-700 hover:text-zinc-100"
|
||||
>
|
||||
{isExpanded ? 'Show Less' : 'Show More Reviews'}
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpDownIcon,
|
||||
BarChartIcon,
|
||||
BrainIcon,
|
||||
@@ -15,16 +14,20 @@ import {
|
||||
TableIcon,
|
||||
WrenchIcon,
|
||||
} from 'lucide-react';
|
||||
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
|
||||
import { AccountButton } from './AccountButton';
|
||||
import { AuthorCredentials } from './AuthorCredentials';
|
||||
import { AuthorQuoteMessage } from './AuthorQuoteMessage';
|
||||
import { BuyButton } from './BuyButton';
|
||||
import { ChapterRow } from './ChapterRow';
|
||||
import { CourseAuthor } from './CourseAuthor';
|
||||
import { CourseFeature } from './CourseFeature';
|
||||
import { FAQSection } from './FAQSection';
|
||||
import { FloatingPurchase } from './FloatingPurchase';
|
||||
import { PlatformDemo } from './PlatformDemo';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { Spotlight } from './Spotlight';
|
||||
import { FloatingPurchase } from './FloatingPurchase';
|
||||
import { CourseAuthor } from './CourseAuthor';
|
||||
import { FAQSection } from './FAQSection';
|
||||
import { BuyButton } from './BuyButton';
|
||||
import { AccountButton } from './AccountButton';
|
||||
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
|
||||
|
||||
type ChapterData = {
|
||||
icon: React.ReactNode;
|
||||
@@ -242,7 +245,7 @@ export function SQLCoursePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col items-center bg-gradient-to-b from-zinc-900 to-zinc-950 px-4 pb-52 pt-3 text-zinc-400 md:px-10 md:pt-8">
|
||||
<div className="relative flex flex-grow flex-col items-center bg-gradient-to-b from-zinc-900 to-zinc-950 px-4 pb-52 pt-3 text-zinc-400 md:px-10 md:pt-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<a
|
||||
href="https://roadmap.sh"
|
||||
@@ -253,8 +256,9 @@ export function SQLCoursePage() {
|
||||
</a>
|
||||
<AccountButton />
|
||||
</div>
|
||||
<div className="relative mt-7 max-w-3xl text-left md:mt-20 md:text-center">
|
||||
<div className="relative mt-7 max-w-4xl text-left md:mt-20 md:text-center">
|
||||
<Spotlight className="left-[-170px] top-[-200px]" fill="#EAB308" />
|
||||
|
||||
<div className="inline-block rounded-full bg-yellow-500/10 px-4 py-1.5 text-base text-yellow-500 md:px-6 md:py-2 md:text-lg">
|
||||
<span className="hidden sm:block">
|
||||
Complete Course to Master Practical SQL
|
||||
@@ -269,6 +273,7 @@ export function SQLCoursePage() {
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<AuthorCredentials />
|
||||
<p className="mx-auto my-5 max-w-2xl text-xl text-zinc-300 md:my-12 lg:text-2xl">
|
||||
A structured course to master database querying - perfect for
|
||||
developers, data analysts, and anyone working with data.
|
||||
@@ -298,10 +303,16 @@ export function SQLCoursePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReviewsSection />
|
||||
|
||||
<PlatformDemo />
|
||||
|
||||
<AuthorQuoteMessage />
|
||||
|
||||
<SectionHeader
|
||||
title="Not your average SQL course"
|
||||
description="Built around a text-based interactive approach and packed with practical challenges, this course stands out with features that make it truly unique."
|
||||
className="mt-16 md:mt-32"
|
||||
description="This SQL programming class is designed to help you go from beginner to expert through hands-on practice with real-world scenarios, mastering everything from basic to complex queries."
|
||||
className="mt-16 md:mt-20"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mt-6 w-full max-w-5xl md:mt-10">
|
||||
@@ -375,7 +386,7 @@ export function SQLCoursePage() {
|
||||
title="About the Author"
|
||||
className="mt-12 md:mt-24"
|
||||
description={
|
||||
<div className="mt-2 md:mt-4 flex flex-col gap-4 md:gap-6 text-lg md:text-xl leading-[1.52]">
|
||||
<div className="mt-2 flex flex-col gap-4 text-lg leading-[1.52] md:mt-4 md:gap-6 md:text-xl">
|
||||
<p>
|
||||
I am Kamran Ahmed, an engineering leader with over a decade of
|
||||
experience in the tech industry. Throughout my career I have built
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
import Icon from '../AstroIcon.astro';
|
||||
|
||||
export interface Props {
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { pageUrl, description } = Astro.props;
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
---
|
||||
|
||||
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5 items-center'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700 mb-0.5'>
|
||||
<Icon icon='twitter' />
|
||||
</a>
|
||||
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='facebook' />
|
||||
</a>
|
||||
<a href={hnUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='hackernews' />
|
||||
</a>
|
||||
<a href={redditUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<Icon icon='reddit' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./sharer.js'></script>
|
||||
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
105
src/components/ShareIcons/ShareIcons.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { FacebookIcon } from '../ReactIcons/FacebookIcon';
|
||||
import { HackerNewsIcon } from '../ReactIcons/HackerNewsIcon';
|
||||
import { RedditIcon } from '../ReactIcons/RedditIcon';
|
||||
import { TwitterIcon } from '../ReactIcons/TwitterIcon';
|
||||
|
||||
type ShareIconsProps = {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
pageUrl: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export function ShareIcons(props: ShareIconsProps) {
|
||||
const { pageUrl, description, resourceType, resourceId } = props;
|
||||
|
||||
const shareIconsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
|
||||
const icons = [
|
||||
{
|
||||
url: twitterUrl,
|
||||
icon: (
|
||||
<TwitterIcon
|
||||
className="size-[24px] [&>path]:fill-[#E5E5E5]"
|
||||
boxColor="currentColor"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
url: fbUrl,
|
||||
icon: <FacebookIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: hnUrl,
|
||||
icon: <HackerNewsIcon className="size-[26px]" />,
|
||||
},
|
||||
{
|
||||
url: redditUrl,
|
||||
icon: <RedditIcon className="size-[26px]" />,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const shareIcons = shareIconsRef.current;
|
||||
if (!shareIcons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
shareIcons.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
shareIcons.classList.remove('hidden');
|
||||
};
|
||||
|
||||
onScroll();
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-[-18px] top-[110px] hidden h-full"
|
||||
ref={shareIconsRef}
|
||||
>
|
||||
<div className="sticky top-[100px] flex flex-col items-center gap-1.5">
|
||||
{icons.map((icon, index) => {
|
||||
const host = new URL(icon.url).host;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={icon.url}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
'text-gray-500 hover:text-gray-700',
|
||||
index === 0 && 'mt-0.5',
|
||||
)}
|
||||
onClick={() => {
|
||||
window.fireEvent({
|
||||
category: 'RoadmapShareLink',
|
||||
action: `Share Roadmap / ${resourceType} / ${resourceId} / ${host}`,
|
||||
label: icon.url,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{icon.icon}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export class Sharer {
|
||||
constructor() {
|
||||
this.init = this.init.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.shareIconsId = 'page-share-icons';
|
||||
}
|
||||
|
||||
get shareIconsEl() {
|
||||
return document.getElementById(this.shareIconsId);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
this.shareIconsEl.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.shareIconsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.shareIconsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sharer = new Sharer();
|
||||
sharer.init();
|
||||
@@ -1,7 +1,8 @@
|
||||
import '../FrameRenderer/FrameRenderer.css';
|
||||
import '../EditorRoadmap/EditorRoadmapRenderer.css';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import '../FrameRenderer/FrameRenderer.css';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import type { TeamMember } from './TeamProgressPage';
|
||||
@@ -18,6 +19,9 @@ import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
|
||||
import { renderFlowJSON } from '../../../editor/renderer/renderer.ts';
|
||||
import { getResourceMeta } from '../../lib/roadmap.ts';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -56,6 +60,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
useState<MemberProgressResponse>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const toast = useToast();
|
||||
const [renderer, setRenderer] = useState<PageType['renderer']>('balsamiq');
|
||||
|
||||
let resourceJsonUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
@@ -88,14 +93,25 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
async function renderResource(jsonUrl: string) {
|
||||
const page = await getResourceMeta(resourceType, resourceId);
|
||||
if (!page) {
|
||||
toast.error('Resource not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = page.renderer || 'balsamiq';
|
||||
setRenderer(renderer);
|
||||
|
||||
const res = await fetch(jsonUrl, {});
|
||||
const json = await res.json();
|
||||
const svg: SVGElement | null = await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
const svg =
|
||||
renderer === 'editor'
|
||||
? await renderFlowJSON(json as any)
|
||||
: await wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
@@ -136,10 +152,10 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
skipped = [],
|
||||
} = memberProgress;
|
||||
|
||||
done.forEach((id: string) => renderTopicProgress(id, 'done'));
|
||||
learning.forEach((id: string) => renderTopicProgress(id, 'learning'));
|
||||
skipped.forEach((id: string) => renderTopicProgress(id, 'skipped'));
|
||||
removed.forEach((id: string) => renderTopicProgress(id, 'removed'));
|
||||
done.forEach((id) => renderTopicProgress(id, 'done'));
|
||||
learning.forEach((id) => renderTopicProgress(id, 'learning'));
|
||||
skipped.forEach((id) => renderTopicProgress(id, 'skipped'));
|
||||
removed.forEach((id) => renderTopicProgress(id, 'removed'));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
@@ -262,7 +278,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
|
||||
<div
|
||||
id={'customized-roadmap'}
|
||||
id={renderer === 'editor' ? undefined : 'customized-roadmap'}
|
||||
className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressItem } from './MemberProgressItem';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $currentTeam } from '../../stores/team';
|
||||
import { GroupRoadmapItem } from './GroupRoadmapItem';
|
||||
import { getUrlParams, setUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { MemberProgressModal } from './MemberProgressModal';
|
||||
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
|
||||
import { canManageCurrentRoadmap } from '../../stores/roadmap.ts';
|
||||
import { MemberProgressItem } from './MemberProgressItem';
|
||||
import { MemberProgressModal } from './MemberProgressModal';
|
||||
|
||||
export type UserProgress = {
|
||||
resourceTitle: string;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useAuth } from '../../hooks/use-auth';
|
||||
import { ModalLoader } from './ModalLoader.tsx';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
import { X } from 'lucide-react';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
|
||||
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
|
||||
import { renderFlowJSON } from '../../../editor/renderer/renderer.ts';
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
import type { VideoFileType } from '../lib/video';
|
||||
|
||||
export interface Props {
|
||||
video: VideoFileType;
|
||||
}
|
||||
|
||||
const { video } = Astro.props;
|
||||
const { frontmatter, id } = video;
|
||||
---
|
||||
|
||||
<a
|
||||
class:list={[
|
||||
'block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b',
|
||||
]}
|
||||
href={`/videos/${id}`}
|
||||
>
|
||||
<span class='group-hover:translate-x-2 transition-transform'>
|
||||
{frontmatter.title}
|
||||
|
||||
{
|
||||
frontmatter.isNew && (
|
||||
<span class='bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5'>
|
||||
New
|
||||
<span class='hidden sm:inline'>
|
||||
·
|
||||
{new Date(frontmatter.date).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<span class='capitalize text-gray-500 text-xs hidden sm:block'>
|
||||
{frontmatter.duration}
|
||||
</span>
|
||||
|
||||
<span class='text-gray-400 text-xs block sm:hidden'> »</span>
|
||||
</a>
|
||||
28
src/components/VideoModal.tsx
Normal file
28
src/components/VideoModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Modal } from './Modal';
|
||||
|
||||
type VideoModalProps = {
|
||||
videoId: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function VideoModal(props: VideoModalProps) {
|
||||
const { videoId, onClose } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
wrapperClassName="w-[90vw] max-w-4xl h-auto"
|
||||
bodyClassName="p-0 bg-black"
|
||||
overlayClassName="items-start md:items-center"
|
||||
>
|
||||
<div className="relative w-full pt-[56.25%]">
|
||||
<iframe
|
||||
className="absolute inset-0 h-full w-full"
|
||||
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ pdfUrl: '/pdfs/best-practices/backend-performance.pdf'
|
||||
order: 1
|
||||
briefTitle: 'Backend Performance'
|
||||
briefDescription: 'Backend Performance Best Practices'
|
||||
isNew: true
|
||||
isNew: false
|
||||
isUpcoming: false
|
||||
title: 'Backend Performance Best Practices'
|
||||
description: 'Detailed list of best practices to improve your backend performance'
|
||||
|
||||
21
src/data/changelogs/cloudflare-roadmap-new-dashboard.md
Normal file
21
src/data/changelogs/cloudflare-roadmap-new-dashboard.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: 'Cloudflare and ASP.NET Roadmaps, New Dashboard'
|
||||
description: 'We just launched our first paid SQL course'
|
||||
images:
|
||||
'New Dashboard': 'https://assets.roadmap.sh/guest/new-dashboard.png'
|
||||
'Cloudflare Roadmap': 'https://assets.roadmap.sh/guest/cloudflare-roadmap.png'
|
||||
'ASP.NET Roadmap Revised': 'https://assets.roadmap.sh/guest/aspnet-core-revision.png'
|
||||
seo:
|
||||
title: 'Cloudflare and ASP.NET Roadmaps, New Dashboard'
|
||||
description: ''
|
||||
date: 2025-02-21
|
||||
---
|
||||
|
||||
We have launched a new Cloudflare roadmap, revised ASP.NET Core roadmap and introduced a new dashboard design.
|
||||
|
||||
- Brand new [Cloudflare roadmap](https://roadmap.sh/cloudflare) to help you learn Cloudflare
|
||||
- [ASP.NET Core roadmap](https://roadmap.sh/aspnet-core) has been revised with new content
|
||||
- Fresh new dashboard design with improved navigation and performance
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
|
||||
19
src/data/changelogs/sql-course.md
Normal file
19
src/data/changelogs/sql-course.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: "Our first paid course about SQL is live"
|
||||
description: 'We just launched our first paid SQL course'
|
||||
images:
|
||||
"SQL Course": "https://assets.roadmap.sh/guest/course-environment-87jg8.png"
|
||||
"Course Creator Platform": "https://assets.roadmap.sh/guest/course-creator-platform.png"
|
||||
seo:
|
||||
title: 'SQL Course is Live'
|
||||
description: ''
|
||||
date: 2025-02-04
|
||||
---
|
||||
|
||||
After months of work, I am excited to announce our [brand-new SQL course](/courses/sql) designed to help you master SQL with a hands-on, practical approach!
|
||||
|
||||
For the past few months, we have been working on building a course platform that would not only help us deliver high-quality educational content but also sustain the development of roadmap.sh. This SQL course is our first step in that direction.
|
||||
|
||||
The course has been designed with a focus on practical learning. Each topic is accompanied by hands-on exercises, real-world examples, and an integrated coding environment where you can practice what you learn. The AI integration provides personalized learning assistance, helping you grasp concepts better and faster.
|
||||
|
||||
Check out the course at [roadmap.sh/courses/sql](https://roadmap.sh/courses/sql)
|
||||
198
src/data/guides/ai-data-scientist-career-path.md
Normal file
198
src/data/guides/ai-data-scientist-career-path.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: 'Is Data Science a Good Career? Advice From a Pro'
|
||||
description: 'Is data science a good career choice? Learn from a professional about the benefits, growth potential, and how to thrive in this exciting field.'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/ai-data-scientist/career-path'
|
||||
seo:
|
||||
title: 'Is Data Science a Good Career? Advice From a Pro'
|
||||
description: 'Is data science a good career choice? Learn from a professional about the benefits, growth potential, and how to thrive in this exciting field.'
|
||||
ogImageUrl: 'https://assets.roadmap.sh/guest/is-data-science-a-good-career-10j3j.jpg'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2025-01-28
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||

|
||||
|
||||
Data science is one of the most talked-about career paths today, but is it the right fit for you?
|
||||
|
||||
With [data science](https://roadmap.sh/ai-data-scientist) at the intersection of technology, creativity, and impact, it can be a very appealing role. It definitely promises high and competitive salaries, and the chance to solve real-world problems. Who would say “no” to that?\!
|
||||
|
||||
But is it the right fit for your skills and aspirations?
|
||||
|
||||
In this guide, I’ll help you uncover the answer to that question by understanding the pros and cons of working as a data scientist. I’ll also look at what the data scientists’ salaries are like and the type of skills you’d need to have to succeed at the job.
|
||||
|
||||
Now sit down, relax, and read carefully, because I’m about to help you answer the question of “Is data science a good career for me?”.
|
||||
|
||||
## Pros of a Career in Data Science
|
||||
|
||||

|
||||
|
||||
There are plenty of “pros” when it comes to picking data science as your career, but let’s take a closer look at the main ones.
|
||||
|
||||
### High Demand and Job Security
|
||||
|
||||
The demand for data scientists has grown exponentially over the past few years and shows no signs of slowing down. According to the [U.S. Bureau of Labor Statistics](https://www.bls.gov/ooh/math/data-scientists.htm), the data science job market is projected to grow by 36% from 2023 to 2033, far outpacing the average for other fields.
|
||||
|
||||
This surge is partly due to the “explosion” of artificial intelligence, particularly tools like ChatGPT, in recent years, which have amplified the need for skilled data scientists to handle complex machine learning models and big data analysis.
|
||||
|
||||
### Competitive Salaries
|
||||
|
||||
One of the most appealing aspects of data science positions is the average data scientist’s salary. Reports from Glassdoor and Indeed highlight that data scientists are among the highest-paid professionals in the technology sector. For example, the national average salary for a data scientist in the United States is approximately $120,000 annually, with experienced professionals earning significantly more.
|
||||
|
||||
These salaries are a reflection of the reality: the high demand for [data science skills](https://roadmap.sh/ai-data-scientist/skills) and the technical expertise required for these roles are not easy to come by. What’s even more, companies in high-cost regions, such as Silicon Valley, New York City, and Seattle, tend to offer premium salaries to attract top talent.
|
||||
|
||||
The financial rewards in this field are usually complemented by additional benefits such as opportunities for professional development like research, publishing, patent registration, etc.
|
||||
|
||||
### Intellectual Challenge and Learning Opportunities
|
||||
|
||||
Data scientists work in a field that demands continuous learning and adaptation to emerging technologies. Their field is rooted in solving complex problems through a combination of technical knowledge, creativity, and critical thinking. In other words, they rarely have any time to get bored.
|
||||
|
||||
What makes data science important and intellectually rewarding, is its ability to address real-world problems. Whether it's optimizing healthcare systems, enhancing customer experiences in retail, or predicting financial risks, data science applications have a tangible impact on people.
|
||||
|
||||
This makes data science a good career for individuals who are passionate about lifelong learning and intellectual stimulation.
|
||||
|
||||
### Versatility
|
||||
|
||||
Data science is a good career choice for those who enjoy variety and flexibility. One of the unique aspects of a career in data science is its ability to reach across various industries and domains (I’m talking technology, healthcare, finance, e-commerce, and even entertainment to name a few). This means data scientists can apply their data science skills in almost any sector that generates or relies on data—which is virtually all industries today.
|
||||
|
||||
## Cons of a Career in Data Science
|
||||
|
||||

|
||||
|
||||
The data science job is not without its “cons”, after all, there is no “perfect” role out there. Let’s now review some of the potential challenges that come with the role.
|
||||
|
||||
### Steep Learning Curve
|
||||
|
||||
The steep learning curve in data science is one of the field’s defining characteristics. New data scientists have to develop a deep understanding of technical skills, including proficiency in programming languages like Python, R, and SQL, as well as tools for machine learning and data visualization.
|
||||
|
||||
On top of the already complex subjects to master, data scientists need to find ways of staying current with the constant advancements in the field. This is not optional; it’s a necessity for anyone trying to achieve long-term success in data science. This constant evolution can feel overwhelming, especially for newcomers who are also learning foundational skills.
|
||||
|
||||
Despite these challenges, the steep learning curve can be incredibly rewarding for those who are passionate about solving problems, making data-driven decisions, and contributing to impactful projects.
|
||||
|
||||
While it might sound harsh, it’s important to note that the dedication required to overcome these challenges often results in a fulfilling and (extremely) lucrative career in the world of data science.
|
||||
|
||||
### High Expectations
|
||||
|
||||
Data science positions come with high expectations from organizations. Data scientists usually have the huge responsibility of delivering actionable insights and ensuring these insights are both accurate and timely.
|
||||
|
||||
One of the key challenges data science professionals face is managing the pressure to deliver results under tight deadlines (they’re always tight). Stakeholders often expect instant answers to complex problems, which can lead to unrealistic demands.
|
||||
|
||||
To succeed in such environments, skilled data scientists need strong communication skills to explain their findings and set realistic expectations with stakeholders.
|
||||
|
||||
### Potential Burnout
|
||||
|
||||
The high demand for data science skills usually translates into heavy workloads and tight deadlines, particularly for data scientists working on high-stakes projects (working extra hours is also not an uncommon scenario).
|
||||
|
||||
Data scientists frequently juggle multiple complex responsibilities, such as modeling data, developing machine learning algorithms, and conducting statistical analysis—often within limited timeframes.
|
||||
|
||||
The intense focus required for these tasks, combined with overlapping priorities (and a small dose of poor project management), can lead to mental fatigue and stress.
|
||||
|
||||
Work-life balance can also be a challenge for data scientists giving them another reason for burnout. Combine that with highly active industries, like finance and you have a very hard-to-balance combination.
|
||||
|
||||
To mitigate the risk of burnout, data scientists can try to prioritize setting boundaries, managing workloads effectively (when that’s an option), and advocating for clearer role definitions (better separation of concerns).
|
||||
|
||||
## Skills Required for a Data Science Career
|
||||
|
||||

|
||||
|
||||
To develop a successful career in data science, not all of your [skills](https://roadmap.sh/ai-data-scientist/skills) need to be technical, you also have to look at soft skills, and domain knowledge and to have a mentality of lifelong learning.
|
||||
|
||||
Let’s take a closer look.
|
||||
|
||||
### Technical Skills
|
||||
|
||||
The field of data requires strong foundational technical skills. At the core of these skills is proficiency in programming languages such as Python, R, and SQL. Python is particularly useful and liked for its versatility and extensive libraries, while SQL is essential for querying and managing database systems. R remains a popular choice for statistical analysis and data visualization.
|
||||
|
||||
In terms of frameworks, look into TensorFlow, PyTorch, or Scikit-learn. They’re all crucial for building predictive models and implementing artificial intelligence solutions. Tools like Tableau, Power BI, and Matplotlib are fantastic for creating clear and effective data visualizations, which play a significant role in presenting actionable insights.
|
||||
|
||||
### Soft Skills
|
||||
|
||||
As I said before, it’s not all about technical skills. Data scientists must develop their soft skills, this is key in the field.
|
||||
|
||||
From problem-solving and analytical thinking to developing your communication skills and your ability to collaborate with others. They all work together to help you communicate complex insights and results to other, non-technical stakeholders, which is going to be a key activity in your day-to-day life.
|
||||
|
||||
### Domain Knowledge
|
||||
|
||||
While technical and soft skills are essential, domain knowledge often distinguishes exceptional data scientists from the rest. Understanding industry-specific contexts—such as healthcare regulations, financial market trends, or retail customer behavior—enables data scientists to deliver tailored insights that directly address business needs. If you understand your problem space, you understand the needs of your client and the data you’re dealing with.
|
||||
|
||||
Getting that domain knowledge often involves on-the-job experience, targeted research, or additional certifications.
|
||||
|
||||
### Lifelong Learning
|
||||
|
||||
Finally, if you’re going to be a data scientist, you’ll need to embrace a mindset of continuous learning. The field evolves rapidly, with emerging technologies, tools, and methodologies reshaping best practices. Staying competitive requires consistent professional development through online courses, certifications, conferences, and engagement with the broader data science community.
|
||||
|
||||
Lifelong learning is not just a necessity but also an opportunity to remain excited and engaged in a dynamic and rewarding career.
|
||||
|
||||
## How to determine if data science is right for you?
|
||||
|
||||

|
||||
|
||||
How can you tell if you’ll actually enjoy working as a data scientist? Even after reading this far, you might still have some doubts. So in this section, I’m going to look at some ways in which you can validate that you’ll enjoy the job of a data scientist before you go through the process of becoming one.
|
||||
|
||||
### Self-Assessment Questions
|
||||
|
||||
Figuring out whether data science is the right career path starts with introspection. Ask yourself the following:
|
||||
|
||||
* Do you enjoy working with numbers and solving complex problems?
|
||||
* Are you comfortable learning and applying programming skills like Python and SQL?
|
||||
* Are you excited by the idea of using algorithms to create data-driven insights and actionable recommendations?
|
||||
* Are you willing to commit to continuous learning in a fast-evolving field?
|
||||
|
||||
Take your time while you think about these questions. You don’t even need a full answer, just try to understand how you feel about the idea of each one. If you don’t feel like saying “yes”, then chances are, this might not be the right path for you (and that’s completely fine\!).
|
||||
|
||||
### Start with Small Projects
|
||||
|
||||
If self-assessment is not your thing, another great way to explore your interest in data science is to dive into small, manageable projects. Platforms like Kaggle offer competitions and publicly available data sets, allowing you to practice exploratory data analysis, data visualization, and predictive modeling. Working on these projects can help you build a portfolio, develop confidence in your skills, and validate that you effectively like working this way.
|
||||
|
||||
Online courses and certifications in data analytics, machine learning, and programming languages provide a structured way to build foundational knowledge. Resources like Coursera, edX, and DataCamp offer beginner-friendly paths to learning data science fundamentals.
|
||||
|
||||
### Network and Seek Mentorship
|
||||
|
||||
Another great way to understand if you would like to be a data scientist, is to ask other data scientists. It might sound basic, but it’s a very powerful way because you’ll get insights about the field from the source.
|
||||
|
||||
Networking, while not easy for everyone, is a key component of entering the data science field. Go to data science meetups, webinars, or conferences to expand your network and stay updated on emerging trends and technologies.
|
||||
|
||||
If you’re not into big groups, try seeking mentorship from data scientists already working in the field. This can accelerate your learning curve. Mentors can offer guidance on career planning, project selection, and skill development.
|
||||
|
||||
## Alternative career paths to consider
|
||||
|
||||

|
||||
|
||||
Not everyone who is interested in data science wants to pursue the full spectrum of technical skills or the specific responsibilities of a data scientist. Lucky for you, there are several related career paths that can still scratch your itch for fun and interesting challenges while working within the data ecosystem.
|
||||
|
||||
### Data-Adjacent Roles
|
||||
|
||||
* **Data Analyst**: If you enjoy working with data but prefer focusing on interpreting and visualizing it to inform business decisions, a data analyst role might be for you. Data analysts primarily work on identifying trends and providing actionable recommendations without diving deeply into machine learning or predictive modeling.
|
||||
* **Data Engineer**: If you’re more inclined toward building the infrastructure that makes data science possible, consider becoming a data engineer. These data professionals design, build, and maintain data pipelines, ensuring the accessibility and reliability of large data sets for analysis. The role requires expertise in database systems, data structures, and programming.
|
||||
|
||||
### Related Fields
|
||||
|
||||
* **Software Engineering**: For those who enjoy coding and software development but want to remain close to data-related projects, software engineering offers opportunities to build tools, applications, and systems that support data analysis and visualization.
|
||||
* **Cybersecurity**: With the increasing emphasis on data privacy and security, cybersecurity professionals play a critical role in protecting sensitive information. This field combines technical knowledge with policy enforcement, making it appealing to those interested in data protection and regulatory compliance.
|
||||
|
||||
### Non-Technical Roles in the Data Ecosystem
|
||||
|
||||
* **Data Governance**: If instead of transforming data and getting insights, you’d like to focus more on how the data is governed (accessed, controlled, cataloged, etc), then this might be the role for you. This role is essential for ensuring that an organization’s data assets are used effectively and responsibly.
|
||||
* **Data Privacy Office**: In a similar vein to a data governance officer, the data privacy officer cares for the actual privacy of the data. With the rise of AI, data is more relevant than ever, and controlling that you comply with regulations like GDPR and CCPA, is critical for organizations. This role focuses on data privacy strategies, audits, and risk management, making it an excellent fit for those interested in the legal and ethical aspects of data.
|
||||
|
||||
## Next steps
|
||||
|
||||

|
||||
|
||||
Data science is a promising career path offering high demand, competitive salaries, and multiple opportunities across various industries. Its ability to address real-world problems, combined with the intellectual challenge it presents, makes it an attractive choice for many. However, it also makes it a very difficult and taxing profession for those who don’t enjoy this type of challenge.
|
||||
|
||||
There are many potential next steps for you to take and answer the question of “Is data science a good career?”.
|
||||
|
||||
For example, you can reflect on your interests and strengths. Ask yourself whether or not you enjoy problem-solving, working with data sets, and learning new technologies. Use this reflection to determine if data science aligns with your career goals.
|
||||
|
||||
You can also consume resources like the [AI/Data Scientist roadmap](https://roadmap.sh/ai-data-scientist) and the [Data Analyst roadmap](https://roadmap.sh/data-analyst), as they offer a clear progression for developing essential skills, so check them out. These tools can help you identify which areas to focus on based on your current expertise and interests.
|
||||
|
||||
In the end, just remember: data science is rapidly evolving so make sure to stay engaged by reading research papers, following industry blogs, or attending conferences. Anything you can do will help, just figure out what works for you and keep doing it.
|
||||
200
src/data/guides/ai-data-scientist-lifecycle.md
Normal file
200
src/data/guides/ai-data-scientist-lifecycle.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: "Data Science Lifecycle 101: A Beginners' Ultimate Guide"
|
||||
description: 'Discover the Data Science Lifecycle step-by-step: Learn key phases, tools, and techniques in this beginner-friendly guide.'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/ai-data-scientist/career-path'
|
||||
seo:
|
||||
title: "Data Science Lifecycle 101: A Beginners' Ultimate Guide"
|
||||
description: 'Discover the Data Science Lifecycle step-by-step: Learn key phases, tools, and techniques in this beginner-friendly guide.'
|
||||
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-lifecycle-eib3s.jpg'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2025-01-29
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||

|
||||
|
||||
Developing a data science project, from beginning to production is not a trivial task. It involves so many steps and so many complex tasks, that without some guardrails, releasing to production becomes ten times harder.
|
||||
|
||||
Here’s where the data science lifecycle comes into play. It brings a structured approach so that [data scientists](https://roadmap.sh/ai-data-scientist), data analysts, and others can move forward together from raw data to actionable insights.
|
||||
|
||||
In this guide, we’ll cover everything you need to know about the data science lifecycle, its many variants, and how to pick the right one for your project.
|
||||
|
||||
So let’s get going\!
|
||||
|
||||
## Core Concepts of a Lifecycle
|
||||
|
||||

|
||||
|
||||
To fully understand the concept of the lifecycle, we have to look at the core concepts inside this framework, and how they contribute to the delivery of a successful data science project.
|
||||
|
||||
### Problem Definition
|
||||
|
||||
Every data science project begins with a clear definition of the problem to be solved. This involves collaborating with key stakeholders to identify objectives and desired outcomes. Data scientists must understand the context and scope of the project to ensure that the goals align with business or research needs.
|
||||
|
||||
### Data Collection
|
||||
|
||||
In the data collection phase, data scientists and data engineers work together and gather relevant data from diverse data sources. This includes both structured and unstructured data, such as historical records, new data, or data streams.
|
||||
|
||||
The process ensures the integration of all pertinent data, creating a robust dataset for the following stages. Data acquisition tools and strategies play a critical role in this phase.
|
||||
|
||||
### Data Preparation
|
||||
|
||||
This stage addresses the quality of raw data by cleaning and organizing it for analysis. Tasks such as treating inaccurate data, handling missing values, and converting raw data into usable formats are central to this stage. This stage prepares the data for further and more detailed analysis.
|
||||
|
||||
### Exploratory Data Analysis (EDA)
|
||||
|
||||
The exploratory data analysis stage is where the “data processing” happens. This stage focuses on uncovering patterns, trends, and relationships within the data. Through data visualization techniques such as bar graphs and statistical models, data scientists perform a thorough data analysis and gain insights into the data’s structure and characteristics.
|
||||
|
||||
Like every stage so far, this one lays the foundation for the upcoming stages. In this particular case, after performing a detailed EDA, data scientists have a much better understanding of the data they have to work with, and a pretty good idea of what they can do with it now.
|
||||
|
||||
### Model Building and Evaluation
|
||||
|
||||
The model building phase involves developing predictive or machine learning models tailored to the defined problem. Data scientists experiment with various machine learning algorithms and statistical models to determine the best approach. Here’s where data modeling happens, bridging the insights gained during the exploratory data analysis (EDA) phase with actionable predictions and outcomes used in the deployment phase.
|
||||
|
||||
Model evaluation follows, where the performance and accuracy of these models are tested to ensure reliability.
|
||||
|
||||
### Deployment and Monitoring
|
||||
|
||||
The final stage of this generic data science lifecycle involves deploying the model into a production environment. Here, data scientists, machine learning engineers, and quality assurance teams ensure that the model operates effectively within existing software systems.
|
||||
|
||||
After this stage, continuous monitoring and maintenance are essential to address new data or changing conditions, which can impact the performance and accuracy of the model.
|
||||
|
||||
## Exploring 6 Popular Lifecycle Variants
|
||||
|
||||

|
||||
|
||||
The data science lifecycle offers various frameworks tailored to specific needs and contexts. Below, we explore six prominent variants:
|
||||
|
||||
### CRISP-DM (Cross Industry Standard Process for Data Mining)
|
||||
|
||||
CRISP-DM is one of the most widely used frameworks in data science projects, especially within business contexts.
|
||||
|
||||
It organizes the lifecycle into six stages: Business Understanding, Data Understanding, Data Preparation, Modeling, Evaluation, and Deployment.
|
||||
|
||||
This iterative approach allows teams to revisit and refine previous steps as new insights emerge. CRISP-DM is ideal for projects where aligning technical efforts with business goals is very important.
|
||||
|
||||
**Example use case**: A retail company wants to improve customer segmentation for targeted marketing campaigns. Using CRISP-DM, the team starts with business understanding to define segmentation goals, gathers transaction and demographic data, prepares and cleans it, builds clustering models, evaluates their performance, and deploys the best model to group customers for personalized offers.
|
||||
|
||||
### KDD (Knowledge Discovery in Databases)
|
||||
|
||||
The KDD process focuses on extracting useful knowledge from large datasets. Its stages include Selection, Preprocessing, Transformation, Data Mining, and Interpretation/Evaluation.
|
||||
|
||||
KDD emphasizes the academic and research-oriented aspects of data science, making it an ideal choice for experimental or exploratory projects in scientific domains. It offers a systematic approach to discovering patterns and insights in complex datasets.
|
||||
|
||||
**Example use case:** A research institute analyzes satellite data to study climate patterns. They follow KDD by selecting relevant datasets, preprocessing to remove noise, transforming data to highlight seasonal trends, applying data mining techniques to identify long-term climate changes, and interpreting results to publish findings.
|
||||
|
||||
### Data Analytics Lifecycle
|
||||
|
||||
This specific data science lifecycle is tailored for enterprise-level projects that prioritize actionable insights. It’s composed of six stages: Discovery, Data Preparation, Model Planning, Model Building, Communicating Results, and Operationalizing.
|
||||
|
||||
The framework’s strengths lie in its alignment with business objectives and readiness for model deployment, making it ideal for organizations seeking to integrate data-driven solutions into their operations.
|
||||
|
||||
**Example use case:** A financial institution uses the Data Analytics Lifecycle to detect fraudulent transactions. They discover patterns in historical transaction data, prepare it by cleaning and normalizing, plan predictive models, build and test them, communicate results to fraud prevention teams, and operationalize the model to monitor real-time transactions.
|
||||
|
||||
### SEMMA (Sample, Explore, Modify, Model, Assess)
|
||||
|
||||
SEMMA is a straightforward and tool-centric framework developed by SAS. It focuses on sampling data, exploring it for patterns, modifying it for analysis, modeling it for predictions, and assessing the outcomes.
|
||||
|
||||
This lifecycle is particularly useful for workflows involving specific analytics tools. Its simplicity and strong emphasis on data exploration make it an excellent choice for teams prioritizing rapid insights.
|
||||
|
||||
**Example use case:** A healthcare organization predicts patient readmission rates using SEMMA. They sample data from hospital records, explore patient histories for trends, modify features like patient age and diagnoses, build machine learning models, and assess their accuracy to choose the most effective predictor.
|
||||
|
||||
### Team Data Science Process (TDSP)
|
||||
|
||||
TDSP offers a collaborative and agile framework that organizes the lifecycle into four key stages: Business Understanding, Data Acquisition, Modeling, and Deployment.
|
||||
|
||||
Designed with team-based workflows in mind, TDSP emphasizes iterative progress and adaptability, ensuring that projects align with business needs while remaining flexible to changes. It’s well-suited for scenarios requiring close collaboration among data scientists, engineers, and stakeholders.
|
||||
|
||||
**Example use case:** A logistics company improves delivery route optimization. Using TDSP, the team collaborates to understand business goals, acquires data from GPS and traffic systems, develops routing models, and deploys them to dynamically suggest the fastest delivery routes.
|
||||
|
||||
### MLOps Lifecycle
|
||||
|
||||
MLOps focuses specifically on machine learning operations and production environments. Its stages include Data Engineering, Model Development, Model Deployment, and Monitoring.
|
||||
|
||||
This lifecycle is essential for projects involving large-scale machine learning systems that demand high scalability and automation.
|
||||
|
||||
MLOps integrates seamlessly with continuous integration and delivery pipelines, ensuring that deployed models remain effective and relevant as new data is introduced.
|
||||
|
||||
Each of these frameworks has its own strengths and is suited to different types of data science operations.
|
||||
|
||||
**Example use case:** An e-commerce platform deploys a recommendation engine using MLOps. They engineer data pipelines from user activity logs, develop collaborative filtering models, deploy them on the website, and monitor their performance to retrain models when new user data is added.
|
||||
|
||||
## How to Choose the Right Data Science Lifecycle
|
||||
|
||||

|
||||
|
||||
Determining the most suitable data science lifecycle for your data science project requires a systematic approach. After all, not all lifecycles are best suited for all situations.
|
||||
|
||||
You can follow these steps to identify the framework that aligns best with your goals and resources:
|
||||
|
||||
1. **Define your objectives:** Clearly identify the goals of your project. Are you solving a business problem, conducting academic research, or deploying a machine learning model? Understanding the end objective will narrow down your choices.
|
||||
2. **Assess project complexity:** Evaluate the scope and intricacy of your project. Simple projects may benefit from streamlined frameworks like SEMMA, while complex projects with iterative requirements might need CRISP-DM or TDSP.
|
||||
3. **Evaluate your team composition:** Consider the expertise within your team. A team with strong machine learning skills may benefit from MLOps, whereas a diverse team with varying levels of experience might prefer a more general framework like CRISP-DM.
|
||||
4. **Analyze industry and domain requirements:** Different industries may have unique needs. For example, business-driven projects often align with the Data Analytics Lifecycle, while academic projects might find KDD more suitable.
|
||||
5. **Examine available tools and resources:** Ensure that the tools, software, and infrastructure you have access to are compatible with your chosen lifecycle. Frameworks like SEMMA may require specific tools such as SAS.
|
||||
6. **Match to key stakeholder needs:** Align the lifecycle with the expectations and requirements of stakeholders. A collaborative framework like TDSP can be ideal for projects needing frequent input and iteration with business partners.
|
||||
7. **Run a trial phase:** If possible, test a smaller project or a subset of your current project with the selected framework. This will help you assess its effectiveness and make adjustments as needed.
|
||||
|
||||
Follow these steps and you can identify the lifecycle that not only suits your project but also ensures that your data science process is efficient and productive. Each project is unique, so tailoring the lifecycle to its specific demands is critical to success.
|
||||
|
||||
## Generic Framework for Beginners
|
||||
|
||||

|
||||
|
||||
While there are many different data science lifecycles and ways to tackle data science projects, if you’re just getting started and you’re trying to push your first project into production, relying on a beginner-friendly lifecycle might be a better idea.
|
||||
|
||||
A generic framework for beginners in data science simplifies the lifecycle into manageable steps, making it easier to understand and implement. You can follow these steps to define your new framework:
|
||||
|
||||
### 1\. Define the problem
|
||||
|
||||

|
||||
|
||||
Start by clearly identifying the problem you aim to solve. Consider the objectives and outcomes you want to achieve, and ensure these are aligned with the needs of any stakeholder. This will help focus your efforts during development and set the right expectations with your stakeholders.
|
||||
|
||||
### 2\. Collect and clean data
|
||||
|
||||

|
||||
|
||||
Gather data from reliable and relevant sources. During this stage, focus on ensuring data quality by treating inaccurate data, filling in missing values, validating and removing potential data biases and finally, converting raw data into usable formats.
|
||||
|
||||
### 3\. Analyze and visualize
|
||||
|
||||

|
||||
|
||||
Explore the data to uncover patterns, trends, and insights. Use simple data visualization techniques such as bar graphs and scatter plots, along with basic statistical methods, to gain a deeper understanding of the dataset’s structure and variables.
|
||||
|
||||
### 4\. Build and evaluate a model
|
||||
|
||||

|
||||
|
||||
Develop a basic predictive model using accessible machine learning or statistical tools. Test the model’s performance to ensure it meets the objectives defined earlier during step 1\. For beginners, tools with user-friendly interfaces like Python libraries or Excel can be highly effective.
|
||||
|
||||
### 5\. Share results and deploy
|
||||
|
||||

|
||||
|
||||
Present your findings to stakeholders in a clear and actionable format. If applicable, deploy the model into a small-scale production environment to observe its impact and gather feedback for further improvement.
|
||||
|
||||
**Tips for small projects:** Start with a problem you’re familiar with, such as analyzing personal expenses or predicting simple outcomes. Focus on learning the process rather than achieving perfect results. Use open-source tools and resources to experiment and build your confidence.
|
||||
|
||||
Use this framework if this is your first data science project, evaluate your results, and most importantly, reflect on your experience.
|
||||
|
||||
Take those insights into your next project and decide if for that one you would actually benefit from using one of the predefined standard lifecycles mentioned above.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The data science lifecycle is a cornerstone of modern data science. By understanding its stages and principles, professionals can navigate the complexities of data science projects with confidence.
|
||||
|
||||
Regardless of what you’re doing, dealing with unstructured data, creating models, or deploying machine learning algorithms, the lifecycle provides a roadmap for success.
|
||||
|
||||
As data science experts and teams continue to explore and refine their approaches, the lifecycle framework remains a key tool for achieving excellence in any and all operations.
|
||||
|
||||
Finally, remember that if you’re interested in developing your data science career, you have our [data scientist](https://roadmap.sh/ai-data-scientist) and [data analyst](https://roadmap.sh/data-analyst) roadmaps at your disposal. These roadmaps will help you focus your learning time on the really important and relevant topics.
|
||||
197
src/data/guides/ai-data-scientist-skills.md
Normal file
197
src/data/guides/ai-data-scientist-skills.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
title: 'Top 11 Data Sience Skills to Master in @currentYear@'
|
||||
description: 'Looking to excel in data science? Learn the must-have skills for @currentYear@ with our expert guide and advance your data science career.'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/ai-data-scientist/skills'
|
||||
seo:
|
||||
title: 'Top 11 Data Sience Skills to Master in @currentYear@'
|
||||
description: 'Looking to excel in data science? Learn the must-have skills for @currentYear@ with our expert guide and advance your data science career.'
|
||||
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-skills-to-master-q36qn.jpg'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2025-01-28
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||

|
||||
|
||||
Data science is becoming more relevant as a field and profession by the day. Part of this constant change is the mind-blowing speed at which AI is evolving these days. Every day a new model is released, every week a new product is built around it, and every month OpenAI releases an earth-shattering change that pushes the field even further than before.
|
||||
|
||||
Data scientists sit at the core of that progress, but what does it take to master the profession?
|
||||
|
||||
Mastering the essential data scientist skills goes beyond just solving complex problems. It includes the ability to handle data workflows, build machine learning models, and interpret data trends effectively.
|
||||
|
||||
In this guide, we'll explore the top 10 skills that future data scientists must work on to shine brighter than the rest in 2025, setting a foundation for long-term success.
|
||||
|
||||
These are the data scientist skills covered in the article:
|
||||
|
||||
* Programming proficiency with **Python, R, and SQL**
|
||||
* Data manipulation and analysis, including **data wrangling** and **exploratory data analysis**
|
||||
* Mastery of **machine learning** and **AI techniques**
|
||||
* Strong statistical and **mathematical** **foundations**
|
||||
* Familiarity with **big data technologies**
|
||||
* Data engineering for infrastructure and **ETL pipelines**
|
||||
* Expertise in **data visualization** with tools like **Plotly** and **D3.js**
|
||||
* **Domain knowledge** for aligning data science projects with business goals
|
||||
* **Soft skills** for communication, collaboration, and creativity
|
||||
* **Feature engineering** and selection for **model optimization**.
|
||||
* Staying current with trends like **MLOps** and **Generative AI.**
|
||||
|
||||
## **Understanding Data Science**
|
||||
|
||||
[Data science](https://roadmap.sh/ai-data-scientist) is an interdisciplinary field that combines multiple disciplines to make sense of data and drive actionable insights. It integrates programming, statistical analysis, and domain knowledge to uncover patterns and trends in both structured and unstructured data. This powerful combination enables data professionals to solve a variety of challenges, such as:
|
||||
|
||||
* Building predictive models to forecast sales or identify customer churn.
|
||||
* Developing optimization techniques to streamline supply chains or allocate resources more effectively.
|
||||
* Leveraging automation and artificial intelligence to create personalized recommendations or detect fraudulent activity in massive datasets.
|
||||
|
||||
At its core, data science empowers organizations to turn raw data into actionable insights. By interpreting data effectively and applying statistical models, data scientists support data-driven decision-making, ensuring businesses maintain a competitive edge.
|
||||
|
||||
The data science field requires a unique mix of technical skills, analytical prowess, and creativity to handle the vast array of complex data sets encountered in real-world scenarios. In other words, being a data scientist is not for everyone.
|
||||
|
||||
**1\. Programming Proficiency**
|
||||
|
||||

|
||||
|
||||
Programming remains a cornerstone of the data science field, forming the foundation for nearly every task in data science projects. Mastery of programming languages like Python, R, and SQL is crucial for aspiring data scientists to handle data workflows effectively.
|
||||
|
||||
Python is the undisputed leader in data science, thanks to its extensive libraries and frameworks. Pandas, NumPy, and Scikit-learn are essential for tasks ranging from data wrangling and numerical analysis to building machine learning models. Deep learning tools such as TensorFlow and PyTorch make Python indispensable for tackling advanced challenges like developing artificial neural networks for image recognition and natural language processing (NLP).
|
||||
|
||||
R excels in statistical analysis and visualization. Its specialized libraries, like ggplot2 for data visualization and caret for machine learning models, make it a preferred choice for academics and data analysis tasks that require interpreting data trends and creating statistical models.
|
||||
|
||||
SQL is the backbone of database management, which is essential for extracting, querying, and preparing data from structured databases. A strong command of SQL allows data professionals to manage massive datasets efficiently and ensure smooth integration with analytical tools.
|
||||
|
||||
## **2\. Data Manipulation and Analysis**
|
||||
|
||||

|
||||
|
||||
The ability to manipulate and analyze data lies at the heart of data science skills. These tasks involve transforming raw data into a format suitable for analysis and extracting insights through statistical concepts and exploratory data analysis (EDA).
|
||||
|
||||
Data wrangling is a critical skill for cleaning and preparing raw data, addressing missing values, and reshaping complex data sets. For example, consider a dataset containing customer transaction records with incomplete information. Using tools like Pandas in Python, a data scientist can identify missing values, impute or drop them as appropriate, and restructure the data to focus on specific variables like transaction frequency or total purchase amounts. This process ensures the dataset is ready for meaningful analysis.
|
||||
|
||||
Tools like Pandas, PySpark, and Dask are invaluable for handling unstructured data or working with massive datasets efficiently. These tools allow data scientists to transform complex data sets into manageable and analyzable forms, which is foundational for building machine learning models or conducting advanced statistical analysis.
|
||||
|
||||
Performing exploratory data analysis allows data scientists to identify patterns, correlations, and anomalies within structured data. Visualization libraries like Matplotlib and Seaborn, combined with Python scripts, play a significant role in understanding data insights before building predictive models or statistical models.
|
||||
|
||||
**3\. Machine Learning and AI**
|
||||
|
||||

|
||||
|
||||
Machine learning is a driving force in the data science industry, enabling data-driven decisions across sectors and revolutionizing how organizations interpret data and make predictions. Mastering machine learning algorithms and frameworks are among the top data science skills for aspiring data scientists who wish to excel in analyzing data and creating impactful solutions.
|
||||
|
||||
Data scientists commonly tackle supervised learning tasks, such as predicting housing prices through regression models or identifying fraudulent transactions with classification algorithms. For example, using Scikit-learn, a data scientist can train a decision tree to categorize customer complaints into predefined categories for better issue resolution. Additionally, unsupervised techniques like clustering are applied in market segmentation to group customers based on purchasing patterns, helping businesses make data-driven decisions.
|
||||
|
||||
Deep learning represents the cutting edge of artificial intelligence, utilizing artificial neural networks to manage unstructured data and solve highly complex problems. Frameworks like TensorFlow and PyTorch are essential tools for developing advanced solutions, such as NLP models for chatbot interactions or generative AI for creating realistic images. These tools empower data scientists to push the boundaries of innovation and unlock actionable insights from vast and complex datasets.
|
||||
|
||||
## **4\. Statistical and Mathematical Foundations**
|
||||
|
||||

|
||||
|
||||
Statistical concepts and mathematical skills form the backbone of building robust data models and interpreting data insights. These foundational skills are indispensable for anyone aiming to succeed in the data science field.
|
||||
|
||||
Probability theory and hypothesis testing play a vital role in understanding uncertainty in data workflows. For instance, a data scientist might use hypothesis testing to evaluate whether a new marketing strategy leads to higher sales compared to the current approach, ensuring data-driven decision-making.
|
||||
|
||||
Linear algebra and calculus are crucial for developing and optimizing machine learning algorithms. Techniques like matrix decomposition and gradient descent are used to train neural networks and enhance their predictive accuracy. These mathematical tools are the engine behind many advanced algorithms, making them essential data scientist skills.
|
||||
|
||||
Advanced statistical analysis, including A/B testing and Bayesian inference, helps validate predictions and understand relationships within complex datasets. For example, A/B testing can determine which website design yields better user engagement, providing actionable insights to businesses.
|
||||
|
||||
## **5\. Big Data Technologies**
|
||||
|
||||

|
||||
|
||||
While big data skills are secondary for most data scientists, understanding big data technologies enhances their ability to handle massive datasets efficiently. Familiarity with tools like Apache Spark and Hadoop allows data scientists to process and analyze distributed data, which is especially important for projects involving millions of records. For example, Apache Spark can be used to calculate real-time metrics on user behavior across e-commerce platforms, enabling businesses to personalize experiences dynamically.
|
||||
|
||||
Cloud computing skills, including proficiency with platforms like AWS or GCP, are also valuable for deploying machine learning projects at scale. A data scientist working with GCP's BigQuery can query massive datasets in seconds, facilitating faster insights for time-sensitive decisions. These technologies, while not the core of a data scientist's responsibilities, are crucial for ensuring scalability and efficiency in data workflows.
|
||||
|
||||
## **6\. Data Engineering**
|
||||
|
||||

|
||||
|
||||
Data engineering complements data science by creating the infrastructure required to analyze data effectively. This skill set ensures that data flows seamlessly through pipelines, enabling analysis and decision-making.
|
||||
|
||||
Designing ETL (Extract, Transform, Load) pipelines is a critical part of data engineering. For instance, a data engineer might create a pipeline to collect raw sales data from multiple sources, transform it by standardizing formats and handling missing values, and load it into a database for further analysis. These workflows are the backbone of data preparation.
|
||||
|
||||
Using tools like Apache Airflow, those workflows can be streamlined, while managing real-time data streaming using Kafka ensures that real-time data—such as social media feeds or IoT sensor data—is processed without delay. For example, a Kafka pipeline could ingest weather data to update forecasts in real-time.
|
||||
|
||||
Finally, storing and querying complex data sets in cloud computing with tools like Snowflake or BigQuery allows data scientists to interact with massive datasets effortlessly.
|
||||
|
||||
These platforms support scalable storage and high-performance queries, enabling faster analysis and actionable insights.
|
||||
|
||||
## **7\. Data Visualization**
|
||||
|
||||

|
||||
|
||||
Data visualization is a cornerstone of the data science field, as it enables data professionals to present data and communicate findings effectively. While traditional tools like Tableau and Power BI are widely used, aspiring data scientists should prioritize programming-based tools like Plotly and D3.js for greater flexibility and customization.
|
||||
|
||||
For example, using Plotly, a data scientist can create an interactive dashboard to visualize customer purchase trends over time, allowing stakeholders to explore the data dynamically. Similarly, D3.js offers unparalleled control for designing custom visualizations, such as heatmaps or network graphs, that convey complex relationships in a visually compelling manner.
|
||||
|
||||
Applying storytelling techniques further enhances the impact of visualizations. By weaving data insights into a narrative, data scientists can ensure their findings resonate with stakeholders and drive actionable decisions. For instance, a well-crafted story supported by visuals can explain how seasonal demand patterns affect inventory management, bridging the gap between technical analysis and strategic planning.
|
||||
|
||||
## **8\. Business and Domain Knowledge**
|
||||
|
||||

|
||||
|
||||
Domain knowledge enhances the relevance of data science projects by aligning them with organizational goals and addressing unique industry-specific challenges. Understanding the context in which data is applied allows data professionals to make their analysis more impactful and actionable.
|
||||
|
||||
For example, in the finance industry, a data scientist with domain expertise can design predictive models that assess credit risk by analyzing complex data sets of customer transactions, income, and past credit behavior. These models enable financial institutions to make data-driven decisions about lending policies.
|
||||
|
||||
In healthcare, domain knowledge allows data scientists to interpret medical data effectively, such as identifying trends in patient outcomes based on treatment history. By leveraging data models tailored to clinical needs, data professionals can help improve patient care and operational efficiency in hospitals.
|
||||
|
||||
This alignment ensures that insights are not only technically robust but also directly applicable to solving real-world problems, making domain knowledge an indispensable skill for data professionals seeking to maximize their impact.
|
||||
|
||||
## **9\. Soft Skills**
|
||||
|
||||

|
||||
|
||||
Soft skills are as essential as technical skills in the data science field, bridging the gap between complex data analysis and practical implementation. These skills enhance a data scientist's ability to communicate findings, collaborate with diverse teams, and approach challenges with innovative solutions.
|
||||
|
||||
**Communication** is critical for translating data insights into actionable strategies. For example, a data scientist might present the results of an exploratory data analysis to marketing executives, breaking down statistical models into simple, actionable insights that drive campaign strategies. The ability to clearly interpret data ensures that stakeholders understand and trust the findings.
|
||||
|
||||
**Collaboration** is equally vital, as data science projects often involve cross-functional teams. For instance, a data scientist might work closely with software engineers to integrate machine learning models into a production environment or partner with domain experts to ensure that data-driven decisions align with business objectives. Effective teamwork ensures seamless data workflows and successful project outcomes.
|
||||
|
||||
**Creativity** allows data scientists to find innovative ways to address complex problems. A creative data scientist might devise a novel approach to handling unstructured data, such as using natural language processing (NLP) techniques to extract insights from customer reviews, providing actionable insights that improve product development.
|
||||
|
||||
These critical soft skills complement technical expertise, making data professionals indispensable contributors to their organizations.
|
||||
|
||||
## 10\. Feature engineering and selection for model optimization
|
||||
|
||||

|
||||
|
||||
For machine learning models to interpret and use any type of data, that data needs to be turned into features. And that is where feature engineering and selection comes into play. These are two critical steps in the data science workflow because they directly influence the performance and accuracy of the models. If you think about it, the better the model understands what data to focus on, the better it'll perform.
|
||||
|
||||
These processes involve creating, selecting, and transforming raw data into useful features loaded with meaning that help represent the underlying problem for the model.
|
||||
|
||||
For example, imagine building a model to predict house prices. Raw data might include information like the size of the house in square meters, the number of rooms, and the year it was built. Through feature engineering, a data scientist could create new features, such as "price per square meter" or "age of the house," which make the data more informative for the model. These features can highlight trends that a model might otherwise miss.
|
||||
|
||||
Feature selection, on the other hand, focuses on optimizing the use and dependency on features by identifying the most relevant ones and removing the redundant or irrelevant features. For example, let's consider a retail scenario where a model is predicting customer churn, here it might benefit from focusing on features like "purchase frequency" and "customer feedback sentiment", while ignoring less impactful ones like "the time of day purchases are made". This helps to avoid the model getting overwhelmed by noise, improving both its efficiency and accuracy.
|
||||
|
||||
If you're looking to improve your data science game, then focusing on feature engineering and selection can definitely have that effect.
|
||||
|
||||
## 11\. Staying Current
|
||||
|
||||

|
||||
|
||||
The data science field evolves at an unprecedented pace, driven by advancements in artificial intelligence, machine learning, and data technologies. Staying current with emerging trends is essential for maintaining a competitive edge and excelling in the industry.
|
||||
|
||||
Joining **data science communities**, such as forums or online groups, provides a platform for exchanging ideas, discussing challenges, and learning from peers. For instance, platforms like Kaggle or GitHub allow aspiring data scientists to collaborate on data science projects and gain exposure to real-world applications.
|
||||
|
||||
Attending **data science conferences** is another effective way to stay informed. Events like NeurIPS, Strata Data Conference, or PyData showcase cutting-edge research and practical case studies, offering insights into the latest advancements in machine learning models, big data technologies, and cloud computing tools.
|
||||
|
||||
Engaging in **open-source projects** not only sharpens technical skills but also helps data professionals contribute to the broader data science community. For example, contributing to an open-source MLOps framework might provide invaluable experience in deploying and monitoring machine learning pipelines.
|
||||
|
||||
Embracing trends like **MLOps** for operationalizing machine learning, **AutoML** for automating model selection, and **Generative AI** for creating synthetic data ensures that data scientists remain at the forefront of innovation. These emerging technologies are reshaping the data science field, making continuous learning a non-negotiable aspect of career growth.
|
||||
|
||||
**Summary**
|
||||
|
||||
Mastering these essential data scientist skills—from programming languages and machine learning skills to interpreting data insights and statistical models—will future-proof your [career path in data science](https://roadmap.sh/ai-data-scientist/career-path). These include the core skills of data manipulation, statistical analysis, and data visualization, all of which are central to the data science field.
|
||||
|
||||
In addition, while big data technologies and data engineering skills are not the central focus of a data scientist's role, they serve as valuable, data science-adjacent competencies. Familiarity with big data tools like Apache Spark and cloud computing platforms can enhance scalability and efficiency in handling massive datasets, while data engineering knowledge helps create robust pipelines to support analysis. By building expertise in these areas and maintaining adaptability, you can excel in this dynamic, data-driven industry.
|
||||
|
||||
Check out our [data science roadmap](https://roadmap.sh/ai-data-scientist) next to discover what your potential learning path could look like in this role.
|
||||
|
||||
313
src/data/guides/ai-data-scientist-tools.md
Normal file
313
src/data/guides/ai-data-scientist-tools.md
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: 'Data Science Tools: Our Top 11 Recommendations for @currentYear@'
|
||||
description: 'Master your data science projects with our top 11 tools for 2025! Discover the best platforms for data analysis, visualization, and machine learning.'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/ai-data-scientist/tools'
|
||||
seo:
|
||||
title: 'Data Science Tools: Our Top 11 Recommendations for @currentYear@'
|
||||
description: 'Master your data science projects with our top 11 tools for 2025! Discover the best platforms for data analysis, visualization, and machine learning.'
|
||||
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-tools-1a9w1.jpg'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2025-01-28
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||

|
||||
|
||||
In case you haven't noticed, the data science industry is constantly evolving, potentially even faster than the web industry (which says a lot\!).
|
||||
|
||||
And 2025 is shaping up to be another transformative year for tools and technologies. Whether you're exploring machine learning tools, predictive modeling, data management, or data visualization tools, there's an incredible array of software to enable [data scientists](https://roadmap.sh/ai-data-scientist) to analyze data efficiently, manage data effectively, and communicate insights.
|
||||
|
||||
In this article, we dive into the essential data science tools you need to know for 2025, complete with ratings and expert picks to help you navigate the options.
|
||||
|
||||
## What is Data Science?
|
||||
|
||||
Data science is an interdisciplinary field that combines mathematics, statistics, computer science, and domain expertise to extract meaningful insights from data. It involves collecting, cleaning, and analyzing large datasets to uncover patterns, trends, and actionable information. At its core, data science aims to solve complex problems through data-driven decision-making, using techniques such as machine learning, predictive modeling, and data visualization.
|
||||
|
||||
The data science process typically involves:
|
||||
|
||||
* **Data Collection**: Gathering data from various sources, such as databases, APIs, or real-time sensors.
|
||||
* **Data Preparation**: Cleaning and transforming raw data into a usable format for analysis.
|
||||
* **Exploratory Data Analysis (EDA):** Identifying trends, correlations, and outliers within the dataset.
|
||||
* **Modeling**: Using algorithms and statistical methods to make predictions or classify data.
|
||||
* **Interpretation and Communication**: Visualizing results and presenting insights to stakeholders in an understandable manner.
|
||||
|
||||
Data science plays a key role in various industries, including healthcare, finance, marketing, and technology, driving innovation and efficiency by leveraging the power of data.
|
||||
|
||||
## Criteria for Ratings
|
||||
|
||||
We rated each of the best data science tools on a 5-star scale based on:
|
||||
|
||||
* **Performance:** How efficiently the tool handles large and complex datasets. This includes speed, resource optimization, and reliability during computation.
|
||||
* **Scalability:** The ability to scale across big data and multiple datasets. Tools were evaluated on their capability to maintain performance as data sizes grow.
|
||||
* **Community and Ecosystem:** Availability of resources, support, and integrations. Tools with strong community support and extensive libraries received higher ratings.
|
||||
* **Learning Curve:** Ease of adoption for new and experienced users. Tools with clear documentation and intuitive interfaces were rated more favorably.
|
||||
|
||||
## Expert Recommendations
|
||||
|
||||
Picking the best tools for your project is never easy, and it's hard to make an objective decision if you don't have experience with any of them.
|
||||
|
||||
So to make your life a bit easier, here's my personal recommendation, you can take it or leave it, it's up to you, but at least you'll know where to start:
|
||||
|
||||
While each tool has its strengths, my favorite pick among them is **TensorFlow**. Its perfect scores in performance, scalability, and community support (you'll see them in a second in the table below), combined with its relatively moderate learning curve, make it an amazing choice for building advanced neural networks and developing predictive analytics systems. You can do so much with it, like image recognition, natural language processing, and recommendation systems cementing its position as the leading choice (and my personal choice) in 2025\.
|
||||
|
||||
Now, to help you understand and compare the rest of the tools from this guide, the table below summarizes their grades across key criteria: performance, scalability, community support, and learning curve. It also highlights the primary use cases for these tools.
|
||||
|
||||
| Tool | Performance | Scalability | Community | Learning Curve | Best For |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| TensorFlow | 5 | 5 | 5 | 4 | Advanced neural networks, predictive analytics |
|
||||
| Apache Spark | 5 | 5 | 5 | 3 | Distributed analytics, real-time streaming |
|
||||
| Jupyter Notebooks | 4 | 4 | 5 | 5 | Exploratory analysis, education |
|
||||
| Julia | 4 | 4 | 4 | 3 | Simulations, statistical modeling |
|
||||
| NumPy | 4 | 3 | 5 | 5 | Numerical arrays, preprocessing workflows |
|
||||
| Polars | 5 | 4 | 4 | 4 | Data preprocessing, ETL acceleration |
|
||||
| Apache Arrow | 5 | 5 | 4 | 3 | Interoperability, streaming analytics |
|
||||
| Streamlit | 4 | 4 | 5 | 5 | Interactive dashboards, rapid deployment |
|
||||
| DuckDB | 4 | 4 | 4 | 5 | SQL queries, lightweight warehousing |
|
||||
| dbt | 4 | 4 | 5 | 4 | SQL transformations, pipeline automation |
|
||||
| Matplotlib | 4 | 3 | 5 | 4 | Advanced visualizations, publication graphics |
|
||||
|
||||
Let's now deep dive into each of these tools to understand in more detail, why they're in this guide.
|
||||
|
||||
## Data science tools for ML & Deep learning tools
|
||||
|
||||
### TensorFlow
|
||||
|
||||

|
||||
|
||||
TensorFlow remains one of the top data science tools for deep learning models and machine learning applications. Developed by Google, this open-source platform excels in building neural networks, predictive analytics, and natural language processing models.
|
||||
|
||||
* **Performance (★★★★★):** TensorFlow achieves top marks here due to its use of GPU and TPU acceleration, which allows seamless handling of extremely large models. Its ability to train complex networks without compromising on speed solidifies its high-performance ranking.
|
||||
* **Scalability (★★★★★):** TensorFlow scales from single devices to distributed systems effortlessly, enabling use in both prototyping and full-scale production.
|
||||
* **Community and Ecosystem (★★★★★):** With an active developer community and comprehensive support, TensorFlow offers unmatched resources and third-party integrations.
|
||||
* **Learning Curve (★★★★):** While it offers immense power, mastering TensorFlow's advanced features requires time, making it slightly less accessible for beginners compared to simpler frameworks.
|
||||
|
||||
**Strengths:** TensorFlow is a powerhouse for performance and scalability in the world of machine learning. Its GPU and TPU acceleration allow users to train and deploy complex models faster than many competitors. The massive community ensures constant innovation, with frequent updates, robust third-party integrations, and an ever-growing library of resources. The inclusion of TensorFlow Lite and TensorFlow.js makes it versatile for both edge computing and web applications.
|
||||
|
||||
**Best For:** Developing advanced neural networks for image recognition, natural language processing pipelines, building recommendation systems, and creating robust predictive analytics tools for a wide array of industries.
|
||||
|
||||
**Used by:** Google itself uses TensorFlow extensively for tasks like search algorithms, image recognition, and natural language processing. Similarly, Amazon employs TensorFlow to power recommendation systems and optimize demand forecasting.
|
||||
|
||||
## Data science tools for big data processing
|
||||
|
||||
### Apache Spark
|
||||
|
||||

|
||||
|
||||
An Apache Software Foundation project, Apache Spark is a powerhouse for big data processing, enabling data scientists to perform batch processing and streaming data analysis. It supports a wide range of programming languages, including Python, Scala, and Java, and integrates well with other big data tools like Hadoop and Kafka.
|
||||
|
||||
* **Performance (★★★★★):** Spark excels in processing speed thanks to its in-memory computing capabilities, making it a leader in real-time and batch data processing.
|
||||
* **Scalability (★★★★★):** Designed for distributed systems, Spark handles petabytes of data with ease, maintaining efficiency across clusters.
|
||||
* **Community and Ecosystem (★★★★★):** Spark's widespread adoption and integration with tools like Kafka and Hadoop make it a staple for big data workflows.
|
||||
* **Learning Curve (★★★):** Beginners may find distributed computing concepts challenging, though excellent documentation helps mitigate this.
|
||||
|
||||
**Strengths:** Spark stands out for its lightning-fast processing speed and flexibility. Its in-memory computation ensures minimal delays during large-scale batch or streaming tasks. The compatibility with multiple programming languages and big data tools enhances its integration into diverse tech stacks.
|
||||
|
||||
**Best For:** Executing large-scale data analytics in distributed systems, real-time stream processing for IoT applications, running ETL pipelines, and data mining for insights in industries like finance and healthcare.
|
||||
|
||||
**Used by:** Apache Spark has been adopted by companies like Uber and Shopify. Uber uses Spark for real-time analytics and stream processing, enabling efficient ride-sharing logistics. Shopify relies on Spark to process large volumes of e-commerce data, supporting advanced analytics and business intelligence workflows.
|
||||
|
||||
## Exploratory & Collaborative tools
|
||||
|
||||
### Jupyter Notebooks
|
||||
|
||||

|
||||
|
||||
Jupyter Notebooks are an essential data science tool for creating interactive and shareable documents that combine code, visualizations, and narrative text. With support for over 40 programming languages, including Python, R, and Julia, Jupyter facilitates collaboration and exploratory data analysis.
|
||||
|
||||
* **Performance (★★★★):** Jupyter is designed for interactivity rather than computational intensity, which makes it highly effective for small to medium-scale projects but less suitable for high-performance tasks.
|
||||
* **Scalability (★★★★):** While Jupyter itself isn't designed for massive datasets, its compatibility with scalable backends like Apache Spark ensures it remains relevant for larger projects.
|
||||
* **Community and Ecosystem (★★★★★):** Jupyter's open-source nature and extensive community-driven extensions make it a powerhouse for versatility and support.
|
||||
* **Learning Curve (★★★★★):** Its simple and intuitive interface makes it one of the most accessible tools for beginners and professionals alike
|
||||
|
||||
**Strengths:** Jupyter's flexibility and ease of use make it indispensable for exploratory analysis and education. Its ability to integrate code, output, and explanatory text in a single interface fosters collaboration and transparency.
|
||||
|
||||
**Best For:** Creating educational tutorials, performing exploratory data analysis, prototyping machine learning models, and sharing reports that integrate code with rich visualizations.
|
||||
|
||||
**Used by:** Jupyter Notebooks have become a staple for exploratory analysis and collaboration. Delivery Hero uses Jupyter to enhance delivery logistics through data analysis and visualization, while Intuit leverages the tool to facilitate financial data analysis in collaborative projects.
|
||||
|
||||
## Data science tools for statistical computing
|
||||
|
||||
### Julia
|
||||
|
||||

|
||||
|
||||
Julia is an emerging open-source programming language tailored for statistical computing and data manipulation. It combines the performance of low-level languages like C with the simplicity of high-level languages like Python. Julia's strengths lie in its speed for numerical computation and its dynamic type system, making it highly suitable for big data applications and machine learning models. The Julia ecosystem is rapidly growing, offering libraries for data visualization, optimization, and deep learning.
|
||||
|
||||
* **Performance (★★★★):** Julia's design prioritizes speed for numerical and statistical computing, placing it ahead of many high-level languages in terms of raw performance.
|
||||
* **Scalability (★★★★):** With built-in support for parallel computing, Julia scales well for tasks requiring significant computational power, although its ecosystem is still catching up to Python's.
|
||||
* **Community and Ecosystem (★★★★):** Julia's growing community and the increasing availability of libraries make it a solid choice, though it's not yet as robust as more established ecosystems.
|
||||
* **Learning Curve (★★★):** Julia's unique syntax, while designed for simplicity, presents a learning barrier for those transitioning from other languages like Python or R.
|
||||
|
||||
**Strengths:** Julia's ability to execute complex numerical tasks at high speed positions it as a top contender in scientific computing. Its built-in support for parallelism allows it to scale efficiently, while its clear syntax lowers barriers for domain experts transitioning from MATLAB or R.
|
||||
|
||||
**Best For:** Performing advanced statistical analysis, numerical optimization, developing simulations in physics and finance, and implementing machine learning models for high-performance environments.
|
||||
|
||||
**Used by:** The high-performance capabilities of Julia make it a favorite for statistical computing in industries like finance. For example, Capital One uses Julia for risk analytics and modeling, and Aviva employs it to improve actuarial computations and financial modeling processes.
|
||||
|
||||
### NumPy
|
||||
|
||||

|
||||
|
||||
A foundational library in the Python ecosystem, NumPy provides powerful tools for managing data structures, numerical computations, and statistical analysis. It is widely used for data preparation, enabling operations on large multi-dimensional arrays and matrices.
|
||||
|
||||
* **Performance (★★★★):** NumPy's optimized C-based implementation allows it to handle numerical operations with high efficiency, but it relies on integration with other tools for larger or distributed workloads.
|
||||
* **Scalability (★★★):** As a single-machine library, NumPy is best suited for datasets that fit in memory, though it integrates well with scalable tools like Dask for extended use.
|
||||
* **Community and Ecosystem (★★★★★):** NumPy's foundational role in Python's data science ecosystem means extensive resources and near-universal compatibility.
|
||||
* **Learning Curve (★★★★★):** Its straightforward API and clear documentation make NumPy an essential and approachable tool for data preparation and numerical computing.
|
||||
|
||||
**Strengths:** NumPy's versatility and efficiency underpin its widespread adoption in the Python ecosystem. Its array manipulation capabilities—from slicing and reshaping to broadcasting—make it a cornerstone for numerical operations.
|
||||
|
||||
**Best For:** Handling numerical arrays for preprocessing, matrix algebra in physics and engineering, foundational operations for machine learning pipelines, and performing basic statistical analysis efficiently.
|
||||
|
||||
**Used by:** NumPy serves as the foundation for many Python-based workflows. Spotify uses NumPy for numerical computations within its recommendation algorithms, and Airbnb employs it to optimize pricing strategies and improve customer experience through data analysis.
|
||||
|
||||
## **Data science tools for data manipulation & preprocessing tools**
|
||||
|
||||
### Polars
|
||||
|
||||

|
||||
|
||||
Polars is a lightning-fast data processing & manipulation library that enables data scientists to handle complex datasets. Unlike traditional libraries, Polars is written in Rust, offering exceptional performance and low memory usage. Its DataFrame API is intuitive and supports multi-threaded operations, making it a strong choice for large-scale data preprocessing and manipulation tasks.
|
||||
|
||||
* **Performance** (★★★★★): Polars' Rust-based architecture ensures exceptional speed and memory efficiency, positioning it as a leading tool for high-performance data manipulation.
|
||||
* **Scalability** (★★★★): While optimized for larger datasets, its scalability is limited to environments supported by multi-threading rather than distributed systems.
|
||||
* **Community and Ecosystem** (★★★★): Though its ecosystem is still growing, Polars' strong integration with Python and intuitive API provide a solid foundation.
|
||||
* **Learning Curve** (★★★★): With a user-friendly interface inspired by Pandas, Polars is easy to adopt for those familiar with similar tools, though Rust concepts may pose challenges for some.
|
||||
|
||||
**Strengths:** Polars stands out due to its unparalleled speed, derived from its Rust-based architecture. Its ability to process data in parallel ensures efficiency even with large datasets, reducing bottlenecks in ETL pipelines. The intuitive API and support for lazy evaluation make it both user-friendly and powerful.
|
||||
|
||||
**Best For:** Processing complex datasets for data cleaning, reshaping large-scale tables, and accelerating ETL pipelines in environments requiring high-speed operations.
|
||||
|
||||
**Used by:** Polars is gaining traction for its exceptional speed in data preprocessing and ETL workflows. Zillow uses Polars for efficient data preprocessing in real estate market analysis, while Stripe adopts it to accelerate ETL processes for handling financial transaction data.
|
||||
|
||||
### Apache Arrow
|
||||
|
||||

|
||||
|
||||
Apache Arrow is revolutionizing how data is stored and transferred for big data applications. Its in-memory columnar format accelerates data processing and integration between multiple datasets and tools. Apache Arrow also acts as a bridge between various programming languages and frameworks, improving the interoperability of data science workflows.
|
||||
|
||||
* **Performance (★★★★★):** Apache Arrow's in-memory columnar format delivers unmatched speed for data processing and transfer between tools.
|
||||
* **Scalability (★★★★★):** Its design supports seamless scalability across distributed systems, making it ideal for large-scale workflows.
|
||||
* **Community and Ecosystem (★★★★):** Arrow's adoption by major data tools ensures growing support, though its standalone ecosystem remains limited compared to broader frameworks.
|
||||
* **Learning Curve (★★★):** Understanding columnar data formats and workflows may require extra effort for beginners but pays off in advanced scenarios.
|
||||
|
||||
**Strengths:** Apache Arrow's columnar format provides a significant boost in performance and compatibility. Its seamless interoperability between tools such as Pandas, Spark, and TensorFlow eliminates data transfer inefficiencies. The library also supports multi-language workflows, making it indispensable for teams leveraging diverse tech stacks.
|
||||
|
||||
**Best For:** Ensuring efficient interoperability between data tools, accelerating data lake operations, and supporting real-time data analytics in distributed systems.
|
||||
|
||||
**Used by:** Google BigQuery integrates Arrow to enhance data interchange and query performance, and AWS Athena relies on Arrow's in-memory format to facilitate faster query responses and real-time analytics.
|
||||
|
||||
## Data science tools for application development
|
||||
|
||||
### Streamlit
|
||||
|
||||

|
||||
|
||||
Streamlit is an open-source framework for creating custom data science applications and dashboards. It simplifies the process of building interactive apps by using Python scripts, making it accessible even for those with minimal web development experience. Streamlit's API enables rapid prototyping of machine learning tools and visualizations.
|
||||
|
||||
* **Performance (★★★★):** Optimized for real-time application development, Streamlit is fast for small to medium-scale projects.
|
||||
* **Scalability (★★★★):** Streamlit scales reasonably well but isn't designed for massive applications.
|
||||
* **Community and Ecosystem (★★★★★):** Its active community and constant updates ensure excellent support for users.
|
||||
* **Learning Curve (★★★★★):** With a simple API and Python-centric design, Streamlit is easy for both developers and non-developers.
|
||||
|
||||
**Strengths:** Streamlit's simplicity and speed make it ideal for crafting interactive dashboards with minimal effort. Its integration with popular Python libraries like Pandas and Matplotlib allows users to transform raw data into meaningful insights quickly.
|
||||
|
||||
**Best For:** Creating interactive dashboards for sharing machine learning predictions, visualizing complex datasets with minimal development effort, and rapidly deploying prototypes for stakeholder feedback.
|
||||
|
||||
**Used by:** Streamlit simplifies the creation of interactive dashboards and data applications. Companies like Snowflake use Streamlit to build client-facing data apps, while Octopus Energy employs it to create dashboards that visualize energy consumption data for their customers.
|
||||
|
||||
### DuckDB
|
||||
|
||||

|
||||
|
||||
DuckDB is an open-source analytics database that simplifies structured queries on raw data. Designed to operate within analytical workflows, it supports SQL-based queries without the need for a dedicated database server. Its efficient storage model makes it ideal for querying structured and unstructured data in ad hoc analysis scenarios, making it highly favored for lightweight data warehousing tasks.
|
||||
|
||||
* **Performance (★★★★):** DuckDB delivers impressive speeds for ad hoc analytics, optimized for single-machine workflows.
|
||||
* **Scalability (★★★★):** Suitable for lightweight to medium-scale tasks, DuckDB integrates well into Python and R environments.
|
||||
* **Community and Ecosystem (★★★★):** Growing adoption and strong SQL compatibility make it increasingly popular.
|
||||
* **Learning Curve (★★★★★):** Its SQL-based interface ensures a smooth learning experience for most users.
|
||||
|
||||
**Strengths:** DuckDB's efficiency and ease of use make it a go-to tool for analysts. Its ability to operate without infrastructure overhead allows rapid deployment, and its SQL compatibility ensures accessibility for non-programmers.
|
||||
|
||||
**Best For:** Running interactive SQL queries in development workflows, performing data warehousing tasks without infrastructure overhead, and integrating ad hoc analyses directly into Python-based projects.
|
||||
|
||||
**Used by:** DuckDB's lightweight and efficient SQL analytics have found applications in various industries. MotherDuck integrates DuckDB to enable fast, in-process analytical queries, and SeMI Technologies leverages DuckDB within its Weaviate platform for high-speed vector search analytics.
|
||||
|
||||
### dbt (Data Build Tool)
|
||||
|
||||

|
||||
|
||||
dbt is a development framework for transforming data in warehouses. It allows analysts and engineers to write modular SQL-based transformations and manage data workflows efficiently. With its focus on collaboration and version control, dbt has become an essential tool for teams working on data pipelines.
|
||||
|
||||
* **Performance (★★★★):** dbt's modular approach allows for efficient and scalable SQL transformations.
|
||||
* **Scalability (★★★★):** Designed for modern data warehouses, dbt handles increasing workloads effectively.
|
||||
* **Community and Ecosystem (★★★★★):** Its thriving community and vendor support make it indispensable for data pipeline management.
|
||||
* **Learning Curve (★★★★):** Familiarity with SQL simplifies adoption, though pipeline concepts may require additional learning.
|
||||
|
||||
**Strengths:** dbt's modularity and focus on collaboration streamline complex SQL transformations. Its integration with version control systems ensures reproducibility, while the ability to test and document transformations within the tool fosters better collaboration among data teams.
|
||||
|
||||
**Best For:** Automating SQL transformations for analytics, managing data warehouse workflows with version control, and creating reusable and modular pipelines for team collaboration.
|
||||
|
||||
**Used by:** dbt has become essential for transforming and managing data workflows. JetBlue uses dbt to optimize their data warehouse for improved analytics, and GitLab adopts it to transform raw data into actionable insights, streamlining their analytics operations.
|
||||
|
||||
## Data science tools for data visualization
|
||||
|
||||
### Matplotlib
|
||||
|
||||

|
||||
|
||||
Matplotlib is a widely used data visualization library in Python that allows users to create static, animated, and interactive visualizations. Known for its flexibility, Matplotlib supports detailed customization, making it suitable for complex visualizations required in data science projects.
|
||||
|
||||
* **Performance (★★★★):** Matplotlib handles visualization tasks efficiently for small to medium datasets but may lag with complex or large-scale rendering.
|
||||
* **Scalability (★★★):** Designed for single-threaded use, it integrates with scalable tools for extended capabilities.
|
||||
* **Community and Ecosystem (★★★★★):** A veteran library with vast resources and tutorials ensures comprehensive support.
|
||||
* **Learning Curve (★★★★):** Accessible for beginners, though mastering advanced features takes effort.
|
||||
|
||||
**Strengths:** Matplotlib's extensive customization options allow it to cater to diverse visualization needs, from simple plots to publication-grade graphics. Its compatibility with libraries like NumPy ensures seamless data integration, while the active community provides extensive tutorials and third-party tools. Despite being a veteran library, Matplotlib remains relevant by adapting to modern visualization demands.
|
||||
|
||||
**Best For:** Creating publication-quality figures, animating time-series data, developing exploratory charts, and embedding visualizations into data-driven applications.
|
||||
|
||||
**Used by:** NASA uses Matplotlib to plot and visualize space mission data, while CERN relies on it for visualizing complex results from particle physics experiments.
|
||||
|
||||
## How to Pick the Right Data Science Tool?
|
||||
|
||||
Choosing the right data science tool can be a daunting task given the vast array of options available. The best tool for your project will depend on several factors, which can be broadly categorized into the evaluation criteria and the context of your specific project.
|
||||
|
||||
### Importance of Evaluation Criteria
|
||||
|
||||
1. **Performance**: This determines how well the tool handles large and complex datasets. Tools that offer fast computation, reliable processing, and efficient use of resources are ideal for high-performance environments.
|
||||
2. **Scalability**: As data grows, the ability to maintain consistent performance is critical. Tools that scale across big data frameworks or distributed systems ensure longevity and adaptability.
|
||||
3. **Community and Ecosystem**: A strong community provides valuable resources such as tutorials, documentation, and support. An extensive ecosystem ensures compatibility with other tools and libraries, making integration seamless.
|
||||
4. **Learning Curve**: A tool's usability can make or break its adoption. Tools with intuitive interfaces and comprehensive documentation enable faster onboarding for teams with diverse expertise.
|
||||
|
||||
### Considering Project Context
|
||||
|
||||
While evaluation criteria provide a standardized way to compare tools, the context of your project ultimately determines the best fit. Key considerations include:
|
||||
|
||||
Tech Stack: The tools should integrate smoothly with your existing technologies and workflows.
|
||||
|
||||
Team Expertise: The skill levels and experience of your team play a significant role in adoption. A tool with a steep learning curve may not be ideal for a team of beginners.
|
||||
|
||||
Project Deadlines: Time constraints can affect the choice of tools. A tool with extensive setup requirements may not suit a project with tight deadlines.
|
||||
|
||||
Data Complexity and Size: The nature and volume of your data should align with the tool's capabilities.
|
||||
|
||||
By balancing these evaluation criteria with the unique needs of your project, you can ensure that the chosen tool maximizes efficiency and effectiveness while minimizing challenges.
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
Data science is an exciting and ever-evolving field, and the tools we've explored here represent the state-of-the-art of innovation for 2025\. Each tool has its own strengths, from high performance and scalability to user-friendly interfaces and robust community support. Whether you're just starting out in data science or managing complex, large-scale projects, there's a tool out there that's just right for you.
|
||||
|
||||
However, choosing that tool isn't just about star ratings or feature lists—it's about finding what works best for your specific context.
|
||||
|
||||
And remember, data science is as much about the journey as it is about the results. Exploring new tools, learning from community resources, and iterating on your processes will make you a better data scientist and help your projects thrive.
|
||||
|
||||
Check out our [data scientist roadmap](https://roadmap.sh/ai-data-scientist) to get a full view of your potential journey ahead\!
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user