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