Compare commits
288 Commits
aperture/P
...
sundass/sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cc8e8b7d2 | ||
|
|
60d960276d | ||
|
|
c8a6f9fbd8 | ||
|
|
7db0d9d6c9 | ||
|
|
2ef5a7baff | ||
|
|
d59c641b3d | ||
|
|
4ea80a2a09 | ||
|
|
45fe50f7f7 | ||
|
|
933a177c78 | ||
|
|
14fff570a4 | ||
|
|
3d0f3806e1 | ||
|
|
b597e2cc14 | ||
|
|
22dc85470a | ||
|
|
9a26dc7088 | ||
|
|
770a248d8c | ||
|
|
70cb1803b4 | ||
|
|
e9aa787ade | ||
|
|
01e2f1af79 | ||
|
|
1e67d51394 | ||
|
|
95936419c2 | ||
|
|
00ada93994 | ||
|
|
c57a924cc3 | ||
|
|
dc2f03dfad | ||
|
|
5c7a521705 | ||
|
|
074374b2af | ||
|
|
0a3aad38dc | ||
|
|
1730b5a2c4 | ||
|
|
12269c2c8e | ||
|
|
630dbefb7e | ||
|
|
00c8697c59 | ||
|
|
8df3d06598 | ||
|
|
f6ed6ee1f5 | ||
|
|
7e8d22ec6a | ||
|
|
d0595e679d | ||
|
|
4c7e713b6e | ||
|
|
bc0683281f | ||
|
|
b954345b38 | ||
|
|
ca4f78bd1c | ||
|
|
3dc8b156fe | ||
|
|
a818cd4dc4 | ||
|
|
2e9dcd165e | ||
|
|
53a52b8f06 | ||
|
|
c66facee92 | ||
|
|
1d0059e6fb | ||
|
|
e8be5b5926 | ||
|
|
c9b3fb929d | ||
|
|
b92efcd422 | ||
|
|
3270e27c94 | ||
|
|
01cd125d4f | ||
|
|
c025310952 | ||
|
|
5fcef4edf4 | ||
|
|
d33c79a525 | ||
|
|
d36c61d44e | ||
|
|
aed6081d37 | ||
|
|
f3a328cbb3 | ||
|
|
66a4eef910 | ||
|
|
6da6fedc57 | ||
|
|
cd7361690b | ||
|
|
a7e095f3bf | ||
|
|
9a393d8e43 | ||
|
|
a03cdc81c0 | ||
|
|
2a99330801 | ||
|
|
1a5c4f2404 | ||
|
|
13aaca03fd | ||
|
|
422aff1915 | ||
|
|
cac4e42364 | ||
|
|
c6d39884c8 | ||
|
|
4bb3678d4c | ||
|
|
5c2951de40 | ||
|
|
35d9ae8fec | ||
|
|
ff32632411 | ||
|
|
2c97964f0b | ||
|
|
fa748719ae | ||
|
|
1fb7b96c40 | ||
|
|
e13d04a5ea | ||
|
|
ca2a79d16f | ||
|
|
2cbdc5e315 | ||
|
|
e3f5129746 | ||
|
|
24b054f55f | ||
|
|
776cd125a2 | ||
|
|
8498d9f04f | ||
|
|
0fa58d8112 | ||
|
|
312b114ef7 | ||
|
|
a05b29e406 | ||
|
|
6e3e67467a | ||
|
|
09b5c8b72d | ||
|
|
5e71737240 | ||
|
|
eb726e80ab | ||
|
|
4f1272b01c | ||
|
|
30e3fcdcba | ||
|
|
b13fc58bad | ||
|
|
31b31ba345 | ||
|
|
f7449bfdcc | ||
|
|
cb0f05955f | ||
|
|
f1586d260a | ||
|
|
b268c43978 | ||
|
|
ed58a41042 | ||
|
|
7e4ba48de8 | ||
|
|
e9e16cc595 | ||
|
|
88d786b49f | ||
|
|
a24fea54ec | ||
|
|
8980b22fff | ||
|
|
389d923a08 | ||
|
|
714851c329 | ||
|
|
b27300e63d | ||
|
|
0869a82a31 | ||
|
|
507525262c | ||
|
|
a69170a4d2 | ||
|
|
26203bb744 | ||
|
|
21fb4702b4 | ||
|
|
aa8d1f9473 | ||
|
|
b6e9df9778 | ||
|
|
a7ed0cd62a | ||
|
|
e61fada0b2 | ||
|
|
3b0b1be197 | ||
|
|
616847c6b5 | ||
|
|
95b41131fc | ||
|
|
59a3150706 | ||
|
|
774b67b208 | ||
|
|
9a855cd30e | ||
|
|
9c6647aeae | ||
|
|
ecdec9083a | ||
|
|
1c0746c907 | ||
|
|
e7f8a5d1ff | ||
|
|
d927b43c01 | ||
|
|
1a36d69bd8 | ||
|
|
db90ce5975 | ||
|
|
b22d411481 | ||
|
|
d7fff65162 | ||
|
|
cf5d69c5ba | ||
|
|
53d26746c1 | ||
|
|
048433a135 | ||
|
|
31136b0523 | ||
|
|
caaf8547dc | ||
|
|
e92332859a | ||
|
|
55529b0b74 | ||
|
|
7682971b63 | ||
|
|
6aaf615c63 | ||
|
|
3512fc56c9 | ||
|
|
fda2ef1977 | ||
|
|
3784c2a6df | ||
|
|
04d74389ed | ||
|
|
1d338d0127 | ||
|
|
0fd6e34a8e | ||
|
|
9d89c79fb2 | ||
|
|
bfc31696ee | ||
|
|
8ebdc1030f | ||
|
|
8c3d706071 | ||
|
|
ee8912530d | ||
|
|
db91e0f1a4 | ||
|
|
5f2d0c089e | ||
|
|
75be372ec9 | ||
|
|
9ef81f7485 | ||
|
|
6a32790f28 | ||
|
|
9f5f7f9a64 | ||
|
|
06f6e3537a | ||
|
|
44f0295d0d | ||
|
|
19be2e93e2 | ||
|
|
5090a62594 | ||
|
|
dc2bbee481 | ||
|
|
8dc37f9bba | ||
|
|
14d93d2f6c | ||
|
|
768351fc15 | ||
|
|
c3dc8b7330 | ||
|
|
9fd2d3f75a | ||
|
|
760fa0c6a2 | ||
|
|
cf6a62266c | ||
|
|
f16fd36a24 | ||
|
|
c30e1b3f17 | ||
|
|
9985afde2d | ||
|
|
ccd6e95f70 | ||
|
|
fb904a5cc2 | ||
|
|
a255bbe2b8 | ||
|
|
a314878cc4 | ||
|
|
61b8b0a509 | ||
|
|
f144cd857e | ||
|
|
c922a1cd29 | ||
|
|
6cefe7b3c0 | ||
|
|
a3611b026c | ||
|
|
d33d3d9a4b | ||
|
|
ec68baa74e | ||
|
|
7216b0f5cb | ||
|
|
83b21bff7d | ||
|
|
cb52875651 | ||
|
|
89bcceaadc | ||
|
|
51dc454948 | ||
|
|
c7d57e24c3 | ||
|
|
54c702fb7f | ||
|
|
da6c9ebd1e | ||
|
|
f987e45c97 | ||
|
|
1f2ac2e1e7 | ||
|
|
b13c26b06a | ||
|
|
b5ace63438 | ||
|
|
15a81ee09f | ||
|
|
fecfdc101c | ||
|
|
4fdf3ef190 | ||
|
|
88f4ac2349 | ||
|
|
deaebd3333 | ||
|
|
6b0655aed3 | ||
|
|
2901345ea6 | ||
|
|
e51677b0f0 | ||
|
|
676166a160 | ||
|
|
761995b623 | ||
|
|
abc1b682d4 | ||
|
|
3b37cbb76c | ||
|
|
6c6229916e | ||
|
|
2aeb7b0710 | ||
|
|
a36c0629e9 | ||
|
|
5a9d3887a7 | ||
|
|
3fdd68b203 | ||
|
|
a1264deda0 | ||
|
|
f9fa5360d9 | ||
|
|
b4a1ec6e60 | ||
|
|
c30eb00c8d | ||
|
|
fc75405597 | ||
|
|
5726470881 | ||
|
|
6bbae1f7f0 | ||
|
|
ef5fe2fa91 | ||
|
|
585856ad89 | ||
|
|
8d1dbfe55d | ||
|
|
8a9c78662f | ||
|
|
8eb0029970 | ||
|
|
2dc20dd3fd | ||
|
|
2fefab5311 | ||
|
|
e7ab90a778 | ||
|
|
28226e4d03 | ||
|
|
e59ab99d45 | ||
|
|
3f1fa50608 | ||
|
|
71a37ace78 | ||
|
|
d4e16347b8 | ||
|
|
76c94ef0e1 | ||
|
|
db6351560a | ||
|
|
bd06a7ada7 | ||
|
|
42810a5e36 | ||
|
|
01e1dd027a | ||
|
|
275b7f9ba5 | ||
|
|
e18b6497ba | ||
|
|
14a4efe86b | ||
|
|
5e602c9c0b | ||
|
|
e2eb4a6eae | ||
|
|
8b195f1531 | ||
|
|
650b47a631 | ||
|
|
bd5c909bbc | ||
|
|
eb347f4d68 | ||
|
|
b3b88779b4 | ||
|
|
ade2bad564 | ||
|
|
4ca59f0984 | ||
|
|
ab9b129b43 | ||
|
|
5d9ee134c1 | ||
|
|
b6e4c34b77 | ||
|
|
c36595170d | ||
|
|
cf6b4dae98 | ||
|
|
994852741e | ||
|
|
9403fd84f6 | ||
|
|
4d5562e2dd | ||
|
|
cad60cff61 | ||
|
|
a9ce16add6 | ||
|
|
8a2755751b | ||
|
|
1401c8b156 | ||
|
|
5cf7db140c | ||
|
|
860d2e6f5e | ||
|
|
a434c0a7b9 | ||
|
|
c6c5521ecf | ||
|
|
7175183d6a | ||
|
|
8566cf9095 | ||
|
|
efeedb3247 | ||
|
|
a3849495d5 | ||
|
|
47a7d77e55 | ||
|
|
943ff02a38 | ||
|
|
8b48dc8bad | ||
|
|
e195dbf1b4 | ||
|
|
7ae5956f06 | ||
|
|
f48a06b8c5 | ||
|
|
48d2766c13 | ||
|
|
d677d11558 | ||
|
|
c396800657 | ||
|
|
1baf21aad9 | ||
|
|
dc068cbf33 | ||
|
|
2f8b9963ce | ||
|
|
6957ad0401 | ||
|
|
ce616fa409 | ||
|
|
e216036d8d | ||
|
|
69cacc1e3b | ||
|
|
8f6b96983f | ||
|
|
779c5078ca | ||
|
|
e6c3d10b37 | ||
|
|
4cc9e53a4d | ||
|
|
88b583865a |
3
.env
@@ -10,6 +10,8 @@ LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
ORDER_HISTORY_URL=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
@@ -27,3 +29,4 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
|
||||
@@ -3,7 +3,9 @@ PORT=1995
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1995'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ACCOUNT_PROFILE_URL=http://localhost:1995
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -28,3 +30,4 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
|
||||
@@ -5,6 +5,8 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
@@ -23,3 +25,4 @@ LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
|
||||
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @openedx/2U-infinity
|
||||
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
### Description
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
16
.github/workflows/ci.yml
vendored
@@ -14,16 +14,16 @@ jobs:
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
- name: Coverage
|
||||
if: ${{ matrix.npm-test == 'test' }}
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
1
.gitignore
vendored
@@ -17,3 +17,4 @@ temp/babel-plugin-react-intl
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
src/i18n/messages
|
||||
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-profile]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
29
Makefile
@@ -1,13 +1,9 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-profile
|
||||
transifex_resource = frontend-app-profile
|
||||
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -39,35 +35,18 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-profile/src/i18n/messages:frontend-app-profile
|
||||
|
||||
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-profile
|
||||
endif
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-profile
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
44
README.rst
@@ -1,5 +1,6 @@
|
||||
#####################
|
||||
frontend-app-profile
|
||||
##########################
|
||||
#####################
|
||||
|
||||
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
||||
|
||||
@@ -17,8 +18,9 @@ frontend-app-profile
|
||||
:target: https://codecov.io/github/openedx/frontend-app-profile?branch=main
|
||||
:alt: Codecov
|
||||
|
||||
********
|
||||
Purpose
|
||||
=======
|
||||
********
|
||||
|
||||
This is a micro-frontend application responsible for the display and updating of user profiles.
|
||||
|
||||
@@ -26,11 +28,12 @@ When a user views their own profile, they're given fields to edit their full nam
|
||||
|
||||
When a user views someone else's profile, they see all those fields that that user set as public.
|
||||
|
||||
***************
|
||||
Getting Started
|
||||
===============
|
||||
***************
|
||||
|
||||
Devstack Installation
|
||||
---------------------
|
||||
Installation
|
||||
============
|
||||
|
||||
Follow these steps to provision, run, and enable an instance of the
|
||||
Profile MFE for local development via the `devstack`_.
|
||||
@@ -46,13 +49,36 @@ Profile MFE for local development via the `devstack`_.
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1995
|
||||
1. Clone your new repo:
|
||||
|
||||
``git clone https://github.com/openedx/frontend-app-profile.git``
|
||||
|
||||
2. Use node v18.x.
|
||||
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
3. Install npm dependencies:
|
||||
|
||||
``cd frontend-app-profile && npm ci``
|
||||
|
||||
4. Start the dev server:
|
||||
|
||||
``npm start``
|
||||
The server will run on port 1995
|
||||
|
||||
Once the dev server is up, visit http://localhost:1995/u/staff.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
=============
|
||||
|
||||
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
||||
|
||||
@@ -72,7 +98,7 @@ frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide. Please tag **@openedx/2u-aperture** on any PRs or issues.
|
||||
with as many details about the issue you are facing as you can provide. Please tag **@openedx/2u-infinity** on any PRs or issues.
|
||||
|
||||
https://github.com/openedx/frontend-app-profile/issues
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'Profile'
|
||||
description: 'This is a micro-frontend application responsible for the display and updating of user profiles.'
|
||||
name: 'frontend-app-profile'
|
||||
description: 'This is a micro-frontend application responsible for displaying and updating the user profiles.'
|
||||
links:
|
||||
- url: 'https://github.com/openedx/frontend-app-profile/blob/master/README.rst'
|
||||
title: 'Documentation'
|
||||
@@ -17,8 +17,9 @@ metadata:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
# This can be multiple comma-separated projects.
|
||||
openedx.org/add-to-projects: "openedx:23"
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
owner: 2U-aperture
|
||||
# (Optional) An array of different components or resources.
|
||||
# (Optional) An array of different components or resources.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
||||
|
||||
nick: prof
|
||||
oeps: {}
|
||||
openedx-release: {ref: master}
|
||||
17887
package-lock.json
generated
54
package.json
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
@@ -28,19 +28,21 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "12.3.0",
|
||||
"@edx/frontend-component-header": "4.7.1",
|
||||
"@edx/frontend-platform": "5.4.0",
|
||||
"@edx/paragon": "^20.44.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",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-platform": "8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.2.2",
|
||||
"@pact-foundation/pact": "^11.0.2",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.32.2",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.40.0",
|
||||
"history": "5.3.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.get": "4.4.2",
|
||||
@@ -51,30 +53,26 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.16.0",
|
||||
"react-router-dom": "6.16.0",
|
||||
"react-router": "6.29.0",
|
||||
"react-router-dom": "6.29.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"reselect": "4.1.8",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "5.1.1",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "17.7.2",
|
||||
"@commitlint/config-angular": "17.7.0",
|
||||
"@commitlint/cli": "19.8.0",
|
||||
"@commitlint/config-angular": "19.8.0",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.9.17",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@openedx/frontend-build": "14.3.2",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"glob": "10.3.10",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"glob": "11.0.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
"redux-mock-store": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { reducer as profilePage } from '../profile';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { reducer as profilePageReducer } from '../profile';
|
||||
import { reducer as newProfilePageReducer } from '../profile-v2';
|
||||
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
profilePage,
|
||||
profilePage: isNewProfileEnabled ? newProfilePageReducer : profilePageReducer,
|
||||
});
|
||||
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { saga as profileSaga } from '../profile';
|
||||
import { saga as newProfileSaga } from '../profile-v2';
|
||||
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
profileSaga(),
|
||||
isNewProfileEnabled ? newProfileSaga() : profileSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Head from './Head';
|
||||
|
||||
describe('Head', () => {
|
||||
const props = {};
|
||||
it('should match render title tag and favicon with the site configuration values', () => {
|
||||
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
render(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
const helmet = Helmet.peek();
|
||||
expect(helmet.title).toEqual(`Profile | ${getConfig().SITE_NAME}`);
|
||||
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||
|
||||
@@ -1,44 +1 @@
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
import dedeCAMessages from './messages/de_DE.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import faIRMessages from './messages/fa_IR.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
import itMessages from './messages/it.json';
|
||||
import ititCAMessages from './messages/it_IT.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import hiMessages from './messages/hi.json';
|
||||
import ptMessages from './messages/pt.json';
|
||||
import ptptCAMessages from './messages/pt_PT.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
'fa-ir': faIRMessages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
pt: ptMessages,
|
||||
it: itMessages,
|
||||
de: deMessages,
|
||||
hi: hiMessages,
|
||||
'fr-ca': frCAMessages,
|
||||
ru: ruMessages,
|
||||
uk: ukMessages,
|
||||
'de-de': dedeCAMessages,
|
||||
'it-it': ititCAMessages,
|
||||
'pt-pt': ptptCAMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
paragonMessages,
|
||||
appMessages,
|
||||
];
|
||||
export default [];
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "الملف الشخصي | {siteName}",
|
||||
"profile.age.details": "لمشاركة ملفك الشخصي مع بقية متعلمي {siteName}، يجب أن تؤكد أنك عمرك يفوق 13 عامًا.",
|
||||
"profile.age.set.date": "ضيط تاريخ ميلادك",
|
||||
"profile.datejoined.member.since": "عضو منذ {year}",
|
||||
"profile.bio.empty": "إضافة نبذة قصيرة",
|
||||
"profile.bio.about.me": "نبذة عنّي",
|
||||
"profile.certificate.organization.label": "من طرف",
|
||||
"profile.certificate.completion.date.label": "صدرت بتاريخ {date}",
|
||||
"profile.no.certificates": "ليست لديك أي شهادات يعد.",
|
||||
"profile.certificates.my.certificates": "شهاداتي",
|
||||
"profile.certificates.view.certificate": "معاينة الشهادة",
|
||||
"profile.certificates.types.verified": "شهادة موثقة",
|
||||
"profile.certificates.types.professional": "شهادة مهنية",
|
||||
"profile.certificates.types.unknown": "شهادة",
|
||||
"profile.country.label": "الموقع",
|
||||
"profile.country.empty": "إضافة الموقع",
|
||||
"profile.education.empty": "إضافة المستوى التعليمي",
|
||||
"profile.education.education": "المستوى التعليمي",
|
||||
"profile.education.levels.p": "دكتوراه",
|
||||
"profile.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
|
||||
"profile.education.levels.b": "بكالوريوس / ليسانس",
|
||||
"profile.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
|
||||
"profile.education.levels.hs": "الثانوية العامة / البكالوريا",
|
||||
"profile.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
|
||||
"profile.education.levels.el": "المدرسة الابتدائية / الأساسية",
|
||||
"profile.education.levels.none": "دون تعليم رسمي",
|
||||
"profile.education.levels.o": "نوع آخر من التعليم",
|
||||
"profile.editbutton.edit": "تعديل",
|
||||
"profile.formcontrols.who.can.see": "من يستطيع رؤية هذا:",
|
||||
"profile.formcontrols.button.cancel": "إلغاء",
|
||||
"profile.formcontrols.button.save": "حفظ",
|
||||
"profile.formcontrols.button.saving": "الحفظ جارٍ",
|
||||
"profile.formcontrols.button.saved": "تم الحفظ",
|
||||
"profile.visibility.who.just.me": "أنا فقط",
|
||||
"profile.visibility.who.everyone": "جميع من على {siteName}",
|
||||
"profile.learningGoal.learningGoal": "هدف التعلم",
|
||||
"profile.learningGoal.options.start_career": "أريد أن أبدأ مسيرتي المهنية",
|
||||
"profile.learningGoal.options.advance_career": "أريد أن ارتقي في مسيرتي المهنية",
|
||||
"profile.learningGoal.options.learn_something_new": "أريد أن أتعلم شيئًا جديدًا",
|
||||
"profile.learningGoal.options.something_else": "شيء آخر",
|
||||
"profile.name.full.name": "الاسم الكامل",
|
||||
"profile.name.details": "هذا هو الاسم الذي يظهر في حسابك وفي شهاداتك",
|
||||
"profile.name.empty": "إضافة الاسم",
|
||||
"profile.preferredlanguage.empty": "إضافة اللغة",
|
||||
"profile.preferredlanguage.label": "لغة التحدّث الأساسية",
|
||||
"profile.profileavatar.upload-button": "تحميل صورة",
|
||||
"profile.profileavatar.remove.button": "حذف",
|
||||
"profile.image.alt.attribute": "صورة الملف الشخصي",
|
||||
"profile.profileavatar.change-button": "تغيير",
|
||||
"profile.sociallinks.add": "إضافة {network}",
|
||||
"profile.sociallinks.social.links": "روابط التواصل الاجتماعي",
|
||||
"profile.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجدّدًا.",
|
||||
"profile.viewMyRecords": "عرض سجلّاتي",
|
||||
"profile.loading": "يتم تحميل الملف الشخصي...",
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profil | {siteName}",
|
||||
"profile.age.details": "Um Ihr Profil mit anderen {siteName}-Lernern zu teilen, müssen Sie bestätigen, dass Sie über 13 Jahre alt sind.",
|
||||
"profile.age.set.date": "Legen Sie Ihr Geburtsdatum fest",
|
||||
"profile.datejoined.member.since": "Mitglied seit {year}",
|
||||
"profile.bio.empty": "Fügen Sie Ihre Kurzbiografie hinzu",
|
||||
"profile.bio.about.me": "Über mich",
|
||||
"profile.certificate.organization.label": "Von",
|
||||
"profile.certificate.completion.date.label": "Abgeschlossen am {date}",
|
||||
"profile.no.certificates": "Sie haben bisher keine Zertifikate erhalten.",
|
||||
"profile.certificates.my.certificates": "Meine Zertifikate",
|
||||
"profile.certificates.view.certificate": "Zertifikat anschauen",
|
||||
"profile.certificates.types.verified": "Beglaubigtes Zertifikat ",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Zertifikat",
|
||||
"profile.country.label": "Ort",
|
||||
"profile.country.empty": "Standort hinzufügen",
|
||||
"profile.education.empty": "Ausbildung hinzufügen",
|
||||
"profile.education.education": "Bildung",
|
||||
"profile.education.levels.p": "Doktortitel",
|
||||
"profile.education.levels.m": "Master oder gleichwertiger akademischer Bildungsgrad",
|
||||
"profile.education.levels.b": "Bachelor",
|
||||
"profile.education.levels.a": "Allgemeine Hochschulreife oder gleichwertiger Abschluss",
|
||||
"profile.education.levels.hs": "Mittlere Reife",
|
||||
"profile.education.levels.jhs": "Hauptschule",
|
||||
"profile.education.levels.el": "Grundschule",
|
||||
"profile.education.levels.none": "Keinen Bildungsabschluss",
|
||||
"profile.education.levels.o": "Sonstige Bildung",
|
||||
"profile.editbutton.edit": "Bearbeiten",
|
||||
"profile.formcontrols.who.can.see": "Wer kann das sehen:",
|
||||
"profile.formcontrols.button.cancel": "Abbrechen",
|
||||
"profile.formcontrols.button.save": "Speichern",
|
||||
"profile.formcontrols.button.saving": "Speichert",
|
||||
"profile.formcontrols.button.saved": "Gespeichert",
|
||||
"profile.visibility.who.just.me": "Nur ich",
|
||||
"profile.visibility.who.everyone": "Alle auf {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Lernziel",
|
||||
"profile.learningGoal.options.start_career": "Ich möchte meine Karriere starten",
|
||||
"profile.learningGoal.options.advance_career": "Ich möchte mich beruflich weiterentwickeln",
|
||||
"profile.learningGoal.options.learn_something_new": "Ich möchte etwas Neues lernen",
|
||||
"profile.learningGoal.options.something_else": "Etwas anderes",
|
||||
"profile.name.full.name": "Vollständiger Name",
|
||||
"profile.name.details": "Dies ist der Name, der in Ihrem Konto und auf Ihren Zertifikaten erscheint.",
|
||||
"profile.name.empty": "Name hinzufügen",
|
||||
"profile.preferredlanguage.empty": "Sprache hinzufügen",
|
||||
"profile.preferredlanguage.label": "Gesprochene Primärsprache ",
|
||||
"profile.profileavatar.upload-button": "Foto hochladen",
|
||||
"profile.profileavatar.remove.button": "Entfernen",
|
||||
"profile.image.alt.attribute": "Profil Avatar",
|
||||
"profile.profileavatar.change-button": "Ändern",
|
||||
"profile.sociallinks.add": "{network} hinzufügen",
|
||||
"profile.sociallinks.social.links": "Soziale Netzwerke",
|
||||
"profile.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"profile.viewMyRecords": "Meine Aufzeichnungen anzeigen",
|
||||
"profile.loading": "Profil lädt...",
|
||||
"profile.username.description": "Ihre Profilinformationen sind nur für Sie sichtbar. Nur Ihr Benutzername ist für andere auf {siteName} sichtbar."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"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}",
|
||||
"profile.bio.empty": "Añade una breve biografía",
|
||||
"profile.bio.about.me": "Sobre Mí",
|
||||
"profile.certificate.organization.label": "Desde",
|
||||
"profile.certificate.completion.date.label": "Completado el {date}",
|
||||
"profile.no.certificates": "Todavía no ha obtenido ningún certificado.",
|
||||
"profile.certificates.my.certificates": "Mis Certificados",
|
||||
"profile.certificates.view.certificate": "Ver Certificado",
|
||||
"profile.certificates.types.verified": "Certificado verificado",
|
||||
"profile.certificates.types.professional": "Certificado profesional",
|
||||
"profile.certificates.types.unknown": "Certificado",
|
||||
"profile.country.label": "Ubicación",
|
||||
"profile.country.empty": "Añade ubicación",
|
||||
"profile.education.empty": "Añade Educación",
|
||||
"profile.education.education": "Educación",
|
||||
"profile.education.levels.p": "Doctorado",
|
||||
"profile.education.levels.m": "Master o magíster",
|
||||
"profile.education.levels.b": "Pregrado o Licenciatura",
|
||||
"profile.education.levels.a": "Grado técnico - tecnológico",
|
||||
"profile.education.levels.hs": "Enseñanza secundaria",
|
||||
"profile.education.levels.jhs": "Formación media",
|
||||
"profile.education.levels.el": "Enseñanza primaria",
|
||||
"profile.education.levels.none": "Ninguna educación formal",
|
||||
"profile.education.levels.o": "Otra educación",
|
||||
"profile.editbutton.edit": "Editar",
|
||||
"profile.formcontrols.who.can.see": "Quién puede ver esto:",
|
||||
"profile.formcontrols.button.cancel": "Cancelar",
|
||||
"profile.formcontrols.button.save": "Guardar",
|
||||
"profile.formcontrols.button.saving": "Guardando",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Añadir idioma",
|
||||
"profile.preferredlanguage.label": "Idioma principal que hablas",
|
||||
"profile.profileavatar.upload-button": "Subir foto",
|
||||
"profile.profileavatar.remove.button": "Eliminar",
|
||||
"profile.image.alt.attribute": "avatar del perfil",
|
||||
"profile.profileavatar.change-button": "Cambiar",
|
||||
"profile.sociallinks.add": "Añade {network}",
|
||||
"profile.sociallinks.social.links": "Enlaces De Redes Sociales",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "پرونده کاربری {siteName}",
|
||||
"profile.age.details": "برای اشتراکگذاری پرونده کاربری خود با سایر یادگیرندگان {siteName}، باید تأیید کنید که بیش از 13 سال سن دارید.",
|
||||
"profile.age.set.date": "تنظیم تاریخ تولد",
|
||||
"profile.datejoined.member.since": "عضو شده از {year}",
|
||||
"profile.bio.empty": "بیوگرافی کوتاهی اضافه کنید",
|
||||
"profile.bio.about.me": "درباره من",
|
||||
"profile.certificate.organization.label": "از",
|
||||
"profile.certificate.completion.date.label": "تکمیل شده در {date}",
|
||||
"profile.no.certificates": "شما هنوز هیچ گواهی ندارید.",
|
||||
"profile.certificates.my.certificates": "گواهیهای من",
|
||||
"profile.certificates.view.certificate": "نمایش گواهی",
|
||||
"profile.certificates.types.verified": "گواهی تأییدشده",
|
||||
"profile.certificates.types.professional": "گواهی حرفهای",
|
||||
"profile.certificates.types.unknown": "گواهی",
|
||||
"profile.country.label": "مکان",
|
||||
"profile.country.empty": "افزودن مکان",
|
||||
"profile.education.empty": "افزودن تحصیلات",
|
||||
"profile.education.education": "تحصیلات",
|
||||
"profile.education.levels.p": "درجه دکتری",
|
||||
"profile.education.levels.m": "کارشناسی ارشد یا مدرک حرفهای",
|
||||
"profile.education.levels.b": "مدرک کارشناسی",
|
||||
"profile.education.levels.a": "مدرک کاردانی",
|
||||
"profile.education.levels.hs": "متوسطه/دبیرستان",
|
||||
"profile.education.levels.jhs": "مدرسه متوسطه دوره اول/ راهنمایی",
|
||||
"profile.education.levels.el": "مدرسه ابتدایی",
|
||||
"profile.education.levels.none": "بدون تحصیلات رسمی",
|
||||
"profile.education.levels.o": "تحصیلات متفرقه",
|
||||
"profile.editbutton.edit": " ویرایش",
|
||||
"profile.formcontrols.who.can.see": "کسانی که میتوانند این را ببینند:",
|
||||
"profile.formcontrols.button.cancel": "لغو",
|
||||
"profile.formcontrols.button.save": "ذخیره",
|
||||
"profile.formcontrols.button.saving": "در حال ذخیره",
|
||||
"profile.formcontrols.button.saved": "ذخیره شد",
|
||||
"profile.visibility.who.just.me": "فقط من",
|
||||
"profile.visibility.who.everyone": "هرکسی در {siteName}",
|
||||
"profile.learningGoal.learningGoal": "هدف یادگیری",
|
||||
"profile.learningGoal.options.start_career": "من می خواهم کارم را شروع کنم",
|
||||
"profile.learningGoal.options.advance_career": "من می خواهم حرفه ام را ارتقا دهم",
|
||||
"profile.learningGoal.options.learn_something_new": "میخواهم چیز جدیدی یاد بگیرم",
|
||||
"profile.learningGoal.options.something_else": "یک چیز دیگر",
|
||||
"profile.name.full.name": "نام و نام خانوادگی",
|
||||
"profile.name.details": "این همان نامی است که در حساب کاربری و گواهیهای شما درج میشود.",
|
||||
"profile.name.empty": "افزودن نام",
|
||||
"profile.preferredlanguage.empty": "افزودن زبان",
|
||||
"profile.preferredlanguage.label": "زبان اصلی صحبت شده",
|
||||
"profile.profileavatar.upload-button": "بارگذاری عکس",
|
||||
"profile.profileavatar.remove.button": "حذف",
|
||||
"profile.image.alt.attribute": "چهرک پرونده کاربری",
|
||||
"profile.profileavatar.change-button": "تغییر",
|
||||
"profile.sociallinks.add": "افزودن {network}",
|
||||
"profile.sociallinks.social.links": "پیوندهای رسانه اجتماعی",
|
||||
"profile.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی آن وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره امتحان کنید.",
|
||||
"profile.viewMyRecords": "مشاهده سوابق من",
|
||||
"profile.loading": "در حال بارگذاری پرونده کاربری...",
|
||||
"profile.username.description": "اطلاعات پرونده کاربری فقط برای شما قابل مشاهده است. سایرین فقط نام کاربری شما را در {siteName} میتوانند ببینند."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "Pour partager votre profil avec d'autres étudiants {siteName}, vous devez confirmer que vous avez plus de 13 ans.",
|
||||
"profile.age.set.date": "Définissez votre date de naissance",
|
||||
"profile.datejoined.member.since": "Membre depuis {year}",
|
||||
"profile.bio.empty": "Ajouter une courte biographie",
|
||||
"profile.bio.about.me": "À propos de moi",
|
||||
"profile.certificate.organization.label": "De",
|
||||
"profile.certificate.completion.date.label": "Terminé le {date}",
|
||||
"profile.no.certificates": "Vous n'avez pas encore de certificats.",
|
||||
"profile.certificates.my.certificates": "Mes certificats",
|
||||
"profile.certificates.view.certificate": "Voir le certificat",
|
||||
"profile.certificates.types.verified": "Certificat vérifié",
|
||||
"profile.certificates.types.professional": "Certificat professionnel",
|
||||
"profile.certificates.types.unknown": "Certificat",
|
||||
"profile.country.label": "Localisation",
|
||||
"profile.country.empty": "Ajouter localisation",
|
||||
"profile.education.empty": "Ajouter une éducation",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorat",
|
||||
"profile.education.levels.m": "Master ou diplôme professionnel",
|
||||
"profile.education.levels.b": "Diplôme de licence",
|
||||
"profile.education.levels.a": "Grade de l'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.formcontrols.who.can.see": "Qui peut voir ça :",
|
||||
"profile.formcontrols.button.cancel": "Annuler",
|
||||
"profile.formcontrols.button.save": "Enregistrer",
|
||||
"profile.formcontrols.button.saving": "Enregistrement",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Ajouter une langue",
|
||||
"profile.preferredlanguage.label": "Langue principale parlée",
|
||||
"profile.profileavatar.upload-button": "Envoyer la photo",
|
||||
"profile.profileavatar.remove.button": "Supprimer",
|
||||
"profile.image.alt.attribute": "Profil avatar",
|
||||
"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": "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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profil | {siteName}",
|
||||
"profile.age.details": "Pour partager votre profil avec d'autres apprenants {siteName}, vous devez confirmer que vous avez plus de 13 ans.",
|
||||
"profile.age.set.date": "Entrez votre date de naissance",
|
||||
"profile.datejoined.member.since": "Membre depuis {year}",
|
||||
"profile.bio.empty": "Ajouter une courte biographie",
|
||||
"profile.bio.about.me": "À propos de moi",
|
||||
"profile.certificate.organization.label": "De",
|
||||
"profile.certificate.completion.date.label": "Terminé le {date}",
|
||||
"profile.no.certificates": "Vous n'avez pas encore d'attestation.",
|
||||
"profile.certificates.my.certificates": "Mes Attestations",
|
||||
"profile.certificates.view.certificate": "Voir votre attestation",
|
||||
"profile.certificates.types.verified": "Attestation vérifiée",
|
||||
"profile.certificates.types.professional": "Attestation professionnelle",
|
||||
"profile.certificates.types.unknown": "Attestation",
|
||||
"profile.country.label": "Adresse",
|
||||
"profile.country.empty": "Ajouter un emplacement",
|
||||
"profile.education.empty": "Ajouter formation",
|
||||
"profile.education.education": "Formation",
|
||||
"profile.education.levels.p": "Doctorat",
|
||||
"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 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",
|
||||
"profile.formcontrols.button.saving": "Sauvegarde en cours",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Ajouter une langue",
|
||||
"profile.preferredlanguage.label": "Langue principale parlée",
|
||||
"profile.profileavatar.upload-button": "Téléverser une photo",
|
||||
"profile.profileavatar.remove.button": "Retirer",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profilo | {siteName}",
|
||||
"profile.age.details": "Per condividere il tuo profilo con altri studenti di {siteName}, devi confermare di avere più di 13 anni.",
|
||||
"profile.age.set.date": "Imposta la data di nascita ",
|
||||
"profile.datejoined.member.since": "Membro da {year}",
|
||||
"profile.bio.empty": "Aggiungi una breve biografia ",
|
||||
"profile.bio.about.me": "Su di me",
|
||||
"profile.certificate.organization.label": "Da ",
|
||||
"profile.certificate.completion.date.label": "Completato il {date}",
|
||||
"profile.no.certificates": "Non si dispone ancora di alcun certificato. ",
|
||||
"profile.certificates.my.certificates": "Certificati personali ",
|
||||
"profile.certificates.view.certificate": "Visualizza il certificato",
|
||||
"profile.certificates.types.verified": "Certificato Verificato",
|
||||
"profile.certificates.types.professional": "Certificato professionale ",
|
||||
"profile.certificates.types.unknown": "Certificato ",
|
||||
"profile.country.label": "Posizione",
|
||||
"profile.country.empty": "Aggiungi posizione ",
|
||||
"profile.education.empty": "Aggiungi titolo di studio ",
|
||||
"profile.education.education": "Educazione",
|
||||
"profile.education.levels.p": "Dottorato",
|
||||
"profile.education.levels.m": "Laurea magistrale o titolo accademico professionale",
|
||||
"profile.education.levels.b": "Laurea di primo livello ",
|
||||
"profile.education.levels.a": "Diploma Professionale",
|
||||
"profile.education.levels.hs": "Scuole superiori/liceo",
|
||||
"profile.education.levels.jhs": "Scuole Medie",
|
||||
"profile.education.levels.el": "Scuola Primaria/Elementare",
|
||||
"profile.education.levels.none": "Nessun livello educativo formale",
|
||||
"profile.education.levels.o": "Altro livello educativo",
|
||||
"profile.editbutton.edit": "Modifica",
|
||||
"profile.formcontrols.who.can.see": "Chi può visualizzare: ",
|
||||
"profile.formcontrols.button.cancel": "Annulla",
|
||||
"profile.formcontrols.button.save": "Salva",
|
||||
"profile.formcontrols.button.saving": "Salvataggio in corso",
|
||||
"profile.formcontrols.button.saved": "Salvato",
|
||||
"profile.visibility.who.just.me": "Solo io ",
|
||||
"profile.visibility.who.everyone": "Tutti su {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Obiettivo di apprendimento",
|
||||
"profile.learningGoal.options.start_career": "Voglio iniziare il mio percorso",
|
||||
"profile.learningGoal.options.advance_career": "Voglio avanzare nel mio percorso",
|
||||
"profile.learningGoal.options.learn_something_new": "Voglio imparare qualcosa di nuovo",
|
||||
"profile.learningGoal.options.something_else": "Qualcos'altro",
|
||||
"profile.name.full.name": "Nome e Cognome",
|
||||
"profile.name.details": "Questo è il nome visualizzato nel proprio account e nei propri certificati. ",
|
||||
"profile.name.empty": "Aggiungi nome ",
|
||||
"profile.preferredlanguage.empty": "Aggiungi lingua",
|
||||
"profile.preferredlanguage.label": "Lingua principale ",
|
||||
"profile.profileavatar.upload-button": "Carica foto",
|
||||
"profile.profileavatar.remove.button": "Rimuovi",
|
||||
"profile.image.alt.attribute": "avatar del profilo ",
|
||||
"profile.profileavatar.change-button": "Cambia",
|
||||
"profile.sociallinks.add": "Aggiungi {network}",
|
||||
"profile.sociallinks.social.links": "Link social ",
|
||||
"profile.notfound.message": "La pagina ricercata non è disponibile oppure è presente un errore nell'URL. Controllare l'URL e riprovare. ",
|
||||
"profile.viewMyRecords": "Visualizza record personali ",
|
||||
"profile.loading": "Caricamento del profilo... ",
|
||||
"profile.username.description": "Le informazioni del tuo profilo sono visibili solo a te. Solo il tuo nome utente è visibile agli altri su {siteName}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Perfil | {siteName}",
|
||||
"profile.age.details": "Para partilhar o seu perfil com outros estudantes da plataforma {siteName}, tem de confirmar que tem mais de 13 anos de idade.",
|
||||
"profile.age.set.date": "Indique a sua data de nascimento",
|
||||
"profile.datejoined.member.since": "Utilizador desde {year}",
|
||||
"profile.bio.empty": "Adicionar uma breve biografia",
|
||||
"profile.bio.about.me": "Sobre Mim",
|
||||
"profile.certificate.organization.label": "De",
|
||||
"profile.certificate.completion.date.label": "Concluído a {date}",
|
||||
"profile.no.certificates": "Ainda não tem certificados.",
|
||||
"profile.certificates.my.certificates": "Os Meus Certificados",
|
||||
"profile.certificates.view.certificate": "Ver Certificado",
|
||||
"profile.certificates.types.verified": "Certificado Validado",
|
||||
"profile.certificates.types.professional": "Certificado Profissional",
|
||||
"profile.certificates.types.unknown": "Certificado",
|
||||
"profile.country.label": "Localização",
|
||||
"profile.country.empty": "Adicionar localização",
|
||||
"profile.education.empty": "Adicionar grau de escolaridade",
|
||||
"profile.education.education": "Educação",
|
||||
"profile.education.levels.p": "Doutoramento",
|
||||
"profile.education.levels.m": "Mestrado ou Grau Profissional",
|
||||
"profile.education.levels.b": "Licenciatura",
|
||||
"profile.education.levels.a": "Pós-graduação",
|
||||
"profile.education.levels.hs": "Secundário",
|
||||
"profile.education.levels.jhs": "2ªciclo/3ºciclo",
|
||||
"profile.education.levels.el": "Primária",
|
||||
"profile.education.levels.none": "Sem estudos",
|
||||
"profile.education.levels.o": "Outra formação",
|
||||
"profile.editbutton.edit": "Editar",
|
||||
"profile.formcontrols.who.can.see": "Quem pode ver isto:",
|
||||
"profile.formcontrols.button.cancel": "Cancelar",
|
||||
"profile.formcontrols.button.save": "Guardar",
|
||||
"profile.formcontrols.button.saving": "A Guardar",
|
||||
"profile.formcontrols.button.saved": "Guardado",
|
||||
"profile.visibility.who.just.me": "Apenas eu",
|
||||
"profile.visibility.who.everyone": "Toda a gente em {siteName}",
|
||||
"profile.learningGoal.learningGoal": "Objectivo de aprendizagem",
|
||||
"profile.learningGoal.options.start_career": "Quero começar a minha carreira",
|
||||
"profile.learningGoal.options.advance_career": "Quero progredir na minha carreira",
|
||||
"profile.learningGoal.options.learn_something_new": "Quero aprender algo novo",
|
||||
"profile.learningGoal.options.something_else": "Outra coisa",
|
||||
"profile.name.full.name": "Nome Completo",
|
||||
"profile.name.details": "Este é o nome que aparece na sua conta e nos seus certificados.",
|
||||
"profile.name.empty": "Adicionar nome",
|
||||
"profile.preferredlanguage.empty": "Adicionar idioma",
|
||||
"profile.preferredlanguage.label": "Língua Materna",
|
||||
"profile.profileavatar.upload-button": "Carregar Fotografia",
|
||||
"profile.profileavatar.remove.button": "Eliminar",
|
||||
"profile.image.alt.attribute": "Icon de perfil",
|
||||
"profile.profileavatar.change-button": "Alterar",
|
||||
"profile.sociallinks.add": "Adicionar {network}",
|
||||
"profile.sociallinks.social.links": "Links de Redes Sociais",
|
||||
"profile.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
|
||||
"profile.viewMyRecords": "Ver os Meus Registos",
|
||||
"profile.loading": "A carregar perfil...",
|
||||
"profile.username.description": "As informações do seu perfil só são visíveis para si. Apenas o seu nome de utilizador é visível para outros em {siteName}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "Мої сертифікати",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"profile.page.title": "Profile | {siteName}",
|
||||
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
|
||||
"profile.age.set.date": "Set your date of birth",
|
||||
"profile.datejoined.member.since": "Member since {year}",
|
||||
"profile.bio.empty": "Add a short bio",
|
||||
"profile.bio.about.me": "About Me",
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
"profile.certificates.types.unknown": "Certificate",
|
||||
"profile.country.label": "Location",
|
||||
"profile.country.empty": "Add location",
|
||||
"profile.education.empty": "Add education",
|
||||
"profile.education.education": "Education",
|
||||
"profile.education.levels.p": "Doctorate",
|
||||
"profile.education.levels.m": "Master's or professional degree",
|
||||
"profile.education.levels.b": "Bachelor's Degree",
|
||||
"profile.education.levels.a": "Associate's degree",
|
||||
"profile.education.levels.hs": "Secondary/high school",
|
||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"profile.education.levels.el": "Elementary/primary school",
|
||||
"profile.education.levels.none": "No formal education",
|
||||
"profile.education.levels.o": "Other education",
|
||||
"profile.editbutton.edit": "Edit",
|
||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
||||
"profile.formcontrols.button.cancel": "Cancel",
|
||||
"profile.formcontrols.button.save": "Save",
|
||||
"profile.formcontrols.button.saving": "Saving",
|
||||
"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",
|
||||
"profile.preferredlanguage.empty": "Add language",
|
||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
||||
"profile.profileavatar.upload-button": "Upload Photo",
|
||||
"profile.profileavatar.remove.button": "Remove",
|
||||
"profile.image.alt.attribute": "profile avatar",
|
||||
"profile.profileavatar.change-button": "Change",
|
||||
"profile.sociallinks.add": "Add {network}",
|
||||
"profile.sociallinks.social.links": "Social Links",
|
||||
"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}."
|
||||
}
|
||||
8
src/index-v2.scss
Executable file
@@ -0,0 +1,8 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import './profile-v2/index';
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import {
|
||||
AppProvider,
|
||||
@@ -17,25 +18,30 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import FooterSlot from '@openedx/frontend-slot-footer';
|
||||
|
||||
import messages from './i18n';
|
||||
import configureStore from './data/configureStore';
|
||||
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
subscribe(APP_READY, async () => {
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW === 'true';
|
||||
if (isNewProfileEnabled) {
|
||||
await import('./index-v2.scss');
|
||||
} else {
|
||||
await import('./index.scss');
|
||||
}
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Header />
|
||||
<main>
|
||||
<AppRoutes />
|
||||
<main id="main">
|
||||
<AppRoutes isNewProfileEnabled={isNewProfileEnabled} />
|
||||
</main>
|
||||
<Footer />
|
||||
<FooterSlot />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
@@ -53,6 +59,7 @@ initialize({
|
||||
mergeConfig({
|
||||
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
|
||||
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
|
||||
ENABLE_NEW_PROFILE_VIEW: process.env.ENABLE_NEW_PROFILE_VIEW || null,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
50
src/plugin-slots/FooterSlot/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `footer_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the footer.
|
||||
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the default footer.
|
||||
|
||||

|
||||
|
||||
with a simple custom footer
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
footer_slot: {
|
||||
plugins: [
|
||||
{
|
||||
// Hide the default footer
|
||||
op: PLUGIN_OPERATIONS.Hide,
|
||||
widgetId: 'default_contents',
|
||||
},
|
||||
{
|
||||
// Insert a custom footer
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_footer',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🦶</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
3
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `frontend-app-profile` Plugin Slots
|
||||
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
146
src/profile-v2/CertificateCard.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import professionalCertificateSVG from './assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from './assets/verified-certificate.svg';
|
||||
import messages from './Certificates.messages';
|
||||
import { useIsOnMobileScreen } from './data/hooks';
|
||||
|
||||
const CertificateCard = ({
|
||||
certificateType,
|
||||
courseDisplayName,
|
||||
courseOrganization,
|
||||
modifiedDate,
|
||||
downloadUrl,
|
||||
courseId,
|
||||
uuid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const certificateIllustration = {
|
||||
professional: professionalCertificateSVG,
|
||||
'no-id-professional': professionalCertificateSVG,
|
||||
verified: verifiedCertificateSVG,
|
||||
honor: null,
|
||||
audit: null,
|
||||
}[certificateType] || null;
|
||||
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${modifiedDate}-${courseId}`}
|
||||
className="col-auto d-flex align-items-center p-0"
|
||||
>
|
||||
<div className="col certificate p-4 border-light-400 bg-light-200 w-100 h-100">
|
||||
<div
|
||||
className="certificate-type-illustration"
|
||||
style={{ backgroundImage: `url(${certificateIllustration})` }}
|
||||
/>
|
||||
<div className={classNames(
|
||||
'd-flex flex-column position-relative p-0',
|
||||
{ 'max-width-19rem': isMobileView },
|
||||
{ 'width-19625rem': !isMobileView },
|
||||
)}
|
||||
>
|
||||
<div className="w-100 color-black">
|
||||
<p className={classNames([
|
||||
'mb-0 font-weight-normal',
|
||||
isMobileView ? 'x-small' : 'small',
|
||||
])}
|
||||
>
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.certificates.types.${certificateType}`,
|
||||
messages['profile.certificates.types.unknown'],
|
||||
))}
|
||||
</p>
|
||||
<p className={classNames([
|
||||
'm-0 color-black',
|
||||
isMobileView ? 'h5' : 'h4',
|
||||
])}
|
||||
>
|
||||
{courseDisplayName}
|
||||
</p>
|
||||
<p className={classNames([
|
||||
'mb-0',
|
||||
isMobileView ? 'x-small' : 'small',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.certificate.organization.label"
|
||||
defaultMessage="From"
|
||||
/>
|
||||
</p>
|
||||
<h5 className="mb-0 color-black">{courseOrganization}</h5>
|
||||
<p className={classNames([
|
||||
'mb-0',
|
||||
isMobileView ? 'x-small' : 'small',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.certificate.completion.date.label"
|
||||
defaultMessage="Completed on {date}"
|
||||
values={{
|
||||
date: <FormattedDate value={new Date(modifiedDate)} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<Hyperlink
|
||||
destination={downloadUrl}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
className={classNames(
|
||||
'btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem',
|
||||
{ 'btn-sm': isMobileView },
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages['profile.certificates.view.certificate'])}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<p
|
||||
className={classNames([
|
||||
'mb-0 pt-3',
|
||||
isMobileView ? 'x-small' : 'small',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.certificate.uuid"
|
||||
defaultMessage="Credential ID {certificate_uuid}"
|
||||
values={{
|
||||
certificate_uuid: uuid,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CertificateCard.propTypes = {
|
||||
certificateType: PropTypes.string,
|
||||
courseDisplayName: PropTypes.string,
|
||||
courseOrganization: PropTypes.string,
|
||||
modifiedDate: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
uuid: PropTypes.string,
|
||||
};
|
||||
|
||||
CertificateCard.defaultProps = {
|
||||
certificateType: 'unknown',
|
||||
courseDisplayName: '',
|
||||
courseOrganization: '',
|
||||
modifiedDate: '',
|
||||
downloadUrl: '',
|
||||
uuid: '',
|
||||
};
|
||||
|
||||
export default CertificateCard;
|
||||
101
src/profile-v2/Certificates.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { connect } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import CertificateCard from './CertificateCard';
|
||||
import { certificatesSelector } from './data/selectors';
|
||||
import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
|
||||
|
||||
const Certificates = ({ certificates }) => {
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
const isTabletView = useIsOnTabletScreen();
|
||||
return (
|
||||
<div>
|
||||
<div className="col justify-content-start align-items-start g-5rem p-0">
|
||||
<div className="col align-self-stretch height-2625rem justify-content-start align-items-start p-0">
|
||||
<p className={classNames([
|
||||
'font-weight-bold text-primary-500 m-0',
|
||||
isMobileView ? 'h3' : 'h2',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.your.certificates"
|
||||
defaultMessage="Your certificates"
|
||||
description="heading for the certificates section"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col justify-content-start align-items-start pt-2 p-0">
|
||||
<p className={classNames([
|
||||
'font-weight-normal text-gray-800 m-0 p-0',
|
||||
isMobileView ? 'h5' : 'p',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.certificates.description"
|
||||
defaultMessage="Your learner records information is only visible to you. Only your username is visible to others on {siteName}."
|
||||
description="description of the certificates section"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{certificates?.length > 0 ? (
|
||||
<div className="col">
|
||||
<div className={classNames(
|
||||
'row align-items-center pt-5 g-3rem',
|
||||
{ 'justify-content-center': isTabletView },
|
||||
)}
|
||||
>
|
||||
{certificates.map(certificate => (
|
||||
<CertificateCard
|
||||
key={certificate.courseId}
|
||||
certificateType={certificate.certificateType}
|
||||
courseDisplayName={certificate.courseDisplayName}
|
||||
courseOrganization={certificate.courseOrganization}
|
||||
modifiedDate={certificate.modifiedDate}
|
||||
downloadUrl={certificate.downloadUrl}
|
||||
courseId={certificate.courseId}
|
||||
uuid={certificate.uuid}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-5">
|
||||
<FormattedMessage
|
||||
id="profile.no.certificates"
|
||||
defaultMessage="You don't have any certificates yet."
|
||||
description="displays when user has no course completion certificates"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Certificates.propTypes = {
|
||||
certificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
certificateType: PropTypes.string,
|
||||
courseDisplayName: PropTypes.string,
|
||||
courseOrganization: PropTypes.string,
|
||||
modifiedDate: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
uuid: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
Certificates.defaultProps = {
|
||||
certificates: [],
|
||||
};
|
||||
|
||||
export default connect(
|
||||
certificatesSelector,
|
||||
{},
|
||||
)(Certificates);
|
||||
31
src/profile-v2/Certificates.messages.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.certificates.my.certificates': {
|
||||
id: 'profile.certificates.my.certificates',
|
||||
defaultMessage: 'My Certificates',
|
||||
description: 'A section of a user profile',
|
||||
},
|
||||
'profile.certificates.view.certificate': {
|
||||
id: 'profile.certificates.view.certificate',
|
||||
defaultMessage: 'View Certificate',
|
||||
description: 'A call to action to view a certificate',
|
||||
},
|
||||
'profile.certificates.types.verified': {
|
||||
id: 'profile.certificates.types.verified',
|
||||
defaultMessage: 'Verified Certificate',
|
||||
description: 'A type of certificate a user may have earned',
|
||||
},
|
||||
'profile.certificates.types.professional': {
|
||||
id: 'profile.certificates.types.professional',
|
||||
defaultMessage: 'Professional Certificate',
|
||||
description: 'A type of certificate a user may have earned',
|
||||
},
|
||||
'profile.certificates.types.unknown': {
|
||||
id: 'profile.certificates.types.unknown',
|
||||
defaultMessage: 'Certificate',
|
||||
description: 'The string to display when a certificate is of an unknown type',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
29
src/profile-v2/DateJoined.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const DateJoined = ({ date }) => {
|
||||
if (!date) { return null; }
|
||||
|
||||
return (
|
||||
<span className="small mb-0 text-gray-800">
|
||||
<FormattedMessage
|
||||
id="profile.datejoined.member.since"
|
||||
defaultMessage="Member since {year}"
|
||||
description="A label for how long the user has been a member"
|
||||
values={{
|
||||
year: <span className="font-weight-bold"> <FormattedDate value={new Date(date)} year="numeric" /> </span>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
DateJoined.propTypes = {
|
||||
date: PropTypes.string,
|
||||
};
|
||||
DateJoined.defaultProps = {
|
||||
date: null,
|
||||
};
|
||||
|
||||
export default memo(DateJoined);
|
||||
16
src/profile-v2/NotFoundPage.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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 max-width-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;
|
||||
18
src/profile-v2/PageLoading.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PageLoading = ({ srMessage }) => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center flex-column height-50vh">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
{srMessage && <span className="sr-only">{srMessage}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageLoading.propTypes = {
|
||||
srMessage: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
215
src/profile-v2/ProfilePage.jsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, {
|
||||
useEffect, useState, useContext, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
fetchProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
} from './data/actions';
|
||||
import ProfileAvatar from './forms/ProfileAvatar';
|
||||
import Certificates from './Certificates';
|
||||
import DateJoined from './DateJoined';
|
||||
import UserCertificateSummary from './UserCertificateSummary';
|
||||
import UsernameDescription from './UsernameDescription';
|
||||
import PageLoading from './PageLoading';
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
import messages from './ProfilePage.messages';
|
||||
import withParams from '../utils/hoc';
|
||||
import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
|
||||
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
|
||||
const ProfilePage = ({ params }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const context = useContext(AppContext);
|
||||
const {
|
||||
requiresParentalConsent,
|
||||
dateJoined,
|
||||
yearOfBirth,
|
||||
courseCertificates,
|
||||
name,
|
||||
profileImage,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
photoUploadError,
|
||||
} = useSelector(profilePageSelector);
|
||||
|
||||
const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
const isTabletView = useIsOnTabletScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const { CREDENTIALS_BASE_URL } = context.config;
|
||||
if (CREDENTIALS_BASE_URL) {
|
||||
setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`);
|
||||
}
|
||||
|
||||
dispatch(fetchProfile(params.username));
|
||||
sendTrackingLogEvent('edx.profile.viewed', {
|
||||
username: params.username,
|
||||
});
|
||||
}, [dispatch, params.username, context.config]);
|
||||
|
||||
const handleSaveProfilePhoto = useCallback((formData) => {
|
||||
dispatch(saveProfilePhoto(context.authenticatedUser.username, formData));
|
||||
}, [dispatch, context.authenticatedUser.username]);
|
||||
|
||||
const handleDeleteProfilePhoto = useCallback(() => {
|
||||
dispatch(deleteProfilePhoto(context.authenticatedUser.username));
|
||||
}, [dispatch, context.authenticatedUser.username]);
|
||||
|
||||
const isYOBDisabled = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13);
|
||||
return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true';
|
||||
};
|
||||
|
||||
const isAuthenticatedUserProfile = () => params.username === context.authenticatedUser.username;
|
||||
|
||||
const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile()
|
||||
|| (!isAuthenticatedUserProfile() && Boolean(blockInfo));
|
||||
|
||||
const renderViewMyRecordsButton = () => {
|
||||
if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Hyperlink
|
||||
className={classNames(
|
||||
'btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap',
|
||||
{ 'btn-sm': isMobileView },
|
||||
)}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
destination={viewMyRecordsUrl}
|
||||
>
|
||||
{intl.formatMessage(messages['profile.viewMyRecords'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPhotoUploadErrorMessage = () => (
|
||||
photoUploadError && (
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-3">
|
||||
<Alert variant="danger" dismissible={false} show>
|
||||
{photoUploadError.userMessage}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
{isLoadingProfile ? (
|
||||
<PageLoading srMessage={intl.formatMessage(messages['profile.loading'])} />
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100',
|
||||
{ 'px-4.5': isMobileView },
|
||||
{ 'px-75rem': !isMobileView },
|
||||
)}
|
||||
>
|
||||
<div className="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75">
|
||||
<div className="col h-100 w-100 py-4 px-0 justify-content-start g-15rem">
|
||||
<div
|
||||
className={classNames([
|
||||
'row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem',
|
||||
isMobileView || isTabletView ? 'flex-column' : 'flex-row',
|
||||
])}
|
||||
>
|
||||
<ProfileAvatar
|
||||
className="col p-0"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={handleSaveProfilePhoto}
|
||||
onDelete={handleDeleteProfilePhoto}
|
||||
savePhotoState={savePhotoState}
|
||||
isEditable={isAuthenticatedUserProfile() && !requiresParentalConsent}
|
||||
/>
|
||||
<div
|
||||
className={classNames([
|
||||
'col h-100 w-100 m-0 p-0',
|
||||
isMobileView || isTabletView
|
||||
? 'd-flex flex-column justify-content-center align-items-center'
|
||||
: 'justify-content-start align-items-start',
|
||||
])}
|
||||
>
|
||||
<p className={classNames([
|
||||
'row m-0 font-weight-bold text-truncate text-primary-500',
|
||||
isMobileView ? 'h4' : 'h3',
|
||||
])}
|
||||
>
|
||||
{params.username}
|
||||
</p>
|
||||
{isBlockVisible(name) && (
|
||||
<p className={classNames([
|
||||
'row pt-2 text-gray-800 font-weight-normal m-0',
|
||||
isMobileView ? 'h5' : 'p',
|
||||
])}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
)}
|
||||
<div className={classNames(
|
||||
'row pt-2 m-0',
|
||||
isMobileView
|
||||
? 'd-flex justify-content-center align-items-center flex-column'
|
||||
: 'g-1rem',
|
||||
)}
|
||||
>
|
||||
<DateJoined date={dateJoined} />
|
||||
<UserCertificateSummary count={courseCertificates.length} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames([
|
||||
'p-0 ',
|
||||
isMobileView || isTabletView ? 'col d-flex justify-content-center' : 'col-auto',
|
||||
])}
|
||||
>
|
||||
{renderViewMyRecordsButton()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4">
|
||||
{isYOBDisabled() && <UsernameDescription />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
{renderPhotoUploadErrorMessage()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem">
|
||||
{isBlockVisible(courseCertificates.length) && (
|
||||
<Certificates
|
||||
certificates={courseCertificates}
|
||||
formId="certificates"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilePage.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withParams(ProfilePage);
|
||||
16
src/profile-v2/ProfilePage.messages.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.viewMyRecords': {
|
||||
id: 'profile.viewMyRecords',
|
||||
defaultMessage: 'View My Records',
|
||||
description: 'A link to go view my academic records',
|
||||
},
|
||||
'profile.loading': {
|
||||
id: 'profile.loading',
|
||||
defaultMessage: 'Profile loading...',
|
||||
description: 'Message displayed when the profile data is loading.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
185
src/profile-v2/ProfilePage.test.jsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import messages from '../i18n';
|
||||
import ProfilePage from './ProfilePage';
|
||||
import loadingApp from './__mocks__/loadingApp.mockStore';
|
||||
import viewOwnProfile from './__mocks__/viewOwnProfile.mockStore';
|
||||
import viewOtherProfile from './__mocks__/viewOtherProfile.mockStore';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const storeMocks = {
|
||||
loadingApp,
|
||||
viewOwnProfile,
|
||||
viewOtherProfile,
|
||||
};
|
||||
const requiredProfilePageProps = {
|
||||
fetchUserAccount: () => {},
|
||||
fetchProfile: () => {},
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
// Mock language cookie
|
||||
Object.defineProperty(global.document, 'cookie', {
|
||||
writable: true,
|
||||
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
configure: () => {},
|
||||
getAuthenticatedUser: () => null,
|
||||
fetchAuthenticatedUser: () => null,
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
AUTHENTICATED_USER_CHANGED: 'user_changed',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
configure: () => {},
|
||||
identifyAnonymousUser: jest.fn(),
|
||||
identifyAuthenticatedUser: jest.fn(),
|
||||
sendTrackingLogEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
configureI18n({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
ENVIRONMENT: 'production',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||
},
|
||||
messages,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
analytics.sendTrackingLogEvent.mockReset();
|
||||
});
|
||||
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, params,
|
||||
}) => (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<ProfilePage {...requiredProfilePageProps} params={params} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
ProfilePageWrapper.defaultProps = {
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
ProfilePageWrapper.propTypes = {
|
||||
contextValue: PropTypes.shape({}).isRequired,
|
||||
store: PropTypes.shape({}).isRequired,
|
||||
params: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
describe('<ProfilePage />', () => {
|
||||
describe('Renders correctly in various states', () => {
|
||||
it('app loading', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.loadingApp)} />;
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing own profile', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.viewOwnProfile)} />;
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing other profile with all fields', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore({
|
||||
...storeMocks.viewOtherProfile,
|
||||
profilePage: {
|
||||
...storeMocks.viewOtherProfile.profilePage,
|
||||
account: {
|
||||
...storeMocks.viewOtherProfile.profilePage.account,
|
||||
name: 'user',
|
||||
country: 'EN',
|
||||
bio: 'bio',
|
||||
courseCertificates: ['course certificates'],
|
||||
levelOfEducation: 'some level',
|
||||
languageProficiencies: ['some lang'],
|
||||
socialLinks: ['twitter'],
|
||||
timeZone: 'time zone',
|
||||
accountPrivacy: 'all_users',
|
||||
},
|
||||
},
|
||||
})}
|
||||
match={{ params: { username: 'verified' } }} // Override default match
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('without credentials service', () => {
|
||||
const config = getConfig();
|
||||
config.CREDENTIALS_BASE_URL = '';
|
||||
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOwnProfile)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles analytics', () => {
|
||||
it('calls sendTrackingLogEvent when mounting', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
params={{ username: 'test-username' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls[0][1]).toEqual({
|
||||
username: 'test-username',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/profile-v2/UserCertificateSummary.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const UserCertificateSummary = ({ count = 0 }) => (
|
||||
<span className="small m-0 text-gray-800">
|
||||
<FormattedMessage
|
||||
id="profile.certificatecount"
|
||||
defaultMessage="{certificate_count} certifications"
|
||||
description="A label for many certificates a user has"
|
||||
values={{
|
||||
certificate_count: <span className="font-weight-bold"> {count} </span>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
UserCertificateSummary.propTypes = {
|
||||
count: PropTypes.number,
|
||||
};
|
||||
|
||||
export default UserCertificateSummary;
|
||||
27
src/profile-v2/UsernameDescription.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import classNames from 'classnames';
|
||||
import { useIsOnMobileScreen } from './data/hooks';
|
||||
|
||||
const UsernameDescription = () => {
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
return (
|
||||
<p className={classNames([
|
||||
'text-gray-800 font-weight-normal m-0',
|
||||
isMobileView ? 'h5' : 'p',
|
||||
])}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.username.description"
|
||||
defaultMessage="Your learner records 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,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameDescription;
|
||||
41
src/profile-v2/__mocks__/loadingApp.mockStore.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: null,
|
||||
bio: null,
|
||||
name: null,
|
||||
country: null,
|
||||
socialLinks: null,
|
||||
profileImage: {
|
||||
imageUrlMedium: null,
|
||||
imageUrlLarge: null
|
||||
},
|
||||
levelOfEducation: null,
|
||||
learningGoal: null
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
account: {
|
||||
username: 'staff',
|
||||
socialLinks: []
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: true,
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
139
src/profile-v2/__mocks__/savingEditedBio.mockStore.js
Normal file
@@ -0,0 +1,139 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: 'pending',
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: 'bio',
|
||||
isAuthenticatedUserProfile: true,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'staff@example.com',
|
||||
username: 'staff',
|
||||
bio: 'This is my bio',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
name: 'Lemon Seltzer',
|
||||
secondaryEmail: null,
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityCertificates: 'private',
|
||||
visibilityLevelOfEducation: 'private',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
prefLang: 'en',
|
||||
visibilityBio: 'all_users',
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
username: 'staff',
|
||||
status: 'downloadable',
|
||||
courseDisplayName: 'edX Demonstration Course',
|
||||
grade: '0.89',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
courseOrganization: 'edX',
|
||||
modifiedDate: '2019-03-04T19:31:39.930255Z',
|
||||
isPassing: true,
|
||||
downloadUrl: 'http://www.example.com/',
|
||||
certificateType: 'verified',
|
||||
createdDate: '2019-03-04T19:31:39.896806Z'
|
||||
}
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
disabledCountries: [],
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
98
src/profile-v2/__mocks__/viewOtherProfile.mockStore.js
Normal file
@@ -0,0 +1,98 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
isAuthenticatedUserProfile: false,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/static/images/profiles/default_500.png',
|
||||
imageUrlLarge: 'http://localhost:18000/static/images/profiles/default_120.png',
|
||||
imageUrlMedium: 'http://localhost:18000/static/images/profiles/default_50.png',
|
||||
imageUrlSmall: 'http://localhost:18000/static/images/profiles/default_30.png',
|
||||
hasImage: false
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:19Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'verified@example.com',
|
||||
username: 'verified',
|
||||
bio: null,
|
||||
isActive: true,
|
||||
yearOfBirth: null,
|
||||
goals: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: true,
|
||||
name: '',
|
||||
secondaryEmail: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
timeZone: null,
|
||||
levelOfEducation: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'private'
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/verified',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
138
src/profile-v2/__mocks__/viewOwnProfile.mockStore.js
Normal file
@@ -0,0 +1,138 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
isAuthenticatedUserProfile: true,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'staff@example.com',
|
||||
username: 'staff',
|
||||
bio: 'This is my bio',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
name: 'Lemon Seltzer',
|
||||
secondaryEmail: null,
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityCertificates: 'private',
|
||||
visibilityLevelOfEducation: 'private',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
prefLang: 'en',
|
||||
visibilityBio: 'all_users',
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
username: 'staff',
|
||||
status: 'downloadable',
|
||||
courseDisplayName: 'edX Demonstration Course',
|
||||
grade: '0.89',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
courseOrganization: 'edX',
|
||||
modifiedDate: '2019-03-04T19:31:39.930255Z',
|
||||
isPassing: true,
|
||||
downloadUrl: 'http://www.example.com/',
|
||||
certificateType: 'verified',
|
||||
createdDate: '2019-03-04T19:31:39.896806Z'
|
||||
}
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
619
src/profile-v2/__snapshots__/ProfilePage.test.jsx.snap
Normal file
@@ -0,0 +1,619 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="profile-page"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center flex-column height-50vh"
|
||||
>
|
||||
<div
|
||||
class="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Profile loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states viewing other profile with all fields 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="profile-page"
|
||||
>
|
||||
<div
|
||||
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
|
||||
>
|
||||
<div
|
||||
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
|
||||
>
|
||||
<div
|
||||
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
|
||||
>
|
||||
<div
|
||||
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar-wrap position-relative"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar rounded-circle bg-light"
|
||||
>
|
||||
<iconmock
|
||||
aria-hidden="true"
|
||||
class="text-muted"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewbox="0 0 24 24"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input
|
||||
accept=".jpg, .jpeg, .png"
|
||||
class="d-none form-control-file"
|
||||
id="photo-file"
|
||||
name="file"
|
||||
type="file"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
|
||||
>
|
||||
<p
|
||||
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
|
||||
>
|
||||
staff
|
||||
</p>
|
||||
<p
|
||||
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
user
|
||||
</p>
|
||||
<div
|
||||
class="row pt-2 m-0 g-1rem"
|
||||
>
|
||||
<span
|
||||
class="small mb-0 text-gray-800"
|
||||
>
|
||||
Member since
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
2017
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="small m-0 text-gray-800"
|
||||
>
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
certifications
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-0 col-auto"
|
||||
>
|
||||
<a
|
||||
class="pgn__hyperlink default-link standalone-link btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap"
|
||||
href="http://localhost:18150/records"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View My Records
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
|
||||
>
|
||||
<p
|
||||
class="text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
Your learner records information is only visible to you. Only your username is visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start g-5rem p-0"
|
||||
>
|
||||
<div
|
||||
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-bold text-primary-500 m-0 h2"
|
||||
>
|
||||
Your certificates
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start pt-2 p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-normal text-gray-800 m-0 p-0 p"
|
||||
>
|
||||
Your learner records information is only visible to you. Only your username is visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pt-5"
|
||||
>
|
||||
You don't have any certificates yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states viewing own profile 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="profile-page"
|
||||
>
|
||||
<div
|
||||
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
|
||||
>
|
||||
<div
|
||||
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
|
||||
>
|
||||
<div
|
||||
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
|
||||
>
|
||||
<div
|
||||
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar-wrap position-relative"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar rounded-circle bg-light"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar-menu-container"
|
||||
>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle btn btn-primary"
|
||||
type="button"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
alt="profile avatar"
|
||||
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
data-hj-suppress="true"
|
||||
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input
|
||||
accept=".jpg, .jpeg, .png"
|
||||
class="d-none form-control-file"
|
||||
id="photo-file"
|
||||
name="file"
|
||||
type="file"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
|
||||
>
|
||||
<p
|
||||
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
|
||||
>
|
||||
staff
|
||||
</p>
|
||||
<p
|
||||
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
Lemon Seltzer
|
||||
</p>
|
||||
<div
|
||||
class="row pt-2 m-0 g-1rem"
|
||||
>
|
||||
<span
|
||||
class="small mb-0 text-gray-800"
|
||||
>
|
||||
Member since
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
2017
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="small m-0 text-gray-800"
|
||||
>
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
1
|
||||
|
||||
</span>
|
||||
certifications
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-0 col-auto"
|
||||
>
|
||||
<a
|
||||
class="pgn__hyperlink default-link standalone-link btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap"
|
||||
href="http://localhost:18150/records"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View My Records
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start g-5rem p-0"
|
||||
>
|
||||
<div
|
||||
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-bold text-primary-500 m-0 h2"
|
||||
>
|
||||
Your certificates
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start pt-2 p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-normal text-gray-800 m-0 p-0 p"
|
||||
>
|
||||
Your learner records information is only visible to you. Only your username is visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
>
|
||||
<div
|
||||
class="row align-items-center pt-5 g-3rem"
|
||||
>
|
||||
<div
|
||||
class="col-auto d-flex align-items-center p-0"
|
||||
>
|
||||
<div
|
||||
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
|
||||
>
|
||||
<div
|
||||
class="certificate-type-illustration"
|
||||
style="background-image: url(icon/mock/path);"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column position-relative p-0 width-19625rem"
|
||||
>
|
||||
<div
|
||||
class="w-100 color-black"
|
||||
>
|
||||
<p
|
||||
class="mb-0 font-weight-normal small"
|
||||
>
|
||||
Verified Certificate
|
||||
</p>
|
||||
<p
|
||||
class="m-0 color-black h4"
|
||||
>
|
||||
edX Demonstration Course
|
||||
</p>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
From
|
||||
</p>
|
||||
<h5
|
||||
class="mb-0 color-black"
|
||||
>
|
||||
edX
|
||||
</h5>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
Completed on
|
||||
3/4/2019
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="pt-3"
|
||||
>
|
||||
<a
|
||||
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem"
|
||||
href="http://www.example.com/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View Certificate
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mb-0 pt-3 small"
|
||||
>
|
||||
Credential ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states without credentials service 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="profile-page"
|
||||
>
|
||||
<div
|
||||
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
|
||||
>
|
||||
<div
|
||||
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
|
||||
>
|
||||
<div
|
||||
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
|
||||
>
|
||||
<div
|
||||
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar-wrap position-relative"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar rounded-circle bg-light"
|
||||
>
|
||||
<div
|
||||
class="profile-avatar-menu-container"
|
||||
>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle btn btn-primary"
|
||||
type="button"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
alt="profile avatar"
|
||||
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
data-hj-suppress="true"
|
||||
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input
|
||||
accept=".jpg, .jpeg, .png"
|
||||
class="d-none form-control-file"
|
||||
id="photo-file"
|
||||
name="file"
|
||||
type="file"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
|
||||
>
|
||||
<p
|
||||
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
|
||||
>
|
||||
staff
|
||||
</p>
|
||||
<p
|
||||
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
Lemon Seltzer
|
||||
</p>
|
||||
<div
|
||||
class="row pt-2 m-0 g-1rem"
|
||||
>
|
||||
<span
|
||||
class="small mb-0 text-gray-800"
|
||||
>
|
||||
Member since
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
2017
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="small m-0 text-gray-800"
|
||||
>
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
|
||||
1
|
||||
|
||||
</span>
|
||||
certifications
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-0 col-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start g-5rem p-0"
|
||||
>
|
||||
<div
|
||||
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-bold text-primary-500 m-0 h2"
|
||||
>
|
||||
Your certificates
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="col justify-content-start align-items-start pt-2 p-0"
|
||||
>
|
||||
<p
|
||||
class="font-weight-normal text-gray-800 m-0 p-0 p"
|
||||
>
|
||||
Your learner records information is only visible to you. Only your username is visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
>
|
||||
<div
|
||||
class="row align-items-center pt-5 g-3rem"
|
||||
>
|
||||
<div
|
||||
class="col-auto d-flex align-items-center p-0"
|
||||
>
|
||||
<div
|
||||
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
|
||||
>
|
||||
<div
|
||||
class="certificate-type-illustration"
|
||||
style="background-image: url(icon/mock/path);"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column position-relative p-0 width-19625rem"
|
||||
>
|
||||
<div
|
||||
class="w-100 color-black"
|
||||
>
|
||||
<p
|
||||
class="mb-0 font-weight-normal small"
|
||||
>
|
||||
Verified Certificate
|
||||
</p>
|
||||
<p
|
||||
class="m-0 color-black h4"
|
||||
>
|
||||
edX Demonstration Course
|
||||
</p>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
From
|
||||
</p>
|
||||
<h5
|
||||
class="mb-0 color-black"
|
||||
>
|
||||
edX
|
||||
</h5>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
Completed on
|
||||
3/4/2019
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="pt-3"
|
||||
>
|
||||
<a
|
||||
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem"
|
||||
href="http://www.example.com/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View Certificate
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mb-0 pt-3 small"
|
||||
>
|
||||
Credential ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
9
src/profile-v2/assets/avatar.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>avatar</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="avatar" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" fill="currentColor"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1006 B |
BIN
src/profile-v2/assets/dot-pattern-light.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
13
src/profile-v2/assets/micro-masters.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="110px" height="100px" viewBox="0 0 110 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>micro-masters</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="micro-masters" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<polygon id="Fill-14" fill="#005585" points="1.00481933 99 1 96.14832 108.995272 96 109 98.85168"></polygon>
|
||||
<polygon id="Fill-16" fill="#005585" points="1.00481933 54 1 51.14832 108.995272 51 109 53.85168"></polygon>
|
||||
<polygon id="Fill-18" fill="#005585" points="24.9998189 88 4.22751315 66.9102771 4.22751315 87.0451102 1 87.0451102 1 59 24.9998189 83.3666894 49 59 49 87.0451102 45.7724869 87.0451102 45.7724869 66.9102771"></polygon>
|
||||
<polygon id="Fill-20" fill="#005585" points="83.0003622 88 62.2275375 66.9102771 62.2275375 87.0451102 59 87.0451102 59 59 83.0003622 83.3666894 107 59 107 87.0451102 103.772372 87.0451102 103.772372 66.9102771"></polygon>
|
||||
<path d="M46.8337141,5.73372796 C40.9959838,9.0009662 32.7599012,14.7887333 26.6032377,23.8303431 C22.1430738,22.980271 18.7577893,22.0941759 16.6724707,21.4890245 C24.4296997,13.0587208 35.0333668,7.38581116 46.8337141,5.73372796 L46.8337141,5.73372796 Z M52.9498983,26.3252576 C44.0609738,26.2888724 36.3049246,25.4607944 30.1183101,24.4518768 C37.8086485,13.8504137 48.4460785,8.26376038 52.9498983,6.25307565 L52.9498983,26.3252576 Z M56.1856092,6.53148511 C60.9824046,8.72165163 70.9689895,14.1856636 78.3079931,24.2371866 C70.4533778,25.5777336 63.0182586,26.1827945 56.1856092,26.3036256 L56.1856092,6.53148511 Z M91.2530147,21.4114572 C88.0536989,22.2655118 84.9019418,22.9909512 81.8101774,23.5971888 C75.749448,14.7672824 67.7047798,9.0930151 61.9038076,5.84261176 C73.3677956,7.61959908 83.6636931,13.2007313 91.2530147,21.4114572 L91.2530147,21.4114572 Z M5.35796556,42 C7.1655563,35.3542865 10.2683027,29.3264862 14.378947,24.1676747 C16.125002,24.7123653 19.7105047,25.7399279 24.7732291,26.7529184 C22.2125965,31.1374149 20.1687556,36.1942214 19.0530339,42 L22.3555447,42 C23.5090228,36.4059248 25.5817257,31.5545765 28.1556093,27.3837747 C34.7214222,28.5242036 43.1689773,29.5005374 52.9498983,29.5419006 L52.9498983,42 L56.1856092,42 L56.1856092,29.5223504 C63.6041191,29.395636 71.7140444,28.7101116 80.2902074,27.16121 C82.9053872,31.3760904 85.0043199,36.3025621 86.1572534,42 L89.460218,42 C88.3437702,36.0858806 86.2630805,30.9538601 83.6533464,26.5180443 C86.9031251,25.8529752 90.216981,25.0530458 93.5773971,24.1181656 C97.7090979,29.2882909 100.82809,35.3331976 102.641853,42 L106,42 C100.00834,18.4385583 78.6589648,2 53.9991832,2 C29.3299624,2 7.9913882,18.4352094 2,42 L5.35796556,42 Z" id="Fill-22" fill="#005585"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/profile-v2/assets/professional-certificate.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/profile-v2/assets/verified-certificate.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
83
src/profile-v2/data/actions.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { AsyncActionType } from '../utils';
|
||||
|
||||
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
|
||||
export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO');
|
||||
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
|
||||
|
||||
// FETCH PROFILE ACTIONS
|
||||
|
||||
export const fetchProfile = username => ({
|
||||
type: FETCH_PROFILE.BASE,
|
||||
payload: { username },
|
||||
});
|
||||
|
||||
export const fetchProfileBegin = () => ({
|
||||
type: FETCH_PROFILE.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchProfileSuccess = (
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
) => ({
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
});
|
||||
|
||||
export const fetchProfileReset = () => ({
|
||||
type: FETCH_PROFILE.RESET,
|
||||
});
|
||||
|
||||
// SAVE PROFILE PHOTO ACTIONS
|
||||
|
||||
export const saveProfilePhoto = (username, formData) => ({
|
||||
type: SAVE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username,
|
||||
formData,
|
||||
},
|
||||
});
|
||||
|
||||
export const saveProfilePhotoBegin = () => ({
|
||||
type: SAVE_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const saveProfilePhotoSuccess = profileImage => ({
|
||||
type: SAVE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { profileImage },
|
||||
});
|
||||
|
||||
export const saveProfilePhotoReset = () => ({
|
||||
type: SAVE_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
|
||||
export const saveProfilePhotoFailure = error => ({
|
||||
type: SAVE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
// DELETE PROFILE PHOTO ACTIONS
|
||||
|
||||
export const deleteProfilePhoto = username => ({
|
||||
type: DELETE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteProfilePhotoBegin = () => ({
|
||||
type: DELETE_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const deleteProfilePhotoSuccess = profileImage => ({
|
||||
type: DELETE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { profileImage },
|
||||
});
|
||||
|
||||
export const deleteProfilePhotoReset = () => ({
|
||||
type: DELETE_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
98
src/profile-v2/data/actions.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
SAVE_PROFILE_PHOTO,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoSuccess,
|
||||
saveProfilePhotoFailure,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhoto,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
deleteProfilePhotoBegin,
|
||||
deleteProfilePhotoSuccess,
|
||||
deleteProfilePhotoReset,
|
||||
deleteProfilePhoto,
|
||||
} from './actions';
|
||||
|
||||
describe('SAVE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo save', () => {
|
||||
const formData = 'multipart form data';
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
formData,
|
||||
},
|
||||
};
|
||||
expect(saveProfilePhoto('myusername', formData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save beginning', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(saveProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save success', () => {
|
||||
const newPhotoData = { hasImage: true };
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: {
|
||||
profileImage: newPhotoData,
|
||||
},
|
||||
};
|
||||
expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save reset', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(saveProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save failure', () => {
|
||||
const error = 'Test failure';
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
};
|
||||
expect(saveProfilePhotoFailure(error)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo deletion', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
},
|
||||
};
|
||||
expect(deleteProfilePhoto('myusername')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion beginning', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(deleteProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion success', () => {
|
||||
const defaultPhotoData = { hasImage: false };
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: {
|
||||
profileImage: defaultPhotoData,
|
||||
},
|
||||
};
|
||||
expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion reset', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(deleteProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
28
src/profile-v2/data/constants.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const EDUCATION_LEVELS = [
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'other',
|
||||
];
|
||||
|
||||
const SOCIAL = {
|
||||
linkedin: {
|
||||
title: 'LinkedIn',
|
||||
},
|
||||
twitter: {
|
||||
title: 'Twitter',
|
||||
},
|
||||
facebook: {
|
||||
title: 'Facebook',
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
EDUCATION_LEVELS,
|
||||
SOCIAL,
|
||||
};
|
||||
11
src/profile-v2/data/hooks.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
export function useIsOnTabletScreen() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width <= breakpoints.medium.minWidth;
|
||||
}
|
||||
|
||||
export function useIsOnMobileScreen() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width <= breakpoints.small.minWidth;
|
||||
}
|
||||
7
src/profile-v2/data/mock_data.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const mockData = {
|
||||
learningGoal: 'advance_career',
|
||||
editMode: 'static',
|
||||
visibilityLearningGoal: 'private',
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
80
src/profile-v2/data/pact-profile.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// This test file simply creates a contract that defines
|
||||
// expectations and correct responses from the Pact stub server.
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||
|
||||
import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { getAccount } from './services';
|
||||
|
||||
const expectedUserInfo200 = {
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
};
|
||||
|
||||
const provider = new PactV3({
|
||||
log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'),
|
||||
dir: path.resolve(process.cwd(), 'src/pacts'),
|
||||
consumer: 'frontend-app-profile',
|
||||
provider: 'edx-platform',
|
||||
});
|
||||
|
||||
describe('getAccount for one username', () => {
|
||||
beforeAll(async () => {
|
||||
initializeMockApp();
|
||||
});
|
||||
it('returns a HTTP 200 and user information', async () => {
|
||||
const username200 = 'staff';
|
||||
await provider.addInteraction({
|
||||
states: [{ description: "I have a user's basic information" }],
|
||||
uponReceiving: "A request for user's basic information",
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/user/v1/accounts/${username200}`,
|
||||
headers: {},
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
headers: {},
|
||||
body: MatchersV3.like(expectedUserInfo200),
|
||||
},
|
||||
});
|
||||
return provider.executeTest(async (mockserver) => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
LMS_BASE_URL: mockserver.url,
|
||||
});
|
||||
const response = await getAccount(username200);
|
||||
expect(response).toEqual(expectedUserInfo200);
|
||||
});
|
||||
});
|
||||
|
||||
it('Account does not exist', async () => {
|
||||
const username404 = 'staff_not_found';
|
||||
await provider.addInteraction({
|
||||
states: [{ description: "Account and user's information does not exist" }],
|
||||
uponReceiving: "A request for user's basic information",
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/user/v1/accounts/${username404}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 404,
|
||||
},
|
||||
});
|
||||
await provider.executeTest(async (mockserver) => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
LMS_BASE_URL: mockserver.url,
|
||||
});
|
||||
await expect(getAccount(username404).then((response) => response.data)).rejects.toThrow('Request failed with status code 404');
|
||||
});
|
||||
});
|
||||
});
|
||||
99
src/profile-v2/data/reducers.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
SAVE_PROFILE_PHOTO,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
FETCH_PROFILE,
|
||||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
errors: {},
|
||||
savePhotoState: null,
|
||||
account: {
|
||||
socialLinks: [],
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: false,
|
||||
disabledCountries: ['RU'],
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_PROFILE.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
// TODO: uncomment this line after ARCH-438 Image Post API returns the url
|
||||
// is complete. Right now we refetch the whole profile causing us to show a full reload
|
||||
// instead of a partial one.
|
||||
// isLoadingProfile: true,
|
||||
};
|
||||
case FETCH_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
account: action.account,
|
||||
preferences: action.preferences,
|
||||
courseCertificates: action.courseCertificates,
|
||||
isLoadingProfile: false,
|
||||
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
|
||||
};
|
||||
|
||||
case SAVE_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
// Merge in new profile image data
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
errors: { ...state.errors, photo: action.payload.error },
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case DELETE_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
// Merge in new profile image data (should be empty or default image)
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default profilePage;
|
||||
140
src/profile-v2/data/reducers.test.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import profilePage, { initialState } from './reducers';
|
||||
import {
|
||||
SAVE_PROFILE_PHOTO,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
FETCH_PROFILE,
|
||||
} from './actions';
|
||||
|
||||
describe('profilePage reducer', () => {
|
||||
it('should return the initial state by default', () => {
|
||||
expect(profilePage(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
describe('FETCH_PROFILE actions', () => {
|
||||
it('should handle FETCH_PROFILE.BEGIN', () => {
|
||||
const action = { type: FETCH_PROFILE.BEGIN };
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
// Uncomment isLoadingProfile: true if this functionality is required.
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle FETCH_PROFILE.SUCCESS', () => {
|
||||
const action = {
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
account: { name: 'John Doe' },
|
||||
preferences: { theme: 'dark' },
|
||||
courseCertificates: ['cert1', 'cert2'],
|
||||
isAuthenticatedUserProfile: true,
|
||||
};
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
account: action.account,
|
||||
preferences: action.preferences,
|
||||
courseCertificates: action.courseCertificates,
|
||||
isLoadingProfile: false,
|
||||
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAVE_PROFILE_PHOTO actions', () => {
|
||||
it('should handle SAVE_PROFILE_PHOTO.BEGIN', () => {
|
||||
const action = { type: SAVE_PROFILE_PHOTO.BEGIN };
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SAVE_PROFILE_PHOTO.SUCCESS', () => {
|
||||
const action = {
|
||||
type: SAVE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { profileImage: 'new-image-url.jpg' },
|
||||
};
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
account: { ...initialState.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SAVE_PROFILE_PHOTO.FAILURE', () => {
|
||||
const action = {
|
||||
type: SAVE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error: 'Photo upload failed' },
|
||||
};
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: 'error',
|
||||
errors: { photo: action.payload.error },
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SAVE_PROFILE_PHOTO.RESET', () => {
|
||||
const action = { type: SAVE_PROFILE_PHOTO.RESET };
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE_PROFILE_PHOTO actions', () => {
|
||||
it('should handle DELETE_PROFILE_PHOTO.BEGIN', () => {
|
||||
const action = { type: DELETE_PROFILE_PHOTO.BEGIN };
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle DELETE_PROFILE_PHOTO.SUCCESS', () => {
|
||||
const action = {
|
||||
type: DELETE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { profileImage: 'default-image-url.jpg' },
|
||||
};
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
account: { ...initialState.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle DELETE_PROFILE_PHOTO.FAILURE', () => {
|
||||
const action = {
|
||||
type: DELETE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { errors: { delete: 'Failed to delete photo' } },
|
||||
};
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: 'error',
|
||||
errors: { ...initialState.errors, delete: action.payload.errors.delete },
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it('should handle DELETE_PROFILE_PHOTO.RESET', () => {
|
||||
const action = { type: DELETE_PROFILE_PHOTO.RESET };
|
||||
const expectedState = {
|
||||
...initialState,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
expect(profilePage(initialState, action)).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
});
|
||||
127
src/profile-v2/data/sagas.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
put,
|
||||
select,
|
||||
takeEvery,
|
||||
} from 'redux-saga/effects';
|
||||
import {
|
||||
deleteProfilePhotoBegin,
|
||||
deleteProfilePhotoReset,
|
||||
deleteProfilePhotoSuccess,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
fetchProfileBegin,
|
||||
fetchProfileReset,
|
||||
fetchProfileSuccess,
|
||||
FETCH_PROFILE,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhotoSuccess,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
} from './actions';
|
||||
import { userAccountSelector } from './selectors';
|
||||
import * as ProfileApiService from './services';
|
||||
|
||||
export function* handleFetchProfile(action) {
|
||||
const { username } = action.payload;
|
||||
const userAccount = yield select(userAccountSelector);
|
||||
const isAuthenticatedUserProfile = username === getAuthenticatedUser().username;
|
||||
// Default our data assuming the account is the current user's account.
|
||||
let preferences = {};
|
||||
let account = userAccount;
|
||||
let courseCertificates = null;
|
||||
|
||||
try {
|
||||
yield put(fetchProfileBegin());
|
||||
|
||||
// Depending on which profile we're loading, we need to make different calls.
|
||||
const calls = [
|
||||
call(ProfileApiService.getAccount, username),
|
||||
call(ProfileApiService.getCourseCertificates, username),
|
||||
];
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
// If the profile is for the current user, get their preferences.
|
||||
// We don't need them for other users.
|
||||
calls.push(call(ProfileApiService.getPreferences, username));
|
||||
}
|
||||
|
||||
// Make all the calls in parallel.
|
||||
const result = yield all(calls);
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
[account, courseCertificates, preferences] = result;
|
||||
} else {
|
||||
[account, courseCertificates] = result;
|
||||
}
|
||||
|
||||
// Set initial visibility values for account
|
||||
// Set account_privacy as custom is necessary so that when viewing another user's profile,
|
||||
// their full name is displayed and change visibility forms are worked correctly
|
||||
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
|
||||
yield call(ProfileApiService.patchPreferences, action.payload.username, {
|
||||
account_privacy: 'custom',
|
||||
'visibility.name': 'all_users',
|
||||
'visibility.bio': 'all_users',
|
||||
'visibility.course_certificates': 'all_users',
|
||||
'visibility.country': 'all_users',
|
||||
'visibility.date_joined': 'all_users',
|
||||
'visibility.level_of_education': 'all_users',
|
||||
'visibility.language_proficiencies': 'all_users',
|
||||
'visibility.social_links': 'all_users',
|
||||
'visibility.time_zone': 'all_users',
|
||||
});
|
||||
}
|
||||
|
||||
yield put(fetchProfileSuccess(
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
));
|
||||
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
if (e.response.status === 404) {
|
||||
history.push('/notfound');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleSaveProfilePhoto(action) {
|
||||
const { username, formData } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(saveProfilePhotoBegin());
|
||||
const photoResult = yield call(ProfileApiService.postProfilePhoto, username, formData);
|
||||
yield put(saveProfilePhotoSuccess(photoResult));
|
||||
yield put(saveProfilePhotoReset());
|
||||
} catch (e) {
|
||||
// Just reset on error, since editing functionality is deprecated
|
||||
yield put(saveProfilePhotoReset());
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleDeleteProfilePhoto(action) {
|
||||
const { username } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(deleteProfilePhotoBegin());
|
||||
const photoResult = yield call(ProfileApiService.deleteProfilePhoto, username);
|
||||
yield put(deleteProfilePhotoSuccess(photoResult));
|
||||
yield put(deleteProfilePhotoReset());
|
||||
} catch (e) {
|
||||
// Just reset on error, since editing functionality is deprecated
|
||||
yield put(deleteProfilePhotoReset());
|
||||
}
|
||||
}
|
||||
|
||||
export default function* profileSaga() {
|
||||
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
|
||||
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
|
||||
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
|
||||
}
|
||||
159
src/profile-v2/data/sagas.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
takeEvery,
|
||||
put,
|
||||
call,
|
||||
select,
|
||||
all,
|
||||
} from 'redux-saga/effects';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import * as profileActions from './actions';
|
||||
import { userAccountSelector } from './selectors';
|
||||
|
||||
import profileSaga, {
|
||||
handleFetchProfile,
|
||||
handleSaveProfilePhoto,
|
||||
handleDeleteProfilePhoto,
|
||||
} from './sagas';
|
||||
import * as ProfileApiService from './services';
|
||||
import {
|
||||
deleteProfilePhotoBegin,
|
||||
deleteProfilePhotoReset,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoReset,
|
||||
} from './actions';
|
||||
|
||||
jest.mock('./services', () => ({
|
||||
getAccount: jest.fn(),
|
||||
getCourseCertificates: jest.fn(),
|
||||
getPreferences: jest.fn(),
|
||||
postProfilePhoto: jest.fn(),
|
||||
deleteProfilePhoto: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('RootSaga', () => {
|
||||
describe('profileSaga', () => {
|
||||
it('should pass actions to the correct sagas', () => {
|
||||
const gen = profileSaga();
|
||||
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile));
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto));
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto));
|
||||
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFetchProfile', () => {
|
||||
it('should fetch certificates and preferences for the current user profile', () => {
|
||||
const userAccount = {
|
||||
username: 'gonzo',
|
||||
other: 'data',
|
||||
};
|
||||
getAuthenticatedUser.mockReturnValue(userAccount);
|
||||
const selectorData = {
|
||||
userAccount,
|
||||
};
|
||||
|
||||
const action = profileActions.fetchProfile('gonzo');
|
||||
const gen = handleFetchProfile(action);
|
||||
|
||||
const result = [userAccount, [1, 2, 3], { preferences: 'stuff' }];
|
||||
|
||||
expect(gen.next().value).toEqual(select(userAccountSelector));
|
||||
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
|
||||
expect(gen.next().value).toEqual(all([
|
||||
call(ProfileApiService.getAccount, 'gonzo'),
|
||||
call(ProfileApiService.getCourseCertificates, 'gonzo'),
|
||||
call(ProfileApiService.getPreferences, 'gonzo'),
|
||||
]));
|
||||
expect(gen.next(result).value)
|
||||
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[2], result[1], true)));
|
||||
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fetch certificates and profile for some other user profile', () => {
|
||||
const userAccount = {
|
||||
username: 'gonzo',
|
||||
other: 'data',
|
||||
};
|
||||
getAuthenticatedUser.mockReturnValue(userAccount);
|
||||
const selectorData = {
|
||||
userAccount,
|
||||
};
|
||||
|
||||
const action = profileActions.fetchProfile('booyah');
|
||||
const gen = handleFetchProfile(action);
|
||||
|
||||
const result = [{}, [1, 2, 3]];
|
||||
|
||||
expect(gen.next().value).toEqual(select(userAccountSelector));
|
||||
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
|
||||
expect(gen.next().value).toEqual(all([
|
||||
call(ProfileApiService.getAccount, 'booyah'),
|
||||
call(ProfileApiService.getCourseCertificates, 'booyah'),
|
||||
]));
|
||||
expect(gen.next(result).value)
|
||||
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false)));
|
||||
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSaveProfilePhoto', () => {
|
||||
it('should publish a reset action on error', () => {
|
||||
const action = profileActions.saveProfilePhoto('my username', {});
|
||||
const gen = handleSaveProfilePhoto(action);
|
||||
const error = new Error('Error occurred');
|
||||
|
||||
expect(gen.next().value).toEqual(put(saveProfilePhotoBegin()));
|
||||
expect(gen.throw(error).value).toEqual(put(saveProfilePhotoReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeleteProfilePhoto', () => {
|
||||
it('should publish a reset action on error', () => {
|
||||
const action = profileActions.deleteProfilePhoto('my username');
|
||||
const gen = handleDeleteProfilePhoto(action);
|
||||
const error = new Error('Error occurred');
|
||||
|
||||
expect(gen.next().value).toEqual(put(deleteProfilePhotoBegin()));
|
||||
expect(gen.throw(error).value).toEqual(put(deleteProfilePhotoReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeleteProfilePhoto', () => {
|
||||
it('should successfully process a deleteProfilePhoto request if there are no exceptions', () => {
|
||||
const action = profileActions.deleteProfilePhoto('my username');
|
||||
const gen = handleDeleteProfilePhoto(action);
|
||||
const photoResult = {};
|
||||
|
||||
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoBegin()));
|
||||
expect(gen.next().value).toEqual(call(ProfileApiService.deleteProfilePhoto, 'my username'));
|
||||
expect(gen.next(photoResult).value).toEqual(put(profileActions.deleteProfilePhotoSuccess(photoResult)));
|
||||
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should publish a failure action on exception', () => {
|
||||
const error = new Error('Error occurred');
|
||||
const action = profileActions.deleteProfilePhoto('my username');
|
||||
const gen = handleDeleteProfilePhoto(action);
|
||||
|
||||
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoBegin()));
|
||||
const result = gen.throw(error);
|
||||
expect(result.value).toEqual(put(profileActions.deleteProfilePhotoReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
58
src/profile-v2/data/selectors.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const userAccountSelector = state => state.userAccount;
|
||||
export const profileAccountSelector = state => state.profilePage.account;
|
||||
export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates;
|
||||
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
|
||||
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
|
||||
export const accountErrorsSelector = state => state.profilePage.errors;
|
||||
|
||||
export const certificatesSelector = createSelector(
|
||||
profileCourseCertificatesSelector,
|
||||
(certificates) => ({
|
||||
certificates,
|
||||
value: certificates,
|
||||
}),
|
||||
);
|
||||
|
||||
export const profileImageSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
account => (account.profileImage != null
|
||||
? {
|
||||
src: account.profileImage.imageUrlFull,
|
||||
isDefault: !account.profileImage.hasImage,
|
||||
}
|
||||
: {}),
|
||||
);
|
||||
|
||||
export const profilePageSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
profileCourseCertificatesSelector,
|
||||
profileImageSelector,
|
||||
savePhotoStateSelector,
|
||||
isLoadingProfileSelector,
|
||||
accountErrorsSelector,
|
||||
(
|
||||
account,
|
||||
courseCertificates,
|
||||
profileImage,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
errors,
|
||||
) => ({
|
||||
// Account data we need
|
||||
username: account.username,
|
||||
profileImage,
|
||||
requiresParentalConsent: account.requiresParentalConsent,
|
||||
dateJoined: account.dateJoined,
|
||||
yearOfBirth: account.yearOfBirth,
|
||||
name: account.name,
|
||||
|
||||
courseCertificates,
|
||||
|
||||
// Other data we need
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
photoUploadError: errors.photo || null,
|
||||
}),
|
||||
);
|
||||
149
src/profile-v2/data/services.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
|
||||
|
||||
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
|
||||
|
||||
function processAccountData(data) {
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
function processAndThrowError(error, errorDataProcessor) {
|
||||
const processedError = Object.create(error);
|
||||
if (error.response && error.response.data && typeof error.response.data === 'object') {
|
||||
processedError.processedData = errorDataProcessor(error.response.data);
|
||||
throw processedError;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET ACCOUNT
|
||||
export async function getAccount(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// PATCH PROFILE
|
||||
export async function patchProfile(username, params) {
|
||||
const processedParams = snakeCaseObject(params);
|
||||
|
||||
const { data } = await getHttpClient()
|
||||
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
processAndThrowError(error, processAccountData);
|
||||
});
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// POST PROFILE PHOTO
|
||||
|
||||
export async function postProfilePhoto(username, formData) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { data } = await getHttpClient().post(
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
).catch((error) => {
|
||||
processAndThrowError(error, camelCaseObject);
|
||||
});
|
||||
|
||||
// TODO: Someday in the future the POST photo endpoint
|
||||
// will return the new values. At that time we should
|
||||
// use the commented line below instead of the separate
|
||||
// getAccount request that follows.
|
||||
// return camelCaseObject(data);
|
||||
const updatedData = await getAccount(username);
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
// DELETE PROFILE PHOTO
|
||||
|
||||
export async function deleteProfilePhoto(username) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
|
||||
|
||||
// TODO: Someday in the future the POST photo endpoint
|
||||
// will return the new values. At that time we should
|
||||
// use the commented line below instead of the separate
|
||||
// getAccount request that follows.
|
||||
// return camelCaseObject(data);
|
||||
const updatedData = await getAccount(username);
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
// GET PREFERENCES
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
// PATCH PREFERENCES
|
||||
export async function patchPreferences(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
visibility_bio: 'visibility.bio',
|
||||
visibility_course_certificates: 'visibility.course_certificates',
|
||||
visibility_country: 'visibility.country',
|
||||
visibility_date_joined: 'visibility.date_joined',
|
||||
visibility_level_of_education: 'visibility.level_of_education',
|
||||
visibility_language_proficiencies: 'visibility.language_proficiencies',
|
||||
visibility_name: 'visibility.name',
|
||||
visibility_social_links: 'visibility.social_links',
|
||||
visibility_time_zone: 'visibility.time_zone',
|
||||
});
|
||||
|
||||
await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
});
|
||||
|
||||
return params; // TODO: Once the server returns the updated preferences object, return that.
|
||||
}
|
||||
|
||||
// GET COURSE CERTIFICATES
|
||||
|
||||
function transformCertificateData(data) {
|
||||
const transformedData = [];
|
||||
data.forEach((cert) => {
|
||||
// download_url may be full url or absolute path.
|
||||
// note: using the URL() api breaks in ie 11
|
||||
const urlIsPath = typeof cert.download_url === 'string'
|
||||
&& cert.download_url.search(/http[s]?:\/\//) !== 0;
|
||||
|
||||
const downloadUrl = urlIsPath
|
||||
? `${getConfig().LMS_BASE_URL}${cert.download_url}`
|
||||
: cert.download_url;
|
||||
|
||||
transformedData.push({
|
||||
...camelCaseObject(cert),
|
||||
certificateType: cert.certificate_type,
|
||||
downloadUrl,
|
||||
});
|
||||
});
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
export async function getCourseCertificates(username) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
|
||||
try {
|
||||
const { data } = await getHttpClient().get(url);
|
||||
return transformCertificateData(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
159
src/profile-v2/forms/ProfileAvatar.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Dropdown } from '@openedx/paragon';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
|
||||
import messages from './ProfileAvatar.messages';
|
||||
|
||||
const ProfileAvatar = ({
|
||||
src,
|
||||
isDefault,
|
||||
onSave,
|
||||
onDelete,
|
||||
savePhotoState,
|
||||
isEditable,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const fileInput = useRef(null);
|
||||
const form = useRef(null);
|
||||
|
||||
const onClickUpload = () => {
|
||||
fileInput.current.click();
|
||||
};
|
||||
|
||||
const onClickDelete = () => {
|
||||
onDelete();
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
onSave(new FormData(form.current));
|
||||
form.current.reset();
|
||||
};
|
||||
|
||||
const onChangeInput = () => {
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
const renderPending = () => (
|
||||
<div
|
||||
className="position-absolute w-100 h-100 d-flex justify-content-center
|
||||
align-items-center rounded-circle background-black-65"
|
||||
>
|
||||
<div className="spinner-border text-primary" role="status" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMenuContent = () => {
|
||||
if (isDefault) {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-white btn-block"
|
||||
onClick={onClickUpload}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.profileavatar.upload-button"
|
||||
defaultMessage="Upload Photo"
|
||||
description="Upload photo button"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle>
|
||||
{intl.formatMessage(messages['profile.profileavatar.change-button'])}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item type="button" onClick={onClickUpload}>
|
||||
<FormattedMessage
|
||||
id="profile.profileavatar.upload-button"
|
||||
defaultMessage="Upload Photo"
|
||||
description="Upload photo button"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item type="button" onClick={onClickDelete}>
|
||||
<FormattedMessage
|
||||
id="profile.profileavatar.remove.button"
|
||||
defaultMessage="Remove"
|
||||
description="Remove photo button"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
if (!isEditable) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="profile-avatar-menu-container">
|
||||
{renderMenuContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAvatar = () => (
|
||||
isDefault ? (
|
||||
<DefaultAvatar className="text-muted" role="img" aria-hidden focusable="false" viewBox="0 0 24 24" />
|
||||
) : (
|
||||
<img
|
||||
data-hj-suppress
|
||||
className="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
alt={intl.formatMessage(messages['profile.image.alt.attribute'])}
|
||||
src={src}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="profile-avatar-wrap position-relative">
|
||||
<div className="profile-avatar rounded-circle bg-light">
|
||||
{savePhotoState === 'pending' ? renderPending() : renderMenu()}
|
||||
{renderAvatar()}
|
||||
</div>
|
||||
<form
|
||||
ref={form}
|
||||
onSubmit={onSubmit}
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
{/* The name of this input must be 'file' */}
|
||||
<input
|
||||
className="d-none form-control-file"
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
name="file"
|
||||
id="photo-file"
|
||||
onChange={onChangeInput}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileAvatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
isDefault: PropTypes.bool,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
isEditable: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProfileAvatar.defaultProps = {
|
||||
src: null,
|
||||
isDefault: true,
|
||||
savePhotoState: null,
|
||||
isEditable: false,
|
||||
};
|
||||
|
||||
export default ProfileAvatar;
|
||||
16
src/profile-v2/forms/ProfileAvatar.messages.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.image.alt.attribute': {
|
||||
id: 'profile.image.alt.attribute',
|
||||
defaultMessage: 'profile avatar',
|
||||
description: 'Alt attribute for a profile photo',
|
||||
},
|
||||
'profile.profileavatar.change-button': {
|
||||
id: 'profile.profileavatar.change-button',
|
||||
defaultMessage: 'Change',
|
||||
description: 'Change photo button',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
5
src/profile-v2/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { default as ProfilePage } from './ProfilePage';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as messages } from './ProfilePage.messages';
|
||||
231
src/profile-v2/index.scss
Normal file
@@ -0,0 +1,231 @@
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
// TODO: Update edx-bootstrap theme to incorporate these edits.
|
||||
.btn, a.btn {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.btn-link {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-page-bg-banner {
|
||||
height: 298px;
|
||||
width: 100%;
|
||||
background-image: url('./assets/dot-pattern-light.png');
|
||||
background-repeat: repeat-x;
|
||||
background-size: auto 85%;
|
||||
}
|
||||
|
||||
.icon-visibility-off {
|
||||
height: 1rem;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
.edit-section-header {
|
||||
@extend .h6;
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label.edit-section-header {
|
||||
margin-bottom: $spacer * .5;
|
||||
}
|
||||
|
||||
.profile-avatar-wrap {
|
||||
@include media-breakpoint-up(md) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar-menu-container {
|
||||
background: rgba(0,0,0,.65);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: $white;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
}
|
||||
|
||||
.profile-avatar-edit-button {
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: .1rem;
|
||||
font-weight: 600;
|
||||
background: rgba(0,0,0,.5);
|
||||
border-radius:0;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
&:focus, &:hover, &:active, &.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.certificate {
|
||||
background-color: #F3F1ED;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px #E7E4DB solid;
|
||||
|
||||
.certificate-type-illustration {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 0;
|
||||
width: 15.15rem;
|
||||
opacity: .06;
|
||||
background-size: 90%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: Move the following to edx-paragon
|
||||
|
||||
.btn-rounded {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.max-width-32em {
|
||||
max-width: 32em;
|
||||
}
|
||||
|
||||
.max-width-19rem{
|
||||
max-width: 19rem;
|
||||
}
|
||||
.width-75rem {
|
||||
width: 75rem;
|
||||
}
|
||||
|
||||
.width-72rem {
|
||||
width: 72rem !important;
|
||||
}
|
||||
|
||||
.width-19625rem {
|
||||
width: 19.625rem;
|
||||
}
|
||||
|
||||
.height-2625rem {
|
||||
height: 2.625rem;
|
||||
}
|
||||
|
||||
.height-50vh {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.rounded-75 {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-4rem {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.py-4rem {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.py-0625rem {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.px-75rem {
|
||||
padding-left: 7.5rem;
|
||||
padding-right: 7.5rem;
|
||||
}
|
||||
|
||||
.px-25rem {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.g-15rem {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.g-5rem {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.g-1rem {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.g-3rem {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.color-black {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.background-black-65 {
|
||||
background-color: rgba(0,0,0,.65)
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
71
src/profile-v2/utils.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
// If the passed in object is not an object, return it.
|
||||
if (
|
||||
object === undefined
|
||||
|| object === null
|
||||
|| (typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*
|
||||
* TODO: Put somewhere common to it can be used by other MFEs.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
}
|
||||
103
src/profile-v2/utils.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
} from './utils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
||||
|
||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
@@ -11,7 +11,11 @@ const AgeMessage = ({ accountSettingsUrl }) => (
|
||||
show
|
||||
>
|
||||
<Alert.Heading id="profile.age.headline">
|
||||
Your profile cannot be shared.
|
||||
<FormattedMessage
|
||||
id="profile.age.cannotShare"
|
||||
defaultMessage="Your profile cannot be shared."
|
||||
description="Error message indicating that the user's profile cannot be shared"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const Banner = () => <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-md-block p-relative" />;
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
@@ -50,10 +50,9 @@ class ProfilePage extends React.Component {
|
||||
super(props, context);
|
||||
|
||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||
|
||||
this.state = {
|
||||
viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
|
||||
accountSettingsUrl: `${context.config.LMS_BASE_URL}/account/settings`,
|
||||
accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL,
|
||||
};
|
||||
|
||||
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
|
||||
@@ -126,7 +125,7 @@ class ProfilePage extends React.Component {
|
||||
|
||||
return (
|
||||
<span data-hj-suppress>
|
||||
<h1 className="h2 mb-0 font-weight-bold">{this.props.params.username}</h1>
|
||||
<h1 className="h2 mb-0 font-weight-bold text-truncate">{this.props.params.username}</h1>
|
||||
<DateJoined date={dateJoined} />
|
||||
{this.isYOBDisabled() && <UsernameDescription />}
|
||||
<hr className="d-none d-md-block" />
|
||||
@@ -178,17 +177,25 @@ class ProfilePage extends React.Component {
|
||||
visibilityLearningGoal,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
courseCertificates,
|
||||
visibilityCourseCertificates,
|
||||
bio,
|
||||
visibilityBio,
|
||||
requiresParentalConsent,
|
||||
isLoadingProfile,
|
||||
username,
|
||||
saveState,
|
||||
navigate,
|
||||
} = this.props;
|
||||
|
||||
if (isLoadingProfile) {
|
||||
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
||||
}
|
||||
|
||||
if (!username && saveState === 'error' && navigate) {
|
||||
navigate('/notfound');
|
||||
}
|
||||
|
||||
const commonFormProps = {
|
||||
openHandler: this.handleOpen,
|
||||
closeHandler: this.handleClose,
|
||||
@@ -196,6 +203,17 @@ class ProfilePage extends React.Component {
|
||||
changeHandler: this.handleChange,
|
||||
};
|
||||
|
||||
const isBlockVisible = (blockInfo) => this.isAuthenticatedUserProfile()
|
||||
|| (!this.isAuthenticatedUserProfile() && Boolean(blockInfo));
|
||||
|
||||
const isLanguageBlockVisible = isBlockVisible(languageProficiencies.length);
|
||||
const isEducationBlockVisible = isBlockVisible(levelOfEducation);
|
||||
const isSocialLinksBLockVisible = isBlockVisible(socialLinks.some((link) => link.socialLink !== null));
|
||||
const isBioBlockVisible = isBlockVisible(bio);
|
||||
const isCertificatesBlockVisible = isBlockVisible(courseCertificates.length);
|
||||
const isNameBlockVisible = isBlockVisible(name);
|
||||
const isLocationBlockVisible = isBlockVisible(country);
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||
@@ -212,7 +230,7 @@ class ProfilePage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col pl-0">
|
||||
<div className="col">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
@@ -230,46 +248,58 @@ class ProfilePage extends React.Component {
|
||||
<div className="d-md-none mb-4">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{isNameBlockVisible && (
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLocationBlockVisible && (
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLanguageBlockVisible && (
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isEducationBlockVisible && (
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isSocialLinksBLockVisible && (
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||
{!this.isYOBDisabled() && this.renderAgeMessage()}
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{isBioBlockVisible && (
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
|
||||
<LearningGoal
|
||||
learningGoal={learningGoal}
|
||||
@@ -278,11 +308,13 @@ class ProfilePage extends React.Component {
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{isCertificatesBlockVisible && (
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,6 +337,7 @@ ProfilePage.propTypes = {
|
||||
// Account data
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
dateJoined: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
|
||||
// Bio form data
|
||||
bio: PropTypes.string,
|
||||
@@ -370,6 +403,7 @@ ProfilePage.propTypes = {
|
||||
openForm: PropTypes.func.isRequired,
|
||||
closeForm: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
params: PropTypes.shape({
|
||||
@@ -382,6 +416,7 @@ ProfilePage.propTypes = {
|
||||
|
||||
ProfilePage.defaultProps = {
|
||||
saveState: null,
|
||||
username: '',
|
||||
savePhotoState: null,
|
||||
photoUploadError: {},
|
||||
profileImage: {},
|
||||
|
||||
@@ -3,13 +3,13 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { BrowserRouter, useNavigate } from 'react-router-dom';
|
||||
|
||||
import messages from '../i18n';
|
||||
import ProfilePage from './ProfilePage';
|
||||
@@ -17,6 +17,7 @@ import ProfilePage from './ProfilePage';
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const storeMocks = {
|
||||
loadingApp: require('./__mocks__/loadingApp.mockStore'),
|
||||
invalidUser: require('./__mocks__/invalidUser.mockStore'),
|
||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'),
|
||||
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'),
|
||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'),
|
||||
@@ -66,6 +67,23 @@ beforeEach(() => {
|
||||
analytics.sendTrackingLogEvent.mockReset();
|
||||
});
|
||||
|
||||
const ProfileWrapper = ({ params, requiresParentalConsent }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
params={params}
|
||||
requiresParentalConsent={requiresParentalConsent}
|
||||
navigate={navigate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileWrapper.propTypes = {
|
||||
params: PropTypes.shape({}).isRequired,
|
||||
requiresParentalConsent: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, params, requiresParentalConsent,
|
||||
}) => (
|
||||
@@ -74,7 +92,12 @@ const ProfilePageWrapper = ({
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<ProfilePage {...requiredProfilePageProps} params={params} requiresParentalConsent={requiresParentalConsent} />
|
||||
<BrowserRouter>
|
||||
<ProfileWrapper
|
||||
params={params}
|
||||
requiresParentalConsent={requiresParentalConsent}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
@@ -100,7 +123,17 @@ describe('<ProfilePage />', () => {
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.loadingApp)} />;
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('successfully redirected to not found page.', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.invalidUser)} />;
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -110,23 +143,41 @@ describe('<ProfilePage />', () => {
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.viewOwnProfile)} />;
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing other profile', () => {
|
||||
it('viewing other profile with all fields', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOtherProfile)}
|
||||
params={{ username: 'verified' }} // Override default params
|
||||
store={mockStore({
|
||||
...storeMocks.viewOtherProfile,
|
||||
profilePage: {
|
||||
...storeMocks.viewOtherProfile.profilePage,
|
||||
account: {
|
||||
...storeMocks.viewOtherProfile.profilePage.account,
|
||||
name: 'user',
|
||||
country: 'EN',
|
||||
bio: 'bio',
|
||||
courseCertificates: ['course certificates'],
|
||||
levelOfEducation: 'some level',
|
||||
languageProficiencies: ['some lang'],
|
||||
socialLinks: ['twitter'],
|
||||
timeZone: 'time zone',
|
||||
accountPrivacy: 'all_users',
|
||||
},
|
||||
},
|
||||
})}
|
||||
match={{ params: { username: 'verified' } }} // Override default match
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -141,7 +192,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeMocks.savingEditedBio)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -158,7 +209,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -176,7 +227,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -194,7 +245,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -212,7 +263,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -230,7 +281,7 @@ describe('<ProfilePage />', () => {
|
||||
store={mockStore(storeMocks.viewOwnProfile)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('test age message alert', () => {
|
||||
@@ -241,17 +292,15 @@ describe('<ProfilePage />', () => {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||
};
|
||||
const component = (
|
||||
const { container } = render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
requiresParentalConsent
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const wrapper = mount(component);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.alert-info').hasClass('show')).toBe(true);
|
||||
expect(container.querySelector('.alert-info')).toHaveClass('show');
|
||||
});
|
||||
it('test photo error alert', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
|
||||
@@ -260,11 +309,38 @@ describe('<ProfilePage />', () => {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeData)} />;
|
||||
const wrapper = mount(component);
|
||||
wrapper.update();
|
||||
const { container } = render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
requiresParentalConsent
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.alert-danger').hasClass('show')).toBe(true);
|
||||
expect(container.querySelector('.alert-danger')).toHaveClass('show');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['test user with non-disabled country', 'PK'],
|
||||
['test user with disabled country', 'RU'],
|
||||
])('test user with %s', (_, accountCountry) => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.country = {};
|
||||
storeData.profilePage.currentlyEditingField = 'country';
|
||||
storeData.profilePage.disabledCountries = ['RU'];
|
||||
storeData.profilePage.account.country = accountCountry;
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -274,15 +350,13 @@ describe('<ProfilePage />', () => {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
params={{ username: 'test-username' }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const wrapper = mount(component);
|
||||
wrapper.update();
|
||||
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { VisibilityOff } from '@edx/paragon/icons';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { VisibilityOff } from '@openedx/paragon/icons';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const UsernameDescription = () => (
|
||||
|
||||
41
src/profile/__mocks__/invalidUser.mockStore.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: null,
|
||||
bio: null,
|
||||
name: null,
|
||||
country: null,
|
||||
socialLinks: null,
|
||||
profileImage: {
|
||||
imageUrlMedium: null,
|
||||
imageUrlLarge: null
|
||||
},
|
||||
levelOfEducation: null,
|
||||
learningGoal: null
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: 'error',
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
account: {
|
||||
username: '',
|
||||
socialLinks: []
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
isAuthenticatedUserProfile: true,
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staffTest',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
@@ -125,7 +125,8 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false
|
||||
isLoadingProfile: false,
|
||||
disabledCountries: [],
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const initialState = {
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: false,
|
||||
disabledCountries: ['RU'],
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action = {}) => {
|
||||
@@ -63,12 +64,14 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
isLoadingProfile: false,
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case SAVE_PROFILE.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveState: null,
|
||||
isLoadingProfile: false,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import {
|
||||
@@ -66,6 +65,25 @@ export function* handleFetchProfile(action) {
|
||||
} else {
|
||||
[account, courseCertificates] = result;
|
||||
}
|
||||
|
||||
// Set initial visibility values for account
|
||||
// Set account_privacy as custom is necessary so that when viewing another user's profile,
|
||||
// their full name is displayed and change visibility forms are worked correctly
|
||||
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
|
||||
yield call(ProfileApiService.patchPreferences, action.payload.username, {
|
||||
account_privacy: 'custom',
|
||||
'visibility.name': 'all_users',
|
||||
'visibility.bio': 'all_users',
|
||||
'visibility.course_certificates': 'all_users',
|
||||
'visibility.country': 'all_users',
|
||||
'visibility.date_joined': 'all_users',
|
||||
'visibility.level_of_education': 'all_users',
|
||||
'visibility.language_proficiencies': 'all_users',
|
||||
'visibility.social_links': 'all_users',
|
||||
'visibility.time_zone': 'all_users',
|
||||
});
|
||||
}
|
||||
|
||||
yield put(fetchProfileSuccess(
|
||||
account,
|
||||
preferences,
|
||||
@@ -76,7 +94,11 @@ export function* handleFetchProfile(action) {
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
if (e.response.status === 404) {
|
||||
history.push('/notfound');
|
||||
if (e.processedData && e.processedData.fieldErrors) {
|
||||
yield put(saveProfileFailure(e.processedData.fieldErrors));
|
||||
} else {
|
||||
yield put(saveProfileFailure(e.customAttributes));
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const isLoadingProfileSelector = state => state.profilePage.isLoadingProf
|
||||
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
|
||||
export const accountErrorsSelector = state => state.profilePage.errors;
|
||||
export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile;
|
||||
export const disabledCountriesSelector = state => state.profilePage.disabledCountries;
|
||||
|
||||
export const editableFormModeSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
@@ -35,9 +36,12 @@ export const editableFormModeSelector = createSelector(
|
||||
// or is being hidden from us (for other users' profiles)
|
||||
let propExists = account[formId] != null && account[formId].length > 0;
|
||||
propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates
|
||||
// If this isn't the current user's profile or if
|
||||
// If this isn't the current user's profile
|
||||
if (!isAuthenticatedUserProfile) {
|
||||
return 'static';
|
||||
}
|
||||
// the current user has no age set / under 13 ...
|
||||
if (!isAuthenticatedUserProfile || account.requiresParentalConsent) {
|
||||
if (account.requiresParentalConsent) {
|
||||
// then there are only two options: static or nothing.
|
||||
// We use 'null' as a return value because the consumers of
|
||||
// getMode render nothing at all on a mode of null.
|
||||
@@ -127,10 +131,14 @@ export const countrySelector = createSelector(
|
||||
editableFormSelector,
|
||||
sortedCountriesSelector,
|
||||
countryMessagesSelector,
|
||||
(editableForm, sortedCountries, countryMessages) => ({
|
||||
disabledCountriesSelector,
|
||||
profileAccountSelector,
|
||||
(editableForm, sortedCountries, countryMessages, disabledCountries, account) => ({
|
||||
...editableForm,
|
||||
sortedCountries,
|
||||
countryMessages,
|
||||
disabledCountries,
|
||||
committedCountry: account.country,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -228,13 +236,13 @@ export const visibilitiesSelector = createSelector(
|
||||
switch (accountPrivacy) {
|
||||
case 'custom':
|
||||
return {
|
||||
visibilityBio: preferences.visibilityBio || 'private',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'private',
|
||||
visibilityCountry: preferences.visibilityCountry || 'private',
|
||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'private',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'private',
|
||||
visibilityName: preferences.visibilityName || 'private',
|
||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'private',
|
||||
visibilityBio: preferences.visibilityBio || 'all_users',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users',
|
||||
visibilityCountry: preferences.visibilityCountry || 'all_users',
|
||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
|
||||
visibilityName: preferences.visibilityName || 'all_users',
|
||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users',
|
||||
};
|
||||
case 'private':
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Bio.messages';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
|
||||
@@ -68,7 +68,7 @@ class Certificates extends React.Component {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div key={`${modifiedDate}-${courseId}`} className="col col-sm-6 d-flex align-items-stretch">
|
||||
<div key={`${modifiedDate}-${courseId}`} className="col-12 col-sm-6 d-flex align-items-stretch">
|
||||
<div className="card mb-4 certificate flex-grow-1">
|
||||
<div
|
||||
className="certificate-type-illustration"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Country.messages';
|
||||
|
||||
@@ -35,7 +35,13 @@ class Country extends React.Component {
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
const {
|
||||
country, disabledCountries, formId, submitHandler,
|
||||
} = this.props;
|
||||
|
||||
if (!disabledCountries.includes(country)) {
|
||||
submitHandler(formId);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
@@ -46,6 +52,26 @@ class Country extends React.Component {
|
||||
this.props.openHandler(this.props.formId);
|
||||
}
|
||||
|
||||
isDisabledCountry = (country) => {
|
||||
const { disabledCountries } = this.props;
|
||||
return disabledCountries.includes(country);
|
||||
};
|
||||
|
||||
filteredCountries = (countryList) => {
|
||||
const { disabledCountries, committedCountry } = this.props;
|
||||
|
||||
if (!disabledCountries.length) {
|
||||
return countryList;
|
||||
}
|
||||
|
||||
return countryList.filter(({ code }) => {
|
||||
const isDisabled = this.isDisabledCountry(code);
|
||||
const isUserCountry = code === committedCountry;
|
||||
|
||||
return !isDisabled || isUserCountry;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId,
|
||||
@@ -58,6 +84,7 @@ class Country extends React.Component {
|
||||
sortedCountries,
|
||||
countryMessages,
|
||||
} = this.props;
|
||||
const filteredCountries = this.filteredCountries(sortedCountries);
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
@@ -84,8 +111,8 @@ class Country extends React.Component {
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
<option value=""> </option>
|
||||
{sortedCountries.map(({ code, name }) => (
|
||||
<option key={code} value={code}>{name}</option>
|
||||
{filteredCountries.map(({ code, name }) => (
|
||||
<option key={code} value={code} disabled={this.isDisabledCountry(code)}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
{error !== null && (
|
||||
@@ -157,7 +184,9 @@ Country.propTypes = {
|
||||
code: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
disabledCountries: PropTypes.arrayOf(PropTypes.string),
|
||||
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
committedCountry: PropTypes.string,
|
||||
|
||||
// Actions
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
@@ -175,6 +204,8 @@ Country.defaultProps = {
|
||||
country: null,
|
||||
visibilityCountry: 'private',
|
||||
error: null,
|
||||
disabledCountries: [],
|
||||
committedCountry: '',
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import get from 'lodash.get';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Education.messages';
|
||||
|
||||
|
||||