diff --git a/package-lock.json b/package-lock.json
index c699112..575f443 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2437,7 +2437,7 @@
}
},
"@edx/edx-bootstrap": {
- "version": "git://github.com/edx/edx-bootstrap.git#e14bb7b678037a675e26c1b196f800e0f573f22e",
+ "version": "git://github.com/edx/edx-bootstrap.git#71fd3272d235eb133cccefc1ce63f35a7696bf28",
"from": "git://github.com/edx/edx-bootstrap.git#update-with-documentation-site",
"requires": {
"@fortawesome/fontawesome-svg-core": "^1.2.13",
@@ -6109,6 +6109,15 @@
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
"dev": true
},
+ "connected-react-router": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-5.0.1.tgz",
+ "integrity": "sha512-0QwWYPRGZQ7f284lmqc5kwC4T3iW3zrAH3zzi6uUMzTOxbA+mn38tAgMOoVo9m3pbskvONFtXiajgVkCElE9EQ==",
+ "requires": {
+ "immutable": "^3.8.1",
+ "seamless-immutable": "^7.1.3"
+ }
+ },
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
@@ -11263,6 +11272,11 @@
"is-cwebp-readable": "^2.0.1"
}
},
+ "immutable": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+ "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+ },
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -17134,14 +17148,25 @@
}
},
"react": {
- "version": "16.8.0",
- "resolved": "https://registry.npmjs.org/react/-/react-16.8.0.tgz",
- "integrity": "sha512-g+nikW2D48kqgWSPwNo0NH9tIGG3DsQFlrtrQ1kj6W77z5ahyIHG0w8kPpz4Sdj6gyLnz0lEd/xsjOoGge2MYQ==",
+ "version": "16.8.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz",
+ "integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.13.0"
+ "scheduler": "^0.13.3"
+ },
+ "dependencies": {
+ "scheduler": {
+ "version": "0.13.3",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz",
+ "integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ }
}
},
"react-dev-utils": {
@@ -17362,14 +17387,25 @@
}
},
"react-dom": {
- "version": "16.8.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.0.tgz",
- "integrity": "sha512-dBzoAGYZpW9Yggp+CzBPC7q1HmWSeRc93DWrwbskmG1eHJWznZB/p0l/Sm+69leIGUS91AXPB/qB3WcPnKx8Sw==",
+ "version": "16.8.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz",
+ "integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.13.0"
+ "scheduler": "^0.13.3"
+ },
+ "dependencies": {
+ "scheduler": {
+ "version": "0.13.3",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz",
+ "integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ }
}
},
"react-element-proptypes": {
@@ -17495,16 +17531,6 @@
"prop-types": "^15.6.0"
}
},
- "react-router-redux": {
- "version": "5.0.0-alpha.9",
- "resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-5.0.0-alpha.9.tgz",
- "integrity": "sha512-euSgNIANnRXr4GydIuwA7RZCefrLQzIw5WdXspS8NPYbV+FxrKSS9MKG7U9vb6vsKHONnA4VxrVNWfnMUnUQAw==",
- "requires": {
- "history": "^4.7.2",
- "prop-types": "^15.6.0",
- "react-router": "^4.2.0"
- }
- },
"react-test-renderer": {
"version": "16.8.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.0.tgz",
@@ -18507,6 +18533,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
+ "reselect": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
+ "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
+ },
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
@@ -19198,6 +19229,7 @@
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.0.tgz",
"integrity": "sha512-w7aJnV30jc7OsiZQNPVmBc+HooZuvQZIZIShKutC3tnMFMkcwVN9CZRRSSNw03OnSCKmEkK8usmwcw6dqBaLzw==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -19242,6 +19274,11 @@
}
}
},
+ "seamless-immutable": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz",
+ "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A=="
+ },
"seek-bzip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
diff --git a/package.json b/package.json
index f0bbebb..ce221c3 100755
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"bootstrap": "^4.2.1",
"camelcase-keys": "^5.0.0",
"classnames": "^2.2.5",
+ "connected-react-router": "^5.0.1",
"copy-webpack-plugin": "^4.6.0",
"email-prop-type": "^1.1.5",
"font-awesome": "^4.7.0",
@@ -48,13 +49,12 @@
"lodash.set": "^4.3.2",
"prop-types": "^15.5.10",
"query-string": "^5.1.1",
- "react": "^16.2.0",
- "react-dom": "^16.2.0",
+ "react": "^16.8.3",
+ "react-dom": "^16.8.3",
"react-intl": "^2.8.0",
- "react-redux": "^5.0.7",
+ "react-redux": "^5.1.1",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
- "react-router-redux": "^5.0.0-alpha.9",
"react-transition-group": "^2.5.3",
"reactstrap": "^7.1.0",
"redux": "^4.0.1",
@@ -62,6 +62,7 @@
"redux-logger": "^3.0.6",
"redux-saga": "^1.0.1",
"redux-thunk": "^2.2.0",
+ "reselect": "^4.0.0",
"snakecase-keys": "^2.1.0",
"whatwg-fetch": "^2.0.3"
},
diff --git a/src/actions/ProfileActions.js b/src/actions/ProfileActions.js
index a051936..afa4b23 100644
--- a/src/actions/ProfileActions.js
+++ b/src/actions/ProfileActions.js
@@ -1,42 +1,31 @@
import AsyncActionType from './AsyncActionType';
-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 OPEN_FORM = 'OPEN_FORM';
+export const CLOSE_FORM = 'CLOSE_FORM';
+export const UPDATE_ACCOUNT_DRAFT = 'UPDATE_ACCOUNT_DRAFT';
+export const UPDATE_VISIBILITY_DRAFT = 'UPDATE_VISIBILITY_DRAFT';
+export const RESET_DRAFTS = 'RESET_DRAFTS';
+// FETCH PROFILE ACTIONS
-export const openField = fieldName => ({
- type: FIELD_OPEN,
- fieldName,
-});
-
-export const closeField = fieldName => ({
- type: FIELD_CLOSE,
- fieldName,
-});
-
-export const updateDrafts = drafts => ({
- type: UPDATE_DRAFTS,
- drafts,
+export const fetchProfile = username => ({
+ type: FETCH_PROFILE.BASE,
+ payload: { username },
});
export const fetchProfileBegin = () => ({
type: FETCH_PROFILE.BEGIN,
});
-export const fetchProfileSuccess = profile => ({
+export const fetchProfileSuccess = (account, preferences, certificates) => ({
type: FETCH_PROFILE.SUCCESS,
- profile,
-});
-
-export const receivePreferences = preferences => ({
- type: RECEIVE_PREFERENCES,
+ account,
preferences,
+ certificates,
});
export const fetchProfileFailure = error => ({
@@ -48,17 +37,25 @@ export const fetchProfileReset = () => ({
type: FETCH_PROFILE.RESET,
});
-export const fetchProfile = username => ({
- type: FETCH_PROFILE.BASE,
- payload: { username },
+// SAVE PROFILE ACTIONS
+
+export const saveProfile = formId => ({
+ type: SAVE_PROFILE.BASE,
+ payload: {
+ formId,
+ },
});
export const saveProfileBegin = () => ({
type: SAVE_PROFILE.BEGIN,
});
-export const saveProfileSuccess = () => ({
+export const saveProfileSuccess = (account, preferences) => ({
type: SAVE_PROFILE.SUCCESS,
+ payload: {
+ account,
+ preferences,
+ },
});
export const saveProfileReset = () => ({
@@ -70,13 +67,13 @@ export const saveProfileFailure = error => ({
payload: { error },
});
-export const saveProfile = (username, { profileData, preferencesData }, fieldName) => ({
- type: SAVE_PROFILE.BASE,
+// SAVE PROFILE PHOTO ACTIONS
+
+export const saveProfilePhoto = (username, formData) => ({
+ type: SAVE_PROFILE_PHOTO.BASE,
payload: {
- fieldName,
username,
- profileData,
- preferencesData,
+ formData,
},
});
@@ -97,11 +94,12 @@ export const saveProfilePhotoFailure = error => ({
payload: { error },
});
-export const saveProfilePhoto = (username, formData) => ({
- type: SAVE_PROFILE_PHOTO.BASE,
+// DELETE PROFILE PHOTO ACTIONS
+
+export const deleteProfilePhoto = username => ({
+ type: DELETE_PROFILE_PHOTO.BASE,
payload: {
username,
- formData,
},
});
@@ -122,9 +120,40 @@ export const deleteProfilePhotoFailure = error => ({
payload: { error },
});
-export const deleteProfilePhoto = username => ({
- type: DELETE_PROFILE_PHOTO.BASE,
+// FIELD STATE ACTIONS
+
+export const openForm = formId => ({
+ type: OPEN_FORM,
payload: {
- username,
+ formId,
},
});
+
+export const closeForm = formId => ({
+ type: CLOSE_FORM,
+ payload: {
+ formId,
+ },
+});
+
+// FORM STATE ACTIONS
+
+export const updateAccountDraft = (name, value) => ({
+ type: UPDATE_ACCOUNT_DRAFT,
+ payload: {
+ name,
+ value,
+ },
+});
+
+export const updateVisibilityDraft = (name, value) => ({
+ type: UPDATE_VISIBILITY_DRAFT,
+ payload: {
+ name,
+ value,
+ },
+});
+
+export const resetDrafts = () => ({
+ type: RESET_DRAFTS,
+});
diff --git a/src/actions/ProfileActions.test.js b/src/actions/ProfileActions.test.js
index a21917f..d373ff2 100644
--- a/src/actions/ProfileActions.test.js
+++ b/src/actions/ProfileActions.test.js
@@ -1,8 +1,8 @@
import {
- openField,
- closeField,
- FIELD_OPEN,
- FIELD_CLOSE,
+ openForm,
+ closeForm,
+ OPEN_FORM,
+ CLOSE_FORM,
SAVE_PROFILE,
saveProfileBegin,
saveProfileSuccess,
@@ -26,51 +26,47 @@ import {
describe('editable field actions', () => {
it('should create an open action', () => {
const expectedAction = {
- type: FIELD_OPEN,
- fieldName: 'name',
+ type: OPEN_FORM,
+ payload: {
+ formId: 'name',
+ },
};
- expect(openField('name')).toEqual(expectedAction);
+ expect(openForm('name')).toEqual(expectedAction);
});
it('should create a closed action', () => {
const expectedAction = {
- type: FIELD_CLOSE,
- fieldName: 'name',
+ type: CLOSE_FORM,
+ payload: {
+ formId: 'name',
+ },
};
- expect(closeField('name')).toEqual(expectedAction);
+ expect(closeForm('name')).toEqual(expectedAction);
});
});
describe('SAVE profile actions', () => {
- const profileData = {
- username: 'verified',
- email: 'verified@example.com',
- bio: 'A great bio.',
- name: 'Veri Fied',
- country: 'US',
- // 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',
- fieldName: 'fullName',
- profileData,
- preferencesData,
+ formId: 'name',
},
};
- expect(saveProfile('user person', { profileData, preferencesData }, 'fullName')).toEqual(expectedAction);
+ expect(saveProfile('name')).toEqual(expectedAction);
});
it('should create an action to signal user profile save success', () => {
+ const accountData = { name: 'Full Name' };
+ const preferencesData = { visibility: { name: 'private' } };
const expectedAction = {
type: SAVE_PROFILE.SUCCESS,
+ payload: {
+ account: accountData,
+ preferences: preferencesData,
+ },
};
- expect(saveProfileSuccess()).toEqual(expectedAction);
+ expect(saveProfileSuccess(accountData, preferencesData)).toEqual(expectedAction);
});
it('should create an action to signal user profile save beginning', () => {
@@ -187,21 +183,25 @@ describe('DELETE profile photo actions', () => {
describe('Editable field opening and closing actions', () => {
- const fieldName = 'fullName';
+ const formId = 'name';
it('should create an action to signal the opening a field', () => {
const expectedAction = {
- type: FIELD_OPEN,
- fieldName,
+ type: OPEN_FORM,
+ payload: {
+ formId,
+ },
};
- expect(openField(fieldName)).toEqual(expectedAction);
+ expect(openForm(formId)).toEqual(expectedAction);
});
it('should create an action to signal the closing a field', () => {
const expectedAction = {
- type: FIELD_CLOSE,
- fieldName,
+ type: CLOSE_FORM,
+ payload: {
+ formId,
+ },
};
- expect(closeField(fieldName)).toEqual(expectedAction);
+ expect(closeForm(formId)).toEqual(expectedAction);
});
});
diff --git a/src/components/App.jsx b/src/components/App.jsx
index 3ad047f..fe307cf 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -2,7 +2,8 @@ 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 { Route, Switch } from 'react-router-dom';
+import { ConnectedRouter } from 'connected-react-router';
import SiteFooter from '@edx/frontend-component-footer';
import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
@@ -27,7 +28,7 @@ class App extends Component {
return (
-
+
-
+
);
@@ -71,6 +72,7 @@ App.propTypes = {
fetchUserAccount: PropTypes.func.isRequired,
username: PropTypes.string.isRequired,
store: PropTypes.object.isRequired, // eslint-disable-line
+ history: PropTypes.object.isRequired, // eslint-disable-line
};
const mapStateToProps = state => ({
diff --git a/src/components/ProfilePage.jsx b/src/components/ProfilePage.jsx
index 75a7ae8..a8a7054 100644
--- a/src/components/ProfilePage.jsx
+++ b/src/components/ProfilePage.jsx
@@ -9,153 +9,79 @@ import {
saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
- openField,
- closeField,
+ openForm,
+ closeForm,
+ updateVisibilityDraft,
+ updateAccountDraft,
} from '../actions/ProfileActions';
// Components
import ProfileAvatar from './ProfilePage/ProfileAvatar';
-import FullName from './ProfilePage/FullName';
-import UserLocation from './ProfilePage/UserLocation';
+import Name from './ProfilePage/Name';
+import Country from './ProfilePage/Country';
import Education from './ProfilePage/Education';
import SocialLinks from './ProfilePage/SocialLinks';
import Bio from './ProfilePage/Bio';
-import MyCertificates from './ProfilePage/MyCertificates';
+import Certificates from './ProfilePage/Certificates';
import AgeMessage from './ProfilePage/AgeMessage';
export class ProfilePage extends React.Component {
constructor(props) {
super(props);
- this.state = {
- fullName: { value: null, visibility: null },
- userLocation: { value: null, visibility: null },
- education: { value: null, visibility: null },
- bio: { value: null, visibility: null },
- socialLinks: { value: null, visibility: null },
- certificates: { value: null, visibility: null },
- };
-
- this.onCancel = this.onCancel.bind(this);
- this.onEdit = this.onEdit.bind(this);
- this.onSave = this.onSave.bind(this);
- this.onSaveProfilePhoto = this.onSaveProfilePhoto.bind(this);
- this.onDeleteProfilePhoto = this.onDeleteProfilePhoto.bind(this);
- this.onChange = this.onChange.bind(this);
- this.onVisibilityChange = this.onVisibilityChange.bind(this);
+ this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
+ this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchProfile(this.props.match.params.username);
}
- onCancel() {
- this.props.closeField(this.props.currentlyEditingField);
- }
-
- onEdit(fieldName) {
- this.props.openField(fieldName);
- }
-
- onSave(fieldName) {
- const {
- value,
- visibility,
- } = this.state[fieldName];
-
- const data = {};
-
- if (value != null) {
- data.profileData = { [fieldName]: value };
- }
- if (visibility != null) {
- data.preferencesData = { visibility: { [fieldName]: visibility } };
- }
-
- if (value == null && visibility == null) {
- this.onCancel();
- } else {
- this.props.saveProfile(this.props.username, data, fieldName);
- }
- }
-
- onSaveProfilePhoto(formData) {
+ handleSaveProfilePhoto(formData) {
this.props.saveProfilePhoto(this.props.username, formData);
}
- onDeleteProfilePhoto() {
+ handleDeleteProfilePhoto() {
this.props.deleteProfilePhoto(this.props.username);
}
- onChange(fieldName, value) {
- this.setState({
- [fieldName]: {
- value,
- visibility: this.state[fieldName].visibility,
- },
- });
+ handleClose(formId) {
+ this.props.closeForm(formId);
}
- onVisibilityChange(fieldName, visibility) {
- this.setState({
- [fieldName]: {
- value: this.state[fieldName].value,
- visibility,
- },
- });
+ handleOpen(formId) {
+ this.props.openForm(formId);
+ }
+
+ handleSubmit(formId) {
+ this.props.saveProfile(formId);
+ }
+
+ handleChange(formId, name, value) {
+ if (name === 'visibility') {
+ this.props.updateVisibilityDraft(formId, value);
+ } else {
+ this.props.updateAccountDraft(formId, value);
+ }
}
render() {
const {
- saveState,
- error,
- profileImage,
- username,
- fullName,
- userLocation,
- bio,
- education,
- socialLinks,
- certificates,
- isCurrentUserProfile,
+ profileImage, username, errors,
} = this.props;
- const commonProps = {
- onSave: this.onSave,
- onEdit: this.onEdit,
- onCancel: this.onCancel,
- onChange: this.onChange,
- onVisibilityChange: this.onVisibilityChange,
- saveState,
- error,
+ const commonFormProps = {
+ openHandler: this.handleOpen,
+ closeHandler: this.handleClose,
+ submitHandler: this.handleSubmit,
+ changeHandler: this.handleChange,
+ errors,
};
- const getMode = (name) => {
- // If the prop doesn't exist, that means it hasn't been set (for the current user's profile)
- // or is being hidden from us (for other users' profiles)
- const propExists = this.props[name] != null && this.props[name].length > 0;
-
- // If this isn't the current user's profile...
- if (!isCurrentUserProfile) {
- // then there are only two options: static or nothing.
- // We use 'null' as a return value because the consumers of
- // getMode render nothing at all on a mode of null.
- return propExists ? 'static' : null;
- }
- // Otherwise, if this is the current user's profile...
- if (name === this.props.currentlyEditingField) {
- return 'editing';
- }
-
- if (!propExists) {
- return 'empty';
- }
-
- return 'editable';
- };
-
- const getVisibility = name => this.props.visibility[name];
-
return (
@@ -166,8 +92,8 @@ export class ProfilePage extends React.Component {
@@ -179,54 +105,20 @@ export class ProfilePage extends React.Component {
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
{this.props.requiresParentalConsent ? : null}
-
-
-
-
-
+
+
@@ -236,39 +128,46 @@ export class ProfilePage extends React.Component {
}
ProfilePage.propTypes = {
+ // Page state helpers
currentlyEditingField: PropTypes.string,
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
- error: PropTypes.string,
isCurrentUserProfile: PropTypes.bool.isRequired,
- profileImage: PropTypes.string,
- fullName: PropTypes.string,
+ errors: PropTypes.objectOf(PropTypes.string),
+
+ // Profile data
username: PropTypes.string,
- userLocation: PropTypes.string,
+ profileImage: PropTypes.string,
+ accountPrivacy: PropTypes.string,
+ certificates: PropTypes.arrayOf(PropTypes.shape({
+ title: PropTypes.string,
+ })),
+
+ // Profile data for form fields
education: PropTypes.string,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
- aboutMe: PropTypes.string,
bio: PropTypes.string,
- certificates: PropTypes.arrayOf(PropTypes.shape({
- title: PropTypes.string,
- })),
+ visibility: PropTypes.objectOf(PropTypes.string),
+ // Actions
fetchProfile: PropTypes.func.isRequired,
saveProfile: PropTypes.func.isRequired,
saveProfilePhoto: PropTypes.func.isRequired,
deleteProfilePhoto: PropTypes.func.isRequired,
- openField: PropTypes.func.isRequired,
- closeField: PropTypes.func.isRequired,
+ openForm: PropTypes.func.isRequired,
+ closeForm: PropTypes.func.isRequired,
+ updateVisibilityDraft: PropTypes.func.isRequired,
+ updateAccountDraft: PropTypes.func.isRequired,
+
+ // Router
match: PropTypes.shape({
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
- accountPrivacy: PropTypes.string,
- visibility: PropTypes.object, // eslint-disable-line
yearOfBirth: PropTypes.number,
requiresParentalConsent: PropTypes.bool,
};
@@ -277,14 +176,11 @@ ProfilePage.defaultProps = {
currentlyEditingField: null,
saveState: null,
savePhotoState: null,
- error: null,
+ errors: {},
profileImage: null,
- fullName: null,
username: null,
- userLocation: null,
education: null,
socialLinks: [],
- aboutMe: null,
bio: null,
certificates: null,
accountPrivacy: null,
@@ -295,27 +191,26 @@ ProfilePage.defaultProps = {
const mapStateToProps = (state) => {
const profileImage =
- state.profilePage.profile.profileImage != null
- ? state.profilePage.profile.profileImage.imageUrlLarge
+ state.profilePage.account.profileImage != null
+ ? state.profilePage.account.profileImage.imageUrlLarge
: null;
return {
- isCurrentUserProfile: state.userAccount.username === state.profilePage.profile.username,
+ isCurrentUserProfile: state.userAccount.username === state.profilePage.account.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,
+
+ username: state.profilePage.account.username,
+ education: state.profilePage.account.levelOfEducation,
+ socialLinks: state.profilePage.account.socialLinks,
+ bio: state.profilePage.account.bio,
+ certificates: state.profilePage.account.certificates,
accountPrivacy: state.profilePage.preferences.accountPrivacy,
visibility: state.profilePage.preferences.visibility || {},
- yearOfBirth: state.profilePage.profile.yearOfBirth,
- requiresParentalConsent: state.profilePage.profile.requiresParentalConsent,
+ yearOfBirth: state.profilePage.account.yearOfBirth,
+ requiresParentalConsent: state.profilePage.account.requiresParentalConsent,
};
};
@@ -323,10 +218,12 @@ export default connect(
mapStateToProps,
{
fetchProfile,
- saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
- openField,
- closeField,
+ saveProfile,
+ openForm,
+ closeForm,
+ updateVisibilityDraft,
+ updateAccountDraft,
},
)(ProfilePage);
diff --git a/src/components/ProfilePage/Bio.jsx b/src/components/ProfilePage/Bio.jsx
index 423e832..9355df5 100644
--- a/src/components/ProfilePage/Bio.jsx
+++ b/src/components/ProfilePage/Bio.jsx
@@ -1,98 +1,142 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
+import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
-import { Input } from 'reactstrap';
-import EditControls from './elements/EditControls';
+// Components
+import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
+// Selectors
+import { editableFormSelector } from '../../selectors/ProfilePageSelector';
-function Bio({
- bio,
- visibility,
- editMode,
- onEdit,
- onChange,
- onSave,
- onCancel,
- onVisibilityChange,
- saveState,
-}) {
- return (
-
-
- onChange('bio', e.target.value)}
- />
- onCancel('bio')}
- onSave={() => onSave('bio')}
- saveState={saveState}
- visibility={visibility}
- onVisibilityChange={e => onVisibilityChange('bio', e.target.value)}
- />
-
- ),
- editable: (
-
- onEdit('bio')}
- showVisibility={Boolean(bio)}
- visibility={visibility}
- />
- {bio}
-
- ),
- empty: (
- onEdit('bio')}>
-
-
- ),
- static: (
-
-
- {bio}
,
-
- ),
- }}
- />
- );
+class Bio extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ }
+
+ handleChange(e) {
+ const { name, value } = e.target;
+ this.props.changeHandler(this.props.formId, name, value);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
+ }
+
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
+ }
+
+ render() {
+ const {
+ formId, value, visibility, editMode, saveState, error,
+ } = this.props;
+
+ return (
+
+
+
+
+ {error}
+
+
+
+ ),
+ editable: (
+
+
+ {value}
+
+ ),
+ empty: (
+
+
+
+ ),
+ static: (
+
+
+ {value}
+
+ ),
+ }}
+ />
+ );
+ }
}
Bio.propTypes = {
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
- saveState: PropTypes.string,
- bio: PropTypes.string,
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ value: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
+ saveState: PropTypes.string,
+ error: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
};
Bio.defaultProps = {
editMode: 'static',
saveState: null,
- bio: null,
+ value: null,
visibility: 'private',
+ error: null,
};
-
-export default Bio;
+export default connect(
+ editableFormSelector,
+ {},
+)(Bio);
diff --git a/src/components/ProfilePage/Certificates.jsx b/src/components/ProfilePage/Certificates.jsx
new file mode 100644
index 0000000..5b545c3
--- /dev/null
+++ b/src/components/ProfilePage/Certificates.jsx
@@ -0,0 +1,168 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { Row, Col, Card, CardBody, CardTitle, Button, Form } from 'reactstrap';
+import { connect } from 'react-redux';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faDownload } from '@fortawesome/free-solid-svg-icons';
+
+// Components
+import FormControls from './elements/FormControls';
+import EditableItemHeader from './elements/EditableItemHeader';
+import SwitchContent from './elements/SwitchContent';
+
+// Selectors
+import { certificatesSelector } from '../../selectors/ProfilePageSelector';
+
+class Certificates extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ }
+
+ handleChange(e) {
+ const { name, value } = e.target;
+ this.props.changeHandler(this.props.formId, name, value);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
+ }
+
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
+ }
+
+ renderCertificate({
+ type: { name }, title, organization, downloadUrl,
+ }) {
+ return (
+
+
+
+
+ {name}
+ {title}
+
+ From
+ {organization}
+
+
+
+
+
+
+ );
+ }
+
+ renderCertificates() {
+ if (this.props.certificates === null) {
+ return null;
+ }
+
+ return (
+ {this.props.certificates.map(certificate => this.renderCertificate(certificate))}
+ );
+ }
+
+ render() {
+ const {
+ formId, visibility, editMode, saveState,
+ } = this.props;
+
+ return (
+
+
+ {this.renderCertificates()}
+
+
+ ),
+ editable: (
+
+
+ {this.renderCertificates()}
+
+ ),
+ empty: (
+
+
+
+ ),
+ static: (
+
+
+ {this.renderCertificates()}
+
+ ),
+ }}
+ />
+ );
+ }
+}
+
+Certificates.propTypes = {
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ certificates: PropTypes.arrayOf(PropTypes.shape({
+ title: PropTypes.string,
+ })),
+ visibility: PropTypes.oneOf(['private', 'all_users']),
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
+ saveState: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
+};
+
+Certificates.defaultProps = {
+ editMode: 'static',
+ saveState: null,
+ visibility: 'private',
+ certificates: null,
+};
+
+export default connect(
+ certificatesSelector,
+ {},
+)(Certificates);
diff --git a/src/components/ProfilePage/Country.jsx b/src/components/ProfilePage/Country.jsx
new file mode 100644
index 0000000..a41a4d8
--- /dev/null
+++ b/src/components/ProfilePage/Country.jsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
+import { connect } from 'react-redux';
+
+// Components
+import FormControls from './elements/FormControls';
+import EditableItemHeader from './elements/EditableItemHeader';
+import EmptyContent from './elements/EmptyContent';
+import SwitchContent from './elements/SwitchContent';
+
+// Constants
+import { ALL_COUNTRIES } from '../../constants/countries';
+
+// Selectors
+import { editableFormSelector } from '../../selectors/ProfilePageSelector';
+
+class Country extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ }
+
+ handleChange(e) {
+ const {
+ name,
+ value,
+ } = e.target;
+ this.props.changeHandler(this.props.formId, name, value);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
+ }
+
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
+ }
+
+ render() {
+ const {
+ formId, value, visibility, editMode, saveState, error,
+ } = this.props;
+
+ return (
+
+
+
+
+ {Object.keys(ALL_COUNTRIES).map(key => (
+
+ ))}
+
+ {error}
+
+
+
+ ),
+ editable: (
+
+
+ {ALL_COUNTRIES[value]}
+
+ ),
+ empty: Add location,
+ static: (
+
+
+ {ALL_COUNTRIES[value]}
+
+ ),
+ }}
+ />
+ );
+ }
+}
+
+Country.propTypes = {
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ value: PropTypes.string,
+ visibility: PropTypes.oneOf(['private', 'all_users']),
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
+ saveState: PropTypes.string,
+ error: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
+};
+
+Country.defaultProps = {
+ editMode: 'static',
+ saveState: null,
+ value: null,
+ visibility: 'private',
+ error: null,
+};
+
+export default connect(
+ editableFormSelector,
+ {},
+)(Country);
diff --git a/src/components/ProfilePage/Education.jsx b/src/components/ProfilePage/Education.jsx
index 94cb6e1..86e7f75 100644
--- a/src/components/ProfilePage/Education.jsx
+++ b/src/components/ProfilePage/Education.jsx
@@ -1,98 +1,143 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Input } from 'reactstrap';
+import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
+import { connect } from 'react-redux';
-import EditControls from './elements/EditControls';
+// Components
+import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
+// Constants
import EDUCATION from '../../constants/education';
+// Selectors
+import { editableFormSelector } from '../../selectors/ProfilePageSelector';
-function Education({
- education,
- visibility,
- editMode,
- onEdit,
- onChange,
- onSave,
- onCancel,
- onVisibilityChange,
- saveState,
-}) {
- return (
-
-
- onChange('education', e.target.value)}
- >
- {Object.keys(EDUCATION).map(key => (
-
- ))}
-
- onCancel('education')}
- onSave={() => onSave('education')}
- saveState={saveState}
- visibility={visibility}
- onVisibilityChange={e => onVisibilityChange('education', e.target.value)}
- />
-
- ),
- editable: (
-
- onEdit('education')}
- showVisibility={Boolean(education)}
- visibility={visibility}
- />
- {EDUCATION[education]}
-
- ),
- empty: (
- onEdit('education')}>Add education
- ),
- static: (
-
-
- {EDUCATION[education]}
-
- ),
- }}
- />
- );
+class Education extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ }
+
+ handleChange(e) {
+ const {
+ name,
+ value,
+ } = e.target;
+ this.props.changeHandler(this.props.formId, name, value);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
+ }
+
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
+ }
+
+ render() {
+ const {
+ formId, value, visibility, editMode, saveState, error,
+ } = this.props;
+
+ return (
+
+
+
+
+ {Object.keys(EDUCATION).map(key => (
+
+ ))}
+
+ {error}
+
+
+
+ ),
+ editable: (
+
+
+ {EDUCATION[value]}
+
+ ),
+ empty: Add education,
+ static: (
+
+
+ {EDUCATION[value]}
+
+ ),
+ }}
+ />
+ );
+ }
}
Education.propTypes = {
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
- saveState: PropTypes.string,
- education: PropTypes.string,
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ value: PropTypes.string,
visibility: PropTypes.oneOf(['private', 'all_users']),
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
+ saveState: PropTypes.string,
+ error: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
};
Education.defaultProps = {
editMode: 'static',
saveState: null,
- education: null,
+ value: null,
visibility: 'private',
+ error: null,
};
-
-export default Education;
+export default connect(
+ editableFormSelector,
+ {},
+)(Education);
diff --git a/src/components/ProfilePage/FullName.jsx b/src/components/ProfilePage/FullName.jsx
deleted file mode 100644
index c434837..0000000
--- a/src/components/ProfilePage/FullName.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Input } from 'reactstrap';
-
-import EditControls from './elements/EditControls';
-import EditableItemHeader from './elements/EditableItemHeader';
-import EmptyContent from './elements/EmptyContent';
-import SwitchContent from './elements/SwitchContent';
-
-
-function FullName({
- fullName,
- visibility,
- editMode,
- onEdit,
- onChange,
- onSave,
- onCancel,
- onVisibilityChange,
- saveState,
-}) {
- return (
-
-
- onChange('fullName', e.target.value)}
- />
- onCancel('fullName')}
- onSave={() => onSave('fullName')}
- saveState={saveState}
- visibility={visibility}
- onVisibilityChange={e => onVisibilityChange('fullName', e.target.value)}
- />
-
- ),
- editable: (
-
- onEdit('fullName')}
- showVisibility={Boolean(fullName)}
- visibility={visibility}
- />
- {fullName}
-
- ),
- empty: (
- onEdit('fullName')}>Add name
- ),
- static: (
-
-
- {fullName}
-
- ),
- }}
- />
- );
-}
-
-
-FullName.propTypes = {
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
- saveState: PropTypes.string,
- fullName: PropTypes.string,
- visibility: PropTypes.oneOf(['private', 'all_users']),
-};
-
-FullName.defaultProps = {
- editMode: 'static',
- saveState: null,
- fullName: null,
- visibility: 'private',
-};
-
-
-export default FullName;
diff --git a/src/components/ProfilePage/MyCertificates.jsx b/src/components/ProfilePage/MyCertificates.jsx
deleted file mode 100644
index cf632be..0000000
--- a/src/components/ProfilePage/MyCertificates.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
-import { Row, Col, Card, CardBody, CardTitle, CardText, Button } from 'reactstrap';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faDownload } from '@fortawesome/free-solid-svg-icons';
-
-import EditControls from './elements/EditControls';
-import EditableItemHeader from './elements/EditableItemHeader';
-import SwitchContent from './elements/SwitchContent';
-
-function MyCertificates({
- certificates,
- visibility,
- editMode,
- onEdit,
- onSave,
- onCancel,
- onVisibilityChange,
- saveState,
-}) {
- const renderCertificates = () => {
- if (!certificates) return null;
-
- return (
-
- {certificates.map(({
- type: { key, name }, // eslint-disable-line no-unused-vars
- title,
- organization,
- downloadUrl,
- }) => (
-
-
-
-
- {name}
- {title}
-
-
- From
- {organization}
-
-
-
-
-
-
-
- ))}
-
- );
- };
-
- return (
-
-
- {renderCertificates()}
- onCancel('certificates')}
- onSave={() => onSave('certificates')}
- saveState={saveState}
- visibility={visibility}
- onVisibilityChange={e => onVisibilityChange('certificates', e.target.value)}
- />
-
- ),
- editable: (
-
- onEdit('certificates')}
- showVisibility={Boolean(certificates)}
- visibility={visibility}
- />
- {renderCertificates()}
-
- ),
- empty: (
-
-
-
- ),
- static: (
-
-
- {renderCertificates()}
-
- ),
- }}
- />
- );
-}
-
-
-MyCertificates.propTypes = {
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
- saveState: PropTypes.string,
- certificates: PropTypes.arrayOf(PropTypes.shape({
- title: PropTypes.string,
- })),
- visibility: PropTypes.oneOf(['private', 'all_users']),
-};
-
-MyCertificates.defaultProps = {
- editMode: 'static',
- saveState: null,
- certificates: null,
- visibility: 'private',
-};
-
-
-export default MyCertificates;
diff --git a/src/components/ProfilePage/Name.jsx b/src/components/ProfilePage/Name.jsx
new file mode 100644
index 0000000..e7db1d7
--- /dev/null
+++ b/src/components/ProfilePage/Name.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Form, FormFeedback, FormGroup, FormText, Input, Label } from 'reactstrap';
+import { connect } from 'react-redux';
+
+// Components
+import FormControls from './elements/FormControls';
+import EditableItemHeader from './elements/EditableItemHeader';
+import EmptyContent from './elements/EmptyContent';
+import SwitchContent from './elements/SwitchContent';
+
+// Selectors
+import { editableFormSelector } from '../../selectors/ProfilePageSelector';
+
+class Name extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
+ }
+
+ handleChange(e) {
+ const {
+ name,
+ value,
+ } = e.target;
+ this.props.changeHandler(this.props.formId, name, value);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
+ }
+
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
+ }
+
+ render() {
+ const {
+ formId, value, visibility, editMode, saveState, error,
+ } = this.props;
+
+ return (
+
+
+
+
+
+ This is the name that appears in your account and on your certificates.
+
+ {error}
+
+
+
+ ),
+ editable: (
+
+
+ {value}
+
+ ),
+ empty: Add name,
+ static: (
+
+
+ {value}
+
+ ),
+ }}
+ />
+ );
+ }
+}
+
+Name.propTypes = {
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ value: PropTypes.string,
+ visibility: PropTypes.oneOf(['private', 'all_users']),
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
+ saveState: PropTypes.string,
+ error: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
+};
+
+Name.defaultProps = {
+ editMode: 'static',
+ saveState: null,
+ value: null,
+ visibility: 'private',
+ error: null,
+};
+
+export default connect(
+ editableFormSelector,
+ {},
+)(Name);
diff --git a/src/components/ProfilePage/SocialLinks.jsx b/src/components/ProfilePage/SocialLinks.jsx
index 1b9efa4..7d7aa20 100644
--- a/src/components/ProfilePage/SocialLinks.jsx
+++ b/src/components/ProfilePage/SocialLinks.jsx
@@ -1,64 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Input } from 'reactstrap';
+import { Form, Input, FormFeedback } from 'reactstrap';
+import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
-import EditControls from './elements/EditControls';
+// Components
+import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
-import SwitchContent from './elements/SwitchContent';
import EmptyContent from './elements/EmptyContent';
+import SwitchContent from './elements/SwitchContent';
-const brandIcons = {
- facebook: faFacebook,
- twitter: faTwitter,
- linkedin: faLinkedin,
+// Selectors
+import { editableFormSelector } from '../../selectors/ProfilePageSelector';
+
+const platformDisplayInfo = {
+ facebook: {
+ icon: faFacebook,
+ name: 'Facebook',
+ },
+ twitter: {
+ icon: faTwitter,
+ name: 'Twitter',
+ },
+ linkedin: {
+ icon: faLinkedin,
+ name: 'LinkedIn',
+ },
};
-
class SocialLinks extends React.Component {
constructor(props) {
super(props);
- this.state = {};
- this.onSave = this.onSave.bind(this);
- this.onEdit = this.onEdit.bind(this);
- this.onInputChange = this.onInputChange.bind(this);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleOpen = this.handleOpen.bind(this);
}
- onSave() {
- const values = this.props.platforms.filter(({ key }) => typeof this.state[key] !== 'undefined').map(({ key }) => ({
- platform: key,
- socialLink: this.state[key],
- }));
+ handleChange(e) {
+ const {
+ name,
+ value,
+ } = e.target;
- this.props.onSave('socialLinks', values);
+ if (name !== 'visibility') {
+ const updatedList = this.props.committedValue.map((socialLink) => {
+ if (socialLink.platform === name) {
+ return { platform: name, social_link: value };
+ }
+ return socialLink;
+ });
+ this.props.changeHandler(this.props.formId, 'socialLinks', updatedList);
+ } else {
+ this.props.changeHandler(this.props.formId, name, value);
+ }
}
- onEdit() {
- this.props.onEdit('socialLinks');
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.submitHandler(this.props.formId);
}
- onInputChange(platform, value) {
- this.setState({ [platform]: value });
- this.props.onChange('socialLinks', this.state);
+ handleClose() {
+ this.props.closeHandler(this.props.formId);
+ }
+
+ handleOpen() {
+ this.props.openHandler(this.props.formId);
}
render() {
const {
- platforms,
- socialLinks,
- editMode,
- visibility,
- saveState,
- onVisibilityChange,
- onCancel,
+ formId, value: values, visibility, editMode, saveState, error,
} = this.props;
- const { onInputChange, onEdit } = this;
- const socialLinksMap = socialLinks.reduce((acc, { platform, socialLink }) => {
- acc[platform] = socialLink;
- return acc;
- }, {});
- const isEmpty = socialLinks && socialLinks.length > 0;
return (
- {platforms.map(({ key, name }) => (
+ {values.map(({ platform }) => (
))}
@@ -80,10 +96,10 @@ class SocialLinks extends React.Component {
- {socialLinks.map(({ platform, socialLink }) => (
+ {values.map(({ platform, social_link: socialLink }) => (
@@ -96,45 +112,46 @@ class SocialLinks extends React.Component {
- {platforms.map(({ key, name }) => (
+ {values.map(({ platform, social_link: socialLink }) => (
))}
),
editing: (
-
+
+
),
}}
/>
@@ -143,42 +160,51 @@ class SocialLinks extends React.Component {
}
SocialLinks.propTypes = {
- socialLinks: PropTypes.arrayOf(PropTypes.shape({
+ // It'd be nice to just set this as a defaultProps...
+ // except the class that comes out on the other side of react-redux's
+ // connect() method won't have it anymore. Static properties won't survive
+ // through the higher order function.
+ formId: PropTypes.string.isRequired,
+
+ // From Selector
+ value: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
- platforms: PropTypes.arrayOf(PropTypes.shape({
- key: PropTypes.string,
- name: PropTypes.string,
+ committedValue: PropTypes.arrayOf(PropTypes.shape({
+ platform: PropTypes.string,
+ socialLink: PropTypes.string,
})),
visibility: PropTypes.oneOf(['private', 'all_users']),
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
+ editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
+ error: PropTypes.string,
+
+ // Actions
+ changeHandler: PropTypes.func.isRequired,
+ submitHandler: PropTypes.func.isRequired,
+ closeHandler: PropTypes.func.isRequired,
+ openHandler: PropTypes.func.isRequired,
};
+
SocialLinks.defaultProps = {
- socialLinks: [],
- platforms: [
- { key: 'twitter', name: 'Twitter' },
- { key: 'linkedin', name: 'LinkedIn' },
- { key: 'facebook', name: 'Facebook' },
- ],
- visibility: 'private',
editMode: 'static',
saveState: null,
+ value: [],
+ committedValue: [],
+ visibility: 'private',
+ error: null,
};
-export default SocialLinks;
-
+export default connect(
+ editableFormSelector,
+ {},
+)(SocialLinks);
function SocialLink({ url, name, platform }) {
return (
-
+
{name}
);
@@ -197,7 +223,7 @@ function EditableListItem({
onClickEmptyContent,
name,
}) {
- const linkDisplay = url != null ?
+ const linkDisplay = url ?
:
Add {name};
@@ -219,29 +245,36 @@ EditableListItem.defaultProps = {
function EditingListItem({
platform,
name,
- defaultValue,
+ value,
onChange,
+ error,
}) {
return (
{name}
onChange(platform, e.target.value)}
+ name={platform}
+ value={value}
+ onChange={onChange}
+ invalid={error != null}
/>
+ {error}
);
}
EditingListItem.propTypes = {
platform: PropTypes.string.isRequired,
- defaultValue: PropTypes.string,
+ value: PropTypes.string,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
+ error: PropTypes.string,
};
+
EditingListItem.defaultProps = {
- defaultValue: null,
+ value: null,
+ error: null,
};
diff --git a/src/components/ProfilePage/UserLocation.jsx b/src/components/ProfilePage/UserLocation.jsx
deleted file mode 100644
index 9f69ab7..0000000
--- a/src/components/ProfilePage/UserLocation.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Input } from 'reactstrap';
-
-import EditControls from './elements/EditControls';
-import EditableItemHeader from './elements/EditableItemHeader';
-import EmptyContent from './elements/EmptyContent';
-import SwitchContent from './elements/SwitchContent';
-
-import { ALL_COUNTRIES } from '../../constants/countries';
-
-
-function UserLocation({
- userLocation,
- visibility,
- editMode,
- onEdit,
- onChange,
- onSave,
- onCancel,
- onVisibilityChange,
- saveState,
-}) {
- return (
-
-
- onChange('userLocation', e.target.value)}
- >
- {Object.keys(ALL_COUNTRIES).map(key => (
-
- ))}
-
- onCancel('userLocation')}
- onSave={() => onSave('userLocation')}
- saveState={saveState}
- visibility={visibility}
- onVisibilityChange={e => onVisibilityChange('userLocation', e.target.value)}
- />
-
- ),
- editable: (
-
- onEdit('userLocation')}
- showVisibility={Boolean(userLocation)}
- visibility={visibility}
- />
- {ALL_COUNTRIES[userLocation]}
-
- ),
- empty: (
- onEdit('userLocation')}>Add location
- ),
- static: (
-
-
- {ALL_COUNTRIES[userLocation]}
-
- ),
- }}
- />
- );
-}
-
-UserLocation.propTypes = {
- editMode: PropTypes.string,
- onEdit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onVisibilityChange: PropTypes.func.isRequired,
- saveState: PropTypes.string,
- userLocation: PropTypes.string,
- visibility: PropTypes.oneOf(['private', 'all_users']),
-};
-
-UserLocation.defaultProps = {
- editMode: 'static',
- saveState: null,
- userLocation: null,
- visibility: 'private',
-};
-
-
-export default UserLocation;
diff --git a/src/components/ProfilePage/elements/AsyncActionButton.jsx b/src/components/ProfilePage/elements/AsyncActionButton.jsx
index a1df039..2dd7e52 100644
--- a/src/components/ProfilePage/elements/AsyncActionButton.jsx
+++ b/src/components/ProfilePage/elements/AsyncActionButton.jsx
@@ -11,6 +11,7 @@ function AsyncActionButton({
style,
variant,
labels,
+ type,
}) {
const renderIcon = () => {
if (variant === 'error') return ;
@@ -30,6 +31,7 @@ function AsyncActionButton({
return (