mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-16 11:51:49 +08:00
Compare commits
16 Commits
feat/ai-co
...
fix/sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75945c7c65 | ||
|
|
8b61bbfcbb | ||
|
|
2c39611b47 | ||
|
|
475cb85600 | ||
|
|
37de8700d5 | ||
|
|
9db05bddbd | ||
|
|
4bda81bf52 | ||
|
|
00c5254ea9 | ||
|
|
bc97fc4c03 | ||
|
|
f953b96d52 | ||
|
|
40793efe4e | ||
|
|
22a29605d8 | ||
|
|
cbfd4f7fcb | ||
|
|
992cf82e5c | ||
|
|
ea89ac864c | ||
|
|
35a4a93ca7 |
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1741697790683
|
||||
"lastUpdateCheck": 1739229597159
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
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_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
|
||||
PUBLIC_COURSE_APP_URL=http://localhost:5173
|
||||
@@ -53,7 +53,6 @@
|
||||
"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",
|
||||
@@ -64,7 +63,6 @@
|
||||
"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",
|
||||
@@ -74,7 +72,6 @@
|
||||
"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",
|
||||
@@ -84,6 +81,7 @@
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/google": "^1.1.19",
|
||||
"@playwright/test": "^1.48.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -94,6 +92,7 @@
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"ai": "^4.1.51",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
||||
371
pnpm-lock.yaml
generated
371
pnpm-lock.yaml
generated
@@ -80,9 +80,6 @@ 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
|
||||
@@ -113,9 +110,6 @@ 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)
|
||||
@@ -143,9 +137,6 @@ 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
|
||||
@@ -168,6 +159,9 @@ importers:
|
||||
specifier: ^4.5.5
|
||||
version: 4.5.6(@types/react@18.3.18)(react@18.3.1)
|
||||
devDependencies:
|
||||
'@ai-sdk/google':
|
||||
specifier: ^1.1.19
|
||||
version: 1.1.19(zod@3.24.1)
|
||||
'@playwright/test':
|
||||
specifier: ^1.48.0
|
||||
version: 1.50.0
|
||||
@@ -198,6 +192,9 @@ importers:
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
ai:
|
||||
specifier: ^4.1.51
|
||||
version: 4.1.51(react@18.3.1)(zod@3.24.1)
|
||||
csv-parser:
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.0
|
||||
@@ -228,6 +225,46 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/google@1.1.19':
|
||||
resolution: {integrity: sha512-Q4l2iWAADUf1pGbXX60A2nnUqEtPLtLpXsbjr3hVcgI9M3q9BqUmSoGsoJ/AAwvZU3uarEb0IJuv+7zlitvCBw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.10':
|
||||
resolution: {integrity: sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.1.20':
|
||||
resolution: {integrity: sha512-4QOM9fR9SryaRraybckDjrhl1O6XejqELdKmrM5g9y9eLnWAfjwF+W1aN0knkSHzbbjMqN77sy9B9yL8EuJbDw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/ui-utils@1.1.16':
|
||||
resolution: {integrity: sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -359,10 +396,6 @@ 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'}
|
||||
@@ -908,6 +941,10 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
@@ -1142,45 +1179,24 @@ 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'}
|
||||
@@ -1321,6 +1337,9 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/dom-to-image@2.6.7':
|
||||
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
|
||||
|
||||
@@ -1429,6 +1448,18 @@ packages:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
ai@4.1.51:
|
||||
resolution: {integrity: sha512-CuJgbi2Ktfv/7jjxvUhFOGZ8OFxWQ8a7ZF19lwJuVLauL4uWHLetm6R3iaafahJ8ZkueSbhR/Bnroy5apd1nCw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
@@ -1777,6 +1808,9 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
diff@5.2.0:
|
||||
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -1897,6 +1931,10 @@ packages:
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
eventsource-parser@3.0.0:
|
||||
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2052,9 +2090,6 @@ 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==}
|
||||
|
||||
@@ -2214,11 +2249,19 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
@@ -2300,9 +2343,6 @@ 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==}
|
||||
|
||||
@@ -2563,9 +2603,6 @@ 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
|
||||
@@ -2810,9 +2847,6 @@ 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==}
|
||||
|
||||
@@ -2908,12 +2942,6 @@ 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:
|
||||
@@ -2937,24 +2965,15 @@ 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==}
|
||||
|
||||
@@ -3054,6 +3073,9 @@ packages:
|
||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -3088,9 +3110,6 @@ 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'}
|
||||
@@ -3185,6 +3204,11 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.3.2:
|
||||
resolution: {integrity: sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
@@ -3200,6 +3224,10 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
@@ -3320,33 +3348,6 @@ 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:
|
||||
@@ -3504,6 +3505,43 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/google@1.1.19(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.10(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
eventsource-parser: 3.0.0
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.1.20(react@18.3.1)(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.1)
|
||||
swr: 2.3.2(react@18.3.1)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
zod: 3.24.1
|
||||
|
||||
'@ai-sdk/ui-utils@1.1.16(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
zod-to-json-schema: 3.24.1(zod@3.24.1)
|
||||
optionalDependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@@ -3705,10 +3743,6 @@ 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
|
||||
@@ -4086,6 +4120,8 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.18.0
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
@@ -4300,65 +4336,32 @@ 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
|
||||
@@ -4553,6 +4556,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/dom-to-image@2.6.7': {}
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
@@ -4662,6 +4667,18 @@ snapshots:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
|
||||
ai@4.1.51(react@18.3.1)(zod@3.24.1):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.1)
|
||||
'@ai-sdk/react': 1.1.20(react@18.3.1)(zod@3.24.1)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.1)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
zod: 3.24.1
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -5022,6 +5039,8 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff@5.2.0: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
@@ -5153,6 +5172,8 @@ snapshots:
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
eventsource-parser@3.0.0: {}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
@@ -5360,20 +5381,6 @@ 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
|
||||
@@ -5515,8 +5522,16 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
chalk: 5.4.1
|
||||
diff-match-patch: 1.0.5
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -5596,11 +5611,6 @@ 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:
|
||||
@@ -6017,12 +6027,6 @@ 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
|
||||
@@ -6210,8 +6214,6 @@ snapshots:
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
property-information@7.0.0: {}
|
||||
|
||||
prosemirror-changeset@2.2.1:
|
||||
dependencies:
|
||||
prosemirror-transform: 1.10.2
|
||||
@@ -6346,15 +6348,6 @@ 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
|
||||
@@ -6388,27 +6381,17 @@ 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
|
||||
@@ -6601,6 +6584,8 @@ snapshots:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.6.3: {}
|
||||
@@ -6670,17 +6655,6 @@ 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:
|
||||
@@ -6769,6 +6743,12 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.3.2(react@18.3.1):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.4.0(react@18.3.1)
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
@@ -6806,6 +6786,8 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -6930,25 +6912,6 @@ 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
|
||||
|
||||
Binary file not shown.
@@ -458,8 +458,29 @@
|
||||
},
|
||||
"O7wjldZ3yTA2s_F-UnJw_": {
|
||||
"title": "Rate Limiting",
|
||||
"description": "Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.",
|
||||
"links": []
|
||||
"description": "Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.\n\nLearn more from the following resources:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Rate limit",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Rate_limit",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Throttle",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Throttle",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "Debounce",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Glossary/Debounce",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "What is rate limiting? | Rate limiting and bots",
|
||||
"url": "https://www.cloudflare.com/en-gb/learning/bots/what-is-rate-limiting/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"20KEgZH6cu_UokqWpV-9I": {
|
||||
"title": "Idempotency",
|
||||
@@ -569,6 +590,11 @@
|
||||
"title": "Caching REST API Response",
|
||||
"url": "https://restfulapi.net/caching/",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "HTTP caching",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
"links": [
|
||||
{
|
||||
"title": "Replace Function",
|
||||
"url": "https://support.microsoft.com/en-us/office/replace-function-6acf209b-01b7-4078-b4b8-e0a4ef67d181",
|
||||
"url": "https://support.microsoft.com/en-us/office/replace-function-8d799074-2425-4a8a-84bc-82472868878a",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -162,8 +162,14 @@
|
||||
},
|
||||
"7PBmYoSmIgZT21a2Ip3_S": {
|
||||
"title": "Trust / Influence Building",
|
||||
"description": "Building trust and influence is crucial for any Engineering Manager. This involves establishing a solid reputation, delivering on promises and being an active listener to your team's ideas and issues. It's a manager's job to ensure there's an open, honest environment that promotes trust. Balancing delegation and taking charge, especially in difficult situations, is key to building influence.\n\nOne challenge in this area is building trust between team members of varying experiences and skills. Managers must not only show the team they're competent, but also that they value everyone's inputs. They can achieve this by promoting inclusivity and praising team contributions regularly.\n\nBeing patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.",
|
||||
"links": []
|
||||
"description": "Building trust and influence is crucial for any Engineering Manager. This involves establishing a solid reputation, delivering on promises and being an active listener to your team's ideas and issues. It's a manager's job to ensure there's an open, honest environment that promotes trust. Balancing delegation and taking charge, especially in difficult situations, is key to building influence.\n\nOne challenge in this area is building trust between team members of varying experiences and skills. Managers must not only show the team they're competent, but also that they value everyone's inputs. They can achieve this by promoting inclusivity and praising team contributions regularly.\n\nBeing patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.\n\nVisit the following resources to learn more:",
|
||||
"links": [
|
||||
{
|
||||
"title": "Understanding The Trust Equation",
|
||||
"url": "https://trustedadvisor.com/why-trust-matters/understanding-trust/understanding-the-trust-equation",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
"b3qoH_LuW-Gz4N8WdGnZs": {
|
||||
"title": "One-on-One Meetings",
|
||||
|
||||
1591
public/roadmap-content/java.json
Normal file
1591
public/roadmap-content/java.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -800,11 +800,6 @@
|
||||
"title": "TypeScript Utility Types Guide",
|
||||
"url": "https://camchenry.com/blog/typescript-utility-types",
|
||||
"type": "article"
|
||||
},
|
||||
{
|
||||
"title": "TypeScript Utility Types: Key Concepts And Best Practices",
|
||||
"url": "https://marketsplash.com/tutorials/typescript/typescript-utility-types/",
|
||||
"type": "article"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 512 KiB |
181
scripts/gemini-roadmap-content.ts
Normal file
181
scripts/gemini-roadmap-content.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Edge, Node } from 'reactflow';
|
||||
import matter from 'gray-matter';
|
||||
import type { RoadmapFrontmatter } from '../src/lib/roadmap';
|
||||
import { slugify } from '../src/lib/slugger';
|
||||
import { runPromisesInBatchSequentially } from '../src/lib/promise';
|
||||
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
// ERROR: `__dirname` is not defined in ES module scope
|
||||
// https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Usage: tsx ./scripts/editor-roadmap-content.ts <roadmapId>
|
||||
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||||
console.log('GEMINI_API_KEY:', GEMINI_API_KEY);
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const google = createGoogleGenerativeAI({
|
||||
apiKey: process.env.GEMINI_API_KEY,
|
||||
});
|
||||
|
||||
const allowedRoadmapIds = await fs.readdir(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('Roadmap Id is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapFrontmatterDir = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapId,
|
||||
`${roadmapId}.md`,
|
||||
);
|
||||
const roadmapFrontmatterRaw = await fs.readFile(roadmapFrontmatterDir, 'utf-8');
|
||||
const { data } = matter(roadmapFrontmatterRaw);
|
||||
|
||||
const roadmapFrontmatter = data as RoadmapFrontmatter;
|
||||
if (!roadmapFrontmatter) {
|
||||
console.error('Invalid roadmap frontmatter');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (roadmapFrontmatter.renderer !== 'editor') {
|
||||
console.error('Only Editor Rendered Roadmaps are allowed');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDir = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapId,
|
||||
`${roadmapId}.json`,
|
||||
);
|
||||
const roadmapContent = await fs.readFile(roadmapDir, 'utf-8');
|
||||
let { nodes, edges } = JSON.parse(roadmapContent) as {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
const enrichedNodes = nodes
|
||||
.filter(
|
||||
(node) =>
|
||||
node?.type &&
|
||||
['topic', 'subtopic'].includes(node.type) &&
|
||||
node.data?.label,
|
||||
)
|
||||
.map((node) => {
|
||||
// Because we only need the parent id and title for subtopics
|
||||
if (node.type !== 'subtopic') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const parentNodeId =
|
||||
edges.find((edge) => edge.target === node.id)?.source || '';
|
||||
const parentNode = nodes.find((n) => n.id === parentNodeId);
|
||||
|
||||
return {
|
||||
...node,
|
||||
parentId: parentNodeId,
|
||||
parentTitle: parentNode?.data?.label || '',
|
||||
};
|
||||
}) as (Node & { parentId?: string; parentTitle?: string })[];
|
||||
|
||||
const roadmapContentDir = path.join(ROADMAP_CONTENT_DIR, roadmapId, 'content');
|
||||
const stats = await fs.stat(roadmapContentDir).catch(() => null);
|
||||
if (!stats || !stats.isDirectory()) {
|
||||
await fs.mkdir(roadmapContentDir, { recursive: true });
|
||||
}
|
||||
|
||||
function writeTopicContent(
|
||||
roadmapTitle: string,
|
||||
childTopic: string,
|
||||
parentTopic?: string,
|
||||
) {
|
||||
const updatedTitle = roadmapTitle.replace('Roadmap', '').trim().replace('Developer', '');
|
||||
let prompt = `I will give you a topic and you need to write a brief introduction for that in "${roadmapTitle}". Your format should be as follows and be in strictly markdown format:
|
||||
|
||||
# (Put a heading for the topic without adding parent "Subtopic in Topic" or "Topic in Roadmap" or "Subtopic under XYZ" etc.)
|
||||
|
||||
(Briefly explain the topic in one paragraph using simple english. Don't start with explaining how important the topic is with regard to "${roadmapTitle}". Don't say something along the lines of "XYZ plays a crucial role in ${roadmapTitle}". Don't include anything saying "In the context of ${roadmapTitle}". Instead, start with a simple explanation of the topic itself. For example, if the topic is "React", you can start with "React is a JavaScript library for building user interfaces."".)
|
||||
`;
|
||||
|
||||
if (!parentTopic) {
|
||||
prompt += `First topic is: ${childTopic}`;
|
||||
} else {
|
||||
prompt += `First topic is: "${parentTopic} > ${childTopic}"`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
generateText({
|
||||
model: google('gemini-2.0-flash'),
|
||||
prompt: prompt,
|
||||
providerOptions: {
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.text;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function writeNodeContent(node: Node & { parentTitle?: string }) {
|
||||
const nodeDirPattern = `${slugify(node.data.label)}@${node.id}.md`;
|
||||
if (!roadmapContentFiles.includes(nodeDirPattern)) {
|
||||
console.log(`Missing file for: ${nodeDirPattern}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeDir = path.join(roadmapContentDir, nodeDirPattern);
|
||||
const nodeContent = await fs.readFile(nodeDir, 'utf-8');
|
||||
const isFileEmpty = !nodeContent.replace(`# ${node.data.label}`, '').trim();
|
||||
if (!isFileEmpty) {
|
||||
console.log(`❌ Ignoring ${nodeDirPattern}. Not empty.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = node.data.label;
|
||||
const parentTopic = node.parentTitle;
|
||||
|
||||
console.log(`⏳ Generating content for ${topic}...`);
|
||||
let newContentFile = '';
|
||||
if (GEMINI_API_KEY) {
|
||||
newContentFile = (await writeTopicContent(
|
||||
roadmapFrontmatter.title,
|
||||
topic,
|
||||
parentTopic,
|
||||
)) as string;
|
||||
} else {
|
||||
newContentFile = `# ${topic}`;
|
||||
}
|
||||
|
||||
await fs.writeFile(nodeDir, newContentFile, 'utf-8');
|
||||
console.log(`✅ Content generated for ${topic}`);
|
||||
}
|
||||
|
||||
let roadmapContentFiles = await fs.readdir(roadmapContentDir, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
if (!GEMINI_API_KEY) {
|
||||
console.log('----------------------------------------');
|
||||
console.log('GEMINI_API_KEY not found. Skipping gemini api calls...');
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
const promises = enrichedNodes.map((node) => () => writeNodeContent(node));
|
||||
await runPromisesInBatchSequentially(promises, 20);
|
||||
console.log('✅ All content generated');
|
||||
@@ -18,16 +18,3 @@ 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;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const sidebarLinks = [
|
||||
href: '/account/update-profile',
|
||||
title: 'Profile',
|
||||
id: 'profile',
|
||||
isNew: false,
|
||||
isNew: true,
|
||||
icon: {
|
||||
glyph: 'user',
|
||||
classes: 'h-4 w-4',
|
||||
@@ -56,7 +56,7 @@ const sidebarLinks = [
|
||||
},
|
||||
{
|
||||
href: '/account/road-card',
|
||||
title: 'Road Card',
|
||||
title: 'Card',
|
||||
id: 'road-card',
|
||||
isNew: false,
|
||||
icon: {
|
||||
@@ -64,16 +64,6 @@ 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',
|
||||
@@ -107,7 +97,7 @@ const sidebarLinks = [
|
||||
}`}
|
||||
>
|
||||
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
|
||||
Teams
|
||||
Teams
|
||||
</a>
|
||||
</li>
|
||||
{
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
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,
|
||||
AlertTriangle,
|
||||
CreditCard,
|
||||
ArrowRightLeft,
|
||||
} from 'lucide-react';
|
||||
|
||||
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 {
|
||||
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 shouldHideDeleteButton =
|
||||
billingDetails?.status === 'canceled' || billingDetails?.cancelAtPeriodEnd;
|
||||
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">
|
||||
{billingDetails?.status === 'past_due' && (
|
||||
<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">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span>
|
||||
We were not able to charge your card.{' '}
|
||||
<button
|
||||
disabled={
|
||||
isCreatingCustomerPortal ||
|
||||
isCreatingCustomerPortalSuccess
|
||||
}
|
||||
onClick={() => {
|
||||
createCustomerPortal({});
|
||||
}}
|
||||
className="font-semibold underline underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Update payment information.
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">
|
||||
{!shouldHideDeleteButton && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 />;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import {
|
||||
CreditCard,
|
||||
Ellipsis,
|
||||
HeartHandshake,
|
||||
MessageCircleIcon,
|
||||
SparklesIcon,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
} 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-6 max-w-3xl">
|
||||
<h2 className="mb-6 text-2xl font-bold text-black">Subscription Details</h2>
|
||||
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
|
||||
<CreditCard className="h-8 w-8 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<h3 className="mt-4 text-xl font-semibold text-black">
|
||||
No Active Subscription
|
||||
</h3>
|
||||
|
||||
<p className="mt-2 max-w-md text-balance text-gray-600">
|
||||
Unlock premium benefits by upgrading to a subscription
|
||||
</p>
|
||||
|
||||
<div className="mt-6 w-full max-w-md rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<h4 className="mb-3 font-medium text-gray-800">Premium Benefits</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
{perks.map((perk) => (
|
||||
<div className="flex items-center gap-2 text-gray-700" key={perk.text}>
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>{perk.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onUpgrade}
|
||||
className="mt-6 inline-flex items-center justify-center rounded-md bg-black px-6 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-black/80 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
|
||||
>
|
||||
Upgrade Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import {
|
||||
Loader2,
|
||||
Zap,
|
||||
Infinity,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
Heart,
|
||||
} 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';
|
||||
|
||||
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 sm:mb-8 text-left">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-black">
|
||||
Unlock Premium Features
|
||||
</h2>
|
||||
<p className="mt-1 sm:mt-2 text-sm sm:text-base text-gray-600">
|
||||
Supercharge your learning experience with premium benefits
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 sm:mb-8 grid grid-cols-1 gap-4 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 sm:space-y-4 rounded-lg bg-white p-4 sm:p-6',
|
||||
isYearly
|
||||
? 'border-2 border-purple-400'
|
||||
: 'border border-gray-200',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-semibold text-black">
|
||||
{isYearly ? 'Yearly Payment' : 'Monthly Payment'}
|
||||
</h4>
|
||||
{isYearly && (
|
||||
<span className="text-xs sm:text-sm font-medium text-blue-600">
|
||||
(2 months free)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isYearly && (
|
||||
<span className="rounded-full bg-purple-600 px-1.5 py-0.5 sm:px-2 sm:py-1 text-xs font-semibold text-white">
|
||||
Most Popular
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-baseline">
|
||||
{isYearly && (
|
||||
<p className="mr-2 text-xs sm:text-sm text-gray-400 line-through">
|
||||
$
|
||||
{calculateYearlyPrice(
|
||||
USER_SUBSCRIPTION_PLAN_PRICES[0].amount,
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-2xl sm:text-3xl font-bold text-black">
|
||||
${plan.amount}{' '}
|
||||
<span className="text-xs sm:text-sm font-normal text-gray-500">
|
||||
/ {isYearly ? 'year' : 'month'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow"></div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className={cn(
|
||||
'flex min-h-9 sm:min-h-11 w-full items-center justify-center rounded-md py-2 sm:py-2.5 text-sm sm:text-base font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
'bg-purple-600 text-white hover:bg-purple-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 sm:h-4 sm:w-4 animate-spin" />
|
||||
) : 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">
|
||||
<div className="flex items-start space-x-2 sm:space-x-3">
|
||||
<Zap className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-purple-400" />
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-medium text-black">
|
||||
Unlimited AI Course Generations
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Generate as many custom courses as you need
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2 sm:space-x-3">
|
||||
<Infinity className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-purple-400" />
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-medium text-black">
|
||||
No Daily Limits on course features
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Use all features without restrictions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2 sm:space-x-3">
|
||||
<MessageSquare className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-purple-400" />
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-medium text-black">
|
||||
Unlimited Course Follow-ups
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Ask as many questions as you need
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2 sm:space-x-3">
|
||||
<Sparkles className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-purple-400" />
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-medium text-black">
|
||||
Early Access to Features
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Be the first to try new tools and features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2 sm:space-x-3">
|
||||
<Heart className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-purple-400" />
|
||||
<div>
|
||||
<h4 className="text-sm sm:text-base font-medium text-black">
|
||||
Support Development
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Help us continue building roadmap.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { SearchIcon, WandIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { UserCoursesList } from './UserCoursesList';
|
||||
|
||||
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 handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && keyword.trim()) {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
function onSubmit() {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}`;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { AICourseListItem } from '../../queries/ai-course';
|
||||
import type { DifficultyLevel } from './AICourse';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
|
||||
type AICourseCardProps = {
|
||||
course: AICourseListItem;
|
||||
};
|
||||
|
||||
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.progress?.done?.length || 0;
|
||||
const progressPercentage =
|
||||
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
BookOpenCheck,
|
||||
ChevronLeft,
|
||||
Loader2,
|
||||
Menu,
|
||||
X,
|
||||
CircleAlert,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { type AiCourse } from '../../lib/ai';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { slugify } from '../../lib/slugger';
|
||||
import { getAiCourseProgressOptions } from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
|
||||
import { AICourseLimit } from './AICourseLimit';
|
||||
import { AICourseModuleList } from './AICourseModuleList';
|
||||
import { AICourseModuleView } from './AICourseModuleView';
|
||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||
import { AILimitsPopup } from './AILimitsPopup';
|
||||
|
||||
type AICourseContentProps = {
|
||||
courseSlug?: string;
|
||||
course: AiCourse;
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function AICourseContent(props: AICourseContentProps) {
|
||||
const { course, courseSlug, isLoading, error } = 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' | 'full'>('full');
|
||||
|
||||
const { data: aiCourseProgress } = useQuery(
|
||||
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const [expandedModules, setExpandedModules] = useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
|
||||
const goToNextModule = () => {
|
||||
if (activeModuleIndex < course.modules.length - 1) {
|
||||
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);
|
||||
} else {
|
||||
const prevModule = course.modules[activeModuleIndex - 1];
|
||||
if (prevModule) {
|
||||
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 = aiCourseProgress?.done?.length || 0;
|
||||
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 icon = isLimitReached ? (
|
||||
<CircleAlert className="mb-4 size-16 text-yellow-500" />
|
||||
) : (
|
||||
<ErrorIcon additionalClasses="mb-4 size-16" />
|
||||
);
|
||||
const title = isLimitReached ? 'Limit Reached' : 'Error Generating Course';
|
||||
const message = isLimitReached
|
||||
? 'You have reached the daily AI usage limit. Please upgrade your account to continue.'
|
||||
: error;
|
||||
return (
|
||||
<>
|
||||
{modals}
|
||||
<div className="flex h-screen flex-col items-center justify-center px-4 text-center">
|
||||
{icon}
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<p className="my-3 max-w-sm text-balance text-gray-500">{message}</p>
|
||||
|
||||
{isLimitReached && (
|
||||
<div className="mt-4">
|
||||
<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-4 text-sm text-black">
|
||||
<a href="/ai-tutor" className="underline underline-offset-2">
|
||||
Back to AI Tutor
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
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<span className="hidden lg:inline"> to Generator</span>
|
||||
</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('full');
|
||||
}}
|
||||
>
|
||||
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('full');
|
||||
}}
|
||||
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="text-xs text-black">
|
||||
<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
|
||||
style={{
|
||||
width: `${finishedPercentage}%`,
|
||||
}}
|
||||
className={cn(
|
||||
'absolute bottom-0 left-0 top-0',
|
||||
'bg-gray-200/50',
|
||||
)}
|
||||
></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="rounded-md p-1 hover:bg-gray-100 lg:hidden"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AICourseModuleList
|
||||
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' && (
|
||||
<AICourseModuleView
|
||||
courseSlug={courseSlug!}
|
||||
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 === 'full' && (
|
||||
<div className="mx-auto rounded-xl border border-gray-200 bg-white shadow-sm lg:max-w-3xl">
|
||||
<div
|
||||
className={cn(
|
||||
'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>
|
||||
</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(courseModule.title)}__${slugify(lesson)}`;
|
||||
const isCompleted =
|
||||
aiCourseProgress?.done.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>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-10 bg-gray-900 bg-opacity-50 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
></div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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>
|
||||
<span className="max-sm:hidden">Still confused? </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>
|
||||
);
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { BookOpen, Bot, Code, HelpCircle, LockIcon, Send } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { readAICourseLessonStream } from '../../helper/read-stream';
|
||||
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import {
|
||||
markdownToHtml,
|
||||
markdownToHtmlWithHighlighting,
|
||||
} from '../../lib/markdown';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
|
||||
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 readAICourseLessonStream(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: (
|
||||
<Code className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />
|
||||
),
|
||||
title: 'Code Help',
|
||||
description: 'Share your code and ask me to help you debug it',
|
||||
},
|
||||
{
|
||||
icon: <Bot className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />,
|
||||
title: 'Best Practices',
|
||||
description: 'Share your code and ask me the best way to do something',
|
||||
},
|
||||
] as const;
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { billingDetailsOptions } from '../../queries/billing';
|
||||
import { getPercentage } from '../../helper/number';
|
||||
import { Gift, Info } from 'lucide-react';
|
||||
|
||||
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 80% of the limit
|
||||
const isNearLimit = used >= limit * 0.8;
|
||||
const isPaidUser = userBillingDetails.status !== 'none';
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react';
|
||||
import type { AiCourse } from '../../lib/ai';
|
||||
import { Check, ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { getAiCourseProgressOptions } from '../../queries/ai-course';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
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' | 'full';
|
||||
setViewMode: (mode: 'module' | 'full') => void;
|
||||
|
||||
expandedModules: Record<number, boolean>;
|
||||
setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>;
|
||||
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export function AICourseModuleList(props: AICourseModuleListProps) {
|
||||
const {
|
||||
course,
|
||||
courseSlug,
|
||||
activeModuleIndex,
|
||||
setActiveModuleIndex,
|
||||
activeLessonIndex,
|
||||
setActiveLessonIndex,
|
||||
setSidebarOpen,
|
||||
setViewMode,
|
||||
expandedModules,
|
||||
setExpandedModules,
|
||||
|
||||
isLoading,
|
||||
} = props;
|
||||
|
||||
const { data: aiCourseProgress } = useQuery(
|
||||
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
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) => {
|
||||
const key = `${slugify(courseModule.title)}__${slugify(lesson)}`;
|
||||
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(courseModule.title)}__${slugify(lesson)}`;
|
||||
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 gap-2.5 w-full cursor-pointer items-center 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>
|
||||
);
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Loader2Icon,
|
||||
LockIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { readAICourseLessonStream } from '../../helper/read-stream';
|
||||
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,
|
||||
getAiCourseProgressOptions,
|
||||
type AICourseProgressDocument,
|
||||
} from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { AICourseFollowUp } from './AICourseFollowUp';
|
||||
import './AICourseFollowUp.css';
|
||||
|
||||
type AICourseModuleViewProps = {
|
||||
courseSlug: string;
|
||||
|
||||
activeModuleIndex: number;
|
||||
totalModules: number;
|
||||
currentModuleTitle: string;
|
||||
activeLessonIndex: number;
|
||||
totalLessons: number;
|
||||
currentLessonTitle: string;
|
||||
|
||||
onGoToPrevLesson: () => void;
|
||||
onGoToNextLesson: () => void;
|
||||
|
||||
onUpgrade: () => void;
|
||||
};
|
||||
|
||||
export function AICourseModuleView(props: AICourseModuleViewProps) {
|
||||
const {
|
||||
courseSlug,
|
||||
|
||||
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 { data: aiCourseProgress } = useQuery(
|
||||
getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const lessonId = `${slugify(currentModuleTitle)}__${slugify(currentLessonTitle)}`;
|
||||
const isLessonDone = aiCourseProgress?.done.includes(lessonId);
|
||||
|
||||
const abortController = useMemo(
|
||||
() => new AbortController(),
|
||||
[activeModuleIndex, activeLessonIndex],
|
||||
);
|
||||
|
||||
const generateAiCourseContent = async () => {
|
||||
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({
|
||||
moduleTitle: currentModuleTitle,
|
||||
lessonTitle: currentLessonTitle,
|
||||
modulePosition: activeModuleIndex,
|
||||
lessonPosition: activeLessonIndex,
|
||||
totalLessonsInModule: totalLessons,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
setIsLoading(false);
|
||||
setError('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setIsGenerating(true);
|
||||
await readAICourseLessonStream(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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { mutate: toggleDone, isPending: isTogglingDone } = useMutation(
|
||||
{
|
||||
mutationFn: () => {
|
||||
return httpPatch<AICourseProgressDocument>(
|
||||
`/v1-toggle-done-ai-lesson/${courseSlug}`,
|
||||
{
|
||||
lessonId,
|
||||
},
|
||||
);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(
|
||||
['ai-course-progress', { aiCourseSlug: courseSlug }],
|
||||
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;
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<button
|
||||
disabled={isLoading || isTogglingDone}
|
||||
className={cn(
|
||||
'absolute right-3 top-3 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>
|
||||
|
||||
<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. Please upgrade
|
||||
your account to continue.
|
||||
</p>
|
||||
|
||||
<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"> Lesson</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onGoToNextLesson}
|
||||
disabled={cantGoForward}
|
||||
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',
|
||||
)}
|
||||
>
|
||||
Next <span className="hidden lg:inline"> Lesson</span>
|
||||
<ChevronRight size={16} className="ml-2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isGenerating && !isLoading && (
|
||||
<AICourseFollowUp
|
||||
courseSlug={courseSlug}
|
||||
moduleTitle={currentModuleTitle}
|
||||
lessonTitle={currentLessonTitle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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 !== 'none';
|
||||
|
||||
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)} /
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getUrlParams } from '../../lib/browser';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { generateAiCourseStructure, type AiCourse } from '../../lib/ai';
|
||||
import { readAICourseStream } from '../../helper/read-stream';
|
||||
import { AICourseContent } from './AICourseContent';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||
|
||||
type GenerateAICourseProps = {};
|
||||
|
||||
export function GenerateAICourse(props: GenerateAICourseProps) {
|
||||
const [term, setTerm] = useState('');
|
||||
const [difficulty, setDifficulty] = 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: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (term || difficulty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = getUrlParams();
|
||||
const paramsTerm = params?.term;
|
||||
const paramsDifficulty = params?.difficulty;
|
||||
if (!paramsTerm || !paramsDifficulty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTerm(paramsTerm);
|
||||
setDifficulty(paramsDifficulty);
|
||||
generateCourse({ term: paramsTerm, difficulty: paramsDifficulty });
|
||||
}, [term, difficulty]);
|
||||
|
||||
const generateCourse = async (options: {
|
||||
term: string;
|
||||
difficulty: string;
|
||||
}) => {
|
||||
const { term, difficulty } = options;
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
window.location.href = '/ai-tutor';
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setCourse({
|
||||
title: '',
|
||||
modules: [],
|
||||
difficulty: '',
|
||||
});
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keyword: term,
|
||||
difficulty,
|
||||
}),
|
||||
credentials: 'include',
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
console.error(
|
||||
'Error generating course:',
|
||||
data?.message || 'Something went wrong',
|
||||
);
|
||||
setIsLoading(false);
|
||||
setError(data?.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
console.error('Failed to get reader from response');
|
||||
setError('Something went wrong');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const COURSE_ID_REGEX = new RegExp('@COURSEID:(\\w+)@');
|
||||
const COURSE_SLUG_REGEX = new RegExp(/@COURSESLUG:([\w-]+)@/);
|
||||
|
||||
await readAICourseStream(reader, {
|
||||
onStream: (result) => {
|
||||
if (result.includes('@COURSEID') || result.includes('@COURSESLUG')) {
|
||||
const courseIdMatch = result.match(COURSE_ID_REGEX);
|
||||
const courseSlugMatch = result.match(COURSE_SLUG_REGEX);
|
||||
const extractedCourseId = courseIdMatch?.[1] || '';
|
||||
const extractedCourseSlug = courseSlugMatch?.[1] || '';
|
||||
|
||||
if (extractedCourseSlug) {
|
||||
window.history.replaceState(
|
||||
{
|
||||
courseId,
|
||||
courseSlug: extractedCourseSlug,
|
||||
term,
|
||||
difficulty,
|
||||
},
|
||||
'',
|
||||
`${origin}/ai-tutor/${extractedCourseSlug}`,
|
||||
);
|
||||
}
|
||||
|
||||
result = result
|
||||
.replace(COURSE_ID_REGEX, '')
|
||||
.replace(COURSE_SLUG_REGEX, '');
|
||||
|
||||
setCourseId(extractedCourseId);
|
||||
setCourseSlug(extractedCourseSlug);
|
||||
}
|
||||
|
||||
try {
|
||||
const aiCourse = generateAiCourseStructure(result);
|
||||
setCourse({
|
||||
...aiCourse,
|
||||
difficulty: difficulty || '',
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error parsing streamed course content:', e);
|
||||
}
|
||||
},
|
||||
onStreamEnd: (result) => {
|
||||
result = result
|
||||
.replace(COURSE_ID_REGEX, '')
|
||||
.replace(COURSE_SLUG_REGEX, '');
|
||||
setIsLoading(false);
|
||||
queryClient.invalidateQueries(getAiCourseLimitOptions());
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
setError(error?.message || 'Something went wrong');
|
||||
console.error('Error in course generation:', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
generateCourse({ term, difficulty }).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AICourseContent
|
||||
courseSlug={courseSlug}
|
||||
course={course}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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 { generateAiCourseStructure } from '../../lib/ai';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
|
||||
type GetAICourseProps = {
|
||||
courseSlug: string;
|
||||
};
|
||||
|
||||
export function GetAICourse(props: GetAICourseProps) {
|
||||
const { courseSlug } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { data: aiCourse, error } = useQuery(
|
||||
{
|
||||
...getAiCourseOptions({ aiCourseSlug: courseSlug }),
|
||||
select: (data) => {
|
||||
return {
|
||||
...data,
|
||||
course: generateAiCourseStructure(data.data),
|
||||
};
|
||||
},
|
||||
enabled: !!courseSlug && !!isLoggedIn(),
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) {
|
||||
window.location.href = '/ai-tutor';
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aiCourse) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [aiCourse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<AICourseContent
|
||||
course={{
|
||||
title: aiCourse?.title || '',
|
||||
modules: aiCourse?.course.modules || [],
|
||||
difficulty: aiCourse?.difficulty || 'Easy',
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
courseSlug={courseSlug}
|
||||
error={error?.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
getAiCourseLimitOptions,
|
||||
listUserAiCoursesOptions,
|
||||
} from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { AICourseCard } from './AICourseCard';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Gift, Loader2, Search, User2 } from 'lucide-react';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { billingDetailsOptions } from '../../queries/billing';
|
||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||
|
||||
type UserCoursesListProps = {};
|
||||
|
||||
export function UserCoursesList(props: UserCoursesListProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
|
||||
|
||||
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 !== 'none';
|
||||
|
||||
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
|
||||
listUserAiCoursesOptions(),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsInitialLoading(false);
|
||||
}, [userAiCourses]);
|
||||
|
||||
const filteredCourses = userAiCourses?.filter((course) => {
|
||||
if (!searchTerm.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
return (
|
||||
course.title.toLowerCase().includes(searchLower) ||
|
||||
course.keyword.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
const isAuthenticated = isLoggedIn();
|
||||
|
||||
const canSearch =
|
||||
!isInitialLoading &&
|
||||
!isUserAiCoursesLoading &&
|
||||
isAuthenticated &&
|
||||
userAiCourses?.length !== 0;
|
||||
|
||||
const limitUsedPercentage = Math.round((used / limit) * 100);
|
||||
|
||||
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">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 opacity-0 transition-opacity',
|
||||
{
|
||||
'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>
|
||||
|
||||
<div className={cn('relative w-64 max-sm:hidden', {})}>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Search 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 transition-all 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>
|
||||
</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 &&
|
||||
userAiCourses?.length === 0 && (
|
||||
<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 &&
|
||||
filteredCourses &&
|
||||
filteredCourses.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredCourses.map((course) => (
|
||||
<AICourseCard key={course._id} course={course} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isUserAiCoursesLoading &&
|
||||
(userAiCourses?.length || 0 > 0) &&
|
||||
filteredCourses?.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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 w-full items-center justify-between gap-2 border-b bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden"
|
||||
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"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{relatedTitle}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
|
||||
isEmpty={!isLoading && progress.length === 0}
|
||||
emptyTitle={
|
||||
<>
|
||||
No bookmars found
|
||||
No bookmarks found
|
||||
<a
|
||||
href="#role-based-roadmaps"
|
||||
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
|
||||
|
||||
@@ -10,8 +10,11 @@ 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 gap-2 text-center sm:justify-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 sm:justify-center gap-2 text-center sm:gap-4">
|
||||
<span className="flex items-center gap-1.5 text-xs font-medium text-black md:text-base">
|
||||
<Database className="hidden h-4 w-4 flex-shrink-0 text-black sm:block" />
|
||||
<span className="hidden sm:block">
|
||||
@@ -19,7 +22,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-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-sm text-xs font-medium uppercase tracking-wide text-white hover:bg-zinc-800 sm:px-3 sm:py-1">
|
||||
<span className="mr-1.5 hidden sm:inline">Start Learning</span>
|
||||
<span className="mr-1.5 inline sm:hidden">Visit</span>
|
||||
<span className="">→</span>
|
||||
|
||||
@@ -311,7 +311,7 @@ export function SQLCoursePage() {
|
||||
|
||||
<SectionHeader
|
||||
title="Not your average SQL course"
|
||||
description="Built around a text-based interactive approach and packed with practical challenges, this course stands out with features that make it truly unique."
|
||||
description="This SQL programming class is designed to help you go from beginner to expert through hands-on practice with real-world scenarios, mastering everything from basic to complex queries."
|
||||
className="mt-16 md:mt-20"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# Rate Limiting in API Design
|
||||
|
||||
Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.
|
||||
Rate Limiting is a critical aspect of API Design that dictates the number of API calls a client can make within a specified timeframe. This helps in managing resource allocation, preventing abuse of the API, and maintaining the overall health of the API system. Proper rate limiting measures should be in place to ensure the API's stability, thereby delivering a consistent and reliable service to all consumers. It works primarily by setting a limit on the frequency of client requests, thereby preventing individual users from overloading the system. It is crucial to design and implement rate limiting carefully for maintaining API availability and performance.
|
||||
|
||||
Learn more from the following resources:
|
||||
|
||||
- [@article@Rate limit](https://developer.mozilla.org/en-US/docs/Glossary/Rate_limit)
|
||||
- [@article@Throttle](https://developer.mozilla.org/en-US/docs/Glossary/Throttle)
|
||||
- [@article@Debounce](https://developer.mozilla.org/en-US/docs/Glossary/Debounce)
|
||||
- [@article@What is rate limiting? | Rate limiting and bots](https://www.cloudflare.com/en-gb/learning/bots/what-is-rate-limiting/)
|
||||
|
||||
@@ -8,5 +8,5 @@ The REPLACE function is used to replace part of a text string with another text
|
||||
|
||||
Learn more from the following resources:
|
||||
|
||||
- [@article@Replace Function](https://support.microsoft.com/en-us/office/replace-function-6acf209b-01b7-4078-b4b8-e0a4ef67d181)
|
||||
- [@article@Replace Function](https://support.microsoft.com/en-us/office/replace-function-8d799074-2425-4a8a-84bc-82472868878a)
|
||||
- [@article@Substitute Function](https://support.microsoft.com/en-us/office/substitute-function-6434944e-a904-4336-a9b0-1e58df3bc332)
|
||||
|
||||
@@ -4,4 +4,8 @@ Building trust and influence is crucial for any Engineering Manager. This involv
|
||||
|
||||
One challenge in this area is building trust between team members of varying experiences and skills. Managers must not only show the team they're competent, but also that they value everyone's inputs. They can achieve this by promoting inclusivity and praising team contributions regularly.
|
||||
|
||||
Being patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.
|
||||
Being patient, communicate clearly, and showing empathy are critical skills that can help an Engineering Manager in trust and influence building. By embodying these traits, managers can build a stronger, united, and more effective engineering team.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Understanding The Trust Equation](https://trustedadvisor.com/why-trust-matters/understanding-trust/understanding-the-trust-equation)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Functions
|
||||
|
||||
A method/function is a way to perform some task. Similarly, in programming like Java, a function method is a block of code written to perform a specific task repeatedly. It provides reusability of code. We write the function once and use it many times. It works on the 'DRY' principle i.e., "Do not repeat yourself".
|
||||
|
||||
Steps -
|
||||
|
||||
1. Define function - `datatype function_name(parameters){body}`
|
||||
2. Call function - `function_name(values)`
|
||||
3. Lambda functions - `x -> x + 1`
|
||||
4. Pass a function as a variable -
|
||||
```
|
||||
final Consumer<Integer> simpleReference1 = App::someMethod1;
|
||||
simpleReference1.accept(1);
|
||||
```
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Methods/Functions in Java.](https://www.javatpoint.com/method-in-java)
|
||||
- [@article@Learn Functions/Methods in Java](https://www.w3schools.com/java/java_methods.asp)
|
||||
- [@video@Functions / Methods in Java](https://www.youtube.com/watch?v=vvanI8NRlSI)
|
||||
- [@article@Lambda functions](https://www.w3schools.com/java/java_lambda.asp)
|
||||
- [@article@Passing functions as a variable](https://northcoder.com/post/passing-java-functions-in-variables/)
|
||||
@@ -1,14 +0,0 @@
|
||||
# Datastructures
|
||||
|
||||
As the name indicates itself, a **Data Structure** is a way of organizing the data in the **memory** so that it can be used efficiently. Some common data structures are array, linked list, stack, hashtable, queue, tree, heap, and graph.
|
||||
|
||||
- Array allocates continuous memory for homogeneous data
|
||||
- Linked List stores data in nodes with references
|
||||
- Stack follows Last In First Out principle
|
||||
- Queue follows First In First Out principle
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Data Structures and Algorithms](https://www.javatpoint.com/data-structure-tutorial)
|
||||
- [@video@Java + DSA + Interview Preparation Course](https://youtube.com/playlist?list=PL9gnSGHSqcnr_DxHsP7AW9ftq0AtAyYqJ)
|
||||
- [@video@Data Structures Illustrated](https://www.youtube.com/watch?v=9rhT3P1MDHk&list=PLkZYeFmDuaN2-KUIv-mvbjfKszIGJ4FaY)
|
||||
@@ -1,20 +0,0 @@
|
||||
# OOP
|
||||
|
||||
Object-oriented programming (OOP) is a programming paradigm based on the concept of objects, which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods). In OOP, computer programs are designed by making them out of objects that interact with one another.
|
||||
|
||||
Abstraction, encapsulation, polymorphism, and inheritance are the four main theoretical principles of object-oriented programming. But Java also works with three further OOP concepts: association, aggregation, and composition.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Class](https://www.javatpoint.com/object-and-class-in-java)
|
||||
- [@article@Inheritance](https://www.javatpoint.com/inheritance-in-java)
|
||||
- [@article@Polymorphism](https://www.javatpoint.com/runtime-polymorphism-in-java)
|
||||
- [@article@Abstraction](https://www.softwaretestinghelp.com/what-is-abstraction-in-java/)
|
||||
- [@article@Encapsulation](https://www.programiz.com/java-programming/encapsulation)
|
||||
- [@article@Association](https://www.javatpoint.com/association-in-java)
|
||||
- [@article@Aggregation](https://www.javatpoint.com/aggregation-in-java)
|
||||
- [@article@Composition](https://www.geeksforgeeks.org/composition-in-java/)
|
||||
- [@article@Java OOPs Concepts](https://www.javatpoint.com/java-oops-concepts)
|
||||
- [@article@Using OOP concepts to write high-performance Java code](https://raygun.com/blog/oop-concepts-java)
|
||||
- [@video@Java complete OOPs playlist](https://youtube.com/playlist?list=PL9gnSGHSqcno1G3XjUbwzXHL8_EttOuKk)
|
||||
- [@video@Java OOPs Concepts](https://www.youtube.com/watch?v=6T_HgnjoYwM)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Memory Management
|
||||
|
||||
In Java, memory management is the process of allocation and de-allocation of objects.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Memory Management in Java](https://www.javatpoint.com/memory-management-in-java)
|
||||
- [@video@Memory Management Tutorial in Java](https://www.youtube.com/watch?v=fM8yj93X80s)
|
||||
- [@video@Memory Managament And Garbage Collection in Java](https://youtu.be/vz6vSZRuS2M?si=4-JyoDkgcxrLmxSt)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Collection Framework
|
||||
|
||||
The Collection in Java is a framework that provides an architecture to store and manipulate the group of objects. Java Collections can achieve all the operations that you perform on a data such as searching, sorting, insertion, manipulation, and deletion.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Collections in Java](https://www.javatpoint.com/collections-in-java)
|
||||
- [@article@Java - Collections Framework](https://www.tutorialspoint.com/java/java_collections.htm)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Serialization
|
||||
|
||||
Serialization is the conversion of the state of an object into a byte stream; deserialization does the opposite. Stated differently, serialization is the conversion of a Java object into a static stream (sequence) of bytes, which we can then save to a database or transfer over a network.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Serialization and Deserialization in Java](https://www.javatpoint.com/serialization-in-java)
|
||||
- [@article@Introduction to Java Serialization](https://www.baeldung.com/java-serialization)
|
||||
@@ -1,12 +0,0 @@
|
||||
# How JVM Works
|
||||
|
||||
The Java Virtual Machine (JVM) is a platform-dependent program that executes platform-independent Java bytecode, embodying the "Write once, run everywhere" principle. As a crucial component of the Java Runtime Environment (JRE), the JVM is responsible for initiating Java programs by calling their main method. While designed primarily for Java, the JVM's ability to run any language compiled to Java bytecode has led to its adoption by other languages like Kotlin, Scala, and Groovy. Multiple JVM implementations exist, with Oracle's HotSpot being the standard and GraalVM emerging as a high-performance alternative, each offering unique features and optimizations
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Introducing the Java Virtual Machine](https://www.infoworld.com/article/3272244/what-is-the-jvm-introducing-the-java-virtual-machine.html)
|
||||
- [@video@How JVM works?](https://youtu.be/G1ubVOl9IBw)
|
||||
- [@article@JVM languages](https://www.whizlabs.com/blog/jvm-languages/)
|
||||
- [@article@GraalVM](https://www.graalvm.org/)
|
||||
- [@feed@Explore top posts about JVM](https://app.daily.dev/tags/jvm?ref=roadmapsh)
|
||||
- [@video@JVM Architecture in 8min](https://www.youtube.com/watch?v=QHIWkwxs0AI)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Garbage Collection
|
||||
|
||||
Java garbage collection is the process by which Java programs perform automatic memory management. Java programs compile to bytecode that can be run on a Java Virtual Machine, or JVM for short. When Java programs run on the JVM, objects are created on the heap, which is a portion of memory dedicated to the program
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Garbage Collection](https://stackify.com/what-is-java-garbage-collection/)
|
||||
- [@article@Java Garbage Collection](https://www.javatpoint.com/Garbage-Collection)
|
||||
- [@video@Garbage Collection in Java - Geekific](https://www.youtube.com/watch?v=XXOaCV5xm9s)
|
||||
@@ -1,10 +0,0 @@
|
||||
# Java Advanced Topics
|
||||
|
||||
Java Advanced Topics delves into crucial concepts that every Java developer should master to build efficient and scalable applications. It covers memory management techniques, deep dives into the collection framework, and explores serialization for data persistence. Networking and sockets are discussed to help you build robust communication systems, while an understanding of how the JVM works and garbage collection mechanisms ensures optimal application performance. Additionally, the basics of threading introduce parallelism, crucial for modern multi-threaded programming.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@What is Advanced Java? - GeeksForGeeks](https://www.geeksforgeeks.org/what-is-advanced-java/)
|
||||
- [@article@What is Advance Java? - JavatPoint](https://www.javatpoint.com/what-is-advance-java)
|
||||
- [@video@Advance Java Full Course 2023 - SimpliLearn](https://youtu.be/Ae-r8hsbPUo?si=faFsqJYfCc5jIO6p)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Ant
|
||||
|
||||
Apache Ant is a Java library and command-line tool whose mission is to drive processes described in build files as targets and extension points dependent upon each other. The main known usage of Ant is the build of Java applications. Ant supplies a number of built-in tasks allowing to compile, assemble, test and run Java applications. Ant can also be used effectively to build non Java applications, for instance C or C++ applications. More generally, Ant can be used to pilot any type of process which can be described in terms of targets and tasks.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Apache Ant](https://ant.apache.org/)
|
||||
- [@article@Apache Ant Tutorial](https://www.javatpoint.com/apache-ant-tutorial)
|
||||
- [@article@Apache Ant](https://en.wikipedia.org/wiki/Apache_Ant)
|
||||
- [@video@What is Apache Ant?](https://youtu.be/3rizinq7bng)
|
||||
@@ -1,14 +0,0 @@
|
||||
# Spring
|
||||
|
||||
Spring is a powerful open-source Java platform (framework), that is used to create and maintain web applications. It starts as the Spring Framework providing a Dependency Injection Container.
|
||||
Spring Boot is an autoconfigurable packaging of multiple Spring projects (like Data, MVC, REST etc) initially created for creating microservices or quick PoC (Proof of concepts)
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Official Site](https://spring.io/)
|
||||
- [@official@Quickstart guide](https://spring.io/quickstart)
|
||||
- [@official@Official guides](https://spring.io/guides)
|
||||
- [@article@Spring Framework Documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/)
|
||||
- [@article@Spring Boot tutorials](https://www.baeldung.com/spring-boot)
|
||||
- [@article@What is Spring Framework? An Unorthodox Guide](https://www.marcobehler.com/guides/spring-framework)
|
||||
- [@feed@Explore top posts about Spring Framework](https://app.daily.dev/tags/spring?ref=roadmapsh)
|
||||
@@ -1,10 +0,0 @@
|
||||
# Spark
|
||||
|
||||
Spark is a micro framework for creating web applications in Kotlin and Java 8. Sinatra, a popular Ruby micro framework, was the inspiration for it.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Spark Java](https://sparkjava.com/)
|
||||
- [@article@Intro to Spark Java Framework](https://www.baeldung.com/spark-framework-rest-api)
|
||||
- [@article@What is Spark java?](https://www.javatpoint.com/spark-java)
|
||||
- [@feed@Explore top posts about Apache Spark](https://app.daily.dev/tags/spark?ref=roadmapsh)
|
||||
@@ -1,20 +0,0 @@
|
||||
# JPA
|
||||
|
||||
The Jakarta Persistence API provides Java developers with an object/relational mapping facility for managing relational data in Java applications. JPA is not a tool nor a framework, but a set of interfaces for accessing, persisting, and managing data between Java objects and (a) relational database. Because it is a set of interfaces, it will require an implementation to work with and persist Java objects. This will be ORM. Here are the main features of JPA:
|
||||
|
||||
- Cleaner, easier, standardized ORM.
|
||||
- Supports inheritance, polymorphism, and polymorphic queries.
|
||||
- Supports metadata annotations/XML descriptors to define the mapping (between objects and relational database).
|
||||
- Supports a rich, SQL-like query language for static and dynamic queries.
|
||||
- Pluggable persistence providers like Hibernate, MyBatis, etc.
|
||||
- Caching: JPA supports 2 kinds of cache - first and second levels - to support performance tuning.
|
||||
|
||||
> Note: In 2019, JPA was renamed from Java Persistence API to Jakarta Persistence.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@TutorialsPoint JPA](https://www.tutorialspoint.com/jpa/jpa_architecture.htm)
|
||||
- [@article@Official Java doc - Package javax.persistence](https://docs.oracle.com/javaee/7/api/javax/persistence/package-summary.html)
|
||||
- [@article@Pro Jakarta Persistence in Jakarta EE 10](https://www.amazon.com/Pro-Jakarta-Persistence-Depth-Development/dp/1484274423)
|
||||
- [@article@Java Persistence with Spring Data and Hibernate by Catalin Tudose](https://www.simonandschuster.com/books/Java-Persistence-with-Spring-Data-and-Hibernate/Catalin-Tudose/9781617299186)
|
||||
- [@feed@Explore top posts about Java](https://app.daily.dev/tags/java?ref=roadmapsh)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Jdbi3
|
||||
|
||||
Jdbi is an open source Java library (Apache license) that uses lambda expressions and reflection to provide a friendlier, higher level interface than JDBC to access the database.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Jdbi](https://jdbi.org/)
|
||||
- [@article@Jdbi Tutorial](https://www.baeldung.com/jdbi)
|
||||
@@ -1,8 +0,0 @@
|
||||
# JDBC Template
|
||||
|
||||
JDBCTemplate is a central class in Spring's JDBC core package that simplifies the use of JDBC and helps to avoid common errors. It internally uses JDBC API and eliminates many problems with JDBC API. It executes SQL queries or updates, initiating iteration over ResultSets, catching JDBC exceptions, and translating them to the generic. It executes core JDBC workflow, leaving application code to provide SQL and extract results. It handles the exception and provides informative exception messages with the help of exception classes defined in the `org.springframework.dao` package.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@JDBC Template tutorial](https://www.baeldung.com/spring-jdbc-jdbctemplate)
|
||||
- [@feed@Explore top posts about Java](https://app.daily.dev/tags/java?ref=roadmapsh)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Cukes
|
||||
|
||||
cukes-rest takes simplicity of Cucumber and provides bindings for HTTP specification. As a sugar on top, cukes-rest adds steps for storing and using request/response content from a file system, variable support in .features, context inflation in all steps and a custom plug-in system to allow users to add additional project specific content.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@opensource@Cukes Github](https://github.com/ctco/cukes)
|
||||
- [@article@Getting Started with Cukes-REST](https://speakerdeck.com/larchaon/getting-started-with-cukes-rest?slide=23)
|
||||
@@ -1,8 +0,0 @@
|
||||
# Jbehave
|
||||
|
||||
JBehave is a framework for Behaviour-Driven Development (BDD). BDD is an evolution of test-driven development (TDD) and acceptance-test driven design, and is intended to make these practices more accessible and intuitive to newcomers and experts alike. It shifts the vocabulary from being test-based to behaviour-based, and positions itself as a design philosophy.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Jbehave](https://jbehave.org/)
|
||||
- [@official@Jbehave Tutorial](https://jbehave.org/tutorials.html)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Abstraction
|
||||
|
||||
The abstract keyword in Java is used to declare a class or a method that cannot be instantiated directly or must be implemented by subclasses, respectively. It is a key part of Java's abstraction mechanism, allowing developers to define abstract classes and methods that provide a blueprint for other classes.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Abstract Classes](https://jenkov.com/tutorials/java/abstract-classes.html)
|
||||
- [@article@Java Interfaces vs. Abstract Classes](https://jenkov.com/tutorials/java/interfaces-vs-abstract-classes.html)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Access Specifiers
|
||||
|
||||
Access specifiers (or access modifiers) in Java are keywords that control the visibility or accessibility of classes, methods, constructors, and other members. They determine from where these members can be accessed. Java provides four access specifiers: `private`, `default` (no keyword), `protected`, and `public`, each offering a different level of access control.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Access Modifiers](https://jenkov.com/tutorials/java/access-modifiers.html)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Annotations
|
||||
|
||||
Annotations are a form of metadata that provide data about a program. They are used to provide supplemental information about the code, but they are not a part of the program itself. Annotations can be used by the compiler to detect errors or suppress warnings, and they can also be used at runtime to modify the behavior of the program.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Annotations Tutorial](https://jenkov.com/tutorials/java/annotations.html)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Array vs ArrayList
|
||||
|
||||
Arrays and ArrayLists are both ways to store collections of elements in Java. An array is a fixed-size, ordered sequence of elements of the same data type. Once you declare its size, you cannot change it. An ArrayList, on the other hand, is a dynamic, resizable array implementation. It can grow or shrink as needed, allowing you to add or remove elements without worrying about the initial size.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Arrays](https://jenkov.com/tutorials/java/arrays.html)
|
||||
- [@article@Java ArrayLists](https://jenkov.com/tutorials/java-collections/list.html)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Arrays
|
||||
|
||||
Arrays are fundamental data structures used to store a collection of elements of the same data type in contiguous memory locations. They provide a way to organize and access multiple values using a single variable name and an index. Each element in an array can be accessed directly using its index, starting from 0.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Arrays](https://jenkov.com/tutorials/java/arrays.html)
|
||||
- [@video@Java Arrays Tutorial](https://www.youtube.com/watch?v=ei_4Nt7XWOw)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Attributes and Methods
|
||||
|
||||
Attributes are variables that hold data about an object, defining its state or characteristics. Methods are functions that define the behavior of an object, allowing it to perform actions or operations. Together, attributes and methods encapsulate the data and behavior of an object within a class.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Classes](https://jenkov.com/tutorials/java/classes.html)
|
||||
- [@article@Java Methods](https://jenkov.com/tutorials/java/methods.html)
|
||||
- [@article@Java Properties](https://jenkov.com/tutorials/java-collections/properties.html)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Basics of OOP
|
||||
|
||||
Object-Oriented Programming (OOP) is a programming paradigm centered around "objects," which contain data in the form of fields (attributes) and code in the form of procedures (methods). OOP focuses on creating reusable code by grouping related data and behavior into objects, allowing for modularity, abstraction, inheritance, and polymorphism. These concepts help in organizing and structuring code in a way that mirrors real-world entities and their interactions.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Classes and Objects](https://jenkov.com/tutorials/java/classes.html)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Bazel
|
||||
|
||||
Bazel is an open-source build and test tool similar to Make, Maven, and Gradle. It uses a human-readable, high-level build language. Bazel supports projects in multiple languages and builds outputs for multiple platforms. It's designed for fast, reliable, and reproducible builds, making it suitable for large codebases and complex projects.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Getting started with Bazel](https://bazel.build/start)
|
||||
- [@article@Build Java Projects with Bazel](https://earthly.dev/blog/build-java-projects-with-bazel/)
|
||||
- [@article@Introduction to the Bazel Build Tool](https://www.baeldung.com/bazel-build-tool)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Classes and Objects
|
||||
|
||||
Classes are blueprints for creating objects, which are instances of those classes. A class defines the characteristics (attributes) and behaviors (methods) that objects of that class will possess. Think of a class as a template and an object as a specific instance created from that template.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Class and Objects](https://www.programiz.com/java-programming/class-objects)
|
||||
- [@article@Java Classes and Objects](https://www.youtube.com/watch?v=IUqKuGNasdM)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Concurrency
|
||||
|
||||
Concurrency is the ability of a program to execute multiple tasks seemingly simultaneously. This doesn't necessarily mean they are running at the exact same instant, but rather that their execution overlaps in time. This can be achieved through techniques like multithreading, where a single program is divided into multiple threads that can run concurrently, or through asynchronous programming, where tasks can be started and then the program can continue executing other tasks without waiting for the first task to complete.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Concurrency and Multithreading Tutorial](https://jenkov.com/tutorials/java-concurrency/index.html)
|
||||
- [@article@Java Concurrency in Practice](https://www.baeldung.com/java-concurrency)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Cryptography
|
||||
|
||||
Cryptography is the practice and study of techniques for secure communication in the presence of adversaries. It involves converting readable data (plaintext) into an unreadable format (ciphertext) through encryption, and then converting the ciphertext back into plaintext through decryption. Cryptography uses algorithms and keys to ensure confidentiality, integrity, authentication, and non-repudiation of information.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Cryptography Tutorial](https://jenkov.com/tutorials/java-cryptography/index.html)
|
||||
|
||||
@@ -9,8 +9,6 @@ Data Types are divided into two group -
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@What are Data Types & Variables?](https://www.guru99.com/java-variables.html)
|
||||
- [@article@Java Data Types](https://jenkov.com/tutorials/java/variables.html)
|
||||
- [@article@What are Data Types & Variables?](https://jenkov.com/tutorials/java/data-types.html)
|
||||
- [@article@Java Variables](https://www.javatpoint.com/java-variables)
|
||||
- [@article@Learn more about Data types and Variables](https://www.javatpoint.com/java-data-types)
|
||||
- [@article@Java enums](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html)
|
||||
- [@article@Java Records](https://www.baeldung.com/java-record-keyword)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Dependency Injection
|
||||
|
||||
Dependency Injection (DI) is a design pattern where objects receive their dependencies from external sources rather than creating them themselves. This means a class doesn't have to worry about how to obtain the objects it needs to function; instead, those objects are "injected" into the class, usually through its constructor, setter methods, or interface. This promotes loose coupling and makes code more testable and maintainable.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Dependency Injection Tutorial](https://jenkov.com/tutorials/dependency-injection/index.html)
|
||||
- [@article@Java Dependency Injection Design Pattern Example Tutorial](https://www.digitalocean.com/community/tutorials/java-dependency-injection-design-pattern-example-tutorial)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Dequeue
|
||||
|
||||
A Dequeue (pronounced "dee-queue") is a double-ended queue, a data structure that allows you to add and remove elements from both the front (head) and the back (tail) of the queue. Unlike a regular queue (FIFO - First-In, First-Out), a dequeue provides flexibility for both FIFO and LIFO (Last-In, First-Out) operations. This makes it useful for implementing various algorithms and data management tasks where elements need to be accessed or modified from either end.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Deque Tutorial](https://jenkov.com/tutorials/java-collections/deque.html)
|
||||
- [@article@Java Deque](https://www.programiz.com/java-programming/deque)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Encapsulation
|
||||
|
||||
Encapsulation is a fundamental concept in object-oriented programming where data and the methods that operate on that data are bundled together as a single unit. This unit, often a class, hides the internal state of the object from the outside world and only exposes a controlled interface for interacting with it. This protects the data from accidental modification and allows for easier maintenance and modification of the code.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java - Encapsulation](https://www.tutorialspoint.com/java/java_encapsulation.htm)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Enums
|
||||
|
||||
Enums, short for enumerations, are a special data type in Java that represent a group of named constants. They allow you to define a type that can only take on a specific set of predefined values. This makes your code more readable and less prone to errors by restricting the possible values a variable can hold.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Enums](https://jenkov.com/tutorials/java/enums.html)
|
||||
- [@article@Java Enums](https://www.programiz.com/java-programming/enums)
|
||||
@@ -11,5 +11,4 @@ There are three types of exceptions -
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@video@Understanding Java Exceptions](https://www.youtube.com/watch?v=W-N2ltgU-X4)
|
||||
- [@article@Mastering Java Exception Handling: A Comprehensive Guide](https://naveen-metta.medium.com/mastering-java-exception-handling-a-comprehensive-guide-a897b8020582)
|
||||
- [@article@Exception Handling in Java](https://www.javatpoint.com/exception-handling-in-java)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Final Keyword
|
||||
|
||||
The `final` keyword in Java is a non-access modifier used to apply restrictions on a variable, method, or class. When applied to a variable, it makes the variable's value constant after initialization. When applied to a method, it prevents the method from being overridden in a subclass. When applied to a class, it prevents the class from being subclassed (inherited).
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Final Keyword](https://www.baeldung.com/java-final)
|
||||
- [@article@How does the final keyword in Java work? I can still modify an object](https://stackoverflow.com/questions/15655012/how-does-the-final-keyword-in-java-work-i-can-still-modify-an-object)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Functional Composition
|
||||
|
||||
Functional composition is the process of combining two or more functions to produce a new function. The resulting function applies each function in order, passing the output of one function as the input to the next. This allows you to build complex operations by chaining together simpler, reusable functions.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Functional Composition in Java](https://jenkov.com/tutorials/java-functional-programming/functional-composition.html)
|
||||
- [@article@Java Functional Programming](https://www.baeldung.com/java-functional-programming)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Functional Interfaces
|
||||
|
||||
Functional interfaces are interfaces that contain only one abstract method. They can have multiple default or static methods, but only one method that needs to be implemented. These interfaces can be used with lambda expressions and method references, allowing for concise and readable code when dealing with single-method operations.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Functional Interfaces](https://jenkov.com/tutorials/java-functional-programming/functional-interfaces.html)
|
||||
- [@article@Java Functional Interfaces](https://www.baeldung.com/java-8-functional-interfaces)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# High Order Functions
|
||||
|
||||
High Order Functions are functions that can either accept other functions as arguments or return functions as their results. This capability allows for more flexible and reusable code by enabling you to abstract over operations. Essentially, you can pass behavior as data, making your code more dynamic and adaptable to different situations.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java High Order Functions](https://jenkov.com/tutorials/java-functional-programming/higher-order-functions.html)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Inheritance
|
||||
|
||||
Inheritance is a fundamental concept in object-oriented programming where a new class (called a subclass or derived class) acquires properties and behaviors from an existing class (called a superclass or base class). This allows for code reuse and the creation of hierarchical relationships between classes, promoting a more organized and maintainable codebase. The subclass can extend the superclass by adding new attributes and methods or overriding existing ones.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Inheritance](https://jenkov.com/tutorials/java/inheritance.html)
|
||||
- [@article@Inheritance in Java with Example](https://www.digitalocean.com/community/tutorials/inheritance-java-example)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Initializer Block
|
||||
|
||||
An initializer block in Java is a block of code, enclosed in curly braces `{}` , that is executed when an instance of a class is created. It's used to initialize instance variables or perform setup tasks before the constructor is called. There are two types: instance initializer blocks, which run every time a new object is created, and static initializer blocks, which run only once when the class is first loaded.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Static and Instance Initializer Blocks in Java](https://www.baeldung.com/java-static-instance-initializer-blocks)
|
||||
- [@article@All About Java Instance Initializer Blocks](https://blogs.oracle.com/javamagazine/post/java-instance-initializer-block)
|
||||
- [@article@What is an initialization block?](https://stackoverflow.com/questions/3987428/what-is-an-initialization-block)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Interfaces
|
||||
|
||||
An interface in Java is a blueprint of a class. It specifies a set of methods that a class must implement if it claims to implement the interface. Think of it as a contract: any class that "signs" the contract (implements the interface) agrees to provide specific behaviors (methods). Interfaces can also contain constants (static final variables). They help achieve abstraction and multiple inheritance in Java.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Interfaces in Java](https://jenkov.com/tutorials/java/interfaces.html)
|
||||
- [@article@A Guide to Java Interfaces](https://www.baeldung.com/java-interfaces)
|
||||
@@ -0,0 +1,7 @@
|
||||
# I/O Operations
|
||||
|
||||
I/O Operations, short for Input/Output Operations, deal with how a program interacts with the outside world. This involves reading data from sources like files, network connections, or the keyboard, and writing data to destinations such as files, the console, or network sockets. Essentially, it's the mechanism by which a program receives information and sends results.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java IO Tutorial](https://jenkov.com/tutorials/java-io/index.html)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Iterator
|
||||
|
||||
An Iterator is an object that enables you to traverse through a collection (like a List or Set) one element at a time. It provides a standard way to access elements sequentially without needing to know the underlying structure of the collection. You can use methods like `hasNext()` to check if there's a next element and `next()` to retrieve it.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Iterator Tutorial](https://jenkov.com/tutorials/java-collections/iterator.html)
|
||||
- [@article@Java Iterable Tutorial](https://jenkov.com/tutorials/java-collections/iterable.html)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Java Memory Model
|
||||
|
||||
The Java Memory Model (JMM) defines how threads in Java interact with memory. It specifies how and when different threads can see writes to shared variables, addressing issues like data visibility and race conditions in concurrent programs. The JMM ensures that multithreaded Java programs behave predictably across different hardware architectures by establishing rules for memory synchronization and ordering.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@article@Java Memory Model](https://jenkov.com/tutorials/java-concurrency/java-memory-model.html)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Javalin
|
||||
|
||||
Javalin is a lightweight web framework for Java and Kotlin that's designed to be simple, intuitive, and fun to use. It allows developers to quickly build web applications and APIs with minimal boilerplate code. Javalin focuses on providing a straightforward approach to routing, request handling, and response generation, making it a good choice for projects where speed of development and ease of understanding are important.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [@official@Javalin Website](https://javalin.io/)
|
||||
- [@article@Creating a REST API with Javalin](https://www.baeldung.com/javalin-rest-microservices)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user