UI for adding by address.

This commit is contained in:
Will O'Beirne 2018-11-09 14:54:04 -05:00
parent 22487b331b
commit 6000d015ff
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
5 changed files with 178 additions and 269 deletions

View File

@ -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;
}
}
}

View File

@ -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 doesnt 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 => ({

View File

@ -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);

View File

@ -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);
};

View File

@ -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);
}