wireframe

use reactstrap

Add banner and header atoms

pull all user data out of JSX

Transform component to class

Add stub for editable content

Add height animation to editable content

progress with editing

Break out components to individual files. Make most editable.

Add ability to change tag of EditableContent

Add avatar

fix some prop type issues

Add an async button

Small update to avatar

Add height animation to editable content

pull edited data up to useraccount component callback

Updating oneOf prop type to provide arrays.

Fixing missing key attributes on repeated tags.

use reactstrap

Add state wrapper container to UserAccount component.

Move crossfade transition of in-context editable elements to a discrete component

Some reusable components.

Form row related stuff and an Alert box.  All backed by reactstrap.

Add passthroughs for button save states

Pointing edx-bootstrap at 2.0 branch

Also adding font-awesome icons via the preferred method, and updating port to 1995.

Fixing font-awesome icons and getting bootstrap imported correctly.

Update edx bootstrap

There was a problem with a missing org prefix

Move headings to a common component. Add full name

fix missing key and conflict of value and defalutValue props

Remove extraneous div in TransitionReplace

Fix some wonkiness with the education component

Add classname prop to TransitionReplace

Add some more consistent margins

Update to layout for some editable content

Update markup to remove repeat renders

Add empty state for bio

Refactoring profile form state out of the user account “fetch” state from frontend-auth

Using redux-saga to manage action side-effects (async calls, secondary actions, and delays).  redux-saga is a very flexible alternate side-effect model for redux (as opposed to redux-thunk).  Used it to good effect in the past, and it helps separate concerns between action creators and side effect management.  It also helps decouple react components from async actions by having redux-saga register as an observer of the redux action stream.

Add empty states for all fields except social links and my certificates

Close fields after save

Remove the delay after successfully saving

Update banner image. Update edit controls layout

Updated edx bootstrap to have focus ring on btn-link buttons

Add event handler props to TransitionReplace. Handle focus when edit fields are swapped in and out

Update empty content icon and width

Hide visibility if a field has no value

Add delays back in

Large reworking of components here. Social links still has a bug. Only twitter will save.

Renaming profileActions -> profile and tweaking editable state stuff.

Adding tests for profile actions - pulled over from frontend-auth.

Removing profile API helpers, fixing a few bugs in saving the user profile.

Make Social Links a full on component again. Can only save one field at a time.

Remove an old version of edx-bootstrap

Renaming UserAccount components to UserProfile.
This commit is contained in:
Adam Butterworth
2019-01-18 11:07:55 -05:00
committed by Douglas Hall
parent 31d89ce8ef
commit e076356455
35 changed files with 3627 additions and 776 deletions

View File

@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input } from 'reactstrap';
class ProfileAvatar extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
this.form = React.createRef();
this.onClick = this.onClick.bind(this);
this.onInput = this.onInput.bind(this);
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onClick() {
this.fileInput.current.click();
}
onInput(e) { // eslint-disable-line no-unused-vars
// console.log('input', e)
this.form.current.submit();
}
onChange(e) { // eslint-disable-line no-unused-vars
// console.log('change', e)
}
onSubmit(e) { // eslint-disable-line no-unused-vars
// console.log('onsubmit', e);
}
render() {
const {
src,
} = this.props;
return (
<div className="profile-avatar rounded-circle overflow-hidden">
<button
className="text-white profile-avatar-edit-button"
onClick={this.onClick}
>
Update
</button>
<img className="w-100" src={src} alt="profile avatar" />
<form
ref={this.form}
onSubmit={this.onSubmit}
method="post"
encType="multipart/form-data"
>
<Input
className="d-none"
innerRef={this.fileInput}
type="file"
name="file"
id="exampleFile"
onInput={this.onInput}
onChange={this.onChange}
accept=".jpg, .jpeg, .png"
/>
</form>
</div>
);
}
}
export default ProfileAvatar;
ProfileAvatar.propTypes = {
src: PropTypes.string,
};
ProfileAvatar.defaultProps = {
src: null,
};

View File

@@ -0,0 +1,170 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import EditControls from './elements/EditControls';
import EditableItemHeader from './elements/EditableItemHeader';
import SwitchContent from './elements/SwitchContent';
class SocialLinks extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.onSave = this.onSave.bind(this);
}
onSave() {
const values = this.props.platforms.filter(({ key }) => typeof this.state[key] !== 'undefined').map(({ key }) => ({
platform: key,
socialLink: this.state[key],
}));
this.props.onSave('socialLinks', values);
}
render() {
const {
socialLinks,
editMode,
onEdit,
onCancel,
onVisibilityChange,
saveState,
} = this.props;
if (socialLinks === null) return null;
const socialLinksObj = {};
socialLinks.forEach(({ platform, socialLink }) => {
socialLinksObj[platform] = socialLink;
});
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="Social Links" />
<ul className="list-unstyled">
{this.props.platforms.map(({ key, name }) => (
<li key={key} className="form-group">
<h6>{name}</h6>
<Input
type="text"
defaultValue={socialLinksObj[key]}
onChange={(e) => {
this.setState({
[key]: e.target.value,
});
}}
/>
</li>
))}
</ul>
<EditControls
onCancel={() => onCancel('socialLinks')}
onSave={this.onSave}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('socialLinks', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="Social Links"
showEditButton
onClickEdit={() => onEdit('socialLinks')}
showVisibility={Boolean(socialLinks.length)}
visibility="Everyone"
/>
<ul className="list-unstyled">
{this.props.platforms.map(({ key, name }) => (
<li key={key} className="form-group">
{
socialLinksObj[key] ? (
<a href={socialLinksObj[key]}>{name}</a>
) : (
<button
className="btn btn-link btn-sm"
tabIndex="0"
onClick={() => onEdit('socialLinks')}
onKeyDown={e => (e.key === 'Enter' ? onEdit('socialLinks') : null)}
>
<FontAwesomeIcon className="mr-2" icon={faPencilAlt} />{`Add ${name}`}
</button>
)
}
</li>
))}
</ul>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="Social Links" />
<ul>
{this.props.platforms.map(({ key, name }) => {
if (!socialLinksObj[key]) return null;
return (
<li key={key}>
<a href={socialLinksObj[key]}>{name}</a>
</li>
);
})}
</ul>
</React.Fragment>
),
}}
/>
);
}
}
const sectionPropTypes = {
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,
};
const sectionDefaultProps = {
editMode: 'static',
saveState: null,
};
SocialLinks.propTypes = {
...sectionPropTypes,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
platforms: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
})),
};
SocialLinks.defaultProps = {
...sectionDefaultProps,
socialLinks: [],
platforms: [
{ key: 'twitter', name: 'Twitter' },
{ key: 'linkedin', name: 'LinkedIn' },
{ key: 'facebook', name: 'Facebook' },
],
};
export default SocialLinks;

View File

@@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Icon } from '@edx/paragon';
import { Button, Spinner } from 'reactstrap';
function AsyncActionButton({
onClick,
className,
color,
style,
variant,
labels,
}) {
const renderIcon = () => {
if (variant === 'error') return <Icon className="icon fa fa-times-circle" />;
if (variant === 'complete') return <Icon className="icon fa fa-check-circle" />;
if (variant === 'pending') return <Spinner size="sm" color="white" />;
return null;
};
const renderLabel = () => {
if (variant) {
return labels[variant];
}
return labels.default;
};
return (
<Button
aria-live="assertive"
onClick={onClick}
disabled={variant === 'pending' || variant === 'complete' || variant === 'error'}
className={classNames(
'btn-async-action',
'd-inline-flex align-items-center justify-content-center',
className,
{
'btn-state-pending': variant === 'pending',
'btn-state-complete': variant === 'complete',
'btn-state-error': variant === 'error',
},
)}
color={color}
style={style}
>
<span aria-hidden className="icon-state d-inline-flex justify-content-start">
{renderIcon()}
</span>
{renderLabel()}
</Button>
);
}
export default AsyncActionButton;
AsyncActionButton.propTypes = {
onClick: PropTypes.func.isRequired,
color: PropTypes.string,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
variant: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
labels: PropTypes.shape({
default: PropTypes.string,
pending: PropTypes.string,
complete: PropTypes.string,
error: PropTypes.string,
}),
};
AsyncActionButton.defaultProps = {
className: null,
color: 'primary',
style: null,
variant: 'default',
labels: {
default: 'Save',
pending: 'Saving',
complete: 'Saved',
error: 'Save Failed',
},
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
function EditButton({ onClick, className, style }) {
return (
<button
className={classNames('btn btn-sm btn-link', className)}
onClick={onClick}
style={style}
>
<FontAwesomeIcon icon={faPencilAlt} /> Edit
</button>
);
}
export default EditButton;
EditButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
};
EditButton.defaultProps = {
className: null,
style: null,
};

View File

@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input, Button, Label, Row, Col } from 'reactstrap';
import AsyncActionButton from './AsyncActionButton';
function EditControls({
onCancel,
onSave,
visibility,
onVisibilityChange,
saveState,
}) {
return (
<Row className="align-items-center flex-wrap-1 pt-3">
<Col xs="auto" className="d-flex mb-3">
<Label className="flex-shrink-0 d-inline-block mb-0 mr-2" size="sm" for="exampleSelect">Who can see this:</Label>
<span>
<Input
className="d-inline-block"
bsSize="sm"
type="select"
name="select"
value={visibility}
onChange={onVisibilityChange}
>
<option key="Just me" value="Just me">Just me</option>
<option key="Everyone" value="Everyone">Everyone</option>
</Input>
</span>
</Col>
<Col xs="auto" className="flex-grow-1 d-flex justify-content-end mb-3">
<Button color="link" onClick={onCancel}>Cancel</Button>
<AsyncActionButton
onClick={onSave}
variant={saveState}
labels={{
default: 'Save',
pending: 'Saving',
complete: 'Saved',
error: 'Save Failed',
}}
/>
</Col>
</Row>
);
}
export default EditControls;
EditControls.propTypes = {
onCancel: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
visibility: PropTypes.oneOf(['Everyone', 'Just me']),
onVisibilityChange: PropTypes.func,
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
};
EditControls.defaultProps = {
visibility: null,
onVisibilityChange: null,
saveState: null,
};

View File

@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import EditButton from './EditButton';
import Visibility from './Visibility';
function EditableItemHeader({
content,
showVisibility,
visibility,
showEditButton,
onClickEdit,
}) {
return (
<React.Fragment>
<div className="editable-item-header mb-2">
<h6 className="font-weight-normal mb-0">
{content}
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right" onClick={onClickEdit} /> : null}
</h6>
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
</div>
</React.Fragment>
);
}
export default EditableItemHeader;
EditableItemHeader.propTypes = {
onClickEdit: PropTypes.func,
showVisibility: PropTypes.bool,
showEditButton: PropTypes.bool,
content: PropTypes.string,
visibility: PropTypes.oneOf(['Everyone', 'Just me']),
};
EditableItemHeader.defaultProps = {
onClickEdit: () => {},
showVisibility: false,
showEditButton: false,
content: '',
visibility: 'Everyone',
};

View File

@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
function EmptyContent({ children, onClick, showPlusIcon }) {
const onKeyDown = (e) => { if (e.key === 'Enter') onClick(); };
const commonProps = {
className: 'd-flex align-items-center p-3 bg-light rounded text-muted w-100',
style: {
cursor: onClick ? 'pointer' : null,
},
};
const interactiveProps = {
onClick,
onKeyDown,
role: 'button',
tabIndex: 0,
};
let props;
if (onClick) {
props = {
...commonProps,
...interactiveProps,
};
} else {
props = commonProps;
}
return (
<div {...props}>
{showPlusIcon ? <FontAwesomeIcon className="ml-1 mr-3" icon={faPencilAlt} /> : null}
{children}
</div>
);
}
export default EmptyContent;
EmptyContent.propTypes = {
onClick: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
showPlusIcon: PropTypes.bool,
};
EmptyContent.defaultProps = {
onClick: null,
children: null,
showPlusIcon: true,
};

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import TransitionReplace from './TransitionReplace';
const onChildEntered = (htmlNode) => {
const focusableElements = htmlNode.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length) {
focusableElements[0].focus();
}
};
const onChildExit = (htmlNode) => {
if (htmlNode.contains(document.activeElement)) {
document.activeElement.blur();
}
};
function SwitchContent({ expression, cases, className }) {
if (!cases[expression] && !cases.default) {
return null;
}
return (
<TransitionReplace
className={className}
onChildEntered={onChildEntered}
onChildExit={onChildExit}
>
{expression ? React.cloneElement(cases[expression], { key: expression }) : React.cloneElement(cases.default, { key: 'default' })}
</TransitionReplace>
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
className: PropTypes.string,
};
SwitchContent.defaultProps = {
expression: null,
className: null,
};
export default SwitchContent;

View File

@@ -0,0 +1,185 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Transition, TransitionGroup } from 'react-transition-group';
class TransitionReplace extends React.Component {
constructor(props) {
super(props);
this.state = {
height: null,
};
this.onChildEnter = this.onChildEnter.bind(this);
this.onChildEntering = this.onChildEntering.bind(this);
this.onChildEntered = this.onChildEntered.bind(this);
this.onChildExit = this.onChildExit.bind(this);
this.onChildExited = this.onChildExited.bind(this);
}
// Transition events are fired in this order:
//
// onEnter > onEntering > onEntered
// onExit > onExiting > onExited
//
// Keep in mind that we always have two transitions happening
// both the entering and leaving children
//
// We set the container height (for animation) in this order:
//
// 1. onChildExit (explicitly set the height to match the current current)
// 2. onChildEntering (set the height to match the new content)
// 3. onChildExited (reset the height to null)
onChildEnter(htmlNode) {
if (this.props.onChildEnter) this.props.onChildEnter(htmlNode);
}
onChildEntering(htmlNode) {
// subtract 1 from the height to improve shifts at the end of the animation
this.setState({
height: htmlNode.offsetHeight - 1,
});
}
onChildEntered(htmlNode) {
if (this.props.onChildEntered) this.props.onChildEntered(htmlNode);
}
onChildExit(htmlNode) {
// subtract 1 from the height to improve shifts at the end of the animation
this.setState({
height: htmlNode.offsetHeight - 1,
});
if (this.props.onChildExit) this.props.onChildExit(htmlNode);
}
onChildExited(htmlNode) {
this.setState({
height: null,
});
if (this.props.onChildExited) this.props.onChildExited(htmlNode);
}
render() {
const {
enterDuration,
exitDuration,
enterFadeEaseFunction,
exitFadeEaseFunction,
heightChangeEaseFunction,
} = this.props;
// Styles for the Transition
const defaultStyle = {
opacity: 0,
padding: '.1px 0',
// margin: '-1px 0',
};
const transitionStyles = {
entering: {
opacity: 1,
position: 'relative',
zIndex: 1,
transition: `opacity ${enterDuration}ms ${enterFadeEaseFunction}`,
},
entered: {
opacity: 1,
position: 'relative',
zIndex: 1,
},
exiting: {
opacity: 0,
position: 'absolute',
top: 0,
left: 0,
right: 0,
pointerEvents: 'none',
transition: `opacity ${exitDuration}ms ${exitFadeEaseFunction}`,
},
exited: {
opacity: 0,
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
};
const transitionProps = {
timeout: {
enter: enterDuration,
exit: exitDuration,
},
unmountOnExit: true,
mountOnEnter: true,
onEnter: this.onChildEnter,
onEntering: this.onChildEntering,
onEntered: this.onChildEntered,
onExit: this.onChildExit,
onExited: this.onChildExited,
};
return (
<TransitionGroup
style={{
position: 'relative',
overflow: this.state.height === null ? null : 'hidden',
height: this.state.height, // prevent rounding shifts from being too noticeable
transition: `height ${enterDuration}ms ${heightChangeEaseFunction}`,
}}
className={this.props.className}
>
{React.Children.map(this.props.children, child => (
<Transition {...transitionProps}>
{state => (
<div
style={{
...defaultStyle,
...transitionStyles[state],
}}
>
{child}
</div>
)}
</Transition>
))}
</TransitionGroup>
);
}
}
export default TransitionReplace;
TransitionReplace.propTypes = {
children: PropTypes.element.isRequired,
enterDuration: PropTypes.number,
exitDuration: PropTypes.number,
enterFadeEaseFunction: PropTypes.string,
exitFadeEaseFunction: PropTypes.string,
heightChangeEaseFunction: PropTypes.string,
className: PropTypes.string,
onChildEnter: PropTypes.func,
onChildEntered: PropTypes.func,
onChildExit: PropTypes.func,
onChildExited: PropTypes.func,
};
TransitionReplace.defaultProps = {
enterDuration: 300,
exitDuration: 300,
enterFadeEaseFunction: 'linear',
exitFadeEaseFunction: 'linear',
heightChangeEaseFunction: 'ease-in-out',
className: null,
onChildEnter: null,
onChildEntered: null,
onChildExit: null,
onChildExited: null,
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
function Visibility({ to }) {
const icon = to === 'Everyone' ? faEye : faEyeSlash;
return (
<span className="ml-auto small text-muted">
<FontAwesomeIcon icon={icon} /> {to}
</span>
);
}
export default Visibility;
Visibility.propTypes = {
to: PropTypes.oneOf(['Everyone', 'Just me']),
};
Visibility.defaultProps = {
to: 'Everyone',
};

View File

@@ -0,0 +1,599 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Row, Col, Input, Card, CardBody, CardTitle } from 'reactstrap';
import EditControls from './elements/EditControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import ProfileAvatar from './ProfileAvatar';
import SocialLinks from './SocialLinks';
import { ALL_COUNTRIES } from '../../constants/countries';
import EDUCATION from '../../constants/education';
class UserProfile 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 },
};
this.onCancel = this.onCancel.bind(this);
this.onEdit = this.onEdit.bind(this);
this.onSave = this.onSave.bind(this);
this.onChange = this.onChange.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
}
onCancel() {
this.props.closeEditableField(this.props.currentlyEditingField);
}
onEdit(fieldName) {
this.props.openEditableField(fieldName);
}
onSave(fieldName, value) {
const userAccountData = {
[fieldName]: value || this.state[fieldName].value,
};
this.props.saveUserProfile(this.props.username, userAccountData, 'Everyone', fieldName);
}
onChange(fieldName, value) {
this.setState({
[fieldName]: {
value,
visibility: this.state[fieldName].visibility,
},
});
}
onVisibilityChange(fieldName, visibility) {
this.setState({
[fieldName]: {
value: this.state[fieldName].value,
visibility,
},
});
}
render() {
const {
saveState,
error,
profileImage,
username,
fullName,
userLocation,
bio,
education,
socialLinks,
certificates,
} = this.props;
const commonProps = {
onSave: this.onSave,
onEdit: this.onEdit,
onCancel: this.onCancel,
onChange: this.onChange,
onVisibilityChange: this.onVisibilityChange,
saveState,
error,
};
const getEditMode = (name) => {
if (name === this.props.currentlyEditingField) return 'editing';
return 'editable';
};
return (
<div>
<div className="bg-banner bg-program-micro-masters d-none d-md-block p-relative" />
<Container fluid>
<Row>
<Col md={4} lg={3}>
<div className="d-flex align-items-center d-md-block mt-4 mt-md-0">
<ProfileAvatar
className="mb-md-3"
src={profileImage}
{...commonProps}
/>
<div>
<h2 className="mb-0">{username}</h2>
<p className="mb-0">Member since 2017</p>
</div>
</div>
</Col>
</Row>
<Row>
<Col xs={{ order: 2 }} md={{ size: 4, order: 1 }} lg={3} className="mt-md-4">
<FullName
fullName={fullName}
editMode={getEditMode('fullName')}
{...commonProps}
/>
<UserLocation
userLocation={userLocation}
editMode={getEditMode('userLocation')}
{...commonProps}
/>
<Education
education={education}
editMode={getEditMode('education')}
{...commonProps}
/>
<SocialLinks
socialLinks={socialLinks}
editMode={getEditMode('socialLinks')}
{...commonProps}
/>
</Col>
<Col xs={{ order: 1 }} md={{ size: 8, order: 2 }} lg={{ size: 8, offset: 1 }} className="mt-4 mt-md-n5">
<Bio
bio={bio}
editMode={getEditMode('bio')}
{...commonProps}
/>
<MyCertificates
certificates={certificates}
editMode={getEditMode('certificates')}
{...commonProps}
/>
</Col>
</Row>
</Container>
</div>
);
}
}
export default UserProfile;
UserProfile.propTypes = {
currentlyEditingField: PropTypes.string,
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
error: PropTypes.string,
profileImage: PropTypes.string,
fullName: PropTypes.string,
username: PropTypes.string,
userLocation: PropTypes.string,
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,
})),
saveUserProfile: PropTypes.func,
openEditableField: PropTypes.func.isRequired,
closeEditableField: PropTypes.func.isRequired,
};
UserProfile.defaultProps = {
currentlyEditingField: null,
saveState: null,
error: null,
profileImage: 'https://source.unsplash.com/featured/200x200/?face',
fullName: 'Hermione Granger',
username: 'itslevioooosa20',
userLocation: 'London, UK',
education: null,
socialLinks: [],
aboutMe: 'These are some words about me and who I am as a person.',
bio: 'These are some words about me and who I am as a person.',
certificates: [{ title: 'Certificate 1' }, { title: 'Certificate 2' }, { title: 'Certificate 3' }],
saveUserProfile: null,
};
const sectionPropTypes = {
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,
};
const sectionDefaultProps = {
editMode: 'static',
saveState: null,
};
function FullName({
fullName,
editMode,
onEdit,
onChange,
onSave,
onCancel,
onVisibilityChange,
saveState,
}) {
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="Full Name" />
<Input
type="text"
name="fullName"
defaultValue={fullName}
onChange={e => onChange('fullName', e.target.value)}
/>
<EditControls
onCancel={() => onCancel('fullName')}
onSave={() => onSave('fullName')}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('fullName', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="Full Name"
showEditButton
onClickEdit={() => onEdit('fullName')}
showVisibility={Boolean(fullName)}
visibility="Everyone"
/>
{fullName ? (
<h5>{fullName}</h5>
) : (
<EmptyContent onClick={() => onEdit('fullName')}>Add name</EmptyContent>
)}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="Full Name" />
<h5>{fullName}</h5>
</React.Fragment>
),
}}
/>
);
}
FullName.propTypes = {
...sectionPropTypes,
fullName: PropTypes.string,
};
FullName.defaultProps = {
...sectionDefaultProps,
fullName: null,
};
function UserLocation({
userLocation,
editMode,
onEdit,
onChange,
onSave,
onCancel,
onVisibilityChange,
saveState,
}) {
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="Location" />
<Input
type="select"
name="userLocation"
className="w-100"
defaultValue={userLocation}
onChange={e => onChange('userLocation', e.target.value)}
>
{Object.keys(ALL_COUNTRIES).map(key => (
<option key={key} value={key}>{ALL_COUNTRIES[key]}</option>
))}
</Input>
<EditControls
onCancel={() => onCancel('userLocation')}
onSave={() => onSave('userLocation')}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('userLocation', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="Location"
showEditButton
onClickEdit={() => onEdit('userLocation')}
showVisibility={Boolean(userLocation)}
visibility="Everyone"
/>
{userLocation ? (
<h5>{ALL_COUNTRIES[userLocation]}</h5>
) : (
<EmptyContent onClick={() => onEdit('userLocation')}>Add location</EmptyContent>
)}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="Location" />
<h5>{ALL_COUNTRIES[userLocation]}</h5>
</React.Fragment>
),
}}
/>
);
}
UserLocation.propTypes = {
...sectionPropTypes,
userLocation: PropTypes.string,
};
UserLocation.defaultProps = {
...sectionDefaultProps,
userLocation: null,
};
function Education({
education,
editMode,
onEdit,
onChange,
onSave,
onCancel,
onVisibilityChange,
saveState,
}) {
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="Education" />
<Input
type="select"
name="education"
className="w-100"
defaultValue={education}
onChange={e => onChange('education', e.target.value)}
>
{Object.keys(EDUCATION).map(key => (
<option key={key} value={key}>{EDUCATION[key]}</option>
))}
</Input>
<EditControls
onCancel={() => onCancel('education')}
onSave={() => onSave('education')}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('education', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="Education"
showEditButton
onClickEdit={() => onEdit('education')}
showVisibility={Boolean(education)}
visibility="Everyone"
/>
{education ? (
<h5>{EDUCATION[education]}</h5>
) : (
<EmptyContent onClick={() => onEdit('education')}>Add education</EmptyContent>
)}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="Education" />
<h5>{EDUCATION[education]}</h5>
</React.Fragment>
),
}}
/>
);
}
Education.propTypes = {
...sectionPropTypes,
education: PropTypes.string,
};
Education.defaultProps = {
...sectionDefaultProps,
education: null,
};
function Bio({
bio,
editMode,
onEdit,
onChange,
onSave,
onCancel,
onVisibilityChange,
saveState,
}) {
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="About Me" />
<Input
type="textarea"
name="bio"
defaultValue={bio}
onChange={e => onChange('bio', e.target.value)}
/>
<EditControls
onCancel={() => onCancel('bio')}
onSave={() => onSave('bio')}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('bio', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="About Me"
showEditButton
onClickEdit={() => onEdit('bio')}
showVisibility={Boolean(bio)}
visibility="Everyone"
/>
{bio ? (
<p className="lead">{bio}</p>
) : (
<EmptyContent onClick={() => onEdit('bio')}>Tell other learners a little about yourself...</EmptyContent>
)}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="About Me" />
<p className="lead">{bio}</p>,
</React.Fragment>
),
}}
/>
);
}
Bio.propTypes = {
...sectionPropTypes,
bio: PropTypes.string,
};
Bio.defaultProps = {
...sectionDefaultProps,
bio: null,
};
function MyCertificates({
certificates,
editMode,
onEdit,
onSave,
onCancel,
onVisibilityChange,
saveState,
}) {
const renderCertificates = () => {
if (!certificates) {
return <EmptyContent onClick={() => onEdit('certificates')}>You don&quot;t have any certificates yet.</EmptyContent>;
}
return (
<Row>
{certificates.map(({ title }) => (
<Col key={title} sm={6}>
<Card className="mb-4">
<CardBody>
<CardTitle>{title}</CardTitle>
</CardBody>
</Card>
</Col>
))}
</Row>
);
};
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<React.Fragment>
<EditableItemHeader content="My Certificates" />
{renderCertificates()}
<EditControls
onCancel={() => onCancel('certificates')}
onSave={() => onSave('certificates')}
saveState={saveState}
visibility="Everyone"
onVisibilityChange={e => onVisibilityChange('certificates', e.target.value)}
/>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content="My Certificates"
showEditButton
onClickEdit={() => onEdit('certificates')}
showVisibility={Boolean(certificates)}
visibility="Everyone"
/>
{renderCertificates()}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content="My Certificates" />
{renderCertificates()}
</React.Fragment>
),
}}
/>
);
}
MyCertificates.propTypes = {
...sectionPropTypes,
certificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
};
MyCertificates.defaultProps = {
...sectionDefaultProps,
certificates: null,
};