diff --git a/src/actions/profile.js b/src/actions/ProfileActions.js
similarity index 78%
rename from src/actions/profile.js
rename to src/actions/ProfileActions.js
index b7538b3..a051936 100644
--- a/src/actions/profile.js
+++ b/src/actions/ProfileActions.js
@@ -1,29 +1,42 @@
import AsyncActionType from './AsyncActionType';
-export const EDITABLE_FIELD_OPEN = 'EDITABLE_FIELD_OPEN';
-export const EDITABLE_FIELD_CLOSE = 'EDITABLE_FIELD_CLOSE';
+export const FIELD_OPEN = 'FIELD_OPEN';
+export const FIELD_CLOSE = 'FIELD_CLOSE';
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE');
export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO');
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
+export const UPDATE_DRAFTS = 'UPDATE_DRAFTS';
+export const RECEIVE_PREFERENCES = 'RECEIVE_PREFERENCES';
-export const openEditableField = fieldName => ({
- type: EDITABLE_FIELD_OPEN,
+
+export const openField = fieldName => ({
+ type: FIELD_OPEN,
fieldName,
});
-export const closeEditableField = fieldName => ({
- type: EDITABLE_FIELD_CLOSE,
+export const closeField = fieldName => ({
+ type: FIELD_CLOSE,
fieldName,
});
+export const updateDrafts = drafts => ({
+ type: UPDATE_DRAFTS,
+ drafts,
+});
+
export const fetchProfileBegin = () => ({
type: FETCH_PROFILE.BEGIN,
});
export const fetchProfileSuccess = profile => ({
type: FETCH_PROFILE.SUCCESS,
- payload: { profile },
+ profile,
+});
+
+export const receivePreferences = preferences => ({
+ type: RECEIVE_PREFERENCES,
+ preferences,
});
export const fetchProfileFailure = error => ({
@@ -57,12 +70,13 @@ export const saveProfileFailure = error => ({
payload: { error },
});
-export const saveProfile = (username, userAccountState, fieldName) => ({
+export const saveProfile = (username, { profileData, preferencesData }, fieldName) => ({
type: SAVE_PROFILE.BASE,
payload: {
fieldName,
username,
- userAccountState,
+ profileData,
+ preferencesData,
},
});
diff --git a/src/actions/profile.test.js b/src/actions/ProfileActions.test.js
similarity index 88%
rename from src/actions/profile.test.js
rename to src/actions/ProfileActions.test.js
index 25b3093..a21917f 100644
--- a/src/actions/profile.test.js
+++ b/src/actions/ProfileActions.test.js
@@ -1,8 +1,8 @@
import {
- openEditableField,
- closeEditableField,
- EDITABLE_FIELD_OPEN,
- EDITABLE_FIELD_CLOSE,
+ openField,
+ closeField,
+ FIELD_OPEN,
+ FIELD_CLOSE,
SAVE_PROFILE,
saveProfileBegin,
saveProfileSuccess,
@@ -21,28 +21,28 @@ import {
deleteProfilePhotoFailure,
deleteProfilePhotoReset,
deleteProfilePhoto,
-} from './profile';
+} from './ProfileActions';
describe('editable field actions', () => {
it('should create an open action', () => {
const expectedAction = {
- type: EDITABLE_FIELD_OPEN,
+ type: FIELD_OPEN,
fieldName: 'name',
};
- expect(openEditableField('name')).toEqual(expectedAction);
+ expect(openField('name')).toEqual(expectedAction);
});
it('should create a closed action', () => {
const expectedAction = {
- type: EDITABLE_FIELD_CLOSE,
+ type: FIELD_CLOSE,
fieldName: 'name',
};
- expect(closeEditableField('name')).toEqual(expectedAction);
+ expect(closeField('name')).toEqual(expectedAction);
});
});
describe('SAVE profile actions', () => {
- const userAccountState = {
+ const profileData = {
username: 'verified',
email: 'verified@example.com',
bio: 'A great bio.',
@@ -51,16 +51,19 @@ describe('SAVE profile actions', () => {
// Good enough for testing / and since we have no factories
};
+ const preferencesData = {};
+
it('should create an action to signal the start of a profile save', () => {
const expectedAction = {
type: SAVE_PROFILE.BASE,
payload: {
username: 'user person',
- userAccountState,
fieldName: 'fullName',
+ profileData,
+ preferencesData,
},
};
- expect(saveProfile('user person', userAccountState, 'fullName')).toEqual(expectedAction);
+ expect(saveProfile('user person', { profileData, preferencesData }, 'fullName')).toEqual(expectedAction);
});
it('should create an action to signal user profile save success', () => {
@@ -188,17 +191,17 @@ describe('Editable field opening and closing actions', () => {
it('should create an action to signal the opening a field', () => {
const expectedAction = {
- type: EDITABLE_FIELD_OPEN,
+ type: FIELD_OPEN,
fieldName,
};
- expect(openEditableField(fieldName)).toEqual(expectedAction);
+ expect(openField(fieldName)).toEqual(expectedAction);
});
it('should create an action to signal the closing a field', () => {
const expectedAction = {
- type: EDITABLE_FIELD_CLOSE,
+ type: FIELD_CLOSE,
fieldName,
};
- expect(closeEditableField(fieldName)).toEqual(expectedAction);
+ expect(closeField(fieldName)).toEqual(expectedAction);
});
});
diff --git a/src/actions/preferences.js b/src/actions/preferences.js
deleted file mode 100644
index fef10e7..0000000
--- a/src/actions/preferences.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import AsyncActionType from './AsyncActionType';
-
-
-export const FETCH_PREFERENCES = new AsyncActionType('PROFILE', 'FETCH_PREFERENCES');
-export const SAVE_PREFERENCES = new AsyncActionType('PROFILE', 'SAVE_PREFERENCES');
-
-export const fetchPreferencesBegin = () => ({
- type: FETCH_PREFERENCES.BEGIN,
-});
-
-export const fetchPreferencesSuccess = preferences => ({
- type: FETCH_PREFERENCES.SUCCESS,
- preferences,
-});
-
-export const fetchPreferencesFailure = error => ({
- type: FETCH_PREFERENCES.FAILURE,
- payload: { error },
-});
-
-export const fetchPreferencesReset = () => ({
- type: FETCH_PREFERENCES.RESET,
-});
-
-export const fetchPreferences = username => ({
- type: FETCH_PREFERENCES.BASE,
- payload: { username },
-});
-
-
-export const savePreferencesBegin = () => ({
- type: SAVE_PREFERENCES.BEGIN,
-});
-
-export const savePreferencesSuccess = () => ({
- type: SAVE_PREFERENCES.SUCCESS,
-});
-
-export const savePreferencesFailure = error => ({
- type: SAVE_PREFERENCES.FAILURE,
- payload: { error },
-});
-
-export const savePreferencesReset = () => ({
- type: SAVE_PREFERENCES.RESET,
-});
-
-export const savePreferences = (username, preferences) => ({
- type: SAVE_PREFERENCES.BASE,
- payload: { username, preferences },
-});
diff --git a/src/analytics.js b/src/analytics.js
index 24a4d57..389ed16 100755
--- a/src/analytics.js
+++ b/src/analytics.js
@@ -1,4 +1,4 @@
-import apiClient from './data/apiClient';
+import apiClient from './config/apiClient';
const handleTrackEvents = (eventName, properties) => {
// Simply forward track events to Segment
diff --git a/src/assets/dot-pattern-light.png b/src/assets/dot-pattern-light.png
deleted file mode 100644
index c84a3c5..0000000
Binary files a/src/assets/dot-pattern-light.png and /dev/null differ
diff --git a/src/components/App.jsx b/src/components/App.jsx
new file mode 100644
index 0000000..3ad047f
--- /dev/null
+++ b/src/components/App.jsx
@@ -0,0 +1,85 @@
+import React, { Component } from 'react';
+import { connect, Provider } from 'react-redux';
+import PropTypes from 'prop-types';
+import { IntlProvider } from 'react-intl';
+import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
+import SiteFooter from '@edx/frontend-component-footer';
+import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
+
+import apiClient from '../config/apiClient';
+import { handleTrackEvents } from '../analytics';
+import { getLocale, getMessages } from '../i18n/i18n-loader';
+import SiteHeader from './/SiteHeader';
+import ConnectedProfilePage from './ProfilePage';
+
+import HeaderLogo from '../../assets/edx-sm.png';
+import FooterLogo from '../../assets/edx-footer.png';
+import NotFoundPage from './NotFoundPage';
+
+class App extends Component {
+ componentDidMount() {
+ const { username } = this.props;
+ const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
+ this.props.fetchUserAccount(userAccountApiService, username);
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+App.propTypes = {
+ fetchUserAccount: PropTypes.func.isRequired,
+ username: PropTypes.string.isRequired,
+ store: PropTypes.object.isRequired, // eslint-disable-line
+};
+
+const mapStateToProps = state => ({
+ username: state.authentication.username,
+});
+
+export default connect(
+ mapStateToProps,
+ {
+ fetchUserAccount,
+ },
+)(App);
diff --git a/src/components/UserProfile/index.jsx b/src/components/ProfilePage.jsx
similarity index 74%
rename from src/components/UserProfile/index.jsx
rename to src/components/ProfilePage.jsx
index 32aa4d1..94d38a5 100644
--- a/src/components/UserProfile/index.jsx
+++ b/src/components/ProfilePage.jsx
@@ -1,16 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Row, Col } from 'reactstrap';
+import { connect } from 'react-redux';
-import ProfileAvatar from './ProfileAvatar';
-import FullName from './FullName';
-import UserLocation from './UserLocation';
-import Education from './Education';
-import SocialLinks from './SocialLinks';
-import Bio from './Bio';
-import MyCertificates from './MyCertificates';
+// Actions
+import {
+ fetchProfile,
+ saveProfile,
+ saveProfilePhoto,
+ deleteProfilePhoto,
+ openField,
+ closeField,
+} from '../actions/ProfileActions';
-class UserProfile extends React.Component {
+// Components
+import ProfileAvatar from './ProfilePage/ProfileAvatar';
+import FullName from './ProfilePage/FullName';
+import UserLocation from './ProfilePage/UserLocation';
+import Education from './ProfilePage/Education';
+import SocialLinks from './ProfilePage/SocialLinks';
+import Bio from './ProfilePage/Bio';
+import MyCertificates from './ProfilePage/MyCertificates';
+
+export class ProfilePage extends React.Component {
constructor(props) {
super(props);
@@ -23,7 +35,6 @@ class UserProfile extends React.Component {
certificates: { value: null, visibility: null },
};
-
this.onCancel = this.onCancel.bind(this);
this.onEdit = this.onEdit.bind(this);
this.onSave = this.onSave.bind(this);
@@ -38,45 +49,32 @@ class UserProfile extends React.Component {
}
onCancel() {
- this.props.closeEditableField(this.props.currentlyEditingField);
+ this.props.closeField(this.props.currentlyEditingField);
}
onEdit(fieldName) {
- this.props.openEditableField(fieldName);
+ this.props.openField(fieldName);
}
- onSave(fieldName, value, visibility) {
+ onSave(fieldName) {
const {
- value: fieldValue,
- visibility: fieldVisibility,
+ value,
+ visibility,
} = this.state[fieldName];
- const valueToSave = value != null ? value : fieldValue;
- const visibilityToSave = visibility != null ? visibility : fieldVisibility;
+ const data = {};
- if (valueToSave != null) {
- this.props.saveProfile(
- this.props.username,
- {
- [fieldName]: valueToSave,
- },
- fieldName,
- );
+ if (value != null) {
+ data.profileData = { [fieldName]: value };
+ }
+ if (visibility != null) {
+ data.preferencesData = { visibility: { [fieldName]: visibility } };
}
- if (visibilityToSave != null) {
- this.props.savePreferences(
- this.props.username,
- {
- visibility: {
- [fieldName]: visibilityToSave,
- },
- },
- );
- }
-
- if (valueToSave == null && visibilityToSave == null) {
+ if (value == null && visibility == null) {
this.onCancel();
+ } else {
+ this.props.saveProfile(this.props.username, data, fieldName);
}
}
@@ -96,6 +94,7 @@ class UserProfile extends React.Component {
},
});
}
+
onVisibilityChange(fieldName, visibility) {
this.setState({
[fieldName]: {
@@ -233,9 +232,7 @@ class UserProfile extends React.Component {
}
}
-export default UserProfile;
-
-UserProfile.propTypes = {
+ProfilePage.propTypes = {
currentlyEditingField: PropTypes.string,
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
@@ -255,13 +252,13 @@ UserProfile.propTypes = {
certificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
+
fetchProfile: PropTypes.func.isRequired,
saveProfile: PropTypes.func.isRequired,
saveProfilePhoto: PropTypes.func.isRequired,
deleteProfilePhoto: PropTypes.func.isRequired,
- openEditableField: PropTypes.func.isRequired,
- closeEditableField: PropTypes.func.isRequired,
- savePreferences: PropTypes.func.isRequired,
+ openField: PropTypes.func.isRequired,
+ closeField: PropTypes.func.isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
username: PropTypes.string.isRequired,
@@ -271,7 +268,7 @@ UserProfile.propTypes = {
visibility: PropTypes.object, // eslint-disable-line
};
-UserProfile.defaultProps = {
+ProfilePage.defaultProps = {
currentlyEditingField: null,
saveState: null,
savePhotoState: null,
@@ -288,3 +285,39 @@ UserProfile.defaultProps = {
accountPrivacy: null,
visibility: {}, // eslint-disable-line
};
+
+const mapStateToProps = (state) => {
+ const profileImage =
+ state.profilePage.profile.profileImage != null
+ ? state.profilePage.profile.profileImage.imageUrlLarge
+ : null;
+ return {
+ isCurrentUserProfile: state.userAccount.username === state.profilePage.profile.username,
+ currentlyEditingField: state.profilePage.currentlyEditingField,
+ saveState: state.profilePage.saveState,
+ savePhotoState: state.profilePage.savePhotoState,
+ error: state.profilePage.error,
+ profileImage,
+ fullName: state.profilePage.profile.name,
+ username: state.profilePage.profile.username,
+ userLocation: state.profilePage.profile.country,
+ education: state.profilePage.profile.levelOfEducation,
+ socialLinks: state.profilePage.profile.socialLinks,
+ bio: state.profilePage.profile.bio,
+ certificates: state.profilePage.profile.certificates,
+ accountPrivacy: state.profilePage.preferences.accountPrivacy,
+ visibility: state.profilePage.preferences.visibility || {},
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ {
+ fetchProfile,
+ saveProfile,
+ saveProfilePhoto,
+ deleteProfilePhoto,
+ openField,
+ closeField,
+ },
+)(ProfilePage);
diff --git a/src/components/UserProfile/Bio.jsx b/src/components/ProfilePage/Bio.jsx
similarity index 100%
rename from src/components/UserProfile/Bio.jsx
rename to src/components/ProfilePage/Bio.jsx
diff --git a/src/components/UserProfile/Education.jsx b/src/components/ProfilePage/Education.jsx
similarity index 100%
rename from src/components/UserProfile/Education.jsx
rename to src/components/ProfilePage/Education.jsx
diff --git a/src/components/UserProfile/FullName.jsx b/src/components/ProfilePage/FullName.jsx
similarity index 100%
rename from src/components/UserProfile/FullName.jsx
rename to src/components/ProfilePage/FullName.jsx
diff --git a/src/components/UserProfile/MyCertificates.jsx b/src/components/ProfilePage/MyCertificates.jsx
similarity index 100%
rename from src/components/UserProfile/MyCertificates.jsx
rename to src/components/ProfilePage/MyCertificates.jsx
diff --git a/src/components/UserProfile/ProfileAvatar.jsx b/src/components/ProfilePage/ProfileAvatar.jsx
similarity index 100%
rename from src/components/UserProfile/ProfileAvatar.jsx
rename to src/components/ProfilePage/ProfileAvatar.jsx
diff --git a/src/components/UserProfile/SocialLinks.jsx b/src/components/ProfilePage/SocialLinks.jsx
similarity index 100%
rename from src/components/UserProfile/SocialLinks.jsx
rename to src/components/ProfilePage/SocialLinks.jsx
diff --git a/src/components/UserProfile/UserLocation.jsx b/src/components/ProfilePage/UserLocation.jsx
similarity index 100%
rename from src/components/UserProfile/UserLocation.jsx
rename to src/components/ProfilePage/UserLocation.jsx
diff --git a/src/components/UserProfile/elements/AsyncActionButton.jsx b/src/components/ProfilePage/elements/AsyncActionButton.jsx
similarity index 100%
rename from src/components/UserProfile/elements/AsyncActionButton.jsx
rename to src/components/ProfilePage/elements/AsyncActionButton.jsx
diff --git a/src/components/UserProfile/elements/EditButton.jsx b/src/components/ProfilePage/elements/EditButton.jsx
similarity index 100%
rename from src/components/UserProfile/elements/EditButton.jsx
rename to src/components/ProfilePage/elements/EditButton.jsx
diff --git a/src/components/UserProfile/elements/EditControls.jsx b/src/components/ProfilePage/elements/EditControls.jsx
similarity index 100%
rename from src/components/UserProfile/elements/EditControls.jsx
rename to src/components/ProfilePage/elements/EditControls.jsx
diff --git a/src/components/UserProfile/elements/EditableItemHeader.jsx b/src/components/ProfilePage/elements/EditableItemHeader.jsx
similarity index 100%
rename from src/components/UserProfile/elements/EditableItemHeader.jsx
rename to src/components/ProfilePage/elements/EditableItemHeader.jsx
diff --git a/src/components/UserProfile/elements/EmptyContent.jsx b/src/components/ProfilePage/elements/EmptyContent.jsx
similarity index 100%
rename from src/components/UserProfile/elements/EmptyContent.jsx
rename to src/components/ProfilePage/elements/EmptyContent.jsx
diff --git a/src/components/UserProfile/elements/SwitchContent.jsx b/src/components/ProfilePage/elements/SwitchContent.jsx
similarity index 100%
rename from src/components/UserProfile/elements/SwitchContent.jsx
rename to src/components/ProfilePage/elements/SwitchContent.jsx
diff --git a/src/components/UserProfile/elements/TransitionReplace.jsx b/src/components/ProfilePage/elements/TransitionReplace.jsx
similarity index 100%
rename from src/components/UserProfile/elements/TransitionReplace.jsx
rename to src/components/ProfilePage/elements/TransitionReplace.jsx
diff --git a/src/components/UserProfile/elements/Visibility.jsx b/src/components/ProfilePage/elements/Visibility.jsx
similarity index 100%
rename from src/components/UserProfile/elements/Visibility.jsx
rename to src/components/ProfilePage/elements/Visibility.jsx
diff --git a/src/containers/SiteHeader/index.jsx b/src/components/SiteHeader.jsx
similarity index 100%
rename from src/containers/SiteHeader/index.jsx
rename to src/components/SiteHeader.jsx
diff --git a/src/data/apiClient.js b/src/config/apiClient.js
similarity index 92%
rename from src/data/apiClient.js
rename to src/config/apiClient.js
index 4af0b6a..1a145f9 100644
--- a/src/data/apiClient.js
+++ b/src/config/apiClient.js
@@ -1,7 +1,6 @@
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
-import { configuration } from '../config';
-
+import { configuration } from './environment';
const apiClient = getAuthenticatedAPIClient({
appBaseUrl: configuration.BASE_URL,
@@ -14,5 +13,4 @@ const apiClient = getAuthenticatedAPIClient({
csrfCookieName: configuration.CSRF_COOKIE_NAME,
});
-
export default apiClient;
diff --git a/src/config/configureStore.dev.js b/src/config/configureStore.dev.js
new file mode 100644
index 0000000..790b605
--- /dev/null
+++ b/src/config/configureStore.dev.js
@@ -0,0 +1,25 @@
+import { applyMiddleware, createStore } from 'redux';
+import createSagaMiddleware from 'redux-saga';
+import thunkMiddleware from 'redux-thunk';
+import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
+import { createLogger } from 'redux-logger';
+
+import apiClient from './apiClient';
+import reducers from '../reducers/RootReducer';
+import rootSaga from '../sagas/RootSaga';
+
+export default function configureStore() {
+ const loggerMiddleware = createLogger();
+ const sagaMiddleware = createSagaMiddleware();
+ const initialState = apiClient.getAuthenticationState();
+
+ const store = createStore(
+ reducers,
+ initialState,
+ composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)),
+ );
+
+ sagaMiddleware.run(rootSaga);
+
+ return store;
+}
diff --git a/src/config/configureStore.js b/src/config/configureStore.js
new file mode 100644
index 0000000..d867a3c
--- /dev/null
+++ b/src/config/configureStore.js
@@ -0,0 +1,9 @@
+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/config/configureStore.prod.js b/src/config/configureStore.prod.js
new file mode 100644
index 0000000..4b85902
--- /dev/null
+++ b/src/config/configureStore.prod.js
@@ -0,0 +1,22 @@
+import { applyMiddleware, createStore } from 'redux';
+import createSagaMiddleware from 'redux-saga';
+import thunkMiddleware from 'redux-thunk';
+
+import apiClient from './apiClient';
+import reducers from '../reducers/RootReducer';
+import rootSaga from '../sagas/RootSaga';
+
+export default function configureStore() {
+ const sagaMiddleware = createSagaMiddleware();
+ const initialState = apiClient.getAuthenticationState();
+
+ const store = createStore(
+ reducers,
+ initialState,
+ applyMiddleware(thunkMiddleware, sagaMiddleware),
+ );
+
+ sagaMiddleware.run(rootSaga);
+
+ return store;
+}
diff --git a/src/config/index.js b/src/config/environment.js
similarity index 85%
rename from src/config/index.js
rename to src/config/environment.js
index 7dd296c..e43e226 100644
--- a/src/config/index.js
+++ b/src/config/environment.js
@@ -1,4 +1,4 @@
-const configuration = {
+export const configuration = {
BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
@@ -10,8 +10,7 @@ const configuration = {
SEGMENT_KEY: process.env.SEGMENT_KEY,
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
CSRF_COOKIE_NAME: process.env.CSRF_COOKIE_NAME,
+ ENVIRONMENT: process.env.NODE_ENV,
};
-const features = {};
-
-export { configuration, features };
+export const features = {};
diff --git a/src/containers/UserProfile/index.jsx b/src/containers/UserProfile/index.jsx
deleted file mode 100644
index e118104..0000000
--- a/src/containers/UserProfile/index.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { connect } from 'react-redux';
-
-import UserProfile from '../../components/UserProfile';
-import {
- fetchProfile,
- saveProfile,
- saveProfilePhoto,
- deleteProfilePhoto,
- openEditableField,
- closeEditableField,
-} from '../../actions/profile';
-import { savePreferences } from '../../actions/preferences';
-
-const mapStateToProps = (state) => {
- const profileImage =
- state.profilePage.profile.profileImage != null
- ? state.profilePage.profile.profileImage.imageUrlLarge
- : null;
- return {
- isCurrentUserProfile: state.userAccount.username === state.profilePage.profile.username,
- currentlyEditingField: state.profilePage.currentlyEditingField,
- saveState: state.profilePage.saveState,
- savePhotoState: state.profilePage.savePhotoState,
- error: state.profilePage.error,
- profileImage,
- fullName: state.profilePage.profile.name,
- username: state.profilePage.profile.username,
- userLocation: state.profilePage.profile.country,
- education: state.profilePage.profile.levelOfEducation,
- socialLinks: state.profilePage.profile.socialLinks,
- bio: state.profilePage.profile.bio,
- certificates: state.profilePage.profile.certificates,
- accountPrivacy: state.profilePage.preferences.accountPrivacy,
- visibility: state.profilePage.preferences.visibility || {},
- };
-};
-
-export default connect(
- mapStateToProps,
- {
- fetchProfile,
- saveProfile,
- saveProfilePhoto,
- deleteProfilePhoto,
- openEditableField,
- closeEditableField,
- savePreferences,
- },
-)(UserProfile);
diff --git a/src/data/store.js b/src/data/store.js
deleted file mode 100755
index 43523b0..0000000
--- a/src/data/store.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { applyMiddleware, createStore } from 'redux';
-import createSagaMiddleware from 'redux-saga';
-import thunkMiddleware from 'redux-thunk';
-import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
-import { createLogger } from 'redux-logger';
-
-import apiClient from './apiClient';
-import reducers from './reducers/RootReducer';
-import rootSaga from '../sagas/RootSaga';
-
-const loggerMiddleware = createLogger();
-const sagaMiddleware = createSagaMiddleware();
-const initialState = apiClient.getAuthenticationState();
-const store = createStore(
- reducers,
- initialState,
- composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)),
-);
-
-sagaMiddleware.run(rootSaga);
-
-export default store;
diff --git a/src/index.jsx b/src/index.jsx
index b30686e..3106a48 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,80 +1,20 @@
import 'babel-polyfill';
-import React, { Component } from 'react';
+import React from 'react';
import ReactDOM from 'react-dom';
-import { IntlProvider } from 'react-intl';
-import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
-import { Provider } from 'react-redux';
-import SiteFooter from '@edx/frontend-component-footer';
-import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
-import { getLocale, getMessages } from './i18n/i18n-loader';
-import apiClient from './data/apiClient';
-import { handleTrackEvents, identifyUser } from './analytics';
-import SiteHeader from './containers/SiteHeader';
-import UserProfile from './containers/UserProfile';
-import store from './data/store';
-import HeaderLogo from '../assets/edx-sm.png';
-import FooterLogo from '../assets/edx-footer.png';
-import './App.scss';
-import NotFoundPage from './components/NotFoundPage';
+import configureStore from './config/configureStore';
+import apiClient from './config/apiClient';
+import { identifyUser } from './analytics';
-import { fetchPreferences } from './actions/preferences';
+import './index.scss';
-class App extends Component {
- componentDidMount() {
- const { username } = store.getState().authentication;
- const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
- store.dispatch(fetchUserAccount(userAccountApiService, username));
- store.dispatch(fetchPreferences(username));
- }
-
- render() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
+import App from './components/App';
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
- ReactDOM.render(, document.getElementById('root'));
+ const store = configureStore();
+
+ ReactDOM.render(, document.getElementById('root'));
// identify user for future analytics calls
// TODO: Call before each page call.
diff --git a/src/App.scss b/src/index.scss
similarity index 96%
rename from src/App.scss
rename to src/index.scss
index bd7f0a7..f97a417 100755
--- a/src/App.scss
+++ b/src/index.scss
@@ -22,7 +22,7 @@ $fa-font-path: "~font-awesome/fonts";
opacity: 0;
width: 0;
}
-
+
.icon {
font-size: 1.125rem;
}
@@ -40,7 +40,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%;
}
diff --git a/src/data/reducers/ProfilePageReducer.js b/src/reducers/ProfilePageReducer.js
similarity index 54%
rename from src/data/reducers/ProfilePageReducer.js
rename to src/reducers/ProfilePageReducer.js
index e439284..a35e205 100644
--- a/src/data/reducers/ProfilePageReducer.js
+++ b/src/reducers/ProfilePageReducer.js
@@ -4,46 +4,30 @@ import {
SAVE_PROFILE,
SAVE_PROFILE_PHOTO,
DELETE_PROFILE_PHOTO,
- EDITABLE_FIELD_CLOSE,
- EDITABLE_FIELD_OPEN,
+ FIELD_CLOSE,
+ FIELD_OPEN,
FETCH_PROFILE,
-} from '../../actions/profile';
+ RECEIVE_PREFERENCES,
-import {
- FETCH_PREFERENCES,
- SAVE_PREFERENCES,
-} from '../../actions/preferences';
+} from '../actions/ProfileActions';
const initialState = {
error: null,
saveState: null,
savePhotoState: null,
- savePreferencesState: null,
- saveProfileState: null,
currentlyEditingField: null,
profile: {},
preferences: {},
};
-// This function returns state based on priority:
-// if any are pending > the state is pending
-// then, if any are errors > the state is error
-// then, if any are complete > the state is complete
-// else null
-const mergeSaveStates = (statesToMerge) => {
- const statePriority = ['pending', 'error', 'complete', null];
- statesToMerge.sort((a, b) => statePriority.indexOf(a) - statePriority.indexOf(b));
- return statesToMerge[0];
-};
-
const profilePage = (state = initialState, action) => {
switch (action.type) {
- case EDITABLE_FIELD_OPEN:
+ case FIELD_OPEN:
return {
...state,
currentlyEditingField: action.fieldName,
};
- case EDITABLE_FIELD_CLOSE:
+ case FIELD_CLOSE:
// Only close if the field to close is undefined or matches the field that is currently open
if (action.fieldName === state.currentlyEditingField) {
return {
@@ -53,71 +37,40 @@ const profilePage = (state = initialState, action) => {
}
return state;
- case FETCH_PREFERENCES.SUCCESS:
+ case FETCH_PROFILE.SUCCESS:
+ return {
+ ...state,
+ profile: action.profile,
+ };
+
+ case RECEIVE_PREFERENCES:
return {
...state,
preferences: defaultsDeep({}, action.preferences, state.preferences),
};
- case SAVE_PREFERENCES.BEGIN:
- return {
- ...state,
- savePreferencesState: 'pending',
- saveState: mergeSaveStates(['pending', state.saveProfileState]),
- };
- case SAVE_PREFERENCES.SUCCESS:
- // defaults deep used because our preferences/state object is multi-dimensional
- return {
- ...state,
- savePreferencesState: 'complete',
- saveState: mergeSaveStates(['complete', state.saveProfileState]),
- };
- case SAVE_PREFERENCES.FAILURE:
- return {
- ...state,
- savePreferencesState: 'error',
- saveState: mergeSaveStates(['error', state.saveProfileState]),
- };
- case SAVE_PREFERENCES.RESET:
- return {
- ...state,
- savePreferencesState: null,
- saveState: mergeSaveStates([null, state.saveProfileState]),
- error: null,
- };
-
- case FETCH_PROFILE.SUCCESS:
- return {
- ...state,
- profile: action.payload.profile,
- };
-
case SAVE_PROFILE.BEGIN:
return {
...state,
- saveProfileState: 'pending',
- saveState: mergeSaveStates(['pending', state.savePreferencesState]),
+ saveState: 'pending',
error: null,
};
case SAVE_PROFILE.SUCCESS:
return {
...state,
- saveProfileState: 'complete',
- saveState: mergeSaveStates(['complete', state.savePreferencesState]),
+ saveState: 'complete',
error: null,
};
case SAVE_PROFILE.FAILURE:
return {
...state,
- saveProfileState: 'error',
- saveState: mergeSaveStates(['error', state.savePreferencesState]),
+ saveState: 'error',
error: action.payload.error,
};
case SAVE_PROFILE.RESET:
return {
...state,
- saveProfileState: null,
- saveState: mergeSaveStates([null, state.savePreferencesState]),
+ saveState: null,
error: null,
};
diff --git a/src/data/reducers/RootReducer.js b/src/reducers/RootReducer.js
similarity index 99%
rename from src/data/reducers/RootReducer.js
rename to src/reducers/RootReducer.js
index f7365b9..bca1391 100755
--- a/src/data/reducers/RootReducer.js
+++ b/src/reducers/RootReducer.js
@@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
+
import profilePage from './ProfilePageReducer';
const identityReducer = (state) => {
diff --git a/src/sagas/RootSaga.js b/src/sagas/RootSaga.js
index 808ec99..75211f1 100644
--- a/src/sagas/RootSaga.js
+++ b/src/sagas/RootSaga.js
@@ -4,6 +4,7 @@ import {
FETCH_PROFILE,
fetchProfileBegin,
fetchProfileSuccess,
+ receivePreferences,
fetchProfileFailure,
fetchProfileReset,
fetchProfile as fetchProfileAction,
@@ -12,7 +13,7 @@ import {
saveProfileSuccess,
saveProfileFailure,
saveProfileReset,
- closeEditableField,
+ closeField,
SAVE_PROFILE_PHOTO,
saveProfilePhotoBegin,
saveProfilePhotoSuccess,
@@ -23,20 +24,7 @@ import {
deleteProfilePhotoSuccess,
deleteProfilePhotoFailure,
deleteProfilePhotoReset,
-} from '../actions/profile';
-
-import {
- FETCH_PREFERENCES,
- fetchPreferencesBegin,
- fetchPreferencesSuccess,
- fetchPreferencesFailure,
- fetchPreferencesReset,
- SAVE_PREFERENCES,
- savePreferencesBegin,
- savePreferencesSuccess,
- savePreferencesFailure,
- savePreferencesReset,
-} from '../actions/preferences';
+} from '../actions/ProfileActions';
import * as ProfileApiService from '../services/ProfileApiService';
@@ -61,22 +49,27 @@ export const mapDataForRequest = (props) => {
return state;
};
-
export function* handleFetchProfile(action) {
const { username } = action.payload;
try {
yield put(fetchProfileBegin());
+
const profile = yield call(
ProfileApiService.getProfile,
username,
);
+ const preferences = yield call(
+ ProfileApiService.getPreferences,
+ username,
+ );
profile.certificates = yield call(
ProfileApiService.getCourseCertificates,
username,
);
yield put(fetchProfileSuccess(profile));
+ yield put(receivePreferences(preferences));
yield put(fetchProfileReset());
} catch (e) {
yield put(fetchProfileFailure(e.message));
@@ -84,19 +77,38 @@ export function* handleFetchProfile(action) {
}
export function* handleSaveProfile(action) {
- const { username, userAccountState } = action.payload;
+ const { username, profileData, preferencesData } = action.payload;
+
try {
yield put(saveProfileBegin());
- const profile = yield call(
- ProfileApiService.patchProfile,
- username,
- userAccountState,
- );
+ const responseData = {};
+
+ if (profileData != null) {
+ responseData.profile = yield call(
+ ProfileApiService.patchProfile,
+ username,
+ profileData,
+ );
+ }
+ if (preferencesData != null) {
+ responseData.preferences = yield call(
+ ProfileApiService.patchPreferences,
+ username,
+ preferencesData,
+ );
+ }
+
+ const { profile, preferences } = responseData;
yield put(saveProfileSuccess());
- yield put(fetchProfileSuccess(profile));
+ if (profile != null) {
+ yield put(fetchProfileSuccess(profile));
+ }
+ if (preferences != null) {
+ yield put(receivePreferences(preferences));
+ }
yield delay(300);
- yield put(closeEditableField(action.payload.fieldName));
+ yield put(closeField(action.payload.fieldName));
yield delay(300);
yield put(saveProfileReset());
} catch (e) {
@@ -138,37 +150,9 @@ export function* handleDeleteProfilePhoto(action) {
}
}
-export function* handleFetchPreferences(action) {
- const { username } = action.payload;
- try {
- yield put(fetchPreferencesBegin());
- const userPreferences = yield call(ProfileApiService.getPreferences, username);
- yield put(fetchPreferencesSuccess(userPreferences));
- yield put(fetchPreferencesReset());
- } catch (e) {
- yield put(fetchPreferencesFailure(e));
- }
-}
-
-export function* handleSavePreferences(action) {
- const { username, preferences: preferencesToSave } = action.payload;
- try {
- yield put(savePreferencesBegin());
- const preferences = yield call(ProfileApiService.postPreferences, username, preferencesToSave);
- yield put(savePreferencesSuccess());
- yield put(fetchPreferencesSuccess(preferences));
- yield put(savePreferencesReset());
- } catch (e) {
- yield put(savePreferencesFailure(e));
- }
-}
-
-
export default function* rootSaga() {
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
- yield takeEvery(FETCH_PREFERENCES.BASE, handleFetchPreferences);
- yield takeEvery(SAVE_PREFERENCES.BASE, handleSavePreferences);
}
diff --git a/src/sagas/RootSaga.test.js b/src/sagas/RootSaga.test.js
index bf72f5e..fac245a 100644
--- a/src/sagas/RootSaga.test.js
+++ b/src/sagas/RootSaga.test.js
@@ -1,7 +1,6 @@
import { takeEvery, put, call, delay } from 'redux-saga/effects';
-import * as profileActions from '../actions/profile';
-import * as preferencesActions from '../actions/preferences';
+import * as profileActions from '../actions/ProfileActions';
jest.mock('../services/ProfileApiService', () => ({
getProfile: jest.fn(),
@@ -18,8 +17,6 @@ import rootSaga, {
handleSaveProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
- handleFetchPreferences,
- handleSavePreferences,
} from './RootSaga';
import * as ProfileApiService from '../services/ProfileApiService';
/* eslint-enable import/first */
@@ -29,30 +26,10 @@ describe('RootSaga', () => {
it('should pass actions to the correct sagas', () => {
const gen = rootSaga();
- expect(gen.next().value).toEqual(takeEvery(
- profileActions.FETCH_PROFILE.BASE,
- handleFetchProfile,
- ));
- expect(gen.next().value).toEqual(takeEvery(
- profileActions.SAVE_PROFILE.BASE,
- handleSaveProfile,
- ));
- expect(gen.next().value).toEqual(takeEvery(
- profileActions.SAVE_PROFILE_PHOTO.BASE,
- handleSaveProfilePhoto,
- ));
- expect(gen.next().value).toEqual(takeEvery(
- profileActions.DELETE_PROFILE_PHOTO.BASE,
- handleDeleteProfilePhoto,
- ));
- expect(gen.next().value).toEqual(takeEvery(
- preferencesActions.FETCH_PREFERENCES.BASE,
- handleFetchPreferences,
- ));
- expect(gen.next().value).toEqual(takeEvery(
- preferencesActions.SAVE_PREFERENCES.BASE,
- handleSavePreferences,
- ));
+ expect(gen.next().value).toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile)); // eslint-disable-line
+ expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile)); // eslint-disable-line
+ expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto)); // eslint-disable-line
+ expect(gen.next().value).toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto)); // eslint-disable-line
expect(gen.next().value).toBeUndefined();
});
@@ -63,8 +40,11 @@ describe('RootSaga', () => {
const action = profileActions.saveProfile(
'my username',
{
- fullName: 'Full Name',
- education: 'b',
+ profileData: {
+ fullName: 'Full Name',
+ education: 'b',
+ },
+ preferencesData: null,
},
'ze field',
);
@@ -74,13 +54,13 @@ describe('RootSaga', () => {
levelOfEducation: 'b',
};
expect(gen.next().value).toEqual(put(profileActions.saveProfileBegin()));
- expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', action.payload.userAccountState));
+ expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', action.payload.profileData));
// The library would supply the result of the above call
// as the parameter to the NEXT yield. Here:
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess()));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileSuccess(profile)));
expect(gen.next().value).toEqual(delay(300));
- expect(gen.next().value).toEqual(put(profileActions.closeEditableField('ze field')));
+ expect(gen.next().value).toEqual(put(profileActions.closeField('ze field')));
expect(gen.next().value).toEqual(delay(300));
expect(gen.next().value).toEqual(put(profileActions.saveProfileReset()));
expect(gen.next().value).toBeUndefined();
diff --git a/src/segment.js b/src/segment.js
index 1206ed6..2dfb2e9 100644
--- a/src/segment.js
+++ b/src/segment.js
@@ -1,7 +1,7 @@
// The code in this file is from Segment's website, with the following update:
// - Pulls the segment key from configuration.
// https://segment.com/docs/sources/website/analytics.js/quickstart/
-import { configuration } from './config';
+import { configuration } from './config/environment';
(function(){
diff --git a/src/services/ProfileApiService.js b/src/services/ProfileApiService.js
index f9831fd..b7e0d18 100644
--- a/src/services/ProfileApiService.js
+++ b/src/services/ProfileApiService.js
@@ -1,8 +1,8 @@
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';
-import apiClient from '../data/apiClient';
-import { configuration } from '../config';
+import apiClient from '../config/apiClient';
+import { configuration } from '../config/environment';
import { unflattenAndTransformKeys, flattenAndTransformKeys } from './utils';
const accountsApiBaseUrl = `${configuration.LMS_BASE_URL}/api/user/v1/accounts`;
@@ -18,6 +18,7 @@ const clientServerKeyMap = {
dateJoined: 'date_joined',
languageProficiencies: 'language_proficiencies',
accountPrivacy: 'account_privacy',
+ userLocation: 'user_location',
};
const serverClientKeyMap = Object.entries(clientServerKeyMap).reduce((acc, [key, value]) => {
acc[value] = key;
@@ -106,7 +107,7 @@ export function getPreferences(username) {
});
}
-export function postPreferences(username, preferences) {
+export function patchPreferences(username, preferences) {
const url = `${preferencesApiBaseUrl}/${username}`;
// Flatten object for server