Check in user refactor. Incomplete, but computer is crashing routinely.

This commit is contained in:
Will O'Beirne 2018-11-16 18:05:17 -05:00
parent 89c1c3345d
commit d7e4c1c533
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
31 changed files with 3790 additions and 3876 deletions

View File

@ -3,6 +3,7 @@ from grant.comment.models import Comment
from grant.email.models import EmailVerification
from grant.extensions import ma, db
from grant.utils.misc import make_url
from grant.utils.social import get_social_info_from_url
from grant.email.send import send_email
@ -122,7 +123,21 @@ class SocialMediaSchema(ma.Schema):
class Meta:
model = SocialMedia
# Fields to expose
fields = ("social_media_link",)
fields = (
"service",
"username",
)
service = ma.Method("get_service")
username = ma.Method("get_username")
def get_service(self, obj):
info = get_social_info_from_url(obj.social_media_link)
return info['service']
def get_username(self, obj):
info = get_social_info_from_url(obj.social_media_link)
return info['username']
social_media_schema = SocialMediaSchema()

View File

@ -39,9 +39,13 @@ def get_me():
@blueprint.route("/<user_identity>", methods=["GET"])
@endpoint.api()
def get_user(user_identity):
print('get by ident')
user = User.get_by_identifier(email_address=user_identity, account_address=user_identity)
print(user)
if user:
print('dumping')
result = user_schema.dump(user)
print(result)
return result
else:
message = "User with account_address or user_identity matching {} not found".format(user_identity)
@ -126,7 +130,7 @@ def auth_user(account_address, signed_message, raw_typed_data):
parameter('displayName', type=str, required=True),
parameter('title', type=str, required=True),
parameter('socialMedias', type=list, required=True),
parameter('avatar', type=dict, required=True)
parameter('avatar', type=str, required=True)
)
def update_user(user_identity, display_name, title, social_medias, avatar):
user = g.current_user
@ -137,22 +141,20 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
if title is not None:
user.title = title
db_socials = SocialMedia.query.filter_by(user_id=user.id).all()
for db_social in db_socials:
db.session.delete(db_social)
if social_medias is not None:
SocialMedia.query.filter_by(user_id=user.id).delete()
for social_media in social_medias:
sm = SocialMedia(social_media_link=social_media.get("link"), user_id=user.id)
sm = SocialMedia(social_media_link=social_media, user_id=user.id)
db.session.add(sm)
else:
SocialMedia.query.filter_by(user_id=user.id).delete()
if avatar is not None:
Avatar.query.filter_by(user_id=user.id).delete()
avatar_link = avatar.get('link')
if avatar_link:
avatar_obj = Avatar(image_url=avatar_link, user_id=user.id)
db.session.add(avatar_obj)
else:
Avatar.query.filter_by(user_id=user.id).delete()
db_avatar = Avatar.query.filter_by(user_id=user.id).first()
if db_avatar:
db.session.delete(db_avatar)
if avatar:
new_avatar = Avatar(image_url=avatar, user_id=user.id)
db.session.add(new_avatar)
db.session.commit()
result = user_schema.dump(user)

View File

@ -0,0 +1,19 @@
import re
username_regex = '([a-zA-Z0-9-_]*)'
social_patterns = {
'GITHUB': 'https://github.com/{}'.format(username_regex),
'TWITTER': 'https://twitter.com/{}'.format(username_regex),
'LINKEDIN': 'https://linkedin.com/in/{}'.format(username_regex),
'KEYBASE': 'https://keybase.io/{}'.format(username_regex),
}
def get_social_info_from_url(url: str):
for service, pattern in social_patterns.items():
match = re.match(pattern, url)
if match:
return {
'service': service,
'username': match.group(1),
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,21 +2,16 @@ import axios from './axios';
import {
Proposal,
ProposalDraft,
TeamMember,
User,
Update,
TeamInvite,
TeamInviteWithProposal,
} from 'types';
import {
formatTeamMemberForPost,
formatTeamMemberFromGet,
generateProposalUrl,
} from 'utils/api';
import { formatUserForPost, generateProposalUrl } from 'utils/api';
export function getProposals(): Promise<{ data: Proposal[] }> {
return axios.get('/api/v1/proposals/').then(res => {
res.data = res.data.map((proposal: any) => {
proposal.team = proposal.team.map(formatTeamMemberFromGet);
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
return proposal;
});
@ -26,7 +21,6 @@ export function getProposals(): Promise<{ data: Proposal[] }> {
export function getProposal(proposalId: number | string): Promise<{ data: Proposal }> {
return axios.get(`/api/v1/proposals/${proposalId}`).then(res => {
res.data.team = res.data.team.map(formatTeamMemberFromGet);
res.data.proposalUrlId = generateProposalUrl(res.data.proposalId, res.data.title);
return res;
});
@ -44,15 +38,12 @@ export function postProposal(payload: ProposalDraft) {
return axios.post(`/api/v1/proposals/`, {
...payload,
// Team has a different shape for POST
team: payload.team.map(formatTeamMemberForPost),
team: payload.team.map(formatUserForPost),
});
}
export function getUser(address: string): Promise<{ data: TeamMember }> {
return axios.get(`/api/v1/users/${address}`).then(res => {
res.data = formatTeamMemberFromGet(res.data);
return res;
});
export function getUser(address: string): Promise<{ data: User }> {
return axios.get(`/api/v1/users/${address}`);
}
export function createUser(payload: {
@ -62,31 +53,20 @@ export function createUser(payload: {
title: string;
signedMessage: string;
rawTypedData: string;
}): Promise<{ data: TeamMember }> {
return axios.post('/api/v1/users', payload).then(res => {
res.data = formatTeamMemberFromGet(res.data);
return res;
});
}): Promise<{ data: User }> {
return axios.post('/api/v1/users', payload);
}
export function authUser(payload: {
accountAddress: string;
signedMessage: string;
rawTypedData: string;
}): Promise<{ data: TeamMember }> {
return axios.post('/api/v1/users/auth', payload).then(res => {
res.data = formatTeamMemberFromGet(res.data);
return res;
});
}): Promise<{ data: User }> {
return axios.post('/api/v1/users/auth', payload);
}
export function updateUser(user: TeamMember): Promise<{ data: TeamMember }> {
return axios
.put(`/api/v1/users/${user.ethAddress}`, formatTeamMemberForPost(user))
.then(res => {
res.data = formatTeamMemberFromGet(res.data);
return res;
});
export function updateUser(user: User): Promise<{ data: User }> {
return axios.put(`/api/v1/users/${user.accountAddress}`, formatUserForPost(user));
}
export function verifyEmail(code: string): Promise<any> {
@ -105,13 +85,7 @@ export function postProposalUpdate(
}
export function getProposalDrafts(): Promise<{ data: ProposalDraft[] }> {
return axios.get('/api/v1/proposals/drafts').then(res => {
res.data = res.data.map((draft: any) => ({
...draft,
team: draft.team.map(formatTeamMemberFromGet),
}));
return res;
});
return axios.get('/api/v1/proposals/drafts');
}
export function postProposalDraft(): Promise<{ data: ProposalDraft }> {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { Button, Alert } from 'antd';
import { authActions } from 'modules/auth';
import { TeamMember } from 'types';
import { User } from 'types';
import { AppState } from 'store/reducers';
import { AUTH_PROVIDER } from 'utils/auth';
import Identicon from 'components/Identicon';
@ -20,7 +20,7 @@ interface DispatchProps {
interface OwnProps {
// TODO: Use common use User type instead
user: TeamMember;
user: User;
provider: AUTH_PROVIDER;
reset(): void;
}
@ -34,11 +34,14 @@ class SignIn extends React.Component<Props> {
<div className="SignIn">
<div className="SignIn-container">
<div className="SignIn-identity">
<Identicon address={user.ethAddress} className="SignIn-identity-identicon" />
<Identicon
address={user.accountAddress}
className="SignIn-identity-identicon"
/>
<div className="SignIn-identity-info">
<div className="SignIn-identity-info-name">{user.name}</div>
<div className="SignIn-identity-info-name">{user.displayName}</div>
<code className="SignIn-identity-info-address">
<ShortAddress address={user.ethAddress} />
<ShortAddress address={user.accountAddress} />
</code>
</div>
</div>
@ -69,7 +72,7 @@ class SignIn extends React.Component<Props> {
}
private authUser = () => {
this.props.authUser(this.props.user.ethAddress);
this.props.authUser(this.props.user.accountAddress);
};
}

View File

@ -218,7 +218,7 @@ const ReviewTeam: React.SFC<{
<div className="ReviewTeam-member" key={idx}>
<UserAvatar className="ReviewTeam-member-avatar" user={u} />
<div className="ReviewTeam-member-info">
<div className="ReviewTeam-member-info-name">{u.name}</div>
<div className="ReviewTeam-member-info-name">{u.displayName}</div>
<div className="ReviewTeam-member-info-title">{u.title}</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { Icon, Form, Input, Button, Popconfirm, message } from 'antd';
import { TeamMember, TeamInvite, ProposalDraft } from 'types';
import { User, TeamInvite, ProposalDraft } from 'types';
import TeamMemberComponent from './TeamMember';
import { postProposalInvite, deleteProposalInvite } from 'api/api';
import { isValidEthAddress, isValidEmail } from 'utils/validators';
@ -9,7 +9,7 @@ import { AppState } from 'store/reducers';
import './Team.less';
interface State {
team: TeamMember[];
team: User[];
invites: TeamInvite[];
address: string;
}
@ -28,16 +28,7 @@ type Props = OwnProps & StateProps;
const MAX_TEAM_SIZE = 6;
const DEFAULT_STATE: State = {
team: [
{
name: '',
title: '',
avatarUrl: '',
ethAddress: '',
emailAddress: '',
socialAccounts: {},
},
],
team: [],
invites: [],
address: '',
};
@ -50,16 +41,8 @@ class CreateFlowTeam extends React.Component<Props, State> {
...(props.initialState || {}),
};
// Don't allow for empty team array
if (!this.state.team.length) {
this.state = {
...this.state,
team: [...DEFAULT_STATE.team],
};
}
// Auth'd user is always first member of a team
if (props.authUser) {
if (props.authUser && !this.state.team.length) {
this.state.team[0] = {
...props.authUser,
};
@ -77,8 +60,8 @@ class CreateFlowTeam extends React.Component<Props, State> {
return (
<div className="TeamForm">
{team.map((user, idx) => (
<TeamMemberComponent key={idx} index={idx} user={user} />
{team.map(user => (
<TeamMemberComponent key={user.userid} user={user} />
))}
{!!pendingInvites.length && (
<div className="TeamForm-pending">

View File

@ -2,18 +2,17 @@ import React from 'react';
import classnames from 'classnames';
import { Icon } from 'antd';
import { SOCIAL_INFO } from 'utils/social';
import { TeamMember } from 'types';
import { User } from 'types';
import UserAvatar from 'components/UserAvatar';
import './TeamMember.less';
interface Props {
index: number;
user: TeamMember;
user: User;
}
export default class CreateFlowTeamMember extends React.PureComponent<Props> {
render() {
const { user, index } = this.props;
const { user } = this.props;
return (
<div className="TeamMember">
@ -21,11 +20,13 @@ export default class CreateFlowTeamMember extends React.PureComponent<Props> {
<UserAvatar className="TeamMember-avatar-img" user={user} />
</div>
<div className="TeamMember-info">
<div className="TeamMember-info-name">{user.name || <em>No name</em>}</div>
<div className="TeamMember-info-name">
{user.displayName || <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 account = user.socialMedias.find(sm => s.service === sm.service);
const cn = classnames(
'TeamMember-info-social-icon',
account && 'is-active',

View File

@ -2,7 +2,7 @@ import React from 'react';
import lodash from 'lodash';
import { Input, Form, Col, Row, Button, Icon, Alert } from 'antd';
import { SOCIAL_INFO } from 'utils/social';
import { SOCIAL_TYPE, TeamMember } from 'types';
import { SOCIAL_SERVICE, User } from 'types';
import { UserState } from 'modules/users/reducers';
import { getCreateTeamMemberError } from 'modules/create/utils';
import UserAvatar from 'components/UserAvatar';
@ -11,18 +11,18 @@ import './ProfileEdit.less';
interface Props {
user: UserState;
onDone(): void;
onEdit(user: TeamMember): void;
onEdit(user: User): void;
}
interface State {
fields: TeamMember;
fields: User;
isChanged: boolean;
showError: boolean;
}
export default class ProfileEdit extends React.PureComponent<Props, State> {
state: State = {
fields: { ...this.props.user } as TeamMember,
fields: { ...this.props.user } as User,
isChanged: false,
showError: false,
};
@ -48,7 +48,10 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
const { fields } = this.state;
const error = getCreateTeamMemberError(fields);
const isMissingField =
!fields.name || !fields.title || !fields.emailAddress || !fields.ethAddress;
!fields.displayName ||
!fields.title ||
!fields.emailAddress ||
!fields.accountAddress;
const isDisabled = !!error || isMissingField || !this.state.isChanged;
return (
@ -62,11 +65,11 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
>
<Icon
className="ProfileEdit-avatar-change-icon"
type={fields.avatarUrl ? 'picture' : 'plus-circle'}
type={fields.avatar ? 'picture' : 'plus-circle'}
/>
<div>{fields.avatarUrl ? 'Change photo' : 'Add photo'}</div>
<div>{fields.avatar ? 'Change photo' : 'Add photo'}</div>
</Button>
{fields.avatarUrl && (
{fields.avatar && (
<Button
className="ProfileEdit-avatar-delete"
icon="delete"
@ -86,7 +89,7 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
name="name"
autoComplete="off"
placeholder="Display name (Required)"
value={fields.name}
value={fields.displayName}
onChange={this.handleChangeField}
/>
</Form.Item>
@ -115,29 +118,32 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
<Form.Item>
<Input
name="ethAddress"
name="accountAddress"
disabled={true}
autoComplete="ethAddress"
autoComplete="accountAddress"
placeholder="Ethereum address (Required)"
value={fields.ethAddress}
value={fields.accountAddress}
onChange={this.handleChangeField}
/>
</Form.Item>
<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>
))}
{Object.values(SOCIAL_INFO).map(s => {
const field = fields.socialMedias.find(sm => sm.service === s.service);
return (
<Col xs={24} sm={12} key={s.service}>
<Form.Item>
<Input
placeholder={`${s.name} account`}
autoComplete="off"
value={field ? field.username : ''}
onChange={ev => this.handleSocialChange(ev, s.service)}
addonBefore={s.icon}
/>
</Form.Item>
</Col>
);
})}
</Row>
{!isMissingField &&
@ -205,20 +211,26 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
private handleSocialChange = (
ev: React.ChangeEvent<HTMLInputElement>,
type: SOCIAL_TYPE,
service: SOCIAL_SERVICE,
) => {
const { value } = ev.currentTarget;
// First remove...
const socialMedias = this.state.fields.socialMedias.filter(
sm => sm.service !== service,
);
if (value) {
// Then re-add if there as a value
socialMedias.push({
service,
username: value,
});
}
const fields = {
...this.state.fields,
socialAccounts: {
...this.state.fields.socialAccounts,
[type]: value,
},
socialMedias,
};
// delete key for empty string
if (!value) {
delete fields.socialAccounts[type];
}
const isChanged = this.isChangedCheck(fields);
this.setState({
isChanged,
@ -232,7 +244,9 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
const num = Math.floor(Math.random() * 80);
const fields = {
...this.state.fields,
avatarUrl: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
avatar: {
image_url: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
},
};
const isChanged = this.isChangedCheck(fields);
this.setState({
@ -242,13 +256,15 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
};
private handleDeletePhoto = () => {
const fields = lodash.clone(this.state.fields);
delete fields.avatarUrl;
const fields = {
...this.state.fields,
avatar: null,
};
const isChanged = this.isChangedCheck(fields);
this.setState({ isChanged, fields });
};
private isChangedCheck = (a: TeamMember) => {
private isChangedCheck = (a: User) => {
return !lodash.isEqual(a, this.props.user);
};
}

View File

@ -42,7 +42,7 @@ class ProfileInvite extends React.Component<Props, State> {
const { invite } = this.props;
const { isAccepting, isRejecting } = this.state;
const { proposal } = invite;
const inviter = proposal.team[0] || { name: 'Unknown user' };
const inviter = proposal.team[0] || { displayName: 'Unknown user' };
return (
<div className="ProfileInvite">
<div className="ProfileInvite-info">
@ -52,7 +52,9 @@ class ProfileInvite extends React.Component<Props, State> {
<div className="ProfileInvite-info-brief">
{proposal.brief || <em>No description</em>}
</div>
<div className="ProfileInvite-info-inviter">created by {inviter.name}</div>
<div className="ProfileInvite-info-inviter">
created by {inviter.displayName}
</div>
</div>
<div className="ProfileInvite-actions">
<Button

View File

@ -30,7 +30,7 @@ export default class Profile extends React.Component<OwnProps> {
<h3>Team</h3>
<div className="ProfileProposal-block-team">
{team.map(user => (
<UserRow key={user.ethAddress || user.emailAddress} user={user} />
<UserRow key={user.accountAddress || user.emailAddress} user={user} />
))}
</div>
</div>

View File

@ -1,13 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'antd';
import { SocialInfo } from 'types';
import { SocialMedia } from 'types';
import { usersActions } from 'modules/users';
import { UserState } from 'modules/users/reducers';
import { typedKeys } from 'utils/ts';
import ProfileEdit from './ProfileEdit';
import UserAvatar from 'components/UserAvatar';
import { SOCIAL_INFO, socialAccountToUrl } from 'utils/social';
import { SOCIAL_INFO, socialMediaToUrl } from 'utils/social';
import ShortAddress from 'components/ShortAddress';
import './ProfileUser.less';
import { AppState } from 'store/reducers';
@ -39,10 +38,10 @@ class ProfileUser extends React.Component<Props> {
const {
authUser,
user,
user: { socialAccounts },
user: { socialMedias },
} = this.props;
const isSelf = !!authUser && authUser.ethAddress === user.ethAddress;
const isSelf = !!authUser && authUser.accountAddress === user.accountAddress;
if (this.state.isEditing) {
return (
@ -60,7 +59,7 @@ class ProfileUser extends React.Component<Props> {
<UserAvatar className="ProfileUser-avatar-img" user={user} />
</div>
<div className="ProfileUser-info">
<div className="ProfileUser-info-name">{user.name}</div>
<div className="ProfileUser-info-name">{user.displayName}</div>
<div className="ProfileUser-info-title">{user.title}</div>
<div>
{user.emailAddress && (
@ -69,26 +68,18 @@ class ProfileUser extends React.Component<Props> {
{user.emailAddress}
</div>
)}
{user.ethAddress && (
{user.accountAddress && (
<div className="ProfileUser-info-address">
<span>ethereum address</span>
<ShortAddress address={user.ethAddress} />
<ShortAddress address={user.accountAddress} />
</div>
)}
</div>
{Object.keys(socialAccounts).length > 0 && (
{socialMedias.length > 0 && (
<div className="ProfileUser-info-social">
{typedKeys(SOCIAL_INFO).map(
s =>
(socialAccounts[s] && (
<Social
key={s}
account={socialAccounts[s] as string}
info={SOCIAL_INFO[s]}
/>
)) ||
null,
)}
{socialMedias.map(sm => (
<Social key={sm.service} socialMedia={sm} />
))}
</div>
)}
{isSelf && (
@ -104,10 +95,12 @@ class ProfileUser extends React.Component<Props> {
}
}
const Social = ({ account, info }: { account: string; info: SocialInfo }) => {
const Social = ({ socialMedia }: { socialMedia: SocialMedia }) => {
return (
<a href={socialAccountToUrl(account, info.type)}>
<div className="ProfileUser-info-social-icon">{info.icon}</div>
<a href={socialMediaToUrl(socialMedia)} target="_blank" rel="noopener nofollow">
<div className="ProfileUser-info-social-icon">
{SOCIAL_INFO[socialMedia.service].icon}
</div>
</a>
);
};

View File

@ -5,7 +5,7 @@ import { usersActions } from 'modules/users';
import { AppState } from 'store/reducers';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { Spin, Tabs, Badge, message } from 'antd';
import { Spin, Tabs, Badge } from 'antd';
import HeaderDetails from 'components/HeaderDetails';
import ProfileUser from './ProfileUser';
import ProfileProposal from './ProfileProposal';
@ -46,8 +46,8 @@ class Profile extends React.Component<Props> {
const userLookupParam = this.props.match.params.id;
const { authUser } = this.props;
if (!userLookupParam) {
if (authUser && authUser.ethAddress) {
return <Redirect to={`/profile/${authUser.ethAddress}`} />;
if (authUser && authUser.accountAddress) {
return <Redirect to={`/profile/${authUser.accountAddress}`} />;
} else {
return <Redirect to="auth" />;
}
@ -56,7 +56,8 @@ class Profile extends React.Component<Props> {
const user = this.props.usersMap[userLookupParam];
const waiting = !user || !user.hasFetched;
// TODO: Replace with userid checks
const isAuthedUser = user && authUser && user.ethAddress === authUser.ethAddress;
const isAuthedUser =
user && authUser && user.accountAddress === authUser.accountAddress;
if (waiting) {
return <Spin />;
@ -77,9 +78,9 @@ class Profile extends React.Component<Props> {
{/* TODO: SSR fetch user details */}
{/* TODO: customize details for funders/creators */}
<HeaderDetails
title={`${user.name} is funding projects on Grant.io`}
description={`Join ${user.name} in funding the future!`}
image={user.avatarUrl}
title={`${user.displayName} is funding projects on Grant.io`}
description={`Join ${user.displayName} in funding the future!`}
image={user.avatar ? user.avatar.image_url : undefined}
/>
<ProfileUser user={user} />
<Tabs>
@ -117,7 +118,11 @@ class Profile extends React.Component<Props> {
<div>
{noneCommented && <Placeholder subtitle="Has not made any comments yet" />}
{comments.map(c => (
<ProfileComment key={c.commentId} userName={user.name} comment={c} />
<ProfileComment
key={c.commentId}
userName={user.displayName}
comment={c}
/>
))}
</div>
</Tabs.TabPane>
@ -137,7 +142,7 @@ class Profile extends React.Component<Props> {
{invites.map(invite => (
<ProfileInvite
key={invite.id}
userId={user.ethAddress}
userId={user.accountAddress}
invite={invite}
/>
))}

View File

@ -10,7 +10,7 @@ interface Props {
const TeamBlock = ({ proposal }: Props) => {
let content;
if (proposal) {
content = proposal.team.map(user => <UserRow key={user.name} user={user} />);
content = proposal.team.map(user => <UserRow key={user.displayName} user={user} />);
} else {
content = <Spin />;
}

View File

@ -65,7 +65,8 @@ export class ProposalCard extends React.Component<Props> {
<div className="ProposalCard-team">
<div className="ProposalCard-team-name">
{team[0].name} {team.length > 1 && <small>+{team.length - 1} other</small>}
{team[0].displayName}{' '}
{team.length > 1 && <small>+{team.length - 1} other</small>}
</div>
<div className="ProposalCard-team-avatars">
{[...team].reverse().map((u, idx) => (

View File

@ -1,18 +1,18 @@
import React from 'react';
import Identicon from 'components/Identicon';
import { TeamMember } from 'types';
import { User } from 'types';
import defaultUserImg from 'static/images/default-user.jpg';
interface Props {
user: TeamMember;
user: User;
className?: string;
}
const UserAvatar: React.SFC<Props> = ({ user, className }) => {
if (user.avatarUrl) {
return <img className={className} src={user.avatarUrl} />;
} else if (user.ethAddress) {
return <Identicon className={className} address={user.ethAddress} />;
if (user.avatar && user.avatar.image_url) {
return <img className={className} src={user.avatar.image_url} />;
} else if (user.accountAddress) {
return <Identicon className={className} address={user.accountAddress} />;
} else {
return <img className={className} src={defaultUserImg} />;
}

View File

@ -1,20 +1,20 @@
import React from 'react';
import UserAvatar from 'components/UserAvatar';
import { TeamMember } from 'types';
import { User } from 'types';
import { Link } from 'react-router-dom';
import './style.less';
interface Props {
user: TeamMember;
user: User;
}
const UserRow = ({ user }: Props) => (
<Link to={`/profile/${user.ethAddress || user.emailAddress}`} className="UserRow">
<Link to={`/profile/${user.accountAddress || user.emailAddress}`} className="UserRow">
<div className="UserRow-avatar">
<UserAvatar user={user} className="UserRow-avatar-img" />
</div>
<div className="UserRow-info">
<div className="UserRow-info-main">{user.name}</div>
<div className="UserRow-info-main">{user.displayName}</div>
<p className="UserRow-info-secondary">{user.title}</p>
</div>
</Link>

View File

@ -1,13 +1,13 @@
import types from './types';
// TODO: Use a common User type instead of this
import { TeamMember, AuthSignatureData } from 'types';
import { User, AuthSignatureData } from 'types';
export interface AuthState {
user: TeamMember | null;
user: User | null;
isAuthingUser: boolean;
authUserError: string | null;
checkedUsers: { [address: string]: TeamMember | false };
checkedUsers: { [address: string]: User | false };
isCheckingUser: boolean;
isCreatingUser: boolean;
@ -53,7 +53,7 @@ export default function createReducer(
...state,
user: action.payload.user,
authSignature: action.payload.authSignature, // TODO: Make this the real token
authSignatureAddress: action.payload.user.ethAddress,
authSignatureAddress: action.payload.user.accountAddress,
isAuthingUser: false,
};
case types.AUTH_USER_REJECTED:
@ -74,7 +74,7 @@ export default function createReducer(
...state,
user: action.payload.user,
authSignature: action.payload.authSignature,
authSignatureAddress: action.payload.user.ethAddress,
authSignatureAddress: action.payload.user.accountAddress,
isCreatingUser: false,
checkedUsers: {
...state.checkedUsers,

View File

@ -1,5 +1,5 @@
import { ProposalDraft, CreateMilestone } from 'types';
import { TeamMember } from 'types';
import { User } from 'types';
import { isValidEthAddress, getAmountError } from 'utils/validators';
import { MILESTONE_STATE, ProposalWithCrowdFund } from 'types';
import { ProposalContractData } from 'modules/web3/actions';
@ -153,7 +153,7 @@ export function getCreateErrors(
if (team) {
let didTeamError = false;
const teamErrors = team.map(u => {
if (!u.name || !u.title || !u.emailAddress || !u.ethAddress) {
if (!u.displayName || !u.title || !u.emailAddress || !u.accountAddress) {
didTeamError = true;
return '';
}
@ -170,14 +170,14 @@ export function getCreateErrors(
return errors;
}
export function getCreateTeamMemberError(user: TeamMember) {
if (user.name.length > 30) {
export function getCreateTeamMemberError(user: User) {
if (user.displayName.length > 30) {
return 'Display name can only be 30 characters maximum';
} else if (user.title.length > 30) {
return 'Title can only be 30 characters maximum';
} else if (!/.+\@.+\..+/.test(user.emailAddress)) {
return 'That doesnt look like a valid email address';
} else if (!isValidEthAddress(user.ethAddress)) {
} else if (!isValidEthAddress(user.accountAddress)) {
return 'That doesnt look like a valid ETH address';
}
@ -235,6 +235,7 @@ export function makeProposalPreviewFromDraft(
proposalAddress: '0x0',
dateCreated: Date.now(),
title: draft.title,
brief: draft.brief,
content: draft.content,
stage: 'preview',
category: draft.category || PROPOSAL_CATEGORY.DAPP,

View File

@ -1,4 +1,4 @@
import { UserProposal, UserComment, TeamMember } from 'types';
import { UserProposal, UserComment, User } from 'types';
import types from './types';
import {
getUser,
@ -28,7 +28,7 @@ export function fetchUser(userFetchId: string) {
};
}
export function updateUser(user: TeamMember) {
export function updateUser(user: User) {
const userClone = cleanClone(INITIAL_TEAM_MEMBER_STATE, user);
return async (dispatch: Dispatch<any>) => {
dispatch({ type: types.UPDATE_USER_PENDING, payload: { user } });

View File

@ -1,14 +1,14 @@
import lodash from 'lodash';
import { UserProposal, UserComment, TeamInviteWithProposal } from 'types';
import types from './types';
import { TeamMember } from 'types';
import { User } from 'types';
export interface TeamInviteWithResponse extends TeamInviteWithProposal {
isResponding: boolean;
respondError: number | null;
}
export interface UserState extends TeamMember {
export interface UserState extends User {
isFetching: boolean;
hasFetched: boolean;
fetchError: number | null;
@ -36,12 +36,13 @@ export interface UsersState {
map: { [index: string]: UserState };
}
export const INITIAL_TEAM_MEMBER_STATE: TeamMember = {
ethAddress: '',
avatarUrl: '',
name: '',
export const INITIAL_TEAM_MEMBER_STATE: User = {
userid: 0,
accountAddress: '',
avatar: null,
displayName: '',
emailAddress: '',
socialAccounts: {},
socialMedias: [],
title: '',
};
@ -105,19 +106,19 @@ export default (state = INITIAL_STATE, action: any) => {
});
// update
case types.UPDATE_USER_PENDING:
return updateUserState(state, payload.user.ethAddress, {
return updateUserState(state, payload.user.accountAddress, {
isUpdating: true,
updateError: null,
});
case types.UPDATE_USER_FULFILLED:
return updateUserState(
state,
payload.user.ethAddress,
payload.user.accountAddress,
{ isUpdating: false },
payload.user,
);
case types.UPDATE_USER_REJECTED:
return updateUserState(state, payload.user.ethAddress, {
return updateUserState(state, payload.user.accountAddress, {
isUpdating: false,
updateError: errorStatus,
});

View File

@ -9,7 +9,7 @@ interface Props {
class ProfilePage extends React.Component<Props> {
render() {
const { user } = this.props;
return <h1>Settings for {user && user.name}</h1>;
return <h1>Settings for {user && user.displayName}</h1>;
}
}

View File

@ -1,29 +1,11 @@
import { TeamMember } from 'types';
import { socialAccountsToUrls, socialUrlsToAccounts } from 'utils/social';
import { User } from 'types';
import { socialMediaToUrl } from 'utils/social';
export function formatTeamMemberForPost(user: TeamMember) {
export function formatUserForPost(user: User) {
return {
displayName: user.name,
title: user.title,
accountAddress: user.ethAddress,
emailAddress: user.emailAddress,
avatar: user.avatarUrl ? { link: user.avatarUrl } : {},
socialMedias: socialAccountsToUrls(user.socialAccounts).map(url => ({
link: url,
})),
};
}
export function formatTeamMemberFromGet(user: any): TeamMember {
return {
name: user.displayName,
title: user.title,
ethAddress: user.accountAddress,
emailAddress: user.emailAddress,
avatarUrl: user.avatar && user.avatar.imageUrl,
socialAccounts: socialUrlsToAccounts(
user.socialMedias.map((sm: any) => sm.socialMediaLink),
),
...user,
avatar: user.avatar ? user.avatar.image_url : null,
socialMedias: user.socialMedias.map(socialMediaToUrl),
};
}

View File

@ -1,60 +1,36 @@
import React from 'react';
import { Icon } from 'antd';
import keybaseIcon from 'static/images/keybase.svg';
import { SOCIAL_TYPE, SocialAccountMap, SocialInfo } from 'types';
import { SOCIAL_SERVICE, SocialMedia, SocialInfo } from 'types';
const accountNameRegex = '([a-zA-Z0-9-_]*)';
export const SOCIAL_INFO: { [key in SOCIAL_TYPE]: SocialInfo } = {
[SOCIAL_TYPE.GITHUB]: {
type: SOCIAL_TYPE.GITHUB,
export const SOCIAL_INFO: { [key in SOCIAL_SERVICE]: SocialInfo } = {
[SOCIAL_SERVICE.GITHUB]: {
service: SOCIAL_SERVICE.GITHUB,
name: 'Github',
format: `https://github.com/${accountNameRegex}`,
icon: <Icon type="github" />,
},
[SOCIAL_TYPE.TWITTER]: {
type: SOCIAL_TYPE.TWITTER,
[SOCIAL_SERVICE.TWITTER]: {
service: SOCIAL_SERVICE.TWITTER,
name: 'Twitter',
format: `https://twitter.com/${accountNameRegex}`,
icon: <Icon type="twitter" />,
},
[SOCIAL_TYPE.LINKEDIN]: {
type: SOCIAL_TYPE.LINKEDIN,
[SOCIAL_SERVICE.LINKEDIN]: {
service: SOCIAL_SERVICE.LINKEDIN,
name: 'LinkedIn',
format: `https://linkedin.com/in/${accountNameRegex}`,
icon: <Icon type="linkedin" />,
},
[SOCIAL_TYPE.KEYBASE]: {
type: SOCIAL_TYPE.KEYBASE,
[SOCIAL_SERVICE.KEYBASE]: {
service: SOCIAL_SERVICE.KEYBASE,
name: 'KeyBase',
format: `https://keybase.io/${accountNameRegex}`,
icon: <Icon component={keybaseIcon} />,
},
};
function urlToAccount(format: string, url: string): string | false {
const matches = url.match(new RegExp(format));
return matches && matches[1] ? matches[1] : false;
}
export function socialAccountToUrl(account: string, type: SOCIAL_TYPE): string {
return SOCIAL_INFO[type].format.replace(accountNameRegex, account);
}
export function socialUrlsToAccounts(urls: string[]): SocialAccountMap {
const accounts: SocialAccountMap = {};
urls.forEach(url => {
Object.values(SOCIAL_INFO).forEach(s => {
const account = urlToAccount(s.format, url);
if (account) {
accounts[s.type] = account;
}
});
});
return accounts;
}
export function socialAccountsToUrls(accounts: SocialAccountMap): string[] {
return Object.entries(accounts).map(([key, value]) => {
return socialAccountToUrl(value as string, key as SOCIAL_TYPE);
});
export function socialMediaToUrl(sm: SocialMedia): string {
return SOCIAL_INFO[sm.service].format.replace(accountNameRegex, sm.username);
}

View File

@ -2,20 +2,31 @@ import * as React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { storiesOf } from '@storybook/react';
import { DONATION } from 'utils/constants';
import { User } from 'types';
import 'components/UserRow/style.less';
import UserRow from 'components/UserRow';
const user = {
name: 'Dana Hayes',
const user: User = {
userid: 123,
displayName: 'Dana Hayes',
title: 'QA Engineer',
avatarUrl: 'https://randomuser.me/api/portraits/women/19.jpg',
ethAddress: DONATION.ETH,
avatar: {
image_url: 'https://randomuser.me/api/portraits/women/19.jpg',
},
accountAddress: DONATION.ETH,
emailAddress: 'test@test.test',
socialAccounts: {},
socialMedias: [],
};
const cases = [
interface Case {
disp: string;
props: {
user: User;
};
}
const cases: Case[] = [
{
disp: 'Full User',
props: {
@ -29,7 +40,7 @@ const cases = [
props: {
user: {
...user,
avatarUrl: '',
avatar: null,
},
},
},
@ -38,8 +49,8 @@ const cases = [
props: {
user: {
...user,
avatarUrl: '',
ethAddress: '',
avatar: null,
accountAddress: '',
},
},
},
@ -48,7 +59,7 @@ const cases = [
props: {
user: {
...user,
name: 'Dr. Baron Longnamivitch von Testeronomous III Esq.',
displayName: 'Dr. Baron Longnamivitch von Testeronomous III Esq.',
title: 'Amazing person, all around cool neat-o guy, 10/10 would order again',
},
},

View File

@ -161,33 +161,37 @@ export function getProposalWithCrowdFund({
proposalAddress: '0x033fDc6C01DC2385118C7bAAB88093e22B8F0710',
dateCreated: created / 1000,
title: 'Crowdfund Title',
brief: 'A cool test crowdfund',
content: 'body',
stage: 'FUNDING_REQUIRED',
category: PROPOSAL_CATEGORY.COMMUNITY,
team: [
{
name: 'Test Proposer',
userid: 123,
displayName: 'Test Proposer',
title: '',
ethAddress: '0x0c7C6178AD0618Bf289eFd5E1Ff9Ada25fC3bDE7',
accountAddress: '0x0c7C6178AD0618Bf289eFd5E1Ff9Ada25fC3bDE7',
emailAddress: '',
avatarUrl: '',
socialAccounts: {},
avatar: null,
socialMedias: [],
},
{
name: 'Test Proposer',
userid: 456,
displayName: 'Test Proposer',
title: '',
ethAddress: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
accountAddress: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
emailAddress: '',
avatarUrl: '',
socialAccounts: {},
avatar: null,
socialMedias: [],
},
{
name: 'Test Proposer',
userid: 789,
displayName: 'Test Proposer',
title: '',
ethAddress: '0x529104532a9779ea9eae0c1e325b3368e0f8add4',
accountAddress: '0x529104532a9779ea9eae0c1e325b3368e0f8add4',
emailAddress: '',
avatarUrl: '',
socialAccounts: {},
avatar: null,
socialMedias: [],
},
],
milestones,

View File

@ -4,7 +4,7 @@ import {
CreateMilestone,
ProposalMilestone,
Update,
TeamMember,
User,
Milestone,
Comment,
} from 'types';
@ -57,7 +57,7 @@ export interface ProposalDraft {
deadlineDuration: number;
voteDuration: number;
milestones: CreateMilestone[];
team: TeamMember[];
team: User[];
invites: TeamInvite[];
}
@ -72,7 +72,7 @@ export interface Proposal {
stage: string;
category: PROPOSAL_CATEGORY;
milestones: ProposalMilestone[];
team: TeamMember[];
team: User[];
}
export interface ProposalWithCrowdFund extends Proposal {
@ -99,7 +99,7 @@ export interface UserProposal {
proposalId: number;
title: string;
brief: string;
team: TeamMember[];
team: User[];
funded: Wei;
target: Wei;
}

View File

@ -1,15 +1,20 @@
import React from 'react';
export type SocialAccountMap = Partial<{ [key in SOCIAL_TYPE]: string }>;
export type SocialAccountMap = Partial<{ [key in SOCIAL_SERVICE]: string }>;
export interface SocialMedia {
service: SOCIAL_SERVICE;
username: string;
}
export interface SocialInfo {
type: SOCIAL_TYPE;
service: SOCIAL_SERVICE;
name: string;
format: string;
icon: React.ReactNode;
}
export enum SOCIAL_TYPE {
export enum SOCIAL_SERVICE {
GITHUB = 'GITHUB',
TWITTER = 'TWITTER',
LINKEDIN = 'LINKEDIN',

View File

@ -1,21 +1,11 @@
import { SocialAccountMap } from 'types';
import { SocialMedia } from 'types';
export interface User {
userid: number;
accountAddress: string;
userid: number | string;
username: string;
emailAddress: string; // TODO: Split into full user type
displayName: string;
title: string;
avatar: {
'120x120': string;
};
}
// TODO: Merge this or extend the `User` type in proposals/reducers.ts
export interface TeamMember {
name: string;
title: string;
avatarUrl: string;
ethAddress: string;
emailAddress: string;
socialAccounts: SocialAccountMap;
socialMedias: SocialMedia[];
avatar: { image_url: string } | null;
}