Compare commits

..

47 Commits

Author SHA1 Message Date
Kamran Ahmed
9663ba019e Improve UI 2025-03-18 18:17:28 +00:00
Arik Chakma
f82eb09f60 feat: delete ai course 2025-03-18 23:54:20 +06:00
Kamran Ahmed
a60bcb5a45 Update message 2025-03-18 16:29:59 +00:00
Kamran Ahmed
3cf2ad9b25 Update course content 2025-03-18 16:27:00 +00:00
Kamran Ahmed
6449c24398 Update AI chat popup 2025-03-18 01:28:56 +00:00
Kamran Ahmed
eaddc81383 Merge branch 'master' of github.com:kamranahmedse/developer-roadmap 2025-03-18 00:39:25 +00:00
Jawher Kl
b6e0d566a6 fix: broken link (#8334) 2025-03-18 00:39:11 +00:00
Gleison
01f93d95fb feat: add content for StyleCop (#8337)
* Added content for StyleCop section

* Update src/data/roadmaps/aspnet-core/content/stylecop-rules@R7Qk5hsEIl9dspQXdaJAJ.md

* Update src/data/roadmaps/aspnet-core/content/stylecop-rules@R7Qk5hsEIl9dspQXdaJAJ.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2025-03-18 00:39:11 +00:00
Kamran Ahmed
fbd39e9079 Moving next should be mark it as done 2025-03-18 00:32:17 +00:00
Kamran Ahmed
3bc00b5b1a Storing fine-tune data 2025-03-18 00:32:17 +00:00
Kamran Ahmed
340ae002ca Improve fine-tuning 2025-03-18 00:32:17 +00:00
Kamran Ahmed
98d8510b60 Add UI 2025-03-18 00:32:17 +00:00
Kamran Ahmed
a82a0e6efb Moving next should be mark it as done 2025-03-18 00:30:46 +00:00
Kamran Ahmed
b17ba1b009 Storing fine-tune data 2025-03-18 00:07:23 +00:00
Kamran Ahmed
a07a5af543 Improve fine-tuning 2025-03-17 23:53:21 +00:00
Jawher Kl
16db649baf fix: broken link (#8334) 2025-03-18 01:22:18 +06:00
Kamran Ahmed
017fe3e0a4 Add UI 2025-03-17 17:15:35 +00:00
Gleison
1f727d2e17 feat: add content for StyleCop (#8337)
* Added content for StyleCop section

* Update src/data/roadmaps/aspnet-core/content/stylecop-rules@R7Qk5hsEIl9dspQXdaJAJ.md

* Update src/data/roadmaps/aspnet-core/content/stylecop-rules@R7Qk5hsEIl9dspQXdaJAJ.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2025-03-17 21:57:14 +06:00
Kamran Ahmed
281f6f369c Remove old guide flags 2025-03-17 15:28:54 +00:00
Kamran Ahmed
eb5e5fadcc Add python FAQs 2025-03-17 13:27:55 +00:00
Kamran Ahmed
4996d51340 Add JavaScript faqs 2025-03-17 12:46:12 +00:00
Kamran Ahmed
ea944a001e Add AI and Data Scientist Roadmap FAQs 2025-03-17 12:25:34 +00:00
Kamran Ahmed
6d28ab40a8 Update signup popup message 2025-03-16 00:19:24 +00:00
Arik Chakma
ebb88721b6 feat: ai course pagination (#8329) 2025-03-15 12:32:24 +00:00
Kamran Ahmed
8878d04f98 Remove autocomplete from ai roadmap search 2025-03-15 04:28:44 +00:00
Kamran Ahmed
1085c33dc4 Regenerate functionality 2025-03-14 21:10:57 +00:00
Kamran Ahmed
6b9007c530 Enable AI tutor 2025-03-14 19:53:42 +00:00
Kamran Ahmed
5ff89fa184 Add regenerate lessons 2025-03-14 14:46:27 +00:00
Kamran Ahmed
dfff959916 Remove ai course lessons before generation 2025-03-14 13:28:53 +00:00
Kamran Ahmed
3ba9abe7e3 Improve AI courses 2025-03-14 13:17:04 +00:00
Kamran Ahmed
fbd149f955 Disable AI tutor 2025-03-14 11:39:58 +00:00
Kamran Ahmed
d78fd6ccff Refactor AI course view 2025-03-14 04:25:23 +00:00
Kamran Ahmed
2be8dbe0c2 Update sidebar ui for courses 2025-03-14 03:24:59 +00:00
Kamran Ahmed
79c6e2be53 refactor: ai-courses (#8327)
* Refactor ai courses

* Refactor

* Regenerate roadmap functionality

* Title and difficulty to refresh also

* Add course regeneration

* Improve the non paid user headings

* Update

* Improve back button logic

* Is paid user checks
2025-03-14 03:05:07 +00:00
github-actions[bot]
cc5585171c chore: update roadmap content json (#8326)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-03-14 02:14:57 +00:00
Kamran Ahmed
38cd727e48 Add ai tutor link in navigation 2025-03-13 12:06:41 +00:00
Kamran Ahmed
fda56a5d30 Remove geeksforgeeks links 2025-03-13 11:48:33 +00:00
Kamran Ahmed
e27146d549 Add billing 2025-03-13 01:16:19 +00:00
Kamran Ahmed
eb95da0bb0 Refactor perks 2025-03-12 15:37:14 +00:00
Kamran Ahmed
554e61947b Update design 2025-03-12 15:23:59 +00:00
Kamran Ahmed
107ae1923b Update back text for ai tutor 2025-03-12 14:18:05 +00:00
Kamran Ahmed
cb64894e49 feat: add ai course generator (#8322)
* Course landing page

* Add ai course page

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip: error handling

* wip

* wip

* wip

* wip: ai course progress

* wip

* wip

* wip

* feat: code highlighting

* feat: usage limit

* feat: follow up message

* Update UI

* wip

* Add course content

* wip: autogrow textarea & examples

* Update types

* Update

* fix: add highlight to the AI chat

* UI changes

* Refactor

* Update

* Improve outline style

* Improve spacing

* Improve spacing

* UI changes for sidebar

* Update UI for sidebar

* Improve course UI

* Mark done, undone

* Add toggle lesson done/undone

* Update forward backward UI

* wip

* Minor ui change

* Responsiveness of sidebar

* wip

* wip

* wip: billing page

* wip

* Update UI

* fix: hide upgrade if paid user

* feat: token usage

* feat: list ai courses

* fix: limit for followup

* Course content responsiveness

* Make course content responsive

* Responsiveness

* Outline button

* Responsiveness of course content

* Responsiveness of course content

* Add course upgrade button

* Update design for upgrade

* Improve logic for upgrade and limits button

* Limits and errors

* Add lesson count

* Add course card

* Improve UI for course generator

* Update course functionality

* Refactor AI course generation

* Responsiveness of screen

* Improve

* Add responsiveness

* Improve empty billing page design

* Add empty billing screen

* Update UI for billing page

* Update UI for billing page

* Update UI for billing page

* Update billing page design

* Update

* Remove sidebar

* Update

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2025-03-12 13:17:38 +00:00
Jawher Kl
faf43f7905 fix: broken link (#8254) 2025-03-12 02:43:46 +06:00
Ed Lan
c9f450e166 fix: ai data scientist meta (#8320) 2025-03-12 02:36:20 +06:00
Vedansh
3b6d620ed8 feat: refractor flutter roadmap content (#8311)
* refractor - 100, 101, 102 topics

* refractor 103

* refractor 104 105

* refractor 106

* refractor 107 108 content

* refractor 109 content

* refractor 110 to 119 content.
2025-03-11 18:59:56 +06:00
github-actions[bot]
bd937f5dbe chore: update roadmap content json (#8317)
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
2025-03-11 18:59:29 +06:00
Kamran Ahmed
cf05610b86 Update course page text 2025-03-11 12:47:07 +00:00
244 changed files with 5609 additions and 988 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1739229597159
"lastUpdateCheck": 1741697790683
}
}

View File

@@ -1,4 +1,10 @@
PUBLIC_API_URL=https://api.roadmap.sh
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh
PUBLIC_COURSE_APP_URL=http://localhost:5173
PUBLIC_COURSE_APP_URL=http://localhost:5173
PUBLIC_STRIPE_INDIVIDUAL_MONTHLY_PRICE_ID=
PUBLIC_STRIPE_INDIVIDUAL_YEARLY_PRICE_ID=
PUBLIC_STRIPE_INDIVIDUAL_MONTHLY_PRICE_AMOUNT=10
PUBLIC_STRIPE_INDIVIDUAL_YEARLY_PRICE_AMOUNT=100

View File

@@ -53,6 +53,7 @@
"js-cookie": "^3.0.5",
"lucide-react": "^0.452.0",
"luxon": "^3.5.0",
"markdown-it-async": "^2.0.0",
"nanoid": "^5.0.7",
"nanostores": "^0.11.3",
"node-html-parser": "^6.1.13",
@@ -63,6 +64,7 @@
"react-calendar-heatmap": "^1.9.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.3.1",
"react-textarea-autosize": "^8.5.7",
"react-tooltip": "^5.28.0",
"reactflow": "^11.11.4",
"rehype-external-links": "^3.0.0",
@@ -72,6 +74,7 @@
"satori": "^0.11.2",
"satori-html": "^0.3.2",
"sharp": "^0.33.5",
"shiki": "^3.1.0",
"slugify": "^1.6.6",
"tailwind-merge": "^2.5.3",
"tailwindcss": "^3.4.13",

204
pnpm-lock.yaml generated
View File

@@ -80,6 +80,9 @@ importers:
luxon:
specifier: ^3.5.0
version: 3.5.0
markdown-it-async:
specifier: ^2.0.0
version: 2.0.0
nanoid:
specifier: ^5.0.7
version: 5.0.9
@@ -110,6 +113,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-textarea-autosize:
specifier: ^8.5.7
version: 8.5.7(@types/react@18.3.18)(react@18.3.1)
react-tooltip:
specifier: ^5.28.0
version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -137,6 +143,9 @@ importers:
sharp:
specifier: ^0.33.5
version: 0.33.5
shiki:
specifier: ^3.1.0
version: 3.1.0
slugify:
specifier: ^1.6.6
version: 1.6.6
@@ -396,6 +405,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.26.9':
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
engines: {node: '>=6.9.0'}
'@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'}
@@ -1179,24 +1192,45 @@ packages:
'@shikijs/core@1.29.1':
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
'@shikijs/core@3.1.0':
resolution: {integrity: sha512-1ppAOyg3F18N8Ge9DmJjGqRVswihN33rOgPovR6gUHW17Hw1L4RlRhnmVQcsacSHh0A8IO1FIgNbtTxUFwodmg==}
'@shikijs/engine-javascript@1.29.1':
resolution: {integrity: sha512-Hpi8k9x77rCQ7F/7zxIOUruNkNidMyBnP5qAGbLFqg4kRrg1HZhkB8btib5EXbQWTtLb5gBHOdBwshk20njD7Q==}
'@shikijs/engine-javascript@3.1.0':
resolution: {integrity: sha512-/LwkhW17jYi7uPcdaaSQQDNW+xgrHXarkrxYPoC6WPzH2xW5mFMw12doHXJBqxmYvtcTbaatcv2MkH9+3PU1FA==}
'@shikijs/engine-oniguruma@1.29.1':
resolution: {integrity: sha512-gSt2WhLNgEeLstcweQOSp+C+MhOpTsgdNXRqr3zP6M+BUBZ8Md9OU2BYwUYsALBxHza7hwaIWtFHjQ/aOOychw==}
'@shikijs/engine-oniguruma@3.1.0':
resolution: {integrity: sha512-reRgy8VzDPdiDocuGDD60Rk/jLxgcgy+6H4n6jYLeN2Yw5ikasRjQQx8ERXtDM35yg2v/d6KolDBcK8hYYhcmw==}
'@shikijs/langs@1.29.1':
resolution: {integrity: sha512-iERn4HlyuT044/FgrvLOaZgKVKf3PozjKjyV/RZ5GnlyYEAZFcgwHGkYboeBv2IybQG1KVS/e7VGgiAU4JY2Gw==}
'@shikijs/langs@3.1.0':
resolution: {integrity: sha512-hAM//sExPXAXG3ZDWjrmV6Vlw4zlWFOcT1ZXNhFRBwPP27scZu/ZIdZ+TdTgy06zSvyF4KIjnF8j6+ScKGu6ww==}
'@shikijs/themes@1.29.1':
resolution: {integrity: sha512-lb11zf72Vc9uxkl+aec2oW1HVTHJ2LtgZgumb4Rr6By3y/96VmlU44bkxEb8WBWH3RUtbqAJEN0jljD9cF7H7g==}
'@shikijs/themes@3.1.0':
resolution: {integrity: sha512-A4MJmy9+ydLNbNCtkmdTp8a+ON+MMXoUe1KTkELkyu0+pHGOcbouhNuobhZoK59cL4cOST6CCz1x+kUdkp9UZA==}
'@shikijs/types@1.29.1':
resolution: {integrity: sha512-aBqAuhYRp5vSir3Pc9+QPu9WESBOjUo03ao0IHLC4TyTioSsp/SkbAZSrIH4ghYYC1T1KTEpRSBa83bas4RnPA==}
'@shikijs/types@3.1.0':
resolution: {integrity: sha512-F8e7Fy4ihtcNpJG572BZZC1ErYrBrzJ5Cbc9Zi3REgWry43gIvjJ9lFAoUnuy7Bvy4IFz7grUSxL5edfrrjFEA==}
'@shikijs/vscode-textmate@10.0.1':
resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@shuding/opentype.js@1.4.0-beta.0':
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
engines: {node: '>= 8.0.0'}
@@ -2090,6 +2124,9 @@ packages:
hast-util-to-html@9.0.4:
resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==}
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
@@ -2343,6 +2380,9 @@ packages:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
markdown-it-async@2.0.0:
resolution: {integrity: sha512-jBthmQR5MwXR9Y8Y0teRoZAenaKQMdjuTfpbNARqMBSRPvyzyXCVduHZHakyyhL3ugIacCobXJrO07t277sIjw==}
markdown-it-task-lists@2.1.1:
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
@@ -2603,6 +2643,9 @@ packages:
oniguruma-to-es@2.3.0:
resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==}
oniguruma-to-es@3.1.1:
resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==}
openai@4.80.1:
resolution: {integrity: sha512-+6+bbXFwbIE88foZsBEt36bPkgZPdyFN82clAXG61gnHb2gXdZApDyRrcAHqEtpYICywpqaNo57kOm9dtnb7Cw==}
hasBin: true
@@ -2847,6 +2890,9 @@ packages:
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
@@ -2942,6 +2988,12 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react-textarea-autosize@8.5.7:
resolution: {integrity: sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-tooltip@5.28.0:
resolution: {integrity: sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==}
peerDependencies:
@@ -2965,15 +3017,24 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
regex-recursion@5.1.1:
resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
regex-utilities@2.3.0:
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
regex@5.1.1:
resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==}
regex@6.0.1:
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
rehype-external-links@3.0.0:
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
@@ -3110,6 +3171,9 @@ packages:
shiki@1.29.1:
resolution: {integrity: sha512-TghWKV9pJTd/N+IgAIVJtr0qZkB7FfFCUrrEJc0aRmZupo3D1OCVRknQWVRVA7AX/M0Ld7QfoAruPzr3CnUJuw==}
shiki@3.1.0:
resolution: {integrity: sha512-LdTNyWQlC5zdCaHdcp1zPA1OVA2ivb+KjGOOnGcy02tGaF5ja+dGibWFH7Ar8YlngUgK/scDqworK18Ys9cbYA==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -3348,6 +3412,33 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
use-composed-ref@1.4.0:
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-isomorphic-layout-effect@1.2.0:
resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-latest@1.3.0:
resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sync-external-store@1.4.0:
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
peerDependencies:
@@ -3743,6 +3834,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/runtime@7.26.9':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
@@ -4336,32 +4431,65 @@ snapshots:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.4
'@shikijs/core@3.1.0':
dependencies:
'@shikijs/types': 3.1.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@1.29.1':
dependencies:
'@shikijs/types': 1.29.1
'@shikijs/vscode-textmate': 10.0.1
oniguruma-to-es: 2.3.0
'@shikijs/engine-javascript@3.1.0':
dependencies:
'@shikijs/types': 3.1.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 3.1.1
'@shikijs/engine-oniguruma@1.29.1':
dependencies:
'@shikijs/types': 1.29.1
'@shikijs/vscode-textmate': 10.0.1
'@shikijs/engine-oniguruma@3.1.0':
dependencies:
'@shikijs/types': 3.1.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@1.29.1':
dependencies:
'@shikijs/types': 1.29.1
'@shikijs/langs@3.1.0':
dependencies:
'@shikijs/types': 3.1.0
'@shikijs/themes@1.29.1':
dependencies:
'@shikijs/types': 1.29.1
'@shikijs/themes@3.1.0':
dependencies:
'@shikijs/types': 3.1.0
'@shikijs/types@1.29.1':
dependencies:
'@shikijs/vscode-textmate': 10.0.1
'@types/hast': 3.0.4
'@shikijs/types@3.1.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.1': {}
'@shikijs/vscode-textmate@10.0.2': {}
'@shuding/opentype.js@1.4.0-beta.0':
dependencies:
fflate: 0.7.4
@@ -5381,6 +5509,20 @@ snapshots:
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
comma-separated-tokens: 2.0.3
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
property-information: 7.0.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -5611,6 +5753,11 @@ snapshots:
dependencies:
semver: 6.3.1
markdown-it-async@2.0.0:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
markdown-it-task-lists@2.1.1: {}
markdown-it@14.1.0:
@@ -6027,6 +6174,12 @@ snapshots:
regex: 5.1.1
regex-recursion: 5.1.1
oniguruma-to-es@3.1.1:
dependencies:
emoji-regex-xs: 1.0.0
regex: 6.0.1
regex-recursion: 6.0.2
openai@4.80.1(zod@3.24.1):
dependencies:
'@types/node': 18.19.74
@@ -6214,6 +6367,8 @@ snapshots:
property-information@6.5.0: {}
property-information@7.0.0: {}
prosemirror-changeset@2.2.1:
dependencies:
prosemirror-transform: 1.10.2
@@ -6348,6 +6503,15 @@ snapshots:
react-refresh@0.14.2: {}
react-textarea-autosize@8.5.7(@types/react@18.3.18)(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.9
react: 18.3.1
use-composed-ref: 1.4.0(@types/react@18.3.18)(react@18.3.1)
use-latest: 1.3.0(@types/react@18.3.18)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
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
@@ -6381,17 +6545,27 @@ snapshots:
dependencies:
picomatch: 2.3.1
regenerator-runtime@0.14.1: {}
regex-recursion@5.1.1:
dependencies:
regex: 5.1.1
regex-utilities: 2.3.0
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
regex-utilities@2.3.0: {}
regex@5.1.1:
dependencies:
regex-utilities: 2.3.0
regex@6.0.1:
dependencies:
regex-utilities: 2.3.0
rehype-external-links@3.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -6655,6 +6829,17 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.1
'@types/hast': 3.0.4
shiki@3.1.0:
dependencies:
'@shikijs/core': 3.1.0
'@shikijs/engine-javascript': 3.1.0
'@shikijs/engine-oniguruma': 3.1.0
'@shikijs/langs': 3.1.0
'@shikijs/themes': 3.1.0
'@shikijs/types': 3.1.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
@@ -6912,6 +7097,25 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
use-composed-ref@1.4.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
use-isomorphic-layout-effect@1.2.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
use-latest@1.3.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.18)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
use-sync-external-store@1.4.0(react@18.3.1):
dependencies:
react: 18.3.1

View File

@@ -2311,31 +2311,10 @@
}
]
},
"gBuaVZfqJ0-g21sKohQtx": {
"azuer-service-bus@gBuaVZfqJ0-g21sKohQtx.md": {
"title": "Azuer Service Bus",
"description": "Azure Service Bus is a scalable and reliable messaging platform that can handle a high volume of messages, it's also easy to use, has a lot of features like subscription, Topics, Dead Letter, and easy to integrate with other Azure services, and it's a managed service which means Microsoft takes care of the infrastructure and scaling. However, it's worth noting that Azure Service Bus is a paid service and the cost will depend on the number of messages and the size of the data that you are sending and receiving.\n\nTo learn more, visit the following links:",
"links": [
{
"title": "Getting Started With Azure Service Bus and ASP.NET Core",
"url": "https://www.c-sharpcorner.com/article/get-started-with-azure-service-bus-queues-asp-net-core-part-1/",
"type": "article"
},
{
"title": "How to Send & receive messages from Azure Service Bus queue (.NET)?",
"url": "https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues?tabs=passwordless",
"type": "article"
},
{
"title": "What is Azure Service Bus?",
"url": "https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview",
"type": "article"
},
{
"title": "Explore top posts about Azure",
"url": "https://app.daily.dev/tags/azure?ref=roadmapsh",
"type": "article"
}
]
"description": "",
"links": []
},
"SQKIUa_UsJ4cls-Vs9yHU": {
"title": "Mass Transit",

View File

@@ -3365,11 +3365,6 @@
"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"
}
]
},
@@ -3417,14 +3412,8 @@
},
"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": [
{
"title": "BASE Model vs. ACID Model",
"url": "https://www.geeksforgeeks.org/acid-model-vs-base-model-for-database/",
"type": "article"
}
]
"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",
"links": []
},
"uqfeiQ9K--QkGNwks4kjk": {
"title": "CAP Theorem",
@@ -4095,29 +4084,8 @@
},
"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",
"links": [
{
"title": "CPU Scheduling in Operating Systems - geeksforgeeks",
"url": "https://www.geeksforgeeks.org/cpu-scheduling-in-operating-systems/",
"type": "article"
},
{
"title": "Lottery Scheduling for Operating Systems - geeksforgeeks",
"url": "https://www.geeksforgeeks.org/lottery-process-scheduling-in-operating-system/",
"type": "article"
},
{
"title": "Program for Round Robin Scheduling for the same Arrival time - geeksforgeeks",
"url": "https://www.geeksforgeeks.org/program-for-round-robin-scheduling-for-the-same-arrival-time/",
"type": "article"
},
{
"title": "Introduction to CPU Scheduling",
"url": "https://youtu.be/EWkQl0n0w5M?si=Lb-PxN_t-rDfn4JL",
"type": "video"
}
]
"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.",
"links": []
},
"cpQvB0qMDL3-NWret7oeA": {
"title": "CPU Interrupts",

View File

@@ -521,11 +521,6 @@
"title": "Operating Systems",
"description": "**Operating systems (OS)** are software that manage computer hardware and provide a platform for applications to run. They handle essential functions such as managing memory, processing tasks, controlling input and output devices, and facilitating file management. Key examples include **Windows**, **macOS**, **Linux**, and **Unix**. Each operating system offers different features and interfaces, tailored to specific user needs or system requirements, from desktop computing to server management and embedded systems.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is an operating system?",
"url": "https://www.geeksforgeeks.org/what-is-an-operating-system/",
"type": "article"
},
{
"title": "What is an operating system as fast as possible",
"url": "https://www.youtube.com/watch?v=pVzRTmdd9j0",
@@ -708,11 +703,6 @@
"title": "What are Network Protocols?",
"url": "https://www.solarwinds.com/resources/it-glossary/network-protocols",
"type": "article"
},
{
"title": "Types of Network Topology",
"url": "https://www.geeksforgeeks.org/types-of-network-topology/",
"type": "article"
}
]
},
@@ -853,11 +843,6 @@
"title": "loopback",
"description": "**Loopback** refers to a special network interface used to send traffic back to the same device for testing and diagnostic purposes. The loopback address for IPv4 is `127.0.0.1`, while for IPv6 it is `::1`. When a device sends a request to the loopback address, the network data does not leave the local machine; instead, it is processed internally, allowing developers to test applications or network services without requiring external network access. Loopback is commonly used to simulate network traffic, check local services, or debug issues locally.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is a loopback address?",
"url": "https://www.geeksforgeeks.org/what-is-a-loopback-address/",
"type": "article"
},
{
"title": "Understanding the loopback address and loopback interfaces",
"url": "https://study-ccna.com/loopback-interface-loopback-address/",
@@ -1256,11 +1241,6 @@
"title": "Star",
"description": "A star network topology is a configuration where all devices (nodes) are connected directly to a central hub or switch. In this arrangement, each node has a dedicated point-to-point link to the central device, forming a star-like structure. This topology offers advantages such as easy installation and reconfiguration, centralized management, and fault isolation. If one connection fails, it doesn't affect others. However, the central hub is a single point of failure for the entire network. Star topologies are commonly used in local area networks (LANs) due to their reliability, scalability, and ease of maintenance, making them a popular choice in both small office and large enterprise environments.\n\nLearn more from the following resources:",
"links": [
{
"title": "Advantages and Disadvantages of Star Topology",
"url": "https://www.geeksforgeeks.org/advantages-and-disadvantages-of-star-topology/",
"type": "article"
},
{
"title": "Star Topology",
"url": "https://www.youtube.com/watch?v=EQ3rW22-Py0",
@@ -1698,11 +1678,6 @@
"title": "Protocol Analyzers",
"description": "**Protocol analyzers**, also known as network analyzers or packet sniffers, are tools used to capture, inspect, and analyze network traffic. They help diagnose network issues, troubleshoot performance problems, and ensure security by providing detailed insights into the data packets transmitted across a network. Protocol analyzers decode and display various network protocols, such as TCP/IP, HTTP, and DNS, allowing users to understand communication patterns, detect anomalies, and identify potential vulnerabilities. Popular examples include Wireshark and tcpdump.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is a protocol analyzer?",
"url": "https://www.geeksforgeeks.org/what-is-protocol-analyzer/",
"type": "article"
},
{
"title": "Protocol Analyzers",
"url": "https://www.youtube.com/watch?v=hTMhlB-o0Ow",
@@ -1733,14 +1708,8 @@
},
"xFuWk7M-Vctk_xb7bHbWs": {
"title": "route",
"description": "The `route` command is a network utility used to view and manipulate the IP routing table on Unix-like and Windows systems. It allows users to display the current routes that data packets take, as well as add, modify, or delete routes for network traffic. This command is often used in network troubleshooting and configuration to control how data flows between different networks and subnets. By specifying routes manually, administrators can define specific paths for network traffic, bypassing default routes and optimizing performance or security.\n\nLearn more from the following resources:",
"links": [
{
"title": "How to check the routing table in Linux",
"url": "https://www.geeksforgeeks.org/route-command-in-linux-with-examples/",
"type": "article"
}
]
"description": "The `route` command is a network utility used to view and manipulate the IP routing table on Unix-like and Windows systems. It allows users to display the current routes that data packets take, as well as add, modify, or delete routes for network traffic. This command is often used in network troubleshooting and configuration to control how data flows between different networks and subnets. By specifying routes manually, administrators can define specific paths for network traffic, bypassing default routes and optimizing performance or security.",
"links": []
},
"y8GaUNpaCT1Ai88wPOk6d": {
"title": "tcpdump",
@@ -2308,11 +2277,6 @@
"title": "Understand Handshakes",
"description": "In networking and cybersecurity, a handshake is a process of establishing a secure connection between two parties before data exchange begins. It typically involves a series of predefined messages exchanged to verify identities, agree on communication parameters, and sometimes establish encryption keys. The most common example is the TCP three-way handshake used to initiate a connection. In cryptographic protocols like TLS/SSL, handshakes are more complex, involving certificate verification and key exchange. Handshakes are crucial for ensuring secure, authenticated communications, preventing unauthorized access, and setting up the parameters for efficient data transfer in various network protocols and security systems.\n\nLearn more from the following resources:",
"links": [
{
"title": "TCP 3-Way Handshake Process",
"url": "https://www.geeksforgeeks.org/tcp-3-way-handshake-process/",
"type": "article"
},
{
"title": "TLS Handshake Explained",
"url": "https://www.youtube.com/watch?v=86cQJ0MMses",
@@ -3110,14 +3074,8 @@
},
"W7bcydXdwlubXF2PHKOuq": {
"title": "Port Blocking",
"description": "Port blocking is an essential practice in hardening the security of your network and devices. It involves restricting, filtering, or entirely denying access to specific network ports to minimize exposure to potential cyber threats. By limiting access to certain ports, you can effectively safeguard your systems against unauthorized access and reduce the likelihood of security breaches.\n\nLearn more from the following resources:",
"links": [
{
"title": "What is port blocking with LAN?",
"url": "https://www.geeksforgeeks.org/what-is-port-blocking-within-lan/",
"type": "article"
}
]
"description": "Port blocking is an essential practice in hardening the security of your network and devices. It involves restricting, filtering, or entirely denying access to specific network ports to minimize exposure to potential cyber threats. By limiting access to certain ports, you can effectively safeguard your systems against unauthorized access and reduce the likelihood of security breaches.",
"links": []
},
"FxuMJmDoDkIsPFp2iocFg": {
"title": "Group Policy",

View File

@@ -19,11 +19,6 @@
"title": "Types of Data Analytics",
"description": "Data Analytics has proven to be a critical part of decision-making in modern business ventures. It is responsible for discovering, interpreting, and transforming data into valuable information. Different types of data analytics look at past, present, or predictive views of business operations.\n\nData Analysts, as ambassadors of this domain, employ these types, to answer various questions:\n\n* Descriptive Analytics _(what happened in the past?)_\n* Diagnostic Analytics _(why did it happened in the past?)_\n* Predictive Analytics _(what will happen in the future?)_\n* Prescriptive Analytics _(how can we make it happen?)_\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Data Analytics and its type",
"url": "https://www.geeksforgeeks.org/data-analytics-and-its-type/",
"type": "article"
},
{
"title": "The 4 Types of Data Analysis: Ultimate Guide",
"url": "https://careerfoundry.com/en/blog/data-analytics/different-types-of-data-analysis/",

View File

@@ -986,11 +986,6 @@
"title": "Computer Graphics",
"description": "Computer Graphics is a subfield of computer science that studies methods for digitally synthesizing and manipulating visual content. It involves creating and manipulating visual content using specialized computer software and hardware. This field is primarily used in the creation of digital and video games, CGI in films, and also in visual effects for commercials. The field is divided into two major categories: **Raster graphics** and **Vector graphics**. Raster graphics, also known as bitmap, involve the representation of images through a dot matrix data structure, while Vector graphics involve the use of polygons to represent images in computer graphics. Both of these methods have their unique usage scenarios. Other concepts integral to the study of computer graphics include rendering (including both real-time rendering and offline rendering), animation, and 3D modeling. Generally, computer graphics skills are essential for game developers and animation experts.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "What is Computer Graphics?",
"url": "https://www.geeksforgeeks.org/introduction-to-computer-graphics/",
"type": "article"
},
{
"title": "Introduction to Computer Graphics",
"url": "https://open.umn.edu/opentextbooks/textbooks/420",
@@ -1896,11 +1891,6 @@
"title": "MCTS Algorithm",
"url": "https://en.wikipedia.org/wiki/Monte_Carlo_tree_search/",
"type": "article"
},
{
"title": "Monte Carlo Tree Search",
"url": "https://www.geeksforgeeks.org/ml-monte-carlo-tree-search-mcts/",
"type": "article"
}
]
},
@@ -1998,11 +1988,6 @@
"title": "Artificial Neural Network",
"description": "Artificial Neural Networks (ANN) are a branch of machine learning that draw inspiration from biological neural networks. ANNs are capable of 'learning' from observational data, thereby enhancing game development in numerous ways. They consist of interconnected layers of nodes, or artificial neurons, that process information through their interconnected network. Each node's connection has numerical weight that gets adjusted during learning, which helps in optimizing problem solving. ANNs are utilized in various aspects of game development, such as improving AI behavior, procedural content generation, and game testing. They can also be used for image recognition tasks, such as identifying objects or actions in a game environment.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Artificial Neural Networks (ANN)",
"url": "https://www.geeksforgeeks.org/artificial-neural-networks-and-its-applications/",
"type": "article"
},
{
"title": "What is ANN?",
"url": "https://www.coursera.org/articles/artificial-neural-network",

View File

@@ -670,11 +670,6 @@
"url": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-outside-collaborators/adding-outside-collaborators-to-repositories-in-your-organization",
"type": "article"
},
{
"title": "What are github collaborators",
"url": "https://www.geeksforgeeks.org/what-are-github-collaborators/",
"type": "article"
},
{
"title": "How to Add Collaborators to Your GitHub Repository",
"url": "https://www.blinkops.com/blog/how-to-add-collaborators-to-your-github-repository",

View File

@@ -334,11 +334,6 @@
"title": "JavaScript Scope",
"url": "https://www.w3schools.com/js/js_scope.asp",
"type": "article"
},
{
"title": "Block Scoping in JavaScript",
"url": "https://www.geeksforgeeks.org/javascript-es2015-block-scoping",
"type": "article"
}
]
},

View File

@@ -251,11 +251,6 @@
"title": "What is the Relational Model?",
"url": "https://www.postgresql.org/docs/7.1/relmodel-oper.html",
"type": "article"
},
{
"title": "The Relational Model",
"url": "https://www.geeksforgeeks.org/relational-model-in-dbms/",
"type": "article"
}
]
},

View File

@@ -243,11 +243,6 @@
"title": "Loops",
"description": "Loops are used to execute a block of code repeatedly.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Loops in Python",
"url": "https://www.geeksforgeeks.org/loops-in-python/",
"type": "article"
},
{
"title": "Python \"while\" Loops (Indefinite Iteration)",
"url": "https://realpython.com/python-while-loop/",

View File

@@ -732,11 +732,6 @@
"title": "Redis Lists",
"url": "https://redis.io/docs/latest/develop/data-types/lists/",
"type": "article"
},
{
"title": "Complete Guide to Redis Lists",
"url": "https://www.geeksforgeeks.org/complete-guide-to-redis-lists/",
"type": "article"
}
]
},
@@ -976,11 +971,6 @@
"title": "Redis Pipelining",
"url": "https://redis.io/docs/latest/develop/use/pipelining/",
"type": "article"
},
{
"title": "Complete Guide to Redis Pipelining",
"url": "https://www.geeksforgeeks.org/complete-guide-to-redis-pipelining/",
"type": "article"
}
]
},

View File

@@ -38,14 +38,8 @@
},
"2sR4KULvAUUoOtopvsEBs": {
"title": "Levels of Architecture",
"description": "Architecture can be done on several “levels” of abstractions. The level influences the importance of necessary skills. As there are many categorizations possible my favorite segmentation includes these 3 levels:\n\n* **Application Level:** The lowest level of architecture. Focus on one single application. Very detailed, low level design. Communication is usually within one development team.\n* **Solution Level:** The mid-level of architecture. Focus on one or more applications which fulfill a business need (business solution). Some high, but mainly low-level design. Communication is between multiple development teams.\n* **Enterprise Level:** The highest level of architecture. Focus on multiple solutions. High level, abstract design, which needs to be detailed out by solution or application architects. Communication is across the organization.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Software Engineering Architecture",
"url": "https://www.geeksforgeeks.org/software-engineering-architectural-design/",
"type": "article"
}
]
"description": "Architecture can be done on several “levels” of abstractions. The level influences the importance of necessary skills. As there are many categorizations possible my favorite segmentation includes these 3 levels:\n\n* **Application Level:** The lowest level of architecture. Focus on one single application. Very detailed, low level design. Communication is usually within one development team.\n* **Solution Level:** The mid-level of architecture. Focus on one or more applications which fulfill a business need (business solution). Some high, but mainly low-level design. Communication is between multiple development teams.\n* **Enterprise Level:** The highest level of architecture. Focus on multiple solutions. High level, abstract design, which needs to be detailed out by solution or application architects. Communication is across the organization.",
"links": []
},
"Lqe47l4j-C4OwkbkwPYry": {
"title": "Application Architecture",
@@ -103,14 +97,8 @@
},
"lBtlDFPEQvQ_xtLtehU0S": {
"title": "Important Skills to Learn",
"description": "To support the laid-out activities specific skills are required. From my experience, read books and discussions we can boil this down to these ten skills every software architect should have:\n\n* Design\n* Decide\n* Simplify\n* Code\n* Document\n* Communicate\n* Estimate\n* Balance\n* Consult\n* Market\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Software Architect Skills",
"url": "https://www.geeksforgeeks.org/software-architects-skills/",
"type": "article"
}
]
"description": "To support the laid-out activities specific skills are required. From my experience, read books and discussions we can boil this down to these ten skills every software architect should have:\n\n* Design\n* Decide\n* Simplify\n* Code\n* Document\n* Communicate\n* Estimate\n* Balance\n* Consult\n* Market",
"links": []
},
"fBd2m8tMJmhuNSaakrpg4": {
"title": "Design & Architecture",
@@ -854,14 +842,8 @@
},
"SuMhTyaBS9vwASxAt39DH": {
"title": "Tools",
"description": "Architect tools are software tools that help architects to design, document, and manage software architectures. These tools can be used to create architecture diagrams, generate code, and automate the software development process.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Top 10 Software Architecture Tools in 2024",
"url": "https://www.geeksforgeeks.org/software-architecture-tools/",
"type": "article"
}
]
"description": "Architect tools are software tools that help architects to design, document, and manage software architectures. These tools can be used to create architecture diagrams, generate code, and automate the software development process.",
"links": []
},
"OaLmlfkZid7hKqJ9G8oNV": {
"title": "Architecture",
@@ -1856,11 +1838,6 @@
"url": "https://www.fortinet.com/resources/cyberglossary/tcp-ip#:~:text=The%20TCP%2FIP%20model%20defines,exchanged%20and%20organized%20over%20networks.",
"type": "article"
},
{
"title": "TCP/IP Model",
"url": "https://www.geeksforgeeks.org/tcp-ip-model/",
"type": "article"
},
{
"title": "What is TCP/IP and How Does it Work?",
"url": "https://www.techtarget.com/searchnetworking/definition/TCP-IP",

View File

@@ -652,14 +652,8 @@
},
"lvtTSHH9yBTCiLng8btnI": {
"title": "Hybrid Types",
"description": "In TypeScript, a hybrid type is a type that combines multiple types into a single type. The resulting type is considered a union of those types. This allows you to specify that a value can have multiple types, rather than just one.\n\nFor example, you can create a hybrid type that can accept either a string or a number:\n\n type StringOrNumber = string | number;\n \n\nYou can also use hybrid types to create more complex types that can represent a combination of several different types of values. For example:\n\n type Education = {\n degree: string;\n school: string;\n year: number;\n };\n \n type User = {\n name: string;\n age: number;\n email: string;\n education: Education;\n };\n \n\nLearn more from the following links:",
"links": [
{
"title": "Geeksforgeeks.org - Hybrid Types",
"url": "https://www.geeksforgeeks.org/what-are-hybrid-types-in-typescript/#:~:text=Hybrid%20types%20are%20a%20combination,properties%20like%20a%20regular%20object.",
"type": "article"
}
]
"description": "In TypeScript, a hybrid type is a type that combines multiple types into a single type. The resulting type is considered a union of those types. This allows you to specify that a value can have multiple types, rather than just one.\n\nFor example, you can create a hybrid type that can accept either a string or a number:\n\n type StringOrNumber = string | number;\n \n\nYou can also use hybrid types to create more complex types that can represent a combination of several different types of values. For example:\n\n type Education = {\n degree: string;\n school: string;\n year: number;\n };\n \n type User = {\n name: string;\n age: number;\n email: string;\n education: Education;\n };",
"links": []
},
"ib0jfZzukYOZ42AdJqt_W": {
"title": "Classes",

View File

@@ -573,11 +573,6 @@
"title": "Binding Events",
"url": "https://vuejs.org/guide/essentials/event-handling",
"type": "article"
},
{
"title": "Vue.js Event Handling",
"url": "https://www.geeksforgeeks.org/vue-js-event-handling/",
"type": "article"
}
]
},

View File

@@ -18,3 +18,16 @@ export function aiRoadmapApi(context: APIContext) {
},
};
}
export interface AICourseDocument {
_id: string;
userId: string;
title: string;
slug?: string;
keyword: string;
difficulty: string;
data: string;
viewCount: number;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -27,7 +27,7 @@ const sidebarLinks = [
href: '/account/update-profile',
title: 'Profile',
id: 'profile',
isNew: true,
isNew: false,
icon: {
glyph: 'user',
classes: 'h-4 w-4',
@@ -56,7 +56,7 @@ const sidebarLinks = [
},
{
href: '/account/road-card',
title: 'Card',
title: 'Road Card',
id: 'road-card',
isNew: false,
icon: {
@@ -64,6 +64,16 @@ const sidebarLinks = [
classes: 'h-4 w-4',
},
},
{
href: '/account/billing',
title: 'Billing',
id: 'billing',
isNew: true,
icon: {
glyph: 'credit-card',
classes: 'h-4 w-4',
},
},
{
href: '/account/settings',
title: 'Settings',
@@ -97,7 +107,7 @@ const sidebarLinks = [
}`}
>
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
Teams
Teams
</a>
</li>
{

View File

@@ -1,5 +1,4 @@
import { getUser } from '../../lib/jwt';
import { getPercentage } from '../../helper/number';
import { ProjectProgressActions } from './ProjectProgressActions';
import { cn } from '../../lib/classname';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';

View File

@@ -1,7 +1,7 @@
import { getUser } from '../../lib/jwt';
import { getPercentage } from '../../helper/number';
import { ResourceProgressActions } from './ResourceProgressActions';
import { cn } from '../../lib/classname';
import { getPercentage } from '../../lib/number';
type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice';

View File

@@ -7,7 +7,7 @@ import { AuthenticationForm } from './AuthenticationForm';
<Popup id='login-popup' title='' subtitle=''>
<div class='mb-7 text-center'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
Login to your account
Login or Signup
</p>
<p class='mt-2 text-sm leading-4 text-slate-600'>
You must be logged in to perform this action.

View File

@@ -0,0 +1,226 @@
import { useEffect, useState } from 'react';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import { useMutation, useQuery } from '@tanstack/react-query';
import {
billingDetailsOptions,
USER_SUBSCRIPTION_PLAN_PRICES,
} from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { UpgradeAccountModal } from './UpgradeAccountModal';
import { getUrlParams } from '../../lib/browser';
import { VerifyUpgrade } from './VerifyUpgrade';
import { EmptyBillingScreen } from './EmptyBillingScreen';
import {
Calendar,
RefreshCw,
Loader2,
CreditCard,
ArrowRightLeft,
CircleX,
} from 'lucide-react';
import { BillingWarning } from './BillingWarning';
export type CreateCustomerPortalBody = {};
export type CreateCustomerPortalResponse = {
url: string;
};
export function BillingPage() {
const toast = useToast();
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showVerifyUpgradeModal, setShowVerifyUpgradeModal] = useState(false);
const { data: billingDetails, isPending: isLoadingBillingDetails } = useQuery(
billingDetailsOptions(),
queryClient,
);
const isCanceled =
billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd;
const isPastDue = billingDetails?.status === 'past_due';
const {
mutate: createCustomerPortal,
isSuccess: isCreatingCustomerPortalSuccess,
isPending: isCreatingCustomerPortal,
} = useMutation(
{
mutationFn: (body: CreateCustomerPortalBody) => {
return httpPost<CreateCustomerPortalResponse>(
'/v1-create-customer-portal',
body,
);
},
onSuccess: (data) => {
window.location.href = data.url;
},
onError: (error) => {
console.error(error);
toast.error(error?.message || 'Failed to Create Customer Portal');
},
},
queryClient,
);
useEffect(() => {
if (isLoadingBillingDetails) {
return;
}
pageProgressMessage.set('');
const shouldVerifyUpgrade = getUrlParams()?.s === '1';
if (shouldVerifyUpgrade) {
setShowVerifyUpgradeModal(true);
}
}, [isLoadingBillingDetails]);
if (isLoadingBillingDetails || !billingDetails) {
return null;
}
const selectedPlanDetails = USER_SUBSCRIPTION_PLAN_PRICES.find(
(plan) => plan.priceId === billingDetails?.priceId,
);
const priceDetails = selectedPlanDetails;
const formattedNextBillDate = new Date(
billingDetails?.currentPeriodEnd || '',
).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<>
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
setShowUpgradeModal(false);
}}
success="/account/billing?s=1"
cancel="/account/billing"
/>
)}
{showVerifyUpgradeModal && <VerifyUpgrade />}
{billingDetails?.status === 'none' && !isLoadingBillingDetails && (
<EmptyBillingScreen onUpgrade={() => setShowUpgradeModal(true)} />
)}
{billingDetails?.status !== 'none' &&
!isLoadingBillingDetails &&
priceDetails && (
<div className="mt-1">
{isCanceled && (
<BillingWarning
icon={CircleX}
message="Your subscription has been canceled."
buttonText="Reactivate?"
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
{isPastDue && (
<BillingWarning
message="We were not able to charge your card."
buttonText="Update payment information."
onButtonClick={() => {
createCustomerPortal({});
}}
isLoading={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
/>
)}
<h2 className="mb-2 text-xl font-semibold text-black">
Current Subscription
</h2>
<p className="text-sm text-gray-500">
Thank you for being a pro member. Your plan details are below.
</p>
<div className="mt-8 flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<RefreshCw className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs uppercase tracking-wider text-gray-400">
Payment
</span>
<h3 className="flex items-baseline text-lg font-semibold text-black">
${priceDetails.amount}
<span className="ml-1 text-sm font-normal text-gray-500">
/ {priceDetails.interval}
</span>
</h3>
</div>
</div>
</div>
<div className="mt-6 border-t border-gray-100 pt-6">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<Calendar className="size-5 text-gray-600" />
</div>
<div>
<span className="text-xs uppercase tracking-wider text-gray-400">
{billingDetails?.cancelAtPeriodEnd
? 'Expires On'
: 'Renews On'}
</span>
<h3 className="text-lg font-semibold text-black">
{formattedNextBillDate}
</h3>
</div>
</div>
<div className="mt-8 flex gap-3 max-sm:flex-col">
{!isCanceled && (
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 max-sm:flex-grow"
onClick={() => {
setShowUpgradeModal(true);
}}
>
<ArrowRightLeft className="mr-2 h-4 w-4" />
Switch Plan
</button>
)}
<button
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => {
createCustomerPortal({});
}}
disabled={
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess
}
>
{isCreatingCustomerPortal ||
isCreatingCustomerPortalSuccess ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<CreditCard className="mr-2 h-4 w-4" />
)}
Manage Subscription
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,39 @@
import { AlertTriangle, type LucideIcon } from 'lucide-react';
export type BillingWarningProps = {
icon?: LucideIcon;
message: string;
onButtonClick?: () => void;
buttonText?: string;
isLoading?: boolean;
};
export function BillingWarning(props: BillingWarningProps) {
const {
message,
onButtonClick,
buttonText,
isLoading,
icon: Icon = AlertTriangle,
} = props;
return (
<div className="mb-6 flex items-center gap-2 rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-600">
<Icon className="h-5 w-5" />
<span>
{message}
{buttonText && (
<button
disabled={isLoading}
onClick={() => {
onButtonClick?.();
}}
className="font-semibold underline underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50 ml-0.5"
>
{buttonText}
</button>
)}
</span>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import { VerifyUpgrade } from "./VerifyUpgrade";
export function CheckSubscriptionVerification() {
const [shouldVerifyUpgrade, setShouldVerifyUpgrade] = useState(false);
useEffect(() => {
const params = getUrlParams();
if (params.s !== '1') {
return;
}
setShouldVerifyUpgrade(true);
}, []);
if (!shouldVerifyUpgrade) {
return null;
}
return <VerifyUpgrade />;
}

View File

@@ -0,0 +1,68 @@
import {
CreditCard,
Ellipsis,
HeartHandshake,
MessageCircleIcon,
SparklesIcon,
Zap,
} from 'lucide-react';
type EmptyBillingScreenProps = {
onUpgrade: () => void;
};
const perks = [
{
icon: Zap,
text: 'Unlimited AI course generations',
},
{
icon: MessageCircleIcon,
text: 'Unlimited AI Chat feature usage',
},
{
icon: SparklesIcon,
text: 'Early access to new features',
},
{
icon: HeartHandshake,
text: 'Support the development of platform',
},
{
icon: Ellipsis,
text: 'more perks coming soon!',
},
];
export function EmptyBillingScreen(props: EmptyBillingScreenProps) {
const { onUpgrade } = props;
return (
<div className="mt-12 flex h-full w-full flex-col items-center">
<CreditCard className="mb-3 h-12 w-12 text-gray-300" />
<h3 className="mb-3 text-xl font-semibold text-black">
No Active Subscription
</h3>
<p className="text-balance text-gray-700">
Unlock pro benefits by upgrading to a subscription
</p>
<div className="my-8 flex flex-col gap-2">
{perks.map((perk) => (
<p className="textsm flex items-center text-gray-600" key={perk.text}>
<perk.icon className="mr-2 h-4 w-4 text-gray-500" />
{perk.text}
</p>
))}
</div>
<button
onClick={onUpgrade}
className="inline-flex items-center justify-center rounded-lg bg-black px-6 py-2.5 text-sm font-medium text-white transition-colors hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
>
Upgrade Account
</button>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { useMutation } from '@tanstack/react-query';
import type { USER_SUBSCRIPTION_PLAN_PRICES } from '../../queries/billing';
import { Modal } from '../Modal';
import { queryClient } from '../../stores/query-client';
import { useToast } from '../../hooks/use-toast';
import { VerifyUpgrade } from './VerifyUpgrade';
import { Loader2Icon } from 'lucide-react';
import { httpPost } from '../../lib/query-http';
type UpdatePlanBody = {
priceId: string;
};
type UpdatePlanResponse = {
status: 'ok';
};
type UpdatePlanConfirmationProps = {
planDetails: (typeof USER_SUBSCRIPTION_PLAN_PRICES)[number];
onClose: () => void;
onCancel: () => void;
};
export function UpdatePlanConfirmation(props: UpdatePlanConfirmationProps) {
const { planDetails, onClose, onCancel } = props;
const toast = useToast();
const {
mutate: updatePlan,
isPending,
status,
} = useMutation(
{
mutationFn: (body: UpdatePlanBody) => {
return httpPost<UpdatePlanResponse>(
'/v1-update-subscription-plan',
body,
);
},
onError: (error) => {
console.error(error);
toast.error(error?.message || 'Failed to Create Customer Portal');
},
},
queryClient,
);
if (!planDetails) {
return null;
}
const selectedPrice = planDetails;
if (status === 'success') {
return <VerifyUpgrade newPriceId={selectedPrice.priceId} />;
}
return (
<Modal
onClose={isPending ? () => {} : onClose}
bodyClassName="rounded-xl bg-white p-6"
>
<h3 className="text-xl font-bold text-black">Subscription Update</h3>
<p className="mt-2 text-balance text-gray-600">
Your plan will be updated to the{' '}
<b className="text-black">{planDetails.interval}</b> plan, and will
be charged{' '}
<b className="text-black">
${selectedPrice.amount}/{selectedPrice.interval}
</b>
.
</p>
<div className="mt-6 grid grid-cols-2 gap-3">
<button
className="rounded-md border border-gray-200 py-2 text-sm font-semibold hover:bg-gray-50 transition-colors disabled:opacity-50"
onClick={onCancel}
disabled={isPending}
>
Cancel
</button>
<button
className="flex items-center justify-center rounded-md bg-purple-600 py-2 text-sm font-semibold text-white hover:bg-purple-500 transition-colors disabled:opacity-50"
disabled={isPending}
onClick={() => {
updatePlan({ priceId: selectedPrice.priceId });
}}
>
{isPending && (
<Loader2Icon className="size-4 animate-spin stroke-[2.5] mr-2" />
)}
{!isPending && 'Confirm'}
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,306 @@
import {
Loader2,
Zap,
Infinity,
MessageSquare,
Sparkles,
Heart,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { getUser } from '../../lib/jwt';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Modal } from '../Modal';
import {
billingDetailsOptions,
USER_SUBSCRIPTION_PLAN_PRICES,
type AllowedSubscriptionInterval,
} from '../../queries/billing';
import { cn } from '../../lib/classname';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { useToast } from '../../hooks/use-toast';
import { UpdatePlanConfirmation } from './UpdatePlanConfirmation';
// Define the perk type
type Perk = {
icon: LucideIcon;
title: string;
description: string;
};
// Define the perks array
const PREMIUM_PERKS: Perk[] = [
{
icon: Zap,
title: 'Unlimited AI Course Generations',
description: 'Generate as many custom courses as you need',
},
{
icon: Infinity,
title: 'No Daily Limits on course features',
description: 'Use all features without restrictions',
},
{
icon: MessageSquare,
title: 'Unlimited Course Follow-ups',
description: 'Ask as many questions as you need',
},
{
icon: Sparkles,
title: 'Early Access to Features',
description: 'Be the first to try new tools and features',
},
{
icon: Heart,
title: 'Support Development',
description: 'Help us continue building roadmap.sh',
},
];
type CreateSubscriptionCheckoutSessionBody = {
priceId: string;
success?: string;
cancel?: string;
};
type CreateSubscriptionCheckoutSessionResponse = {
checkoutUrl: string;
};
type UpgradeAccountModalProps = {
onClose: () => void;
success?: string;
cancel?: string;
};
export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
const { onClose, success, cancel } = props;
const [selectedPlan, setSelectedPlan] =
useState<AllowedSubscriptionInterval>('month');
const [isUpdatingPlan, setIsUpdatingPlan] = useState(false);
const user = getUser();
const {
data: userBillingDetails,
isLoading,
error: billingError,
} = useQuery(billingDetailsOptions(), queryClient);
const toast = useToast();
const {
mutate: createCheckoutSession,
isPending: isCreatingCheckoutSession,
} = useMutation(
{
mutationFn: (body: CreateSubscriptionCheckoutSessionBody) => {
return httpPost<CreateSubscriptionCheckoutSessionResponse>(
'/v1-create-subscription-checkout-session',
body,
);
},
onSuccess: (data) => {
window.location.href = data.checkoutUrl;
},
onError: (error) => {
console.error(error);
toast.error(error?.message || 'Failed to create checkout session');
},
},
queryClient,
);
const selectedPlanDetails = USER_SUBSCRIPTION_PLAN_PRICES.find(
(plan) => plan.interval === selectedPlan,
);
const currentPlanPriceId = userBillingDetails?.priceId;
const currentPlan = USER_SUBSCRIPTION_PLAN_PRICES.find(
(plan) => plan.priceId === currentPlanPriceId,
);
useEffect(() => {
if (!currentPlan) {
return;
}
setSelectedPlan(currentPlan.interval);
}, [currentPlan]);
if (!user) {
return null;
}
const loader = isLoading ? (
<div className="absolute inset-0 flex h-[540px] w-full items-center justify-center bg-white">
<Loader2 className="h-6 w-6 animate-spin stroke-[3px] text-green-600" />
</div>
) : null;
const error = billingError;
const errorContent = error ? (
<div className="flex h-full w-full flex-col">
<p className="text-center text-red-400">
{error?.message ||
'An error occurred while loading the billing details.'}
</p>
</div>
) : null;
const calculateYearlyPrice = (monthlyPrice: number) => {
return (monthlyPrice * 12).toFixed(2);
};
if (isUpdatingPlan && selectedPlanDetails) {
return (
<UpdatePlanConfirmation
planDetails={selectedPlanDetails}
onClose={() => setIsUpdatingPlan(false)}
onCancel={() => setIsUpdatingPlan(false)}
/>
);
}
return (
<Modal
onClose={onClose}
bodyClassName="p-4 sm:p-6 bg-white"
wrapperClassName="h-auto rounded-xl max-w-3xl w-full min-h-[540px] mx-2 sm:mx-4"
overlayClassName="items-start md:items-center"
>
<div onClick={(e) => e.stopPropagation()}>
{errorContent}
{loader}
{!isLoading && !error && (
<div className="flex flex-col">
<div className="mb-6 text-left sm:mb-8">
<h2 className="text-xl font-bold text-black sm:text-2xl">
Unlock Premium Features
</h2>
<p className="mt-1 text-sm text-gray-600 sm:mt-2 sm:text-base">
Supercharge your learning experience with premium benefits
</p>
</div>
<div className="mb-6 grid grid-cols-1 gap-4 sm:mb-8 sm:gap-6 md:grid-cols-2">
{USER_SUBSCRIPTION_PLAN_PRICES.map((plan) => {
const isCurrentPlanSelected =
currentPlan?.priceId === plan.priceId;
const isYearly = plan.interval === 'year';
return (
<div
key={plan.interval}
className={cn(
'flex flex-col space-y-3 rounded-lg bg-white p-4 sm:space-y-4 sm:p-6',
isYearly
? 'border-2 border-yellow-400'
: 'border border-gray-200',
)}
>
<div className="flex items-start justify-between">
<div>
<h4 className="text-sm font-semibold text-black sm:text-base">
{isYearly ? 'Yearly Payment' : 'Monthly Payment'}
</h4>
{isYearly && (
<span className="text-xs font-medium text-green-500 sm:text-sm">
(2 months free)
</span>
)}
</div>
{isYearly && (
<span className="rounded-full bg-yellow-400 px-1.5 py-0.5 text-xs font-semibold text-black sm:px-2 sm:py-1">
Most Popular
</span>
)}
</div>
<div className="flex items-baseline">
{isYearly && (
<p className="mr-2 text-xs text-gray-400 line-through sm:text-sm">
$
{calculateYearlyPrice(
USER_SUBSCRIPTION_PLAN_PRICES[0].amount,
)}
</p>
)}
<p className="text-2xl font-bold text-black sm:text-3xl">
${plan.amount}{' '}
<span className="text-xs font-normal text-gray-500 sm:text-sm">
/ {isYearly ? 'year' : 'month'}
</span>
</p>
</div>
<div className="flex-grow"></div>
<div>
<button
className={cn(
'flex min-h-9 w-full items-center justify-center rounded-md py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-11 sm:py-2.5 sm:text-base',
'bg-yellow-400 text-black hover:bg-yellow-500',
)}
disabled={
isCurrentPlanSelected || isCreatingCheckoutSession
}
onClick={() => {
setSelectedPlan(plan.interval);
if (!currentPlanPriceId) {
const currentUrlPath = window.location.pathname;
createCheckoutSession({
priceId: plan.priceId,
success: success || `${currentUrlPath}?s=1`,
cancel: cancel || `${currentUrlPath}?s=0`,
});
return;
}
setIsUpdatingPlan(true);
}}
data-1p-ignore=""
data-form-type="other"
data-lpignore="true"
>
{isCreatingCheckoutSession &&
selectedPlan === plan.interval ? (
<Loader2 className="h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4" />
) : isCurrentPlanSelected ? (
'Current Plan'
) : (
'Select Plan'
)}
</button>
</div>
</div>
);
})}
</div>
{/* Benefits Section */}
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2">
{PREMIUM_PERKS.map((perk, index) => {
const Icon = perk.icon;
return (
<div key={index} className="flex items-start space-x-2 sm:space-x-3">
<Icon className="mt-0.5 h-4 w-4 text-yellow-500 sm:h-5 sm:w-5" />
<div>
<h4 className="text-sm font-medium text-black sm:text-base">
{perk.title}
</h4>
<p className="text-xs text-gray-600 sm:text-sm">
{perk.description}
</p>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</Modal>
);
}

View File

@@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { Loader2, CheckCircle } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { Modal } from '../Modal';
import { deleteUrlParam } from '../../lib/browser';
type VerifyUpgradeProps = {
newPriceId?: string;
};
export function VerifyUpgrade(props: VerifyUpgradeProps) {
const { newPriceId } = props;
const { data: userBillingDetails } = useQuery(
{
...billingDetailsOptions(),
refetchInterval: 1000,
},
queryClient,
);
useEffect(() => {
if (!userBillingDetails) {
return;
}
if (
userBillingDetails.status === 'active' &&
(newPriceId ? userBillingDetails.priceId === newPriceId : true)
) {
deleteUrlParam('s');
window.location.reload();
}
}, [userBillingDetails]);
return (
<Modal
// it's an unique modal, so we don't need to close it
// user can close it by refreshing the page
onClose={() => {}}
bodyClassName="rounded-xl bg-white p-6"
>
<div className="mb-4 flex flex-col items-center text-center">
<CheckCircle className="mb-3 h-12 w-12 text-green-600" />
<h3 className="text-xl font-bold text-black">Subscription Activated</h3>
</div>
<p className="mt-2 text-balance text-center text-gray-600">
Your subscription has been activated successfully.
</p>
<p className="mt-4 text-balance text-center text-gray-600">
It might take a minute for the changes to reflect. We will{' '}
<b className="text-black">reload</b> the page for you.
</p>
<div className="my-6 flex animate-pulse items-center justify-center gap-2">
<Loader2 className="size-4 animate-spin stroke-[2.5px] text-green-600" />
<span className="text-gray-600">Please wait...</span>
</div>
<p className="text-center text-sm text-gray-500">
If it takes longer than expected, please email us at{' '}
<a
href="mailto:info@roadmap.sh"
className="text-blue-600 underline underline-offset-2 hover:text-blue-700"
>
info@roadmap.sh
</a>
.
</p>
</Modal>
);
}

View File

@@ -1,5 +1,5 @@
import { getPercentage } from '../../helper/number';
import { getRelativeTimeString } from '../../lib/date';
import { getPercentage } from '../../lib/number';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
type DashboardCustomProgressCardProps = {

View File

@@ -1,6 +1,5 @@
import { getPercentage } from '../../helper/number';
import { getPercentage } from '../../lib/number';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import { ArrowUpRight, ExternalLink } from 'lucide-react';
type DashboardProgressCardProps = {
progress: UserProgress;

View File

@@ -0,0 +1,172 @@
import { SearchIcon, WandIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { cn } from '../../lib/classname';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { UserCoursesList } from './UserCoursesList';
import { FineTuneCourse } from './FineTuneCourse';
import {
clearFineTuneData,
getCourseFineTuneData,
getLastSessionId,
storeFineTuneData,
} from '../../lib/ai';
export const difficultyLevels = [
'beginner',
'intermediate',
'advanced',
] as const;
export type DifficultyLevel = (typeof difficultyLevels)[number];
type AICourseProps = {};
export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner');
const [hasFineTuneData, setHasFineTuneData] = useState(false);
const [about, setAbout] = useState('');
const [goal, setGoal] = useState('');
const [customInstructions, setCustomInstructions] = useState('');
useEffect(() => {
const lastSessionId = getLastSessionId();
if (!lastSessionId) {
return;
}
const fineTuneData = getCourseFineTuneData(lastSessionId);
if (!fineTuneData) {
return;
}
setAbout(fineTuneData.about);
setGoal(fineTuneData.goal);
setCustomInstructions(fineTuneData.customInstructions);
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword.trim()) {
onSubmit();
}
};
function onSubmit() {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
let sessionId = '';
if (hasFineTuneData) {
clearFineTuneData();
sessionId = storeFineTuneData({
about,
goal,
customInstructions,
});
}
window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}`;
}
return (
<section className="flex flex-grow flex-col bg-gray-100">
<div className="container mx-auto flex max-w-3xl flex-col py-24 max-sm:py-4">
<h1 className="mb-2.5 text-center text-4xl font-bold max-sm:mb-2 max-sm:text-left max-sm:text-xl">
Learn anything with AI
</h1>
<p className="mb-6 text-center text-lg text-gray-600 max-sm:hidden max-sm:text-left max-sm:text-sm">
Enter a topic below to generate a personalized course for it
</p>
<div className="rounded-lg border border-gray-200 bg-white p-6 max-sm:p-4">
<form
className="flex flex-col gap-5"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<div className="flex flex-col">
<label
htmlFor="keyword"
className="mb-2.5 text-sm font-medium text-gray-700"
>
Course Topic
</label>
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<SearchIcon size={18} />
</div>
<input
id="keyword"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Algebra, JavaScript, Photography"
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-500 max-sm:placeholder:text-base"
maxLength={50}
/>
</div>
</div>
<div className="flex flex-col">
<label className="mb-2.5 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<div className="flex gap-2 max-sm:flex-col max-sm:gap-1">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => setDifficulty(level)}
className={cn(
'rounded-md border px-4 py-2 capitalize max-sm:text-sm',
difficulty === level
? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
)}
>
{level}
</button>
))}
</div>
</div>
<FineTuneCourse
hasFineTuneData={hasFineTuneData}
setHasFineTuneData={setHasFineTuneData}
about={about}
goal={goal}
customInstructions={customInstructions}
setAbout={setAbout}
setGoal={setGoal}
setCustomInstructions={setCustomInstructions}
/>
<button
type="submit"
disabled={!keyword.trim()}
className={cn(
'mt-2 flex items-center justify-center rounded-md px-4 py-2 font-medium text-white transition-colors max-sm:text-sm',
!keyword.trim()
? 'cursor-not-allowed bg-gray-400'
: 'bg-black hover:bg-gray-800',
)}
>
<WandIcon size={18} className="mr-2" />
Generate Course
</button>
</form>
</div>
<div className="mt-8 min-h-[200px]">
<UserCoursesList />
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,116 @@
import { MoreVertical, Play, Trash2 } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import { useToast } from '../../hooks/use-toast';
import { useMutation } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpDelete } from '../../lib/query-http';
type AICourseActionsType = {
courseSlug: string;
onDeleted?: () => void;
};
export function AICourseActions(props: AICourseActionsType) {
const { courseSlug, onDeleted } = props;
const toast = useToast();
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const { mutate: deleteCourse, isPending: isDeleting } = useMutation(
{
mutationFn: async () => {
return httpDelete(`/v1-delete-ai-course/${courseSlug}`);
},
onSuccess: () => {
toast.success('Course deleted');
queryClient.invalidateQueries({
predicate: (query) => query.queryKey?.[0] === 'user-ai-courses',
});
onDeleted?.();
},
onError: (error) => {
toast.error(error?.message || 'Failed to delete course');
},
},
queryClient,
);
useOutsideClick(dropdownRef, () => {
setIsOpen(false);
});
useKeydown('Escape', () => {
setIsOpen(false);
});
return (
<div className="relative h-full" ref={dropdownRef}>
<button
className="h-full text-gray-400 hover:text-gray-700"
onClick={(e) => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
>
<MoreVertical size={16} />
</button>
{isOpen && (
<div className="absolute right-0 top-8 z-10 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
<a
href={`/ai-tutor/${courseSlug}`}
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
>
<Play className="h-3.5 w-3.5" />
Start Course
</a>
{!isConfirming && (
<button
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
onClick={() => setIsConfirming(true)}
disabled={isDeleting}
>
{!isDeleting ? (
<>
<Trash2 className="h-3.5 w-3.5" />
Delete Course
</>
) : (
'Deleting...'
)}
</button>
)}
{isConfirming && (
<span className="flex w-full items-center justify-between gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70">
Are you sure?
<div className="flex items-center gap-2">
<button
onClick={() => {
setIsConfirming(false);
deleteCourse();
}}
disabled={isDeleting}
className="text-red-500 underline hover:text-red-800"
>
Yes
</button>
<button
onClick={() => setIsConfirming(false)}
className="text-red-500 underline hover:text-red-800"
>
No
</button>
</div>
</span>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,82 @@
import type { AICourseWithLessonCount } from '../../queries/ai-course';
import type { DifficultyLevel } from './AICourse';
import { BookOpen } from 'lucide-react';
import { AICourseActions } from './AICourseActions';
type AICourseCardProps = {
course: AICourseWithLessonCount;
};
export function AICourseCard(props: AICourseCardProps) {
const { course } = props;
// Format date if available
const formattedDate = course.createdAt
? new Date(course.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
: null;
// Map difficulty to color
const difficultyColor =
{
beginner: 'text-green-700',
intermediate: 'text-blue-700',
advanced: 'text-purple-700',
}[course.difficulty as DifficultyLevel] || 'text-gray-700';
// Calculate progress percentage
const totalTopics = course.lessonCount || 0;
const completedTopics = course.done?.length || 0;
const progressPercentage =
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
return (
<div className="relative">
<a
href={`/ai-tutor/${course.slug}`}
className="hover:border-gray-3 00 group relative flex w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:bg-gray-50"
>
<div className="flex items-center justify-between">
<span
className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
>
{course.difficulty}
</span>
</div>
<h3 className="my-2 text-base font-semibold text-gray-900">
{course.title}
</h3>
<div className="mt-auto flex items-center justify-between pt-2">
<div className="flex items-center text-xs text-gray-600">
<BookOpen className="mr-1 h-3.5 w-3.5" />
<span>{totalTopics} lessons</span>
</div>
{totalTopics > 0 && (
<div className="flex items-center">
<div className="mr-2 h-1.5 w-16 overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-blue-600"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<span className="text-xs font-medium text-gray-700">
{progressPercentage}%
</span>
</div>
)}
</div>
</a>
{course.slug && (
<div className="absolute right-2 top-2">
<AICourseActions courseSlug={course.slug} />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,533 @@
import {
BookOpenCheck,
ChevronLeft,
CircleAlert,
CircleOff,
Loader2,
Menu,
Play,
X,
} from 'lucide-react';
import { useState } from 'react';
import { type AiCourse } from '../../lib/ai';
import { cn } from '../../lib/classname';
import { slugify } from '../../lib/slugger';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
import { AICourseLesson } from './AICourseLesson';
import { AICourseLimit } from './AICourseLimit';
import { AICourseSidebarModuleList } from './AICourseSidebarModuleList';
import { AILimitsPopup } from './AILimitsPopup';
import { RegenerateOutline } from './RegenerateOutline';
type AICourseContentProps = {
courseSlug?: string;
course: AiCourse;
isLoading: boolean;
error?: string;
onRegenerateOutline: (prompt?: string) => void;
};
export function AICourseContent(props: AICourseContentProps) {
const { course, courseSlug, isLoading, error, onRegenerateOutline } = props;
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const [activeModuleIndex, setActiveModuleIndex] = useState(0);
const [activeLessonIndex, setActiveLessonIndex] = useState(0);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [viewMode, setViewMode] = useState<'module' | 'outline'>('outline');
const { isPaidUser } = useIsPaidUser();
const aiCourseProgress = course.done || [];
const [expandedModules, setExpandedModules] = useState<
Record<number, boolean>
>({});
const goToNextModule = () => {
if (activeModuleIndex >= course.modules.length) {
return;
}
const nextModuleIndex = activeModuleIndex + 1;
setActiveModuleIndex(nextModuleIndex);
setActiveLessonIndex(0);
setExpandedModules((prev) => {
const newState: Record<number, boolean> = {};
course.modules.forEach((_, idx) => {
newState[idx] = false;
});
newState[nextModuleIndex] = true;
return newState;
});
};
const goToNextLesson = () => {
const currentModule = course.modules[activeModuleIndex];
if (currentModule && activeLessonIndex < currentModule.lessons.length - 1) {
setActiveLessonIndex(activeLessonIndex + 1);
} else {
goToNextModule();
}
};
const goToPrevLesson = () => {
if (activeLessonIndex > 0) {
setActiveLessonIndex(activeLessonIndex - 1);
return;
}
const prevModule = course.modules[activeModuleIndex - 1];
if (!prevModule) {
return;
}
const prevModuleIndex = activeModuleIndex - 1;
setActiveModuleIndex(prevModuleIndex);
setActiveLessonIndex(prevModule.lessons.length - 1);
// Expand the previous module in the sidebar
setExpandedModules((prev) => {
const newState: Record<number, boolean> = {};
// Set all modules to collapsed
course.modules.forEach((_, idx) => {
newState[idx] = false;
});
// Expand only the previous module
newState[prevModuleIndex] = true;
return newState;
});
};
const currentModule = course.modules[activeModuleIndex];
const currentLesson = currentModule?.lessons[activeLessonIndex];
const totalModules = course.modules.length;
const totalLessons = currentModule?.lessons.length || 0;
const totalCourseLessons = course.modules.reduce(
(total, module) => total + module.lessons.length,
0,
);
const totalDoneLessons = (course?.done || []).length;
const finishedPercentage = Math.round(
(totalDoneLessons / totalCourseLessons) * 100,
);
const modals = (
<>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
{showAILimitsPopup && (
<AILimitsPopup
onClose={() => setShowAILimitsPopup(false)}
onUpgrade={() => {
setShowAILimitsPopup(false);
setShowUpgradeModal(true);
}}
/>
)}
</>
);
if (error && !isLoading) {
const isLimitReached = error.includes('limit');
const isNotFound = error.includes('not exist');
let icon = <ErrorIcon additionalClasses="mb-4 size-16" />;
let title = 'Error occurred';
let message = error;
if (isLimitReached) {
icon = <CircleAlert className="mb-4 size-16 text-yellow-500" />;
title = 'Limit Reached';
message =
'You have reached the daily AI usage limit. Please upgrade your account to continue.';
} else if (isNotFound) {
icon = <CircleOff className="mb-4 size-16 text-gray-300" />;
title = 'Course Not Found';
message =
'The course you are looking for does not exist. Why not create your own course?';
}
const showUpgradeButton = isLimitReached && !isPaidUser;
return (
<>
{modals}
<div className="flex h-screen flex-col items-center justify-center px-4 text-center">
{icon}
<h1 className="mb-2 text-2xl font-bold">{title}</h1>
<p className="max-w-sm text-balance text-gray-500">{message}</p>
{showUpgradeButton && (
<div className="my-5">
<button
onClick={() => setShowUpgradeModal(true)}
className="rounded-md bg-yellow-400 px-6 py-2 text-sm font-medium text-black hover:bg-yellow-500"
>
Upgrade to remove Limits
</button>
<p className="mt-5 text-sm text-black">
<a
href="/ai-tutor"
className="font-medium underline underline-offset-2"
>
Back to AI Tutor
</a>
</p>
</div>
)}
{(isNotFound || !showUpgradeButton) && (
<div className="my-5">
<a
href="/ai-tutor"
className="rounded-md bg-black px-6 py-2 text-sm font-medium text-white hover:bg-opacity-80"
>
Create a course with AI
</a>
</div>
)}
</div>
</>
);
}
const isViewingLesson = viewMode === 'module';
return (
<section className="flex h-screen flex-grow flex-col overflow-hidden bg-gray-50">
{modals}
<div className="border-b border-gray-200 bg-gray-100">
<div className="flex items-center justify-between px-4 py-2">
<a
href="/ai-tutor"
onClick={(e) => {
if (isViewingLesson) {
e.preventDefault();
setViewMode('outline');
}
}}
className="flex flex-row items-center gap-1.5 text-sm font-medium text-gray-700 hover:text-gray-900"
aria-label="Back to generator"
>
<ChevronLeft className="size-4" strokeWidth={2.5} />
Back {isViewingLesson ? 'to Outline' : 'to AI Tutor'}
</a>
<div className="flex items-center gap-2">
<div className="flex flex-row lg:hidden">
<AICourseLimit
onUpgrade={() => setShowUpgradeModal(true)}
onShowLimits={() => setShowAILimitsPopup(true)}
/>
</div>
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="flex items-center justify-center text-gray-400 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-900 lg:hidden"
>
{sidebarOpen ? (
<X size={17} strokeWidth={3} />
) : (
<Menu size={17} strokeWidth={3} />
)}
</button>
</div>
</div>
</div>
<header className="flex items-center justify-between border-b border-gray-200 bg-white px-6 max-lg:py-4 lg:h-[80px]">
<div className="flex items-center">
<div className="flex flex-col">
<h1 className="text-balance text-xl font-bold !leading-tight text-gray-900 max-lg:mb-0.5 max-lg:text-lg">
{course.title || 'Loading Course...'}
</h1>
<div className="mt-1 flex flex-row items-center gap-2 text-sm text-gray-600 max-lg:text-xs">
<span className="font-medium">{totalModules} modules</span>
<span className="text-gray-400"></span>
<span className="font-medium">{totalCourseLessons} lessons</span>
{viewMode === 'module' && (
<span className="flex flex-row items-center gap-1 lg:hidden">
<span className="text-gray-400"></span>
<button
className="underline underline-offset-2"
onClick={() => {
setExpandedModules({});
setViewMode('outline');
}}
>
View outline
</button>
</span>
)}
{finishedPercentage > 0 && (
<>
<span className="text-gray-400"></span>
<span className="font-medium text-green-600">
{finishedPercentage}% complete
</span>
</>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<div className="hidden gap-2 lg:flex">
<AICourseLimit
onUpgrade={() => setShowUpgradeModal(true)}
onShowLimits={() => setShowAILimitsPopup(true)}
/>
</div>
{viewMode === 'module' && (
<button
onClick={() => {
setExpandedModules({});
setViewMode('outline');
}}
className="flex flex-shrink-0 items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-900 max-lg:hidden"
>
<BookOpenCheck size={18} className="mr-2" />
View Course Outline
</button>
)}
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<aside
className={cn(
'fixed inset-y-0 left-0 z-20 w-80 transform overflow-y-auto border-r border-gray-200 bg-white transition-transform duration-200 ease-in-out lg:relative lg:mt-0 lg:translate-x-0',
sidebarOpen ? 'translate-x-0' : '-translate-x-full',
)}
>
<div
className={cn(
'relative flex min-h-[40px] items-center justify-between border-b border-gray-200 px-3',
isLoading && 'striped-loader bg-gray-50',
)}
>
{!isLoading && (
<div className="flex w-full items-center justify-between text-xs text-black">
<span>
<span className="relative z-10 rounded-full bg-yellow-400 px-1.5 py-0.5">
{finishedPercentage}%
</span>{' '}
<span className="relative z-10">Completed</span>
</span>
<span
style={{
width: `${finishedPercentage}%`,
}}
className={cn(
'absolute bottom-0 left-0 top-0',
'bg-gray-200/50',
)}
></span>
{viewMode !== 'outline' && (
<button
onClick={() => {
setExpandedModules({});
setViewMode('outline');
}}
className="flex items-center gap-1 rounded-md bg-gray-200 px-2.5 py-1.5 text-xs transition-colors hover:bg-gray-300"
>
<BookOpenCheck size={14} />
View Outline
</button>
)}
{viewMode === 'outline' && (
<button
onClick={() => {
setExpandedModules({
...expandedModules,
0: true,
});
setActiveModuleIndex(0);
setActiveLessonIndex(0);
setViewMode('module');
}}
className="flex items-center gap-1 rounded-md bg-gray-200 px-2.5 py-1.5 text-xs transition-colors hover:bg-gray-300"
>
<Play size={14} />
Start Course
</button>
)}
</div>
)}
<button
onClick={() => setSidebarOpen(false)}
className="rounded-md p-1 hover:bg-gray-100 lg:hidden"
>
<X size={18} />
</button>
</div>
<AICourseSidebarModuleList
course={course}
courseSlug={courseSlug}
activeModuleIndex={
viewMode === 'module' ? activeModuleIndex : undefined
}
setActiveModuleIndex={setActiveModuleIndex}
activeLessonIndex={
viewMode === 'module' ? activeLessonIndex : undefined
}
setActiveLessonIndex={setActiveLessonIndex}
setSidebarOpen={setSidebarOpen}
viewMode={viewMode}
setViewMode={setViewMode}
expandedModules={expandedModules}
setExpandedModules={setExpandedModules}
isLoading={isLoading}
/>
</aside>
<main
className={cn(
'flex-1 overflow-y-auto p-6 transition-all duration-200 ease-in-out max-lg:p-3',
sidebarOpen ? 'lg:ml-0' : '',
)}
>
{viewMode === 'module' && (
<AICourseLesson
courseSlug={courseSlug!}
progress={aiCourseProgress}
activeModuleIndex={activeModuleIndex}
totalModules={totalModules}
currentModuleTitle={currentModule?.title || ''}
activeLessonIndex={activeLessonIndex}
totalLessons={totalLessons}
currentLessonTitle={currentLesson || ''}
onGoToPrevLesson={goToPrevLesson}
onGoToNextLesson={goToNextLesson}
key={`${courseSlug}-${activeModuleIndex}-${activeLessonIndex}`}
onUpgrade={() => setShowUpgradeModal(true)}
/>
)}
{viewMode === 'outline' && (
<div className="mx-auto rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-3xl">
<div
className={cn(
'relative mb-1 flex items-start justify-between border-b border-gray-100 p-6 max-lg:hidden',
isLoading && 'striped-loader',
)}
>
<div>
<h2 className="mb-1 text-balance text-2xl font-bold max-lg:text-lg max-lg:leading-tight">
{course.title || 'Loading course ..'}
</h2>
<p className="text-sm capitalize text-gray-500">
{course.title ? course.difficulty : 'Please wait ..'}
</p>
</div>
{!isLoading && (
<RegenerateOutline
onRegenerateOutline={onRegenerateOutline}
/>
)}
</div>
{course.title ? (
<div className="flex flex-col p-6 max-lg:mt-0.5 max-lg:p-4">
{course.modules.map((courseModule, moduleIdx) => {
return (
<div
key={moduleIdx}
className="mb-5 pb-4 last:border-0 last:pb-0 max-lg:mb-2"
>
<h2 className="mb-4 text-xl font-bold text-gray-800 max-lg:mb-2 max-lg:text-lg max-lg:leading-tight">
{courseModule.title}
</h2>
<div className="divide-y divide-gray-100">
{courseModule.lessons.map((lesson, lessonIdx) => {
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
const isCompleted = aiCourseProgress.includes(key);
return (
<div
key={key}
className="flex cursor-pointer items-center gap-2 px-2 py-2.5 transition-colors hover:bg-gray-100 max-lg:px-0 max-lg:py-1.5"
onClick={() => {
setActiveModuleIndex(moduleIdx);
setActiveLessonIndex(lessonIdx);
setExpandedModules((prev) => {
const newState: Record<number, boolean> =
{};
course.modules.forEach((_, idx) => {
newState[idx] = false;
});
newState[moduleIdx] = true;
return newState;
});
setSidebarOpen(false);
setViewMode('module');
}}
>
{!isCompleted && (
<span
className={cn(
'flex size-6 flex-shrink-0 items-center justify-center rounded-full bg-gray-200 text-sm font-medium text-gray-800 max-lg:size-5 max-lg:text-xs',
)}
>
{lessonIdx + 1}
</span>
)}
{isCompleted && (
<CheckIcon additionalClasses="size-6 flex-shrink-0 text-green-500" />
)}
<p className="flex-1 truncate text-base text-gray-800 max-lg:text-sm">
{lesson.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
</p>
<span className="text-sm font-medium text-gray-700 max-lg:hidden">
{isCompleted ? 'View' : 'Start'}
</span>
</div>
);
})}
</div>
</div>
);
})}
</div>
) : (
<div className="flex h-64 items-center justify-center">
<Loader2 size={36} className="animate-spin text-gray-300" />
</div>
)}
</div>
)}
<div className="mb-10 mt-5 mx-auto text-center text-sm text-gray-400">
AI can make mistakes, check imporant info.
</div>
</main>
</div>
{sidebarOpen && (
<div
className="fixed inset-0 z-10 bg-gray-900 bg-opacity-50 lg:hidden"
onClick={() => setSidebarOpen(false)}
></div>
)}
</section>
);
}

View File

@@ -0,0 +1,131 @@
.prose ul li > code,
.prose ol li > code,
p code,
a > code,
strong > code,
em > code,
h1 > code,
h2 > code,
h3 > code {
background: #ebebeb !important;
color: currentColor !important;
font-size: 14px;
font-weight: normal !important;
}
.course-ai-content.course-content.prose ul li > code,
.course-ai-content.course-content.prose ol li > code,
.course-ai-content.course-content.prose p code,
.course-ai-content.course-content.prose a > code,
.course-ai-content.course-content.prose strong > code,
.course-ai-content.course-content.prose em > code,
.course-ai-content.course-content.prose h1 > code,
.course-ai-content.course-content.prose h2 > code,
.course-ai-content.course-content.prose h3 > code,
.course-notes-content.prose ul li > code,
.course-notes-content.prose ol li > code,
.course-notes-content.prose p code,
.course-notes-content.prose a > code,
.course-notes-content.prose strong > code,
.course-notes-content.prose em > code,
.course-notes-content.prose h1 > code,
.course-notes-content.prose h2 > code,
.course-notes-content.prose h3 > code {
font-size: 12px !important;
}
.course-ai-content pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.course-ai-content pre::-webkit-scrollbar {
display: none;
}
.course-ai-content pre,
.course-notes-content pre {
overflow: scroll;
font-size: 15px;
margin: 10px 0;
}
.prose ul li > code:before,
p > code:before,
.prose ul li > code:after,
.prose ol li > code:before,
p > code:before,
.prose ol li > code:after,
.course-content h1 > code:after,
.course-content h1 > code:before,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content h3 > code:after,
.course-content h3 > code:before,
.course-content h4 > code:after,
.course-content h4 > code:before,
p > code:after,
a > code:after,
a > code:before {
content: '' !important;
}
.course-content.prose ul li > code,
.course-content.prose ol li > code,
.course-content p code,
.course-content a > code,
.course-content strong > code,
.course-content em > code,
.course-content h1 > code,
.course-content h2 > code,
.course-content h3 > code,
.course-content table code {
background: #f4f4f5 !important;
border: 1px solid #282a36 !important;
color: #282a36 !important;
padding: 2px 4px;
border-radius: 5px;
font-size: 16px !important;
white-space: pre;
font-weight: normal;
}
.course-content blockquote {
font-style: normal;
}
.course-content.prose blockquote h1,
.course-content.prose blockquote h2,
.course-content.prose blockquote h3,
.course-content.prose blockquote h4 {
font-style: normal;
margin-bottom: 8px;
}
.course-content.prose ul li > code:before,
.course-content p > code:before,
.course-content.prose ul li > code:after,
.course-content p > code:after,
.course-content h2 > code:after,
.course-content h2 > code:before,
.course-content table code:before,
.course-content table code:after,
.course-content a > code:after,
.course-content a > code:before,
.course-content h2 code:after,
.course-content h2 code:before,
.course-content h2 code:after,
.course-content h2 code:before {
content: '' !important;
}
.course-content table {
border-collapse: collapse;
border: 1px solid black;
border-radius: 5px;
}
.course-content table td,
.course-content table th {
padding: 5px 10px;
}

View File

@@ -0,0 +1,74 @@
import { ArrowRightIcon, BotIcon } from 'lucide-react';
import { useState } from 'react';
import {
AICourseFollowUpPopover,
type AIChatHistoryType,
} from './AICourseFollowUpPopover';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type AICourseFollowUpProps = {
courseSlug: string;
moduleTitle: string;
lessonTitle: string;
};
export function AICourseFollowUp(props: AICourseFollowUpProps) {
const { courseSlug, moduleTitle, lessonTitle } = props;
const [isOpen, setIsOpen] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [courseAIChatHistory, setCourseAIChatHistory] = useState<
AIChatHistoryType[]
>([
{
role: 'assistant',
content:
'Hey, I am your AI instructor. Here are some examples of what you can ask me about 🤖',
isDefault: true,
},
]);
return (
<div className="relative">
<button
className="mt-4 flex w-full items-center gap-2 rounded-lg border border-yellow-300 bg-yellow-100 p-4 hover:bg-yellow-200 max-lg:mt-3 max-lg:text-sm"
onClick={() => setIsOpen(true)}
>
<BotIcon className="h-4 w-4" />
<span>Ask AI some follow up questions</span>
<ArrowRightIcon className="ml-auto h-4 w-4 max-sm:hidden" />
</button>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
{isOpen && (
<AICourseFollowUpPopover
courseSlug={courseSlug}
moduleTitle={moduleTitle}
lessonTitle={lessonTitle}
courseAIChatHistory={courseAIChatHistory}
setCourseAIChatHistory={setCourseAIChatHistory}
onUpgradeClick={() => {
setIsOpen(false);
setShowUpgradeModal(true);
}}
onOutsideClick={() => {
if (!isOpen) {
return;
}
setIsOpen(false);
}}
/>
)}
{isOpen && (
<div className="pointer-events-none fixed inset-0 z-50 bg-black/50" />
)}
</div>
);
}

View File

@@ -0,0 +1,390 @@
import { useQuery } from '@tanstack/react-query';
import {
BookOpen,
Bot,
Code,
Globe, Hammer,
HelpCircle,
LockIcon,
Send,
} from 'lucide-react';
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
import { flushSync } from 'react-dom';
import TextareaAutosize from 'react-textarea-autosize';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useToast } from '../../hooks/use-toast';
import { readStream } from '../../lib/ai';
import { cn } from '../../lib/classname';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import {
markdownToHtml,
markdownToHtmlWithHighlighting,
} from '../../lib/markdown';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = {
role: AllowedAIChatRole;
content: string;
isDefault?: boolean;
html?: string;
};
type AICourseFollowUpPopoverProps = {
courseSlug: string;
moduleTitle: string;
lessonTitle: string;
courseAIChatHistory: AIChatHistoryType[];
setCourseAIChatHistory: (value: AIChatHistoryType[]) => void;
onOutsideClick?: () => void;
onUpgradeClick: () => void;
};
export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
const {
courseSlug,
moduleTitle,
lessonTitle,
onOutsideClick,
onUpgradeClick,
courseAIChatHistory,
setCourseAIChatHistory,
} = props;
const toast = useToast();
const containerRef = useRef<HTMLDivElement | null>(null);
const scrollareaRef = useRef<HTMLDivElement | null>(null);
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [message, setMessage] = useState('');
const [streamedMessage, setStreamedMessage] = useState('');
useOutsideClick(containerRef, onOutsideClick);
const { data: tokenUsage, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const handleChatSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmedMessage = message.trim();
if (
!trimmedMessage ||
isStreamingMessage ||
!isLoggedIn() ||
isLimitExceeded ||
isLoading
) {
return;
}
const newMessages: AIChatHistoryType[] = [
...courseAIChatHistory,
{
role: 'user',
content: trimmedMessage,
},
];
flushSync(() => {
setCourseAIChatHistory(newMessages);
setMessage('');
});
scrollToBottom();
completeCourseAIChat(newMessages);
};
const scrollToBottom = () => {
scrollareaRef.current?.scrollTo({
top: scrollareaRef.current.scrollHeight,
behavior: 'smooth',
});
};
const completeCourseAIChat = async (messages: AIChatHistoryType[]) => {
setIsStreamingMessage(true);
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-follow-up-ai-course/${courseSlug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
moduleTitle,
lessonTitle,
messages: messages.slice(-10),
}),
},
);
if (!response.ok) {
const data = await response.json();
toast.error(data?.message || 'Something went wrong');
setCourseAIChatHistory([...messages].slice(0, messages.length - 1));
setIsStreamingMessage(false);
if (data.status === 401) {
removeAuthToken();
window.location.reload();
}
}
const reader = response.body?.getReader();
if (!reader) {
setIsStreamingMessage(false);
toast.error('Something went wrong');
return;
}
await readStream(reader, {
onStream: async (content) => {
flushSync(() => {
setStreamedMessage(content);
});
scrollToBottom();
},
onStreamEnd: async (content) => {
const newMessages: AIChatHistoryType[] = [
...messages,
{
role: 'assistant',
content,
html: await markdownToHtmlWithHighlighting(content),
},
];
flushSync(() => {
setStreamedMessage('');
setIsStreamingMessage(false);
setCourseAIChatHistory(newMessages);
});
queryClient.invalidateQueries(getAiCourseLimitOptions());
scrollToBottom();
},
});
setIsStreamingMessage(false);
};
useEffect(() => {
scrollToBottom();
}, []);
return (
<div
className="absolute bottom-0 left-0 z-[99] flex h-[500px] w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow"
ref={containerRef}
>
<div className="flex items-center justify-between gap-2 border-b border-gray-200 px-4 py-2 text-sm">
<h4 className="text-base font-medium">Course AI</h4>
</div>
<div
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
ref={scrollareaRef}
>
<div className="absolute inset-0 flex flex-col">
<div className="flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-2 px-3 py-2">
{courseAIChatHistory.map((chat, index) => {
return (
<>
<AIChatCard
key={`chat-${index}`}
role={chat.role}
content={chat.content}
html={chat.html}
/>
{chat.isDefault && (
<div className="mb-1 mt-0.5">
<div className="grid grid-cols-2 gap-2">
{capabilities.map((capability, index) => (
<CapabilityCard
key={`capability-${index}`}
{...capability}
/>
))}
</div>
</div>
)}
</>
);
})}
{isStreamingMessage && !streamedMessage && (
<AIChatCard role="assistant" content="Thinking..." />
)}
{streamedMessage && (
<AIChatCard role="assistant" content={streamedMessage} />
)}
</div>
</div>
</div>
</div>
<form
className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={handleChatSubmit}
>
{isLimitExceeded && (
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon className="size-4 cursor-not-allowed" strokeWidth={2.5} />
<p className="cursor-not-allowed">Limit reached for today</p>
<button
onClick={() => {
onUpgradeClick();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Upgrade for more
</button>
</div>
)}
<TextareaAutosize
className="h-full min-h-[41px] grow resize-none bg-transparent px-4 py-2 focus:outline-none"
placeholder="Ask AI anything about the lesson..."
value={message}
onChange={(e) => setMessage(e.target.value)}
autoFocus={true}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleChatSubmit(e as unknown as FormEvent<HTMLFormElement>);
}
}}
/>
<button
type="submit"
disabled={isStreamingMessage || isLimitExceeded}
className="flex aspect-square size-[41px] items-center justify-center text-zinc-500 hover:text-black"
>
<Send className="size-4 stroke-[2.5]" />
</button>
</form>
</div>
);
}
type AIChatCardProps = {
role: AllowedAIChatRole;
content: string;
html?: string;
};
function AIChatCard(props: AIChatCardProps) {
const { role, content, html: defaultHtml } = props;
const html = useMemo(() => {
if (defaultHtml) {
return defaultHtml;
}
return markdownToHtml(content, false);
}, [content, defaultHtml]);
return (
<div
className={cn(
'flex flex-col rounded-lg',
role === 'user' ? 'bg-gray-300/30' : 'bg-yellow-500/30',
)}
>
<div className="flex items-start gap-2.5 p-3">
<div
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-full',
role === 'user'
? 'bg-gray-200 text-black'
: 'bg-yellow-400 text-black',
)}
>
<Bot className="size-4 stroke-[2.5]" />
</div>
<div
className="course-content course-ai-content prose prose-sm mt-0.5 max-w-full overflow-hidden text-sm"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</div>
);
}
type CapabilityCardProps = {
icon: React.ReactNode;
title: string;
description: string;
className?: string;
};
function CapabilityCard({
icon,
title,
description,
className,
}: CapabilityCardProps) {
return (
<div
className={cn(
'flex flex-col gap-2 rounded-lg bg-yellow-500/10 p-3',
className,
)}
>
<div className="flex items-center gap-2">
{icon}
<span className="text-[13px] font-medium leading-none text-black">
{title}
</span>
</div>
<p className="text-[12px] leading-normal text-gray-600">{description}</p>
</div>
);
}
const capabilities = [
{
icon: (
<HelpCircle
className="size-4 shrink-0 text-yellow-600"
strokeWidth={2.5}
/>
),
title: 'Clarify Concepts',
description: "If you don't understand a concept, ask me to clarify it",
},
{
icon: (
<BookOpen className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />
),
title: 'More Details',
description: 'Get deeper insights about topics covered in the lesson',
},
{
icon: (
<Hammer className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />
),
title: 'Real-world Examples',
description: 'Ask for real-world examples to understand better',
},
{
icon: <Bot className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />,
title: 'Best Practices',
description: 'Learn about best practices and common pitfalls',
},
] as const;

View File

@@ -0,0 +1,387 @@
import { useMutation } from '@tanstack/react-query';
import {
CheckIcon,
ChevronLeft,
ChevronRight,
Loader2Icon,
LockIcon,
XIcon,
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import type { AICourseDocument } from '../../api/ai-roadmap';
import { readStream } from '../../lib/ai';
import { cn } from '../../lib/classname';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import {
markdownToHtml,
markdownToHtmlWithHighlighting,
} from '../../lib/markdown';
import { httpPatch } from '../../lib/query-http';
import { slugify } from '../../lib/slugger';
import {
getAiCourseLimitOptions,
getAiCourseOptions,
} from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { AICourseFollowUp } from './AICourseFollowUp';
import './AICourseFollowUp.css';
import { RegenerateLesson } from './RegenerateLesson';
type AICourseLessonProps = {
courseSlug: string;
progress: string[];
activeModuleIndex: number;
totalModules: number;
currentModuleTitle: string;
activeLessonIndex: number;
totalLessons: number;
currentLessonTitle: string;
onGoToPrevLesson: () => void;
onGoToNextLesson: () => void;
onUpgrade: () => void;
};
export function AICourseLesson(props: AICourseLessonProps) {
const {
courseSlug,
progress = [],
activeModuleIndex,
totalModules,
currentModuleTitle,
activeLessonIndex,
totalLessons,
currentLessonTitle,
onGoToPrevLesson,
onGoToNextLesson,
onUpgrade,
} = props;
const [isLoading, setIsLoading] = useState(true);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState('');
const [lessonHtml, setLessonHtml] = useState('');
const lessonId = `${slugify(String(activeModuleIndex))}-${slugify(String(activeLessonIndex))}`;
const isLessonDone = progress?.includes(lessonId);
const { isPaidUser } = useIsPaidUser();
const abortController = useMemo(
() => new AbortController(),
[activeModuleIndex, activeLessonIndex],
);
const generateAiCourseContent = async (
isForce?: boolean,
customPrompt?: string,
) => {
setIsLoading(true);
setError('');
setLessonHtml('');
if (!isLoggedIn()) {
setIsLoading(false);
setError('Please login to generate course content');
return;
}
if (!currentModuleTitle || !currentLessonTitle) {
setIsLoading(false);
setError('Invalid module title or lesson title');
return;
}
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-lesson/${courseSlug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
credentials: 'include',
body: JSON.stringify({
moduleIndex: activeModuleIndex,
lessonIndex: activeLessonIndex,
isForce,
customPrompt,
}),
},
);
if (!response.ok) {
const data = await response.json();
setError(data?.message || 'Something went wrong');
setIsLoading(false);
// Logout user if token is invalid
if (data.status === 401) {
removeAuthToken();
window.location.reload();
}
return;
}
if (!response.body) {
setIsLoading(false);
setError('No response body received');
return;
}
try {
const reader = response.body.getReader();
setIsLoading(false);
setIsGenerating(true);
await readStream(reader, {
onStream: async (result) => {
if (abortController.signal.aborted) {
return;
}
setLessonHtml(markdownToHtml(result, false));
},
onStreamEnd: async (result) => {
if (abortController.signal.aborted) {
return;
}
setLessonHtml(await markdownToHtmlWithHighlighting(result));
queryClient.invalidateQueries(getAiCourseLimitOptions());
setIsGenerating(false);
},
});
} catch (e) {
setError(e instanceof Error ? e.message : 'Something went wrong');
setIsLoading(false);
}
};
const { mutate: toggleDone, isPending: isTogglingDone } = useMutation(
{
mutationFn: () => {
return httpPatch<AICourseDocument>(
`/v1-toggle-done-ai-lesson/${courseSlug}`,
{
moduleIndex: activeModuleIndex,
lessonIndex: activeLessonIndex,
},
);
},
onSuccess: (data) => {
queryClient.setQueryData(
getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey,
data,
);
},
},
queryClient,
);
useEffect(() => {
generateAiCourseContent();
}, [currentModuleTitle, currentLessonTitle]);
useEffect(() => {
return () => {
abortController.abort();
};
}, [abortController]);
const cantGoForward =
(activeModuleIndex === totalModules - 1 &&
activeLessonIndex === totalLessons - 1) ||
isGenerating ||
isLoading;
const cantGoBack =
(activeModuleIndex === 0 && activeLessonIndex === 0) || isGenerating || isLoading;
return (
<div className="mx-auto max-w-4xl">
<div className="relative rounded-lg border border-gray-200 bg-white p-6 shadow-sm max-lg:px-4 max-lg:pb-4 max-lg:pt-3">
{(isGenerating || isLoading) && (
<div className="absolute right-3 top-3 flex items-center justify-center">
<Loader2Icon
size={18}
strokeWidth={3}
className="animate-spin text-gray-400/70"
/>
</div>
)}
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-500">
Lesson {activeLessonIndex + 1} of {totalLessons}
</div>
{!isGenerating && !isLoading && (
<div className="absolute right-3 top-3 flex items-center justify-between gap-2">
<RegenerateLesson
onRegenerateLesson={(prompt) => {
generateAiCourseContent(true, prompt);
}}
/>
<button
disabled={isLoading || isTogglingDone}
className={cn(
'flex items-center gap-1.5 rounded-full bg-black py-1 pl-2 pr-3 text-sm text-white hover:bg-gray-800 disabled:opacity-50 max-lg:text-xs',
isLessonDone
? 'bg-red-500 hover:bg-red-600'
: 'bg-green-500 hover:bg-green-600',
)}
onClick={() => toggleDone()}
>
{isTogglingDone ? (
<>
<Loader2Icon
size={16}
strokeWidth={3}
className="animate-spin text-white"
/>
Please wait ...
</>
) : (
<>
{isLessonDone ? (
<>
<XIcon size={16} />
Mark as Undone
</>
) : (
<>
<CheckIcon size={16} />
Mark as Done
</>
)}
</>
)}
</button>
</div>
)}
</div>
<h1 className="mb-6 text-balance text-3xl font-semibold max-lg:mb-3 max-lg:text-xl">
{currentLessonTitle?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
</h1>
{!error && isLoggedIn() && (
<div
className="course-content prose prose-lg mt-8 max-w-full text-black prose-headings:mb-3 prose-headings:mt-8 prose-blockquote:font-normal prose-pre:rounded-2xl prose-pre:text-lg prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800 max-lg:mt-4 max-lg:text-base max-lg:prose-h2:mt-3 max-lg:prose-h2:text-lg max-lg:prose-h3:text-base max-lg:prose-pre:px-3 max-lg:prose-pre:text-sm"
dangerouslySetInnerHTML={{ __html: lessonHtml }}
/>
)}
{error && isLoggedIn() && (
<div className="mt-8 flex min-h-[300px] items-center justify-center rounded-xl bg-red-50/80">
{error.includes('reached the limit') ? (
<div className="flex max-w-sm flex-col items-center text-center">
<h2 className="text-xl font-semibold text-red-600">
Limit reached
</h2>
<p className="my-3 text-red-600">
You have reached the AI usage limit for today.
{!isPaidUser && <>Please upgrade your account to continue.</>}
{isPaidUser && <>Please wait until tomorrow to continue.</>}
</p>
{!isPaidUser && (
<button
onClick={() => {
onUpgrade();
}}
className="rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
>
Upgrade Account
</button>
)}
</div>
) : (
<p className="text-red-600">{error}</p>
)}
</div>
)}
{!isLoggedIn() && (
<div className="mt-8 flex min-h-[152px] flex-col items-center justify-center gap-3 rounded-lg border border-gray-200 p-8">
<LockIcon className="size-7 stroke-[2] text-gray-400/90" />
<p className="text-sm text-gray-500">
Please login to generate course content
</p>
</div>
)}
<div className="mt-8 flex items-center justify-between">
<button
onClick={onGoToPrevLesson}
disabled={cantGoBack}
className={cn(
'flex items-center rounded-full px-4 py-2 disabled:opacity-50 max-lg:px-3 max-lg:py-1.5 max-lg:text-sm',
cantGoBack
? 'cursor-not-allowed text-gray-400'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
)}
>
<ChevronLeft size={16} className="mr-2" />
Previous <span className="hidden lg:inline">&nbsp;Lesson</span>
</button>
<div>
<button
onClick={() => {
if (!isLessonDone) {
toggleDone(undefined, {
onSuccess: () => {
onGoToNextLesson();
},
});
} else {
onGoToNextLesson();
}
}}
disabled={cantGoForward || isTogglingDone}
className={cn(
'flex items-center rounded-full px-4 py-2 disabled:opacity-50 max-lg:px-3 max-lg:py-1.5 max-lg:text-sm',
cantGoForward
? 'cursor-not-allowed text-gray-400'
: 'bg-gray-800 text-white hover:bg-gray-700',
)}
>
{isTogglingDone ? (
<>
<Loader2Icon
size={16}
strokeWidth={3}
className="animate-spin text-white"
/>
Please wait ...
</>
) : (
<>
Next <span className="hidden lg:inline">&nbsp;Lesson</span>
<ChevronRight size={16} className="ml-2" />
</>
)}
</button>
</div>
</div>
</div>
{!isGenerating && !isLoading && (
<AICourseFollowUp
courseSlug={courseSlug}
moduleTitle={currentModuleTitle}
lessonTitle={currentLessonTitle}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,81 @@
import { useQuery } from '@tanstack/react-query';
import { Gift, Info } from 'lucide-react';
import { getPercentage } from '../../lib/number';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
type AICourseLimitProps = {
onUpgrade: () => void;
onShowLimits: () => void;
};
export function AICourseLimit(props: AICourseLimitProps) {
const { onUpgrade, onShowLimits } = props;
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
if (isLoading || !limits || isBillingDetailsLoading || !userBillingDetails) {
return (
<div className="hidden h-[38px] w-[208.09px] animate-pulse rounded-lg border border-gray-200 bg-gray-200 lg:block"></div>
);
}
const { used, limit } = limits;
const totalPercentage = getPercentage(used, limit);
// has consumed 85% of the limit
const isNearLimit = used >= limit * 0.85;
const isPaidUser = userBillingDetails.status === 'active';
return (
<>
{!isPaidUser ||
(isNearLimit && (
<button
className="mr-1 flex items-center gap-1 text-sm font-medium underline underline-offset-2 lg:hidden"
onClick={() => onShowLimits()}
>
<Info className="size-4" />
{totalPercentage}% limit used
</button>
))}
{(!isPaidUser || isNearLimit) && (
<button
onClick={() => {
onShowLimits();
}}
className="relative hidden h-full min-h-[38px] cursor-pointer items-center overflow-hidden rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 lg:flex"
>
<span className="relative z-10">
{totalPercentage}% of the daily limit used
</span>
<div
className="absolute inset-0 h-full bg-gray-200/80"
style={{
width: `${totalPercentage}%`,
}}
></div>
</button>
)}
{!isPaidUser && (
<button
className="hidden items-center justify-center gap-1 rounded-md bg-yellow-400 px-4 py-1 text-sm font-medium underline-offset-2 hover:bg-yellow-500 lg:flex"
onClick={() => onUpgrade()}
>
<Gift className="size-4" />
Upgrade
</button>
)}
</>
);
}

View File

@@ -0,0 +1,46 @@
import { SearchIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useDebounceValue } from '../../hooks/use-debounce';
type AICourseSearchProps = {
value: string;
onChange: (value: string) => void;
};
export function AICourseSearch(props: AICourseSearchProps) {
const { value: defaultValue, onChange } = props;
const [searchTerm, setSearchTerm] = useState(defaultValue);
const debouncedSearchTerm = useDebounceValue(searchTerm, 500);
useEffect(() => {
setSearchTerm(defaultValue);
}, [defaultValue]);
useEffect(() => {
if (debouncedSearchTerm && debouncedSearchTerm.length < 3) {
return;
}
if (debouncedSearchTerm === defaultValue) {
return;
}
onChange(debouncedSearchTerm);
}, [debouncedSearchTerm]);
return (
<div className="relative w-64 max-sm:hidden">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<SearchIcon className="h-4 w-4 text-gray-400" />
</div>
<input
type="text"
className="block w-full rounded-md border border-gray-200 bg-white py-1.5 pl-10 pr-3 leading-5 placeholder-gray-500 focus:border-gray-300 focus:outline-none focus:ring-blue-500 disabled:opacity-70 sm:text-sm"
placeholder="Search your courses..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
);
}

View File

@@ -0,0 +1,204 @@
import { type Dispatch, type SetStateAction } from 'react';
import type { AiCourse } from '../../lib/ai';
import { Check, ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
import { slugify } from '../../lib/slugger';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { CircularProgress } from './CircularProgress';
type AICourseModuleListProps = {
course: AiCourse;
courseSlug?: string;
activeModuleIndex: number | undefined;
setActiveModuleIndex: (index: number) => void;
activeLessonIndex: number | undefined;
setActiveLessonIndex: (index: number) => void;
setSidebarOpen: (open: boolean) => void;
viewMode: 'module' | 'outline';
setViewMode: (mode: 'module' | 'outline') => void;
expandedModules: Record<number, boolean>;
setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>;
isLoading: boolean;
};
export function AICourseSidebarModuleList(props: AICourseModuleListProps) {
const {
course,
courseSlug,
activeModuleIndex,
setActiveModuleIndex,
activeLessonIndex,
setActiveLessonIndex,
setSidebarOpen,
setViewMode,
expandedModules,
setExpandedModules,
isLoading,
} = props;
const aiCourseProgress = course.done || [];
const toggleModule = (index: number) => {
setExpandedModules((prev) => {
// If this module is already expanded, collapse it
if (prev[index]) {
return {
...prev,
[index]: false,
};
}
// Otherwise, collapse all modules and expand only this one
const newState: Record<number, boolean> = {};
// Set all modules to collapsed
course.modules.forEach((_, idx) => {
newState[idx] = false;
});
// Expand only the clicked module
newState[index] = true;
return newState;
});
};
const done = aiCourseProgress || [];
return (
<nav className="bg-gray-100">
{course.modules.map((courseModule, moduleIdx) => {
const totalLessons = courseModule.lessons.length;
const completedLessons = courseModule.lessons.filter(
(lesson, lessonIdx) => {
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
return done.includes(key);
},
).length;
const percentage = Math.round((completedLessons / totalLessons) * 100);
const isActive = expandedModules[moduleIdx];
const isModuleCompleted = completedLessons === totalLessons;
return (
<div key={moduleIdx} className="rounded-md">
<button
onClick={() => toggleModule(moduleIdx)}
className={cn(
'relative z-10 flex w-full cursor-pointer flex-row items-center gap-2 border-b border-b-gray-200 bg-white px-2 py-3 text-base text-gray-600 hover:bg-gray-100',
activeModuleIndex === moduleIdx
? 'text-gray-900'
: 'text-gray-700',
moduleIdx === 0 && 'pt-4',
)}
>
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex-shrink-0">
<CircularProgress
percentage={percentage}
isVisible={!isModuleCompleted}
isActive={isActive}
isLoading={isLoading}
>
<span
className={cn(
'flex size-[21px] flex-shrink-0 items-center justify-center rounded-full bg-gray-400/70 text-xs font-semibold text-white',
{
'bg-black': isActive,
'bg-green-600': isModuleCompleted,
},
)}
>
{!isModuleCompleted && moduleIdx + 1}
{isModuleCompleted && (
<Check className="size-3 stroke-[3] text-white" />
)}
</span>
</CircularProgress>
</div>
<span className="flex flex-1 items-center break-words text-left text-sm leading-relaxed">
{courseModule.title?.replace(/^Module\s*?\d+[\.:]\s*/, '')}
</span>
</div>
<div className="ml-auto self-center">
{expandedModules[moduleIdx] ? (
<ChevronDownIcon size={16} className="flex-shrink-0" />
) : (
<ChevronRightIcon size={16} className="flex-shrink-0" />
)}
</div>
</button>
{/* Lessons */}
{expandedModules[moduleIdx] && (
<div className="flex flex-col border-b border-b-gray-200 bg-gray-100">
{courseModule.lessons.map((lesson, lessonIdx) => {
const key = `${slugify(String(moduleIdx))}-${slugify(String(lessonIdx))}`;
const isCompleted = done.includes(key);
return (
<button
key={key}
onClick={() => {
setActiveModuleIndex(moduleIdx);
setActiveLessonIndex(lessonIdx);
setExpandedModules((prev) => {
const newState: Record<number, boolean> = {};
course.modules.forEach((_, idx) => {
newState[idx] = false;
});
newState[moduleIdx] = true;
return newState;
});
setSidebarOpen(false);
setViewMode('module');
}}
className={cn(
'flex w-full cursor-pointer items-center gap-2.5 py-3 pl-3.5 pr-2 text-left text-sm leading-normal',
activeModuleIndex === moduleIdx &&
activeLessonIndex === lessonIdx
? 'bg-gray-200 text-black'
: 'text-gray-600 hover:bg-gray-200/70',
)}
>
{isCompleted ? (
<CheckIcon
additionalClasses={cn(
'size-[18px] relative bg-white rounded-full top-[2px] flex-shrink-0 text-green-600',
{
'text-black':
activeModuleIndex === moduleIdx &&
activeLessonIndex === lessonIdx,
},
)}
/>
) : (
<span
className={cn(
'flex size-[18px] flex-shrink-0 items-center justify-center rounded-full bg-gray-400/70 text-xs font-semibold text-white',
{
'bg-black':
activeModuleIndex === moduleIdx &&
activeLessonIndex === lessonIdx,
},
)}
>
{lessonIdx + 1}
</span>
)}
<span className="break-words">
{lesson?.replace(/^Lesson\s*?\d+[\.:]\s*/, '')}
</span>
</button>
);
})}
</div>
)}
</div>
);
})}
</nav>
);
}

View File

@@ -0,0 +1,103 @@
import { Gift } from 'lucide-react';
import { Modal } from '../Modal';
import { formatCommaNumber } from '../../lib/number';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
type AILimitsPopupProps = {
onClose: () => void;
onUpgrade: () => void;
};
export function AILimitsPopup(props: AILimitsPopupProps) {
const { onClose, onUpgrade } = props;
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status === 'active';
return (
<Modal
onClose={onClose}
wrapperClassName="rounded-xl max-w-xl w-full h-auto"
bodyClassName="p-6"
overlayClassName="items-start md:items-center"
>
<h2 className="mb-8 text-center text-xl font-semibold">
Daily AI Limits
</h2>
{/* Usage Progress Bar */}
<div className="mb-6">
<div className="mb-2 flex justify-between">
<span className="text-sm font-medium">
Usage: {formatCommaNumber(used)}&nbsp;/&nbsp;
{formatCommaNumber(limit)} tokens
</span>
<span className="text-sm font-medium">
{Math.round((used / limit) * 100)}%
</span>
</div>
<div className="h-2.5 w-full rounded-full bg-gray-200">
<div
className="h-2.5 rounded-full bg-yellow-500"
style={{ width: `${Math.min(100, (used / limit) * 100)}%` }}
></div>
</div>
</div>
{/* Usage Stats */}
<div className="rounded-lg bg-gray-50 p-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">Used Today</p>
<p className="text-2xl font-bold">{formatCommaNumber(used)}</p>
</div>
<div>
<p className="text-sm text-gray-500">Daily Limit</p>
<p className="text-2xl font-bold">{formatCommaNumber(limit)}</p>
</div>
</div>
</div>
{/* Explanation */}
<div className="mt-2">
<div className="space-y-3 text-gray-600">
<p className="text-sm">
Limit resets every 24 hours.{' '}
{!isPaidUser && 'Consider upgrading for more tokens.'}
</p>
</div>
</div>
{/* Action Button */}
<div className="mt-auto flex flex-col gap-2 pt-4">
{!isPaidUser && (
<button
onClick={onUpgrade}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-yellow-400 px-4 py-2.5 text-sm font-medium text-black transition-colors hover:bg-yellow-500"
>
<Gift className="size-4" />
Upgrade to Unlimited
</button>
)}
<button
onClick={onClose}
className="w-full rounded-lg bg-gray-200 px-4 py-2.5 text-sm text-gray-600 transition-colors hover:bg-gray-300"
>
Close
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,57 @@
import { cn } from '../../lib/classname';
export function ChapterNumberSkeleton() {
return (
<div className="h-[28px] w-[28px] animate-pulse rounded-full bg-gray-200" />
);
}
type CircularProgressProps = {
percentage: number;
children: React.ReactNode;
isVisible?: boolean;
isActive?: boolean;
isLoading?: boolean;
};
export function CircularProgress(props: CircularProgressProps) {
const {
percentage,
children,
isVisible = true,
isActive = false,
isLoading = false,
} = props;
const circumference = 2 * Math.PI * 13;
const strokeDasharray = `${circumference}`;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div className="relative flex h-[28px] w-[28px] flex-shrink-0 items-center justify-center">
{isVisible && !isLoading && (
<svg className="absolute h-full w-full -rotate-90">
<circle
cx="14"
cy="14"
r="13"
stroke="currentColor"
strokeWidth="1.75"
fill="none"
className={cn('text-gray-400/70', {
'text-black': isActive,
})}
style={{
strokeDasharray,
strokeDashoffset,
transition: 'stroke-dashoffset 0.3s ease',
}}
/>
</svg>
)}
{!isLoading && children}
{isLoading && <ChapterNumberSkeleton />}
</div>
);
}

View File

@@ -0,0 +1,103 @@
import { useState } from 'react';
import { cn } from '../../lib/classname';
type QuestionProps = {
label: string;
placeholder: string;
autoFocus?: boolean;
value: string;
onChange: (value: string) => void;
};
function Question(props: QuestionProps) {
const { label, placeholder, value, onChange, autoFocus = false } = props;
return (
<div className="flex flex-col">
<label className="border-y bg-gray-100 px-4 py-2.5 text-sm font-medium text-gray-700">
{label}
</label>
<textarea
placeholder={placeholder}
className="min-h-[80px] w-full resize-none px-4 py-3 text-gray-700 placeholder:text-gray-400 focus:outline-none"
value={value}
onChange={(e) => onChange(e.target.value)}
autoFocus={autoFocus}
/>
</div>
);
}
type FineTuneCourseProps = {
hasFineTuneData: boolean;
about: string;
goal: string;
customInstructions: string;
setHasFineTuneData: (hasMetadata: boolean) => void;
setAbout: (about: string) => void;
setGoal: (goal: string) => void;
setCustomInstructions: (customInstructions: string) => void;
};
export function FineTuneCourse(props: FineTuneCourseProps) {
const {
about,
goal,
customInstructions,
hasFineTuneData,
setAbout,
setGoal,
setCustomInstructions,
setHasFineTuneData,
} = props;
return (
<div className="flex flex-col overflow-hidden rounded-lg border border-gray-200 transition-all">
<label
className={cn(
'group flex cursor-pointer select-none flex-row items-center gap-2.5 px-4 py-3 text-left text-gray-500 transition-colors hover:bg-gray-100 focus:outline-none',
hasFineTuneData && 'bg-gray-100',
)}
>
<input
id="fine-tune-checkbox"
type="checkbox"
className="h-4 w-4 group-hover:fill-current"
checked={hasFineTuneData}
onChange={() => {
setHasFineTuneData(!hasFineTuneData);
}}
/>
Tell us more to tailor the course (optional){' '}
<span className="ml-auto rounded-md bg-gray-400 px-2 py-0.5 text-xs text-white">
recommended
</span>
</label>
{hasFineTuneData && (
<div className="mt-0 flex flex-col">
<Question
label="Tell us about your self"
placeholder="e.g. I am a frontend developer and have good knowledge of HTML, CSS, and JavaScript."
value={about}
onChange={setAbout}
autoFocus={true}
/>
<Question
label="What is your goal with this course?"
placeholder="e.g. I want to be able to build Node.js APIs with Express.js and MongoDB."
value={goal}
onChange={setGoal}
/>
<Question
label="Custom Instructions (Optional)"
placeholder="Give additional instructions to the AI as if you were giving them to a friend."
value={customInstructions}
onChange={setCustomInstructions}
/>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,166 @@
import { useEffect, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { getCourseFineTuneData, type AiCourse } from '../../lib/ai';
import { AICourseContent } from './AICourseContent';
import { generateCourse } from '../../helper/generate-ai-course';
import { useQuery } from '@tanstack/react-query';
import { getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
type GenerateAICourseProps = {};
export function GenerateAICourse(props: GenerateAICourseProps) {
const [term, setTerm] = useState('');
const [difficulty, setDifficulty] = useState('');
const [sessionId, setSessionId] = useState('');
const [goal, setGoal] = useState('');
const [about, setAbout] = useState('');
const [customInstructions, setCustomInstructions] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [courseId, setCourseId] = useState('');
const [courseSlug, setCourseSlug] = useState('');
const [course, setCourse] = useState<AiCourse>({
title: '',
modules: [],
difficulty: '',
done: [],
});
// Once the course is generated, we fetch the course from the database
// so that we get the up-to-date course data and also so that we
// can reload the changes (e.g. progress) etc using queryClient.setQueryData
const { data: aiCourse } = useQuery(
getAiCourseOptions({ aiCourseSlug: courseSlug }),
queryClient,
);
useEffect(() => {
if (aiCourse) {
setCourse(aiCourse);
}
}, [aiCourse]);
useEffect(() => {
if (term || difficulty) {
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsDifficulty = params?.difficulty;
if (!paramsTerm || !paramsDifficulty) {
return;
}
setTerm(paramsTerm);
setDifficulty(paramsDifficulty);
const sessionId = params?.id;
setSessionId(sessionId);
let paramsGoal = '';
let paramsAbout = '';
let paramsCustomInstructions = '';
if (sessionId) {
const fineTuneData = getCourseFineTuneData(sessionId);
if (fineTuneData) {
paramsGoal = fineTuneData.goal;
paramsAbout = fineTuneData.about;
paramsCustomInstructions = fineTuneData.customInstructions;
setGoal(paramsGoal);
setAbout(paramsAbout);
setCustomInstructions(paramsCustomInstructions);
}
}
handleGenerateCourse({
term: paramsTerm,
difficulty: paramsDifficulty,
instructions: paramsCustomInstructions,
goal: paramsGoal,
about: paramsAbout,
});
}, [term, difficulty]);
const handleGenerateCourse = async (options: {
term: string;
difficulty: string;
instructions?: string;
goal?: string;
about?: string;
isForce?: boolean;
prompt?: string;
}) => {
const { term, difficulty, isForce, prompt, instructions, goal, about } =
options;
if (!isLoggedIn()) {
window.location.href = '/ai-tutor';
return;
}
await generateCourse({
term,
difficulty,
slug: courseSlug,
onCourseIdChange: setCourseId,
onCourseSlugChange: setCourseSlug,
onCourseChange: setCourse,
onLoadingChange: setIsLoading,
onError: setError,
instructions,
goal,
about,
isForce,
prompt,
});
};
useEffect(() => {
const handlePopState = (e: PopStateEvent) => {
const { courseId, courseSlug, term, difficulty } = e.state || {};
if (!courseId || !courseSlug) {
window.location.reload();
return;
}
setCourseId(courseId);
setCourseSlug(courseSlug);
setTerm(term);
setDifficulty(difficulty);
setIsLoading(true);
handleGenerateCourse({ term, difficulty }).finally(() => {
setIsLoading(false);
});
};
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);
return (
<AICourseContent
courseSlug={courseSlug}
course={course}
isLoading={isLoading}
error={error}
onRegenerateOutline={(prompt) => {
handleGenerateCourse({
term,
difficulty,
isForce: true,
prompt,
});
}}
/>
);
}

View File

@@ -0,0 +1,107 @@
import { useQuery } from '@tanstack/react-query';
import { getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useEffect, useState } from 'react';
import { AICourseContent } from './AICourseContent';
import { isLoggedIn } from '../../lib/jwt';
import { generateCourse } from '../../helper/generate-ai-course';
type GetAICourseProps = {
courseSlug: string;
};
export function GetAICourse(props: GetAICourseProps) {
const { courseSlug } = props;
const [isLoading, setIsLoading] = useState(true);
const [isRegenerating, setIsRegenerating] = useState(false);
const [error, setError] = useState('');
const { data: aiCourse, error: queryError } = useQuery(
{
...getAiCourseOptions({ aiCourseSlug: courseSlug }),
enabled: !!courseSlug && !!isLoggedIn(),
},
queryClient,
);
useEffect(() => {
if (!isLoggedIn()) {
window.location.href = '/ai-tutor';
}
}, [isLoggedIn]);
useEffect(() => {
if (!aiCourse) {
return;
}
setIsLoading(false);
}, [aiCourse]);
useEffect(() => {
if (!queryError) {
return;
}
setIsLoading(false);
setError(queryError.message);
}, [queryError]);
const handleRegenerateCourse = async (prompt?: string) => {
if (!aiCourse) {
return;
}
queryClient.setQueryData(
getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey,
{
...aiCourse,
title: '',
difficulty: '',
modules: [],
},
);
await generateCourse({
term: aiCourse.keyword,
difficulty: aiCourse.difficulty,
slug: courseSlug,
prompt,
onCourseChange: (course, rawData) => {
queryClient.setQueryData(
getAiCourseOptions({ aiCourseSlug: courseSlug }).queryKey,
{
...aiCourse,
title: course.title,
difficulty: course.difficulty,
modules: course.modules,
},
);
},
onLoadingChange: (isNewLoading) => {
setIsRegenerating(isNewLoading);
if (!isNewLoading) {
// TODO: Update progress
}
},
onError: setError,
isForce: true,
});
};
return (
<AICourseContent
course={{
title: aiCourse?.title || '',
modules: aiCourse?.modules || [],
difficulty: aiCourse?.difficulty || 'Easy',
done: aiCourse?.done || [],
}}
isLoading={isLoading || isRegenerating}
courseSlug={courseSlug}
error={error}
onRegenerateOutline={handleRegenerateCourse}
/>
);
}

View File

@@ -0,0 +1,69 @@
import { useState } from 'react';
import { Modal } from '../Modal';
export type ModifyCoursePromptProps = {
onClose: () => void;
onSubmit: (prompt: string) => void;
};
export function ModifyCoursePrompt(props: ModifyCoursePromptProps) {
const { onClose, onSubmit } = props;
const [prompt, setPrompt] = useState('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(prompt);
};
return (
<Modal
onClose={onClose}
wrapperClassName="rounded-xl max-w-xl w-full h-auto"
bodyClassName="p-6"
overlayClassName="items-start md:items-center"
>
<div className="flex flex-col gap-4">
<div>
<h2 className="mb-2 text-left text-xl font-semibold">
Give AI more context
</h2>
<p className="text-sm text-gray-500">
Pass additional information to the AI to generate a course outline.
</p>
</div>
<form className="flex flex-col gap-2" onSubmit={handleSubmit}>
<textarea
id="prompt"
autoFocus
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="w-full rounded-md border border-gray-200 p-2 placeholder:text-sm focus:outline-black"
placeholder="e.g. make sure to add a section on React hooks"
/>
<p className="text-sm text-gray-500">
Complete the sentence: "I want AI to..."
</p>
<div className="flex justify-end gap-2">
<button
className="rounded-md bg-gray-200 px-4 py-2.5 text-sm text-black hover:opacity-80"
onClick={onClose}
>
Cancel
</button>
<button
type="submit"
disabled={!prompt.trim()}
className="rounded-md bg-black px-4 py-2.5 text-sm text-white hover:opacity-80 disabled:opacity-50"
>
Modify Prompt
</button>
</div>
</form>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,88 @@
import { PenSquare, RefreshCcw } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { cn } from '../../lib/classname';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { ModifyCoursePrompt } from './ModifyCoursePrompt';
type RegenerateLessonProps = {
onRegenerateLesson: (prompt?: string) => void;
};
export function RegenerateLesson(props: RegenerateLessonProps) {
const { onRegenerateLesson } = props;
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showPromptModal, setShowPromptModal] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => setIsDropdownVisible(false));
return (
<>
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
setShowUpgradeModal(false);
}}
/>
)}
{showPromptModal && (
<ModifyCoursePrompt
title="Give AI more context"
description="Pass additional information to the AI to generate a lesson."
onClose={() => setShowPromptModal(false)}
onSubmit={(prompt) => {
setShowPromptModal(false);
onRegenerateLesson(prompt);
}}
/>
)}
<div className="relative mr-2 flex items-center" ref={ref}>
<button
className={cn('rounded-full p-1 text-gray-400 hover:text-black', {
'text-black': isDropdownVisible,
})}
onClick={() => setIsDropdownVisible(!isDropdownVisible)}
>
<PenSquare className="text-current" size={16} strokeWidth={2.5} />
</button>
{isDropdownVisible && (
<div className="absolute right-0 top-full min-w-[170px] overflow-hidden rounded-md border border-gray-200 bg-white">
<button
onClick={() => {
onRegenerateLesson();
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<RefreshCcw
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Regenerate
</button>
<button
onClick={() => {
setIsDropdownVisible(false);
setShowPromptModal(true);
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<PenSquare
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Modify Prompt
</button>
</div>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,86 @@
import { PenSquare, RefreshCcw } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { cn } from '../../lib/classname';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { ModifyCoursePrompt } from './ModifyCoursePrompt';
type RegenerateOutlineProps = {
onRegenerateOutline: (prompt?: string) => void;
};
export function RegenerateOutline(props: RegenerateOutlineProps) {
const { onRegenerateOutline } = props;
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showPromptModal, setShowPromptModal] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => setIsDropdownVisible(false));
return (
<>
{showUpgradeModal && (
<UpgradeAccountModal
onClose={() => {
setShowUpgradeModal(false);
}}
/>
)}
{showPromptModal && (
<ModifyCoursePrompt
onClose={() => setShowPromptModal(false)}
onSubmit={(prompt) => {
setShowPromptModal(false);
onRegenerateOutline(prompt);
}}
/>
)}
<div className="absolute right-3 top-3" ref={ref}>
<button
className={cn('text-gray-400 hover:text-black', {
'text-black': isDropdownVisible,
})}
onClick={() => setIsDropdownVisible(!isDropdownVisible)}
>
<PenSquare className="text-current" size={16} strokeWidth={2.5} />
</button>
{isDropdownVisible && (
<div className="absolute right-0 top-full min-w-[170px] overflow-hidden rounded-md border border-gray-200 bg-white">
<button
onClick={() => {
onRegenerateOutline();
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<RefreshCcw
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Regenerate
</button>
<button
onClick={() => {
setIsDropdownVisible(false);
setShowPromptModal(true);
}}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
>
<PenSquare
size={16}
className="text-gray-400"
strokeWidth={2.5}
/>
Modify Prompt
</button>
</div>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,195 @@
import { useQuery } from '@tanstack/react-query';
import {
getAiCourseLimitOptions,
listUserAiCoursesOptions,
type ListUserAiCoursesQuery,
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
import { useEffect, useState } from 'react';
import { Gift, Loader2, User2 } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
import { AICourseSearch } from './AICourseSearch';
import { Pagination } from '../Pagination/Pagination';
type UserCoursesListProps = {};
export function UserCoursesList(props: UserCoursesListProps) {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const [pageState, setPageState] = useState<ListUserAiCoursesQuery>({
perPage: '10',
currPage: '1',
query: '',
});
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(pageState),
queryClient,
);
useEffect(() => {
setIsInitialLoading(false);
}, [userAiCourses]);
const courses = userAiCourses?.data ?? [];
const isAuthenticated = isLoggedIn();
const limitUsedPercentage = Math.round((used / limit) * 100);
useEffect(() => {
const queryParams = getUrlParams();
setPageState({
...pageState,
currPage: queryParams?.p || '1',
query: queryParams?.q || '',
});
}, []);
useEffect(() => {
if (pageState?.currPage !== '1' || pageState?.query !== '') {
setUrlParams({
p: pageState?.currPage || '1',
q: pageState?.query || '',
});
} else {
deleteUrlParam('p');
deleteUrlParam('q');
}
}, [pageState]);
return (
<>
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">
<span className="max-md:hidden">Your </span>Courses
</h2>
</div>
<div className="flex items-center gap-2">
{used > 0 && limit > 0 && !isPaidUserLoading && (
<div
className={cn(
'pointer-events-none flex items-center gap-2 opacity-0 transition-opacity',
{
'pointer-events-auto opacity-100': !isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">
{limitUsedPercentage}% used
</span>
<button
onClick={() => {
setShowUpgradePopup(true);
}}
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pl-1.5 pr-2 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
</button>
</p>
</div>
)}
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</div>
</div>
{!isInitialLoading && !isUserAiCoursesLoading && !isAuthenticated && (
<div className="flex min-h-[152px] flex-col items-center justify-center rounded-lg border border-gray-200 bg-white px-6 py-4">
<User2 className="mb-2 size-8 text-gray-300" />
<p className="max-w-sm text-balance text-center text-gray-500">
<button
onClick={() => {
showLoginPopup();
}}
className="font-medium text-black underline underline-offset-2 hover:opacity-80"
>
Sign up (free and takes 2s) or login
</button>{' '}
to generate and save courses.
</p>
</div>
)}
{!isUserAiCoursesLoading && !isInitialLoading && courses.length === 0 && isAuthenticated && (
<div className="flex min-h-[152px] items-center justify-center rounded-lg border border-gray-200 bg-white py-4">
<p className="text-sm text-gray-600">
You haven't generated any courses yet.
</p>
</div>
)}
{(isUserAiCoursesLoading || isInitialLoading) && (
<div className="flex min-h-[152px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white py-4">
<Loader2
className="size-4 animate-spin text-gray-400"
strokeWidth={2.5}
/>
<p className="text-sm font-medium text-gray-600">Loading...</p>
</div>
)}
{!isUserAiCoursesLoading && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
{courses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
<Pagination
totalCount={userAiCourses?.totalCount || 0}
totalPages={userAiCourses?.totalPages || 0}
currPage={Number(userAiCourses?.currPage || 1)}
perPage={Number(userAiCourses?.perPage || 10)}
onPageChange={(page) => {
setPageState({ ...pageState, currPage: String(page) });
}}
className="rounded-lg border border-gray-200 bg-white p-4"
/>
</div>
)}
{!isUserAiCoursesLoading &&
(userAiCourses?.data?.length || 0 > 0) &&
courses.length === 0 && (
<div className="flex min-h-[114px] items-center justify-center rounded-lg border border-gray-200 bg-white py-4">
<p className="text-sm text-gray-600">
No courses match your search.
</p>
</div>
)}
</>
);
}

View File

@@ -128,32 +128,32 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
return allRoadmaps;
};
useEffect(() => {
if (debouncedSearchValue.length === 0 || isFirstRender.current) {
setSearchResults([]);
return;
}
// useEffect(() => {
// if (debouncedSearchValue.length === 0 || isFirstRender.current) {
// setSearchResults([]);
// return;
// }
setIsActive(true);
setIsLoading(true);
loadTopAIRoadmapTerm()
.then((results) => {
const normalizedSearchText = debouncedSearchValue.trim().toLowerCase();
const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => {
return (
roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1
);
});
// setIsActive(true);
// setIsLoading(true);
// loadTopAIRoadmapTerm()
// .then((results) => {
// const normalizedSearchText = debouncedSearchValue.trim().toLowerCase();
// const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => {
// return (
// roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1
// );
// });
setSearchResults(
[...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [],
);
setActiveCounter(0);
})
.finally(() => {
setIsLoading(false);
});
}, [debouncedSearchValue]);
// setSearchResults(
// [...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [],
// );
// setActiveCounter(0);
// })
// .finally(() => {
// setIsLoading(false);
// });
// }, [debouncedSearchValue]);
useEffect(() => {
if (isFirstRender.current) {

View File

@@ -11,7 +11,6 @@ import { useToast } from '../../hooks/use-toast';
import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator';
import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { replaceChildren } from '../../lib/dom';
import { readAIRoadmapStream } from '../../helper/read-stream';
import {
getOpenAIKey,
isLoggedIn,
@@ -31,7 +30,7 @@ import { showLoginPopup } from '../../lib/popup.ts';
import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
import { IS_KEY_ONLY_ROADMAP_GENERATION, readAIRoadmapStream } from '../../lib/ai.ts';
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
import { AuthenticationForm } from '../AuthenticationFlow/AuthenticationForm.tsx';

View File

@@ -3,13 +3,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { markdownToHtml } from '../../lib/markdown';
import { Ban, Cog, Contact, FileText, User, UserRound, X } from 'lucide-react';
import { Ban, Cog, Contact, FileText, X } from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { readAIRoadmapContentStream } from '../../helper/read-stream';
import { cn } from '../../lib/classname';
import { showLoginPopup } from '../../lib/popup';
import { readAIRoadmapContentStream } from '../../lib/ai';
type RoadmapTopicDetailProps = RoadmapNodeDetails & {
onClose?: () => void;

View File

@@ -1,6 +1,6 @@
import { ChevronDown } from 'lucide-react';
import { useState } from 'react';
import { cn } from '../../lib/classname';
import { ChevronDown } from 'lucide-react';
type RelatedGuidesProps = {
relatedTitle?: string;
@@ -27,7 +27,7 @@ export function RelatedGuides(props: RelatedGuidesProps) {
<div className={cn('relative min-w-[250px] pt-0 lg:px-5 lg:pt-10')}>
<h4 className="text-lg font-medium max-lg:hidden">{relatedTitle}</h4>
<button
className="flex border-b w-full items-center justify-between gap-2 bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden"
className="flex w-full items-center justify-between gap-2 border-b bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden"
onClick={() => setIsOpen(!isOpen)}
>
{relatedTitle}

View File

@@ -21,10 +21,10 @@ import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
</a>
<a
href='/roadmaps'
href='/ai-tutor'
class='group relative inline text-gray-400 hover:text-white sm:hidden'
>
Roadmaps
AI Tutor
</a>
<!-- Desktop navigation items -->
@@ -34,14 +34,26 @@ import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
Start Here
</a>
<RoadmapDropdownMenu client:load />
<a href='/teams' class='group relative text-gray-400 hover:text-white'>
Teams
<a
href='/ai-tutor'
class='group relative mr-3 text-blue-300 hover:text-white'
>
AI Tutor
<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>
<a
href='/changelog'
class='group relative ml-0.5 hidden text-gray-400 hover:text-white md:block'
href='/teams'
class='group relative hidden text-gray-400 hover:text-white xl:block'
>
Changelog
Teams
</a>
</div>
</div>

View File

@@ -11,6 +11,7 @@ type PaginationProps = {
totalCount: number;
isDisabled?: boolean;
onPageChange: (page: number) => void;
className?: string;
};
export function Pagination(props: PaginationProps) {
@@ -22,6 +23,7 @@ export function Pagination(props: PaginationProps) {
currPage,
perPage,
isDisabled = false,
className,
} = props;
if (!totalPages || totalPages === 1) {
@@ -32,10 +34,14 @@ export function Pagination(props: PaginationProps) {
return (
<div
className={cn('flex items-center', {
'justify-between': variant === 'default',
'justify-start': variant === 'minimal',
})}
className={cn(
'flex items-center',
{
'justify-between': variant === 'default',
'justify-start': variant === 'minimal',
},
className,
)}
>
<div className="flex items-center gap-1 text-xs font-medium">
<button

View File

@@ -60,7 +60,7 @@ const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
class:list={[
'border-t bg-gray-100',
{
'mt-8': !relatedQuestionDetails.length,
'mt-0': !relatedQuestionDetails.length,
},
]}
>

View File

@@ -10,11 +10,8 @@ export function CourseAnnouncement() {
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">
<a href="/courses/sql" className="flex items-center bg-yellow-400 py-1.5">
<span className="container mx-auto flex items-center justify-start gap-2 text-center sm:justify-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">
@@ -22,7 +19,7 @@ export function CourseAnnouncement() {
</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="items-center gap-1.5 rounded-full bg-black px-2 py-0.5 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>

View File

@@ -311,7 +311,7 @@ export function SQLCoursePage() {
<SectionHeader
title="Not your average SQL course"
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."
description="Built around a text-based interactive approach and packed with practical challenges, this comprehensive SQL bootcamp stands out with features that make it truly unique."
className="mt-16 md:mt-20"
/>
@@ -370,9 +370,7 @@ export function SQLCoursePage() {
<SectionHeader
title="Course Overview"
description="The course is designed to help you go from SQL beginner to expert
through hands-on practice with real-world scenarios, mastering
everything from basic to complex queries."
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-8 md:mt-24"
/>

View File

@@ -2,7 +2,7 @@ import type {
GetUserProfileRoadmapResponse,
GetPublicProfileResponse,
} from '../../api/user';
import { getPercentage } from '../../helper/number';
import { getPercentage } from '../../lib/number';
import { PrivateProfileBanner } from './PrivateProfileBanner';
import { UserProfileRoadmapRenderer } from './UserProfileRoadmapRenderer';

View File

@@ -1,5 +1,5 @@
import { getPercentage } from '../../helper/number';
import { getRelativeTimeString } from '../../lib/date';
import { getPercentage } from '../../lib/number';
type UserPublicProgressStats = {
resourceType: 'roadmap';

View File

@@ -1,6 +1,5 @@
import type { GetPublicProfileResponse } from '../../api/user';
import { UserPublicProgressStats } from './UserPublicProgressStats';
import { getPercentage } from '../../helper/number.ts';
import { getPercentage } from '../../lib/number';
type UserPublicProgressesProps = {
userId: string;
@@ -73,15 +72,15 @@ export function UserPublicProgresses(props: UserPublicProgressesProps) {
target="_blank"
key={roadmap.id + counter}
href={`/${roadmap.id}?s=${userId}`}
className="relative group border-gray-300 flex items-center justify-between rounded-md border bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400 overflow-hidden"
className="group relative flex items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400"
>
<span className="flex-grow truncate">{roadmap.title}</span>
<span className="text-xs text-gray-400">
{parseInt(percentageDone, 10)}%
{percentageDone}%
</span>
<span
className="absolute transition-colors left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10"
className="absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 transition-colors group-hover:bg-black/10"
style={{
width: `${percentageDone}%`,
}}

View File

@@ -7,7 +7,7 @@ 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
isNew: false
type: 'textual'
date: 2025-01-29
sitemap:

View File

@@ -7,7 +7,7 @@ seo:
title: 'Data Science vs. Computer Science: Which Path to Choose'
description: 'Data science or computer science? Learn the tools, roles, and paths in each field to decide which fits your strengths and career goals.'
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-vs-computer-science-rudoc.jpg'
isNew: true
isNew: false
type: 'textual'
date: 2025-02-06
sitemap:

View File

@@ -7,7 +7,7 @@ seo:
title: 'Data Science vs. Data Analytics: Which is Right for You?'
description: 'Data science vs. Data analytics? This guide breaks down roles, tools, and growth opportunities for aspiring data professionals.'
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-vs-data-analytics-3ol7o.jpg'
isNew: true
isNew: false
type: 'textual'
date: 2025-02-06
sitemap:

View File

@@ -7,7 +7,7 @@ seo:
title: 'Data Science vs Machine Learning: How are they different?'
description: 'Excited about a career in data science or machine learning? Learn the differences, key skills, tools, and how to choose the role that aligns with your ambitions.'
ogImageUrl: 'https://assets.roadmap.sh/guest/data-science-vs-machine-learning-gaa7s.jpg'
isNew: true
isNew: false
type: 'textual'
date: 2025-02-06
sitemap:

View File

@@ -9,7 +9,7 @@ seo:
ogImageUrl: 'https://assets.roadmap.sh/guest/become-a-full-stack-developer-54s51.jpg'
relatedTitle: 'Other Guides'
relatedGuidesId: full-stack
isNew: true
isNew: false
type: 'textual'
date: 2025-02-04
sitemap:

View File

@@ -8,7 +8,7 @@ seo:
description: 'Comparing Go vs Java for your projects? Explore features like concurrency, memory management, and learning curves to find the right fit for your needs.'
ogImageUrl: 'https://assets.roadmap.sh/guest/go-vs-java-fo08l.jpg'
relatedGuidesTitle: 'Other Guides'
isNew: true
isNew: false
type: 'textual'
date: 2025-02-04
sitemap:

View File

@@ -8,7 +8,7 @@ seo:
description: 'Understand the unique strengths of Java and JavaScript to decide which suits your programming needs best.'
ogImageUrl: 'https://assets.roadmap.sh/guest/java-vs-javascript-66pqp.jpg'
relatedTitle: 'Other Guides'
isNew: true
isNew: false
type: 'textual'
date: 2025-01-30
sitemap:
@@ -68,7 +68,7 @@ Usually saved in a .js file extension, this code will show **"Sign up on roadmap
### Java vs. JavaScript: Execution and runtime
Java is often used for server-side programming. It is a compiled language that requires a [Java Virtual Machine](https://www.geeksforgeeks.org/jvm-works-jvm-architecture/) **(JVM)** to run Java code. Before running your code, it's compiled into bytecode, a platform-independent format. The JVM then executes the bytecode to ensure that Java programs run on any system with JVM installed. It makes Java a good choice for large-scale applications that run on any operating system. These operating systems include Windows, macOS, Linux, and mobile devices.
Java is often used for server-side programming. It is a compiled language that requires a Java Virtual Machine **(JVM)** to run Java code. Before running your code, it's compiled into bytecode, a platform-independent format. The JVM then executes the bytecode to ensure that Java programs run on any system with JVM installed. It makes Java a good choice for large-scale applications that run on any operating system. These operating systems include Windows, macOS, Linux, and mobile devices.
![](https://assets.roadmap.sh/guest/java-vs-javascript-execution-and-runtime-csnnz.png)

View File

@@ -5,7 +5,7 @@ briefDescription: 'Test, Rate and Improve your Full-stack knowledge with these q
title: 'Top 50 Full Stack Developer Interview Questions'
description: 'Ace your interview with our curated list of 50 full-stack developer interview questions, perfect for beginners and experienced candidates.'
authorId: 'fernando'
isNew: true
isNew: false
date: 2025-01-29
seo:
title: 'Top 50 Full Stack Developer Interview Questions'

View File

@@ -3,10 +3,10 @@ jsonUrl: '/jsons/roadmaps/ai-data-scientist.json'
pdfUrl: '/pdfs/roadmaps/ai-data-scientist.pdf'
order: 5
renderer: 'editor'
briefTitle: 'AI and Data Scientist'
briefTitle: 'AI and Data Scientist Roadmap'
briefDescription: 'Step by step guide to becoming an AI and Data Scientist in 2025'
title: 'AI and Data Scientist'
description: 'Step by step guide to becoming an AI and Data Scientist in 2025'
title: 'AI and Data Scientist Roadmap'
description: 'Step by step roadmap guide to becoming an AI and Data Scientist in 2025'
hasTopics: true
isNew: false
dimensions:
@@ -18,6 +18,52 @@ schema:
imageUrl: 'https://roadmap.sh/roadmaps/ai-data-scientist.png'
datePublished: '2023-08-17'
dateModified: '2023-08-17'
question:
title: 'What is a data scientist?'
description: |
A data scientist is a person who extracts actionable insights from data by using programming, statistics, machine learning, and domain knowledge.
That is a very generic description, however, the field of data science is so broad that it's tough to define the role without going into the specifics.
To give you an example of what a data scientist can do, take a closer look at the last selfie you took. Look at your face; what emotion are you showing? Are you happy? Sad? Crying? Laughing? All at the same time? For you, answering those questions is trivially simple; however, getting a computer to do it is a whole different problem.
And that's where data scientists come into play.
Data scientists take unstructured data (like video, photos, text files, etc) and structured data (like database rows, spreadsheets, etc) and figure out what it all means. By analyzing this data (some call it "big data"), they help companies make better decisions, such as understanding what customers want, how they feel about their products, or even predicting future trends.
They help find the hidden answers in the data, which is what makes this profession so appealing to some.
## What does a data scientist do?
Most data scientists collect, organize, and study data to uncover useful insights. At a high level, here's a simple way to break that process down:
**Collecting Data:** They gather information from various sources, like websites, databases, or devices. Depending on the project, the sources of information might be very different, but the point is that once the data enters the domains of the data scientist, it's all 1's and 0's for them to process.
**Cleaning Data:** Before being able to use the data, they need to ensure the data is formatted correctly, doesn't have any holes, and that the values actually make sense within the context of their source (i.e., that there are not too many "outliers"). They fix these mistakes and make sure the data is ready to use.
**Analyzing Data:** They use tools and techniques, like exploratory data analysis, charts, or algorithms, to find patterns and trends.
**Sharing Insights:** Once they're done with their analysis, the last step is sharing the results. Data scientists explain their findings in easy-to-understand ways, often with visuals, so that others can take action based on the data.
For example, using these steps, a data scientist might help a company predict which products will sell best next month based on historical sales data and customer trends.
## How do you become a data scientist?
There is no single way to become a data scientist, however, the journey usually involves these steps:
**#1. Learn the Basics:** Start with math (like statistics) and programming (Python or R) to understand and process data efficiently.
**#2.** Practice with Data: Begin with small projects, like analyzing trends or creating charts, and gradually tackle more complex goals.
**#3. Take Courses:** Use online classes and tutorials to learn Data Science step by step.
**#4. Build a Portfolio:** Solve real-world problems and share your work to showcase your skills and attract opportunities.
**#5. Get Experience:** Seek internships or entry-level roles to apply and grow your skills.
In the end, you have to keep in mind that this is a marathon, not a race. Rushing through knowledge or cutting corners for the sake of speed will only limit your options and your understanding by the time you actually do get the job.
With curiosity and practice, anyone can start exploring the world of Data Science.
seo:
title: 'AI and Data Scientist Roadmap'
description: 'Learn to become an AI and Data Scientist using this roadmap. Community driven, articles, resources, guides, interview questions, quizzes for modern backend development.'

View File

@@ -0,0 +1,116 @@
---
import type { FAQType } from '../../components/FAQs/FAQs.astro';
export const faqs: FAQType[] = [
{
question: 'What degree do you need to become a data scientist?',
answer: [
"You don't need a specific degree to become a data scientist, but fields like Computer Science, Mathematics, Statistics, or Engineering are helpful for their focus on programming, algorithms, and databases.",
'Degrees in Physics, Economics, or Social Sciences also provide critical thinking and research skills valuable for analyzing data.',
'Recently, many have transitioned into Data Science through bootcamps or online courses, highlighting the importance of practical skills over formal degrees.',
],
},
{
question: 'Is becoming a data scientist a good career path?',
answer: [
'Yes, [becoming a data scientist is a good career path](https://roadmap.sh/ai-data-scientist/career-path) for many reasons, although all of them stem from the same one: technology is generating more and more data every day, and making sense of it is crucial for any business. The main derived reasons validating data science as a great career choice are:',
'**High Demand:** Companies in almost every industry need data scientists to help them make sense of their data. This creates plenty of job opportunities.',
'**Competitive Salaries:** Data Science is one of the highest-paying fields in tech, making it financially rewarding.',
'**Diverse Applications:** Getting bored in the field of data science is quite a challenge. If you think about it, data science skills can be applied in healthcare, finance, marketing, sports, and more, offering flexibility in choosing industries.',
'**Continuous Learning:** The field evolves quickly, which makes it exciting for those who love learning and staying up-to-date with new tools and techniques.',
'**Impactful Work:** Data scientists solve real-world problems, like predicting diseases, optimizing business processes, or making products more user-friendly.',
'While the path requires dedication and learning, the rewards—both professional and personal—make it a worthwhile choice for those who enjoy working with data and solving problems.',
],
},
{
question: 'What are data scientist salaries like?',
answer: [
'Data scientist salaries vary based on factors such as location, experience, and industry, making them very hard to average and provide values that are useful to everyone around the globe.',
"Here's an overview of average annual salaries for entry-level data scientists in various regions based on information gathered from Glassdoor and Indeed:",
'In the United States, according to Glassdoor, the average salary for an entry-level data scientist is approximately $110k per year. Indeed, on the other hand, reports an average salary of around $54,313 per year for entry-level data scientists.',
"For European countries, like Spain, for example, the average salary for an entry-level data scientist is about $40k per year. In the **United Kingdom**, while there aren't a lot of details for entry-level positions, reports show that the average salary for a data scientist in London is £50k per year, suggesting that entry-level positions may start lower.",
'Finally, in **Canada**, the average salary for entry-level data scientists is around CAD 88k.',
'Remember that all these figures are averages and can vary based on individual qualifications, specific job roles, the employing organization, and even your ability to negotiate your salary.',
'However, generally speaking, Data Science is considered a well-compensated field with opportunities for growth and advancement.',
],
},
{
question: 'What skills does a data scientist need?',
answer: [
'The most important [data science skills](https://roadmap.sh/ai-data-scientist/skills) a data scientist needs to possess are all listed in this roadmap.',
'At a high level, a data scientist needs a mix of technical and soft skills to succeed. Here are some of the key skills:',
'**Programming:** Knowing Python, R, or [SQL](https://roadmap.sh/sql) is a big plus, as relying on others to deploy your work can be limiting.',
'**Statistics & Math:** Essential for interpreting and modeling data, focusing on statistics, probability, and linear algebra.',
'**Data Visualization:** Master creating charts, graphs, and dashboards to effectively share your findings.',
'**Machine Learning:** Understand algorithms and models for predicting and classifying data.',
'**Big Data Tools:** Basic knowledge of Hadoop or Spark helps in handling large datasets and collaborating with data engineers.',
'**Data Wrangling:** Cleaning and prepping messy data is a must-have skill.',
'**Critical Thinking:** Asking the right questions and solving novel problems is key.',
'**Communication:** Simplify complex findings for stakeholders.',
'**Domain Knowledge:** Knowing your industry (e.g., finance or healthcare) helps you choose the right tools and approaches.',
'These skills combined will help data scientists extract actionable insights from data and drive decision-making in organizations.',
],
},
{
question: 'What tools do data scientists use?',
answer: [
"The [tools used by data scientists](https://roadmap.sh/ai-data-scientist/tools) vary quite a lot depending on the projects they're working on, the industry they're in, and even on their focus (whether they're purely theoretical data scientists or if they're also writing production-ready code).",
'That said, here are some of the most common tools used in the data science field:',
'**Programming Languages:** **Python** is one of the most popular programming languages for data analysis, machine learning, and visualization. It is also ideal for developing microservices that make your ML models available to the public. On the other hand, something like R would be perfect for statistical computing and data visualization. Finally, **SQL** is used to query and manage databases.',
"**Data Manipulation and Analysis Tools:** Libraries like **Pandas** and **NumPy** are industry standards for data manipulation in Python. If you're using R instead, check out Dplyr and Tidyr; they're both great for data manipulation in that language. Both quantitative and qualitative data are processed and analyzed using tools like Pandas, NumPy, Dplyr, and Tidyr.",
'**Data Visualization Tools:** Tableau and Power BI are some of the most used tools for creating interactive dashboards. If, on the other hand, you require more control and customization, you might want to look at Matplotlib and Seaborn; they are Python libraries for generating graphs and plots.',
"**Machine Learning Frameworks:** In this case, there aren't that many options; the industry is currently focusing on Scikit-learn, a Python library for machine learning, TensorFlow, and PyTorch, which focus more on deep learning applications.",
'**Big Data Tools:** Hadoop and Spark are de facto standards at this point for handling and processing large datasets.',
"**Databases:** If you're looking into SQL, MySQL, and [PostgreSQL](https://roadmap.sh/postgresql-dba), they are your best bets. For NoSQL, a great starting point is MongoDB.",
"**Cloud Platforms:** In this category, nothing beats the 3 big ones: **AWS**, **Google Cloud**, and **Azure**. If you're looking for scalable storage, processing, and machine learning services, you've found your answers.",
'**Version Control:** In terms of industry standards, **Git** is pretty much alone here.',
'**Collaboration Tools:** **Jupyter Notebooks** and **RStudio** are designed for sharing code and analysis in an interactive format.',
],
},
{
question: 'What is the Data Science Lifecycle?',
answer: [
'The [Data Science Lifecycle](https://roadmap.sh/ai-data-scientist/lifecycle) is the process data scientists follow to complete a data science project.',
'It consists of several stages:',
'**Problem Definition:** Clearly define the problem you want to solve and understand the objectives.',
'**Data Collection:** Gather relevant data from various sources, such as databases, APIs, or external datasets.',
"**Data Preparation:** Clean, organize, and preprocess the data to ensure it's ready for analysis. This includes handling missing values, removing duplicates, and formatting data correctly.",
'**Exploratory Data Analysis (EDA):** Analyze the data to identify patterns, trends, and relationships. Use visualization tools to gain insights.',
'**Model Building:** Develop and train machine learning models or statistical algorithms to solve the problem.',
"**Model Evaluation:** Test the model's performance using metrics like accuracy, precision, recall, or F1 score to ensure it meets the objectives.",
'**Deployment:** Integrate the model into production systems so it can be used in real-world applications.',
"**Monitoring and Maintenance:** Continuously monitor the model's performance and update it as needed to adapt to new data or changing requirements.",
'With these steps, data scientists ensure that they cover all the basics when working on a project, from ideation to production release.',
],
},
{
question: 'How are data scientists different from AI Engineers?',
answer: [
"Data scientists are different from [AI Engineers](https://roadmap.sh/ai-engineer), however, they're often confused due to overlapping skills.",
'For **data scientists**, the focus is to analyze data and uncover insights, while in the case of **AI Engineers**, their focus is on building, deploying, and maintaining AI systems. **Data scientists** tend to be great at data manipulation (Python, R, SQL) and statistical analysis, while **AI Engineers** are quite skilled in software engineering, programming, and machine learning frameworks.',
'In the end, **data scientists** will provide insights, reports, and predictive models. While **AI Engineers** will deliver AI-powered applications, APIs, and scalable systems.',
],
},
{
question: 'What is the difference between Data Science and Data Analytics?',
answer: [
"The difference between [data science and data analytics](https://roadmap.sh/ai-data-scientist/vs-data-analytics) might not be obvious at first sight, but it's a big one once you look closer into both roles. Data science involves creating predictive models, applying statistical methods, and exploring data to uncover insights. It usually includes advanced techniques such as machine learning. Data analysts, on the other hand, focus on analyzing current and historical data to answer specific questions and generate reports or dashboards, often with less emphasis on predictive modeling or advanced algorithms.",
],
},
{
question:
'What is the difference between Data Science and Data Engineering?',
answer: [
'The main difference between data science and data engineering is their focus.',
'Data Science focuses on analyzing and modeling data to extract insights and make predictions. It emphasizes statistics, machine learning, and visualization. Data engineering involves building and maintaining the infrastructure and pipelines needed to collect, store, and process data efficiently from multiple data sources. Data engineers ensure that data scientists have clean, accessible, and reliable data for their analyses.',
],
},
{
question: 'How long does it take to become a data scientist?',
answer: [
"Becoming a data scientist can take between 1 to 3 years, on average, considering a focused approach. Of course, keep in mind that this answer will highly depend on your approach to becoming a data scientist and your prior experience. And if you're aiming for a position as a senior data scientist, the time to get there will increase significantly if you haven't started yet.",
'A strong foundation in programming, statistics, and ML is essential for this to happen. Many achieve this through a combination of formal education, such as a degree or certification program, and hands-on projects to build practical skills.',
],
},
];
---

View File

@@ -1 +1,10 @@
# StyleCop Rules
# StyleCop Rules
StyleCop is a tool used for developers to standardize their code and ensure they all follow the same syntax principles. With StyleCop, one standard can be defined in a `stylecop.json` file and shared across your team so that each member has the same guidelines when formatting your code. Beyond a single project, StyleCop can also be added as an extension, so all of the projects on your IDE follow the same formatting rules, this is especially useful if your organization follows the same rule standards for all projects.
Visit the following resources to learn more:
- [@opensource@StyleCop GitHub official page](https://github.com/StyleCop/StyleCop)
- [@opensource@StyeleCop Analyzers, a more modern version of StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
- [@video@The StyleCop setup and Advantages](https://www.youtube.com/watch?v=dmpOKmz3lPw)
- [@article@StyleCop: A Detailed Guide to Starting and Using It](https://blog.submain.com/stylecop-detailed-guide/)

View File

@@ -5,7 +5,3 @@ The rise in popularity of NoSQL databases provided a flexible and fluidity with
- **B**asically **A**vailable
- **S**oft state
- **E**ventual consistency
Visit the following resources to learn more:
- [@article@BASE Model vs. ACID Model](https://www.geeksforgeeks.org/acid-model-vs-base-model-for-database/)

View File

@@ -4,5 +4,4 @@ DCL includes commands such as GRANT and REVOKE which mainly deal with the rights
Visit the following resources to learn more:
- [@article@DCL](https://en.wikipedia.org/wiki/Data_Control_Language)
- [@article@DCL Commands](https://www.geeksforgeeks.org/sql-ddl-dql-dml-dcl-tcl-commands/)
- [@article@DCL](https://en.wikipedia.org/wiki/Data_Control_Language)

View File

@@ -13,10 +13,3 @@ Here is the list of some of the most commonly used scheduling algorithms:
- **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.
- **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.
- **Lottery Scheduling:** The process is allocated the CPU based on a lottery system. It is a preemptive algorithm.
Visit the following resources to learn more
- [@video@Introduction to CPU Scheduling](https://youtu.be/EWkQl0n0w5M?si=Lb-PxN_t-rDfn4JL)
- [@article@CPU Scheduling in Operating Systems - geeksforgeeks](https://www.geeksforgeeks.org/cpu-scheduling-in-operating-systems/)
- [@article@Lottery Scheduling for Operating Systems - geeksforgeeks](https://www.geeksforgeeks.org/lottery-process-scheduling-in-operating-system/)
- [@article@Program for Round Robin Scheduling for the same Arrival time - geeksforgeeks](https://www.geeksforgeeks.org/program-for-round-robin-scheduling-for-the-same-arrival-time/)

View File

@@ -7,5 +7,4 @@ Visit the following resources to learn more:
- [@article@What is DNS cache poisoning? | DNS spoofing](https://www.cloudflare.com/learning/dns/dns-cache-poisoning/)
- [@article@What Is DNS Poisoning?](https://www.fortinet.com/resources/cyberglossary/dns-poisoning)
- [@article@DNS Spoofing or DNS Cache poisoning](https://www.geeksforgeeks.org/dns-spoofing-or-dns-cache-poisoning/)
- [@article@DNS Poisoning (DNS Spoofing): Definition, Technique & Defense](https://www.okta.com/identity-101/dns-poisoning/)

View File

@@ -4,5 +4,4 @@
Learn more from the following resources:
- [@article@What is a loopback address?](https://www.geeksforgeeks.org/what-is-a-loopback-address/)
- [@article@Understanding the loopback address and loopback interfaces](https://study-ccna.com/loopback-interface-loopback-address/)

View File

@@ -14,4 +14,3 @@ This knowledge is essential for designing, implementing, and maintaining effecti
Learn more from the following resources:
- [@article@What are Network Protocols?](https://www.solarwinds.com/resources/it-glossary/network-protocols)
- [@article@Types of Network Topology](https://www.geeksforgeeks.org/types-of-network-topology/)

View File

@@ -4,5 +4,4 @@
Learn more from the following resources:
- [@article@What is an operating system?](https://www.geeksforgeeks.org/what-is-an-operating-system/)
- [@video@What is an operating system as fast as possible](https://www.youtube.com/watch?v=pVzRTmdd9j0)

View File

@@ -1,7 +1,3 @@
# Port Blocking
Port blocking is an essential practice in hardening the security of your network and devices. It involves restricting, filtering, or entirely denying access to specific network ports to minimize exposure to potential cyber threats. By limiting access to certain ports, you can effectively safeguard your systems against unauthorized access and reduce the likelihood of security breaches.
Learn more from the following resources:
- [@article@What is port blocking with LAN?](https://www.geeksforgeeks.org/what-is-port-blocking-within-lan/)
Port blocking is an essential practice in hardening the security of your network and devices. It involves restricting, filtering, or entirely denying access to specific network ports to minimize exposure to potential cyber threats. By limiting access to certain ports, you can effectively safeguard your systems against unauthorized access and reduce the likelihood of security breaches.

View File

@@ -4,5 +4,4 @@
Learn more from the following resources:
- [@article@What is a protocol analyzer?](https://www.geeksforgeeks.org/what-is-protocol-analyzer/)
- [@video@Protocol Analyzers](https://www.youtube.com/watch?v=hTMhlB-o0Ow)

View File

@@ -1,7 +1,3 @@
# route
The `route` command is a network utility used to view and manipulate the IP routing table on Unix-like and Windows systems. It allows users to display the current routes that data packets take, as well as add, modify, or delete routes for network traffic. This command is often used in network troubleshooting and configuration to control how data flows between different networks and subnets. By specifying routes manually, administrators can define specific paths for network traffic, bypassing default routes and optimizing performance or security.
Learn more from the following resources:
- [@article@How to check the routing table in Linux](https://www.geeksforgeeks.org/route-command-in-linux-with-examples/)
The `route` command is a network utility used to view and manipulate the IP routing table on Unix-like and Windows systems. It allows users to display the current routes that data packets take, as well as add, modify, or delete routes for network traffic. This command is often used in network troubleshooting and configuration to control how data flows between different networks and subnets. By specifying routes manually, administrators can define specific paths for network traffic, bypassing default routes and optimizing performance or security.

View File

@@ -4,5 +4,4 @@ A star network topology is a configuration where all devices (nodes) are connect
Learn more from the following resources:
- [@article@Advantages and Disadvantages of Star Topology](https://www.geeksforgeeks.org/advantages-and-disadvantages-of-star-topology/)
- [@video@Star Topology](https://www.youtube.com/watch?v=EQ3rW22-Py0)

View File

@@ -4,5 +4,4 @@ In networking and cybersecurity, a handshake is a process of establishing a secu
Learn more from the following resources:
- [@article@TCP 3-Way Handshake Process](https://www.geeksforgeeks.org/tcp-3-way-handshake-process/)
- [@video@TLS Handshake Explained](https://www.youtube.com/watch?v=86cQJ0MMses)

View File

@@ -11,7 +11,6 @@ Data Analysts, as ambassadors of this domain, employ these types, to answer vari
Visit the following resources to learn more:
- [@article@Data Analytics and its type](https://www.geeksforgeeks.org/data-analytics-and-its-type/)
- [@article@The 4 Types of Data Analysis: Ultimate Guide](https://careerfoundry.com/en/blog/data-analytics/different-types-of-data-analysis/)
- [@video@Descriptive vs Diagnostic vs Predictive vs Prescriptive Analytics: What's the Difference?](https://www.youtube.com/watch?v=QoEpC7jUb9k)
- [@video@Types of Data Analytics](https://www.youtube.com/watch?v=lsZnSgxMwBA)

View File

@@ -4,5 +4,4 @@ Data structures are crucial in the field of computer science and coding because
Learn more from the following links:
- [@article@Why Data Structures and Algorithms Are Important to Learn?](https://www.geeksforgeeks.org/why-data-structures-and-algorithms-are-important-to-learn/)
- [@video@What are Data Structures? Why is it Important?](https://www.youtube.com/watch?v=18V8Avz2OH8)

View File

@@ -4,6 +4,5 @@ Heap Sort is an efficient, comparison-based sorting algorithm. It utilizes a dat
Learn more from the following resources:
- [@article@Heap Sort - W3Schools](https://www.geeksforgeeks.org/heap-sort/)
- [@article@Heap Sort Visualize](https://www.hackerearth.com/practice/algorithms/sorting/heap-sort/tutorial/)
- [@video@Heap sort in 4 minutes](https://www.youtube.com/watch?v=2DmK_H7IdTo)

View File

@@ -5,5 +5,4 @@ Linear search is one of the simplest search algorithms. In this method, every el
Learn more from the following resources:
- [@article@DSA Linear Search - W3Schools](https://www.w3schools.com/dsa/dsa_algo_linearsearch.php)
- [@article@Linear Search - GeeksForGeeks](https://www.geeksforgeeks.org/linear-search/)
- [@video@Learn Linear Search in 3 minutes](https://www.youtube.com/watch?v=246V51AWwZM)

View File

@@ -4,5 +4,4 @@ An **AVL tree** is a type of binary search tree that is self-balancing, which me
Learn more from the following links:
- [@article@AVL Tree Data Structure](https://www.geeksforgeeks.org/introduction-to-avl-tree/)
- [@video@AVL trees in 5 minutes — Intro & Search](https://www.youtube.com/watch?v=DB1HFCEdLxA)

View File

@@ -4,5 +4,4 @@ B-Tree is a self-balanced search tree data structure that maintains sorted data
Learn more from the following links:
- [@article@Introduction of B-Tree](https://www.geeksforgeeks.org/introduction-of-b-tree-2/)
- [@video@B-trees in 4 minutes — Intro](https://www.youtube.com/watch?v=FgWbADOG44s)

View File

@@ -4,6 +4,4 @@
Learn more from the following resources:
- [@article@Introduction of B Tree](https://www.geeksforgeeks.org/introduction-of-b-tree-2/)
- [@article@Introduction of B+ Tree](https://www.geeksforgeeks.org/introduction-of-b-tree/)
- [@video@B Trees and B+ Trees. How they are useful in Databases](https://www.youtube.com/watch?v=aZjYr87r1b8)

View File

@@ -4,5 +4,4 @@ A **Skip List** is a probabilistic data structure that allows efficient search,
Learn more from the following resources:
- [@article@Skip List Efficient Search, Insert and Delete in Linked List](https://www.geeksforgeeks.org/skip-list/)
- [@video@Skip Lists](https://www.youtube.com/watch?v=NDGpsfwAaqo)

View File

@@ -4,5 +4,4 @@ ISAM, which stands for Indexed Sequential Access Method, is a type of disk stora
Learn more from the following resources:
- [@article@ISAM in Database](https://www.geeksforgeeks.org/isam-in-database/)
- [@video@DBMS - Index Sequential Access Method (ISAM)](https://www.youtube.com/watch?v=EiW1VVPor10)

View File

@@ -4,5 +4,4 @@ Backtracking is a powerful algorithmic technique that aims to solve a problem in
Learn more from the following links:
- [@article@Backtracking Algorithm](https://www.geeksforgeeks.org/backtracking-algorithms/)
- [@video@What is backtracking?](https://www.youtube.com/watch?v=Peo7k2osVVs)

View File

@@ -4,5 +4,4 @@ Greedy algorithms follow the problem-solving heuristic of making the locally opt
Learn more from the following links:
- [@article@Greedy Algorithm Tutorial Examples, Application and Practice Problem](https://www.geeksforgeeks.org/introduction-to-greedy-algorithm-data-structures-and-algorithm-tutorials/)
- [@video@Greedy Algorithms Tutorial ](https://www.youtube.com/watch?v=bC7o8P_Ste4)

View File

@@ -4,5 +4,4 @@ Randomised algorithms are a type of algorithm that employs a degree of randomnes
Learn more from the following links:
- [@article@Randomized Algorithms](https://www.geeksforgeeks.org/randomized-algorithms/)
- [@video@Algorithm Classification Randomized Algorithm](https://www.youtube.com/watch?v=J_EVG6yCOz0)

View File

@@ -4,5 +4,4 @@ Divide and conquer is a powerful algorithm design technique that solves a proble
Learn more from the following links:
- [@article@Introduction to Divide and Conquer Algorithm](https://www.geeksforgeeks.org/introduction-to-divide-and-conquer-algorithm/)
- [@video@Divide & Conquer Algorithm In 3 Minutes](https://www.youtube.com/watch?v=YOh6hBtX5l0)

View File

@@ -4,5 +4,4 @@ Recursion is a method where the solution to a problem depends on solutions to sh
Learn more from the following links:
- [@article@Introduction to Recursion](https://www.geeksforgeeks.org/introduction-to-recursion-2/)
- [@video@Recursion in 100 Seconds](https://www.youtube.com/watch?v=rf60MejMz3E)

Some files were not shown because too many files have changed in this diff Show More