feat: first two questions of Skills Builder

This commit is contained in:
Maxwell Frank
2023-02-17 20:00:36 +00:00
parent 21a3e9259d
commit 68dc8a1045
17 changed files with 691 additions and 89 deletions

326
package-lock.json generated
View File

@@ -30,6 +30,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-instantsearch-hooks-web": "^6.40.1",
"react-redux": "7.2.9",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
@@ -129,6 +130,11 @@
"@algolia/transporter": "4.6.0"
}
},
"node_modules/@algolia/events": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz",
"integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ=="
},
"node_modules/@algolia/logger-common": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.6.0.tgz",
@@ -173,6 +179,20 @@
"@algolia/requester-common": "4.6.0"
}
},
"node_modules/@algolia/ui-components-highlight-vdom": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@algolia/ui-components-highlight-vdom/-/ui-components-highlight-vdom-1.2.1.tgz",
"integrity": "sha512-IlYgIaCUEkz9ezNbwugwKv991oOHhveyq6nzL0F1jDzg1p3q5Yj/vO4KpNG910r2dwGCG3nEm5GtChcLnarhFA==",
"dependencies": {
"@algolia/ui-components-shared": "1.2.1",
"@babel/runtime": "^7.0.0"
}
},
"node_modules/@algolia/ui-components-shared": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@algolia/ui-components-shared/-/ui-components-shared-1.2.1.tgz",
"integrity": "sha512-a7mYHf/GVQfhAx/HRiMveKkFvHspQv/REdG+C/FIOosiSmNZxX7QebDwJkrGSmDWdXO12D0Qv1xn3AytFcEDlQ=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -4496,6 +4516,11 @@
"version": "0.3.3",
"license": "MIT"
},
"node_modules/@types/dom-speech-recognition": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz",
"integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw=="
},
"node_modules/@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@@ -4564,6 +4589,11 @@
"@types/node": "*"
}
},
"node_modules/@types/google.maps": {
"version": "3.52.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.52.0.tgz",
"integrity": "sha512-cIwkgSBUOCerEwEpAahg1SxUqqGV+D786TkVWrcZZyPvuCozmXFtzQcpOzvUXBtTUqDzEbCDGlAXDfDSYFXFIw=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -4573,6 +4603,11 @@
"@types/node": "*"
}
},
"node_modules/@types/hogan.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.1.tgz",
"integrity": "sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg=="
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"license": "MIT",
@@ -4681,8 +4716,7 @@
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
@@ -5071,6 +5105,11 @@
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"dev": true
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -5298,6 +5337,17 @@
"@algolia/transporter": "4.6.0"
}
},
"node_modules/algoliasearch-helper": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.3.tgz",
"integrity": "sha512-TbaEvLwiuGygHQIB8y+OsJKQQ40+JKUua5B91X66tMUHyyhbNHvqyr0lqd3wCoyKx7WybyQrC0WJvzoIeh24Aw==",
"dependencies": {
"@algolia/events": "^4.0.1"
},
"peerDependencies": {
"algoliasearch": ">= 3.1 < 6"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -10334,6 +10384,27 @@
"value-equal": "^1.0.1"
}
},
"node_modules/hogan.js": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
"dependencies": {
"mkdirp": "0.3.0",
"nopt": "1.0.10"
},
"bin": {
"hulk": "bin/hulk"
}
},
"node_modules/hogan.js/node_modules/mkdirp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
"engines": {
"node": "*"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"license": "BSD-3-Clause",
@@ -10400,6 +10471,11 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/htm": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ=="
},
"node_modules/html-element-map": {
"version": "1.3.1",
"dev": true,
@@ -10885,6 +10961,40 @@
"dev": true,
"license": "ISC"
},
"node_modules/instantsearch.js": {
"version": "4.51.1",
"resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.51.1.tgz",
"integrity": "sha512-l4nETzassgSMBqOhNxntNH2MGG0KvFagwVwnXFis9P06nlspAyOdFLCQ0HA5wnlRDDev2WAZg9xRcTPCTLrwZw==",
"dependencies": {
"@algolia/events": "^4.0.1",
"@algolia/ui-components-highlight-vdom": "^1.2.1",
"@algolia/ui-components-shared": "^1.2.1",
"@types/dom-speech-recognition": "^0.0.1",
"@types/google.maps": "^3.45.3",
"@types/hogan.js": "^3.0.0",
"@types/qs": "^6.5.3",
"algoliasearch-helper": "^3.11.3",
"hogan.js": "^3.0.2",
"htm": "^3.0.0",
"preact": "^10.10.0",
"qs": "^6.5.1 < 6.10",
"search-insights": "^2.1.0"
},
"peerDependencies": {
"algoliasearch": ">= 3.1 < 6"
}
},
"node_modules/instantsearch.js/node_modules/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==",
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/internal-slot": {
"version": "1.0.3",
"dev": true,
@@ -13524,6 +13634,20 @@
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
"dev": true
},
"node_modules/nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "*"
}
},
"node_modules/normalize-package-data": {
"version": "3.0.3",
"dev": true,
@@ -14920,6 +15044,15 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@@ -15502,6 +15635,36 @@
"react": ">=16.3.0"
}
},
"node_modules/react-instantsearch-hooks": {
"version": "6.40.1",
"resolved": "https://registry.npmjs.org/react-instantsearch-hooks/-/react-instantsearch-hooks-6.40.1.tgz",
"integrity": "sha512-Nqbmencg5816UVDkWUFpGyurAhp8WUBQgHiYj/bTnYQ7KuwXbbxIppIwexZqzXG8Q3DKVBWZw4PnmJiStLK2CA==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"algoliasearch-helper": "^3.11.3",
"instantsearch.js": "4.51.1",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"algoliasearch": ">= 3.1 < 5",
"react": ">= 16.8.0 < 19"
}
},
"node_modules/react-instantsearch-hooks-web": {
"version": "6.40.1",
"resolved": "https://registry.npmjs.org/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.40.1.tgz",
"integrity": "sha512-MbsS1nhyZWPJjSVRzkDVmb0TM4L2kjHANsr0e1NDwkVWicP8LtXdX6UINDnekMKVnk1OmoF10NAJt4LmcSOqaA==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"instantsearch.js": "4.51.1",
"react-instantsearch-hooks": "6.40.1"
},
"peerDependencies": {
"algoliasearch": ">= 3.1 < 5",
"react": ">= 16.8.0 < 19",
"react-dom": ">= 16.8.0 < 19"
}
},
"node_modules/react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -16951,6 +17114,14 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/search-insights": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.3.0.tgz",
"integrity": "sha512-0v/TTO4fbd6I91sFBK/e2zNfD0f51A+fMoYNkMplmR77NpThUye/7gIxNoJ3LejKpZH6Z2KNBIpxxFmDKj10Yw==",
"engines": {
"node": ">=8.16.0"
}
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -19189,6 +19360,14 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"dev": true,
@@ -20173,6 +20352,11 @@
"@algolia/transporter": "4.6.0"
}
},
"@algolia/events": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz",
"integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ=="
},
"@algolia/logger-common": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.6.0.tgz",
@@ -20217,6 +20401,20 @@
"@algolia/requester-common": "4.6.0"
}
},
"@algolia/ui-components-highlight-vdom": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@algolia/ui-components-highlight-vdom/-/ui-components-highlight-vdom-1.2.1.tgz",
"integrity": "sha512-IlYgIaCUEkz9ezNbwugwKv991oOHhveyq6nzL0F1jDzg1p3q5Yj/vO4KpNG910r2dwGCG3nEm5GtChcLnarhFA==",
"requires": {
"@algolia/ui-components-shared": "1.2.1",
"@babel/runtime": "^7.0.0"
}
},
"@algolia/ui-components-shared": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@algolia/ui-components-shared/-/ui-components-shared-1.2.1.tgz",
"integrity": "sha512-a7mYHf/GVQfhAx/HRiMveKkFvHspQv/REdG+C/FIOosiSmNZxX7QebDwJkrGSmDWdXO12D0Qv1xn3AytFcEDlQ=="
},
"@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -23399,6 +23597,11 @@
"@types/cookie": {
"version": "0.3.3"
},
"@types/dom-speech-recognition": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz",
"integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw=="
},
"@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@@ -23467,6 +23670,11 @@
"@types/node": "*"
}
},
"@types/google.maps": {
"version": "3.52.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.52.0.tgz",
"integrity": "sha512-cIwkgSBUOCerEwEpAahg1SxUqqGV+D786TkVWrcZZyPvuCozmXFtzQcpOzvUXBtTUqDzEbCDGlAXDfDSYFXFIw=="
},
"@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -23476,6 +23684,11 @@
"@types/node": "*"
}
},
"@types/hogan.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.1.tgz",
"integrity": "sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"requires": {
@@ -23577,8 +23790,7 @@
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
},
"@types/range-parser": {
"version": "1.2.4",
@@ -23945,6 +24157,11 @@
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -24116,6 +24333,14 @@
"@algolia/transporter": "4.6.0"
}
},
"algoliasearch-helper": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.3.tgz",
"integrity": "sha512-TbaEvLwiuGygHQIB8y+OsJKQQ40+JKUua5B91X66tMUHyyhbNHvqyr0lqd3wCoyKx7WybyQrC0WJvzoIeh24Aw==",
"requires": {
"@algolia/events": "^4.0.1"
}
},
"ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -27838,6 +28063,22 @@
"value-equal": "^1.0.1"
}
},
"hogan.js": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
"requires": {
"mkdirp": "0.3.0",
"nopt": "1.0.10"
},
"dependencies": {
"mkdirp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew=="
}
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"requires": {
@@ -27901,6 +28142,11 @@
}
}
},
"htm": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ=="
},
"html-element-map": {
"version": "1.3.1",
"dev": true,
@@ -28221,6 +28467,33 @@
"version": "1.3.8",
"dev": true
},
"instantsearch.js": {
"version": "4.51.1",
"resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.51.1.tgz",
"integrity": "sha512-l4nETzassgSMBqOhNxntNH2MGG0KvFagwVwnXFis9P06nlspAyOdFLCQ0HA5wnlRDDev2WAZg9xRcTPCTLrwZw==",
"requires": {
"@algolia/events": "^4.0.1",
"@algolia/ui-components-highlight-vdom": "^1.2.1",
"@algolia/ui-components-shared": "^1.2.1",
"@types/dom-speech-recognition": "^0.0.1",
"@types/google.maps": "^3.45.3",
"@types/hogan.js": "^3.0.0",
"@types/qs": "^6.5.3",
"algoliasearch-helper": "^3.11.3",
"hogan.js": "^3.0.2",
"htm": "^3.0.0",
"preact": "^10.10.0",
"qs": "^6.5.1 < 6.10",
"search-insights": "^2.1.0"
},
"dependencies": {
"qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
}
}
},
"internal-slot": {
"version": "1.0.3",
"dev": true,
@@ -30206,6 +30479,14 @@
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==",
"dev": true
},
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"requires": {
"abbrev": "1"
}
},
"normalize-package-data": {
"version": "3.0.3",
"dev": true,
@@ -31136,6 +31417,11 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="
},
"prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@@ -31566,6 +31852,27 @@
"react-side-effect": "^2.1.0"
}
},
"react-instantsearch-hooks": {
"version": "6.40.1",
"resolved": "https://registry.npmjs.org/react-instantsearch-hooks/-/react-instantsearch-hooks-6.40.1.tgz",
"integrity": "sha512-Nqbmencg5816UVDkWUFpGyurAhp8WUBQgHiYj/bTnYQ7KuwXbbxIppIwexZqzXG8Q3DKVBWZw4PnmJiStLK2CA==",
"requires": {
"@babel/runtime": "^7.1.2",
"algoliasearch-helper": "^3.11.3",
"instantsearch.js": "4.51.1",
"use-sync-external-store": "^1.0.0"
}
},
"react-instantsearch-hooks-web": {
"version": "6.40.1",
"resolved": "https://registry.npmjs.org/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.40.1.tgz",
"integrity": "sha512-MbsS1nhyZWPJjSVRzkDVmb0TM4L2kjHANsr0e1NDwkVWicP8LtXdX6UINDnekMKVnk1OmoF10NAJt4LmcSOqaA==",
"requires": {
"@babel/runtime": "^7.1.2",
"instantsearch.js": "4.51.1",
"react-instantsearch-hooks": "6.40.1"
}
},
"react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -32592,6 +32899,11 @@
"ajv-keywords": "^3.5.2"
}
},
"search-insights": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.3.0.tgz",
"integrity": "sha512-0v/TTO4fbd6I91sFBK/e2zNfD0f51A+fMoYNkMplmR77NpThUye/7gIxNoJ3LejKpZH6Z2KNBIpxxFmDKj10Yw=="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -34297,6 +34609,12 @@
"tslib": "^2.0.0"
}
},
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
},
"util-deprecate": {
"version": "1.0.2",
"dev": true

View File

@@ -48,6 +48,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-instantsearch-hooks-web": "^6.40.1",
"react-redux": "7.2.9",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",

View File

@@ -1,7 +1,9 @@
// Actions for Skills Context
export const SET_GOAL = 'SET_GOAL';
export const SET_CURRENT_JOB_TITLE = 'SET_CURRENT_JOB_TITLE';
export const ADD_CAREER_INTEREST = 'ADD_CAREER_INTEREST';
export const REMOVE_CAREER_INTEREEST = 'REMOVE_CAREER_INTEREEST';
// Stepper keys
export const STEP1 = 'select-your-preferences';
export const STEP2 = 'review-your-results';

View File

@@ -1,12 +1,24 @@
import React, { createContext, useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
import reducer, { skillsInitialState } from '../data/reducer';
import { useAlgoliaSearch } from '../utils/search';
export const SkillsBuilderContext = createContext();
export const SkillsBuilderProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, skillsInitialState);
const value = useMemo(() => ([state, dispatch]), [state]);
const [searchClient, productSearchIndex, jobSearchIndex] = useAlgoliaSearch();
const value = useMemo(() => ({
state,
dispatch,
algolia: {
searchClient,
productSearchIndex,
jobSearchIndex,
},
}), [state, searchClient, productSearchIndex, jobSearchIndex]);
return (
<SkillsBuilderContext.Provider value={value}>

View File

@@ -1,63 +0,0 @@
import React, { useContext } from 'react';
import {
Button,
} from '@edx/paragon';
import {
setGoal,
setCurrentJobTitle,
addCareerInterest,
removeCareerInterest,
} from '../data/actions';
import { SkillsBuilderContext } from '../skills-builder-context';
import { useAlgoliaSearch } from '../utils/search';
const SelectPreferences = () => {
// TODO: Temporarily disable the no-unused-vars check, we'll see these later
// eslint-disable-next-line no-unused-vars
const [algoliaClient, productSearchIndex, jobSearchIndex] = useAlgoliaSearch();
const [{ currentGoal, currentJobTitle, careerInterests }, dispatch] = useContext(SkillsBuilderContext);
return (
<>
<div className="p-4">
<h3>Render Question 1</h3>
<Button onClick={() => dispatch(setGoal('learn new things'))}>
Answer question 1
</Button>
<p>Goal: {currentGoal}</p>
</div>
{currentGoal && (
<div className="p-4">
<h3>Render question 2</h3>
<Button onClick={() => dispatch(setCurrentJobTitle('Software Engineer'))}>
Answer question 2
</Button>
<p>Current Job Title: {currentJobTitle}</p>
</div>
)}
{currentJobTitle && (
<div className="p-4">
<h3>Render Question 3</h3>
<Button
onClick={() => dispatch(addCareerInterest(`Joining the circus ${Math.random().toFixed(2)}`))}
disabled={careerInterests.length >= 3}
>
Answer question 3
</Button>
<p>
Career Interests (click to remove):
{careerInterests.map(interest => (
<Button onClick={() => dispatch(removeCareerInterest(interest))}>
{interest}
</Button>
))}
</p>
</div>
)}
</>
);
};
export default SelectPreferences;

View File

@@ -1,27 +1,25 @@
import React, { useState, useContext } from 'react';
import {
Button,
Container,
Stepper,
ModalDialog,
Button, Container, Stepper, ModalDialog,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { useHistory } from 'react-router';
import {
STEP1,
STEP2,
STEP1, STEP2,
} from '../data/constants';
import messages from './messages';
import { SkillsBuilderContext } from '../skills-builder-context';
import { SkillsBuilderHeader } from '../skills-builder-header';
import SelectPreferences from './SelectPreferences';
import ViewResults from './ViewResults';
import { SelectPreferences } from './select-preferences';
import ViewResults from './view-results/ViewResults';
import headerImage from '../images/headerImage.png';
const SkillsBuilderModal = () => {
const [{ careerInterests }] = useContext(SkillsBuilderContext);
const intl = useIntl();
const { state } = useContext(SkillsBuilderContext);
const { careerInterests } = state;
const [currentStep, setCurrentStep] = useState(STEP1);
const history = useHistory();
@@ -48,13 +46,15 @@ const SkillsBuilderModal = () => {
</ModalDialog.Hero.Content>
</ModalDialog.Hero>
<Stepper.Header />
<ModalDialog.Body>
<Container size="lg">
<Stepper.Step eventKey={STEP1} title="Select preferences">
<Container size="md">
<Stepper.Step eventKey={STEP1} title={intl.formatMessage(messages.selectPreferences)}>
<SelectPreferences />
</Stepper.Step>
<Stepper.Step eventKey={STEP2} title="Review results">
<Stepper.Step eventKey={STEP2} title={intl.formatMessage(messages.reviewResults)}>
<ViewResults />
</Stepper.Step>
</Container>
@@ -62,7 +62,7 @@ const SkillsBuilderModal = () => {
<ModalDialog.Footer>
<Stepper.ActionRow eventKey={STEP1}>
<Button variant="outline-primary">
<Button variant="outline-primary" onClick={onCloseHandle}>
<FormattedMessage {...messages.goBackButton} />
</Button>
<Stepper.ActionRow.Spacer />

View File

@@ -17,6 +17,16 @@ const messages = defineMessages({
defaultMessage: 'Exit',
description: 'Button that exits the Skills Builder.',
},
selectPreferences: {
id: 'select.preferences',
defaultMessage: 'Select preferences',
description: 'The first step of the Skills Builder for selecting a goal, a current job/occupation, and career interests',
},
reviewResults: {
id: 'review.results',
defaultMessage: 'Review results',
description: 'The second step of the Skills Builder for rendering results from learner input',
},
});
export default messages;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
const CareerInterestSelect = () => (
<div>
<h4>
<FormattedMessage {...messages.careerInterestPrompt} />
</h4>
<p>
JobTitleAutosuggest component can be reused here
</p>
</div>
);
export default CareerInterestSelect;

View File

@@ -0,0 +1,38 @@
import React, { useContext } from 'react';
import {
Form,
Stack,
} from '@edx/paragon';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { setGoal } from '../../data/actions';
import { SkillsBuilderContext } from '../../skills-builder-context';
import messages from './messages';
const GoalDropdown = () => {
const intl = useIntl();
const { state, dispatch } = useContext(SkillsBuilderContext);
const { currentGoal } = state;
return (
<Stack gap={2}>
<h4><FormattedMessage {...messages.learningGoalPrompt} /></h4>
<Form.Group>
<Form.Control
as="select"
value={currentGoal}
onChange={(e) => dispatch(setGoal(e.target.value))}
>
<option value="">{intl.formatMessage(messages.selectLearningGoal)}</option>
<option>{intl.formatMessage(messages.learningGoalStartCareer)}</option>
<option>{intl.formatMessage(messages.learningGoalAdvanceCareer)}</option>
<option>{intl.formatMessage(messages.learningGoalChangeCareer)}</option>
<option>{intl.formatMessage(messages.learningGoalSomethingNew)}</option>
<option>{intl.formatMessage(messages.learningGoalSomethingElse)}</option>
</Form.Control>
</Form.Group>
</Stack>
);
};
export default GoalDropdown;

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Form,
} from '@edx/paragon';
import { useHits, useSearchBox } from 'react-instantsearch-hooks-web';
const JobTitleInstantSearch = (props) => {
const { refine } = useSearchBox(props);
const { hits } = useHits(props);
const [jobInput, setJobInput] = useState('');
const handleAutosuggestChange = (value) => {
setJobInput(value);
refine(value);
};
return (
<Form.Autosuggest
value={jobInput}
onChange={handleAutosuggestChange}
name="job-title-suggest"
onSelected={props.onSelected}
>
{hits.map(job => (
<Form.AutosuggestOption key={job.id}>
{job.name}
</Form.AutosuggestOption>
))}
</Form.Autosuggest>
);
};
JobTitleInstantSearch.propTypes = {
onSelected: PropTypes.func.isRequired,
};
export default JobTitleInstantSearch;

View File

@@ -0,0 +1,46 @@
import React, { useContext } from 'react';
import {
Form, Stack,
} from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { InstantSearch } from 'react-instantsearch-hooks-web';
import { setCurrentJobTitle } from '../../data/actions';
import { SkillsBuilderContext } from '../../skills-builder-context';
import JobTitleInstantSearch from './JobTitleInstantSearch';
import messages from './messages';
const JobTitleSelect = () => {
const { dispatch, algolia } = useContext(SkillsBuilderContext);
const { searchClient } = algolia;
// Below implementation sets the job title to "student" or "looking_for_work" — this overwrites any previous selection
// This will need to be revisited when we decide what to do with this data
const handleCheckboxChange = (e) => dispatch(setCurrentJobTitle(e.target.value));
return (
<Stack gap={2}>
<h4>
<FormattedMessage {...messages.jobTitlePrompt} />
</h4>
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
<JobTitleInstantSearch onSelected={(value) => dispatch(setCurrentJobTitle(value))} />
</InstantSearch>
<Form.Group>
<Form.CheckboxSet
name="other-occupations"
onChange={handleCheckboxChange}
>
<Form.Checkbox value="student">
<FormattedMessage {...messages.studentCheckboxPrompt} />
</Form.Checkbox>
<Form.Checkbox value="looking_for_work">
<FormattedMessage {...messages.currentlyLookingCheckboxPrompt} />
</Form.Checkbox>
</Form.CheckboxSet>
</Form.Group>
</Stack>
);
};
export default JobTitleSelect;

View File

@@ -0,0 +1,36 @@
import React, { useContext } from 'react';
import {
Stack,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { SkillsBuilderContext } from '../../skills-builder-context';
import GoalSelect from './GoalSelect';
import JobTitleSelect from './JobTitleSelect';
import CareerInterestSelect from './CareerInterestSelect';
import messages from './messages';
const SelectPreferences = () => {
const { state } = useContext(SkillsBuilderContext);
const { currentGoal, currentJobTitle } = state;
return (
<Stack gap={5}>
<p className="lead">
<FormattedMessage {...messages.skillsBuilderDescription} />
</p>
<GoalSelect />
{currentGoal && (
<JobTitleSelect />
)}
{currentJobTitle && (
<CareerInterestSelect />
)}
</Stack>
);
};
export default SelectPreferences;

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as SelectPreferences } from './SelectPreferences';

View File

@@ -0,0 +1,66 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
skillsBuilderDescription: {
id: 'skills.builder.description',
defaultMessage: 'Find the right courses and programs that help you reach your goals.',
description: 'Description of what the Skills Builder seeks to accomplish',
},
learningGoalPrompt: {
id: 'learning.goal.prompt',
defaultMessage: 'First, tell us what you want to achieve',
description: 'Prompts the user to select their current goal.',
},
selectLearningGoal: {
id: 'select.learning.goal',
defaultMessage: 'Select a goal',
description: 'Placeholder text for the goal selection component.',
},
learningGoalStartCareer: {
id: 'learning.goal.start_career',
defaultMessage: 'I want to start my career',
description: 'Selected by user if their goal is to start their career.',
},
learningGoalAdvanceCareer: {
id: 'learning.goal.advance_career',
defaultMessage: 'I want to advance my career',
description: 'Selected by user if their goal is to advance their career.',
},
learningGoalChangeCareer: {
id: 'learning.goal.change_career',
defaultMessage: 'I want to change careers',
description: 'Selected by user if their goal is to change careers.',
},
learningGoalSomethingNew: {
id: 'learning.goal.something.new',
defaultMessage: 'I want to learn something new',
description: 'Selected by user if their goal is to learn something new.',
},
learningGoalSomethingElse: {
id: 'learning.goal.something.else',
defaultMessage: 'Something else',
description: 'Selected by user if their goal is not described by the other choices.',
},
jobTitlePrompt: {
id: 'job.title.prompt',
defaultMessage: 'Next, search and select your current job title',
description: 'Prompts the user to select their current job title or occupation.',
},
studentCheckboxPrompt: {
id: 'student.checkbox.prompt',
defaultMessage: 'I\'m a student',
description: 'Label text for the corresponding checkbox',
},
currentlyLookingCheckboxPrompt: {
id: 'currently.looking.checkbox.prompt',
defaultMessage: 'I\'m currently looking for work',
description: 'Label text for the corresponding checkbox',
},
careerInterestPrompt: {
id: 'career.interest.prompt',
defaultMessage: 'What careers are you interested in?',
description: 'Prompts the user to select careers they are interested in pursuing.',
},
});
export default messages;

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as ViewResults } from './ViewResults';

View File

@@ -1,19 +1,96 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import React from 'react';
import { screen, render } from '@testing-library/react';
import {
screen, render, cleanup, fireEvent, act,
} from '@testing-library/react';
import { mergeConfig } from '@edx/frontend-platform';
import { SkillsBuilder } from '..';
import { SkillsBuilderModal } from '../skills-builder-modal';
import { SkillsBuilderProvider, SkillsBuilderContext } from '../skills-builder-context';
import { skillsInitialState } from '../data/reducer';
const SkillsBuilderWrapper = () => (
jest.mock('react-instantsearch-hooks-web', () => ({
// eslint-disable-next-line react/prop-types
InstantSearch: ({ children }) => (<div>{children}</div>),
useSearchBox: jest.fn(() => ({ refine: jest.fn() })),
useHits: jest.fn(() => ({ hits: [{ name: 'Text File Engineer' }, { name: 'Screen Viewer' }] })),
}));
const dispatchMock = jest.fn();
const contextValue = {
state: {
...skillsInitialState,
},
dispatch: dispatchMock,
algolia: {
// Without this, tests would fail to destructure `searchClient` in the <JobTitleSelect> component
searchClient: {},
},
};
const SkillsBuilderWrapperWithContext = (value) => (
<IntlProvider locale="en">
<SkillsBuilder />
<SkillsBuilderContext.Provider value={value}>
<SkillsBuilderModal />
</SkillsBuilderContext.Provider>
</IntlProvider>
);
describe('skills-builder', () => {
it('should render a Skills Builder modal', () => {
render(
<SkillsBuilderWrapper />,
);
beforeAll(async () => {
await mergeConfig({
ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name',
});
});
beforeEach(() => cleanup());
it('should render a Skills Builder modal with a prompt for the user', () => {
act(() => {
render(
<IntlProvider locale="en">
<SkillsBuilderProvider>
<SkillsBuilder />
</SkillsBuilderProvider>
</IntlProvider>,
);
});
expect(screen.getByText('Skills Builder')).toBeTruthy();
expect(screen.getByText('First, tell us what you want to achieve')).toBeTruthy();
});
it('should render the second prompt if a goal is selected', () => {
render(
SkillsBuilderWrapperWithContext(
{
...contextValue,
state: {
...contextValue.state,
currentGoal: 'I want to start my career',
},
},
),
);
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' });
fireEvent.click(checkbox);
expect(dispatchMock).toHaveBeenCalled();
});
it('should render the third prompt if a goal is selected', () => {
render(
SkillsBuilderWrapperWithContext(
{
...contextValue,
state: {
...contextValue.state,
currentGoal: 'I want to start my career',
currentJobTitle: 'Goblin Guide',
},
},
),
);
expect(screen.getByText('What careers are you interested in?')).toBeTruthy();
});
});