zcash-grant-system/frontend/client/components/Profile/ProfileEdit.tsx

363 lines
10 KiB
TypeScript

import React from 'react';
import lodash from 'lodash';
import qs from 'query-string';
import { withRouter, RouteComponentProps, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import axios from 'api/axios';
import { getSocialAuthUrl, verifySocial } from 'api/api';
import { usersActions } from 'modules/users';
import { AppState } from 'store/reducers';
import { Input, Form, Col, Row, Button, Alert, Icon } from 'antd';
import { SOCIAL_INFO } from 'utils/social';
import { SOCIAL_SERVICE, User } from 'types';
import { UserState } from 'modules/users/reducers';
import { validateUserProfile } from 'modules/create/utils';
import AvatarEdit from './AvatarEdit';
import './ProfileEdit.less';
interface OwnProps {
user: UserState;
}
interface StateProps {
authUser: AppState['auth']['user'];
hasCheckedAuthUser: AppState['auth']['hasCheckedUser'];
}
interface DispatchProps {
fetchUser: typeof usersActions['fetchUser'];
updateUser: typeof usersActions['updateUser'];
}
type Props = OwnProps & StateProps & DispatchProps & RouteComponentProps;
interface State {
fields: User;
isChanged: boolean;
showError: boolean;
isDone: boolean;
socialVerificationMessage: string;
socialVerificationError: string;
activeSocialService: SOCIAL_SERVICE | null;
}
class ProfileEdit extends React.PureComponent<Props, State> {
state: State = {
fields: { ...this.props.user } as User,
isChanged: false,
showError: false,
isDone: false,
socialVerificationError: '',
socialVerificationMessage: '',
activeSocialService: null,
};
componentDidMount() {
this.verifySocial();
}
componentDidUpdate(prevProps: Props, _: State) {
if (
prevProps.user.isUpdating &&
!this.props.user.isUpdating &&
!this.state.showError
) {
this.setState({ showError: true });
}
if (
prevProps.user.isUpdating &&
!this.props.user.isUpdating &&
!this.props.user.updateError
) {
this.handleDone();
}
}
render() {
const { fields } = this.state;
const {
user,
user: { userid },
} = this.props;
const {
socialVerificationMessage,
socialVerificationError,
activeSocialService,
} = this.state;
const error = validateUserProfile(fields);
const isMissingField = !fields.displayName || !fields.title;
const isDisabled =
!!error ||
isMissingField ||
!this.state.isChanged ||
!!this.state.activeSocialService;
if (this.state.isDone) {
return <Redirect to={`/profile/${userid}`} />;
}
return (
<>
<div className="ProfileEdit">
<AvatarEdit
user={fields}
onDone={this.handleChangePhoto}
onDelete={this.handleDeletePhoto}
/>
<div className="ProfileEdit-info">
<Form
className="ProfileEdit-info-form"
layout="vertical"
onSubmit={this.handleSave}
>
<Form.Item>
<Input
name="displayName"
autoComplete="off"
placeholder="Display name (Required)"
value={fields.displayName}
onChange={this.handleChangeField}
/>
</Form.Item>
<Form.Item>
<Input
name="title"
autoComplete="off"
placeholder="Title (Required)"
value={fields.title}
onChange={this.handleChangeField}
/>
</Form.Item>
<Row gutter={12}>
{Object.values(SOCIAL_INFO).map(s => {
const field = fields.socialMedias.find(sm => sm.service === s.service);
const loading = s.service === activeSocialService;
return (
<Col xs={24} md={8} key={s.service}>
<Form.Item>
{field &&
field.username && (
<Button
className="ProfileEdit-socialButton is-delete"
type="primary"
ghost
onClick={() => this.handleSocialDelete(s.service)}
loading={loading}
block
>
<div className="ProfileEdit-socialButton-text">
{!loading && s.icon} <strong>{field.username}</strong>
</div>
<div className="ProfileEdit-socialButton-delete">
<Icon type="delete" /> Unlink account
</div>
</Button>
)}
{!field && (
<Button
className="ProfileEdit-socialButton is-add"
onClick={() => this.handleSocialAdd(s.service)}
loading={loading}
block
>
{!loading && s.icon} <>Connect to {s.name}</>
</Button>
)}
</Form.Item>
</Col>
);
})}
</Row>
{socialVerificationError && (
<Alert type="error" message={socialVerificationError} closable />
)}
{socialVerificationMessage && (
<Alert type="success" message={socialVerificationMessage} closable />
)}
{!isMissingField &&
error && <Alert type="error" message={error} showIcon />}
<Row>
<Button
type="primary"
htmlType="submit"
disabled={isDisabled}
loading={user.isUpdating}
>
Save changes
</Button>
<Button type="ghost" htmlType="button" onClick={this.handleCancel}>
Cancel
</Button>
</Row>
</Form>
{this.state.showError &&
user.updateError && (
<Alert
className="ProfileEdit-alert"
message={`There was an error attempting to update your profile. (code ${
user.updateError
})`}
type="error"
/>
)}
</div>
</div>
<div className="ProfileEditShade" />
</>
);
}
private verifySocial = () => {
const args = qs.parse(this.props.location.search);
const { userid } = this.props.user;
if (args.code && args.service) {
this.setState({ activeSocialService: args.service });
verifySocial(args.service, args.code)
.then(async res => {
// refresh user data
await this.props.fetchUser(userid.toString());
// update just the socialMedias on state.fields
const socialMedias = this.props.user.socialMedias;
const fields = {
...this.state.fields,
socialMedias,
};
this.setState({
fields,
activeSocialService: null,
socialVerificationMessage: `
Verified ${res.data.username} on ${args.service.toLowerCase()}.
`,
});
// remove search query from url
this.props.history.push({ pathname: `/profile/${userid}/edit` });
})
.catch(e => {
this.setState({
activeSocialService: null,
socialVerificationError: e.message || e.toString(),
});
});
}
};
private handleSave = (evt: React.SyntheticEvent<any>) => {
evt.preventDefault();
this.props.updateUser(this.state.fields);
};
private handleCancel = () => {
const propsAvatar = this.props.user.avatar;
const stateAvatar = this.state.fields.avatar;
// cleanup uploaded file if we cancel
if (
stateAvatar &&
stateAvatar.imageUrl &&
(!propsAvatar || propsAvatar.imageUrl !== stateAvatar.imageUrl)
) {
axios.delete('/api/v1/users/avatar', {
params: { url: stateAvatar.imageUrl },
});
}
this.handleDone();
};
private handleChangeField = (ev: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = ev.currentTarget;
const fields = {
...this.state.fields,
[name as any]: value,
};
const isChanged = this.isChangedCheck(fields);
this.setState({
isChanged,
fields,
});
};
private handleSocialAdd = async (service: SOCIAL_SERVICE) => {
this.setState({ activeSocialService: service });
if (this.state.isChanged) {
// save any changes first
await this.props.updateUser(this.state.fields);
}
getSocialAuthUrl(service)
.then(res => {
window.location.href = res.data.url;
})
.catch(e => {
this.setState({
activeSocialService: null,
socialVerificationError: e.message || e.toString(),
});
});
};
private handleSocialDelete = (service: SOCIAL_SERVICE) => {
const socialMedias = this.state.fields.socialMedias.filter(
sm => sm.service !== service,
);
const fields = {
...this.state.fields,
socialMedias,
};
this.setState({
fields,
isChanged: this.isChangedCheck(fields),
});
};
private handleChangePhoto = (url: string) => {
const fields = {
...this.state.fields,
avatar: {
imageUrl: url,
},
};
const isChanged = this.isChangedCheck(fields);
this.setState({
isChanged,
fields,
});
};
private handleDeletePhoto = () => {
const fields = {
...this.state.fields,
avatar: null,
};
const isChanged = this.isChangedCheck(fields);
this.setState({ isChanged, fields });
};
private isChangedCheck = (a: User) => {
return !lodash.isEqual(a, this.props.user);
};
private handleDone = () => {
this.setState({ isDone: true });
};
}
const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
state => ({
authUser: state.auth.user,
hasCheckedAuthUser: state.auth.hasCheckedUser,
}),
{
fetchUser: usersActions.fetchUser,
updateUser: usersActions.updateUser,
},
);
export default compose<Props, OwnProps>(
withRouter,
withConnect,
)(ProfileEdit);