Mark favorite in the Roadmap's page (#4098)

* chore: favorite in roadmap header

* chore: best practices header

* chore: mark favorite

* fix: bookmark position

* UI changes and fix

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
pull/4104/head
Arik Chakma 1 year ago committed by GitHub
parent ff0e10c16c
commit 9c246984d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      package.json
  2. 151
      pnpm-lock.yaml
  3. 69
      src/components/BestPracticeHeader.astro
  4. 21
      src/components/FeaturedItems/MarkFavorite.tsx
  5. 78
      src/components/RoadmapHeader.astro
  6. 32
      src/lib/resource-progress.ts

@ -26,11 +26,11 @@
"@astrojs/tailwind": "^3.1.3", "@astrojs/tailwind": "^3.1.3",
"@fingerprintjs/fingerprintjs": "^3.4.1", "@fingerprintjs/fingerprintjs": "^3.4.1",
"@nanostores/preact": "^0.5.0", "@nanostores/preact": "^0.5.0",
"astro": "^2.6.3", "astro": "^2.6.6",
"astro-compress": "^1.1.47", "astro-compress": "^1.1.47",
"jose": "^4.14.4", "jose": "^4.14.4",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"nanostores": "^0.9.1", "nanostores": "^0.9.2",
"node-html-parser": "^6.1.5", "node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12", "npm-check-updates": "^16.10.12",
"preact": "^10.15.1", "preact": "^10.15.1",
@ -39,14 +39,14 @@
"tailwindcss": "^3.3.2" "tailwindcss": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.35.0", "@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3", "@types/js-cookie": "^3.0.3",
"csv-parser": "^3.0.0", "csv-parser": "^3.0.0",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"openai": "^3.2.1", "openai": "^3.3.0",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-astro": "^0.10.0", "prettier-plugin-astro": "^0.10.0",
"prettier-plugin-tailwindcss": "^0.3.0" "prettier-plugin-tailwindcss": "^0.3.0"

@ -6,10 +6,10 @@ specifiers:
'@astrojs/tailwind': ^3.1.3 '@astrojs/tailwind': ^3.1.3
'@fingerprintjs/fingerprintjs': ^3.4.1 '@fingerprintjs/fingerprintjs': ^3.4.1
'@nanostores/preact': ^0.5.0 '@nanostores/preact': ^0.5.0
'@playwright/test': ^1.35.0 '@playwright/test': ^1.35.1
'@tailwindcss/typography': ^0.5.9 '@tailwindcss/typography': ^0.5.9
'@types/js-cookie': ^3.0.3 '@types/js-cookie': ^3.0.3
astro: ^2.6.3 astro: ^2.6.6
astro-compress: ^1.1.47 astro-compress: ^1.1.47
csv-parser: ^3.0.0 csv-parser: ^3.0.0
gh-pages: ^5.0.0 gh-pages: ^5.0.0
@ -17,10 +17,10 @@ specifiers:
js-cookie: ^3.0.5 js-cookie: ^3.0.5
js-yaml: ^4.1.0 js-yaml: ^4.1.0
markdown-it: ^13.0.1 markdown-it: ^13.0.1
nanostores: ^0.9.1 nanostores: ^0.9.2
node-html-parser: ^6.1.5 node-html-parser: ^6.1.5
npm-check-updates: ^16.10.12 npm-check-updates: ^16.10.12
openai: ^3.2.1 openai: ^3.3.0
preact: ^10.15.1 preact: ^10.15.1
prettier: ^2.8.8 prettier: ^2.8.8
prettier-plugin-astro: ^0.10.0 prettier-plugin-astro: ^0.10.0
@ -32,14 +32,14 @@ specifiers:
dependencies: dependencies:
'@astrojs/preact': 2.2.1_preact@10.15.1 '@astrojs/preact': 2.2.1_preact@10.15.1
'@astrojs/sitemap': 1.3.3 '@astrojs/sitemap': 1.3.3
'@astrojs/tailwind': 3.1.3_ez5tr46f34xemqtmnzl54w7fmm '@astrojs/tailwind': 3.1.3_nqn7iohbkhifv3v6vivhcsj3ue
'@fingerprintjs/fingerprintjs': 3.4.1 '@fingerprintjs/fingerprintjs': 3.4.1
'@nanostores/preact': 0.5.0_m2wbkjxz7237icvaxqi7ignbgm '@nanostores/preact': 0.5.0_goi3tttstrh6kq4nibjxbyzyja
astro: 2.6.4 astro: 2.6.6
astro-compress: 1.1.47 astro-compress: 1.1.47
jose: 4.14.4 jose: 4.14.4
js-cookie: 3.0.5 js-cookie: 3.0.5
nanostores: 0.9.1 nanostores: 0.9.2
node-html-parser: 6.1.5 node-html-parser: 6.1.5
npm-check-updates: 16.10.12 npm-check-updates: 16.10.12
preact: 10.15.1 preact: 10.15.1
@ -48,7 +48,7 @@ dependencies:
tailwindcss: 3.3.2 tailwindcss: 3.3.2
devDependencies: devDependencies:
'@playwright/test': 1.35.0 '@playwright/test': 1.35.1
'@tailwindcss/typography': 0.5.9_tailwindcss@3.3.2 '@tailwindcss/typography': 0.5.9_tailwindcss@3.3.2
'@types/js-cookie': 3.0.3 '@types/js-cookie': 3.0.3
csv-parser: 3.0.0 csv-parser: 3.0.0
@ -87,7 +87,7 @@ packages:
dependencies: dependencies:
'@astrojs/compiler': 1.5.1 '@astrojs/compiler': 1.5.1
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
'@vscode/emmet-helper': 2.8.9 '@vscode/emmet-helper': 2.9.1
events: 3.3.0 events: 3.3.0
prettier: 2.8.8 prettier: 2.8.8
prettier-plugin-astro: 0.9.1 prettier-plugin-astro: 0.9.1
@ -100,13 +100,13 @@ packages:
vscode-uri: 3.0.7 vscode-uri: 3.0.7
dev: false dev: false
/@astrojs/markdown-remark/2.2.1_astro@2.6.4: /@astrojs/markdown-remark/2.2.1_astro@2.6.6:
resolution: {integrity: sha512-VF0HRv4GpC1XEMLnsKf6jth7JSmlt9qpqP0josQgA2eSpCIAC/Et+y94mgdBIZVBYH/yFnMoIxgKVe93xfO2GA==} resolution: {integrity: sha512-VF0HRv4GpC1XEMLnsKf6jth7JSmlt9qpqP0josQgA2eSpCIAC/Et+y94mgdBIZVBYH/yFnMoIxgKVe93xfO2GA==}
peerDependencies: peerDependencies:
astro: ^2.5.0 astro: ^2.5.0
dependencies: dependencies:
'@astrojs/prism': 2.1.2 '@astrojs/prism': 2.1.2
astro: 2.6.4 astro: 2.6.6
github-slugger: 1.5.0 github-slugger: 1.5.0
import-meta-resolve: 2.2.2 import-meta-resolve: 2.2.2
rehype-raw: 6.1.1 rehype-raw: 6.1.1
@ -153,14 +153,14 @@ packages:
zod: 3.21.4 zod: 3.21.4
dev: false dev: false
/@astrojs/tailwind/3.1.3_ez5tr46f34xemqtmnzl54w7fmm: /@astrojs/tailwind/3.1.3_nqn7iohbkhifv3v6vivhcsj3ue:
resolution: {integrity: sha512-10S1omrv5K5HRVAZ0fBgN5vQykn2HRL332LAVFyBASMn1Ff6gDfSK+CPUeUu94eZUOEaPnECLK8EHAqZ8iY9CA==} resolution: {integrity: sha512-10S1omrv5K5HRVAZ0fBgN5vQykn2HRL332LAVFyBASMn1Ff6gDfSK+CPUeUu94eZUOEaPnECLK8EHAqZ8iY9CA==}
peerDependencies: peerDependencies:
astro: ^2.5.0 astro: ^2.5.0
tailwindcss: ^3.0.24 tailwindcss: ^3.0.24
dependencies: dependencies:
'@proload/core': 0.3.3 '@proload/core': 0.3.3
astro: 2.6.4 astro: 2.6.6
autoprefixer: 10.4.14_postcss@8.4.24 autoprefixer: 10.4.14_postcss@8.4.24
postcss: 8.4.24 postcss: 8.4.24
postcss-load-config: 4.0.1_postcss@8.4.24 postcss-load-config: 4.0.1_postcss@8.4.24
@ -252,7 +252,7 @@ packages:
'@babel/compat-data': 7.22.5 '@babel/compat-data': 7.22.5
'@babel/core': 7.22.5 '@babel/core': 7.22.5
'@babel/helper-validator-option': 7.22.5 '@babel/helper-validator-option': 7.22.5
browserslist: 4.21.8 browserslist: 4.21.9
lru-cache: 5.1.1 lru-cache: 5.1.1
semver: 6.3.0 semver: 6.3.0
dev: false dev: false
@ -700,14 +700,14 @@ packages:
resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==} resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==}
dev: false dev: false
/@nanostores/preact/0.5.0_m2wbkjxz7237icvaxqi7ignbgm: /@nanostores/preact/0.5.0_goi3tttstrh6kq4nibjxbyzyja:
resolution: {integrity: sha512-Zq5DEAY+kIfwJ1NPd43D1mpsbISuiD6N/SuTHrt/8jUoifLwXaReaZMAnvkvbIGOgcB1Hy++A9jZix2taNNYxQ==} resolution: {integrity: sha512-Zq5DEAY+kIfwJ1NPd43D1mpsbISuiD6N/SuTHrt/8jUoifLwXaReaZMAnvkvbIGOgcB1Hy++A9jZix2taNNYxQ==}
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
peerDependencies: peerDependencies:
nanostores: ^0.9.0 nanostores: ^0.9.0
preact: '>=10.0.0' preact: '>=10.0.0'
dependencies: dependencies:
nanostores: 0.9.1 nanostores: 0.9.2
preact: 10.15.1 preact: 10.15.1
dev: false dev: false
@ -733,7 +733,7 @@ packages:
resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==} resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies: dependencies:
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/@npmcli/git/4.1.0: /@npmcli/git/4.1.0:
@ -746,7 +746,7 @@ packages:
proc-log: 3.0.0 proc-log: 3.0.0
promise-inflight: 1.0.1 promise-inflight: 1.0.1
promise-retry: 2.0.1 promise-retry: 2.0.1
semver: 7.5.1 semver: 7.5.2
which: 3.0.1 which: 3.0.1
transitivePeerDependencies: transitivePeerDependencies:
- bluebird - bluebird
@ -805,13 +805,13 @@ packages:
tslib: 2.5.3 tslib: 2.5.3
dev: false dev: false
/@playwright/test/1.35.0: /@playwright/test/1.35.1:
resolution: {integrity: sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q==} resolution: {integrity: sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==}
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
dependencies: dependencies:
'@types/node': 20.3.1 '@types/node': 20.3.1
playwright-core: 1.35.0 playwright-core: 1.35.1
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
@ -1033,8 +1033,8 @@ packages:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: false dev: false
/@vscode/emmet-helper/2.8.9: /@vscode/emmet-helper/2.9.1:
resolution: {integrity: sha512-ygpVStaePHt9aI9zk4NNJWI/NsRaeDSW1vQsZVmtpVRVCOdwYlsc3BfB/eppUu1OucT0x3OHDAzKcxnitjcSXQ==} resolution: {integrity: sha512-+cdkHZ/QlvSXXsA/fstnyl1EwIFJyKA9Mw4Yz4oC6gXTTewfViYWOddtDPVYKGtda/vA0rcnHTAF/Xl7+QGKgQ==}
dependencies: dependencies:
emmet: 2.4.4 emmet: 2.4.4
jsonc-parser: 2.3.1 jsonc-parser: 2.3.1
@ -1051,8 +1051,8 @@ packages:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: false dev: false
/acorn/8.8.2: /acorn/8.9.0:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: false dev: false
@ -1192,8 +1192,8 @@ packages:
terser: 5.17.7 terser: 5.17.7
dev: false dev: false
/astro/2.6.4: /astro/2.6.6:
resolution: {integrity: sha512-YM5H9SLHflxCB/3H8S2Bi+1Lbwn/MA9Vl/eOZmkCT491gvBsyuKCTsoUas6fwggeKn+fIR2XpdYd2F+unQve3g==} resolution: {integrity: sha512-npeTXVaSOWKYYF6Znj6Yfxfq+WIFZ9u/Q+vtFP3nXbl7/XimvE+LbmWoK+hPFBOXC/KRLHxqQSltXJX5ALFmFg==}
engines: {node: '>=16.12.0', npm: '>=6.14.0'} engines: {node: '>=16.12.0', npm: '>=6.14.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -1205,7 +1205,7 @@ packages:
'@astrojs/compiler': 1.5.1 '@astrojs/compiler': 1.5.1
'@astrojs/internal-helpers': 0.1.0 '@astrojs/internal-helpers': 0.1.0
'@astrojs/language-server': 1.0.8 '@astrojs/language-server': 1.0.8
'@astrojs/markdown-remark': 2.2.1_astro@2.6.4 '@astrojs/markdown-remark': 2.2.1_astro@2.6.6
'@astrojs/telemetry': 2.1.1 '@astrojs/telemetry': 2.1.1
'@astrojs/webapi': 2.2.0 '@astrojs/webapi': 2.2.0
'@babel/core': 7.22.5 '@babel/core': 7.22.5
@ -1216,7 +1216,7 @@ packages:
'@babel/types': 7.22.5 '@babel/types': 7.22.5
'@types/babel__core': 7.20.1 '@types/babel__core': 7.20.1
'@types/yargs-parser': 21.0.0 '@types/yargs-parser': 21.0.0
acorn: 8.8.2 acorn: 8.9.0
boxen: 6.2.1 boxen: 6.2.1
chokidar: 3.5.3 chokidar: 3.5.3
ci-info: 3.8.0 ci-info: 3.8.0
@ -1244,7 +1244,7 @@ packages:
preferred-pm: 3.0.3 preferred-pm: 3.0.3
prompts: 2.4.2 prompts: 2.4.2
rehype: 12.0.1 rehype: 12.0.1
semver: 7.5.1 semver: 7.5.2
server-destroy: 1.0.1 server-destroy: 1.0.1
shiki: 0.14.2 shiki: 0.14.2
slash: 4.0.0 slash: 4.0.0
@ -1284,8 +1284,8 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
dependencies: dependencies:
browserslist: 4.21.8 browserslist: 4.21.9
caniuse-lite: 1.0.30001503 caniuse-lite: 1.0.30001506
fraction.js: 4.2.0 fraction.js: 4.2.0
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
@ -1405,15 +1405,15 @@ packages:
dependencies: dependencies:
fill-range: 7.0.1 fill-range: 7.0.1
/browserslist/4.21.8: /browserslist/4.21.9:
resolution: {integrity: sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==} resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001503 caniuse-lite: 1.0.30001506
electron-to-chromium: 1.4.430 electron-to-chromium: 1.4.435
node-releases: 2.0.12 node-releases: 2.0.12
update-browserslist-db: 1.0.11_browserslist@4.21.8 update-browserslist-db: 1.0.11_browserslist@4.21.9
dev: false dev: false
/buffer-from/1.1.2: /buffer-from/1.1.2:
@ -1437,7 +1437,7 @@ packages:
/builtins/5.0.1: /builtins/5.0.1:
resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
dependencies: dependencies:
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/bundle-name/3.0.0: /bundle-name/3.0.0:
@ -1477,12 +1477,12 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: false dev: false
/cacheable-request/10.2.10: /cacheable-request/10.2.11:
resolution: {integrity: sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==} resolution: {integrity: sha512-kn0t0oJnlFo1Nzl/AYQzS/oByMtmaqLasFUa7MUMsiTrIHy8TxSkx2KzWCybE3Nuz1F4sJRGnLAfUGsPe47viQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.1 '@types/http-cache-semantics': 4.0.1
get-stream: 6.0.1 get-stream: 7.0.0
http-cache-semantics: 4.1.1 http-cache-semantics: 4.1.1
keyv: 4.5.2 keyv: 4.5.2
mimic-response: 4.0.0 mimic-response: 4.0.0
@ -1511,8 +1511,8 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: false dev: false
/caniuse-lite/1.0.30001503: /caniuse-lite/1.0.30001506:
resolution: {integrity: sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==} resolution: {integrity: sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw==}
dev: false dev: false
/ccount/2.0.1: /ccount/2.0.1:
@ -1977,8 +1977,8 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: false dev: false
/electron-to-chromium/1.4.430: /electron-to-chromium/1.4.435:
resolution: {integrity: sha512-FytjTbGwz///F+ToZ5XSeXbbSaXalsVRXsz2mHityI5gfxft7ieW3HqFLkU5V1aIrY42aflICqbmFoDxW10etg==} resolution: {integrity: sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==}
dev: false dev: false
/email-addresses/5.0.0: /email-addresses/5.0.0:
@ -2368,6 +2368,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: false dev: false
/get-stream/7.0.0:
resolution: {integrity: sha512-ql6FW5b8tgMYvI4UaoxG3EQN3VyZ6VeQpxNBGg5BZ4xD4u+HJeprzhMMA4OCBEGQgSR+m87pstWMpiVW64W8Fw==}
engines: {node: '>=16'}
dev: false
/gh-pages/5.0.0: /gh-pages/5.0.0:
resolution: {integrity: sha512-Nqp1SjkPIB94Xw/3yYNTUL+G2dxlhjvv1zeN/4kMC1jfViTEqhtVz/Ba1zSXHuvXCN9ADNS1dN4r5/J/nZWEQQ==} resolution: {integrity: sha512-Nqp1SjkPIB94Xw/3yYNTUL+G2dxlhjvv1zeN/4kMC1jfViTEqhtVz/Ba1zSXHuvXCN9ADNS1dN4r5/J/nZWEQQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2491,7 +2496,7 @@ packages:
'@sindresorhus/is': 5.4.1 '@sindresorhus/is': 5.4.1
'@szmarczak/http-timer': 5.0.1 '@szmarczak/http-timer': 5.0.1
cacheable-lookup: 7.0.0 cacheable-lookup: 7.0.0
cacheable-request: 10.2.10 cacheable-request: 10.2.11
decompress-response: 6.0.0 decompress-response: 6.0.0
form-data-encoder: 2.1.4 form-data-encoder: 2.1.4
get-stream: 6.0.1 get-stream: 6.0.1
@ -3063,7 +3068,7 @@ packages:
resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dependencies: dependencies:
package-json: 8.1.0 package-json: 8.1.1
dev: false dev: false
/lilconfig/2.1.0: /lilconfig/2.1.0:
@ -3800,8 +3805,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
/nanostores/0.9.1: /nanostores/0.9.2:
resolution: {integrity: sha512-DmAL3oTieICqnl2XVq5wegFE7EXIoPnIv1CNWNGEhXpwrHk7Prctch4/nX5x95i95WHdesI5sPeoNAUFpFsGtg==} resolution: {integrity: sha512-wfKlqLGtOYV9+qzGveqDOSWZUBgTeMr/g+JzfV/GofXQ//0wp0cgHF+QBVlmNH/JW9YA9QN+vR6N0vpniPpARA==}
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
dev: false dev: false
@ -3831,7 +3836,7 @@ packages:
resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==} resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
dependencies: dependencies:
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/node-addon-api/6.1.0: /node-addon-api/6.1.0:
@ -3851,7 +3856,7 @@ packages:
nopt: 6.0.0 nopt: 6.0.0
npmlog: 6.0.2 npmlog: 6.0.2
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.5.1 semver: 7.5.2
tar: 6.1.15 tar: 6.1.15
which: 2.0.2 which: 2.0.2
transitivePeerDependencies: transitivePeerDependencies:
@ -3883,7 +3888,7 @@ packages:
dependencies: dependencies:
hosted-git-info: 6.1.1 hosted-git-info: 6.1.1
is-core-module: 2.12.1 is-core-module: 2.12.1
semver: 7.5.1 semver: 7.5.2
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
dev: false dev: false
@ -3936,7 +3941,7 @@ packages:
rc-config-loader: 4.1.3 rc-config-loader: 4.1.3
remote-git-tags: 3.0.0 remote-git-tags: 3.0.0
rimraf: 5.0.1 rimraf: 5.0.1
semver: 7.5.1 semver: 7.5.2
semver-utils: 1.1.4 semver-utils: 1.1.4
source-map-support: 0.5.21 source-map-support: 0.5.21
spawn-please: 2.0.1 spawn-please: 2.0.1
@ -3952,7 +3957,7 @@ packages:
resolution: {integrity: sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==} resolution: {integrity: sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies: dependencies:
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/npm-normalize-package-bin/3.0.1: /npm-normalize-package-bin/3.0.1:
@ -3966,7 +3971,7 @@ packages:
dependencies: dependencies:
hosted-git-info: 6.1.1 hosted-git-info: 6.1.1
proc-log: 3.0.0 proc-log: 3.0.0
semver: 7.5.1 semver: 7.5.2
validate-npm-package-name: 5.0.0 validate-npm-package-name: 5.0.0
dev: false dev: false
@ -3984,7 +3989,7 @@ packages:
npm-install-checks: 6.1.1 npm-install-checks: 6.1.1
npm-normalize-package-bin: 3.0.1 npm-normalize-package-bin: 3.0.1
npm-package-arg: 10.1.0 npm-package-arg: 10.1.0
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/npm-registry-fetch/14.0.5: /npm-registry-fetch/14.0.5:
@ -4149,14 +4154,14 @@ packages:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
/package-json/8.1.0: /package-json/8.1.1:
resolution: {integrity: sha512-hySwcV8RAWeAfPsXb9/HGSPn8lwDnv6fabH+obUZKX169QknRkRhPxd1yMubpKDskLFATkl3jHpNtVtDPFA0Wg==} resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dependencies: dependencies:
got: 12.6.1 got: 12.6.1
registry-auth-token: 5.0.2 registry-auth-token: 5.0.2
registry-url: 6.0.1 registry-url: 6.0.1
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/pacote/15.1.1: /pacote/15.1.1:
@ -4290,8 +4295,8 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/pirates/4.0.5: /pirates/4.0.6:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
/pkg-dir/4.2.0: /pkg-dir/4.2.0:
@ -4307,8 +4312,8 @@ packages:
find-up: 3.0.0 find-up: 3.0.0
dev: false dev: false
/playwright-core/1.35.0: /playwright-core/1.35.1:
resolution: {integrity: sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==} resolution: {integrity: sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==}
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
dev: true dev: true
@ -4915,7 +4920,7 @@ packages:
resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
semver: 7.5.1 semver: 7.5.2
dev: false dev: false
/semver-utils/1.1.4: /semver-utils/1.1.4:
@ -4926,8 +4931,8 @@ packages:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true hasBin: true
/semver/7.5.1: /semver/7.5.2:
resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} resolution: {integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -4951,7 +4956,7 @@ packages:
detect-libc: 2.0.1 detect-libc: 2.0.1
node-addon-api: 6.1.0 node-addon-api: 6.1.0
prebuild-install: 7.1.1 prebuild-install: 7.1.1
semver: 7.5.1 semver: 7.5.2
simple-get: 4.0.1 simple-get: 4.0.1
tar-fs: 2.1.1 tar-fs: 2.1.1
tunnel-agent: 0.6.0 tunnel-agent: 0.6.0
@ -5236,7 +5241,7 @@ packages:
glob: 7.1.6 glob: 7.1.6
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
mz: 2.7.0 mz: 2.7.0
pirates: 4.0.5 pirates: 4.0.6
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
/suf-log/2.5.3: /suf-log/2.5.3:
@ -5358,7 +5363,7 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
'@jridgewell/source-map': 0.3.3 '@jridgewell/source-map': 0.3.3
acorn: 8.8.2 acorn: 8.9.0
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
dev: false dev: false
@ -5575,13 +5580,13 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/update-browserslist-db/1.0.11_browserslist@4.21.8: /update-browserslist-db/1.0.11_browserslist@4.21.9:
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
dependencies: dependencies:
browserslist: 4.21.8 browserslist: 4.21.9
escalade: 3.1.1 escalade: 3.1.1
picocolors: 1.0.0 picocolors: 1.0.0
dev: false dev: false
@ -5601,7 +5606,7 @@ packages:
is-yarn-global: 0.4.1 is-yarn-global: 0.4.1
latest-version: 7.0.0 latest-version: 7.0.0
pupa: 3.1.0 pupa: 3.1.0
semver: 7.5.1 semver: 7.5.2
semver-diff: 4.0.0 semver-diff: 4.0.0
xdg-basedir: 5.1.0 xdg-basedir: 5.1.0
dev: false dev: false

@ -2,6 +2,7 @@
import Icon from './AstroIcon.astro'; import Icon from './AstroIcon.astro';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro'; import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import BestPracticeHint from './BestPracticeHint.astro'; import BestPracticeHint from './BestPracticeHint.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import ProgressHelpPopup from './ProgressHelpPopup.astro'; import ProgressHelpPopup from './ProgressHelpPopup.astro';
export interface Props { export interface Props {
@ -18,35 +19,41 @@ const isBestPracticeReady = !isUpcoming;
<LoginPopup /> <LoginPopup />
<ProgressHelpPopup /> <ProgressHelpPopup />
<div class='border-b'> <div class="border-b">
<div class='container relative py-5 sm:py-12'> <div class="container relative py-5 sm:py-12">
<div class='mb-3 mt-0 sm:mb-6'> <div class="mb-3 mt-0 sm:mb-6">
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'> <h1 class="mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl">
{title} {title}
<MarkFavorite
resourceId={bestPracticeId}
resourceType="best-practice"
className="text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1 relative focus:outline-0"
client:load
/>
</h1> </h1>
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p> <p class="text-sm text-gray-500 sm:text-lg">{description}</p>
</div> </div>
<div class='flex justify-between'> <div class="flex justify-between">
<div class='flex gap-1 sm:gap-2'> <div class="flex gap-1 sm:gap-2">
<a <a
href='/best-practices' href="/best-practices"
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm' class="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label='Back to All Best Practices' aria-label="Back to All Best Practices"
> >
&larr;<span class='hidden sm:inline'>&nbsp;All Best Practices</span> &larr;<span class="hidden sm:inline">&nbsp;All Best Practices</span>
</a> </a>
{ {
isBestPracticeReady && ( isBestPracticeReady && (
<button <button
data-guest-required data-guest-required
data-popup='login-popup' data-popup="login-popup"
class='hidden inline-flex items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Download Roadmap' aria-label="Download Roadmap"
> >
<Icon icon='download' /> <Icon icon="download" />
<span class='ml-2 hidden sm:inline'>Download</span> <span class="ml-2 hidden sm:inline">Download</span>
</button> </button>
) )
} }
@ -55,25 +62,25 @@ const isBestPracticeReady = !isUpcoming;
isBestPracticeReady && ( isBestPracticeReady && (
<a <a
data-auth-required data-auth-required
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Download Roadmap' aria-label="Download Roadmap"
target="_blank" target="_blank"
href={`/pdfs/best-practices/${bestPracticeId}.pdf`} href={`/pdfs/best-practices/${bestPracticeId}.pdf`}
> >
<Icon icon='download' /> <Icon icon="download" />
<span class='ml-2 hidden sm:inline'>Download</span> <span class="ml-2 hidden sm:inline">Download</span>
</a> </a>
) )
} }
<button <button
data-guest-required data-guest-required
data-popup='login-popup' data-popup="login-popup"
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Subscribe for Updates' aria-label="Subscribe for Updates"
> >
<Icon icon='email' /> <Icon icon="email" />
<span class='ml-2'>Subscribe</span> <span class="ml-2">Subscribe</span>
</button> </button>
</div> </div>
@ -81,13 +88,13 @@ const isBestPracticeReady = !isUpcoming;
isBestPracticeReady && ( isBestPracticeReady && (
<a <a
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`} href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
target='_blank' target="_blank"
class='inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm' class="inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label='Suggest Changes' aria-label="Suggest Changes"
> >
<Icon icon='comment' class='h-3 w-3' /> <Icon icon="comment" class="h-3 w-3" />
<span class='ml-2 hidden sm:inline'>Suggest Changes</span> <span class="ml-2 hidden sm:inline">Suggest Changes</span>
<span class='ml-2 inline sm:hidden'>Suggest</span> <span class="ml-2 inline sm:hidden">Suggest</span>
</a> </a>
) )
} }

@ -10,11 +10,21 @@ type MarkFavoriteType = {
resourceType: ResourceType; resourceType: ResourceType;
resourceId: string; resourceId: string;
favorite?: boolean; favorite?: boolean;
className?: string;
}; };
export function MarkFavorite({ resourceId, resourceType, favorite }: MarkFavoriteType) { export function MarkFavorite({
resourceId,
resourceType,
favorite,
className,
}: MarkFavoriteType) {
const localStorageKey = `${resourceType}-${resourceId}-favorite`;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFavorite, setIsFavorite] = useState(favorite ?? false); const [isFavorite, setIsFavorite] = useState(
favorite ?? localStorage.getItem(localStorageKey) === '1'
);
async function toggleFavoriteHandler(e: Event) { async function toggleFavoriteHandler(e: Event) {
e.preventDefault(); e.preventDefault();
@ -70,6 +80,7 @@ export function MarkFavorite({ resourceId, resourceType, favorite }: MarkFavorit
} = (e as CustomEvent).detail; } = (e as CustomEvent).detail;
if (id === resourceId && type === resourceType) { if (id === resourceId && type === resourceType) {
setIsFavorite(fav); setIsFavorite(fav);
localStorage.setItem(localStorageKey, fav ? '1' : '0');
} }
}; };
@ -83,9 +94,9 @@ export function MarkFavorite({ resourceId, resourceType, favorite }: MarkFavorit
<button <button
onClick={toggleFavoriteHandler} onClick={toggleFavoriteHandler}
tabIndex={-1} tabIndex={-1}
className={`${ className={`${isFavorite ? '' : 'opacity-30 hover:opacity-100'} ${
isFavorite ? '' : 'opacity-30 hover:opacity-100' className || 'absolute right-1.5 top-1.5 z-30 focus:outline-0'
} absolute right-1.5 top-1.5 z-30 focus:outline-0`} }`}
> >
{isLoading ? <Spinner /> : <FavoriteIcon isFavorite={isFavorite} />} {isLoading ? <Spinner /> : <FavoriteIcon isFavorite={isFavorite} />}
</button> </button>

@ -2,11 +2,11 @@
import Icon from './AstroIcon.astro'; import Icon from './AstroIcon.astro';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro'; import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import RoadmapHint from './RoadmapHint.astro'; import RoadmapHint from './RoadmapHint.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite.tsx';
import RoadmapNote from './RoadmapNote.astro'; import RoadmapNote from './RoadmapNote.astro';
import TopicSearch from './TopicSearch/TopicSearch.astro'; import TopicSearch from './TopicSearch/TopicSearch.astro';
import YouTubeAlert from './YouTubeAlert.astro'; import YouTubeAlert from './YouTubeAlert.astro';
import ProgressHelpPopup from './ProgressHelpPopup.astro'; import ProgressHelpPopup from './ProgressHelpPopup.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
export interface Props { export interface Props {
title: string; title: string;
@ -36,63 +36,69 @@ const isRoadmapReady = !isUpcoming;
<LoginPopup /> <LoginPopup />
<ProgressHelpPopup /> <ProgressHelpPopup />
<div class='border-b'> <div class="border-b">
<div class='container relative py-5 sm:py-12'> <div class="container relative py-5 sm:py-12">
<YouTubeAlert /> <YouTubeAlert />
<div class='mb-3 mt-0 sm:mb-4 sm:mt-4'> <div class="mb-3 mt-0 sm:mb-4 sm:mt-4">
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'> <h1 class="mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl">
{title} {title}
<MarkFavorite
resourceId={roadmapId}
resourceType="roadmap"
className="text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1 relative focus:outline-0"
client:load
/>
</h1> </h1>
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p> <p class="text-sm text-gray-500 sm:text-lg">{description}</p>
</div> </div>
<div class='flex justify-between'> <div class="flex justify-between">
<div class='flex gap-1 sm:gap-2'> <div class="flex gap-1 sm:gap-2">
{ {
!hasSearch && ( !hasSearch && (
<> <>
<a <a
href='/roadmaps' href="/roadmaps"
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm' class="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label='Back to All Roadmaps' aria-label="Back to All Roadmaps"
> >
&larr;<span class='hidden sm:inline'>&nbsp;All Roadmaps</span> &larr;<span class="hidden sm:inline">&nbsp;All Roadmaps</span>
</a> </a>
{isRoadmapReady && ( {isRoadmapReady && (
<> <>
<button <button
data-guest-required data-guest-required
data-popup='login-popup' data-popup="login-popup"
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Download Roadmap' aria-label="Download Roadmap"
> >
<Icon icon='download' /> <Icon icon="download" />
<span class='ml-2 hidden sm:inline'>Download</span> <span class="ml-2 hidden sm:inline">Download</span>
</button> </button>
<a <a
data-auth-required data-auth-required
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Download Roadmap' aria-label="Download Roadmap"
target='_blank' target="_blank"
href={`/pdfs/roadmaps/${roadmapId}.pdf`} href={`/pdfs/roadmaps/${roadmapId}.pdf`}
> >
<Icon icon='download' /> <Icon icon="download" />
<span class='ml-2 hidden sm:inline'>Download</span> <span class="ml-2 hidden sm:inline">Download</span>
</a> </a>
</> </>
)} )}
<button <button
data-guest-required data-guest-required
data-popup='login-popup' data-popup="login-popup"
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm' class="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label='Subscribe for Updates' aria-label="Subscribe for Updates"
> >
<Icon icon='email' /> <Icon icon="email" />
<span class='ml-2'>Subscribe</span> <span class="ml-2">Subscribe</span>
</button> </button>
</> </>
) )
@ -102,11 +108,11 @@ const isRoadmapReady = !isUpcoming;
hasSearch && ( hasSearch && (
<a <a
href={`/${roadmapId}`} href={`/${roadmapId}`}
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm' class="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label='Back to Visual Roadmap' aria-label="Back to Visual Roadmap"
> >
&larr; &larr;
<span class='inline'>&nbsp;Visual Roadmap</span> <span class="inline">&nbsp;Visual Roadmap</span>
</a> </a>
) )
} }
@ -116,13 +122,13 @@ const isRoadmapReady = !isUpcoming;
isRoadmapReady && ( isRoadmapReady && (
<a <a
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`} href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
target='_blank' target="_blank"
class='inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm' class="inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label='Suggest Changes' aria-label="Suggest Changes"
> >
<Icon icon='comment' class='h-3 w-3' /> <Icon icon="comment" class="h-3 w-3" />
<span class='ml-2 hidden sm:inline'>Suggest Changes</span> <span class="ml-2 hidden sm:inline">Suggest Changes</span>
<span class='ml-2 inline sm:hidden'>Suggest</span> <span class="ml-2 inline sm:hidden">Suggest</span>
</a> </a>
) )
} }

@ -51,6 +51,7 @@ export async function updateResourceProgress(
done: string[]; done: string[];
learning: string[]; learning: string[];
skipped: string[]; skipped: string[];
isFavorite: boolean;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, { }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, {
topicId, topicId,
resourceType, resourceType,
@ -87,6 +88,10 @@ export async function getResourceProgress(
} }
const progressKey = `${resourceType}-${resourceId}-progress`; const progressKey = `${resourceType}-${resourceId}-progress`;
const isFavoriteKey = `${resourceType}-${resourceId}-favorite`;
const rawIsFavorite = localStorage.getItem(isFavoriteKey);
const isFavorite = JSON.parse(rawIsFavorite || '0') === 1;
const rawProgress = localStorage.getItem(progressKey); const rawProgress = localStorage.getItem(progressKey);
const progress = JSON.parse(rawProgress || 'null'); const progress = JSON.parse(rawProgress || 'null');
@ -99,6 +104,17 @@ export async function getResourceProgress(
return loadFreshProgress(resourceType, resourceId); return loadFreshProgress(resourceType, resourceId);
} }
// Dispatch event to update favorite status in the MarkFavorite component
window.dispatchEvent(
new CustomEvent('mark-favorite', {
detail: {
resourceType,
resourceId,
isFavorite
}
})
);
return progress; return progress;
} }
@ -110,6 +126,7 @@ async function loadFreshProgress(
done: string[]; done: string[];
learning: string[]; learning: string[];
skipped: string[]; skipped: string[];
isFavorite: boolean;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, { }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, {
resourceType, resourceType,
resourceId, resourceId,
@ -129,7 +146,18 @@ async function loadFreshProgress(
resourceId, resourceId,
response?.done || [], response?.done || [],
response?.learning || [], response?.learning || [],
response?.skipped || [] response?.skipped || [],
);
// Dispatch event to update favorite status in the MarkFavorite component
window.dispatchEvent(
new CustomEvent('mark-favorite', {
detail: {
resourceType,
resourceId,
isFavorite: response.isFavorite
}
})
); );
return response; return response;
@ -140,7 +168,7 @@ export function setResourceProgress(
resourceId: string, resourceId: string,
done: string[], done: string[],
learning: string[], learning: string[],
skipped: string[] skipped: string[],
): void { ): void {
localStorage.setItem( localStorage.setItem(
`${resourceType}-${resourceId}-progress`, `${resourceType}-${resourceId}-progress`,

Loading…
Cancel
Save