chore: roadmap select

pull/4053/head
Arik Chakma 1 year ago
parent 33c34f878e
commit 0c615b75d2
  1. 4
      astro.config.mjs
  2. 7
      package.json
  3. 339
      pnpm-lock.yaml
  4. 89
      src/components/RoadCard/RoadCardPage.tsx
  5. 122
      src/components/RoadCard/RoadmapSelect.tsx
  6. 19
      src/components/RoadCard/TallBadgeTab.tsx
  7. 17
      src/components/RoadCard/WideBadgeTab.tsx
  8. 36
      src/helper/get-badge-link.ts
  9. 3
      src/lib/home-progress.ts
  10. 1
      src/pages/account/road-card.astro

@ -58,6 +58,8 @@ export default defineConfig({
css: false,
js: false,
}),
preact(),
preact({
compat: true,
}),
],
});

@ -34,6 +34,7 @@
"node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12",
"preact": "^10.15.1",
"react-select": "^5.7.3",
"rehype-external-links": "^2.1.0",
"roadmap-renderer": "^1.0.6",
"tailwindcss": "^3.3.2"
@ -50,5 +51,11 @@
"prettier": "^2.8.8",
"prettier-plugin-astro": "^0.10.0",
"prettier-plugin-tailwindcss": "^0.3.0"
},
"pnpm": {
"overrides": {
"react": "npm:@preact/compat@latest",
"react-dom": "npm:@preact/compat@latest"
}
}
}

@ -25,6 +25,7 @@ specifiers:
prettier: ^2.8.8
prettier-plugin-astro: ^0.10.0
prettier-plugin-tailwindcss: ^0.3.0
react-select: ^5.7.3
rehype-external-links: ^2.1.0
roadmap-renderer: ^1.0.6
tailwindcss: ^3.3.2
@ -43,6 +44,7 @@ dependencies:
node-html-parser: 6.1.5
npm-check-updates: 16.10.12
preact: 10.15.1
react-select: 5.7.3_biqbaboplfbrettd7655fr4n2y
rehype-external-links: 2.1.0
roadmap-renderer: 1.0.6
tailwindcss: 3.3.2
@ -503,6 +505,13 @@ packages:
'@babel/types': 7.21.5
dev: false
/@babel/runtime/7.22.5:
resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
dev: false
/@babel/template/7.20.7:
resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
engines: {node: '>=6.9.0'}
@ -598,6 +607,94 @@ packages:
resolution: {integrity: sha512-1ESCGgXRgn1r29hRmz8K0G4Ywr5jDWezMgRnICComBCWmg3znLWU8+tmakuM1og1Vn4W/sauvlABl/oq2pve8w==}
dev: false
/@emotion/babel-plugin/11.11.0:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
'@babel/helper-module-imports': 7.21.4
'@babel/runtime': 7.22.5
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/serialize': 1.1.2
babel-plugin-macros: 3.1.0
convert-source-map: 1.9.0
escape-string-regexp: 4.0.0
find-root: 1.1.0
source-map: 0.5.7
stylis: 4.2.0
dev: false
/@emotion/cache/11.11.0:
resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==}
dependencies:
'@emotion/memoize': 0.8.1
'@emotion/sheet': 1.2.2
'@emotion/utils': 1.2.1
'@emotion/weak-memoize': 0.3.1
stylis: 4.2.0
dev: false
/@emotion/hash/0.9.1:
resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==}
dev: false
/@emotion/memoize/0.8.1:
resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
dev: false
/@emotion/react/11.11.1_react@18.2.0:
resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==}
peerDependencies:
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.5
'@emotion/babel-plugin': 11.11.0
'@emotion/cache': 11.11.0
'@emotion/serialize': 1.1.2
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1_react@18.2.0
'@emotion/utils': 1.2.1
'@emotion/weak-memoize': 0.3.1
hoist-non-react-statics: 3.3.2
react: 18.2.0
dev: false
/@emotion/serialize/1.1.2:
resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==}
dependencies:
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/unitless': 0.8.1
'@emotion/utils': 1.2.1
csstype: 3.1.2
dev: false
/@emotion/sheet/1.2.2:
resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==}
dev: false
/@emotion/unitless/0.8.1:
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
dev: false
/@emotion/use-insertion-effect-with-fallbacks/1.0.1_react@18.2.0:
resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
dev: false
/@emotion/utils/1.2.1:
resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==}
dev: false
/@emotion/weak-memoize/0.3.1:
resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==}
dev: false
/@esbuild/android-arm/0.17.18:
resolution: {integrity: sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==}
engines: {node: '>=12'}
@ -802,6 +899,16 @@ packages:
tslib: 2.5.0
dev: false
/@floating-ui/core/1.3.0:
resolution: {integrity: sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ==}
dev: false
/@floating-ui/dom/1.3.0:
resolution: {integrity: sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==}
dependencies:
'@floating-ui/core': 1.3.0
dev: false
/@gar/promisify/1.1.3:
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
dev: false
@ -1162,10 +1269,32 @@ packages:
/@types/node/18.16.3:
resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==}
/@types/parse-json/4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: false
/@types/parse5/6.0.3:
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
dev: false
/@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
dev: false
/@types/react-transition-group/4.4.6:
resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==}
dependencies:
'@types/react': 18.2.12
dev: false
/@types/react/18.2.12:
resolution: {integrity: sha512-ndmBMLCgn38v3SntMeoJaIrO6tGHYKMEBohCUmw8HoLLQdRMOIGXfeYaBTLe2lsFaSB3MOK1VXscYFnmLtTSmw==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.3
csstype: 3.1.2
dev: false
/@types/resolve/1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
dev: false
@ -1176,6 +1305,10 @@ packages:
'@types/node': 18.16.3
dev: false
/@types/scheduler/0.16.3:
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
dev: false
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
@ -1451,6 +1584,15 @@ packages:
- debug
dev: true
/babel-plugin-macros/3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
dependencies:
'@babel/runtime': 7.22.5
cosmiconfig: 7.1.0
resolve: 1.22.2
dev: false
/babel-plugin-module-resolver/5.0.0:
resolution: {integrity: sha512-g0u+/ChLSJ5+PzYwLwP8Rp8Rcfowz58TJNCe+L/ui4rpzE/mg//JVX0EWBUYoxaextqnwuGHzfGp2hh0PPV25Q==}
engines: {node: '>= 16'}
@ -1650,6 +1792,11 @@ packages:
responselike: 3.0.0
dev: false
/callsites/3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
dev: false
/camel-case/4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
dependencies:
@ -1904,6 +2051,17 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/cosmiconfig/7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
dependencies:
'@types/parse-json': 4.0.0
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
dev: false
/cross-spawn/7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -1963,6 +2121,10 @@ packages:
css-tree: 2.2.1
dev: false
/csstype/3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: false
/csv-parser/3.0.0:
resolution: {integrity: sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==}
engines: {node: '>= 10'}
@ -2078,6 +2240,13 @@ packages:
/dlv/1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
/dom-helpers/5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
'@babel/runtime': 7.22.5
csstype: 3.1.2
dev: false
/dom-serializer/2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
@ -2184,6 +2353,12 @@ packages:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
dev: false
/error-ex/1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
dev: false
/es-module-lexer/1.2.1:
resolution: {integrity: sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==}
dev: false
@ -2232,6 +2407,11 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
/escape-string-regexp/4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
dev: false
/escape-string-regexp/5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
@ -2346,6 +2526,10 @@ packages:
pkg-dir: 4.2.0
dev: true
/find-root/1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
dev: false
/find-up/3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
@ -2761,6 +2945,12 @@ packages:
hasBin: true
dev: false
/hoist-non-react-statics/3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
react-is: 16.13.1
dev: false
/hosted-git-info/5.2.1:
resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@ -2865,6 +3055,14 @@ packages:
engines: {node: '>= 4'}
dev: false
/import-fresh/3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
dev: false
/import-lazy/4.0.0:
resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
engines: {node: '>=8'}
@ -2920,6 +3118,10 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/is-arrayish/0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: false
/is-arrayish/0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
@ -3104,6 +3306,10 @@ packages:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
dev: false
/json-parse-even-better-errors/2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: false
/json-parse-even-better-errors/3.0.0:
resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -3244,6 +3450,13 @@ packages:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
dev: false
/loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
dependencies:
js-tokens: 4.0.0
dev: false
/lower-case/2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
@ -3499,6 +3712,10 @@ packages:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
dev: true
/memoize-one/6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
dev: false
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: false
@ -4333,12 +4550,29 @@ packages:
tslib: 2.5.0
dev: false
/parent-module/1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
dependencies:
callsites: 3.1.0
dev: false
/parse-github-url/1.0.2:
resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==}
engines: {node: '>=0.10.0'}
hasBin: true
dev: false
/parse-json/5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
'@babel/code-frame': 7.21.4
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: false
/parse-latin/5.0.1:
resolution: {integrity: sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==}
dependencies:
@ -4697,6 +4931,14 @@ packages:
sisteransi: 1.0.5
dev: false
/prop-types/15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: false
/property-information/6.2.0:
resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==}
dev: false
@ -4748,6 +4990,62 @@ packages:
strip-json-comments: 2.0.1
dev: false
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
react: ^18.2.0
dependencies:
loose-envify: 1.4.0
react: 18.2.0
scheduler: 0.23.0
dev: false
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
/react-select/5.7.3_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.22.5
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1_react@18.2.0
'@floating-ui/dom': 1.3.0
'@types/react-transition-group': 4.4.6
memoize-one: 6.0.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y
use-isomorphic-layout-effect: 1.1.2_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
react: '>=16.6.0'
react-dom: '>=16.6.0'
dependencies:
'@babel/runtime': 7.22.5
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
dependencies:
loose-envify: 1.4.0
dev: false
/read-cache/1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
dependencies:
@ -4786,6 +5084,10 @@ packages:
dependencies:
picomatch: 2.3.1
/regenerator-runtime/0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: false
/registry-auth-token/5.0.2:
resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==}
engines: {node: '>=14'}
@ -4908,6 +5210,11 @@ packages:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
dev: false
/resolve-from/4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
dev: false
/resolve/1.22.2:
resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
hasBin: true
@ -5035,6 +5342,12 @@ packages:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
/scheduler/0.23.0:
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
dependencies:
loose-envify: 1.4.0
dev: false
/section-matter/1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
@ -5210,6 +5523,11 @@ packages:
source-map: 0.6.1
dev: false
/source-map/0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
dev: false
/source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@ -5360,6 +5678,10 @@ packages:
escape-string-regexp: 1.0.5
dev: true
/stylis/4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
dev: false
/sucrase/3.32.0:
resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==}
engines: {node: '>=8'}
@ -5756,6 +6078,18 @@ packages:
xdg-basedir: 5.1.0
dev: false
/use-isomorphic-layout-effect/1.1.2_react@18.2.0:
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
react: 18.2.0
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -6006,6 +6340,11 @@ packages:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false
/yaml/1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
dev: false
/yaml/2.2.2:
resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==}
engines: {node: '>= 14'}

@ -1,38 +1,14 @@
import { useState } from 'preact/hooks';
import { useEffect, useState } from 'preact/hooks';
import { useCopyText } from '../../hooks/use-copy-text';
import { useAuth } from '../../hooks/use-auth';
import { TallBadgeTab } from './TallBadgeTab';
import WideBadgeTab from './WideBadgeTab';
import { WideBadgeTab } from './WideBadgeTab';
import CopyIcon from '../../icons/copy.svg';
export type GetBadgeLinkProps = {
user: ReturnType<typeof useAuth>;
variant: 'dark' | 'light';
badge: 'tall' | 'wide';
};
export function getBadgeLink({ user, variant, badge }: GetBadgeLinkProps) {
const badgeUrl = `${import.meta.env.PUBLIC_API_URL}/v1-badge/${badge}/${
user?.id
}${variant ? `?variant=${variant}` : ''}`;
const textareaContent = `
<a href="${badgeUrl}">
<img src="${badgeUrl}" alt="${user?.name}${user?.name && "'s"} Road Card"/>
</a>
`.trim();
const markdownSnippet = `
[![${user?.name}${
user?.name && "'s"
} Road Card](${badgeUrl})](${badgeUrl})
`.trim();
return {
badgeUrl,
textareaContent,
markdownSnippet,
};
}
import { RoadmapSelect, RoadmapSelectProps } from './RoadmapSelect';
import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import type { UserProgressResponse } from '../../lib/home-progress';
type EditorProps = {
title: string;
@ -45,7 +21,7 @@ export function Editor(props: EditorProps) {
const { isCopied, copyText } = useCopyText();
return (
<div className="flex flex-grow flex-col overflow-hidden rounded border border-gray-300 bg-gray-50 w-full">
<div className="flex w-full flex-grow flex-col overflow-hidden rounded border border-gray-300 bg-gray-50">
<div className="flex items-center justify-between gap-2 border-b border-gray-300 px-3 py-2">
<span className="text-xs uppercase leading-none text-gray-400">
{title}
@ -76,12 +52,45 @@ export type BadgeProps = {
export function RoadCardPage() {
const [selectedBadge, setSelectedBadge] = useState<'tall' | 'wide'>('tall');
const [progress, setProgress] = useState<RoadmapSelectProps['options']>([]);
const [selectedRoadmaps, setSelectedRoadmap] = useState<
RoadmapSelectProps['options']
>([]);
const user = useAuth();
if (!user) {
return null;
}
const fetchProgress = async () => {
const { response: progressList, error } =
await httpGet<UserProgressResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-all-progress`
);
if (error || !progressList) {
return;
}
const enrichedRoadmaps = [];
for (const progress of progressList) {
if (progress.resourceType !== 'roadmap') return;
enrichedRoadmaps.push({
label: progress.resourceTitle,
value: progress.resourceId,
});
}
setProgress(enrichedRoadmaps);
};
useEffect(() => {
fetchProgress().finally(() => {
pageProgressMessage.set('');
});
}, []);
return (
<>
<div className="mb-8 hidden md:block">
@ -95,8 +104,7 @@ export function RoadCardPage() {
<div className="mb-6 flex items-center border-b">
<div className="flex items-center">
<button
className={`relative top-px flex items-center justify-center px-3 pb-3 leading-none shadow-gray-600 ${
selectedBadge === 'tall'
className={`relative top-px flex items-center justify-center px-3 pb-3 leading-none shadow-gray-600 ${selectedBadge === 'tall'
? 'shadow-[inset_0_-1px_0_var(--tw-shadow-color)]'
: 'text-gray-600'
}`}
@ -108,8 +116,7 @@ export function RoadCardPage() {
</button>
<button
className={`relative top-px flex items-center justify-center px-3 pb-3 leading-none shadow-gray-600 ${
selectedBadge === 'wide'
className={`relative top-px flex items-center justify-center px-3 pb-3 leading-none shadow-gray-600 ${selectedBadge === 'wide'
? 'shadow-[inset_0_-1px_0_var(--tw-shadow-color)]'
: 'text-gray-600'
}`}
@ -123,8 +130,16 @@ export function RoadCardPage() {
</div>
</div>
{selectedBadge === 'tall' && <TallBadgeTab />}
{selectedBadge === 'wide' && <WideBadgeTab />}
<RoadmapSelect
options={progress}
selectedRoadmaps={selectedRoadmaps}
setSelectedRoadmap={setSelectedRoadmap}
/>
<div className="mt-6">
{selectedBadge === 'tall' && <TallBadgeTab selectedRoadmaps={selectedRoadmaps} />}
{selectedBadge === 'wide' && <WideBadgeTab selectedRoadmaps={selectedRoadmaps} />}
</div>
</>
);
}

@ -0,0 +1,122 @@
import Select, { ActionMeta, OnChangeValue } from 'react-select';
export type RoadmapOptionProps = {
value: string;
label: string;
};
export interface RoadmapSelectProps {
options: RoadmapOptionProps[];
selectedRoadmaps: RoadmapOptionProps[];
setSelectedRoadmap: (roadmap: RoadmapOptionProps[]) => void;
}
const controlStyles = {
base: 'border rounded-lg bg-white hover:cursor-pointer',
focus: 'border-primary-600 ring-1 ring-primary-500',
nonFocus: 'border-gray-300 hover:border-gray-400',
};
const placeholderStyles = 'text-gray-500 pl-1 py-0.5';
const selectInputStyles = 'pl-1 py-0.5';
const valueContainerStyles = 'p-1 gap-1';
const singleValueStyles = 'text-sm ml-1';
const multiValueStyles =
'bg-gray-100 rounded items-center py-0.5 pl-2 pr-1 gap-1.5';
const multiValueLabelStyles = 'leading-none py-1 text-sm';
const multiValueRemoveStyles =
'border border-gray-200 bg-white hover:bg-red-50 hover:text-red-800 text-gray-500 hover:border-red-300 rounded-md p-0.5';
const indicatorsContainerStyles = 'py-0 px-1 gap-1';
const clearIndicatorStyles =
'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-red-800';
const indicatorSeparatorStyles = 'bg-gray-300';
const dropdownIndicatorStyles =
'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black';
const menuStyles =
'p-1 mt-2 border border-gray-200 bg-white rounded-lg shadow-lg';
const groupHeadingStyles = 'ml-3 mt-2 mb-1 text-gray-500 text-sm';
const optionStyles = {
base: 'hover:cursor-pointer px-3 text-sm py-2 rounded',
focus: 'bg-gray-100 active:bg-gray-200',
selected: "after:content-['✔'] after:ml-2 after:text-green-500 text-gray-500",
};
const noOptionsMessageStyles =
'text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm';
export function RoadmapSelect({
options,
selectedRoadmaps,
setSelectedRoadmap,
}: RoadmapSelectProps) {
const onChange = (
newValue: OnChangeValue<RoadmapOptionProps, true>,
actionMeta: ActionMeta<RoadmapOptionProps>
) => {
if (actionMeta.action === 'clear') {
setSelectedRoadmap([]);
return;
}
// Only allow selecting up to 4 roadmaps.
if (newValue.length > 4) {
return;
}
setSelectedRoadmap(Array.isArray(newValue) ? newValue : [newValue]);
};
return (
<div>
{/* @ts-ignore */}
<Select
isMulti
options={options}
onChange={onChange}
value={selectedRoadmaps}
unstyled
styles={{
input: (base) => ({
...base,
'input:focus': {
boxShadow: 'none',
},
}),
// On mobile, the label will truncate automatically, so we want to
// override that behaviour.
multiValueLabel: (base) => ({
...base,
// whiteSpace: 'normal',
// overflow: 'visible',
}),
control: (base) => ({
...base,
transition: 'none',
}),
}}
classNames={{
control: ({ isFocused }) =>
`${isFocused ? controlStyles.focus : controlStyles.nonFocus} ${
controlStyles.base
}`,
placeholder: () => placeholderStyles,
input: () => selectInputStyles,
valueContainer: () => valueContainerStyles,
singleValue: () => singleValueStyles,
multiValue: () => multiValueStyles,
multiValueLabel: () => multiValueLabelStyles,
multiValueRemove: () => multiValueRemoveStyles,
indicatorsContainer: () => indicatorsContainerStyles,
clearIndicator: () => clearIndicatorStyles,
indicatorSeparator: () => indicatorSeparatorStyles,
dropdownIndicator: () => dropdownIndicatorStyles,
menu: () => menuStyles,
groupHeading: () => groupHeadingStyles,
option: ({ isFocused, isSelected }) =>
`${isFocused && optionStyles.focus} ${
isSelected && optionStyles.selected
} ${optionStyles.base}`,
noOptionsMessage: () => noOptionsMessageStyles,
}}
/>
<div className="mt-1 text-sm text-gray-500">Select up to 4 roadmaps</div>
</div>
);
}

@ -1,10 +1,16 @@
import { useState } from 'preact/hooks';
import { LongBadge } from './TallBadge';
import { Editor, getBadgeLink } from './RoadCardPage';
import { Editor } from './RoadCardPage';
import { GithubReadmeBanner } from './GithubReadmeBanner';
import { useAuth } from '../../hooks/use-auth';
import { getBadgeLink } from '../../helper/get-badge-link';
import type { RoadmapOptionProps } from './RoadmapSelect';
export function TallBadgeTab() {
export function TallBadgeTab({
selectedRoadmaps,
}: {
selectedRoadmaps: RoadmapOptionProps[];
}) {
const [selectedVariant, setSelectedVariant] = useState<'dark' | 'light'>(
'dark'
);
@ -17,13 +23,14 @@ export function TallBadgeTab() {
user,
variant: selectedVariant,
badge: 'tall',
roadmaps: selectedRoadmaps,
});
return (
<div className="sm:grid sm:grid-cols-5 sm:gap-6">
<LongBadge badgeUrl={badgeUrl} />
<div className="sm:col-span-3 mt-6 sm:mt-0">
<div className="mt-6 sm:col-span-3 sm:mt-0">
<div>
<span className="text-xs uppercase leading-none text-gray-400">
Variant
@ -31,8 +38,7 @@ export function TallBadgeTab() {
<div className="mt-2 flex items-center gap-2">
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('dark')}
>
@ -40,8 +46,7 @@ export function TallBadgeTab() {
</button>
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('light')}
>

@ -1,10 +1,16 @@
import { useState } from 'preact/hooks';
import { useAuth } from '../../hooks/use-auth';
import { WideBadge } from './WideBadge';
import { Editor, getBadgeLink } from './RoadCardPage';
import { Editor } from './RoadCardPage';
import { GithubReadmeBanner } from './GithubReadmeBanner';
import { getBadgeLink } from '../../helper/get-badge-link';
import type { RoadmapOptionProps } from './RoadmapSelect';
export default function WideBadgeTab() {
export function WideBadgeTab({
selectedRoadmaps,
}: {
selectedRoadmaps: RoadmapOptionProps[];
}) {
const [selectedVariant, setSelectedVariant] = useState<'dark' | 'light'>(
'dark'
);
@ -17,6 +23,7 @@ export default function WideBadgeTab() {
user,
variant: selectedVariant,
badge: 'wide',
roadmaps: selectedRoadmaps,
});
return (
@ -28,8 +35,7 @@ export default function WideBadgeTab() {
<div className="mt-2 flex items-center gap-2">
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('dark')}
>
@ -37,8 +43,7 @@ export default function WideBadgeTab() {
</button>
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('light')}
>

@ -0,0 +1,36 @@
import type { RoadmapOptionProps } from "../components/RoadCard/RoadmapSelect";
import type { useAuth } from "../hooks/use-auth";
export type GetBadgeLinkProps = {
user: ReturnType<typeof useAuth>;
variant: 'dark' | 'light';
badge: 'tall' | 'wide';
roadmaps?: RoadmapOptionProps[]
};
export function getBadgeLink({ user, variant, badge, roadmaps }: GetBadgeLinkProps) {
const badgeUrl = new URL(`${import.meta.env.PUBLIC_API_URL}/v1-badge/${badge}/${user?.id}`);
if (variant) {
badgeUrl.searchParams.set('variant', variant);
}
if (roadmaps && roadmaps?.length > 0) {
for (const roadmap of roadmaps) {
badgeUrl.searchParams.append('roadmaps', roadmap.value);
}
}
const textareaContent = `
<a href="${badgeUrl}">
<img src="${badgeUrl}" alt="${user?.name}${user?.name && "'s"} Road Card"/>
</a>
`.trim();
const markdownSnippet = `
[![${user?.name}${user?.name && "'s"
} Road Card](${badgeUrl})](${badgeUrl})
`.trim();
return {
badgeUrl: badgeUrl.toString(),
textareaContent,
markdownSnippet,
};
}

@ -1,7 +1,8 @@
import { httpGet } from './http';
import { isLoggedIn } from './jwt';
type UserProgressResponse = {
export type UserProgressResponse = {
resourceTitle: string;
resourceId: string;
resourceType: 'roadmap' | 'best-practice';
done: number;

@ -7,6 +7,7 @@ import { RoadCardPage } from '../../components/RoadCard/RoadCardPage';
<AccountLayout
title='Road Card'
noIndex={true}
initialLoadingMessage='Loading your road card...'
>
<AccountSidebar activePageId='road-card' activePageTitle='Road Card'>
<RoadCardPage client:load />

Loading…
Cancel
Save