Compare commits
103 Commits
djoy/use_h
...
registrati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25532dee77 | ||
|
|
b56f0e75cc | ||
|
|
c5aec2aa78 | ||
|
|
6542a29c1d | ||
|
|
0c0b14cdfe | ||
|
|
01f17bccf7 | ||
|
|
4db5823570 | ||
|
|
ee72aa5caf | ||
|
|
41f3317fd4 | ||
|
|
071c666add | ||
|
|
cf4ae4a51f | ||
|
|
563757d492 | ||
|
|
8009c0ac7b | ||
|
|
f75e9e05c3 | ||
|
|
752b9a36da | ||
|
|
23837b316b | ||
|
|
b411f22ff7 | ||
|
|
06f320c1be | ||
|
|
147f305cd2 | ||
|
|
d4ce099596 | ||
|
|
6721869a2d | ||
|
|
75ff0f8079 | ||
|
|
d19a332a5f | ||
|
|
2e5e5a1d3d | ||
|
|
9db0a99981 | ||
|
|
563bbc524a | ||
|
|
68428f7f98 | ||
|
|
7883ba5c77 | ||
|
|
b11a560d2f | ||
|
|
00bf8ad342 | ||
|
|
c064f7f413 | ||
|
|
8e9d07971b | ||
|
|
1ff9083755 | ||
|
|
0617585ee5 | ||
|
|
2cfc6dcdb4 | ||
|
|
c21cd8c011 | ||
|
|
9f31f341c1 | ||
|
|
44cefb7c20 | ||
|
|
d326eb5892 | ||
|
|
945b14fa4b | ||
|
|
92b8998b96 | ||
|
|
a158f8c708 | ||
|
|
637375e890 | ||
|
|
abe5af2870 | ||
|
|
0495c7f6ba | ||
|
|
56362695dd | ||
|
|
1d56ea026f | ||
|
|
edb5998617 | ||
|
|
b8cf476d01 | ||
|
|
7f5e840538 | ||
|
|
e5e950937b | ||
|
|
95cb4c9138 | ||
|
|
aac7244aec | ||
|
|
69f9c8faf5 | ||
|
|
c40ba138ce | ||
|
|
13d71b3257 | ||
|
|
2f459362ad | ||
|
|
bc706c9fe6 | ||
|
|
db7955b3e6 | ||
|
|
b59a39c4b3 | ||
|
|
382a68ef92 | ||
|
|
0e722e1906 | ||
|
|
5fb8e05e6b | ||
|
|
1766de1145 | ||
|
|
6edf9becb5 | ||
|
|
a1c74bd9b8 | ||
|
|
0139d2db75 | ||
|
|
38de20b454 | ||
|
|
8daddeadde | ||
|
|
74e5f2bf76 | ||
|
|
172f9bce4d | ||
|
|
fbbadfedd1 | ||
|
|
adf031e264 | ||
|
|
da5c6b592a | ||
|
|
39b57ead67 | ||
|
|
7c24f47560 | ||
|
|
4276442bfa | ||
|
|
bdad621102 | ||
|
|
c0be21fc3f | ||
|
|
75c1354fae | ||
|
|
39eb2bb310 | ||
|
|
ec0f816b0e | ||
|
|
c863e53855 | ||
|
|
f4811efe66 | ||
|
|
ea37cf01dd | ||
|
|
15d8836a8c | ||
|
|
05ea18f70d | ||
|
|
cca4af7cc1 | ||
|
|
b1a5f98541 | ||
|
|
8020ff58b8 | ||
|
|
569099e88a | ||
|
|
90260ec263 | ||
|
|
5ca2b801c7 | ||
|
|
149cce731d | ||
|
|
274af6c2e5 | ||
|
|
9cdf40f1bb | ||
|
|
22e855702c | ||
|
|
c566b157d9 | ||
|
|
d124a91688 | ||
|
|
5c578af96c | ||
|
|
3076227249 | ||
|
|
5cb1947a69 | ||
|
|
39cd052a81 |
41
.babelrc
41
.babelrc
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ["last 2 versions", "ie 11"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"babel-preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties",
|
||||
["transform-imports", {
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"transform": "@fortawesome/free-brands-svg-icons/${member}",
|
||||
"skipDefaultConversion": true
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"transform": "@fortawesome/free-regular-svg-icons/${member}",
|
||||
"skipDefaultConversion": true
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"transform": "@fortawesome/free-solid-svg-icons/${member}",
|
||||
"skipDefaultConversion": true
|
||||
}
|
||||
}]
|
||||
],
|
||||
"env": {
|
||||
"i18n": {
|
||||
"plugins": [
|
||||
["react-intl", {
|
||||
"messagesDir": "./temp/babel-plugin-react-intl",
|
||||
"moduleSourceName": "@edx/frontend-i18n"
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
18
.env
Normal file
18
.env
Normal file
@@ -0,0 +1,18 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=null
|
||||
BASE_URL=null
|
||||
CREDENTIALS_BASE_URL=null
|
||||
CSRF_TOKEN_API_PATH=null
|
||||
ECOMMERCE_BASE_URL=null
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=null
|
||||
LMS_BASE_URL=null
|
||||
LOGIN_URL=null
|
||||
LOGOUT_URL=null
|
||||
MARKETING_SITE_BASE_URL=null
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
ENABLE_LOGIN_AND_REGISTRATION=false
|
||||
19
.env.development
Normal file
19
.env.development
Normal file
@@ -0,0 +1,19 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
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'
|
||||
LOGIN_URL='http://localhost:1997/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
NODE_ENV='development'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
PORT=1997
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_LOGIN_AND_REGISTRATION=true
|
||||
18
.env.test
Normal file
18
.env.test
Normal file
@@ -0,0 +1,18 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
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'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_LOGIN_AND_REGISTRATION=false #hackathon 23 todo
|
||||
@@ -2,3 +2,4 @@ coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
|
||||
34
.eslintrc
34
.eslintrc
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"extends": "eslint-config-edx",
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"webpack/*.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.test.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
// https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/340#issuecomment-338424908
|
||||
"jsx-a11y/anchor-is-valid": [ "error", {
|
||||
"components": [ "Link" ],
|
||||
"specialLink": [ "to" ]
|
||||
}],
|
||||
"jsx-a11y/label-has-for": [ 2, {
|
||||
"components": [ "label" ],
|
||||
"required": {
|
||||
"some": [ "nesting", "id" ]
|
||||
},
|
||||
"allowChildren": false
|
||||
}]
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"newrelic": false
|
||||
}
|
||||
}
|
||||
3
.eslintrc.js
Executable file
3
.eslintrc.js
Executable file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
22
.npmignore
22
.npmignore
@@ -1,13 +1,15 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
webpack
|
||||
.tx
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
src
|
||||
.dockerignore
|
||||
.eslintignore
|
||||
.eslintrc
|
||||
.gitignore
|
||||
.releaserc
|
||||
.travis.yml
|
||||
babel.config.js
|
||||
Makefile
|
||||
renovate.json
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,23 +1,15 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- lts/*
|
||||
cache:
|
||||
directories:
|
||||
- "~/.npm"
|
||||
node_js: 12
|
||||
before_install:
|
||||
- npm install -g npm@latest
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
- npm install -g npm@6
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
- npm ci
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run i18n_extract
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
- npm run is-es5
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run i18n_extract
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
- npm run is-es5
|
||||
after_success:
|
||||
- npm run coveralls
|
||||
- codecov
|
||||
- codecov
|
||||
|
||||
149
LICENSE
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
66
README.rst
66
README.rst
@@ -1,42 +1,60 @@
|
||||
|Build Status| |Coveralls| |npm_version| |npm_downloads| |license|
|
||||
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
frontend-app-account
|
||||
=========================
|
||||
====================
|
||||
|
||||
Please tag **@edx/arch-team** on any PRs or issues.
|
||||
This is a micro-frontend application responsible for the display and updating of a user's account information. Please tag **@edx/arch-team** on any PRs or issues.
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
React app for account settings.
|
||||
|
||||
Get Started
|
||||
Development
|
||||
-----------
|
||||
|
||||
1. Start up your local devstack
|
||||
2. If you don't have node installed. Install Node
|
||||
3. In the project directory: npm install
|
||||
4. Then run npm start
|
||||
5. Open your browser to http://localhost:1997/account-settings
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Important Note
|
||||
--------------
|
||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
||||
|
||||
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`_
|
||||
to remove unused CSS from the production css file. In webpack/webpack.prod.config.js the Purgecss
|
||||
plugin is configured to scan directories to determine what css selectors should remain. Currently
|
||||
the src/ directory is scanned along with all @edx/frontend-component* node modules and paragon.
|
||||
If you add and use a component in this repo that relies on HTML classes or ids for styling you
|
||||
must add it to the Purgecss configuration or it will be unstyled in the production build.
|
||||
- Start devstack
|
||||
- Log in (http://localhost:18000/login)
|
||||
|
||||
Start the development server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In this project, install requirements and start the development server by running:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
|
||||
Once the dev server is up visit http://localhost:1997.
|
||||
|
||||
Configuration and Deployment
|
||||
----------------------------
|
||||
|
||||
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:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`__ to remove unused CSS from the production css file. In ``webpack.prod.config.js`` the Purgecss plugin is configured to scan directories to determine what css selectors should remain. Currently the src/ directory is scanned along with all ``@edx/frontend-component*`` node modules and ``@edx/paragon``. **If you add and use a component in this repo that relies on HTML classes or ids for styling you must add it to the Purgecss configuration or it will be unstyled in the production build.**
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.org/edx/frontend-app-account
|
||||
.. |Coveralls| image:: https://img.shields.io/coveralls/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://coveralls.io/github/edx/frontend-app-account
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/edx/frontend-app-account
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
|
||||
7
jest.config.js
Normal file
7
jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
@@ -4,3 +4,4 @@
|
||||
nick: acct
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release: {ref: master}
|
||||
|
||||
18137
package-lock.json
generated
18137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
204
package.json
204
package.json
@@ -1,154 +1,88 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-account",
|
||||
"version": "0.1.0",
|
||||
"description": "User account React app",
|
||||
"version": "1.0.0-semantically-released",
|
||||
"description": "User account micro-frontend for Open edX",
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-account.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=webpack/webpack.prod.config.js",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"i18n_extract": "BABEL_ENV=i18n babel src --quiet > /dev/null",
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "eslint --ext .js --ext .jsx .",
|
||||
"precommit": "npm run lint",
|
||||
"start": "NODE_ENV=development BABEL_ENV=development webpack-dev-server --config=webpack/webpack.dev.config.js --progress",
|
||||
"test": "jest --coverage --passWithNoTests",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
"lint": "fedx-scripts eslint",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-account/issues"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-account#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
],
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "^2.0.2",
|
||||
"@edx/edx-bootstrap": "^2.2.1",
|
||||
"@edx/frontend-analytics": "^2.0.0",
|
||||
"@edx/frontend-auth": "^5.3.4",
|
||||
"@edx/frontend-component-footer": "^6.0.2",
|
||||
"@edx/frontend-component-site-header": "^2.4.0",
|
||||
"@edx/frontend-i18n": "^2.1.0",
|
||||
"@edx/frontend-logging": "^2.0.2",
|
||||
"@edx/paragon": "^4.2.6",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.18",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.8.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"classnames": "^2.2.6",
|
||||
"connected-react-router": "^5.0.1",
|
||||
"email-prop-type": "^1.1.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"form-urlencoded": "^3.0.0",
|
||||
"formdata-polyfill": "^3.0.18",
|
||||
"glob": "^7.1.3",
|
||||
"history": "^4.7.2",
|
||||
"i18n-iso-countries": "^3.7.8",
|
||||
"iso-countries-languages": "^0.2.1",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.findindex": "^4.6.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"memoize-one": "^5.0.4",
|
||||
"newrelic": "^5.5.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.8.3",
|
||||
"react-dom": "^16.8.3",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-hash-link": "^1.2.1",
|
||||
"react-scrollspy": "^3.4.0",
|
||||
"react-transition-group": "^2.5.3",
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^1.0.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^3.1.0",
|
||||
"url-polyfill": "^1.1.5"
|
||||
"@edx/frontend-component-footer": "10.0.7",
|
||||
"@edx/frontend-component-header": "2.0.5",
|
||||
"@edx/frontend-platform": "1.2.0",
|
||||
"@edx/paragon": "7.1.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.8",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.2.6",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "4.0.1",
|
||||
"formdata-polyfill": "3.0.19",
|
||||
"history": "4.10.1",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.findindex": "4.6.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.isempty": "4.4.0",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"memoize-one": "5.1.1",
|
||||
"newrelic": "5.13.1",
|
||||
"prop-types": "15.7.2",
|
||||
"querystring": "0.2.0",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2",
|
||||
"react-redux": "7.1.3",
|
||||
"react-router": "5.1.2",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-router-hash-link": "1.2.2",
|
||||
"react-scrollspy": "3.4.2",
|
||||
"react-transition-group": "4.3.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^4.2.0",
|
||||
"autoprefixer": "^9.4.2",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-jest": "^22.4.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-react-intl": "^3.0.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-imports": "^1.5.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"codecov": "^3.0.0",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.28.9",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"es-check": "^5.0.0",
|
||||
"eslint-config-edx": "^4.0.3",
|
||||
"fetch-mock": "^6.3.0",
|
||||
"file-loader": "^1.1.9",
|
||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||
"html-webpack-new-relic-plugin": "^1.1.0",
|
||||
"html-webpack-plugin": "^3.0.3",
|
||||
"husky": "^0.14.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"image-webpack-loader": "^4.2.0",
|
||||
"jest": "^22.4.0",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"new-relic-source-map-webpack-plugin": "1.1.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-rtl": "^1.3.3",
|
||||
"purgecss-webpack-plugin": "^1.5.0",
|
||||
"react-dev-utils": "^5.0.0",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"@edx/frontend-build": "2.0.6",
|
||||
"codecov": "3.6.5",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.2",
|
||||
"es-check": "5.0.0",
|
||||
"glob": "7.1.6",
|
||||
"husky": "3.0.9",
|
||||
"purgecss-webpack-plugin": "1.6.0",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.20.2",
|
||||
"travis-deploy-once": "^5.0.9",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.0",
|
||||
"webpack-merge": "^4.1.1"
|
||||
},
|
||||
"jest": {
|
||||
"testURL": "http://localhost/",
|
||||
"setupFiles": [
|
||||
"./src/setupTest.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.svg": "<rootDir>/__mocks__/svgrMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|scss)$": "identity-obj-proxy"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"src/setupTest.js",
|
||||
"src/index.js",
|
||||
"/tests/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(@edx/paragon)/).*/"
|
||||
]
|
||||
"redux-mock-store": "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<title>Account | edX</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
9
renovate.json
Normal file
9
renovate.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig, history, getQueryParameters } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import memoize from 'memoize-one';
|
||||
import findIndex from 'lodash.findindex';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
injectIntl,
|
||||
intlShape,
|
||||
FormattedMessage,
|
||||
} from '@edx/frontend-i18n';
|
||||
getCountryList,
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import { fetchSettings, saveSettings, updateDraft } from './actions';
|
||||
import { accountSettingsPageSelector } from './selectors';
|
||||
|
||||
import { Alert, PageLoading } from '../common';
|
||||
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
|
||||
import { accountSettingsPageSelector } from './data/selectors';
|
||||
import PageLoading from './PageLoading';
|
||||
import Alert from './Alert';
|
||||
import JumpNav from './JumpNav';
|
||||
import DeleteAccount from './delete-account';
|
||||
import EditableField from './EditableField';
|
||||
@@ -27,40 +31,39 @@ import {
|
||||
YEAR_OF_BIRTH_OPTIONS,
|
||||
EDUCATION_LEVELS,
|
||||
GENDER_OPTIONS,
|
||||
} from './constants';
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.educationLevels = EDUCATION_LEVELS.map(key => ({
|
||||
value: key,
|
||||
label: props.intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]),
|
||||
}));
|
||||
this.genderOptions = GENDER_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
|
||||
}));
|
||||
this.languageProficiencyOptions = [{
|
||||
value: '',
|
||||
label: props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']),
|
||||
}].concat(props.languageProficiencyOptions);
|
||||
this.yearOfBirthOptions = [{
|
||||
value: '',
|
||||
label: props.intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']),
|
||||
}].concat(YEAR_OF_BIRTH_OPTIONS);
|
||||
this.countryOptions = [{
|
||||
value: '',
|
||||
label: props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
|
||||
}].concat(props.countryOptions);
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another Open edX account. We use this to display a message to that effect, and remove the
|
||||
// parameter from the URL.
|
||||
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
|
||||
if (duplicateTpaProvider !== undefined) {
|
||||
history.replace(history.location.pathname);
|
||||
}
|
||||
this.state = {
|
||||
duplicateTpaProvider,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSettings();
|
||||
this.props.fetchSiteLanguages();
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
page: 'account',
|
||||
visibility: null,
|
||||
user_id: this.context.authenticatedUser.userId,
|
||||
});
|
||||
}
|
||||
|
||||
getTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions) => {
|
||||
// NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it!
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => {
|
||||
const concatTimeZoneOptions = [{
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.default']),
|
||||
value: '',
|
||||
@@ -78,6 +81,29 @@ class AccountSettingsPage extends React.Component {
|
||||
return concatTimeZoneOptions;
|
||||
});
|
||||
|
||||
getLocalizedOptions = memoize(locale => ({
|
||||
countryOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
|
||||
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
languageProficiencyOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']),
|
||||
}].concat(getLanguageList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
yearOfBirthOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']),
|
||||
}].concat(YEAR_OF_BIRTH_OPTIONS),
|
||||
educationLevelOptions: EDUCATION_LEVELS.map(key => ({
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]),
|
||||
})),
|
||||
genderOptions: GENDER_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
|
||||
})),
|
||||
}));
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -97,7 +123,7 @@ class AccountSettingsPage extends React.Component {
|
||||
};
|
||||
|
||||
renderDuplicateTpaProviderMessage() {
|
||||
if (!this.props.duplicateTpaProvider) {
|
||||
if (!this.state.duplicateTpaProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +135,7 @@ class AccountSettingsPage extends React.Component {
|
||||
defaultMessage="The {provider} account you selected is already linked to another edX account."
|
||||
description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
|
||||
values={{
|
||||
provider: <b>{this.props.duplicateTpaProvider}</b>,
|
||||
provider: <b>{this.state.duplicateTpaProvider}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
@@ -132,7 +158,7 @@ class AccountSettingsPage extends React.Component {
|
||||
values={{
|
||||
managerTitle: <b>{this.props.profileDataManager}</b>,
|
||||
support: (
|
||||
<Hyperlink destination={this.props.supportUrl} target="_blank">
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.managed.settings.support"
|
||||
defaultMessage="support"
|
||||
@@ -179,9 +205,19 @@ class AccountSettingsPage extends React.Component {
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
const timeZoneOptions = this.getTimeZoneOptions(
|
||||
// Memoized options lists
|
||||
const {
|
||||
countryOptions,
|
||||
languageProficiencyOptions,
|
||||
yearOfBirthOptions,
|
||||
educationLevelOptions,
|
||||
genderOptions,
|
||||
} = this.getLocalizedOptions(this.context.locale);
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
this.props.countryTimeZoneOptions,
|
||||
this.context.locale,
|
||||
);
|
||||
|
||||
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
|
||||
@@ -240,14 +276,14 @@ class AccountSettingsPage extends React.Component {
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])}
|
||||
value={this.props.formValues.year_of_birth}
|
||||
options={this.yearOfBirthOptions}
|
||||
options={yearOfBirthOptions}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="country"
|
||||
type="select"
|
||||
value={this.props.formValues.country}
|
||||
options={this.countryOptions}
|
||||
options={countryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
|
||||
emptyLabel={
|
||||
this.isEditable('country') ?
|
||||
@@ -268,7 +304,7 @@ class AccountSettingsPage extends React.Component {
|
||||
name="level_of_education"
|
||||
type="select"
|
||||
value={this.props.formValues.level_of_education}
|
||||
options={this.educationLevels}
|
||||
options={educationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])}
|
||||
{...editableFieldProps}
|
||||
@@ -277,7 +313,7 @@ class AccountSettingsPage extends React.Component {
|
||||
name="gender"
|
||||
type="select"
|
||||
value={this.props.formValues.gender}
|
||||
options={this.genderOptions}
|
||||
options={genderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
@@ -286,7 +322,7 @@ class AccountSettingsPage extends React.Component {
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
value={this.props.formValues.language_proficiencies}
|
||||
options={this.languageProficiencyOptions}
|
||||
options={languageProficiencyOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
@@ -335,7 +371,7 @@ class AccountSettingsPage extends React.Component {
|
||||
name="siteLanguage"
|
||||
type="select"
|
||||
options={this.props.siteLanguageOptions}
|
||||
value={this.props.siteLanguage.draftOrSavedValue}
|
||||
value={this.props.siteLanguage.draft !== undefined ? this.props.siteLanguage.draft : this.context.locale}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.site.language'])}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
|
||||
{...editableFieldProps}
|
||||
@@ -366,7 +402,6 @@ class AccountSettingsPage extends React.Component {
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
logoutUrl={this.props.logoutUrl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -420,6 +455,8 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
AccountSettingsPage.contextType = AppContext;
|
||||
|
||||
AccountSettingsPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
@@ -444,21 +481,12 @@ AccountSettingsPage.propTypes = {
|
||||
}).isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draftOrSavedValue: PropTypes.string,
|
||||
savedValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
}),
|
||||
siteLanguageOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
countryOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
languageProficiencyOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
profileDataManager: PropTypes.string,
|
||||
staticFields: PropTypes.arrayOf(PropTypes.string),
|
||||
hiddenFields: PropTypes.arrayOf(PropTypes.string),
|
||||
@@ -476,10 +504,7 @@ AccountSettingsPage.propTypes = {
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
duplicateTpaProvider: PropTypes.string,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.object),
|
||||
supportUrl: PropTypes.string.isRequired,
|
||||
logoutUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
@@ -488,14 +513,11 @@ AccountSettingsPage.defaultProps = {
|
||||
loadingError: null,
|
||||
siteLanguage: null,
|
||||
siteLanguageOptions: [],
|
||||
countryOptions: [],
|
||||
timeZoneOptions: [],
|
||||
countryTimeZoneOptions: [],
|
||||
languageProficiencyOptions: [],
|
||||
profileDataManager: null,
|
||||
staticFields: [],
|
||||
hiddenFields: ['secondary_email'],
|
||||
duplicateTpaProvider: null,
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-i18n';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.page.heading': {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { betaLanguageBannerSelector } from './selectors';
|
||||
import { betaLanguageBannerSelector } from './data/selectors';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { saveSettings } from './actions';
|
||||
import { TRANSIFEX_LANGUAGE_BASE_URL } from './constants';
|
||||
import { Alert } from '../common';
|
||||
import { saveSettings } from './data/actions';
|
||||
import { TRANSIFEX_LANGUAGE_BASE_URL } from './data/constants';
|
||||
import Alert from './Alert';
|
||||
|
||||
class BetaLanguageBanner extends React.Component {
|
||||
getSiteLanguageEntry(languageCode) {
|
||||
@@ -47,7 +48,7 @@ class BetaLanguageBanner extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const savedLanguage = this.getSiteLanguageEntry(this.props.siteLanguage.savedValue);
|
||||
const savedLanguage = this.getSiteLanguageEntry(this.context.locale);
|
||||
const isSavedLanguageReleased = savedLanguage.released === true;
|
||||
const noPreviousLanguageSet = this.props.siteLanguage.previousValue === null;
|
||||
if (isSavedLanguageReleased || noPreviousLanguageSet) {
|
||||
@@ -87,12 +88,13 @@ class BetaLanguageBanner extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
BetaLanguageBanner.contextType = AppContext;
|
||||
|
||||
BetaLanguageBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draftOrSavedValue: PropTypes.string,
|
||||
savedValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
}),
|
||||
siteLanguageList: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { SwitchContent } from '../common';
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
} from './actions';
|
||||
import { editableFieldSelector } from './selectors';
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
|
||||
function EditableField(props) {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Alert, SwitchContent } from '../common';
|
||||
import Alert from './Alert';
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
} from './actions';
|
||||
import { editableFieldSelector } from './selectors';
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
|
||||
function EmailField(props) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
@@ -20,6 +20,9 @@
|
||||
}
|
||||
li {
|
||||
margin-bottom: .5rem;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +32,7 @@
|
||||
}
|
||||
.account-section {
|
||||
// These properties together will shift the hashlink position
|
||||
margin-bottom: map-get($spacers, 5);
|
||||
margin-bottom: map-get($spacers, 5);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { utils } from '../common';
|
||||
|
||||
const { AsyncActionType } = utils;
|
||||
import { AsyncActionType } from './utils';
|
||||
|
||||
export const FETCH_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_SETTINGS');
|
||||
export const SAVE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_SETTINGS');
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
RESET_DRAFTS,
|
||||
} from './actions';
|
||||
|
||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from './delete-account';
|
||||
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from './site-language';
|
||||
import { reducer as resetPasswordReducer, RESET_PASSWORD } from './reset-password';
|
||||
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from './third-party-auth';
|
||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
|
||||
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
|
||||
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
|
||||
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
|
||||
|
||||
export const defaultState = {
|
||||
loading: false,
|
||||
@@ -151,8 +151,8 @@ const reducer = (state = defaultState, action) => {
|
||||
countryTimeZones: action.payload.timeZones,
|
||||
};
|
||||
|
||||
// TODO: Once all the above cases have been converted into sub-reducers, we can use
|
||||
// combineReducers in this file to greatly simplify it.
|
||||
// TODO: Once all the above cases have been converted into sub-reducers, we can use
|
||||
// combineReducers in this file to greatly simplify it.
|
||||
|
||||
// Delete My Account
|
||||
case DELETE_ACCOUNT.CONFIRMATION:
|
||||
@@ -1,4 +1,8 @@
|
||||
import { call, put, delay, takeEvery, select, all } from 'redux-saga/effects';
|
||||
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
|
||||
|
||||
import { publish } from '@edx/frontend-platform';
|
||||
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
@@ -16,29 +20,29 @@ import {
|
||||
fetchTimeZones,
|
||||
fetchTimeZonesSuccess,
|
||||
} from './actions';
|
||||
import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors';
|
||||
|
||||
// Sub-modules
|
||||
import { saga as deleteAccountSaga } from './delete-account';
|
||||
import { saga as resetPasswordSaga } from './reset-password';
|
||||
import { saga as siteLanguageSaga, ApiService as SiteLanguageApiService } from './site-language';
|
||||
import { saga as thirdPartyAuthSaga } from './third-party-auth';
|
||||
import { saga as deleteAccountSaga } from '../delete-account';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
import {
|
||||
saga as siteLanguageSaga,
|
||||
patchPreferences,
|
||||
postSetLang,
|
||||
} from '../site-language';
|
||||
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
|
||||
|
||||
// Services
|
||||
import * as ApiService from './service';
|
||||
|
||||
import { setLocale, handleRtl } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
import { getSettings, patchSettings, getTimeZones } from './service';
|
||||
|
||||
export function* handleFetchSettings() {
|
||||
try {
|
||||
yield put(fetchSettingsBegin());
|
||||
const username = yield select(usernameSelector);
|
||||
const userRoles = yield select(userRolesSelector);
|
||||
const { username, roles: userRoles } = getAuthenticatedUser();
|
||||
|
||||
const {
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
|
||||
} = yield call(
|
||||
ApiService.getSettings,
|
||||
getSettings,
|
||||
username,
|
||||
userRoles,
|
||||
);
|
||||
@@ -61,22 +65,25 @@ export function* handleSaveSettings(action) {
|
||||
try {
|
||||
yield put(saveSettingsBegin());
|
||||
|
||||
const username = yield select(usernameSelector);
|
||||
const { username } = getAuthenticatedUser();
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
let savedValues = null;
|
||||
if (formId === 'siteLanguage') {
|
||||
const previousSiteLanguage = yield select(siteLanguageSelector);
|
||||
yield all([
|
||||
call(SiteLanguageApiService.patchPreferences, username, { prefLang: commitValues }),
|
||||
call(SiteLanguageApiService.postSetLang, commitValues),
|
||||
]);
|
||||
yield put(setLocale(commitValues));
|
||||
yield put(savePreviousSiteLanguage(previousSiteLanguage.savedValue));
|
||||
const previousSiteLanguage = getLocale();
|
||||
// The following two requests need to be done sequentially, with patching preferences before
|
||||
// the post to setlang. They used to be done in parallel, but this might create ambiguous
|
||||
// behavior.
|
||||
yield call(patchPreferences, username, { prefLang: commitValues });
|
||||
yield call(postSetLang, commitValues);
|
||||
|
||||
yield put(savePreviousSiteLanguage(previousSiteLanguage));
|
||||
|
||||
publish(LOCALE_CHANGED, getLocale());
|
||||
handleRtl();
|
||||
savedValues = commitData;
|
||||
} else {
|
||||
savedValues = yield call(ApiService.patchSettings, username, commitData);
|
||||
savedValues = yield call(patchSettings, username, commitData);
|
||||
}
|
||||
yield put(saveSettingsSuccess(savedValues, commitData));
|
||||
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
|
||||
@@ -93,7 +100,7 @@ export function* handleSaveSettings(action) {
|
||||
}
|
||||
|
||||
export function* handleFetchTimeZones(action) {
|
||||
const response = yield call(ApiService.getTimeZones, action.payload.country);
|
||||
const response = yield call(getTimeZones, action.payload.country);
|
||||
yield put(fetchTimeZonesSuccess(response, action.payload.country));
|
||||
}
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import {
|
||||
localeSelector,
|
||||
getCountryList,
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
|
||||
import { siteLanguageOptionsSelector, siteLanguageListSelector } from './site-language';
|
||||
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
|
||||
|
||||
export const storeName = 'accountSettings';
|
||||
|
||||
export const usernameSelector = state => state.authentication.username;
|
||||
|
||||
export const userRolesSelector = state => state.authentication.roles || [];
|
||||
|
||||
export const accountSettingsSelector = state => ({ ...state[storeName] });
|
||||
|
||||
const duplicateTpaProviderSelector = state => state.errors.duplicateTpaProvider;
|
||||
|
||||
const configurationSelector = state => state.configuration;
|
||||
|
||||
const editableFieldNameSelector = (state, props) => props.name;
|
||||
|
||||
const valuesSelector = createSelector(
|
||||
@@ -100,17 +87,6 @@ const formValuesSelector = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
const countryOptionsSelector = createSelector(
|
||||
localeSelector,
|
||||
locale => getCountryList(locale).map(({ code, name }) => ({ value: code, label: name })),
|
||||
);
|
||||
|
||||
const languageProficiencyOptionsSelector = createSelector(
|
||||
localeSelector,
|
||||
locale => getLanguageList(locale).map(({ code, name }) => ({ value: code, label: name })),
|
||||
);
|
||||
|
||||
|
||||
const transformTimeZonesToOptions = timeZoneArr => timeZoneArr
|
||||
.map(({ time_zone, description }) => ({ // eslint-disable-line camelcase
|
||||
value: time_zone, label: description,
|
||||
@@ -131,40 +107,24 @@ const activeAccountSelector = createSelector(
|
||||
accountSettings => accountSettings.values.is_active,
|
||||
);
|
||||
|
||||
/**
|
||||
* This selector converts the site language code back to the server version so that it can match up
|
||||
* with one of the options in the site language dropdown. The drafts version will already be the
|
||||
* server version, but if it's from localeSelector, it will be our client (two character) version.
|
||||
*/
|
||||
export const siteLanguageSelector = createSelector(
|
||||
previousSiteLanguageSelector,
|
||||
draftsSelector,
|
||||
localeSelector,
|
||||
(previousValue, drafts, locale) => ({
|
||||
(previousValue, drafts) => ({
|
||||
previousValue,
|
||||
draftOrSavedValue: (drafts.siteLanguage !== undefined ? drafts.siteLanguage : locale),
|
||||
savedValue: locale,
|
||||
draft: drafts.siteLanguage,
|
||||
}),
|
||||
);
|
||||
|
||||
export const betaLanguageBannerSelector = createSelector(
|
||||
siteLanguageListSelector,
|
||||
siteLanguageSelector,
|
||||
(
|
||||
siteLanguageList,
|
||||
siteLanguage,
|
||||
) => ({
|
||||
siteLanguageList,
|
||||
siteLanguage,
|
||||
}),
|
||||
);
|
||||
export const betaLanguageBannerSelector = createStructuredSelector({
|
||||
siteLanguageList: siteLanguageListSelector,
|
||||
siteLanguage: siteLanguageSelector,
|
||||
});
|
||||
|
||||
export const accountSettingsPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
siteLanguageOptionsSelector,
|
||||
siteLanguageSelector,
|
||||
countryOptionsSelector,
|
||||
languageProficiencyOptionsSelector,
|
||||
formValuesSelector,
|
||||
profileDataManagerSelector,
|
||||
staticFieldsSelector,
|
||||
@@ -172,14 +132,10 @@ export const accountSettingsPageSelector = createSelector(
|
||||
timeZonesSelector,
|
||||
countryTimeZonesSelector,
|
||||
activeAccountSelector,
|
||||
duplicateTpaProviderSelector,
|
||||
configurationSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
countryOptions,
|
||||
languageProficiencyOptions,
|
||||
formValues,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
@@ -187,13 +143,9 @@ export const accountSettingsPageSelector = createSelector(
|
||||
timeZoneOptions,
|
||||
countryTimeZoneOptions,
|
||||
activeAccount,
|
||||
duplicateTpaProvider,
|
||||
configuration,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
countryOptions,
|
||||
languageProficiencyOptions,
|
||||
loading: accountSettings.loading,
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
@@ -204,9 +156,6 @@ export const accountSettingsPageSelector = createSelector(
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
hiddenFields,
|
||||
duplicateTpaProvider,
|
||||
tpaProviders: accountSettings.thirdPartyAuth.providers,
|
||||
supportUrl: configuration.SUPPORT_URL,
|
||||
logoutUrl: configuration.LOGOUT_URL,
|
||||
}),
|
||||
);
|
||||
@@ -1,22 +1,11 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { applyConfiguration, handleRequestError, unpackFieldErrors } from '../common/serviceUtils';
|
||||
import { configureService as configureDeleteAccountApiService } from './delete-account';
|
||||
import { configureService as configureResetPasswordApiService } from './reset-password';
|
||||
import { configureService as configureSiteLanguageApiService } from './site-language';
|
||||
import { configureService as configureThirdPartyAuthApiService, getThirdPartyAuthProviders } from './third-party-auth';
|
||||
|
||||
let config = {
|
||||
BASE_URL: null,
|
||||
ACCOUNTS_API_BASE_URL: null,
|
||||
PREFERENCES_API_BASE_URL: null,
|
||||
ECOMMERCE_API_BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
DELETE_ACCOUNT_URL: null,
|
||||
PASSWORD_RESET_URL: null,
|
||||
};
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -24,18 +13,6 @@ const SOCIAL_PLATFORMS = [
|
||||
{ id: 'linkedin', key: 'social_link_linkedin' },
|
||||
];
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
export function configureService(newConfig, newApiClient) {
|
||||
config = applyConfiguration(config, newConfig);
|
||||
apiClient = newApiClient;
|
||||
|
||||
configureDeleteAccountApiService(config, apiClient);
|
||||
configureResetPasswordApiService(config, apiClient);
|
||||
configureSiteLanguageApiService(config, apiClient);
|
||||
configureThirdPartyAuthApiService(config, apiClient);
|
||||
}
|
||||
|
||||
function unpackAccountResponseData(data) {
|
||||
const unpackedData = data;
|
||||
|
||||
@@ -90,7 +67,8 @@ function packAccountCommitData(commitData) {
|
||||
}
|
||||
|
||||
export async function getAccount(username) {
|
||||
const { data } = await apiClient.get(`${config.ACCOUNTS_API_BASE_URL}/${username}`);
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
||||
return unpackAccountResponseData(data);
|
||||
}
|
||||
|
||||
@@ -99,9 +77,9 @@ export async function patchAccount(username, commitValues) {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
};
|
||||
|
||||
const { data } = await apiClient
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(
|
||||
`${config.ACCOUNTS_API_BASE_URL}/${username}`,
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`,
|
||||
packAccountCommitData(commitValues),
|
||||
requestConfig,
|
||||
)
|
||||
@@ -122,23 +100,25 @@ export async function patchAccount(username, commitValues) {
|
||||
}
|
||||
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await apiClient.get(`${config.PREFERENCES_API_BASE_URL}/${username}`);
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function patchPreferences(username, commitValues) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/merge-patch+json' } };
|
||||
const requestUrl = `${config.PREFERENCES_API_BASE_URL}/${username}`;
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`;
|
||||
|
||||
// Ignore the success response, the API does not currently return any data.
|
||||
await apiClient.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
|
||||
|
||||
return commitValues;
|
||||
}
|
||||
|
||||
export async function getTimeZones(forCountry) {
|
||||
const { data } = await apiClient
|
||||
.get(`${config.LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
|
||||
params: { country_code: forCountry },
|
||||
})
|
||||
.catch(handleRequestError);
|
||||
@@ -153,8 +133,8 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
const userRoleNames = userRoles.map(role => role.split(':')[0]);
|
||||
|
||||
if (userRoleNames.includes('enterprise_learner')) {
|
||||
const url = `${config.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
|
||||
const { data } = await apiClient.get(url).catch(handleRequestError);
|
||||
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
|
||||
|
||||
if ('results' in data) {
|
||||
for (let i = 0; i < data.results.length; i += 1) {
|
||||
@@ -217,4 +197,3 @@ export async function patchSettings(username, commitValues) {
|
||||
const combinedResults = Object.assign({}, ...results);
|
||||
return combinedResults;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;
|
||||
38
src/account-settings/data/utils/dataUtils.js
Normal file
38
src/account-settings/data/utils/dataUtils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
keepKeys,
|
||||
getModuleState,
|
||||
} from './utils';
|
||||
} from './dataUtils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
@@ -91,77 +88,3 @@ describe('convertKeyNames', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keepKeys', () => {
|
||||
it('should keep the specified keys only', () => {
|
||||
const result = keepKeys(
|
||||
{
|
||||
one: 123,
|
||||
two: { three: 'skip me' },
|
||||
four: 'five',
|
||||
six: null,
|
||||
8: 'sneaky',
|
||||
},
|
||||
[
|
||||
'one',
|
||||
'three',
|
||||
'six',
|
||||
'seven',
|
||||
'8', // yup, the 8 integer will be converted to a string.
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: 123,
|
||||
six: null,
|
||||
8: 'sneaky',
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleState', () => {
|
||||
const state = {
|
||||
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
second: { other: 'data' },
|
||||
};
|
||||
|
||||
it('should return everything if given an empty path', () => {
|
||||
expect(getModuleState(state, [])).toEqual(state);
|
||||
});
|
||||
|
||||
it('should resolve paths correctly', () => {
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first'],
|
||||
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
|
||||
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first', 'red'],
|
||||
)).toEqual({ awesome: 'sauce' });
|
||||
|
||||
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
|
||||
});
|
||||
|
||||
it('should throw an exception on a bad path', () => {
|
||||
expect(() => {
|
||||
getModuleState(state, ['uhoh']);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should return non-objects correctly', () => {
|
||||
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
src/account-settings/data/utils/index.js
Normal file
12
src/account-settings/data/utils/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
modifyObjectKeys,
|
||||
snakeCaseObject,
|
||||
} from './dataUtils';
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
export { default as handleFailure } from './sagaUtils';
|
||||
export { unpackFieldErrors, handleRequestError } from './serviceUtils';
|
||||
@@ -1,50 +1,32 @@
|
||||
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;
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
|
||||
export function keepKeys(data, whitelist) {
|
||||
const result = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (whitelist.indexOf(key) > -1) {
|
||||
result[key] = data[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,36 +60,3 @@ export function getModuleState(state, originalPath) {
|
||||
}
|
||||
return getModuleState(state[key], path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
}
|
||||
}
|
||||
51
src/account-settings/data/utils/reduxUtils.test.js
Normal file
51
src/account-settings/data/utils/reduxUtils.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleState', () => {
|
||||
const state = {
|
||||
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
second: { other: 'data' },
|
||||
};
|
||||
|
||||
it('should return everything if given an empty path', () => {
|
||||
expect(getModuleState(state, [])).toEqual(state);
|
||||
});
|
||||
|
||||
it('should resolve paths correctly', () => {
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first'],
|
||||
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
|
||||
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first', 'red'],
|
||||
)).toEqual({ awesome: 'sauce' });
|
||||
|
||||
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
|
||||
});
|
||||
|
||||
it('should throw an exception on a bad path', () => {
|
||||
expect(() => {
|
||||
getModuleState(state, ['uhoh']);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should return non-objects correctly', () => {
|
||||
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,16 @@
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { push } from 'connected-react-router';
|
||||
import { logAPIErrorResponse } from '@edx/frontend-logging';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
|
||||
if (error.fieldErrors && failureAction !== null) {
|
||||
yield put(failureAction({ fieldErrors: error.fieldErrors }));
|
||||
}
|
||||
logAPIErrorResponse(error);
|
||||
logError(error);
|
||||
if (failureAction !== null) {
|
||||
yield put(failureAction(error.message));
|
||||
}
|
||||
if (failureRedirectPath !== null) {
|
||||
yield put(push(failureRedirectPath));
|
||||
history.push(failureRedirectPath);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,3 @@
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
export function applyConfiguration(expected, actual) {
|
||||
Object.keys(expected).forEach((key) => {
|
||||
if (actual[key] === undefined) {
|
||||
throw new Error(`Service configuration error: ${key} is required.`);
|
||||
}
|
||||
});
|
||||
return pick(actual, Object.keys(expected));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns field errors of the form:
|
||||
*
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
@@ -9,10 +9,10 @@ import { Hyperlink } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
|
||||
// Components
|
||||
import { Alert } from '../../common';
|
||||
import Alert from '../Alert';
|
||||
|
||||
const BeforeProceedingBanner = (props) => {
|
||||
const { instructionMessageId, intl, supportUrl } = props;
|
||||
const { instructionMessageId, intl, supportArticleUrl } = props;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -25,7 +25,7 @@ const BeforeProceedingBanner = (props) => {
|
||||
description="Error that appears if you are trying to delete your edX account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
values={{
|
||||
actionLink: (
|
||||
<Hyperlink destination={supportUrl}>
|
||||
<Hyperlink destination={supportArticleUrl}>
|
||||
{intl.formatMessage(messages[instructionMessageId])}
|
||||
</Hyperlink>
|
||||
),
|
||||
@@ -38,7 +38,7 @@ const BeforeProceedingBanner = (props) => {
|
||||
BeforeProceedingBanner.propTypes = {
|
||||
instructionMessageId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
supportUrl: PropTypes.string.isRequired,
|
||||
supportArticleUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BeforeProceedingBanner);
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import messages from './messages';
|
||||
import { Alert } from '../../common';
|
||||
import Alert from '../Alert';
|
||||
import PrintingInstructions from './PrintingInstructions';
|
||||
|
||||
export class ConfirmationModal extends Component {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-i18n';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
@@ -21,7 +21,6 @@ describe('ConfirmationModal', () => {
|
||||
status: null,
|
||||
errorType: null,
|
||||
password: 'fluffy bunnies',
|
||||
logoutUrl: 'http://localhost/logout',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
// Actions
|
||||
@@ -46,7 +47,7 @@ export class DeleteAccount extends React.Component {
|
||||
};
|
||||
|
||||
handleFinalClose = () => {
|
||||
global.location = this.props.logoutUrl;
|
||||
global.location = getConfig().LOGOUT_URL;
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -87,14 +88,14 @@ export class DeleteAccount extends React.Component {
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.activate"
|
||||
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -123,7 +124,6 @@ DeleteAccount.propTypes = {
|
||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||
hasLinkedTPA: PropTypes.bool,
|
||||
isVerifiedAccount: PropTypes.bool,
|
||||
logoutUrl: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-i18n';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Testing the modals separately, they just clutter up the snapshots if included here.
|
||||
jest.mock('./ConfirmationModal');
|
||||
@@ -24,7 +24,6 @@ describe('DeleteAccount', () => {
|
||||
errorType: null,
|
||||
hasLinkedTPA: false,
|
||||
isVerifiedAccount: true,
|
||||
logoutUrl: 'http://localhost/logout',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-i18n';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
@@ -8,11 +8,11 @@ exports[`ConfirmationModal should match default closed confirmation modal snapsh
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
@@ -26,7 +26,7 @@ exports[`ConfirmationModal should match default closed confirmation modal snapsh
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
id="id2"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
@@ -120,6 +120,7 @@ exports[`ConfirmationModal should match default closed confirmation modal snapsh
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -142,11 +143,11 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id5"
|
||||
aria-labelledby="id6"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
@@ -160,7 +161,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id5"
|
||||
id="id6"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
@@ -287,6 +288,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton5"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -309,7 +311,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
@@ -421,6 +423,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton3"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -8,11 +8,11 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
@@ -26,7 +26,7 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
id="id2"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
@@ -47,6 +47,7 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -69,7 +70,7 @@ exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
@@ -108,6 +109,7 @@ exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton3"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -130,68 +132,7 @@ exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onClick={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id5"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id5"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
@@ -230,6 +171,69 @@ exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton5"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id8"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id8"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton7"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -252,11 +256,11 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id7"
|
||||
aria-labelledby="id10"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
@@ -270,7 +274,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id7"
|
||||
id="id10"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
@@ -291,6 +295,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton9"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { utils } from '../../../common';
|
||||
|
||||
const { AsyncActionType } = utils;
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const DELETE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'DELETE_ACCOUNT');
|
||||
DELETE_ACCOUNT.CONFIRMATION = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CONFIRMATION';
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
|
||||
|
||||
let config = {
|
||||
DELETE_ACCOUNT_URL: null,
|
||||
};
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
export function configureService(newConfig, newApiClient) {
|
||||
config = applyConfiguration(config, newConfig);
|
||||
apiClient = newApiClient;
|
||||
}
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
/**
|
||||
* Request deletion of the user's account.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postDeleteAccount(password) {
|
||||
const { data } = await apiClient
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
config.DELETE_ACCOUNT_URL,
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/deactivate_logout/`,
|
||||
formurlencoded({ password }),
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { default } from './DeleteAccount';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { configureService } from './data/service';
|
||||
export { DELETE_ACCOUNT } from './data/actions';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-i18n';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.delete.account.header': {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import ConnectedAccountSettingsPage from './AccountSettingsPage';
|
||||
import reducer from './reducers';
|
||||
import saga from './sagas';
|
||||
import { configureService } from './service';
|
||||
import { storeName } from './selectors';
|
||||
|
||||
export {
|
||||
configureService,
|
||||
ConnectedAccountSettingsPage,
|
||||
reducer,
|
||||
saga,
|
||||
storeName,
|
||||
};
|
||||
export { default } from './AccountSettingsPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Alert } from '../../common';
|
||||
import Alert from '../Alert';
|
||||
|
||||
const ConfirmationAlert = (props) => {
|
||||
const { email } = props;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { StatefulButton } from '@edx/paragon';
|
||||
|
||||
import { resetPassword } from './data/actions';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { utils } from '../../../common';
|
||||
|
||||
const { AsyncActionType } = utils;
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const RESET_PASSWORD = new AsyncActionType('ACCOUNT_SETTINGS', 'RESET_PASSWORD');
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
|
||||
|
||||
let config = {
|
||||
PASSWORD_RESET_URL: null,
|
||||
};
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
export function configureService(newConfig, newApiClient) {
|
||||
config = applyConfiguration(config, newConfig);
|
||||
apiClient = newApiClient;
|
||||
}
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postResetPassword(email) {
|
||||
const { data } = await apiClient
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
config.PASSWORD_RESET_URL,
|
||||
`${getConfig().LMS_BASE_URL}/password_reset/`,
|
||||
formurlencoded({ email }),
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -2,4 +2,3 @@ export { default } from './ResetPassword';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { RESET_PASSWORD } from './data/actions';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { configureService } from './data/service';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-i18n';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.editable.field.password.reset.button': {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AsyncActionType } from '../../common/utils';
|
||||
import { AsyncActionType } from '../data/utils';
|
||||
|
||||
export const FETCH_SITE_LANGUAGES = new AsyncActionType('SITE_LANGUAGE', 'FETCH_SITE_LANGUAGES');
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import reducer from './reducers';
|
||||
import saga from './sagas';
|
||||
import { configureService, ApiService } from './service';
|
||||
import { siteLanguageOptionsSelector, siteLanguageListSelector } from './selectors';
|
||||
import { fetchSiteLanguages, FETCH_SITE_LANGUAGES } from './actions';
|
||||
|
||||
export { default as reducer } from './reducers';
|
||||
export { default as saga } from './sagas';
|
||||
export {
|
||||
ApiService,
|
||||
configureService,
|
||||
fetchSiteLanguages,
|
||||
FETCH_SITE_LANGUAGES,
|
||||
reducer,
|
||||
saga,
|
||||
siteLanguageListSelector,
|
||||
siteLanguageOptionsSelector,
|
||||
};
|
||||
getSiteLanguageList,
|
||||
patchPreferences,
|
||||
postSetLang,
|
||||
} from './service';
|
||||
export { siteLanguageOptionsSelector, siteLanguageListSelector } from './selectors';
|
||||
export { fetchSiteLanguages, FETCH_SITE_LANGUAGES } from './actions';
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
FETCH_SITE_LANGUAGES,
|
||||
} from './actions';
|
||||
|
||||
import { ApiService } from './service';
|
||||
import handleFailure from '../../common/sagaUtils';
|
||||
import { getSiteLanguageList } from './service';
|
||||
import { handleFailure } from '../data/utils';
|
||||
|
||||
function* handleFetchSiteLanguages() {
|
||||
try {
|
||||
yield put(fetchSiteLanguagesBegin());
|
||||
const siteLanguageList = yield call(ApiService.getSiteLanguageList);
|
||||
const siteLanguageList = yield call(getSiteLanguageList);
|
||||
yield put(fetchSiteLanguagesSuccess(siteLanguageList));
|
||||
} catch (e) {
|
||||
yield call(handleFailure, e, fetchSiteLanguagesFailure);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { getModuleState } from '../../common/utils';
|
||||
import { getModuleState } from '../data/utils';
|
||||
|
||||
export const storePath = ['accountSettings', 'siteLanguage'];
|
||||
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import siteLanguageList from './constants';
|
||||
import { snakeCaseObject, convertKeyNames } from '../../common/utils';
|
||||
import { applyConfiguration } from '../../common/serviceUtils';
|
||||
import { snakeCaseObject, convertKeyNames } from '../data/utils';
|
||||
|
||||
let config = {
|
||||
BASE_URL: null,
|
||||
PREFERENCES_API_BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
};
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
export function configureService(newConfig, newApiClient) {
|
||||
config = applyConfiguration(config, newConfig);
|
||||
apiClient = newApiClient;
|
||||
}
|
||||
|
||||
async function getSiteLanguageList() {
|
||||
export async function getSiteLanguageList() {
|
||||
return siteLanguageList;
|
||||
}
|
||||
|
||||
async function patchPreferences(username, params) {
|
||||
export async function patchPreferences(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
pref_lang: 'pref-lang',
|
||||
});
|
||||
|
||||
await apiClient.patch(`${config.PREFERENCES_API_BASE_URL}/${username}`, processedParams, {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
});
|
||||
await getAuthenticatedHttpClient()
|
||||
.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.
|
||||
}
|
||||
|
||||
async function postSetLang(code) {
|
||||
export async function postSetLang(code) {
|
||||
const formData = new FormData();
|
||||
formData.append('language', code);
|
||||
|
||||
await apiClient.post(`${config.LMS_BASE_URL}/i18n/setlang/`, formData, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
});
|
||||
await getAuthenticatedHttpClient()
|
||||
.post(`${getConfig().LMS_BASE_URL}/i18n/setlang/`, formData, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
});
|
||||
}
|
||||
|
||||
export const ApiService = {
|
||||
getSiteLanguageList,
|
||||
patchPreferences,
|
||||
postSetLang,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, StatefulButton } from '@edx/paragon';
|
||||
|
||||
import { Alert } from '../../common';
|
||||
import Alert from '../Alert';
|
||||
import { disconnectAuth } from './data/actions';
|
||||
|
||||
class ThirdPartyAuth extends Component {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { utils } from '../../../common';
|
||||
|
||||
const { AsyncActionType } = utils;
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const DISCONNECT_AUTH = new AsyncActionType('ACCOUNT_SETTINGS', 'DISCONNECT_AUTH');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
import { logAPIErrorResponse } from '@edx/frontend-logging';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import {
|
||||
disconnectAuthReset,
|
||||
@@ -23,7 +23,7 @@ function* handleDisconnectAuth(action) {
|
||||
const thirdPartyAuthProviders = yield call(getThirdPartyAuthProviders);
|
||||
yield put(disconnectAuthSuccess(providerId, thirdPartyAuthProviders));
|
||||
} catch (e) {
|
||||
logAPIErrorResponse(e);
|
||||
logError(e);
|
||||
yield put(disconnectAuthFailure(providerId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
let config = {
|
||||
LMS_BASE_URL: null,
|
||||
};
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
export function configureService(newConfig, newApiClient) {
|
||||
config = applyConfiguration(config, newConfig);
|
||||
apiClient = newApiClient;
|
||||
}
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
export async function getThirdPartyAuthProviders() {
|
||||
const { data } = await apiClient
|
||||
.get(`${config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`)
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`)
|
||||
.catch(handleRequestError);
|
||||
|
||||
return data.map(({ connect_url: connectUrl, disconnect_url: disconnectUrl, ...provider }) => ({
|
||||
...provider,
|
||||
connectUrl: `${config.LMS_BASE_URL}${connectUrl}`,
|
||||
disconnectUrl: `${config.LMS_BASE_URL}${disconnectUrl}`,
|
||||
connectUrl: `${getConfig().LMS_BASE_URL}${connectUrl}`,
|
||||
disconnectUrl: `${getConfig().LMS_BASE_URL}${disconnectUrl}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function postDisconnectAuth(url) {
|
||||
const { data } = await apiClient.post(url).catch(handleRequestError);
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(url)
|
||||
.catch(handleRequestError);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { default } from './ThirdPartyAuth';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { configureService, getThirdPartyAuthProviders } from './data/service';
|
||||
export { getThirdPartyAuthProviders, postDisconnectAuth } from './data/service';
|
||||
export { DISCONNECT_AUTH } from './data/actions';
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/favicon.ico
Normal file
BIN
src/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -12,4 +12,4 @@
|
||||
<path d="M710.619925,85.5 L664.019925,85.5 L664.019925,0.02 L576.019925,0.02 L576.019925,85.5 L632.859925,85.5 L632.859925,159.2 C598.417874,134.487772 557.04992,121.286425 514.659925,121.48 C456.044663,121.405246 400.107354,146.01621 360.559925,189.28 C381.937732,213.470272 397.422343,242.283149 405.799925,273.46 C426.944121,233.500977 468.451514,208.51034 513.659925,208.52 C581.059925,208.52 632.879925,263.16 632.879925,330.52 L632.879925,331.2 C632.539925,398.28 580.879925,452.56 513.659925,452.56 C468.477451,452.593197 426.976426,427.652566 405.799925,387.74 L405.799925,387.74 C401.869213,380.340239 398.718926,372.551658 396.399925,364.5 L308.399925,364.5 C309.686934,372.450225 311.443338,380.317312 313.659925,388.06 C315.970162,396.190434 318.775397,404.171995 322.059925,411.96 L397.659925,411.96 C388.805702,433.929527 376.258024,454.222269 360.559925,471.96 C400.107354,515.22379 456.044663,539.834754 514.659925,539.76 C571.465111,540.091874 625.745998,516.316729 664.019925,474.34 L664.019925,452.04 L705.059925,452.04 L718.899925,434.96 L718.899925,95.74 L710.619925,85.5 Z M632.879925,501.9 L632.879925,539.74 L664.019925,539.74 L664.019925,474.18 C654.623775,484.469293 644.18821,493.758755 632.879925,501.9 L632.879925,501.9 Z M313.599925,273.14 C311.569597,280.231983 309.927163,287.429316 308.679925,294.7 L322.179925,294.7 C320.007771,287.25831 317.134915,280.039338 313.599925,273.14 L313.599925,273.14 Z" id="Shape" fill="#8A8C8F" fill-rule="nonzero"></path>
|
||||
<path d="M410.399925,294.7 C409.199925,287.5 407.659925,280.4 405.799925,273.46 C402.19356,280.242036 399.246706,287.354671 396.999925,294.7 C390.06397,317.386469 389.855554,341.597488 396.399925,364.4 L410.719925,364.4 C414.264276,341.293291 414.156293,317.77319 410.399925,294.7 L410.399925,294.7 Z M209.059925,121.48 C107.422724,121.487508 20.5081632,194.571683 3.05992537,294.7 L91.3999254,294.7 C107.135726,243.467257 154.465065,208.503753 208.059925,208.52 C252.638644,208.335148 293.496156,233.351373 313.599925,273.14 C322.501981,241.908137 338.58337,213.190393 360.559925,189.28 C322.206855,145.880863 266.976617,121.163964 209.059925,121.48 L209.059925,121.48 Z M297.479925,411.86 C275.077969,437.877726 242.392659,452.761934 208.059925,452.58 C153.691226,452.598435 105.87164,416.63791 90.7999254,364.4 L308.339925,364.4 C304.575868,341.309464 304.690779,317.752715 308.679925,294.7 L3.05992537,294.7 C-0.902504563,317.755068 -1.01739385,341.307372 2.71992537,364.4 L2.71992537,364.4 C19.3292424,465.441984 106.661918,539.594765 209.059925,539.6 C266.986094,539.900862 322.217868,515.161403 360.559925,471.74 C344.364089,454.096816 331.320914,433.801419 321.999925,411.74 L297.479925,411.86 Z" id="Shape" fill="#B72768" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`keepKeys getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { fetchUserAccount as _fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
|
||||
|
||||
let userAccountApiService = null;
|
||||
|
||||
export function configureUserAccountApiService(configuration, apiClient) {
|
||||
userAccountApiService = new UserAccountApiService(apiClient, configuration.LMS_BASE_URL);
|
||||
}
|
||||
|
||||
export function fetchUserAccount(username) {
|
||||
return _fetchUserAccount(userAccountApiService, username);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { logAPIErrorResponse } from '@edx/frontend-logging';
|
||||
|
||||
import ErrorPage from './ErrorPage';
|
||||
|
||||
/*
|
||||
Error boundary component used to log caught errors and display the error page.
|
||||
*/
|
||||
export default class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
logAPIErrorResponse(`${error} ${info}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ErrorBoundary.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
export default class ErrorPage extends Component {
|
||||
reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<p className="my-0 py-5 text-muted">
|
||||
<FormattedMessage
|
||||
id="unexpected.error.message.text"
|
||||
defaultMessage="An unexpected error occurred. Please click the button below to return to refresh the page."
|
||||
description="error message when an unexpected error occurs"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
onClick={this.reload}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="unexpected.error.button.text"
|
||||
defaultMessage="Try Again"
|
||||
description="text for button that tries to reload the app by refreshing the page"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as utils from './utils';
|
||||
import Alert from './components/Alert';
|
||||
import PageLoading from './components/PageLoading';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import SwitchContent from './components/SwitchContent';
|
||||
import { configureUserAccountApiService, fetchUserAccount } from './actions';
|
||||
|
||||
export {
|
||||
Alert,
|
||||
ErrorBoundary,
|
||||
PageLoading,
|
||||
SwitchContent,
|
||||
utils,
|
||||
configureUserAccountApiService,
|
||||
fetchUserAccount,
|
||||
};
|
||||
@@ -1,266 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect, Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import { sendTrackEvent } from '@edx/frontend-analytics';
|
||||
import { IntlProvider, injectIntl, intlShape, getMessages } from '@edx/frontend-i18n';
|
||||
import SiteHeader from '@edx/frontend-component-site-header';
|
||||
import SiteFooter from '@edx/frontend-component-footer';
|
||||
|
||||
import {
|
||||
faFacebookSquare,
|
||||
faTwitterSquare,
|
||||
faYoutubeSquare,
|
||||
faLinkedin,
|
||||
faRedditSquare,
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { ErrorBoundary, fetchUserAccount } from '../common';
|
||||
import { ConnectedAccountSettingsPage } from '../account-settings';
|
||||
|
||||
import FooterLogo from '../assets/edx-footer.png';
|
||||
import HeaderLogo from '../assets/logo.svg';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
|
||||
import messages from './App.messages';
|
||||
|
||||
function PageContent({
|
||||
configuration,
|
||||
username,
|
||||
avatar,
|
||||
intl,
|
||||
}) {
|
||||
const mainMenu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['siteheader.links.courses']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/dashboard/programs`,
|
||||
content: intl.formatMessage(messages['siteheader.links.programs']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.MARKETING_SITE_BASE_URL}/course`,
|
||||
content: intl.formatMessage(messages['siteheader.links.content.search']),
|
||||
onClick: () => {
|
||||
sendTrackEvent(
|
||||
'edx.bi.dashboard.find_courses_button.clicked',
|
||||
{ category: 'account', label: 'header' },
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const userMenu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}`,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/u/${username}`,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.profile']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/account/settings`,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: configuration.ORDER_HISTORY_URL,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.order.history']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: configuration.LOGOUT_URL,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.logout']),
|
||||
},
|
||||
];
|
||||
const loggedOutItems = [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/login`,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.login']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${configuration.LMS_BASE_URL}/register`,
|
||||
content: intl.formatMessage(messages['siteheader.user.menu.register']),
|
||||
},
|
||||
];
|
||||
const socialLinks = [
|
||||
{
|
||||
title: 'Facebook',
|
||||
url: configuration.FACEBOOK_URL,
|
||||
icon: <FontAwesomeIcon icon={faFacebookSquare} className="social-icon" size="2x" />,
|
||||
screenReaderText: 'Like edX on Facebook',
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
url: configuration.TWITTER_URL,
|
||||
icon: <FontAwesomeIcon icon={faTwitterSquare} className="social-icon" size="2x" />,
|
||||
screenReaderText: 'Follow edX on Twitter',
|
||||
},
|
||||
{
|
||||
title: 'Youtube',
|
||||
url: configuration.YOU_TUBE_URL,
|
||||
icon: <FontAwesomeIcon icon={faYoutubeSquare} className="social-icon" size="2x" />,
|
||||
screenReaderText: 'Subscribe to the edX YouTube channel',
|
||||
},
|
||||
{
|
||||
title: 'LinkedIn',
|
||||
url: configuration.LINKED_IN_URL,
|
||||
icon: <FontAwesomeIcon icon={faLinkedin} className="social-icon" size="2x" />,
|
||||
screenReaderText: 'Follow edX on LinkedIn',
|
||||
},
|
||||
{
|
||||
title: 'Reddit',
|
||||
url: configuration.REDDIT_URL,
|
||||
icon: <FontAwesomeIcon icon={faRedditSquare} className="social-icon" size="2x" />,
|
||||
screenReaderText: 'Subscribe to the edX subreddit',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div id="app">
|
||||
<SiteHeader
|
||||
logo={HeaderLogo}
|
||||
loggedIn
|
||||
username={username}
|
||||
avatar={avatar}
|
||||
logoAltText={configuration.SITE_NAME}
|
||||
logoDestination={`${configuration.LMS_BASE_URL}/dashboard`}
|
||||
mainMenu={mainMenu}
|
||||
userMenu={userMenu}
|
||||
loggedOutItems={loggedOutItems}
|
||||
/>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/" component={ConnectedAccountSettingsPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={configuration.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
marketingSiteBaseUrl={configuration.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={configuration.SUPPORT_URL}
|
||||
contactUrl={configuration.CONTACT_URL}
|
||||
openSourceUrl={configuration.OPEN_SOURCE_URL}
|
||||
termsOfServiceUrl={configuration.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={configuration.PRIVACY_POLICY_URL}
|
||||
appleAppStoreUrl={configuration.APPLE_APP_STORE_URL}
|
||||
googlePlayUrl={configuration.GOOGLE_PLAY_URL}
|
||||
socialLinks={socialLinks}
|
||||
enterpriseMarketingLink={{
|
||||
url: configuration.ENTERPRISE_MARKETING_URL,
|
||||
queryParams: {
|
||||
utm_source: configuration.ENTERPRISE_MARKETING_UTM_SOURCE,
|
||||
utm_campaign: configuration.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
|
||||
utm_medium: configuration.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
|
||||
},
|
||||
}}
|
||||
handleAllTrackEvents={sendTrackEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IntlPageContent = injectIntl(PageContent);
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
const { username } = this.props;
|
||||
this.props.fetchUserAccount(username);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<IntlProvider locale={this.props.locale} messages={getMessages()}>
|
||||
<Provider store={this.props.store}>
|
||||
<ConnectedRouter history={this.props.history}>
|
||||
<IntlPageContent
|
||||
configuration={this.props.configuration}
|
||||
username={this.props.username}
|
||||
avatar={this.props.avatar}
|
||||
/>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const configurationPropTypes = {
|
||||
SITE_NAME: PropTypes.string.isRequired,
|
||||
LMS_BASE_URL: PropTypes.string.isRequired,
|
||||
LOGOUT_URL: PropTypes.string.isRequired,
|
||||
MARKETING_SITE_BASE_URL: PropTypes.string.isRequired,
|
||||
SUPPORT_URL: PropTypes.string.isRequired,
|
||||
CONTACT_URL: PropTypes.string.isRequired,
|
||||
OPEN_SOURCE_URL: PropTypes.string.isRequired,
|
||||
TERMS_OF_SERVICE_URL: PropTypes.string.isRequired,
|
||||
PRIVACY_POLICY_URL: PropTypes.string.isRequired,
|
||||
FACEBOOK_URL: PropTypes.string.isRequired,
|
||||
TWITTER_URL: PropTypes.string.isRequired,
|
||||
YOU_TUBE_URL: PropTypes.string.isRequired,
|
||||
LINKED_IN_URL: PropTypes.string.isRequired,
|
||||
REDDIT_URL: PropTypes.string.isRequired,
|
||||
APPLE_APP_STORE_URL: PropTypes.string.isRequired,
|
||||
GOOGLE_PLAY_URL: PropTypes.string.isRequired,
|
||||
ORDER_HISTORY_URL: PropTypes.string.isRequired,
|
||||
ENTERPRISE_MARKETING_URL: PropTypes.string.isRequired,
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE: PropTypes.string.isRequired,
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: PropTypes.string.isRequired,
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
PageContent.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string,
|
||||
configuration: PropTypes.shape(configurationPropTypes).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
PageContent.defaultProps = {
|
||||
avatar: null,
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
fetchUserAccount: PropTypes.func.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string,
|
||||
store: PropTypes.object.isRequired, // eslint-disable-line
|
||||
history: PropTypes.object.isRequired, // eslint-disable-line
|
||||
locale: PropTypes.string.isRequired,
|
||||
configuration: PropTypes.shape(configurationPropTypes).isRequired,
|
||||
};
|
||||
|
||||
App.defaultProps = {
|
||||
avatar: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
username: state.authentication.username,
|
||||
configuration: state.configuration,
|
||||
locale: state.i18n.locale,
|
||||
avatar: state.userAccount.profileImage.hasImage
|
||||
? state.userAccount.profileImage.imageUrlMedium
|
||||
: null,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
fetchUserAccount,
|
||||
},
|
||||
)(App);
|
||||
@@ -1,61 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'siteheader.links.courses': {
|
||||
id: 'siteheader.links.courses',
|
||||
defaultMessage: 'Courses',
|
||||
description: 'Link to the learner course dashboard',
|
||||
},
|
||||
'siteheader.links.programs': {
|
||||
id: 'siteheader.links.programs',
|
||||
defaultMessage: 'Programs',
|
||||
description: 'Link to the learner program dashboard',
|
||||
},
|
||||
'siteheader.links.content.search': {
|
||||
id: 'siteheader.links.content.search',
|
||||
defaultMessage: 'Discover New',
|
||||
description: 'Link to the content search page',
|
||||
},
|
||||
'siteheader.user.menu.dashboard': {
|
||||
id: 'siteheader.user.menu.dashboard',
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'Link to the user dashboard',
|
||||
},
|
||||
'siteheader.user.menu.profile': {
|
||||
id: 'siteheader.user.menu.profile',
|
||||
defaultMessage: 'Profile',
|
||||
description: 'Link to the user profile',
|
||||
},
|
||||
'siteheader.user.menu.account.settings': {
|
||||
id: 'siteheader.user.menu.account.settings',
|
||||
defaultMessage: 'Account',
|
||||
description: 'Link to account settings',
|
||||
},
|
||||
'siteheader.user.menu.order.history': {
|
||||
id: 'siteheader.user.menu.order.history',
|
||||
defaultMessage: 'Order History',
|
||||
description: 'Link to order history',
|
||||
},
|
||||
'siteheader.user.menu.logout': {
|
||||
id: 'siteheader.user.menu.logout',
|
||||
defaultMessage: 'Logout',
|
||||
description: 'Logout link',
|
||||
},
|
||||
'siteheader.user.menu.login': {
|
||||
id: 'siteheader.user.menu.login',
|
||||
defaultMessage: 'Login',
|
||||
description: 'Login link',
|
||||
},
|
||||
'siteheader.user.menu.register': {
|
||||
id: 'siteheader.user.menu.register',
|
||||
defaultMessage: 'Sign Up',
|
||||
description: 'Link to registration',
|
||||
},
|
||||
'app.loading.message': {
|
||||
id: 'app.loading.message',
|
||||
defaultMessage: 'Loading...',
|
||||
description: 'Message shown when page content is loading.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
33
src/data/configureStore.js
Normal file
33
src/data/configureStore.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
|
||||
import createRootReducer from './reducers';
|
||||
import rootSaga from './sagas';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
function composeMiddleware() {
|
||||
if (getConfig().ENVIRONMENT === 'development') {
|
||||
const loggerMiddleware = createLogger({
|
||||
collapsed: true,
|
||||
});
|
||||
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
|
||||
}
|
||||
|
||||
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
|
||||
}
|
||||
|
||||
export default function configureStore(initialState = {}) {
|
||||
const store = createStore(
|
||||
createRootReducer(),
|
||||
initialState,
|
||||
composeMiddleware(),
|
||||
);
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
return store;
|
||||
}
|
||||
16
src/data/reducers.js
Executable file
16
src/data/reducers.js
Executable file
@@ -0,0 +1,16 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
reducer as accountSettingsReducer,
|
||||
storeName as accountSettingsStoreName,
|
||||
} from '../account-settings';
|
||||
|
||||
import {
|
||||
reducer as registrationReducer,
|
||||
} from '../registration';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[accountSettingsStoreName]: accountSettingsReducer,
|
||||
registration: registrationReducer,
|
||||
});
|
||||
export default createRootReducer;
|
||||
10
src/data/sagas.js
Normal file
10
src/data/sagas.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { saga as accountSettingsSaga } from '../account-settings';
|
||||
import { saga as registrationSaga } from '../registration';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
accountSettingsSaga(),
|
||||
registrationSaga(),
|
||||
]);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
export const configuration = {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
USER_INFO_COOKIE_NAME: process.env.USER_INFO_COOKIE_NAME,
|
||||
CSRF_COOKIE_NAME: process.env.CSRF_COOKIE_NAME,
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: process.env.LANGUAGE_PREFERENCE_COOKIE_NAME,
|
||||
SITE_NAME: process.env.SITE_NAME,
|
||||
MARKETING_SITE_BASE_URL: process.env.MARKETING_SITE_BASE_URL,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
CONTACT_URL: process.env.CONTACT_URL,
|
||||
OPEN_SOURCE_URL: process.env.OPEN_SOURCE_URL,
|
||||
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL,
|
||||
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL,
|
||||
FACEBOOK_URL: process.env.FACEBOOK_URL,
|
||||
TWITTER_URL: process.env.TWITTER_URL,
|
||||
YOU_TUBE_URL: process.env.YOU_TUBE_URL,
|
||||
LINKED_IN_URL: process.env.LINKED_IN_URL,
|
||||
REDDIT_URL: process.env.REDDIT_URL,
|
||||
APPLE_APP_STORE_URL: process.env.APPLE_APP_STORE_URL,
|
||||
GOOGLE_PLAY_URL: process.env.GOOGLE_PLAY_URL,
|
||||
ACCOUNT_SETTINGS_URL: `${process.env.LMS_BASE_URL}/account/settings`,
|
||||
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
ENVIRONMENT: process.env.NODE_ENV,
|
||||
ACCOUNTS_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/user/v1/accounts`,
|
||||
PREFERENCES_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/user/v1/preferences`,
|
||||
CERTIFICATES_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/certificates/v0/certificates`,
|
||||
VIEW_MY_RECORDS_URL: `${process.env.CREDENTIALS_BASE_URL}/records`,
|
||||
ECOMMERCE_API_BASE_URL: `${process.env.ECOMMERCE_BASE_URL}/api/v2`,
|
||||
ORDER_HISTORY_URL: process.env.ORDER_HISTORY_URL,
|
||||
DELETE_ACCOUNT_URL: `${process.env.LMS_BASE_URL}/api/user/v1/accounts/deactivate_logout/`,
|
||||
PASSWORD_RESET_URL: `${process.env.LMS_BASE_URL}/password_reset/`,
|
||||
ENTERPRISE_MARKETING_URL: process.env.ENTERPRISE_MARKETING_URL,
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE: process.env.ENTERPRISE_MARKETING_UTM_SOURCE,
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: process.env.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: process.env.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
|
||||
};
|
||||
|
||||
export const features = {};
|
||||
@@ -100,6 +100,7 @@
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"error.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.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
@@ -108,19 +109,5 @@
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"unexpected.error.message.text": "An unexpected error occurred. Please click the button below to return to refresh the page.",
|
||||
"unexpected.error.button.text": "Try Again",
|
||||
"siteheader.links.courses": "Courses",
|
||||
"siteheader.links.programs": "Programs",
|
||||
"siteheader.links.content.search": "Discover New",
|
||||
"siteheader.user.menu.dashboard": "Dashboard",
|
||||
"siteheader.user.menu.profile": "Profile",
|
||||
"siteheader.user.menu.account.settings": "Account",
|
||||
"siteheader.user.menu.order.history": "Order History",
|
||||
"siteheader.user.menu.logout": "Logout",
|
||||
"siteheader.user.menu.login": "Login",
|
||||
"siteheader.user.menu.register": "Sign Up",
|
||||
"app.loading.message": "Loading...",
|
||||
"error.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."
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "La cuenta de {provider} seleccionada ya está vinculada con otra cuenta de edX. ",
|
||||
"account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
|
||||
"account.settings.message.managed.settings.support": "support",
|
||||
"account.settings.message.managed.settings": "Los ajustes en el perfil son administrados por {managerTitle}. Contacte su administrador o {support} para obtener ayuda.",
|
||||
"account.settings.message.managed.settings.support": "soporte",
|
||||
"account.settings.page.heading": "Configuración de cuenta",
|
||||
"account.settings.loading.message": "Cargando...",
|
||||
"account.settings.loading.error": "Error: {error}",
|
||||
"account.settings.banner.beta.language": "Ha establecido su idioma en {beta_language}, el cual no está traducido completamente. Puede ayudarnos a traducir este idioma totalmente uniéndose la comunidad Transifex y adicionando traducciones desde el Inglés para los estudiantes que hablan {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
|
||||
"account.settings.banner.beta.language.action.switch.back": "Regresar a {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Ayude a traducir a {beta_language}",
|
||||
"account.settings.section.account.information": "Información de la cuenta",
|
||||
"account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.profile.information": "Información del perfil",
|
||||
"account.settings.section.site.preferences": "Preferencias del sitio",
|
||||
"account.settings.section.linked.accounts": "Cuentas vinculadas",
|
||||
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
|
||||
"account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en edX.",
|
||||
"account.settings.field.username": "Nombre de usuario",
|
||||
"account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
|
||||
"account.settings.field.username.help.text": "El nombre que lo identifica en edX. No podrá cambiar el nombre de usuario.",
|
||||
"account.settings.field.full.name": "Nombre completo",
|
||||
"account.settings.field.full.name.empty": "Añade nombre",
|
||||
"account.settings.field.full.name.help.text": "El nombre que es usado para la verificación de identidad y aparece en sus certificados.",
|
||||
"account.settings.field.email": "Email address (Sign in)",
|
||||
"account.settings.field.email.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your email address.",
|
||||
"account.settings.field.email.help.text": "You receive messages from edX and course teams at this address.",
|
||||
"account.settings.field.secondary.email": "Recovery email address",
|
||||
"account.settings.field.secondary.email.empty": "Add a recovery email address",
|
||||
"account.settings.field.secondary.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
|
||||
"account.settings.email.field.confirmation.header": "Pending confirmation",
|
||||
"account.settings.field.email": "Correo electrónico (Ingresar)",
|
||||
"account.settings.field.email.empty": "Agregar correo electrónico",
|
||||
"account.settings.field.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
|
||||
"account.settings.field.email.help.text": "Recibes mensajes de edX y equipos del curso en esta dirección.",
|
||||
"account.settings.field.secondary.email": "Correo electrónico de recuperación",
|
||||
"account.settings.field.secondary.email.empty": "Agregar un correo electrónico de recuperación",
|
||||
"account.settings.field.secondary.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
|
||||
"account.settings.email.field.confirmation.header": "Confirmación pendiente",
|
||||
"account.settings.field.dob": "Año de nacimiento",
|
||||
"account.settings.field.dob.empty": "Add year of birth",
|
||||
"account.settings.field.year_of_birth.options.empty": "Select a year of birth",
|
||||
"account.settings.field.dob.empty": "Agregar año de nacimiento",
|
||||
"account.settings.field.year_of_birth.options.empty": "Selecciona año de nacimiento",
|
||||
"account.settings.field.country": "País",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"account.settings.field.country.empty": "Agregar país",
|
||||
"account.settings.field.country.options.empty": "Seleccionar un país",
|
||||
"account.settings.field.site.language": "Idioma del sitio",
|
||||
"account.settings.field.site.language.help.text": "El idioma que se usará para el sitio. Actualmente solo hay disponibilidad de usar un número limitado de idiomas.",
|
||||
"account.settings.field.education": "Educación",
|
||||
"account.settings.field.education.empty": "Add level of education",
|
||||
"account.settings.field.education.levels.empty": "Select a level of education",
|
||||
"account.settings.field.education.empty": "Agregar un nivel educativo",
|
||||
"account.settings.field.education.levels.empty": "Seleccionar un nivel educativo",
|
||||
"account.settings.field.education.levels.p": "Doctorado",
|
||||
"account.settings.field.education.levels.m": "Master o magíster",
|
||||
"account.settings.field.education.levels.b": "Bachelor's Degree",
|
||||
"account.settings.field.education.levels.b": "Pregrado o Licenciatura",
|
||||
"account.settings.field.education.levels.a": "Grado técnico - tecnológico",
|
||||
"account.settings.field.education.levels.hs": "Enseñanza secundaria",
|
||||
"account.settings.field.education.levels.jhs": "Formación media",
|
||||
@@ -48,79 +48,66 @@
|
||||
"account.settings.field.education.levels.none": "Ninguna educación formal",
|
||||
"account.settings.field.education.levels.o": "Otra educación",
|
||||
"account.settings.field.gender": "Género",
|
||||
"account.settings.field.gender.empty": "Add gender",
|
||||
"account.settings.field.gender.options.empty": "Select a gender",
|
||||
"account.settings.field.gender.empty": "Agregar género",
|
||||
"account.settings.field.gender.options.empty": "Seleccionar el género",
|
||||
"account.settings.field.gender.options.f": "Femenino",
|
||||
"account.settings.field.gender.options.m": "Masculino",
|
||||
"account.settings.field.gender.options.o": "Otro",
|
||||
"account.settings.field.language.proficiencies": "Spoken languages",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
"account.settings.field.time.zone.empty": "Set time zone",
|
||||
"account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browser’s local time zone.",
|
||||
"account.settings.field.language.proficiencies": "Idiomas hablados",
|
||||
"account.settings.field.language.proficiencies.empty": "Agregar un idioma hablado",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Selecciona lenguaje",
|
||||
"account.settings.field.time.zone": "Zona horaria",
|
||||
"account.settings.field.time.zone.empty": "Indicar zona horaria",
|
||||
"account.settings.field.time.zone.description": "Selecciona la zona horaria para exponer las fechas del curso. Si no especificas una zona horaria, las fechas del curso, incluidas las fechas límites de tareas, serán expuestas en la zona horaria local de tu navegador.",
|
||||
"account.settings.field.time.zone.default": "Por defecto (Zona horaria local)",
|
||||
"account.settings.field.time.zone.all": "All time zones",
|
||||
"account.settings.field.time.zone.country": "Country time zones",
|
||||
"account.settings.field.time.zone.all": "Todas las zonas horarias",
|
||||
"account.settings.field.time.zone.country": "Zonas horarias",
|
||||
"account.settings.section.social.media": "Enlaces de redes sociales",
|
||||
"account.settings.section.social.media.description": "Opcionalmente, conecte sus cuentas personales a los iconos de redes sociales en su perfil de edX.",
|
||||
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Agregar perfil de LinkedIn",
|
||||
"account.settings.jump.nav.delete.account": "Eliminar mi cuenta",
|
||||
"account.settings.field.social.platform.name.twitter": "Twitter",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "Agregar perfil de Twitter",
|
||||
"account.settings.field.social.platform.name.facebook": "Facebook",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "Agregar perfil de Facebook",
|
||||
"account.settings.editable.field.action.save": "Guardar",
|
||||
"account.settings.editable.field.action.cancel": "Cancelar",
|
||||
"account.settings.editable.field.action.edit": "Editar",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.",
|
||||
"account.settings.delete.account.header": "Eliminar mi cuenta",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.text.2": "Una vez tu cuenta haya sido eliminada, no la podrás usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o de tu universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Online y Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "follow the instructions for printing or downloading a certificate",
|
||||
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.",
|
||||
"account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
|
||||
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
|
||||
"account.settings.delete.account.text.1": "Cuidado: la eliminación de tu cuenta y datos personales es permanente e irreversible. edX no podrá recuperar ni tu cuenta ni los datos eliminados.",
|
||||
"account.settings.delete.account.text.2": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "siga las instrucciones para imprimir o descargar el certificado",
|
||||
"account.settings.delete.account.text.warning": "Warning: La eliminación de la cuenta es permanente. Por favor lee la información de más arriba con atención antes de proceder. Esta es una acción irreversible, y no podrás volver a usar el mismo correo electrónico en edX.",
|
||||
"account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?",
|
||||
"account.settings.delete.account.button": "Eliminar mi cuenta",
|
||||
"account.settings.delete.account.please.activate": "activate your account",
|
||||
"account.settings.delete.account.please.activate": "activar su cuenta",
|
||||
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
|
||||
"account.settings.delete.account.modal.header": "¿Está seguro?",
|
||||
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.text.1": "Has seleccionado “Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
|
||||
"account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "Si deseas continuar y eliminar tu cuenta, por favor introduce la contraseña de tu cuenta:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "Si, Eliminar",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
|
||||
"account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.no.password": "Se requiere una contraseña",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Ocurrió un error al procesar tu solicitud. Por favor, intente nuevamente más tarde.",
|
||||
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
|
||||
"account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.",
|
||||
"account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.",
|
||||
"account.settings.delete.account.modal.after.button": "Cerrar",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.delete.account.text.3": "Puede que también pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminación, {actionLink}.",
|
||||
"error.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.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "soporte técnico",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "Hemos mandado un mensaje a {email}. Haz clic en el enlace en el mensaje para restablecer tu contraseña. ¿No recibiste el mensaje? Contáctate con {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Restablecer contraseña",
|
||||
"account.settings.editable.field.password.reset.label": "Contraseña",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.link.account": "Iniciar sesión con {name}",
|
||||
"account.settings.sso.account.connected": "Vinculado",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"unexpected.error.message.text": "An unexpected error occurred. Please click the button below to return to refresh the page.",
|
||||
"unexpected.error.button.text": "Try Again",
|
||||
"siteheader.links.courses": "Cursos",
|
||||
"siteheader.links.programs": "Programs",
|
||||
"siteheader.links.content.search": "Discover New",
|
||||
"siteheader.user.menu.dashboard": "Panel de Control",
|
||||
"siteheader.user.menu.profile": "Perfil",
|
||||
"siteheader.user.menu.account.settings": "Cuenta",
|
||||
"siteheader.user.menu.order.history": "Historial de órdenes",
|
||||
"siteheader.user.menu.logout": "Salir",
|
||||
"siteheader.user.menu.login": "Iniciar Sesión",
|
||||
"siteheader.user.menu.register": "Registrarse",
|
||||
"app.loading.message": "Cargando...",
|
||||
"error.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."
|
||||
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -100,6 +100,7 @@
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"error.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.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
@@ -108,19 +109,5 @@
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"unexpected.error.message.text": "An unexpected error occurred. Please click the button below to return to refresh the page.",
|
||||
"unexpected.error.button.text": "Try Again",
|
||||
"siteheader.links.courses": "Courses",
|
||||
"siteheader.links.programs": "Programs",
|
||||
"siteheader.links.content.search": "Discover New",
|
||||
"siteheader.user.menu.dashboard": "Dashboard",
|
||||
"siteheader.user.menu.profile": "Profile",
|
||||
"siteheader.user.menu.account.settings": "Account",
|
||||
"siteheader.user.menu.order.history": "Order History",
|
||||
"siteheader.user.menu.logout": "Logout",
|
||||
"siteheader.user.menu.login": "Login",
|
||||
"siteheader.user.menu.register": "Sign Up",
|
||||
"app.loading.message": "Loading...",
|
||||
"error.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."
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -100,6 +100,7 @@
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"error.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.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
@@ -108,19 +109,5 @@
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"unexpected.error.message.text": "An unexpected error occurred. Please click the button below to return to refresh the page.",
|
||||
"unexpected.error.button.text": "Try Again",
|
||||
"siteheader.links.courses": "Courses",
|
||||
"siteheader.links.programs": "Programs",
|
||||
"siteheader.links.content.search": "Discover New",
|
||||
"siteheader.user.menu.dashboard": "Dashboard",
|
||||
"siteheader.user.menu.profile": "Profile",
|
||||
"siteheader.user.menu.account.settings": "Account",
|
||||
"siteheader.user.menu.order.history": "Order History",
|
||||
"siteheader.user.menu.logout": "Logout",
|
||||
"siteheader.user.menu.login": "Login",
|
||||
"siteheader.user.menu.register": "Sign Up",
|
||||
"app.loading.message": "Loading...",
|
||||
"error.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."
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
167
src/index.jsx
167
src/index.jsx
@@ -1,99 +1,92 @@
|
||||
import 'babel-polyfill';
|
||||
import 'url-polyfill';
|
||||
import 'formdata-polyfill';
|
||||
import { AppProvider, ErrorPage, AuthenticatedPageRoute } from '@edx/frontend-platform/react';
|
||||
import { subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig, getConfig } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
configureAnalytics,
|
||||
identifyAnonymousUser,
|
||||
identifyAuthenticatedUser,
|
||||
initializeSegment,
|
||||
sendPageEvent,
|
||||
sendTrackingLogEvent,
|
||||
} from '@edx/frontend-analytics';
|
||||
import { configureLoggingService, NewRelicLoggingService } from '@edx/frontend-logging';
|
||||
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
|
||||
import { configure as configureI18n } from '@edx/frontend-i18n';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { configuration } from './environment';
|
||||
import configureStore from './store';
|
||||
import { configureUserAccountApiService } from './common';
|
||||
import { configureService as configureAccountSettingsApiService } from './account-settings';
|
||||
import messages from './i18n';
|
||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import AccountSettingsPage, { NotFoundPage } from './account-settings';
|
||||
import LoginPage from './registration/LoginPage';
|
||||
import RegistrationPage from './registration/RegistrationPage';
|
||||
import appMessages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import App from './components/App';
|
||||
import './assets/favicon.ico';
|
||||
import logo from './assets/headerlogo.svg';
|
||||
|
||||
const apiClient = getAuthenticatedAPIClient({
|
||||
appBaseUrl: configuration.BASE_URL,
|
||||
authBaseUrl: configuration.LMS_BASE_URL,
|
||||
loginUrl: configuration.LOGIN_URL,
|
||||
logoutUrl: configuration.LOGOUT_URL,
|
||||
csrfTokenApiPath: configuration.CSRF_TOKEN_API_PATH,
|
||||
refreshAccessTokenEndpoint: configuration.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
accessTokenCookieName: configuration.ACCESS_TOKEN_COOKIE_NAME,
|
||||
userInfoCookieName: configuration.USER_INFO_COOKIE_NAME,
|
||||
csrfCookieName: configuration.CSRF_COOKIE_NAME,
|
||||
loggingService: NewRelicLoggingService,
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Switch>
|
||||
<AuthenticatedPageRoute exact path="/">
|
||||
<Header />
|
||||
<main>
|
||||
<AccountSettingsPage />
|
||||
</main>
|
||||
</AuthenticatedPageRoute>
|
||||
<Route path="/notfound">
|
||||
<Header />
|
||||
<main>
|
||||
<NotFoundPage />
|
||||
</main>
|
||||
</Route>
|
||||
{
|
||||
getConfig().ENABLE_LOGIN_AND_REGISTRATION &&
|
||||
<>
|
||||
<Route path="/login" >
|
||||
<div className="registration-header">
|
||||
<img src={logo} alt="edX" className="logo" />
|
||||
</div>
|
||||
<main>
|
||||
<LoginPage />
|
||||
</main>
|
||||
</Route>
|
||||
<Route path="/registration">
|
||||
<div className="registration-header">
|
||||
<img src={logo} alt="edX" className="logo" />
|
||||
</div>
|
||||
<main>
|
||||
<RegistrationPage />
|
||||
</main>
|
||||
</Route>
|
||||
</>
|
||||
}
|
||||
<Route path="*">
|
||||
<Header />
|
||||
<main>
|
||||
<NotFoundPage />
|
||||
</main>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Footer />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* We need to merge the application configuration with the authentication state
|
||||
* so that we can hand it all to the redux store's initializer.
|
||||
*/
|
||||
function createInitialState() {
|
||||
const errors = {};
|
||||
const url = new URL(window.location.href);
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
});
|
||||
|
||||
// Extract duplicate third-party auth provider message from query string
|
||||
errors.duplicateTpaProvider = url.searchParams.get('duplicate_provider');
|
||||
if (errors.duplicateTpaProvider) {
|
||||
// Remove the duplicate_provider query param to avoid bookmarking.
|
||||
window.history.replaceState(null, '', `${url.protocol}//${url.host}${url.pathname}`);
|
||||
}
|
||||
|
||||
return Object.assign({}, { configuration }, apiClient.getAuthenticationState(), { errors });
|
||||
}
|
||||
|
||||
function configure() {
|
||||
configureI18n(configuration, messages);
|
||||
|
||||
const { store, history } = configureStore(createInitialState(), configuration.ENVIRONMENT);
|
||||
|
||||
configureLoggingService(NewRelicLoggingService);
|
||||
configureAccountSettingsApiService(configuration, apiClient);
|
||||
configureUserAccountApiService(configuration, apiClient);
|
||||
initializeSegment(configuration.SEGMENT_KEY);
|
||||
configureAnalytics({
|
||||
loggingService: NewRelicLoggingService,
|
||||
authApiClient: apiClient,
|
||||
analyticsApiBaseUrl: configuration.LMS_BASE_URL,
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
history,
|
||||
};
|
||||
}
|
||||
|
||||
apiClient.ensurePublicOrAuthenticationAndCookies(
|
||||
window.location.pathname,
|
||||
(accessToken) => {
|
||||
const { store, history } = configure();
|
||||
|
||||
ReactDOM.render(<App store={store} history={history} />, document.getElementById('root'));
|
||||
|
||||
if (accessToken) {
|
||||
identifyAuthenticatedUser(accessToken.userId);
|
||||
} else {
|
||||
identifyAnonymousUser();
|
||||
}
|
||||
sendPageEvent();
|
||||
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
page: 'account',
|
||||
visibility: null,
|
||||
user_id: accessToken ? accessToken.userId : null,
|
||||
});
|
||||
initialize({
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: false,
|
||||
hydrateAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
ENABLE_LOGIN_AND_REGISTRATION: process.env.ENABLE_LOGIN_AND_REGISTRATION,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
@import "~@edx/edx-bootstrap/scss/edx/theme.scss";
|
||||
@import '~@edx/paragon/scss/edx/theme.scss';
|
||||
@import '~@edx/paragon/scss/edx/fonts.scss'; // Roboto
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
@import "~@edx/paragon/src/index.scss";
|
||||
@import "~@edx/frontend-component-site-header/src/index";
|
||||
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import "./account-settings/style";
|
||||
@import "./registration/style";
|
||||
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { userAccount } from '@edx/frontend-auth';
|
||||
import { connectRouter } from 'connected-react-router';
|
||||
|
||||
import { reducer as i18nReducer } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
|
||||
import {
|
||||
reducer as accountSettingsReducer,
|
||||
storeName as accountSettingsStoreName,
|
||||
} from './account-settings';
|
||||
|
||||
const identityReducer = (state) => {
|
||||
const newState = { ...state };
|
||||
return newState;
|
||||
};
|
||||
|
||||
const createRootReducer = history =>
|
||||
combineReducers({
|
||||
// The authentication state is added as initialState when
|
||||
// creating the store in data/store.js.
|
||||
authentication: identityReducer,
|
||||
configuration: identityReducer,
|
||||
errors: identityReducer,
|
||||
i18n: i18nReducer,
|
||||
userAccount,
|
||||
[accountSettingsStoreName]: accountSettingsReducer,
|
||||
router: connectRouter(history),
|
||||
});
|
||||
|
||||
export default createRootReducer;
|
||||
115
src/registration/LoginPage.jsx
Normal file
115
src/registration/LoginPage.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFacebookF, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
export default class LoginPage extends React.Component {
|
||||
state = {
|
||||
password: '',
|
||||
email: '',
|
||||
errors: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
emailValid: false,
|
||||
passwordValid: false,
|
||||
formValid: false,
|
||||
}
|
||||
|
||||
handleOnChange(e) {
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
this.validateInput(e.target.name, e.target.value);
|
||||
}
|
||||
|
||||
validateInput(inputName, value) {
|
||||
let inputErrors = this.state.errors;
|
||||
let emailValid = this.state.emailValid;
|
||||
let passwordValid = this.state.passwordValid;
|
||||
|
||||
switch (inputName) {
|
||||
case 'email':
|
||||
emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i);
|
||||
inputErrors.email = emailValid ? '' : null;
|
||||
break;
|
||||
case 'password':
|
||||
passwordValid = value.length >= 8 && value.match(/\d+/g);
|
||||
inputErrors.password = passwordValid ? '' : null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errors: inputErrors,
|
||||
emailValid,
|
||||
passwordValid,
|
||||
}, this.validateForm);
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.setState({
|
||||
formValid: this.state.emailValid && this.state.passwordValid,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="d-flex justify-content-center registration-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="d-flex flex-row">
|
||||
<p>We <span>❤️</span>our learners.</p>
|
||||
<p> First time here?</p>
|
||||
<a className="ml-2" href="/registration">Join our community!</a>
|
||||
</div>
|
||||
<form className="m-0">
|
||||
<div className="form-group">
|
||||
<h3 className="text-center mt-3">Sign In</h3>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<ValidationFormGroup
|
||||
for="email"
|
||||
invalid={this.state.errors.email !== ''}
|
||||
invalidMessage="The email address you've provided isn't formatted correctly."
|
||||
>
|
||||
<label htmlFor="loginEmail" className="h6 mr-1">Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
id="loginEmail"
|
||||
type="email"
|
||||
placeholder="email@domain.com"
|
||||
value={this.state.email}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
style={{ width: '400px' }}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
<p className="mb-4">The email address you used to register with edX.</p>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<label htmlFor="loginPassword" className="h6 mr-1">Password</label>
|
||||
<Input
|
||||
name="password"
|
||||
id="loginPassword"
|
||||
type="password"
|
||||
value={this.state.password}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="btn-primary submit">Sign in</Button>
|
||||
</form>
|
||||
<div className="section-heading-line mb-4">
|
||||
<h4>or sign in with</h4>
|
||||
</div>
|
||||
<div className="row text-center d-block mb-4">
|
||||
<button className="btn-social facebook"><FontAwesomeIcon className="mr-2" icon={faFacebookF} />Facebook</button>
|
||||
<button className="btn-social google"><FontAwesomeIcon className="mr-2" icon={faGoogle} />Google</button>
|
||||
<button className="btn-social microsoft"><FontAwesomeIcon className="mr-2" icon={faMicrosoft} />Microsoft</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
256
src/registration/RegistrationPage.jsx
Normal file
256
src/registration/RegistrationPage.jsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Input, ValidationFormGroup, StatusAlert } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFacebookF, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faGraduationCap } from '@fortawesome/free-solid-svg-icons';
|
||||
import countryList from './countryList';
|
||||
|
||||
import { registerNewUser } from './data/actions';
|
||||
|
||||
export class RegistrationPage extends React.Component {
|
||||
state = {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailValid: false,
|
||||
nameValid: false,
|
||||
usernameValid: false,
|
||||
passwordValid: false,
|
||||
countryValid: false,
|
||||
formValid: false,
|
||||
open: false,
|
||||
}
|
||||
|
||||
handleSelectCountry = (e) => {
|
||||
this.setState({
|
||||
country: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
console.log('clicked submit', e);
|
||||
e.preventDefault();
|
||||
this.setState({ open: true });
|
||||
|
||||
const payload = {
|
||||
email: this.state.email,
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
name: this.state.name,
|
||||
honor_code: true,
|
||||
country: this.state.country,
|
||||
};
|
||||
|
||||
this.props.registerNewUser(payload);
|
||||
}
|
||||
|
||||
resetStatusAlertWrapperState() {
|
||||
this.setState({ open: false });
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
handleOnChange(e) {
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
this.validateInput(e.target.name, e.target.value);
|
||||
}
|
||||
|
||||
validateInput(inputName, value) {
|
||||
let inputErrors = this.state.errors;
|
||||
let emailValid = this.state.emailValid;
|
||||
let nameValid = this.state.nameValid;
|
||||
let usernameValid = this.state.usernameValid;
|
||||
let passwordValid = this.state.passwordValid;
|
||||
let countryValid = this.state.countryValid;
|
||||
|
||||
switch (inputName) {
|
||||
case 'email':
|
||||
emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i);
|
||||
inputErrors.email = emailValid ? '' : null;
|
||||
break;
|
||||
case 'name':
|
||||
nameValid = value.length >= 1;
|
||||
inputErrors.name = nameValid ? '' : null;
|
||||
break;
|
||||
case 'username':
|
||||
usernameValid = value.length >= 2 && value.length <= 30;
|
||||
inputErrors.username = usernameValid ? '' : null;
|
||||
break;
|
||||
case 'password':
|
||||
passwordValid = value.length >= 8 && value.match(/\d+/g);
|
||||
inputErrors.password = passwordValid ? '' : null;
|
||||
break;
|
||||
case 'country':
|
||||
countryValid = value !== 'Country or Region of Residence (required)';
|
||||
inputErrors.country = countryValid ? '' : null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errors: inputErrors,
|
||||
emailValid,
|
||||
nameValid,
|
||||
usernameValid,
|
||||
passwordValid,
|
||||
countryValid,
|
||||
}, this.validateForm);
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.setState({
|
||||
formValid: this.state.emailValid && this.state.nameValid &&
|
||||
this.state.usernameValid && this.state.passwordValid && this.state.countryValid,
|
||||
});
|
||||
}
|
||||
|
||||
renderCountryList() {
|
||||
const items = [{ value: 'Country or Region of Residence (required)', label: 'Country or Region of Residence (required)' }];
|
||||
const countries = Object.values(countryList);
|
||||
for (let i = 0; i < countries.length; i += 1) {
|
||||
items.push({ value: countries[i], label: countries[i] });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="registration-container d-flex flex-column align-items-center mx-auto" style={{ width: '30rem' }}>
|
||||
<div className="mb-4">
|
||||
<FontAwesomeIcon className="d-block mx-auto fa-2x" icon={faGraduationCap} />
|
||||
<h4 className="d-block mx-auto">Start learning now!</h4>
|
||||
</div>
|
||||
<div className="d-block mb-4">
|
||||
<span className="d-block mx-auto mb-4 section-heading-line">Create an account using</span>
|
||||
<button className="btn-social facebook"><FontAwesomeIcon className="mr-2" icon={faFacebookF} />Facebook</button>
|
||||
<button className="btn-social google"><FontAwesomeIcon className="mr-2" icon={faGoogle} />Google</button>
|
||||
<button className="btn-social microsoft"><FontAwesomeIcon className="mr-2" icon={faMicrosoft} />Microsoft</button>
|
||||
<span className="d-block mx-auto text-center mt-4 section-heading-line">or create a new one here</span>
|
||||
</div>
|
||||
|
||||
<form className="mb-4 mx-auto form-group">
|
||||
<ValidationFormGroup
|
||||
for="email"
|
||||
invalid={this.state.errors.email !== ''}
|
||||
invalidMessage="Enter a valid email address that contains at least 3 characters."
|
||||
>
|
||||
<label htmlFor="registrationEmail" className="h6 pt-3">Email (required)</label>
|
||||
<Input
|
||||
name="email"
|
||||
id="registrationEmail"
|
||||
type="email"
|
||||
placeholder="email@domain.com"
|
||||
value={this.state.email}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="name"
|
||||
invalid={this.state.errors.name !== ''}
|
||||
invalidMessage="Enter your full name."
|
||||
>
|
||||
<label htmlFor="registrationName" className="h6 pt-3">Full Name (required)</label>
|
||||
<Input
|
||||
name="name"
|
||||
id="registrationName"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={this.state.name}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="username"
|
||||
invalid={this.state.errors.username !== ''}
|
||||
invalidMessage="Username must be between 2 and 30 characters long."
|
||||
>
|
||||
<label htmlFor="registrationUsername" className="h6 pt-3">Public Username (required)</label>
|
||||
<Input
|
||||
name="username"
|
||||
id="registrationUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.username}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="password"
|
||||
invalid={this.state.errors.password !== ''}
|
||||
invalidMessage="This password is too short. It must contain at least 8 characters. This password must contain at least 1 number."
|
||||
>
|
||||
<label htmlFor="registrationPassword" className="h6 pt-3">Password (required)</label>
|
||||
<Input
|
||||
name="password"
|
||||
id="registrationPassword"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={this.state.password}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="country"
|
||||
invalid={this.state.errors.country !== ''}
|
||||
invalidMessage="Select your country or region of residence."
|
||||
>
|
||||
<label htmlFor="registrationCountry" className="h6 pt-3">Country (required)</label>
|
||||
<Input
|
||||
type="select"
|
||||
placeholder="Country or Region of Residence"
|
||||
value={this.state.country}
|
||||
options={this.renderCountryList()}
|
||||
onChange={this.handleSelectCountry}
|
||||
required
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<span>By creating an account, you agree to the <a href="https://www.edx.org/edx-terms-service">Terms of Service and Honor Code</a> and you acknowledge that edX and each Member process your personal data in accordance with the <a href="https://www.edx.org/edx-privacy-policy">Privacy Policy</a>.</span>
|
||||
<Button
|
||||
className="btn-primary mt-4 submit"
|
||||
onClick={this.handleSubmit}
|
||||
inputRef={(input) => {
|
||||
this.button = input;
|
||||
}}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
open={this.state.open}
|
||||
dialog="❤️❤️❤️ Your account was has been created! Welcome to our learning community! ❤️❤️❤️"
|
||||
onClose={this.resetStatusAlertWrapperState}
|
||||
/>
|
||||
</form>
|
||||
<div className="text-center mb-2 pt-2">
|
||||
<span>Already have an edX account?</span>
|
||||
<a href="/login"> Sign in.</a>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
() => ({}),
|
||||
{
|
||||
registerNewUser,
|
||||
},
|
||||
)(RegistrationPage);
|
||||
72
src/registration/_style.scss
Normal file
72
src/registration/_style.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.registration-container {
|
||||
margin: 4rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.registration-header {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
height: 3.75rem;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.registration-header img {
|
||||
height: 1.75rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
padding: 0.5rem 1rem;
|
||||
color: white;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.facebook {
|
||||
border-color: #4267b2;
|
||||
background-color: #4267b2;
|
||||
}
|
||||
|
||||
.google {
|
||||
border-color: #4285f4;
|
||||
background-color: #4285f4;
|
||||
}
|
||||
|
||||
.microsoft {
|
||||
border-color: #2f2f2f;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: inherit;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-heading-line{
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
253
src/registration/countryList.js
Normal file
253
src/registration/countryList.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const countryList = {
|
||||
AF: 'Afghanistan',
|
||||
AL: 'Albania',
|
||||
DZ: 'Algeria',
|
||||
AS: 'American Samoa',
|
||||
AD: 'Andorra',
|
||||
AO: 'Angola',
|
||||
AI: 'Anguilla',
|
||||
AQ: 'Antarctica',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AR: 'Argentina',
|
||||
AM: 'Armenia',
|
||||
AW: 'Aruba',
|
||||
AU: 'Australia',
|
||||
AT: 'Austria',
|
||||
AZ: 'Azerbaijan',
|
||||
BS: 'Bahamas (the)',
|
||||
BH: 'Bahrain',
|
||||
BD: 'Bangladesh',
|
||||
BB: 'Barbados',
|
||||
BY: 'Belarus',
|
||||
BE: 'Belgium',
|
||||
BZ: 'Belize',
|
||||
BJ: 'Benin',
|
||||
BM: 'Bermuda',
|
||||
BT: 'Bhutan',
|
||||
BO: 'Bolivia (Plurinational State of)',
|
||||
BQ: 'Bonaire, Sint Eustatius and Saba',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BW: 'Botswana',
|
||||
BV: 'Bouvet Island',
|
||||
BR: 'Brazil',
|
||||
IO: 'British Indian Ocean Territory (the)',
|
||||
BN: 'Brunei Darussalam',
|
||||
BG: 'Bulgaria',
|
||||
BF: 'Burkina Faso',
|
||||
BI: 'Burundi',
|
||||
CV: 'Cabo Verde',
|
||||
KH: 'Cambodia',
|
||||
CM: 'Cameroon',
|
||||
CA: 'Canada',
|
||||
KY: 'Cayman Islands (the)',
|
||||
CF: 'Central African Republic (the)',
|
||||
TD: 'Chad',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CX: 'Christmas Island',
|
||||
CC: 'Cocos (Keeling) Islands (the)',
|
||||
CO: 'Colombia',
|
||||
KM: 'Comoros (the)',
|
||||
CD: 'Congo (the Democratic Republic of the)',
|
||||
CG: 'Congo (the)',
|
||||
CK: 'Cook Islands (the)',
|
||||
CR: 'Costa Rica',
|
||||
HR: 'Croatia',
|
||||
CU: 'Cuba',
|
||||
CW: 'Curaçao',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czechia',
|
||||
CI: 'Côte d\'Ivoire',
|
||||
DK: 'Denmark',
|
||||
DJ: 'Djibouti',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic (the)',
|
||||
EC: 'Ecuador',
|
||||
EG: 'Egypt',
|
||||
SV: 'El Salvador',
|
||||
GQ: 'Equatorial Guinea',
|
||||
ER: 'Eritrea',
|
||||
EE: 'Estonia',
|
||||
SZ: 'Eswatini',
|
||||
ET: 'Ethiopia',
|
||||
FK: 'Falkland Islands (the) [Malvinas]',
|
||||
FO: 'Faroe Islands (the)',
|
||||
FJ: 'Fiji',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GF: 'French Guiana',
|
||||
PF: 'French Polynesia',
|
||||
TF: 'French Southern Territories (the)',
|
||||
GA: 'Gabon',
|
||||
GM: 'Gambia (the)',
|
||||
GE: 'Georgia',
|
||||
DE: 'Germany',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GR: 'Greece',
|
||||
GL: 'Greenland',
|
||||
GD: 'Grenada',
|
||||
GP: 'Guadeloupe',
|
||||
GU: 'Guam',
|
||||
GT: 'Guatemala',
|
||||
GG: 'Guernsey',
|
||||
GN: 'Guinea',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HT: 'Haiti',
|
||||
HM: 'Heard Island and McDonald Islands',
|
||||
VA: 'Holy See (the)',
|
||||
HN: 'Honduras',
|
||||
HK: 'Hong Kong',
|
||||
HU: 'Hungary',
|
||||
IS: 'Iceland',
|
||||
IN: 'India',
|
||||
ID: 'Indonesia',
|
||||
IR: 'Iran (Islamic Republic of)',
|
||||
IQ: 'Iraq',
|
||||
IE: 'Ireland',
|
||||
IM: 'Isle of Man',
|
||||
IL: 'Israel',
|
||||
IT: 'Italy',
|
||||
JM: 'Jamaica',
|
||||
JP: 'Japan',
|
||||
JE: 'Jersey',
|
||||
JO: 'Jordan',
|
||||
KZ: 'Kazakhstan',
|
||||
KE: 'Kenya',
|
||||
KI: 'Kiribati',
|
||||
KP: 'Korea (the Democratic People\'s Republic of)',
|
||||
KR: 'Korea (the Republic of)',
|
||||
KW: 'Kuwait',
|
||||
KG: 'Kyrgyzstan',
|
||||
LA: 'Lao People\'s Democratic Republic (the)',
|
||||
LV: 'Latvia',
|
||||
LB: 'Lebanon',
|
||||
LS: 'Lesotho',
|
||||
LR: 'Liberia',
|
||||
LY: 'Libya',
|
||||
LI: 'Liechtenstein',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MO: 'Macao',
|
||||
MG: 'Madagascar',
|
||||
MW: 'Malawi',
|
||||
MY: 'Malaysia',
|
||||
MV: 'Maldives',
|
||||
ML: 'Mali',
|
||||
MT: 'Malta',
|
||||
MH: 'Marshall Islands (the)',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MU: 'Mauritius',
|
||||
YT: 'Mayotte',
|
||||
MX: 'Mexico',
|
||||
FM: 'Micronesia (Federated States of)',
|
||||
MD: 'Moldova (the Republic of)',
|
||||
MC: 'Monaco',
|
||||
MN: 'Mongolia',
|
||||
ME: 'Montenegro',
|
||||
MS: 'Montserrat',
|
||||
MA: 'Morocco',
|
||||
MZ: 'Mozambique',
|
||||
MM: 'Myanmar',
|
||||
NA: 'Namibia',
|
||||
NR: 'Nauru',
|
||||
NP: 'Nepal',
|
||||
NL: 'Netherlands (the)',
|
||||
NC: 'New Caledonia',
|
||||
NZ: 'New Zealand',
|
||||
NI: 'Nicaragua',
|
||||
NE: 'Niger (the)',
|
||||
NG: 'Nigeria',
|
||||
NU: 'Niue',
|
||||
NF: 'Norfolk Island',
|
||||
MP: 'Northern Mariana Islands (the)',
|
||||
NO: 'Norway',
|
||||
OM: 'Oman',
|
||||
PK: 'Pakistan',
|
||||
PW: 'Palau',
|
||||
PS: 'Palestine, State of',
|
||||
PA: 'Panama',
|
||||
PG: 'Papua New Guinea',
|
||||
PY: 'Paraguay',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines (the)',
|
||||
PN: 'Pitcairn',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PR: 'Puerto Rico',
|
||||
QA: 'Qatar',
|
||||
MK: 'Republic of North Macedonia',
|
||||
RO: 'Romania',
|
||||
RU: 'Russian Federation (the)',
|
||||
RW: 'Rwanda',
|
||||
RE: 'Réunion',
|
||||
BL: 'Saint Barthélemy',
|
||||
SH: 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
LC: 'Saint Lucia',
|
||||
MF: 'Saint Martin (French part)',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
WS: 'Samoa',
|
||||
SM: 'San Marino',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SA: 'Saudi Arabia',
|
||||
SN: 'Senegal',
|
||||
RS: 'Serbia',
|
||||
SC: 'Seychelles',
|
||||
SL: 'Sierra Leone',
|
||||
SG: 'Singapore',
|
||||
SX: 'Sint Maarten (Dutch part)',
|
||||
SK: 'Slovakia',
|
||||
SI: 'Slovenia',
|
||||
SB: 'Solomon Islands',
|
||||
SO: 'Somalia',
|
||||
ZA: 'South Africa',
|
||||
GS: 'South Georgia and the South Sandwich Islands',
|
||||
SS: 'South Sudan',
|
||||
ES: 'Spain',
|
||||
LK: 'Sri Lanka',
|
||||
SD: 'Sudan (the)',
|
||||
SR: 'Suriname',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SE: 'Sweden',
|
||||
CH: 'Switzerland',
|
||||
SY: 'Syrian Arab Republic',
|
||||
TW: 'Taiwan (Province of China)',
|
||||
TJ: 'Tajikistan',
|
||||
TZ: 'Tanzania, United Republic of',
|
||||
TH: 'Thailand',
|
||||
TL: 'Timor-Leste',
|
||||
TG: 'Togo',
|
||||
TK: 'Tokelau',
|
||||
TO: 'Tonga',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Turkey',
|
||||
TM: 'Turkmenistan',
|
||||
TC: 'Turks and Caicos Islands (the)',
|
||||
TV: 'Tuvalu',
|
||||
UG: 'Uganda',
|
||||
UA: 'Ukraine',
|
||||
AE: 'United Arab Emirates (the)',
|
||||
GB: 'United Kingdom of Great Britain and Northern Ireland (the)',
|
||||
UM: 'United States Minor Outlying Islands (the)',
|
||||
US: 'United States of America (the)',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VU: 'Vanuatu',
|
||||
VE: 'Venezuela (Bolivarian Republic of)',
|
||||
VN: 'Viet Nam',
|
||||
VG: 'Virgin Islands (British)',
|
||||
VI: 'Virgin Islands (U.S.)',
|
||||
WF: 'Wallis and Futuna',
|
||||
EH: 'Western Sahara',
|
||||
YE: 'Yemen',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
AX: 'Åland Islands',
|
||||
};
|
||||
|
||||
export default countryList;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user