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:
committed by
Douglas Hall
parent
31d89ce8ef
commit
e076356455
80
src/components/UserProfile/ProfileAvatar.jsx
Normal file
80
src/components/UserProfile/ProfileAvatar.jsx
Normal 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,
|
||||
};
|
||||
170
src/components/UserProfile/SocialLinks.jsx
Normal file
170
src/components/UserProfile/SocialLinks.jsx
Normal 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;
|
||||
86
src/components/UserProfile/elements/AsyncActionButton.jsx
Normal file
86
src/components/UserProfile/elements/AsyncActionButton.jsx
Normal 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',
|
||||
},
|
||||
};
|
||||
30
src/components/UserProfile/elements/EditButton.jsx
Normal file
30
src/components/UserProfile/elements/EditButton.jsx
Normal 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,
|
||||
};
|
||||
62
src/components/UserProfile/elements/EditControls.jsx
Normal file
62
src/components/UserProfile/elements/EditControls.jsx
Normal 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,
|
||||
};
|
||||
44
src/components/UserProfile/elements/EditableItemHeader.jsx
Normal file
44
src/components/UserProfile/elements/EditableItemHeader.jsx
Normal 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',
|
||||
};
|
||||
56
src/components/UserProfile/elements/EmptyContent.jsx
Normal file
56
src/components/UserProfile/elements/EmptyContent.jsx
Normal 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,
|
||||
};
|
||||
49
src/components/UserProfile/elements/SwitchContent.jsx
Normal file
49
src/components/UserProfile/elements/SwitchContent.jsx
Normal 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;
|
||||
185
src/components/UserProfile/elements/TransitionReplace.jsx
Normal file
185
src/components/UserProfile/elements/TransitionReplace.jsx
Normal 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,
|
||||
};
|
||||
25
src/components/UserProfile/elements/Visibility.jsx
Normal file
25
src/components/UserProfile/elements/Visibility.jsx
Normal 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',
|
||||
};
|
||||
599
src/components/UserProfile/index.jsx
Normal file
599
src/components/UserProfile/index.jsx
Normal 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"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,
|
||||
};
|
||||
Reference in New Issue
Block a user