Organize application according to semantic modules. (#153)
@@ -6,7 +6,7 @@
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"config/*.js",
|
||||
"webpack/*.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.test.js"
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
config
|
||||
webpack
|
||||
coverage
|
||||
node_modules
|
||||
public
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
nick: acct
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
track-pulls: true
|
||||
|
||||
60
package-lock.json
generated
@@ -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",
|
||||
|
||||
10
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",
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
11
src/common/actions.js
Normal file
@@ -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);
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import Banner from './Banner';
|
||||
|
||||
function PageLoading() {
|
||||
return (
|
||||
<div>
|
||||
<Banner />
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={{
|
||||
10
src/common/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as utils from './utils';
|
||||
import PageLoading from './components/PageLoading';
|
||||
import { configureUserAccountApiService, fetchUserAccount } from './actions';
|
||||
|
||||
export {
|
||||
PageLoading,
|
||||
utils,
|
||||
configureUserAccountApiService,
|
||||
fetchUserAccount,
|
||||
};
|
||||
@@ -46,3 +46,36 @@ export function keepKeys(data, whitelist) {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*
|
||||
* TODO: Put somewhere common to it can be used by other MFEs.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { modifyObjectKeys, camelCaseObject, snakeCaseObject, convertKeyNames, keepKeys } from './utils';
|
||||
import { AsyncActionType, modifyObjectKeys, camelCaseObject, snakeCaseObject, convertKeyNames, keepKeys } from './utils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 <PageLoading />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<SiteHeader
|
||||
logo={HeaderLogo}
|
||||
loggedIn
|
||||
username={username}
|
||||
avatar={avatar}
|
||||
logoAltText={configuration.SITE_NAME}
|
||||
logoDestination={configuration.MARKETING_SITE_BASE_URL}
|
||||
mainMenu={mainMenu}
|
||||
userMenu={userMenu}
|
||||
loggedOutItems={loggedOutItems}
|
||||
/>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ConnectedProfilePage} />
|
||||
<Route path="/error" component={ErrorPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={configuration.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
marketingSiteBaseUrl={configuration.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={configuration.SUPPORT_URL}
|
||||
contactUrl={configuration.CONTACT_URL}
|
||||
openSourceUrl={configuration.OPEN_SOURCE_URL}
|
||||
termsOfServiceUrl={configuration.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={configuration.PRIVACY_POLICY_URL}
|
||||
facebookUrl={configuration.FACEBOOK_URL}
|
||||
twitterUrl={configuration.TWITTER_URL}
|
||||
youTubeUrl={configuration.YOU_TUBE_URL}
|
||||
linkedInUrl={configuration.LINKED_IN_URL}
|
||||
googlePlusUrl={configuration.GOOGLE_PLUS_URL}
|
||||
redditUrl={configuration.REDDIT_URL}
|
||||
appleAppStoreUrl={configuration.APPLE_APP_STORE_URL}
|
||||
googlePlayUrl={configuration.GOOGLE_PLAY_URL}
|
||||
handleAllTrackEvents={sendTrackEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ConnectedProfilePage} />
|
||||
<Route path="/error" component={ErrorPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={process.env.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={process.env.SUPPORT_URL}
|
||||
contactUrl={process.env.CONTACT_URL}
|
||||
openSourceUrl={process.env.OPEN_SOURCE_URL}
|
||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||
facebookUrl={process.env.FACEBOOK_URL}
|
||||
twitterUrl={process.env.TWITTER_URL}
|
||||
youTubeUrl={process.env.YOU_TUBE_URL}
|
||||
linkedInUrl={process.env.LINKED_IN_URL}
|
||||
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
|
||||
redditUrl={process.env.REDDIT_URL}
|
||||
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
|
||||
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
|
||||
handleAllTrackEvents={sendTrackEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
this.props.fetchUserAccount(username);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -69,7 +169,12 @@ class App extends Component {
|
||||
<IntlProvider locale={getLocale()} messages={getMessages()}>
|
||||
<Provider store={this.props.store}>
|
||||
<ConnectedRouter history={this.props.history}>
|
||||
{this.renderContent()}
|
||||
<IntlPageContent
|
||||
ready={this.props.ready}
|
||||
configuration={this.props.configuration}
|
||||
username={this.props.username}
|
||||
avatar={this.props.avatar}
|
||||
/>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<p className="my-0 py-5 text-muted">
|
||||
<FormattedMessage
|
||||
id="profile.error.message.text"
|
||||
defaultMessage="An unexpected error occurred. Please click the button below to return to your profile and try again."
|
||||
description="error message when an unexpected error occurs"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Link to={`/u/${username}`}>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="profile.error.button.text"
|
||||
defaultMessage="Return to Your Profile"
|
||||
description="text for button that navigates back to your profile page after an error has occured"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
function ErrorPage({ username }) {
|
||||
return (
|
||||
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<p className="my-0 py-5 text-muted">
|
||||
<FormattedMessage
|
||||
id="profile.error.message.text"
|
||||
defaultMessage="An unexpected error occurred. Please click the button below to return to your profile and try again."
|
||||
description="error message when an unexpected error occurs"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Link to={`/u/${username}`}>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="profile.error.button.text"
|
||||
defaultMessage="Return to Your Profile"
|
||||
description="text for button that navigates back to your profile page after an error has occured"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorPage.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
username: state.authentication.username,
|
||||
}),
|
||||
{},
|
||||
)(ErrorPage);
|
||||
|
||||
@@ -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 (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p
|
||||
className="my-0 py-5 text-muted"
|
||||
style={{ maxWidth: '32em' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
const EDUCATION_LEVELS = [
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
];
|
||||
|
||||
export default EDUCATION_LEVELS;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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`,
|
||||
@@ -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(<App store={store} history={history} />, document.getElementById('root'));
|
||||
|
||||
@@ -28,3 +67,4 @@ apiClient.ensurePublicOrAuthenticationAndCookies(
|
||||
sendPageEvent();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
deleteProfilePhotoSuccess,
|
||||
deleteProfilePhotoReset,
|
||||
deleteProfilePhoto,
|
||||
} from './ProfileActions';
|
||||
} from './actions';
|
||||
|
||||
describe('editable field actions', () => {
|
||||
it('should create an open action', () => {
|
||||
|
Before Width: | Height: | Size: 1006 B After Width: | Height: | Size: 1006 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -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 (
|
||||
<StatusAlert
|
||||
alertType="info"
|
||||
@@ -24,7 +21,7 @@ function AgeMessage() {
|
||||
description="error message"
|
||||
tagName="p"
|
||||
/>
|
||||
<a href={ACCOUNT_SETTINGS_URL}>
|
||||
<a href={accountSettingsUrl}>
|
||||
<FormattedMessage
|
||||
id="profile.age.set.date"
|
||||
defaultMessage="Set your date of birth"
|
||||
@@ -39,4 +36,8 @@ function AgeMessage() {
|
||||
);
|
||||
}
|
||||
|
||||
AgeMessage.propTypes = {
|
||||
accountSettingsUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AgeMessage;
|
||||
@@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { StatusAlert, Hyperlink } from '@edx/paragon';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
|
||||
// Analytics
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-analytics';
|
||||
|
||||
// Actions
|
||||
@@ -16,25 +14,22 @@ import {
|
||||
openForm,
|
||||
closeForm,
|
||||
updateDraft,
|
||||
} from '../actions/ProfileActions';
|
||||
} from '../actions';
|
||||
|
||||
// Components
|
||||
import ProfileAvatar from './ProfilePage/ProfileAvatar';
|
||||
import Name from './ProfilePage/Name';
|
||||
import Country from './ProfilePage/Country';
|
||||
import PreferredLanguage from './ProfilePage/PreferredLanguage';
|
||||
import Education from './ProfilePage/Education';
|
||||
import SocialLinks from './ProfilePage/SocialLinks';
|
||||
import Bio from './ProfilePage/Bio';
|
||||
import Certificates from './ProfilePage/Certificates';
|
||||
import AgeMessage from './ProfilePage/AgeMessage';
|
||||
import DateJoined from './ProfilePage/DateJoined';
|
||||
import PageLoading from './common/PageLoading';
|
||||
import Banner from './common/Banner';
|
||||
import { profilePageSelector } from '../selectors/ProfilePageSelector';
|
||||
|
||||
// Configuration
|
||||
import { configuration } from '../config/environment';
|
||||
import ProfileAvatar from './forms/ProfileAvatar';
|
||||
import Name from './forms/Name';
|
||||
import Country from './forms/Country';
|
||||
import PreferredLanguage from './forms/PreferredLanguage';
|
||||
import Education from './forms/Education';
|
||||
import SocialLinks from './forms/SocialLinks';
|
||||
import Bio from './forms/Bio';
|
||||
import Certificates from './forms/Certificates';
|
||||
import AgeMessage from './AgeMessage';
|
||||
import DateJoined from './DateJoined';
|
||||
import { PageLoading } from '../../common';
|
||||
import Banner from './Banner';
|
||||
import { profilePageSelector } from '../selectors';
|
||||
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
@@ -89,7 +84,7 @@ export class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Hyperlink className="btn btn-primary" href={configuration.VIEW_MY_RECORDS_URL} target="_blank">
|
||||
<Hyperlink className="btn btn-primary" href={this.props.configuration.VIEW_MY_RECORDS_URL} target="_blank">
|
||||
{this.props.intl.formatMessage(messages['profile.viewMyRecords'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
@@ -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 <AgeMessage accountSettingsURL={this.props.configuration.ACCOUNT_SETTINGS_URL} />;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
profileImage,
|
||||
name,
|
||||
@@ -142,11 +147,12 @@ export class ProfilePage extends React.Component {
|
||||
bio,
|
||||
visibilityBio,
|
||||
requiresParentalConsent,
|
||||
isAuthenticatedUserProfile,
|
||||
isLoadingProfile,
|
||||
} = this.props;
|
||||
|
||||
if (isLoadingProfile) return <PageLoading />;
|
||||
if (isLoadingProfile) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
const commonFormProps = {
|
||||
openHandler: this.handleOpen,
|
||||
@@ -155,98 +161,108 @@ export class ProfilePage extends React.Component {
|
||||
changeHandler: this.handleChange,
|
||||
};
|
||||
|
||||
const shouldShowAgeMessage = requiresParentalConsent && isAuthenticatedUserProfile;
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<Banner />
|
||||
<div className="container-fluid">
|
||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||
<div className="col-auto col-md-4 col-lg-3">
|
||||
<div className="d-flex align-items-center d-md-block">
|
||||
<ProfileAvatar
|
||||
className="mb-md-3"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={this.handleSaveProfilePhoto}
|
||||
onDelete={this.handleDeleteProfilePhoto}
|
||||
savePhotoState={this.props.savePhotoState}
|
||||
isEditable={this.props.isAuthenticatedUserProfile && !requiresParentalConsent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col pl-0">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="d-none d-md-block float-right">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
<div className="container-fluid">
|
||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||
<div className="col-auto col-md-4 col-lg-3">
|
||||
<div className="d-flex align-items-center d-md-block">
|
||||
<ProfileAvatar
|
||||
className="mb-md-3"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={this.handleSaveProfilePhoto}
|
||||
onDelete={this.handleDeleteProfilePhoto}
|
||||
savePhotoState={this.props.savePhotoState}
|
||||
isEditable={this.props.isAuthenticatedUserProfile && !requiresParentalConsent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderPhotoUploadErrorMessage()}
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-4">
|
||||
<div className="d-none d-md-block mb-4">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="d-md-none mb-4">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<div className="col pl-0">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||
{shouldShowAgeMessage ? <AgeMessage accountURL="#account" /> : null}
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<div className="d-none d-md-block float-right">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderPhotoUploadErrorMessage()}
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-4">
|
||||
<div className="d-none d-md-block mb-4">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="d-md-none mb-4">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||
{this.renderAgeMessage()}
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<Banner />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -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'),
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -1,22 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="profile-page"
|
||||
>
|
||||
<div
|
||||
className="profile-page-bg-banner bg-primary d-none d-md-block p-relative"
|
||||
/>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
/>
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -332,7 +336,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
|
||||
>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href="undefined/records"
|
||||
href="http://localhost:18150/records"
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@@ -384,7 +388,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
|
||||
>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href="undefined/records"
|
||||
href="http://localhost:18150/records"
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@@ -1344,7 +1348,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
|
||||
>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href="undefined/records"
|
||||
href="http://localhost:18150/records"
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@@ -1396,7 +1400,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
|
||||
>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href="undefined/records"
|
||||
href="http://localhost:18150/records"
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
@@ -14,7 +14,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
|
||||
class Bio extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -14,12 +14,12 @@ import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Assets
|
||||
import microMastersSVG from '../../../assets/micro-masters.svg';
|
||||
import professionalCertificateSVG from '../../../assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from '../../../assets/verified-certificate.svg';
|
||||
import microMastersSVG from '../../assets/micro-masters.svg';
|
||||
import professionalCertificateSVG from '../../assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from '../../assets/verified-certificate.svg';
|
||||
|
||||
// Selectors
|
||||
import { certificatesSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { certificatesSelector } from '../../selectors';
|
||||
|
||||
class Certificates extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,7 +13,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { countrySelector } from '../../selectors/ProfilePageSelector';
|
||||
import { countrySelector } from '../../selectors';
|
||||
|
||||
class Country extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -15,10 +15,10 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Constants
|
||||
import EDUCATION_LEVELS from '../../constants/education';
|
||||
import { EDUCATION_LEVELS } from '../../constants';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
|
||||
class Education extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -12,7 +12,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
|
||||
class Name extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,7 +13,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { preferredLanguageSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { preferredLanguageSelector } from '../../selectors';
|
||||
|
||||
class PreferredLanguage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -17,7 +17,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
|
||||
const platformDisplayInfo = {
|
||||
facebook: {
|
||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import EditButton from './EditButton';
|
||||
import { Visibility } from './Visibility';
|
||||
|
||||
|
||||
function EditableItemHeader({
|
||||
content,
|
||||
showVisibility,
|
||||
28
src/profile/constants.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const EDUCATION_LEVELS = [
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
];
|
||||
|
||||
const SOCIAL = {
|
||||
linkedin: {
|
||||
title: 'LinkedIn',
|
||||
},
|
||||
twitter: {
|
||||
title: 'Twitter',
|
||||
},
|
||||
facebook: {
|
||||
title: 'Facebook',
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
EDUCATION_LEVELS,
|
||||
SOCIAL,
|
||||
};
|
||||
11
src/profile/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import profileReducer from './reducers';
|
||||
import profileSaga from './sagas';
|
||||
import ConnectedProfilePage from './components/ProfilePage';
|
||||
import configureProfileApiService from './services';
|
||||
|
||||
export {
|
||||
ConnectedProfilePage,
|
||||
profileReducer,
|
||||
profileSaga,
|
||||
configureProfileApiService,
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FETCH_PROFILE,
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
} from '../actions/ProfileActions';
|
||||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
errors: {},
|
||||
@@ -25,16 +25,18 @@ import {
|
||||
deleteProfilePhotoSuccess,
|
||||
deleteProfilePhotoReset,
|
||||
resetDrafts,
|
||||
} from '../actions/ProfileActions';
|
||||
} from './actions';
|
||||
|
||||
// Selectors
|
||||
import { handleSaveProfileSelector, handleFetchProfileSelector } from '../selectors/ProfilePageSelector';
|
||||
import { handleSaveProfileSelector, handleFetchProfileSelector } from './selectors';
|
||||
|
||||
// Services
|
||||
import * as ProfileApiService from '../services/ProfileApiService';
|
||||
import * as ProfileApiService from './services';
|
||||
|
||||
// Utils
|
||||
import { keepKeys } from '../services/utils';
|
||||
import { utils } from '../common';
|
||||
|
||||
const { keepKeys } = utils;
|
||||
|
||||
export function* handleFetchProfile(action) {
|
||||
const { username } = action.payload;
|
||||
@@ -188,7 +190,7 @@ export function* handleFetchUserAccountFailure(action) {
|
||||
yield put(push('/error'));
|
||||
}
|
||||
|
||||
export default function* rootSaga() {
|
||||
export default function* profileSaga() {
|
||||
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
|
||||
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
|
||||
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
|
||||
@@ -1,10 +1,10 @@
|
||||
import { takeEvery, put, call, delay, select, all } from 'redux-saga/effects';
|
||||
import { FETCH_USER_ACCOUNT_FAILURE } from '@edx/frontend-auth';
|
||||
|
||||
import * as profileActions from '../actions/ProfileActions';
|
||||
import { handleSaveProfileSelector, handleFetchProfileSelector } from '../selectors/ProfilePageSelector';
|
||||
import * as profileActions from './actions';
|
||||
import { handleSaveProfileSelector, handleFetchProfileSelector } from './selectors';
|
||||
|
||||
jest.mock('../services/ProfileApiService', () => ({
|
||||
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));
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
9
src/sagas.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
|
||||
import { profileSaga } from './profile';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
profileSaga(),
|
||||
]);
|
||||
}
|
||||
@@ -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),
|
||||
@@ -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),
|
||||
9
src/store/index.js
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||