Compare commits
76 Commits
open-relea
...
ags/upgrad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532cd5001b | ||
|
|
87487e37d7 | ||
|
|
6b451a4437 | ||
|
|
b10b31860d | ||
|
|
83e2b66c77 | ||
|
|
6fa5681e91 | ||
|
|
e9e48e4eb0 | ||
|
|
17b4933278 | ||
|
|
68dc8a1045 | ||
|
|
21a3e9259d | ||
|
|
a6086fd4bf | ||
|
|
0c427cf5e3 | ||
|
|
20159f140e | ||
|
|
2d05de92af | ||
|
|
10f93420f4 | ||
|
|
a12f91f7a5 | ||
|
|
8f42e6fbfb | ||
|
|
4ecdf583ea | ||
|
|
e75864b860 | ||
|
|
a697e3c543 | ||
|
|
04607dba1d | ||
|
|
6a4c8d9138 | ||
|
|
dbf716eef5 | ||
|
|
5eaab4f07d | ||
|
|
a139c2f71d | ||
|
|
9e34fdd68d | ||
|
|
109c5d437d | ||
|
|
25d0ecb531 | ||
|
|
767af3c40b | ||
|
|
6a05552969 | ||
|
|
628914dce3 | ||
|
|
be7e204c91 | ||
|
|
c1d4c36a65 | ||
|
|
ce04a04c36 | ||
|
|
346c08e5d6 | ||
|
|
fe61237464 | ||
|
|
c89285f0e8 | ||
|
|
7523a1edb3 | ||
|
|
9f1c16a599 | ||
|
|
fdcef0edc8 | ||
|
|
1f056dfac7 | ||
|
|
2814349f37 | ||
|
|
a4327a98e4 | ||
|
|
50fe0ecb6f | ||
|
|
8e011fdf7b | ||
|
|
a6a35f3762 | ||
|
|
259f4b2a5e | ||
|
|
65a4091e78 | ||
|
|
e5ee7894b0 | ||
|
|
8f781ea867 | ||
|
|
7d208a91ac | ||
|
|
80599a617f | ||
|
|
652559b157 | ||
|
|
5363663170 | ||
|
|
b34041f090 | ||
|
|
a82e3e9918 | ||
|
|
6c15c2a0fd | ||
|
|
da5bf2f533 | ||
|
|
e507548d48 | ||
|
|
ff6c63c86b | ||
|
|
6e24a48570 | ||
|
|
42a0d27b47 | ||
|
|
6a79462567 | ||
|
|
afe39e8b9e | ||
|
|
38afcb8a5a | ||
|
|
5bf65de9e4 | ||
|
|
28423c261d | ||
|
|
0986fd05ab | ||
|
|
21adb70478 | ||
|
|
daca35ffbe | ||
|
|
9fe9164bb2 | ||
|
|
09e010443d | ||
|
|
87377a1443 | ||
|
|
5f89049506 | ||
|
|
4f00bc43b9 | ||
|
|
f732fa7ffc |
8
.env
8
.env
@@ -22,8 +22,12 @@ LOGO_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
FAVICON_URL=''
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL=''
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ENABLE_SKILLS_BUILDER=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ALGOLIA_APP_ID=''
|
||||
ALGOLIA_JOBS_INDEX_NAME=''
|
||||
ALGOLIA_PRODUCT_INDEX_NAME=''
|
||||
ALGOLIA_SEARCH_API_KEY=''
|
||||
|
||||
@@ -23,8 +23,12 @@ LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ENABLE_SKILLS_BUILDER='true'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ALGOLIA_APP_ID=''
|
||||
ALGOLIA_JOBS_INDEX_NAME=''
|
||||
ALGOLIA_PRODUCT_INDEX_NAME=''
|
||||
ALGOLIA_SEARCH_API_KEY=''
|
||||
|
||||
@@ -18,7 +18,12 @@ LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ALGOLIA_APP_ID=''
|
||||
ALGOLIA_JOBS_INDEX_NAME=''
|
||||
ALGOLIA_PRODUCT_INDEX_NAME=''
|
||||
ALGOLIA_SEARCH_API_KEY=''
|
||||
|
||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Update Browserslist DB
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-browserslist:
|
||||
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||
|
||||
secrets:
|
||||
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||
2
Makefile
Executable file → Normal file
2
Makefile
Executable file → Normal file
@@ -54,7 +54,7 @@ push_translations:
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
16212
package-lock.json
generated
16212
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -27,18 +27,19 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "12.0.0",
|
||||
"@edx/frontend-component-header": "4.0.0",
|
||||
"@edx/frontend-platform": "4.2.0",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "11.6.3",
|
||||
"@edx/frontend-component-header": "3.6.1",
|
||||
"@edx/frontend-platform": "3.4.0",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"algoliasearch": "4.6.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.25.5",
|
||||
"core-js": "3.27.2",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.pick": "4.4.0",
|
||||
@@ -46,29 +47,31 @@
|
||||
"prop-types": "15.8.1",
|
||||
"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",
|
||||
"react-helmet": "6.1.0",
|
||||
"redux": "4.2.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.2.1",
|
||||
"redux-saga": "1.2.2",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"universal-cookie": "3.1.0"
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "17.2.0",
|
||||
"@commitlint/config-angular": "17.2.0",
|
||||
"@commitlint/cli": "17.4.4",
|
||||
"@commitlint/config-angular": "17.4.4",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.4.19",
|
||||
"@edx/reactifex": "2.1.1",
|
||||
"@edx/frontend-build": "12.0.6",
|
||||
"@testing-library/react": "11.2.7",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"glob": "7.2.3",
|
||||
"glob": "8.1.0",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
|
||||
@@ -5,16 +5,14 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function Head({ intl }) {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['profile.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['profile.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
Head.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "تم الحفظ",
|
||||
"profile.visibility.who.just.me": "أنا فقط",
|
||||
"profile.visibility.who.everyone": "جميع من على {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "الاسم الكامل",
|
||||
"profile.name.details": "هذا هو الاسم الذي يظهر في حسابك وفي شهاداتك",
|
||||
"profile.name.empty": "إضافة الاسم",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجدّدًا.",
|
||||
"profile.viewMyRecords": "عرض سجلّاتي",
|
||||
"profile.loading": "يتم تحميل الملف الشخصي...",
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}."
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.page.title": "Perfil | {siteName}",
|
||||
"profile.age.details": "Para compartir el perfil con otros {siteName} estudiantes, debe confirmar que es mayor de 13 años.",
|
||||
"profile.age.set.date": "Establece tu fecha de nacimiento",
|
||||
"profile.datejoined.member.since": "Miembro desde {year}",
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Guardado",
|
||||
"profile.visibility.who.just.me": "Solo yo",
|
||||
"profile.visibility.who.everyone": "Todos en {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Objetivo de aprendizaje",
|
||||
"profile.learningGoal.options.start_career": "quiero empezar mi carrera",
|
||||
"profile.learningGoal.options.advance_career": "Quiero avanzar en mi carrera",
|
||||
"profile.learningGoal.options.learn_something_new": "quiero aprender algo nuevo",
|
||||
"profile.learningGoal.options.something_else": "Algo más",
|
||||
"profile.name.full.name": "Nombre completo",
|
||||
"profile.name.details": "Este es el nombre que aparecerá en tu cuenta y en tus certificados.",
|
||||
"profile.name.empty": "Añade nombre",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
|
||||
"profile.viewMyRecords": "Ver mis registros",
|
||||
"profile.loading": "Cargando perfil...",
|
||||
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}."
|
||||
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}.",
|
||||
"skills.builder.header.title": "Constructor de habilidades",
|
||||
"skills.builder.header.subheading": "Dejanos ser tu guía",
|
||||
"go.back.button": "Volver Atrás",
|
||||
"next.step.button": "Próximo paso",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Enregistré",
|
||||
"profile.visibility.who.just.me": "Juste moi",
|
||||
"profile.visibility.who.everyone": "Tout le monde sur {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Nom complet",
|
||||
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos certificats.",
|
||||
"profile.name.empty": "Ajouter un nom",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"profile.viewMyRecords": "Voir mes succès",
|
||||
"profile.loading": "Chargement du profil....",
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -16,17 +16,17 @@
|
||||
"profile.country.label": "Adresse",
|
||||
"profile.country.empty": "Ajouter un emplacement",
|
||||
"profile.education.empty": "Ajouter formation",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.education": "Formation",
|
||||
"profile.education.levels.p": "Doctorat",
|
||||
"profile.education.levels.m": "Maitrise ou diplôme professionnel",
|
||||
"profile.education.levels.m": "Maîtrise ou diplôme professionnel",
|
||||
"profile.education.levels.b": "Diplôme de baccalauréat",
|
||||
"profile.education.levels.a": "Diplôme d'associé",
|
||||
"profile.education.levels.hs": "Lycée / enseignement secondaire",
|
||||
"profile.education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
||||
"profile.education.levels.el": "Enseignement primaire",
|
||||
"profile.education.levels.none": "Sans diplôme",
|
||||
"profile.education.levels.o": "Autre niveau d'étude",
|
||||
"profile.editbutton.edit": "Modifier",
|
||||
"profile.education.levels.none": "Sans formation formelle",
|
||||
"profile.education.levels.o": "Autre niveau de formation",
|
||||
"profile.editbutton.edit": "Éditer",
|
||||
"profile.formcontrols.who.can.see": "Qui peut voir ça :",
|
||||
"profile.formcontrols.button.cancel": "Annuler",
|
||||
"profile.formcontrols.button.save": "Sauvegarder",
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Sauvegardé",
|
||||
"profile.visibility.who.just.me": "Juste moi",
|
||||
"profile.visibility.who.everyone": "Tout le monde sur {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Objectif d'apprentissage",
|
||||
"profile.learningGoal.options.start_career": "Je veux commencer ma carrière",
|
||||
"profile.learningGoal.options.advance_career": "Je veux faire progresser ma carrière",
|
||||
"profile.learningGoal.options.learn_something_new": "Je veux apprendre quelque chose de nouveau",
|
||||
"profile.learningGoal.options.something_else": "Autre chose",
|
||||
"profile.name.full.name": "Nom complet",
|
||||
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos attestations.",
|
||||
"profile.name.empty": "Ajouter un nom",
|
||||
@@ -41,12 +46,17 @@
|
||||
"profile.preferredlanguage.label": "Langue principale parlée",
|
||||
"profile.profileavatar.upload-button": "Téléverser une photo",
|
||||
"profile.profileavatar.remove.button": "Supprimer",
|
||||
"profile.image.alt.attribute": "Avatar de profil",
|
||||
"profile.image.alt.attribute": "avatar de profil",
|
||||
"profile.profileavatar.change-button": "Modifier",
|
||||
"profile.sociallinks.add": "Ajouter {network}",
|
||||
"profile.sociallinks.social.links": "Liens vers les réseaux sociaux",
|
||||
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"profile.viewMyRecords": "Afficher mes dossiers",
|
||||
"profile.loading": "Chargement du profil...",
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
|
||||
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
|
||||
"skills.builder.header.title": "Constructeur de compétences",
|
||||
"skills.builder.header.subheading": "Laissez EDUlib être votre guide",
|
||||
"go.back.button": "Retour",
|
||||
"next.step.button": "Prochaine étape",
|
||||
"exit.button": "Sortie"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -34,6 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "Saved",
|
||||
"profile.visibility.who.just.me": "Just me",
|
||||
"profile.visibility.who.everyone": "Everyone on {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "Full Name",
|
||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
||||
"profile.name.empty": "Add name",
|
||||
@@ -48,5 +53,10 @@
|
||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"profile.viewMyRecords": "View My Records",
|
||||
"profile.loading": "Profile loading...",
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'regenerator-runtime/runtime';
|
||||
import {
|
||||
APP_INIT_ERROR,
|
||||
APP_READY,
|
||||
getConfig,
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
@@ -22,6 +23,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
|
||||
|
||||
import appMessages from './i18n';
|
||||
import { ProfilePage, NotFoundPage } from './profile';
|
||||
import { SkillsBuilder } from './skills-builder';
|
||||
import configureStore from './data/configureStore';
|
||||
|
||||
import './index.scss';
|
||||
@@ -34,6 +36,9 @@ subscribe(APP_READY, () => {
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
{getConfig().ENABLE_SKILLS_BUILDER && (
|
||||
<Route path="/skills" component={SkillsBuilder} />
|
||||
)}
|
||||
<Route path="/u/:username" component={ProfilePage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
@@ -60,9 +65,13 @@ initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
ENABLE_LEARNER_RECORD_MFE: (process.env.ENABLE_LEARNER_RECORD_MFE || false),
|
||||
LEARNER_RECORD_MFE_BASE_URL: process.env.LEARNER_RECORD_MFE_BASE_URL,
|
||||
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
|
||||
ENABLE_SKILLS_BUILDER: process.env.ENABLE_SKILLS_BUILDER,
|
||||
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
|
||||
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || null,
|
||||
ALGOLIA_JOBS_INDEX_NAME: process.env.ALGOLIA_JOBS_INDEX_NAME || null,
|
||||
ALGOLIA_PRODUCT_INDEX_NAME: process.env.ALGOLIA_PRODUCT_INDEX_NAME || null,
|
||||
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || null,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,3 +6,6 @@
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import './profile/index';
|
||||
|
||||
@import './skills-builder/skills-builder-modal/skillsBuilderModal.scss';
|
||||
@import './skills-builder/skills-builder-header/skillsBuilderHeader.scss';
|
||||
|
||||
@@ -4,35 +4,33 @@ import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
function AgeMessage({ accountSettingsUrl }) {
|
||||
return (
|
||||
<Alert
|
||||
variant="info"
|
||||
dismissible={false}
|
||||
show
|
||||
>
|
||||
<Alert.Heading id="profile.age.headline">
|
||||
Your profile cannot be shared.
|
||||
</Alert.Heading>
|
||||
const AgeMessage = ({ accountSettingsUrl }) => (
|
||||
<Alert
|
||||
variant="info"
|
||||
dismissible={false}
|
||||
show
|
||||
>
|
||||
<Alert.Heading id="profile.age.headline">
|
||||
Your profile cannot be shared.
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
|
||||
description="Error message"
|
||||
tagName="p"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
<Alert.Link href={accountSettingsUrl}>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
|
||||
description="Error message"
|
||||
tagName="p"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
id="profile.age.set.date"
|
||||
defaultMessage="Set your date of birth"
|
||||
description="Label on a link to set birthday"
|
||||
/>
|
||||
<Alert.Link href={accountSettingsUrl}>
|
||||
<FormattedMessage
|
||||
id="profile.age.set.date"
|
||||
defaultMessage="Set your date of birth"
|
||||
description="Label on a link to set birthday"
|
||||
/>
|
||||
</Alert.Link>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
</Alert.Link>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
AgeMessage.propTypes = {
|
||||
accountSettingsUrl: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
function Banner() {
|
||||
return <div className="profile-page-bg-banner bg-primary d-none d-md-block p-relative" />;
|
||||
}
|
||||
const Banner = () => <div className="profile-page-bg-banner bg-primary d-none d-md-block p-relative" />;
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
function DateJoined({ date }) {
|
||||
const DateJoined = ({ date }) => {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function DateJoined({ date }) {
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DateJoined.propTypes = {
|
||||
date: PropTypes.string,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
@@ -33,6 +33,7 @@ import DateJoined from './DateJoined';
|
||||
import UsernameDescription from './UsernameDescription';
|
||||
import PageLoading from './PageLoading';
|
||||
import Banner from './Banner';
|
||||
import LearningGoal from './forms/LearningGoal';
|
||||
|
||||
// Selectors
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
@@ -46,10 +47,10 @@ class ProfilePage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const recordsUrl = this.getRecordsUrl(context);
|
||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||
|
||||
this.state = {
|
||||
viewMyRecordsUrl: recordsUrl,
|
||||
viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
|
||||
accountSettingsUrl: `${context.config.LMS_BASE_URL}/account/settings`,
|
||||
};
|
||||
|
||||
@@ -92,19 +93,6 @@ class ProfilePage extends React.Component {
|
||||
this.props.updateDraft(name, value);
|
||||
}
|
||||
|
||||
getRecordsUrl(context) {
|
||||
let recordsUrl = null;
|
||||
|
||||
if (getConfig().ENABLE_LEARNER_RECORD_MFE) {
|
||||
recordsUrl = getConfig().LEARNER_RECORD_MFE_BASE_URL;
|
||||
} else {
|
||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||
recordsUrl = credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null;
|
||||
}
|
||||
|
||||
return recordsUrl;
|
||||
}
|
||||
|
||||
isYOBDisabled() {
|
||||
const { yearOfBirth } = this.props;
|
||||
const currentYear = new Date().getFullYear();
|
||||
@@ -184,6 +172,8 @@ class ProfilePage extends React.Component {
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
learningGoal,
|
||||
visibilityLearningGoal,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
visibilityCourseCertificates,
|
||||
@@ -278,6 +268,14 @@ class ProfilePage extends React.Component {
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
|
||||
<LearningGoal
|
||||
learningGoal={learningGoal}
|
||||
visibilityLearningGoal={visibilityLearningGoal}
|
||||
formId="learningGoal"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
@@ -346,6 +344,10 @@ ProfilePage.propTypes = {
|
||||
})),
|
||||
visibilitySocialLinks: PropTypes.string.isRequired,
|
||||
|
||||
// Learning Goal form data
|
||||
learningGoal: PropTypes.string,
|
||||
visibilityLearningGoal: PropTypes.string.isRequired,
|
||||
|
||||
// Other data we need
|
||||
profileImage: PropTypes.shape({
|
||||
src: PropTypes.string,
|
||||
@@ -390,6 +392,7 @@ ProfilePage.defaultProps = {
|
||||
socialLinks: [],
|
||||
draftSocialLinksByPlatform: {},
|
||||
bio: null,
|
||||
learningGoal: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: null,
|
||||
|
||||
@@ -66,21 +66,19 @@ beforeEach(() => {
|
||||
analytics.sendTrackingLogEvent.mockReset();
|
||||
});
|
||||
|
||||
function ProfilePageWrapper({
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, match, requiresParentalConsent,
|
||||
}) {
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<ProfilePage {...requiredProfilePageProps} match={match} requiresParentalConsent={requiresParentalConsent} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<ProfilePage {...requiredProfilePageProps} match={match} requiresParentalConsent={requiresParentalConsent} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
ProfilePageWrapper.defaultProps = {
|
||||
match: { params: { username: 'staff' } },
|
||||
|
||||
@@ -4,22 +4,20 @@ import { VisibilityOff } from '@edx/paragon/icons';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
function UsernameDescription() {
|
||||
return (
|
||||
<div className="d-flex align-items-center mt-3 mb-2rem">
|
||||
<Icon src={VisibilityOff} className="icon-visibility-off" />
|
||||
<div className="username-description">
|
||||
<FormattedMessage
|
||||
id="profile.username.description"
|
||||
defaultMessage="Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
description="A description of the username field"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const UsernameDescription = () => (
|
||||
<div className="d-flex align-items-center mt-3 mb-2rem">
|
||||
<Icon src={VisibilityOff} className="icon-visibility-off" />
|
||||
<div className="username-description">
|
||||
<FormattedMessage
|
||||
id="profile.username.description"
|
||||
defaultMessage="Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
description="A description of the username field"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UsernameDescription;
|
||||
|
||||
@@ -12,7 +12,8 @@ module.exports = {
|
||||
imageUrlMedium: null,
|
||||
imageUrlLarge: null
|
||||
},
|
||||
levelOfEducation: null
|
||||
levelOfEducation: null,
|
||||
learningGoal: null
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -91,7 +92,8 @@ module.exports = {
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
@@ -104,7 +106,8 @@ module.exports = {
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -83,7 +84,8 @@ module.exports = {
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false
|
||||
isLoadingProfile: false,
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -91,7 +92,8 @@ module.exports = {
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
@@ -104,7 +106,8 @@ module.exports = {
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
|
||||
@@ -2995,7 +2995,7 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
|
||||
No formal education
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
value="o"
|
||||
>
|
||||
Other education
|
||||
</option>
|
||||
|
||||
@@ -7,7 +7,7 @@ const EDUCATION_LEVELS = [
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'other',
|
||||
'o',
|
||||
];
|
||||
|
||||
const SOCIAL = {
|
||||
|
||||
7
src/profile/data/mock_data.js
Normal file
7
src/profile/data/mock_data.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const mockData = {
|
||||
learningGoal: 'advance_career',
|
||||
editMode: 'static',
|
||||
visibilityLearningGoal: 'private',
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
92
src/profile/forms/LearningGoal.jsx
Normal file
92
src/profile/forms/LearningGoal.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import get from 'lodash.get';
|
||||
|
||||
// Mock Data
|
||||
import mockData from '../data/mock_data';
|
||||
|
||||
import messages from './LearningGoal.messages';
|
||||
|
||||
// Components
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
const LearningGoal = (props) => {
|
||||
let { learningGoal, editMode, visibilityLearningGoal } = props;
|
||||
const { intl } = props;
|
||||
|
||||
if (!learningGoal) {
|
||||
learningGoal = mockData.learningGoal;
|
||||
}
|
||||
|
||||
if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet
|
||||
editMode = mockData.editMode;
|
||||
}
|
||||
|
||||
if (!visibilityLearningGoal) {
|
||||
visibilityLearningGoal = mockData.visibilityLearningGoal;
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-5"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])}
|
||||
showVisibility={visibilityLearningGoal !== null}
|
||||
visibility={visibilityLearningGoal}
|
||||
/>
|
||||
<p data-hj-suppress className="lead">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.learningGoal.options.${learningGoal}`,
|
||||
messages['profile.learningGoal.options.something_else'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])} />
|
||||
<p data-hj-suppress className="lead">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.learningGoal.options.${learningGoal}`,
|
||||
messages['profile.learningGoal.options.something_else'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LearningGoal.propTypes = {
|
||||
// From Selector
|
||||
learningGoal: PropTypes.oneOf(['advance_career', 'start_career', 'learn_something_new', 'something_else']),
|
||||
visibilityLearningGoal: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editable', 'static']),
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
LearningGoal.defaultProps = {
|
||||
editMode: 'static',
|
||||
learningGoal: null,
|
||||
visibilityLearningGoal: 'private',
|
||||
};
|
||||
|
||||
export default connect(
|
||||
editableFormSelector,
|
||||
{},
|
||||
)(injectIntl(LearningGoal));
|
||||
31
src/profile/forms/LearningGoal.messages.jsx
Normal file
31
src/profile/forms/LearningGoal.messages.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.learningGoal.learningGoal': {
|
||||
id: 'profile.learningGoal.learningGoal',
|
||||
defaultMessage: 'Learning Goal',
|
||||
description: 'A section of a user profile that displays their current learning goal.',
|
||||
},
|
||||
'profile.learningGoal.options.start_career': {
|
||||
id: 'profile.learningGoal.options.start_career',
|
||||
defaultMessage: 'I want to start my career',
|
||||
description: 'Selected by user if their goal is to start their career.',
|
||||
},
|
||||
'profile.learningGoal.options.advance_career': {
|
||||
id: 'profile.learningGoal.options.advance_career',
|
||||
defaultMessage: 'I want to advance my career',
|
||||
description: 'Selected by user if their goal is to advance their career.',
|
||||
},
|
||||
'profile.learningGoal.options.learn_something_new': {
|
||||
id: 'profile.learningGoal.options.learn_something_new',
|
||||
defaultMessage: 'I want to learn something new',
|
||||
description: 'Selected by user if their goal is to learn something new.',
|
||||
},
|
||||
'profile.learningGoal.options.something_else': {
|
||||
id: 'profile.learningGoal.options.something_else',
|
||||
defaultMessage: 'Something else',
|
||||
description: 'Selected by user if their goal is not described by the other choices.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
122
src/profile/forms/LearningGoal.test.jsx
Normal file
122
src/profile/forms/LearningGoal.test.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import messages from '../../i18n';
|
||||
|
||||
import viewOwnProfileMockStore from '../__mocks__/viewOwnProfile.mockStore';
|
||||
import savingEditedBioMockStore from '../__mocks__/savingEditedBio.mockStore';
|
||||
|
||||
import LearningGoal from './LearningGoal';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
// props to be passed down to LearningGoal component
|
||||
const requiredLearningGoalProps = {
|
||||
formId: 'learningGoal',
|
||||
learningGoal: 'advance_career',
|
||||
drafts: {},
|
||||
visibilityLearningGoal: 'private',
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
error: null,
|
||||
openHandler: jest.fn(),
|
||||
};
|
||||
|
||||
configureI18n({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
ENVIRONMENT: 'production',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||
},
|
||||
messages,
|
||||
});
|
||||
|
||||
const LearningGoalWrapper = (props) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
}), []);
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={props.store}>
|
||||
<LearningGoal {...props} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
LearningGoalWrapper.defaultProps = {
|
||||
store: mockStore(viewOwnProfileMockStore),
|
||||
};
|
||||
|
||||
LearningGoalWrapper.propTypes = {
|
||||
store: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
const LearningGoalWrapperWithStore = ({ store }) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
}), []);
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(store)}>
|
||||
<LearningGoal {...requiredLearningGoalProps} formId="learningGoal" />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
LearningGoalWrapperWithStore.defaultProps = {
|
||||
store: mockStore(savingEditedBioMockStore),
|
||||
};
|
||||
|
||||
LearningGoalWrapperWithStore.propTypes = {
|
||||
store: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
describe('<LearningGoal />', () => {
|
||||
describe('renders the current learning goal', () => {
|
||||
it('renders "I want to advance my career"', () => {
|
||||
const learningGoalRenderer = renderer.create(
|
||||
<LearningGoalWrapper
|
||||
{...requiredLearningGoalProps}
|
||||
formId="learningGoal"
|
||||
/>,
|
||||
);
|
||||
|
||||
const learningGoalInstance = learningGoalRenderer.root;
|
||||
|
||||
expect(learningGoalInstance.findByProps({ className: 'lead' }).children).toEqual(['I want to advance my career']);
|
||||
});
|
||||
|
||||
it('renders "Something else"', () => {
|
||||
requiredLearningGoalProps.learningGoal = 'something_else';
|
||||
|
||||
const learningGoalRenderer = renderer.create(
|
||||
<LearningGoalWrapper
|
||||
{...requiredLearningGoalProps}
|
||||
formId="learningGoal"
|
||||
/>,
|
||||
);
|
||||
|
||||
const learningGoalInstance = learningGoalRenderer.root;
|
||||
|
||||
expect(learningGoalInstance.findByProps({ className: 'lead' }).children).toEqual(['Something else']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -244,14 +244,12 @@ export default connect(
|
||||
{},
|
||||
)(injectIntl(SocialLinks));
|
||||
|
||||
function SocialLink({ url, name, platform }) {
|
||||
return (
|
||||
<a href={url} className="font-weight-bold">
|
||||
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const SocialLink = ({ url, name, platform }) => (
|
||||
<a href={url} className="font-weight-bold">
|
||||
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
|
||||
SocialLink.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
@@ -259,9 +257,9 @@ SocialLink.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function EditableListItem({
|
||||
const EditableListItem = ({
|
||||
url, platform, onClickEmptyContent, name,
|
||||
}) {
|
||||
}) => {
|
||||
const linkDisplay = url ? (
|
||||
<SocialLink name={name} url={url} platform={platform} />
|
||||
) : (
|
||||
@@ -269,7 +267,7 @@ function EditableListItem({
|
||||
);
|
||||
|
||||
return <li className="form-group">{linkDisplay}</li>;
|
||||
}
|
||||
};
|
||||
|
||||
EditableListItem.propTypes = {
|
||||
url: PropTypes.string,
|
||||
@@ -282,24 +280,22 @@ EditableListItem.defaultProps = {
|
||||
onClickEmptyContent: null,
|
||||
};
|
||||
|
||||
function EditingListItem({
|
||||
const EditingListItem = ({
|
||||
platform, name, value, onChange, error,
|
||||
}) {
|
||||
return (
|
||||
<li className="form-group">
|
||||
<label htmlFor={`social-${platform}`}>{name}</label>
|
||||
<input
|
||||
className={classNames('form-control', { 'is-invalid': Boolean(error) })}
|
||||
type="text"
|
||||
id={`social-${platform}`}
|
||||
name={platform}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
aria-describedby="social-error-feedback"
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<li className="form-group">
|
||||
<label htmlFor={`social-${platform}`}>{name}</label>
|
||||
<input
|
||||
className={classNames('form-control', { 'is-invalid': Boolean(error) })}
|
||||
type="text"
|
||||
id={`social-${platform}`}
|
||||
name={platform}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
aria-describedby="social-error-feedback"
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
||||
EditingListItem.propTypes = {
|
||||
platform: PropTypes.string.isRequired,
|
||||
@@ -314,35 +310,31 @@ EditingListItem.defaultProps = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
function EmptyListItem({ onClick, name }) {
|
||||
return (
|
||||
<li className="mb-4">
|
||||
<EmptyContent onClick={onClick}>
|
||||
<FormattedMessage
|
||||
id="profile.sociallinks.add"
|
||||
defaultMessage="Add {network}"
|
||||
values={{
|
||||
network: name,
|
||||
}}
|
||||
description="{network} is the name of a social network such as Facebook or Twitter"
|
||||
/>
|
||||
</EmptyContent>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const EmptyListItem = ({ onClick, name }) => (
|
||||
<li className="mb-4">
|
||||
<EmptyContent onClick={onClick}>
|
||||
<FormattedMessage
|
||||
id="profile.sociallinks.add"
|
||||
defaultMessage="Add {network}"
|
||||
values={{
|
||||
network: name,
|
||||
}}
|
||||
description="{network} is the name of a social network such as Facebook or Twitter"
|
||||
/>
|
||||
</EmptyContent>
|
||||
</li>
|
||||
);
|
||||
|
||||
EmptyListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function StaticListItem({ name, url, platform }) {
|
||||
return (
|
||||
<li className="mb-2">
|
||||
<SocialLink name={name} url={url} platform={platform} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const StaticListItem = ({ name, url, platform }) => (
|
||||
<li className="mb-2">
|
||||
<SocialLink name={name} url={url} platform={platform} />
|
||||
</li>
|
||||
);
|
||||
|
||||
StaticListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
||||
@@ -47,7 +47,7 @@ configureI18n({
|
||||
messages,
|
||||
});
|
||||
|
||||
function SocialLinksWrapper(props) {
|
||||
const SocialLinksWrapper = (props) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
@@ -63,7 +63,7 @@ function SocialLinksWrapper(props) {
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SocialLinksWrapper.defaultProps = {
|
||||
store: mockStore(savingEditedBio),
|
||||
@@ -73,7 +73,7 @@ SocialLinksWrapper.propTypes = {
|
||||
store: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
function SocialLinksWrapperWithStore({ store }) {
|
||||
const SocialLinksWrapperWithStore = ({ store }) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
@@ -89,7 +89,7 @@ function SocialLinksWrapperWithStore({ store }) {
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SocialLinksWrapperWithStore.defaultProps = {
|
||||
store: mockStore(savingEditedBio),
|
||||
|
||||
@@ -7,22 +7,20 @@ import { Button } from '@edx/paragon';
|
||||
|
||||
import messages from './EditButton.messages';
|
||||
|
||||
function EditButton({
|
||||
const EditButton = ({
|
||||
onClick, className, style, intl,
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default injectIntl(EditButton);
|
||||
|
||||
|
||||
@@ -4,24 +4,22 @@ import PropTypes from 'prop-types';
|
||||
import EditButton from './EditButton';
|
||||
import { Visibility } from './Visibility';
|
||||
|
||||
function EditableItemHeader({
|
||||
const EditableItemHeader = ({
|
||||
content,
|
||||
showVisibility,
|
||||
visibility,
|
||||
showEditButton,
|
||||
onClickEdit,
|
||||
headingId,
|
||||
}) {
|
||||
return (
|
||||
<div className="editable-item-header mb-2">
|
||||
<h2 className="edit-section-header" id={headingId}>
|
||||
{content}
|
||||
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
||||
</h2>
|
||||
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<div className="editable-item-header mb-2">
|
||||
<h2 className="edit-section-header" id={headingId}>
|
||||
{content}
|
||||
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
||||
</h2>
|
||||
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EditableItemHeader;
|
||||
|
||||
|
||||
@@ -3,24 +3,22 @@ import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function EmptyContent({ children, onClick, showPlusIcon }) {
|
||||
return (
|
||||
<div>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pl-0 text-left btn btn-link"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
||||
{children}
|
||||
</button>
|
||||
) : children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
|
||||
<div>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pl-0 text-left btn btn-link"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
||||
{children}
|
||||
</button>
|
||||
) : children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EmptyContent;
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import messages from './FormControls.messages';
|
||||
|
||||
import { VisibilitySelect } from './Visibility';
|
||||
|
||||
function FormControls({
|
||||
const FormControls = ({
|
||||
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
||||
}) {
|
||||
}) => {
|
||||
// Eliminate error/failed state for save button
|
||||
const buttonState = saveState === 'error' ? null : saveState;
|
||||
|
||||
@@ -57,7 +57,7 @@ function FormControls({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(FormControls);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const onChildExit = (htmlNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const SwitchContent = ({ expression, cases, className }) => {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
@@ -48,7 +48,7 @@ function SwitchContent({ expression, cases, className }) {
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import messages from './Visibility.messages';
|
||||
|
||||
function Visibility({ to, intl }) {
|
||||
const Visibility = ({ to, intl }) => {
|
||||
const icon = to === 'private' ? faEyeSlash : faEye;
|
||||
const label = to === 'private'
|
||||
? intl.formatMessage(messages['profile.visibility.who.just.me'])
|
||||
@@ -18,7 +18,7 @@ function Visibility({ to, intl }) {
|
||||
<FontAwesomeIcon icon={icon} /> {label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Visibility.propTypes = {
|
||||
to: PropTypes.oneOf(['private', 'all_users']),
|
||||
@@ -30,7 +30,7 @@ Visibility.defaultProps = {
|
||||
to: 'private',
|
||||
};
|
||||
|
||||
function VisibilitySelect({ intl, className, ...props }) {
|
||||
const VisibilitySelect = ({ intl, className, ...props }) => {
|
||||
const { value } = props;
|
||||
const icon = value === 'private' ? faEyeSlash : faEye;
|
||||
|
||||
@@ -49,7 +49,7 @@ function VisibilitySelect({ intl, className, ...props }) {
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
VisibilitySelect.propTypes = {
|
||||
id: PropTypes.string,
|
||||
|
||||
11
src/skills-builder/SkillsBuilder.jsx
Normal file
11
src/skills-builder/SkillsBuilder.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { SkillsBuilderModal } from './skills-builder-modal';
|
||||
import { SkillsBuilderProvider } from './skills-builder-context';
|
||||
|
||||
const SkillsBuilder = () => (
|
||||
<SkillsBuilderProvider>
|
||||
<SkillsBuilderModal />
|
||||
</SkillsBuilderProvider>
|
||||
);
|
||||
|
||||
export default SkillsBuilder;
|
||||
26
src/skills-builder/data/actions.js
Normal file
26
src/skills-builder/data/actions.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} from './constants';
|
||||
|
||||
export const setGoal = (payload) => ({
|
||||
type: SET_GOAL,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const setCurrentJobTitle = (payload) => ({
|
||||
type: SET_CURRENT_JOB_TITLE,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const addCareerInterest = (payload) => ({
|
||||
type: ADD_CAREER_INTEREST,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const removeCareerInterest = (payload) => ({
|
||||
type: REMOVE_CAREER_INTEREEST,
|
||||
payload,
|
||||
});
|
||||
9
src/skills-builder/data/constants.js
Normal file
9
src/skills-builder/data/constants.js
Normal file
@@ -0,0 +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';
|
||||
41
src/skills-builder/data/reducer.js
Normal file
41
src/skills-builder/data/reducer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} from './constants';
|
||||
|
||||
export function skillsReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case SET_GOAL:
|
||||
return {
|
||||
...state,
|
||||
currentGoal: action.payload,
|
||||
};
|
||||
case SET_CURRENT_JOB_TITLE:
|
||||
return {
|
||||
...state,
|
||||
currentJobTitle: action.payload,
|
||||
};
|
||||
case ADD_CAREER_INTEREST:
|
||||
return {
|
||||
...state,
|
||||
careerInterests: [...state.careerInterests, action.payload],
|
||||
};
|
||||
case REMOVE_CAREER_INTEREEST:
|
||||
return {
|
||||
...state,
|
||||
careerInterests: state.careerInterests.filter(interest => interest !== action.payload),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const skillsInitialState = {
|
||||
currentGoal: '',
|
||||
currentJobTitle: '',
|
||||
careerInterests: [],
|
||||
};
|
||||
|
||||
export default skillsReducer;
|
||||
60
src/skills-builder/data/test/reducer.test.js
Normal file
60
src/skills-builder/data/test/reducer.test.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { skillsReducer, skillsInitialState } from '../reducer';
|
||||
import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} from '../constants';
|
||||
|
||||
describe('skillsReducer', () => {
|
||||
const testState = skillsInitialState;
|
||||
beforeEach(() => jest.resetModules());
|
||||
|
||||
it('does not remove present data when SET_GOAL action is dispatched', () => {
|
||||
const newGoalPayload = 'test-goal';
|
||||
const returnedState = skillsReducer(testState, { type: SET_GOAL, payload: newGoalPayload });
|
||||
const finalState = {
|
||||
...testState,
|
||||
currentGoal: 'test-goal',
|
||||
};
|
||||
expect(returnedState).toEqual(finalState);
|
||||
});
|
||||
|
||||
it('does not remove present data when SET_JOB_TITLE action is dispatched', () => {
|
||||
const newJobTitlePayload = 'test-job-title';
|
||||
const returnedState = skillsReducer(testState, { type: SET_CURRENT_JOB_TITLE, payload: newJobTitlePayload });
|
||||
const finalState = {
|
||||
...testState,
|
||||
currentJobTitle: 'test-job-title',
|
||||
};
|
||||
expect(returnedState).toEqual(finalState);
|
||||
});
|
||||
|
||||
it('adds a careerInterest when ADD_CAREER_INTEREST action is dispatched', () => {
|
||||
const newCareerInterestPayload = 'test-career-interest';
|
||||
const returnedState = skillsReducer(testState, { type: ADD_CAREER_INTEREST, payload: newCareerInterestPayload });
|
||||
const finalState = {
|
||||
...testState,
|
||||
careerInterests: [...testState.careerInterests, 'test-career-interest'],
|
||||
};
|
||||
expect(returnedState).toEqual(finalState);
|
||||
});
|
||||
|
||||
it('removes a careerInterest when REMOVE_CAREER_INTEREST action is dispatched', () => {
|
||||
const newCareerInterestPayload = 'test-career-interest';
|
||||
const testStateWithInterest = {
|
||||
...testState,
|
||||
careerInterests: [newCareerInterestPayload],
|
||||
};
|
||||
const returnedState = skillsReducer(
|
||||
testStateWithInterest,
|
||||
{ type: REMOVE_CAREER_INTEREEST, payload: newCareerInterestPayload },
|
||||
);
|
||||
const finalState = {
|
||||
...testStateWithInterest,
|
||||
// override the 'careerInterests` field and remove 'test-career-interest' from the array
|
||||
careerInterests: testStateWithInterest.careerInterests.filter(interest => interest !== newCareerInterestPayload),
|
||||
};
|
||||
expect(returnedState).toEqual(finalState);
|
||||
});
|
||||
});
|
||||
3
src/skills-builder/images/edX-logo.svg
Normal file
3
src/skills-builder/images/edX-logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="148" height="83" viewBox="0 0 148 83" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.6992 0L94.8413 14.525H148L133.537 83H66.3135L70.1777 64.325H0L13.7661 0H97.6992ZM45.3759 37.6596C45.419 37.1851 45.4513 36.7212 45.4729 36.2654C45.4944 35.8123 45.5052 35.4154 45.5025 35.0776C45.5025 32.8602 45.1523 30.8949 44.452 29.1842C43.7517 27.4736 42.8063 26.0311 41.6184 24.8594C40.4306 23.685 39.0461 22.7948 37.465 22.1808C35.8812 21.5668 34.2112 21.2611 32.4496 21.2611C29.8396 21.2611 27.4343 21.7679 25.2391 22.7814C23.0412 23.7949 21.1584 25.2267 19.588 27.0741C18.0177 28.9241 16.7922 31.1415 15.9114 33.7289C15.0306 36.3163 14.5915 39.1853 14.5915 42.333C14.5915 44.6362 14.9579 46.674 15.6905 48.449C16.4231 50.2239 17.4198 51.7174 18.683 52.932C19.9463 54.1466 21.4277 55.0663 23.1246 55.6883C24.8216 56.313 26.6262 56.6241 28.5359 56.6241C30.5319 56.6241 32.3608 56.3854 34.0281 55.9109C35.6927 55.4363 37.1579 54.7794 38.4212 53.9455C39.6845 53.1116 40.7296 52.1223 41.5565 50.9827C42.3834 49.8432 42.9787 48.6179 43.3396 47.3067H37.5135C36.7916 48.6393 35.7411 49.6957 34.3621 50.476C32.983 51.2562 31.1568 51.6477 28.8861 51.6477C27.8464 51.6477 26.8175 51.4734 25.7993 51.1248C24.7812 50.7763 23.8627 50.2132 23.0465 49.4303C22.2277 48.6474 21.5651 47.6392 21.056 46.4032C20.5469 45.1671 20.2911 43.6737 20.2911 41.9201C20.2911 41.6252 20.2964 41.341 20.3072 41.0648C20.318 40.7913 20.3342 40.5071 20.3557 40.2095H45.0904C45.155 39.8931 45.2062 39.507 45.2493 39.0539C45.2762 38.7515 45.3044 38.4432 45.333 38.1306L45.3759 37.6596ZM37.8017 28.5488C37.1445 27.7873 36.3256 27.1867 35.3506 26.7416C34.3728 26.2992 33.2388 26.0767 31.9433 26.0767C30.6261 26.0767 29.3952 26.3099 28.2504 26.7738C27.103 27.2376 26.0633 27.8999 25.1313 28.7552C24.1967 29.6105 23.3913 30.6348 22.7125 31.8279C22.0338 33.0211 21.4924 34.3483 21.0883 35.8042H39.7114C39.7329 35.6997 39.7491 35.5039 39.7599 35.217C39.7706 34.9328 39.776 34.6513 39.776 34.3778C39.776 33.257 39.6117 32.1953 39.2831 31.1925C38.9518 30.1924 38.4589 29.3102 37.8017 28.5488ZM71.4707 56.0235L72.4565 51.3661H71.9475C70.695 52.9937 69.1246 54.2753 67.2365 55.2164C65.3483 56.1549 63.3093 56.6268 61.1249 56.6268C59.4064 56.6268 57.793 56.305 56.2846 55.6588C54.7762 55.0153 53.4564 54.0903 52.3224 52.8864C51.1857 51.6825 50.2888 50.2239 49.6316 48.5133C48.9743 46.8027 48.6457 44.8909 48.6457 42.7781C48.6457 40.7297 48.8531 38.7697 49.2652 36.9008C49.68 35.032 50.2672 33.2999 51.0322 31.7046C51.7972 30.1092 52.713 28.6667 53.785 27.3797C54.857 26.0901 56.0449 24.9934 57.3512 24.0845C58.6576 23.1755 60.0663 22.4784 61.5854 21.9931C63.1019 21.5078 64.6884 21.2638 66.3449 21.2638C67.5759 21.2638 68.7475 21.4327 69.8627 21.7706C70.9778 22.1084 71.9852 22.5776 72.8875 23.1809C73.7898 23.7842 74.5629 24.5108 75.212 25.3661C75.8585 26.2214 76.3325 27.1572 76.6288 28.1707H77.1379L81.3721 8.3H86.9423L76.7554 56.0261H71.4707V56.0235ZM70.3556 26.8381C71.4169 27.2805 72.3192 27.9106 73.0626 28.723C73.806 29.5381 74.3771 30.5248 74.7784 31.6831C75.1824 32.8468 75.3844 34.1552 75.3844 35.6138C75.3844 37.8741 75.0639 39.9816 74.4282 41.9362C73.7926 43.8908 72.9118 45.5907 71.7859 47.0386C70.6627 48.4838 69.3402 49.626 67.8237 50.4599C66.3046 51.2964 64.6561 51.712 62.873 51.712C61.6016 51.712 60.4434 51.4734 59.4037 50.9988C58.364 50.5216 57.4725 49.8566 56.7291 49.0013C55.9856 48.146 55.4092 47.1164 54.9944 45.9125C54.5796 44.7086 54.3722 43.3894 54.3722 41.9523C54.3722 39.6491 54.6927 37.5309 55.3284 35.5978C55.9641 33.6673 56.8341 32.0022 57.9384 30.608C59.0428 29.2137 60.3465 28.1278 61.8548 27.3449C63.3605 26.5646 64.9955 26.1732 66.7571 26.1732C68.0957 26.1732 69.2944 26.393 70.3556 26.8381ZM126.409 74.7H114.031L105.938 57.0191H104.927L89.9496 74.7H77.6479L100.989 47.1736L90.1767 22.825H102.789L109.982 39.5884H110.652L124.254 22.825H136.724L114.442 48.3925L126.409 74.7Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/skills-builder/images/headerImage.png
Normal file
BIN
src/skills-builder/images/headerImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
2
src/skills-builder/index.js
Normal file
2
src/skills-builder/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as SkillsBuilder } from './SkillsBuilder';
|
||||
@@ -0,0 +1,32 @@
|
||||
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 [searchClient, productSearchIndex, jobSearchIndex] = useAlgoliaSearch();
|
||||
|
||||
const value = useMemo(() => ({
|
||||
state,
|
||||
dispatch,
|
||||
algolia: {
|
||||
searchClient,
|
||||
productSearchIndex,
|
||||
jobSearchIndex,
|
||||
},
|
||||
}), [state, searchClient, productSearchIndex, jobSearchIndex]);
|
||||
|
||||
return (
|
||||
<SkillsBuilderContext.Provider value={value}>
|
||||
{children}
|
||||
</SkillsBuilderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
SkillsBuilderProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
};
|
||||
2
src/skills-builder/skills-builder-context/index.js
Normal file
2
src/skills-builder/skills-builder-context/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { SkillsBuilderProvider, SkillsBuilderContext } from './SkillsBuilderProvider';
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import edXLogo from '../images/edX-logo.svg';
|
||||
import messages from './messages';
|
||||
|
||||
const SkillsBuilderHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<img src={edXLogo} alt="edx-logo" className="mt-2 h-50" />
|
||||
<div className="ml-5 vertical-line" />
|
||||
<div className="w-100 ml-5">
|
||||
<h1 className="h1 text-warning-300">
|
||||
{formatMessage(messages.skillsBuilderHeaderTitle)}
|
||||
</h1>
|
||||
<p className="h2 text-white">
|
||||
{formatMessage(messages.skillsBuilderHeaderSubheading)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsBuilderHeader;
|
||||
2
src/skills-builder/skills-builder-header/index.js
Normal file
2
src/skills-builder/skills-builder-header/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as SkillsBuilderHeader } from './SkillsBuilderHeader';
|
||||
16
src/skills-builder/skills-builder-header/messages.js
Normal file
16
src/skills-builder/skills-builder-header/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
skillsBuilderHeaderTitle: {
|
||||
id: 'skills.builder.header.title',
|
||||
defaultMessage: 'Skills Builder',
|
||||
description: 'Title for the Skills Builder feature',
|
||||
},
|
||||
skillsBuilderHeaderSubheading: {
|
||||
id: 'skills.builder.header.subheading',
|
||||
defaultMessage: 'Let edX be your guide',
|
||||
description: 'Subheading to the Skills Builder title in the header component',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,4 @@
|
||||
.vertical-line {
|
||||
border-left: 7px solid #D23228;
|
||||
transform: rotate(13deg);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import {
|
||||
Button, Container, Stepper, ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
STEP1, STEP2,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
import { SkillsBuilderContext } from '../skills-builder-context';
|
||||
import { SkillsBuilderHeader } from '../skills-builder-header';
|
||||
import { SelectPreferences } from './select-preferences';
|
||||
import ViewResults from './view-results/ViewResults';
|
||||
|
||||
import headerImage from '../images/headerImage.png';
|
||||
|
||||
const SkillsBuilderModal = () => {
|
||||
const intl = useIntl();
|
||||
const { state } = useContext(SkillsBuilderContext);
|
||||
const { careerInterests } = state;
|
||||
const [currentStep, setCurrentStep] = useState(STEP1);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const onCloseHandle = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stepper activeKey={currentStep}>
|
||||
<ModalDialog
|
||||
title="Skills Builder"
|
||||
size="fullscreen"
|
||||
className="skills-builder-modal"
|
||||
isOpen
|
||||
onClose={onCloseHandle}
|
||||
>
|
||||
<ModalDialog.Hero>
|
||||
<ModalDialog.Hero.Background className="bg-primary-500">
|
||||
<img src={headerImage} alt="" className="h-100" />
|
||||
</ModalDialog.Hero.Background>
|
||||
<ModalDialog.Hero.Content>
|
||||
<SkillsBuilderHeader />
|
||||
</ModalDialog.Hero.Content>
|
||||
</ModalDialog.Hero>
|
||||
|
||||
<Stepper.Header />
|
||||
|
||||
<ModalDialog.Body>
|
||||
<Container size="md">
|
||||
<Stepper.Step eventKey={STEP1} title={intl.formatMessage(messages.selectPreferences)}>
|
||||
<SelectPreferences />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step eventKey={STEP2} title={intl.formatMessage(messages.reviewResults)}>
|
||||
<ViewResults />
|
||||
</Stepper.Step>
|
||||
</Container>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<Stepper.ActionRow eventKey={STEP1}>
|
||||
<Button variant="outline-primary" onClick={onCloseHandle}>
|
||||
<FormattedMessage {...messages.goBackButton} />
|
||||
</Button>
|
||||
<Stepper.ActionRow.Spacer />
|
||||
<Button
|
||||
onClick={() => setCurrentStep(STEP2)}
|
||||
disabled={careerInterests.length === 0}
|
||||
>
|
||||
<FormattedMessage {...messages.nextStepButton} />
|
||||
</Button>
|
||||
</Stepper.ActionRow>
|
||||
<Stepper.ActionRow eventKey={STEP2}>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => setCurrentStep(STEP1)}
|
||||
>
|
||||
<FormattedMessage {...messages.goBackButton} />
|
||||
</Button>
|
||||
<Stepper.ActionRow.Spacer />
|
||||
<Button onClick={onCloseHandle}>
|
||||
<FormattedMessage {...messages.exitButton} />
|
||||
</Button>
|
||||
</Stepper.ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
</Stepper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsBuilderModal;
|
||||
2
src/skills-builder/skills-builder-modal/index.js
Normal file
2
src/skills-builder/skills-builder-modal/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as SkillsBuilderModal } from './SkillsBuilderModal';
|
||||
32
src/skills-builder/skills-builder-modal/messages.js
Normal file
32
src/skills-builder/skills-builder-modal/messages.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
/* Modal Action Row Buttons */
|
||||
goBackButton: {
|
||||
id: 'go.back.button',
|
||||
defaultMessage: 'Go Back',
|
||||
description: 'Button that sends the user to the previous step in the skills builder.',
|
||||
},
|
||||
nextStepButton: {
|
||||
id: 'next.step.button',
|
||||
defaultMessage: 'Next Step',
|
||||
description: 'Button that sends the user to the next step in the skills builder.',
|
||||
},
|
||||
exitButton: {
|
||||
id: 'exit.button',
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as SelectPreferences } from './SelectPreferences';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
.skills-builder-modal {
|
||||
button[aria-label="Close"][type="button"]{
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const ViewResults = () => (
|
||||
<h3>Results will render on this step</h3>
|
||||
);
|
||||
|
||||
export default ViewResults;
|
||||
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as ViewResults } from './ViewResults';
|
||||
96
src/skills-builder/test/SkillsBuilder.test.jsx
Normal file
96
src/skills-builder/test/SkillsBuilder.test.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import React from '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';
|
||||
|
||||
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">
|
||||
<SkillsBuilderContext.Provider value={value}>
|
||||
<SkillsBuilderModal />
|
||||
</SkillsBuilderContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('skills-builder', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
110
src/skills-builder/utils/search.jsx
Normal file
110
src/skills-builder/utils/search.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Algolia utility functions used by the Skills Builder feature.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import algoliasearch from 'algoliasearch';
|
||||
|
||||
/*
|
||||
* Utility function to create and return an Algolia client, as well as Index objects for our product and job data.
|
||||
*
|
||||
* @return {SearchClient} searchClient - An instantiated Algolia client
|
||||
* @return {SearchIndex} productSearchIndex - An Algolia index of product data. Used to retrieve product
|
||||
* recommendations for learners
|
||||
* @return {SearchIndex} jobSearchIndex - An Algolia index of job taxonomy data. Used to retrieve job metadata that a
|
||||
* learner is interested in.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useAlgoliaSearch = () => {
|
||||
const config = getConfig();
|
||||
|
||||
const [searchClient, productSearchIndex, jobSearchIndex] = useMemo(
|
||||
() => {
|
||||
const client = algoliasearch(
|
||||
config.ALGOLIA_APP_ID,
|
||||
config.ALGOLIA_SEARCH_API_KEY,
|
||||
);
|
||||
const productIndex = client.initIndex(config.ALGOLIA_PRODUCT_INDEX_NAME);
|
||||
const jobIndex = client.initIndex(config.ALGOLIA_JOBS_INDEX_NAME);
|
||||
return [client, productIndex, jobIndex];
|
||||
},
|
||||
[
|
||||
config.ALGOLIA_APP_ID,
|
||||
config.ALGOLIA_PRODUCT_INDEX_NAME,
|
||||
config.ALGOLIA_JOBS_INDEX_NAME,
|
||||
config.ALGOLIA_SEARCH_API_KEY,
|
||||
],
|
||||
);
|
||||
return [searchClient, productSearchIndex, jobSearchIndex];
|
||||
};
|
||||
|
||||
/*
|
||||
* Utility function used to format a list of data so it matches syntax Algolia expects.
|
||||
*
|
||||
* @param {String} facetFilterType - A string declaring the facet filter type to prepend each search item (e.g. `name`)
|
||||
* @param {Array[String]} data - An array of job or skills used to query data in Algolia.
|
||||
*
|
||||
* @return {Array[String]} formattedData - The transformed array of data to search prepended with the facet filter type
|
||||
*/
|
||||
export function formatFacetFilterData(facetFilterType, data) {
|
||||
const formattedData = [];
|
||||
if (data) {
|
||||
data.forEach(item => formattedData.push(`${facetFilterType}:${item}`));
|
||||
}
|
||||
|
||||
return formattedData;
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility function responsible for querying and returning job information based on input received from a learner.
|
||||
*
|
||||
* @param {SearchIndex} jobIndex - An Algolia index of taxonomy connector data used to retrieve job information a
|
||||
* learner is interested in
|
||||
* @param {Array[String]} jobNames - A list of job names a learner is interested in
|
||||
*
|
||||
* @return {Array[Object]} results - Job information retrieved from Algolia
|
||||
*/
|
||||
export const searchJobs = async (jobIndex, jobNames) => {
|
||||
const formattedJobNames = formatFacetFilterData('name', jobNames);
|
||||
try {
|
||||
const { hits } = await jobIndex.search('', {
|
||||
facetFilters: [
|
||||
formattedJobNames,
|
||||
],
|
||||
});
|
||||
return hits;
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/*
|
||||
* Utility function responsible for returning recommendations on products based on the skills of a job a learner is
|
||||
* interested in.
|
||||
*
|
||||
* @param {SearchIndex} productIndex - An Algolia index of product data used to retrieve recommendations for learners.
|
||||
* @param {String} productType - The type of product information you are trying to retrieve (e.g. `course` or `program`)
|
||||
* @param {Array[String]} skills - An array of skill names related to a job/career a learner expressed interest in
|
||||
*
|
||||
* @return {Array[Object]} results - Product information retrieved from Algolia
|
||||
*/
|
||||
export const getProductRecommendations = async (productIndex, productType, skills) => {
|
||||
const formattedSkillNames = formatFacetFilterData('skills.skill', skills);
|
||||
try {
|
||||
const { hits } = await productIndex.search('', {
|
||||
filters: `product:${productType}`,
|
||||
facetFilters: [
|
||||
formattedSkillNames,
|
||||
],
|
||||
});
|
||||
return hits;
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
74
src/skills-builder/utils/tests/search.test.jsx
Normal file
74
src/skills-builder/utils/tests/search.test.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
formatFacetFilterData,
|
||||
getProductRecommendations,
|
||||
searchJobs,
|
||||
} from '../search';
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging');
|
||||
|
||||
const mockAlgoliaResult = {
|
||||
hits: [
|
||||
{
|
||||
key: 'test-course-key',
|
||||
title: 'Test Title',
|
||||
skill_names: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Skill Name',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockIndex = {
|
||||
search: jest.fn().mockImplementation(() => mockAlgoliaResult),
|
||||
};
|
||||
|
||||
describe('Algolias utility function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('formatFacetFilterData() should return a new array with data formatted as expected', () => {
|
||||
const result = formatFacetFilterData('name', ['Organic Farmer']);
|
||||
expect(result).toEqual(['name:Organic Farmer']);
|
||||
});
|
||||
|
||||
it('searchJobs() queries Algolia with the expected search parameters', async () => {
|
||||
const expectedSearchParameters = {
|
||||
facetFilters: [
|
||||
['name:Enchanter'],
|
||||
],
|
||||
};
|
||||
|
||||
const results = await searchJobs(mockIndex, ['Enchanter']);
|
||||
expect(mockIndex.search).toHaveBeenCalledTimes(1);
|
||||
expect(mockIndex.search).toHaveBeenCalledWith('', expectedSearchParameters);
|
||||
expect(results).toEqual(mockAlgoliaResult.hits);
|
||||
});
|
||||
|
||||
it('searchJobs() returns an empty array when an exception occurs querying Algolia', async () => {
|
||||
const results = await searchJobs(null, ['Organic Farmer']);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('getProductRecommendations() queries Algolia with the expected search parameters', async () => {
|
||||
const expectedSearchParameters = {
|
||||
filters: 'product:Course',
|
||||
facetFilters: [
|
||||
['skills.skill:Sword Lobbing'],
|
||||
],
|
||||
};
|
||||
|
||||
const results = await getProductRecommendations(mockIndex, 'Course', ['Sword Lobbing']);
|
||||
expect(mockIndex.search).toHaveBeenCalledTimes(1);
|
||||
expect(mockIndex.search).toHaveBeenCalledWith('', expectedSearchParameters);
|
||||
expect(results).toEqual(mockAlgoliaResult.hits);
|
||||
});
|
||||
|
||||
it('getProductRecommendations() returns an empty array when an exception occurs querying Algolia', async () => {
|
||||
const results = await getProductRecommendations(null, 'Course', ['Management']);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user