diff --git a/.eslintrc b/.eslintrc
index ba8734a..6dafdde 100755
--- a/.eslintrc
+++ b/.eslintrc
@@ -6,7 +6,7 @@
"error",
{
"devDependencies": [
- "config/*.js",
+ "webpack/*.js",
"**/*.test.jsx",
"**/*.test.js"
]
diff --git a/.npmignore b/.npmignore
index f61567e..0e6624e 100755
--- a/.npmignore
+++ b/.npmignore
@@ -7,7 +7,7 @@ Dockerfile
Makefile
npm-debug.log
-config
+webpack
coverage
node_modules
public
diff --git a/openedx.yaml b/openedx.yaml
index 2529d74..a56e024 100644
--- a/openedx.yaml
+++ b/openedx.yaml
@@ -4,4 +4,3 @@
nick: acct
oeps: {}
owner: edx/arch-team
-track-pulls: true
diff --git a/package-lock.json b/package-lock.json
index dc6ecc8..77d12d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5490,11 +5490,6 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
- "bootstrap": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.2.1.tgz",
- "integrity": "sha512-tt/7vIv3Gm2mnd/WeDx36nfGGHleil0Wg8IeB7eMrVkY0fZ5iTaBisSh8oNANc2IBsCc6vCgCNTIM/IEN0+50Q=="
- },
"boxen": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
@@ -7932,7 +7927,8 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
},
"decompress": {
"version": "4.2.0",
@@ -10399,7 +10395,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -10420,12 +10417,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -10440,17 +10439,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -10567,7 +10569,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -10579,6 +10582,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -10593,6 +10597,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -10600,12 +10605,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -10624,6 +10631,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -10704,7 +10712,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -10716,6 +10725,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -10801,7 +10811,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -10837,6 +10848,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -10856,6 +10868,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -10899,12 +10912,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -14041,6 +14056,11 @@
"integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=",
"dev": true
},
+ "lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
+ },
"lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
@@ -18384,6 +18404,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
+ "dev": true,
"requires": {
"decode-uri-component": "^0.2.0",
"object-assign": "^4.1.0",
@@ -25227,11 +25248,6 @@
"iconv-lite": "0.4.24"
}
},
- "whatwg-fetch": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
- "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
- },
"whatwg-mimetype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
diff --git a/package.json b/package.json
index 6139d47..1929587 100755
--- a/package.json
+++ b/package.json
@@ -7,13 +7,13 @@
"url": "git+https://github.com/edx/frontend-app-profile.git"
},
"scripts": {
- "build": "NODE_ENV=production BABEL_ENV=production webpack --config=config/webpack.prod.config.js",
+ "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",
"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=config/webpack.dev.config.js --progress",
+ "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"
},
@@ -38,7 +38,6 @@
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"babel-polyfill": "^6.26.0",
- "bootstrap": "^4.2.1",
"classnames": "^2.2.6",
"connected-react-router": "^5.0.1",
"email-prop-type": "^1.1.5",
@@ -50,10 +49,10 @@
"iso-countries-languages": "^0.2.1",
"lodash.camelcase": "^4.3.0",
"lodash.get": "^4.4.2",
+ "lodash.pick": "^4.4.0",
"lodash.snakecase": "^4.1.1",
"newrelic": "^5.5.0",
"prop-types": "^15.5.10",
- "query-string": "^5.1.1",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-intl": "^2.8.0",
@@ -68,8 +67,7 @@
"redux-thunk": "^2.2.0",
"reselect": "^4.0.0",
"universal-cookie": "^3.1.0",
- "webpack-rtl-plugin": "^2.0.0",
- "whatwg-fetch": "^2.0.3"
+ "webpack-rtl-plugin": "^2.0.0"
},
"devDependencies": {
"@svgr/webpack": "^4.1.0",
diff --git a/src/actions/AsyncActionType.js b/src/actions/AsyncActionType.js
deleted file mode 100644
index cdde42d..0000000
--- a/src/actions/AsyncActionType.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * 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 default 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`;
- }
-}
diff --git a/src/actions/AsyncActionType.test.js b/src/actions/AsyncActionType.test.js
deleted file mode 100644
index 70a9b5a..0000000
--- a/src/actions/AsyncActionType.test.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import AsyncActionType from './AsyncActionType';
-
-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');
- });
-});
diff --git a/assets/dot-pattern-light.png b/src/assets/dot-pattern-light.png
similarity index 100%
rename from assets/dot-pattern-light.png
rename to src/assets/dot-pattern-light.png
diff --git a/assets/edx-footer.png b/src/assets/edx-footer.png
similarity index 100%
rename from assets/edx-footer.png
rename to src/assets/edx-footer.png
diff --git a/assets/edx-sm.png b/src/assets/edx-sm.png
similarity index 100%
rename from assets/edx-sm.png
rename to src/assets/edx-sm.png
diff --git a/src/common/actions.js b/src/common/actions.js
new file mode 100644
index 0000000..2a17c48
--- /dev/null
+++ b/src/common/actions.js
@@ -0,0 +1,11 @@
+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);
+}
diff --git a/src/components/common/PageLoading.jsx b/src/common/components/PageLoading.jsx
similarity index 87%
rename from src/components/common/PageLoading.jsx
rename to src/common/components/PageLoading.jsx
index c4ccead..9e5fa89 100644
--- a/src/components/common/PageLoading.jsx
+++ b/src/common/components/PageLoading.jsx
@@ -1,11 +1,8 @@
import React from 'react';
-import Banner from './Banner';
-
function PageLoading() {
return (
-
{
it('should use the provided modify function to change all keys in and object and its children', () => {
@@ -102,4 +102,17 @@ describe('keepKeys', () => {
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');
+ });
+ });
});
diff --git a/src/components/App.jsx b/src/components/App.jsx
index 43a04dc..593e70a 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -1,67 +1,167 @@
import React, { Component } from 'react';
import { connect, Provider } from 'react-redux';
import PropTypes from 'prop-types';
-import { IntlProvider } from 'react-intl';
+import { IntlProvider, injectIntl, intlShape } from 'react-intl';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { sendTrackEvent } from '@edx/frontend-analytics';
+import SiteHeader from '@edx/frontend-component-site-header';
import SiteFooter from '@edx/frontend-component-footer';
-import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
-
-import apiClient from '../config/apiClient';
import { getLocale, getMessages } from '@edx/frontend-i18n'; // eslint-disable-line
-import SiteHeader from './common/SiteHeader';
-import ConnectedProfilePage from './ProfilePage';
-import FooterLogo from '../../assets/edx-footer.png';
+import { PageLoading, fetchUserAccount } from '../common';
+import { ConnectedProfilePage } from '../profile';
+
+import FooterLogo from '../assets/edx-footer.png';
+import HeaderLogo from '../assets/logo.svg';
import ErrorPage from './ErrorPage';
import NotFoundPage from './NotFoundPage';
-import PageLoading from './common/PageLoading';
+
+import messages from './App.messages';
+
+
+function PageContent({
+ ready,
+ configuration,
+ username,
+ avatar,
+ intl,
+}) {
+ if (!ready) {
+ return
;
+ }
+
+ const mainMenu = [
+ {
+ type: 'item',
+ href: `${process.env.MARKETING_SITE_BASE_URL}/course`,
+ content: intl.formatMessage(messages['siteheader.links.courses']),
+ },
+ {
+ type: 'item',
+ href: `${process.env.MARKETING_SITE_BASE_URL}/course?program=all`,
+ content: intl.formatMessage(messages['siteheader.links.programs']),
+ },
+ {
+ type: 'item',
+ href: `${process.env.MARKETING_SITE_BASE_URL}/schools-partners`,
+ content: intl.formatMessage(messages['siteheader.links.schools']),
+ },
+ ];
+ const userMenu = [
+ {
+ type: 'item',
+ href: `${process.env.LMS_BASE_URL}`,
+ content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
+ },
+ {
+ type: 'item',
+ href: `${process.env.BASE_URL}/u/${username}`,
+ content: intl.formatMessage(messages['siteheader.user.menu.profile']),
+ },
+ {
+ type: 'item',
+ href: `${process.env.LMS_BASE_URL}/account/settings`,
+ content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
+ },
+ {
+ type: 'item',
+ href: process.env.LOGOUT_URL,
+ content: intl.formatMessage(messages['siteheader.user.menu.logout']),
+ },
+ ];
+ const loggedOutItems = [
+ {
+ type: 'item',
+ href: `${process.env.LMS_BASE_URL}/login`,
+ content: intl.formatMessage(messages['siteheader.user.menu.login']),
+ },
+ {
+ type: 'item',
+ href: `${process.env.LMS_BASE_URL}/register`,
+ content: intl.formatMessage(messages['siteheader.user.menu.register']),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+PageContent.propTypes = {
+ username: PropTypes.string.isRequired,
+ avatar: PropTypes.string,
+ ready: PropTypes.bool,
+ configuration: PropTypes.shape({
+ SITE_NAME: 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,
+ GOOGLE_PLUS_URL: PropTypes.string.isRequired,
+ REDDIT_URL: PropTypes.string.isRequired,
+ APPLE_APP_STORE_URL: PropTypes.string.isRequired,
+ GOOGLE_PLAY_URL: PropTypes.string.isRequired,
+ }).isRequired,
+ intl: intlShape.isRequired,
+};
+
+PageContent.defaultProps = {
+ ready: false,
+ avatar: null,
+};
+
+const IntlPageContent = injectIntl(PageContent);
class App extends Component {
componentDidMount() {
const { username } = this.props;
- const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
- this.props.fetchUserAccount(userAccountApiService, username);
- }
-
- renderContent() {
- if (!this.props.ready) {
- return
;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ this.props.fetchUserAccount(username);
}
render() {
@@ -69,7 +169,12 @@ class App extends Component {
- {this.renderContent()}
+
@@ -80,13 +185,32 @@ class App extends Component {
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
ready: PropTypes.bool,
+ configuration: PropTypes.shape({
+ SITE_NAME: 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,
+ GOOGLE_PLUS_URL: PropTypes.string.isRequired,
+ REDDIT_URL: PropTypes.string.isRequired,
+ APPLE_APP_STORE_URL: PropTypes.string.isRequired,
+ GOOGLE_PLAY_URL: PropTypes.string.isRequired,
+ }).isRequired,
};
App.defaultProps = {
ready: false,
+ avatar: null,
};
const mapStateToProps = state => ({
@@ -94,6 +218,10 @@ const mapStateToProps = state => ({
// An error means that we tried to load the user account and failed,
// which also means we're ready to display something.
ready: state.userAccount.loaded || state.userAccount.error != null,
+ configuration: state.configuration,
+ avatar: state.userAccount.profileImage.hasImage
+ ? state.userAccount.profileImage.imageUrlMedium
+ : null,
});
export default connect(
diff --git a/src/components/common/SiteHeader.messages.jsx b/src/components/App.messages.jsx
similarity index 100%
rename from src/components/common/SiteHeader.messages.jsx
rename to src/components/App.messages.jsx
diff --git a/src/components/ErrorPage.jsx b/src/components/ErrorPage.jsx
index 7fc2e77..c95b50f 100644
--- a/src/components/ErrorPage.jsx
+++ b/src/components/ErrorPage.jsx
@@ -1,46 +1,51 @@
-import React, { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
-import apiClient from '../config/apiClient';
-
-export default class ErrorPage extends Component {
- componentDidMount() {}
-
- render() {
- const { username } = apiClient.getAuthenticationState().authentication;
-
- return (
-
-
-
-
-
-
- }
- />
-
-
+function ErrorPage({ username }) {
+ return (
+
+ );
}
+
+ErrorPage.propTypes = {
+ username: PropTypes.string.isRequired,
+};
+
+export default connect(
+ state => ({
+ username: state.authentication.username,
+ }),
+ {},
+)(ErrorPage);
diff --git a/src/components/NotFoundPage.jsx b/src/components/NotFoundPage.jsx
index 0a4807f..81bb930 100644
--- a/src/components/NotFoundPage.jsx
+++ b/src/components/NotFoundPage.jsx
@@ -1,23 +1,16 @@
-import React, { Component } from 'react';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
-export default class NotFoundPage extends Component {
- componentDidMount() {}
-
- render() {
- return (
-
- );
- }
+export default function NotFoundPage() {
+ return (
+
+ );
}
diff --git a/src/components/common/SiteHeader.jsx b/src/components/common/SiteHeader.jsx
deleted file mode 100644
index e65be54..0000000
--- a/src/components/common/SiteHeader.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { connect } from 'react-redux';
-import SiteHeader from '@edx/frontend-component-site-header';
-import { injectIntl } from '@edx/frontend-i18n'; // eslint-disable-line
-
-import messages from './SiteHeader.messages';
-
-import Logo from '../../assets/logo.svg';
-
-const mapStateToProps = (state, { intl }) => ({
- logo: Logo,
- logoDestination: process.env.MARKETING_SITE_BASE_URL,
- logoAltText: 'edX',
- mainMenu: [
- {
- type: 'item',
- href: `${process.env.MARKETING_SITE_BASE_URL}/course`,
- content: intl.formatMessage(messages['siteheader.links.courses']),
- },
- {
- type: 'item',
- href: `${process.env.MARKETING_SITE_BASE_URL}/course?program=all`,
- content: intl.formatMessage(messages['siteheader.links.programs']),
- },
- {
- type: 'item',
- href: `${process.env.MARKETING_SITE_BASE_URL}/schools-partners`,
- content: intl.formatMessage(messages['siteheader.links.schools']),
- },
- ],
- loggedIn: true,
- username: state.userAccount.username,
- avatar: state.userAccount.profileImage.hasImage ?
- state.userAccount.profileImage.imageUrlMedium :
- null,
- userMenu: [
- {
- type: 'item',
- href: `${process.env.LMS_BASE_URL}`,
- content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
- },
- {
- type: 'item',
- href: `${process.env.BASE_URL}/u/${state.userAccount.username}`,
- content: intl.formatMessage(messages['siteheader.user.menu.profile']),
- },
- {
- type: 'item',
- href: `${process.env.LMS_BASE_URL}/account/settings`,
- content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
- },
- {
- type: 'item',
- href: process.env.LOGOUT_URL,
- content: intl.formatMessage(messages['siteheader.user.menu.logout']),
- },
- ],
- loggedOutItems: [
- {
- type: 'item',
- href: `${process.env.LMS_BASE_URL}/login`,
- content: intl.formatMessage(messages['siteheader.user.menu.login']),
- },
- {
- type: 'item',
- href: `${process.env.LMS_BASE_URL}/register`,
- content: intl.formatMessage(messages['siteheader.user.menu.register']),
- },
- ],
-});
-
-export default injectIntl(connect(mapStateToProps)(SiteHeader));
diff --git a/src/config/analytics.js b/src/config/analytics.js
deleted file mode 100644
index 4a40e6c..0000000
--- a/src/config/analytics.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { configureAnalytics, initializeSegment } from '@edx/frontend-analytics';
-import LoggingService from '@edx/frontend-logging';
-
-import apiClient from '../config/apiClient';
-import { configuration } from '../config/environment';
-
-initializeSegment(configuration.SEGMENT_KEY);
-configureAnalytics({
- loggingService: LoggingService,
- authApiClient: apiClient,
- analyticsApiBaseUrl: configuration.LMS_BASE_URL,
-});
diff --git a/src/config/apiClient.js b/src/config/apiClient.js
deleted file mode 100644
index 9c43820..0000000
--- a/src/config/apiClient.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
-
-import { configuration } from './environment';
-
-const apiClient = getAuthenticatedAPIClient({
- appBaseUrl: configuration.BASE_URL,
- authBaseUrl: process.env.LMS_BASE_URL,
- loginUrl: configuration.LOGIN_URL,
- logoutUrl: configuration.LOGOUT_URL,
- csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
- refreshAccessTokenEndpoint: configuration.REFRESH_ACCESS_TOKEN_ENDPOINT,
- accessTokenCookieName: configuration.ACCESS_TOKEN_COOKIE_NAME,
- userInfoCookieName: process.env.USER_INFO_COOKIE_NAME,
- csrfCookieName: configuration.CSRF_COOKIE_NAME,
-});
-
-export default apiClient;
diff --git a/src/config/configureStore.js b/src/config/configureStore.js
deleted file mode 100644
index d867a3c..0000000
--- a/src/config/configureStore.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { configuration } from './environment';
-import configureStoreProd from './configureStore.prod';
-import configureStoreDev from './configureStore.dev';
-
-if (configuration.ENVIRONMENT === 'production') {
- module.exports = configureStoreProd;
-} else {
- module.exports = configureStoreDev;
-}
diff --git a/src/constants/education.js b/src/constants/education.js
deleted file mode 100644
index a62cfff..0000000
--- a/src/constants/education.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const EDUCATION_LEVELS = [
- 'p',
- 'm',
- 'b',
- 'a',
- 'hs',
- 'jhs',
- 'el',
- 'none',
- 'o',
-];
-
-export default EDUCATION_LEVELS;
diff --git a/src/constants/social.js b/src/constants/social.js
deleted file mode 100644
index 90f7639..0000000
--- a/src/constants/social.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const SOCIAL = {
- linkedin: {
- title: 'LinkedIn',
- },
- twitter: {
- title: 'Twitter',
- },
- facebook: {
- title: 'Facebook',
- },
-};
-
-const getSocialLinks = (socialLinksState) => {
- const socialLinks = {};
- if (socialLinksState) {
- socialLinksState.forEach((link) => {
- const socialLinkUrl = link.socialLink;
- if (socialLinkUrl) {
- socialLinks[link.platform] = {
- ...SOCIAL[link.platform],
- url: link.socialLink,
- display: link.socialLink,
- };
- }
- });
- }
-
- return socialLinks;
-};
-
-export {
- getSocialLinks,
- SOCIAL,
-};
diff --git a/src/config/environment.js b/src/environment.js
similarity index 61%
rename from src/config/environment.js
rename to src/environment.js
index ec272c9..debca25 100644
--- a/src/config/environment.js
+++ b/src/environment.js
@@ -1,18 +1,34 @@
export const configuration = {
BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
+ CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
- ACCOUNT_SETTINGS_URL: `${process.env.LMS_BASE_URL}/account/settings`,
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,
- DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
- SECURE_COOKIES: process.env.NODE_ENV !== 'development',
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,
+ GOOGLE_PLUS_URL: process.env.GOOGLE_PLUS_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`,
diff --git a/src/index.jsx b/src/index.jsx
index f31ad8f..62f03e0 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,26 +1,65 @@
import 'babel-polyfill';
-
import React from 'react';
import ReactDOM from 'react-dom';
-import { identifyAuthenticatedUser, sendPageEvent } from '@edx/frontend-analytics';
+import { identifyAuthenticatedUser, sendPageEvent, configureAnalytics, initializeSegment } from '@edx/frontend-analytics';
+import LoggingService from '@edx/frontend-logging';
+import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
-import './config/analytics';
-import configureStore from './config/configureStore';
-import apiClient from './config/apiClient';
+import { configuration } from './environment';
import { handleRtl } from './i18n/i18n-loader';
+import configureStore from './store';
+import { configureProfileApiService } from './profile';
+import { configureUserAccountApiService } from './common';
import './index.scss';
-
import App from './components/App';
+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,
+});
+
+/**
+ * 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() {
+ return Object.assign({}, { configuration }, apiClient.getAuthenticationState());
+}
+
+function configure() {
+ const { store, history } = configureStore(createInitialState(), configuration.ENVIRONMENT);
+
+ configureProfileApiService(configuration, apiClient);
+ configureUserAccountApiService(configuration, apiClient);
+ initializeSegment(configuration.SEGMENT_KEY);
+ configureAnalytics({
+ loggingService: LoggingService,
+ authApiClient: apiClient,
+ analyticsApiBaseUrl: configuration.LMS_BASE_URL,
+ });
+
+ if (configuration.ENVIRONMENT === 'production') {
+ handleRtl();
+ }
+
+ return {
+ store,
+ history,
+ };
+}
+
apiClient.ensurePublicOrAuthenticationAndCookies(
window.location.pathname,
() => {
- const { store, history } = configureStore();
-
- if (process.env.NODE_ENV === 'production') {
- handleRtl();
- }
+ const { store, history } = configure();
ReactDOM.render(
, document.getElementById('root'));
@@ -28,3 +67,4 @@ apiClient.ensurePublicOrAuthenticationAndCookies(
sendPageEvent();
},
);
+
diff --git a/src/index.scss b/src/index.scss
index 0ede029..125ea8a 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -15,7 +15,7 @@ $fa-font-path: "~font-awesome/fonts";
.btn, a.btn {
text-decoration: none;
&:hover {
- text-decoration: none;
+ text-decoration: none;
}
}
.btn-link {
@@ -27,7 +27,7 @@ $fa-font-path: "~font-awesome/fonts";
.profile-page-bg-banner {
height: 12rem;
- background-image: url('../assets/dot-pattern-light.png');
+ background-image: url('./assets/dot-pattern-light.png');
background-repeat: repeat-x;
background-size: auto 85%;
}
@@ -71,7 +71,7 @@ $fa-font-path: "~font-awesome/fonts";
margin-bottom: 1.2rem;
}
}
-
+
.dropdown {
@include media-breakpoint-up(md) {
margin-bottom: 1.2rem;
diff --git a/src/actions/ProfileActions.js b/src/profile/actions.js
similarity index 97%
rename from src/actions/ProfileActions.js
rename to src/profile/actions.js
index d83035b..e7aa9ea 100644
--- a/src/actions/ProfileActions.js
+++ b/src/profile/actions.js
@@ -1,4 +1,6 @@
-import AsyncActionType from './AsyncActionType';
+import { utils } from '../common';
+
+const { AsyncActionType } = utils;
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE');
diff --git a/src/actions/ProfileActions.test.js b/src/profile/actions.test.js
similarity index 99%
rename from src/actions/ProfileActions.test.js
rename to src/profile/actions.test.js
index 2703989..7fd7fad 100644
--- a/src/actions/ProfileActions.test.js
+++ b/src/profile/actions.test.js
@@ -20,7 +20,7 @@ import {
deleteProfilePhotoSuccess,
deleteProfilePhotoReset,
deleteProfilePhoto,
-} from './ProfileActions';
+} from './actions';
describe('editable field actions', () => {
it('should create an open action', () => {
diff --git a/src/assets/avatar.svg b/src/profile/assets/avatar.svg
similarity index 100%
rename from src/assets/avatar.svg
rename to src/profile/assets/avatar.svg
diff --git a/assets/micro-masters.svg b/src/profile/assets/micro-masters.svg
similarity index 100%
rename from assets/micro-masters.svg
rename to src/profile/assets/micro-masters.svg
diff --git a/assets/professional-certificate.svg b/src/profile/assets/professional-certificate.svg
similarity index 100%
rename from assets/professional-certificate.svg
rename to src/profile/assets/professional-certificate.svg
diff --git a/assets/verified-certificate.svg b/src/profile/assets/verified-certificate.svg
similarity index 100%
rename from assets/verified-certificate.svg
rename to src/profile/assets/verified-certificate.svg
diff --git a/src/components/ProfilePage/AgeMessage.jsx b/src/profile/components/AgeMessage.jsx
similarity index 83%
rename from src/components/ProfilePage/AgeMessage.jsx
rename to src/profile/components/AgeMessage.jsx
index c0ff14c..38ed7b3 100644
--- a/src/components/ProfilePage/AgeMessage.jsx
+++ b/src/profile/components/AgeMessage.jsx
@@ -1,12 +1,9 @@
import React from 'react';
+import PropTypes from 'prop-types';
import { StatusAlert } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
-import { configuration } from '../../config/environment';
-
-const { ACCOUNT_SETTINGS_URL } = configuration;
-
-function AgeMessage() {
+function AgeMessage({ accountSettingsUrl }) {
return (
-
+
+
{this.props.intl.formatMessage(messages['profile.viewMyRecords'])}
);
@@ -124,7 +119,17 @@ export class ProfilePage extends React.Component {
);
}
- render() {
+ renderAgeMessage() {
+ const { requiresParentalConsent, isAuthenticatedUserProfile } = this.props;
+ const shouldShowAgeMessage = requiresParentalConsent && isAuthenticatedUserProfile;
+
+ if (!shouldShowAgeMessage) {
+ return null;
+ }
+ return ;
+ }
+
+ renderContent() {
const {
profileImage,
name,
@@ -142,11 +147,12 @@ export class ProfilePage extends React.Component {
bio,
visibilityBio,
requiresParentalConsent,
- isAuthenticatedUserProfile,
isLoadingProfile,
} = this.props;
- if (isLoadingProfile) return ;
+ if (isLoadingProfile) {
+ return ;
+ }
const commonFormProps = {
openHandler: this.handleOpen,
@@ -155,98 +161,108 @@ export class ProfilePage extends React.Component {
changeHandler: this.handleChange,
};
- const shouldShowAgeMessage = requiresParentalConsent && isAuthenticatedUserProfile;
return (
-
-
-
-
-
-
-
- {this.renderHeadingLockup()}
-
-
- {this.renderViewMyRecordsButton()}
-
+
+
+
- {this.renderPhotoUploadErrorMessage()}
-
-
-
- {this.renderHeadingLockup()}
-
-
- {this.renderViewMyRecordsButton()}
-
-
-
-
-
-
+
+
+ {this.renderHeadingLockup()}
-
- {shouldShowAgeMessage ?
: null}
-
-
+
+ {this.renderViewMyRecordsButton()}
+ {this.renderPhotoUploadErrorMessage()}
+
+
+
+ {this.renderHeadingLockup()}
+
+
+ {this.renderViewMyRecordsButton()}
+
+
+
+
+
+
+
+
+ {this.renderAgeMessage()}
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+
+
+ {this.renderContent()}
);
}
}
ProfilePage.propTypes = {
+ // Config
+ configuration: PropTypes.shape({
+ VIEW_MY_RECORDS_URL: PropTypes.string.isRequired,
+ ACCOUNT_SETTINGS_URL: PropTypes.string.isRequired,
+ }).isRequired,
// Account data
username: PropTypes.string,
requiresParentalConsent: PropTypes.bool,
diff --git a/src/components/ProfilePage.messages.jsx b/src/profile/components/ProfilePage.messages.jsx
similarity index 100%
rename from src/components/ProfilePage.messages.jsx
rename to src/profile/components/ProfilePage.messages.jsx
diff --git a/src/components/ProfilePage.test.jsx b/src/profile/components/ProfilePage.test.jsx
similarity index 99%
rename from src/components/ProfilePage.test.jsx
rename to src/profile/components/ProfilePage.test.jsx
index f2d5c15..50e35ba 100644
--- a/src/components/ProfilePage.test.jsx
+++ b/src/profile/components/ProfilePage.test.jsx
@@ -9,7 +9,6 @@ import configureMockStore from 'redux-mock-store';
import * as analytics from '@edx/frontend-analytics';
import ConnectedProfilePage from './ProfilePage';
-
const mockStore = configureMockStore();
const storeMocks = {
loadingApp: require('./__mocks__/loadingApp.mockStore.js'),
diff --git a/src/components/__mocks__/loadingApp.mockStore.js b/src/profile/components/__mocks__/loadingApp.mockStore.js
similarity index 82%
rename from src/components/__mocks__/loadingApp.mockStore.js
rename to src/profile/components/__mocks__/loadingApp.mockStore.js
index dc37d83..f4c9b59 100644
--- a/src/components/__mocks__/loadingApp.mockStore.js
+++ b/src/profile/components/__mocks__/loadingApp.mockStore.js
@@ -3,6 +3,10 @@ module.exports = {
userId: 9,
username: 'staff'
},
+ configuration: {
+ VIEW_MY_RECORDS_URL: 'http://localhost:18150/records',
+ ACCOUNT_SETTINGS_URL: 'http://localhost:18000/account/settings',
+ },
userAccount: {
loading: false,
error: null,
diff --git a/src/components/__mocks__/savingEditedBio.mockStore.js b/src/profile/components/__mocks__/savingEditedBio.mockStore.js
similarity index 96%
rename from src/components/__mocks__/savingEditedBio.mockStore.js
rename to src/profile/components/__mocks__/savingEditedBio.mockStore.js
index 7159e27..5aaf04c 100644
--- a/src/components/__mocks__/savingEditedBio.mockStore.js
+++ b/src/profile/components/__mocks__/savingEditedBio.mockStore.js
@@ -3,6 +3,10 @@ module.exports = {
userId: 9,
username: 'staff'
},
+ configuration: {
+ VIEW_MY_RECORDS_URL: 'http://localhost:18150/records',
+ ACCOUNT_SETTINGS_URL: 'http://localhost:18000/account/settings',
+ },
userAccount: {
loading: false,
error: null,
diff --git a/src/components/__mocks__/viewOtherProfile.mockStore.js b/src/profile/components/__mocks__/viewOtherProfile.mockStore.js
similarity index 94%
rename from src/components/__mocks__/viewOtherProfile.mockStore.js
rename to src/profile/components/__mocks__/viewOtherProfile.mockStore.js
index b67409b..bc984f7 100644
--- a/src/components/__mocks__/viewOtherProfile.mockStore.js
+++ b/src/profile/components/__mocks__/viewOtherProfile.mockStore.js
@@ -3,6 +3,10 @@ module.exports = {
userId: 9,
username: 'staff'
},
+ configuration: {
+ VIEW_MY_RECORDS_URL: 'http://localhost:18150/records',
+ ACCOUNT_SETTINGS_URL: 'http://localhost:18000/account/settings',
+ },
userAccount: {
loading: false,
error: null,
diff --git a/src/components/__mocks__/viewOwnProfile.mockStore.js b/src/profile/components/__mocks__/viewOwnProfile.mockStore.js
similarity index 96%
rename from src/components/__mocks__/viewOwnProfile.mockStore.js
rename to src/profile/components/__mocks__/viewOwnProfile.mockStore.js
index f7d169a..ed58442 100644
--- a/src/components/__mocks__/viewOwnProfile.mockStore.js
+++ b/src/profile/components/__mocks__/viewOwnProfile.mockStore.js
@@ -3,6 +3,10 @@ module.exports = {
userId: 9,
username: 'staff'
},
+ configuration: {
+ VIEW_MY_RECORDS_URL: 'http://localhost:18150/records',
+ ACCOUNT_SETTINGS_URL: 'http://localhost:18000/account/settings',
+ },
userAccount: {
loading: false,
error: null,
diff --git a/src/components/__snapshots__/ProfilePage.test.jsx.snap b/src/profile/components/__snapshots__/ProfilePage.test.jsx.snap
similarity index 99%
rename from src/components/__snapshots__/ProfilePage.test.jsx.snap
rename to src/profile/components/__snapshots__/ProfilePage.test.jsx.snap
index f6b2746..1328809 100644
--- a/src/components/__snapshots__/ProfilePage.test.jsx.snap
+++ b/src/profile/components/__snapshots__/ProfilePage.test.jsx.snap
@@ -1,22 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`
Renders correctly in various states app loading 1`] = `
-
+
-
+
+ className="d-flex justify-content-center align-items-center flex-column"
+ style={
+ Object {
+ "height": "50vh",
+ }
+ }
+ >
+
+
`;
@@ -332,7 +336,7 @@ exports[`
Renders correctly in various states viewing own profile
>
Renders correctly in various states viewing own profile
>
Renders correctly in various states while saving an edi
>
Renders correctly in various states while saving an edi
>
({
+jest.mock('./services', () => ({
getProfile: jest.fn(),
patchProfile: jest.fn(),
postProfilePhoto: jest.fn(),
@@ -16,20 +16,20 @@ jest.mock('../services/ProfileApiService', () => ({
// RootSaga and ProfileApiService must be imported AFTER the mock above.
/* eslint-disable import/first */
-import rootSaga, {
+import profileSaga, {
handleFetchProfile,
handleSaveProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
handleFetchUserAccountFailure,
-} from './RootSaga';
-import * as ProfileApiService from '../services/ProfileApiService';
+} from './sagas';
+import * as ProfileApiService from './services';
/* eslint-enable import/first */
describe('RootSaga', () => {
- describe('rootSaga', () => {
+ describe('profileSaga', () => {
it('should pass actions to the correct sagas', () => {
- const gen = rootSaga();
+ const gen = profileSaga();
expect(gen.next().value)
.toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile));
diff --git a/src/selectors/ProfilePageSelector.js b/src/profile/selectors.js
similarity index 99%
rename from src/selectors/ProfilePageSelector.js
rename to src/profile/selectors.js
index 08b0ba9..5768ff5 100644
--- a/src/selectors/ProfilePageSelector.js
+++ b/src/profile/selectors.js
@@ -16,6 +16,7 @@ export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
export const accountErrorsSelector = state => state.profilePage.errors;
+export const configurationSelector = state => state.configuration;
export const isAuthenticatedUserProfileSelector = createSelector(
authenticationUsernameSelector,
@@ -329,6 +330,7 @@ export const profilePageSelector = createSelector(
isAuthenticatedUserProfileSelector,
draftSocialLinksByPlatformSelector,
accountErrorsSelector,
+ configurationSelector,
(
account,
formValues,
@@ -339,6 +341,7 @@ export const profilePageSelector = createSelector(
isAuthenticatedUserProfile,
draftSocialLinksByPlatform,
errors,
+ configuration,
) => ({
// Account data we need
username: account.username,
@@ -381,5 +384,6 @@ export const profilePageSelector = createSelector(
isLoadingProfile,
isAuthenticatedUserProfile,
photoUploadError: errors.photo || null,
+ configuration,
}),
);
diff --git a/src/services/ProfileApiService.js b/src/profile/services.js
similarity index 68%
rename from src/services/ProfileApiService.js
rename to src/profile/services.js
index fc8a7dd..691dee9 100644
--- a/src/services/ProfileApiService.js
+++ b/src/profile/services.js
@@ -1,12 +1,32 @@
import LoggingService from '@edx/frontend-logging';
+import pick from 'lodash.pick';
-import apiClient from '../config/apiClient';
-import { configuration } from '../config/environment';
-import {
- camelCaseObject,
- convertKeyNames,
- snakeCaseObject,
-} from './utils';
+import { utils } from '../common';
+
+const { camelCaseObject, convertKeyNames, snakeCaseObject } = utils;
+
+let config = {
+ ACCOUNTS_API_BASE_URL: null,
+ CERTIFICATES_API_BASE_URL: null,
+ LMS_BASE_URL: null,
+ PREFERENCES_API_BASE_URL: null,
+};
+
+let apiClient = null;
+
+function validateConfiguration(newConfig) {
+ Object.keys(config).forEach((key) => {
+ if (newConfig[key] === undefined) {
+ throw new Error(`Service configuration error: ${key} is required.`);
+ }
+ });
+}
+
+export default function configure(newConfig, newApiClient) {
+ validateConfiguration(newConfig);
+ config = pick(newConfig, Object.keys(config));
+ apiClient = newApiClient;
+}
function processAccountData(data) {
return camelCaseObject(data);
@@ -24,7 +44,7 @@ function processAndThrowError(error, errorDataProcessor) {
// GET ACCOUNT
export async function getAccount(username) {
- const { data } = await apiClient.get(`${configuration.ACCOUNTS_API_BASE_URL}/${username}`);
+ const { data } = await apiClient.get(`${config.ACCOUNTS_API_BASE_URL}/${username}`);
// Process response data
return processAccountData(data);
@@ -34,17 +54,15 @@ export async function getAccount(username) {
export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
- const { data } = await apiClient.patch(
- `${configuration.ACCOUNTS_API_BASE_URL}/${username}`,
- processedParams,
- {
+ const { data } = await apiClient
+ .patch(`${config.ACCOUNTS_API_BASE_URL}/${username}`, processedParams, {
headers: {
'Content-Type': 'application/merge-patch+json',
},
- },
- ).catch((error) => {
- processAndThrowError(error, processAccountData);
- });
+ })
+ .catch((error) => {
+ processAndThrowError(error, processAccountData);
+ });
// Process response data
return processAccountData(data);
@@ -55,7 +73,7 @@ export async function patchProfile(username, params) {
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await apiClient.post(
- `${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`,
+ `${config.ACCOUNTS_API_BASE_URL}/${username}/image`,
formData,
{
headers: {
@@ -79,7 +97,7 @@ export async function postProfilePhoto(username, formData) {
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
- const { data } = await apiClient.delete(`${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`);
+ const { data } = await apiClient.delete(`${config.ACCOUNTS_API_BASE_URL}/${username}/image`);
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
@@ -92,7 +110,7 @@ export async function deleteProfilePhoto(username) {
// GET PREFERENCES
export async function getPreferences(username) {
- const { data } = await apiClient.get(`${configuration.PREFERENCES_API_BASE_URL}/${username}`);
+ const { data } = await apiClient.get(`${config.PREFERENCES_API_BASE_URL}/${username}`);
return camelCaseObject(data);
}
@@ -112,11 +130,9 @@ export async function patchPreferences(username, params) {
visibility_time_zone: 'visibility.time_zone',
});
- await apiClient.patch(
- `${configuration.PREFERENCES_API_BASE_URL}/${username}`,
- processedParams,
- { headers: { 'Content-Type': 'application/merge-patch+json' } },
- );
+ await apiClient.patch(`${config.PREFERENCES_API_BASE_URL}/${username}`, processedParams, {
+ headers: { 'Content-Type': 'application/merge-patch+json' },
+ });
return params; // TODO: Once the server returns the updated preferences object, return that.
}
@@ -129,15 +145,14 @@ function transformCertificateData(data) {
transformedData.push({
...camelCaseObject(cert),
certificateType: cert.certificate_type,
- downloadUrl: new URL(cert.download_url, configuration.LMS_BASE_URL).toString(),
+ downloadUrl: new URL(cert.download_url, config.LMS_BASE_URL).toString(),
});
});
return transformedData;
}
-
export async function getCourseCertificates(username) {
- const url = `${configuration.CERTIFICATES_API_BASE_URL}/${username}/`;
+ const url = `${config.CERTIFICATES_API_BASE_URL}/${username}/`;
try {
const { data } = await apiClient.get(url);
return transformCertificateData(data);
diff --git a/src/reducers/RootReducer.js b/src/reducers.js
similarity index 82%
rename from src/reducers/RootReducer.js
rename to src/reducers.js
index dbd3cb4..3558881 100755
--- a/src/reducers/RootReducer.js
+++ b/src/reducers.js
@@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
import { connectRouter } from 'connected-react-router';
-import profilePage from './ProfilePageReducer';
+import { profileReducer } from './profile';
const identityReducer = (state) => {
const newState = { ...state };
@@ -14,8 +14,9 @@ const createRootReducer = history =>
// The authentication state is added as initialState when
// creating the store in data/store.js.
authentication: identityReducer,
+ configuration: identityReducer,
userAccount,
- profilePage,
+ profilePage: profileReducer,
router: connectRouter(history),
});
diff --git a/src/sagas.js b/src/sagas.js
new file mode 100644
index 0000000..134eeeb
--- /dev/null
+++ b/src/sagas.js
@@ -0,0 +1,9 @@
+import { all } from 'redux-saga/effects';
+
+import { profileSaga } from './profile';
+
+export default function* rootSaga() {
+ yield all([
+ profileSaga(),
+ ]);
+}
diff --git a/src/config/configureStore.dev.js b/src/store/configureStore.dev.js
similarity index 77%
rename from src/config/configureStore.dev.js
rename to src/store/configureStore.dev.js
index bb9bfac..af8fe2b 100644
--- a/src/config/configureStore.dev.js
+++ b/src/store/configureStore.dev.js
@@ -6,18 +6,16 @@ import { routerMiddleware } from 'connected-react-router';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
-import apiClient from './apiClient';
-import createRootReducer from '../reducers/RootReducer';
-import rootSaga from '../sagas/RootSaga';
+import createRootReducer from '../reducers';
+import rootSaga from '../sagas';
-export default function configureStore() {
+export default function configureStore(initialState = {}) {
const history = createBrowserHistory();
const loggerMiddleware = createLogger({
collapsed: true,
});
const sagaMiddleware = createSagaMiddleware();
- const initialState = apiClient.getAuthenticationState();
const store = createStore(
createRootReducer(history),
diff --git a/src/config/configureStore.prod.js b/src/store/configureStore.prod.js
similarity index 71%
rename from src/config/configureStore.prod.js
rename to src/store/configureStore.prod.js
index 853017c..ba0418d 100644
--- a/src/config/configureStore.prod.js
+++ b/src/store/configureStore.prod.js
@@ -4,15 +4,13 @@ import thunkMiddleware from 'redux-thunk';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
-import apiClient from './apiClient';
-import createRootReducer from '../reducers/RootReducer';
-import rootSaga from '../sagas/RootSaga';
+import createRootReducer from '../reducers';
+import rootSaga from '../sagas';
-export default function configureStore() {
+export default function configureStore(initialState = {}) {
const history = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware();
- const initialState = apiClient.getAuthenticationState();
const store = createStore(
createRootReducer(history),
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..23e9275
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,9 @@
+import configureStoreProd from './configureStore.prod';
+import configureStoreDev from './configureStore.dev';
+
+export default function configureStore(state, env) {
+ if (env === 'production') {
+ return configureStoreProd(state);
+ }
+ return configureStoreDev(state);
+}
diff --git a/config/webpack.common.config.js b/webpack/webpack.common.config.js
similarity index 100%
rename from config/webpack.common.config.js
rename to webpack/webpack.common.config.js
diff --git a/config/webpack.dev.config.js b/webpack/webpack.dev.config.js
similarity index 100%
rename from config/webpack.dev.config.js
rename to webpack/webpack.dev.config.js
diff --git a/config/webpack.prod.config.js b/webpack/webpack.prod.config.js
similarity index 100%
rename from config/webpack.prod.config.js
rename to webpack/webpack.prod.config.js
index f18040d..7216743 100755
--- a/config/webpack.prod.config.js
+++ b/webpack/webpack.prod.config.js
@@ -145,8 +145,6 @@ module.exports = Merge.smart(commonConfig, {
USER_INFO_COOKIE_NAME: null,
CSRF_COOKIE_NAME: 'csrftoken',
LANGUAGE_PREFERENCE_COOKIE_NAME: null,
- NEW_RELIC_APP_ID: null,
- NEW_RELIC_LICENSE_KEY: null,
SITE_NAME: null,
MARKETING_SITE_BASE_URL: null,
SUPPORT_URL: null,
@@ -162,6 +160,8 @@ module.exports = Merge.smart(commonConfig, {
REDDIT_URL: null,
APPLE_APP_STORE_URL: null,
GOOGLE_PLAY_URL: null,
+ NEW_RELIC_APP_ID: null,
+ NEW_RELIC_LICENSE_KEY: null,
}),
new HtmlWebpackNewRelicPlugin({
// This plugin fixes an issue where the newrelic script will break if