UI for adding by address.
This commit is contained in:
parent
22487b331b
commit
6000d015ff
|
@ -6,49 +6,76 @@
|
|||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
&-pending,
|
||||
&-add {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 80ms ease, transform 80ms ease;
|
||||
outline: none;
|
||||
margin-top: 2rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
&:active {
|
||||
transform: translateY(2px);
|
||||
&-title {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
&-pending {
|
||||
&-invite {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1.25rem;
|
||||
width: 7.4rem;
|
||||
height: 7.4rem;
|
||||
border: 2px dashed @success-color;
|
||||
color: @success-color;
|
||||
border-radius: 8px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 1px 2px rgba(#000, 0.2);
|
||||
border-bottom: 1px solid rgba(#000, 0.05);
|
||||
|
||||
&-text {
|
||||
text-align: left;
|
||||
|
||||
&-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 300;
|
||||
color: @success-color;
|
||||
&:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
opacity: 0.7;
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
opacity: 0.3;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
transition: opacity 100ms ease, color 100ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: @error-color;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-add {
|
||||
&-form {
|
||||
display: flex;
|
||||
padding: 1rem 1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
background: #FFF;
|
||||
box-shadow: 0 1px 2px rgba(#000, 0.2);
|
||||
|
||||
&-field {
|
||||
flex: 1;
|
||||
|
||||
.ant-form-explain {
|
||||
margin-top: 0.3rem;
|
||||
padding-left: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-submit {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Icon } from 'antd';
|
||||
import { Icon, Form, Input, Button, Popconfirm } from 'antd';
|
||||
import { CreateFormState, TeamMember } from 'types';
|
||||
import TeamMemberComponent from './TeamMember';
|
||||
import './Team.less';
|
||||
import { isValidEthAddress, isValidEmail } from 'utils/validators';
|
||||
import { AppState } from 'store/reducers';
|
||||
import './Team.less';
|
||||
|
||||
interface State {
|
||||
team: TeamMember[];
|
||||
teamInvites: string[];
|
||||
invite: string;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
|
@ -33,6 +36,8 @@ const DEFAULT_STATE: State = {
|
|||
socialAccounts: {},
|
||||
},
|
||||
],
|
||||
teamInvites: [],
|
||||
invite: '',
|
||||
};
|
||||
|
||||
class CreateFlowTeam extends React.Component<Props, State> {
|
||||
|
@ -60,7 +65,13 @@ class CreateFlowTeam extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { team } = this.state;
|
||||
const { team, teamInvites, invite } = this.state;
|
||||
const inviteError =
|
||||
invite && !isValidEmail(invite) && !isValidEthAddress(invite)
|
||||
? 'That doesn’t look like an email address or ETH address'
|
||||
: undefined;
|
||||
const inviteDisabled = !!inviteError || !invite;
|
||||
|
||||
return (
|
||||
<div className="TeamForm">
|
||||
{team.map((user, idx) => (
|
||||
|
@ -68,39 +79,76 @@ class CreateFlowTeam extends React.Component<Props, State> {
|
|||
key={idx}
|
||||
index={idx}
|
||||
user={user}
|
||||
initialEditingState={!user.name}
|
||||
onChange={this.handleChange}
|
||||
onRemove={this.removeMember}
|
||||
/>
|
||||
))}
|
||||
{team.length < MAX_TEAM_SIZE && (
|
||||
<button className="TeamForm-add" onClick={this.addMember}>
|
||||
<div className="TeamForm-add-icon">
|
||||
<Icon type="plus" />
|
||||
</div>
|
||||
<div className="TeamForm-add-text">
|
||||
<div className="TeamForm-add-text-title">Add a team member</div>
|
||||
<div className="TeamForm-add-text-subtitle">
|
||||
Find an existing user, or fill out their info yourself
|
||||
{!!teamInvites.length && (
|
||||
<div className="TeamForm-pending">
|
||||
<h3 className="TeamForm-pending-title">Pending invitations</h3>
|
||||
{teamInvites.map((ti, idx) => (
|
||||
<div key={ti} className="TeamForm-pending-invite">
|
||||
<div className="TeamForm-pending-invite-name">{ti}</div>
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
onConfirm={() => this.removeInvitation(idx)}
|
||||
>
|
||||
<button className="TeamForm-pending-invite-delete">
|
||||
<Icon type="delete" />
|
||||
</button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{team.length < MAX_TEAM_SIZE && (
|
||||
<div className="TeamForm-add">
|
||||
<h3 className="TeamForm-add-title">Add a team member</h3>
|
||||
<Form className="TeamForm-add-form" onSubmit={this.handleAddSubmit}>
|
||||
<Form.Item
|
||||
className="TeamForm-add-form-field"
|
||||
validateStatus={inviteError ? 'error' : undefined}
|
||||
help={
|
||||
inviteError ||
|
||||
'They will be notified and will have to accept the invitation before being added'
|
||||
}
|
||||
>
|
||||
<Input
|
||||
className="TeamForm-add-form-field-input"
|
||||
placeholder="Email address or ETH address"
|
||||
size="large"
|
||||
value={invite}
|
||||
onChange={this.handleChangeInvite}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
className="TeamForm-add-form-submit"
|
||||
type="primary"
|
||||
disabled={inviteDisabled}
|
||||
htmlType="submit"
|
||||
icon="user-add"
|
||||
size="large"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange = (user: TeamMember, idx: number) => {
|
||||
const team = [...this.state.team];
|
||||
team[idx] = user;
|
||||
this.setState({ team });
|
||||
this.props.updateForm({ team });
|
||||
private handleChangeInvite = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ invite: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private addMember = () => {
|
||||
const team = [...this.state.team, { ...DEFAULT_STATE.team[0] }];
|
||||
this.setState({ team });
|
||||
this.props.updateForm({ team });
|
||||
private handleAddSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
const teamInvites = [...this.state.teamInvites, this.state.invite];
|
||||
this.setState({
|
||||
teamInvites,
|
||||
invite: '',
|
||||
});
|
||||
this.props.updateForm({ teamInvites });
|
||||
};
|
||||
|
||||
private removeMember = (index: number) => {
|
||||
|
@ -111,6 +159,15 @@ class CreateFlowTeam extends React.Component<Props, State> {
|
|||
this.setState({ team });
|
||||
this.props.updateForm({ team });
|
||||
};
|
||||
|
||||
private removeInvitation = (index: number) => {
|
||||
const teamInvites = [
|
||||
...this.state.teamInvites.slice(0, index),
|
||||
...this.state.teamInvites.slice(index + 1),
|
||||
];
|
||||
this.setState({ teamInvites });
|
||||
this.props.updateForm({ teamInvites });
|
||||
};
|
||||
}
|
||||
|
||||
const withConnect = connect<StateProps, {}, {}, AppState>(state => ({
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin: 0 auto 1rem;
|
||||
border-radius: 2px;
|
||||
background: #FFF;
|
||||
box-shadow: 0 1px 2px rgba(#000, 0.2);
|
||||
|
||||
|
|
|
@ -1,240 +1,60 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Input, Form, Col, Row, Button, Icon, Alert } from 'antd';
|
||||
import { Icon } from 'antd';
|
||||
import { SOCIAL_INFO } from 'utils/social';
|
||||
import { SOCIAL_TYPE, TeamMember } from 'types';
|
||||
import { getCreateTeamMemberError } from 'modules/create/utils';
|
||||
import { TeamMember } from 'types';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import './TeamMember.less';
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
user: TeamMember;
|
||||
initialEditingState?: boolean;
|
||||
onChange(user: TeamMember, index: number): void;
|
||||
onRemove(index: number): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fields: TeamMember;
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
export default class CreateFlowTeamMember extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
fields: { ...this.props.user },
|
||||
isEditing: this.props.initialEditingState || false,
|
||||
};
|
||||
|
||||
export default class CreateFlowTeamMember extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { user, index } = this.props;
|
||||
const { fields, isEditing } = this.state;
|
||||
const error = getCreateTeamMemberError(fields);
|
||||
const isMissingField =
|
||||
!fields.name || !fields.title || !fields.emailAddress || !fields.ethAddress;
|
||||
const isDisabled = !!error || isMissingField;
|
||||
|
||||
return (
|
||||
<div className={classnames('TeamMember', isEditing && 'is-editing')}>
|
||||
<div className="TeamMember">
|
||||
<div className="TeamMember-avatar">
|
||||
<UserAvatar className="TeamMember-avatar-img" user={fields} />
|
||||
{isEditing && (
|
||||
<Button className="TeamMember-avatar-change" onClick={this.handleChangePhoto}>
|
||||
Change
|
||||
</Button>
|
||||
)}
|
||||
<UserAvatar className="TeamMember-avatar-img" user={user} />
|
||||
</div>
|
||||
<div className="TeamMember-info">
|
||||
{isEditing ? (
|
||||
<Form
|
||||
className="TeamMember-info-form"
|
||||
layout="vertical"
|
||||
onSubmit={this.toggleEditing}
|
||||
>
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="name"
|
||||
autoComplete="off"
|
||||
placeholder="Display name (Required)"
|
||||
value={fields.name}
|
||||
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}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="ethAddress"
|
||||
autoComplete="ethAddress"
|
||||
placeholder="Ethereum address (Required)"
|
||||
value={fields.ethAddress}
|
||||
onChange={this.handleChangeField}
|
||||
<div className="TeamMember-info-name">{user.name || <em>No name</em>}</div>
|
||||
<div className="TeamMember-info-title">{user.title || <em>No title</em>}</div>
|
||||
<div className="TeamMember-info-social">
|
||||
{Object.values(SOCIAL_INFO).map(s => {
|
||||
const account = user.socialAccounts[s.type];
|
||||
const cn = classnames(
|
||||
'TeamMember-info-social-icon',
|
||||
account && 'is-active',
|
||||
);
|
||||
return (
|
||||
<div key={s.name} className={cn}>
|
||||
{s.icon}
|
||||
{account && (
|
||||
<Icon
|
||||
className="TeamMember-info-social-icon-check"
|
||||
type="check-circle"
|
||||
theme="filled"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="emailAddress"
|
||||
placeholder="Email address (Required)"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={fields.emailAddress}
|
||||
onChange={this.handleChangeField}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
{Object.values(SOCIAL_INFO).map(s => (
|
||||
<Col xs={24} sm={12} key={s.type}>
|
||||
<Form.Item>
|
||||
<Input
|
||||
placeholder={`${s.name} account`}
|
||||
autoComplete="off"
|
||||
value={fields.socialAccounts[s.type]}
|
||||
onChange={ev => this.handleSocialChange(ev, s.type)}
|
||||
addonBefore={s.icon}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{!isMissingField &&
|
||||
error && (
|
||||
<Alert
|
||||
type="error"
|
||||
message={error}
|
||||
showIcon
|
||||
style={{ marginBottom: '0.75rem' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<Button type="primary" htmlType="submit" disabled={isDisabled}>
|
||||
Save changes
|
||||
</Button>
|
||||
<Button type="ghost" htmlType="button" onClick={this.cancelEditing}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Row>
|
||||
</Form>
|
||||
) : (
|
||||
<>
|
||||
<div className="TeamMember-info-name">{user.name || <em>No name</em>}</div>
|
||||
<div className="TeamMember-info-title">
|
||||
{user.title || <em>No title</em>}
|
||||
</div>
|
||||
<div className="TeamMember-info-social">
|
||||
{Object.values(SOCIAL_INFO).map(s => {
|
||||
const account = user.socialAccounts[s.type];
|
||||
const cn = classnames(
|
||||
'TeamMember-info-social-icon',
|
||||
account && 'is-active',
|
||||
);
|
||||
return (
|
||||
<div key={s.name} className={cn}>
|
||||
{s.icon}
|
||||
{account && (
|
||||
<Icon
|
||||
className="TeamMember-info-social-icon-check"
|
||||
type="check-circle"
|
||||
theme="filled"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{index !== 0 && (
|
||||
<>
|
||||
<button className="TeamMember-info-edit" onClick={this.toggleEditing}>
|
||||
<Icon type="form" /> Edit
|
||||
</button>
|
||||
<button className="TeamMember-info-remove" onClick={this.removeMember}>
|
||||
<Icon type="close-circle" theme="filled" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{index !== 0 && (
|
||||
<button className="TeamMember-info-remove" onClick={this.removeMember}>
|
||||
<Icon type="close-circle" theme="filled" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleEditing = (ev?: React.SyntheticEvent<any>) => {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const { isEditing, fields } = this.state;
|
||||
if (isEditing) {
|
||||
// TODO: Check if valid first
|
||||
this.props.onChange(fields, this.props.index);
|
||||
}
|
||||
|
||||
this.setState({ isEditing: !isEditing });
|
||||
};
|
||||
|
||||
private cancelEditing = () => {
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
fields: { ...this.props.user },
|
||||
});
|
||||
};
|
||||
|
||||
private handleChangeField = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = ev.currentTarget;
|
||||
this.setState({
|
||||
fields: {
|
||||
...this.state.fields,
|
||||
[name as any]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private handleSocialChange = (
|
||||
ev: React.ChangeEvent<HTMLInputElement>,
|
||||
type: SOCIAL_TYPE,
|
||||
) => {
|
||||
const { value } = ev.currentTarget;
|
||||
this.setState({
|
||||
fields: {
|
||||
...this.state.fields,
|
||||
socialAccounts: {
|
||||
...this.state.fields.socialAccounts,
|
||||
[type]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private handleChangePhoto = () => {
|
||||
// TODO: Actual file uploading
|
||||
const gender = ['men', 'women'][Math.floor(Math.random() * 2)];
|
||||
const num = Math.floor(Math.random() * 80);
|
||||
this.setState({
|
||||
fields: {
|
||||
...this.state.fields,
|
||||
avatarUrl: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private removeMember = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
|
|
@ -29,3 +29,7 @@ export function isValidEthAddress(addr: string): boolean {
|
|||
return addr === toChecksumAddress(addr);
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidEmail(email: string): boolean {
|
||||
return /\S+@\S+\.\S+/.test(email);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue