357 lines
11 KiB
TypeScript
357 lines
11 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
withRouter,
|
||
RouteComponentProps,
|
||
Redirect,
|
||
Switch,
|
||
Route,
|
||
} from 'react-router-dom';
|
||
import { connect } from 'react-redux';
|
||
import { compose } from 'recompose';
|
||
import { Tabs, Badge } from 'antd';
|
||
import { usersActions } from 'modules/users';
|
||
import { AppState } from 'store/reducers';
|
||
import HeaderDetails from 'components/HeaderDetails';
|
||
import ProfileUser from './ProfileUser';
|
||
import ProfileEdit from './ProfileEdit';
|
||
import ProfilePendingList from './ProfilePendingList';
|
||
import ProfileRejectedPermanentlyList from './ProfileRejectedPermanentlyList';
|
||
import ProfileProposal from './ProfileProposal';
|
||
import ProfileContribution from './ProfileContribution';
|
||
import ProfileComment from './ProfileComment';
|
||
import ProfileInvite from './ProfileInvite';
|
||
import ProfileCCR from './ProfileCCR';
|
||
import Placeholder from 'components/Placeholder';
|
||
import Loader from 'components/Loader';
|
||
import ExceptionPage from 'components/ExceptionPage';
|
||
import ContributionModal from 'components/ContributionModal';
|
||
import LinkableTabs from 'components/LinkableTabs';
|
||
import { UserContribution } from 'types';
|
||
import ProfileArbitrated from './ProfileArbitrated';
|
||
import './style.less';
|
||
|
||
interface StateProps {
|
||
usersMap: AppState['users']['map'];
|
||
authUser: AppState['auth']['user'];
|
||
hasCheckedUser: AppState['auth']['hasCheckedUser'];
|
||
}
|
||
|
||
interface DispatchProps {
|
||
fetchUser: typeof usersActions['fetchUser'];
|
||
fetchUserInvites: typeof usersActions['fetchUserInvites'];
|
||
}
|
||
|
||
type Props = RouteComponentProps<any> & StateProps & DispatchProps;
|
||
|
||
interface State {
|
||
activeContribution: UserContribution | null;
|
||
}
|
||
|
||
class Profile extends React.Component<Props, State> {
|
||
state: State = {
|
||
activeContribution: null,
|
||
};
|
||
|
||
componentDidMount() {
|
||
this.fetchData();
|
||
}
|
||
|
||
componentDidUpdate(prevProps: Props) {
|
||
const userLookupId = this.props.match.params.id;
|
||
const prevUserLookupId = prevProps.match.params.id;
|
||
if (userLookupId !== prevUserLookupId) {
|
||
window.scrollTo(0, 0);
|
||
this.fetchData();
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { authUser, match, location, hasCheckedUser } = this.props;
|
||
const { activeContribution } = this.state;
|
||
const userLookupParam = match.params.id;
|
||
|
||
if (!userLookupParam) {
|
||
if (authUser && authUser.userid) {
|
||
return <Redirect to={{ ...location, pathname: `/profile/${authUser.userid}` }} />;
|
||
} else {
|
||
return <Redirect to={{ ...location, pathname: '/auth' }} />;
|
||
}
|
||
}
|
||
|
||
const user = this.props.usersMap[userLookupParam];
|
||
const waiting = !user || !user.hasFetched || !hasCheckedUser;
|
||
const isAuthedUser = user && authUser && user.userid === authUser.userid;
|
||
|
||
if (waiting) {
|
||
return <Loader size="large" />;
|
||
}
|
||
|
||
if (user.fetchError) {
|
||
return <ExceptionPage code="404" desc="No user could be found" />;
|
||
}
|
||
|
||
const {
|
||
proposals,
|
||
pendingProposals,
|
||
pendingRequests,
|
||
rejectedPermanentlyProposals,
|
||
rejectedPermanentlyRequests,
|
||
requests,
|
||
contributions,
|
||
comments,
|
||
invites,
|
||
arbitrated,
|
||
} = user;
|
||
|
||
const isLoading = user.isFetching;
|
||
const noProposalsPending = pendingProposals.length === 0;
|
||
const noProposalsCreated = proposals.length === 0;
|
||
const noRequestsPending = pendingRequests.length === 0;
|
||
const noRequestsCreated = requests.length === 0;
|
||
const noneFunded = contributions.length === 0;
|
||
const noneCommented = comments.length === 0;
|
||
const noneArbitrated = arbitrated.length === 0;
|
||
const noneInvites = user.hasFetchedInvites && invites.length === 0;
|
||
const rejectedPermanentlyCount =
|
||
rejectedPermanentlyProposals.length + rejectedPermanentlyRequests.length;
|
||
const noneRejectedPermanently = rejectedPermanentlyCount === 0;
|
||
|
||
// do not allow a tab to be linked to if it won't be visible in the ui
|
||
const skippedTabs = [
|
||
...(noneFunded ? ['funded'] : []),
|
||
...(noneRejectedPermanently ? ['rejected'] : []),
|
||
];
|
||
|
||
const privateMsg = 'Note: This tab is private to your account';
|
||
const publicMsg = 'Note: This tab is public to ZF Grants';
|
||
|
||
const renderTabMsg = (msg: string) =>
|
||
isAuthedUser && (
|
||
<div
|
||
style={{
|
||
textAlign: 'center',
|
||
paddingBottom: '1rem',
|
||
opacity: 1,
|
||
textTransform: 'uppercase',
|
||
fontSize: '0.5rem',
|
||
letterSpacing: '0.05em',
|
||
marginTop: '-0.4rem',
|
||
}}
|
||
>
|
||
<strong>{msg}</strong>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className="Profile">
|
||
<HeaderDetails
|
||
title={`${user.displayName} on ZF Grants`}
|
||
description={`Join ${user.displayName} in improving the Zcash ecosystem!`}
|
||
image={user.avatar ? user.avatar.imageUrl : undefined}
|
||
/>
|
||
<Switch>
|
||
<Route
|
||
path={`${match.path}`}
|
||
exact={true}
|
||
render={() => <ProfileUser user={user} />}
|
||
/>
|
||
<Route
|
||
path={`${match.path}/edit`}
|
||
exact={true}
|
||
render={() => <ProfileEdit user={user} />}
|
||
/>
|
||
</Switch>
|
||
<div className="Profile-tabs">
|
||
<LinkableTabs
|
||
ignoredKeys={skippedTabs}
|
||
defaultActiveKey={(isAuthedUser && 'pending') || 'created'}
|
||
>
|
||
{isAuthedUser && (
|
||
<Tabs.TabPane
|
||
tab={TabTitle(
|
||
'Pending',
|
||
pendingProposals.length + pendingRequests.length,
|
||
)}
|
||
key="pending"
|
||
>
|
||
{renderTabMsg(privateMsg)}
|
||
<div>
|
||
{noProposalsPending &&
|
||
noRequestsPending && (
|
||
<Placeholder
|
||
loading={isLoading}
|
||
title="No pending items"
|
||
subtitle="You do not have any proposals or requests awaiting approval."
|
||
/>
|
||
)}
|
||
<ProfilePendingList
|
||
proposals={pendingProposals}
|
||
requests={pendingRequests}
|
||
/>
|
||
</div>
|
||
</Tabs.TabPane>
|
||
)}
|
||
<Tabs.TabPane
|
||
tab={TabTitle('Created', proposals.length + requests.length)}
|
||
key="created"
|
||
>
|
||
{renderTabMsg(publicMsg)}
|
||
<div>
|
||
{noProposalsCreated &&
|
||
noRequestsCreated && (
|
||
<Placeholder
|
||
loading={isLoading}
|
||
title="No created items"
|
||
subtitle="There have not been any created proposals or requests."
|
||
/>
|
||
)}
|
||
{proposals.map(p => (
|
||
<ProfileProposal key={p.proposalId} proposal={p} />
|
||
))}
|
||
{requests.map(c => (
|
||
<ProfileCCR key={c.ccrId} ccr={c} />
|
||
))}
|
||
</div>
|
||
</Tabs.TabPane>
|
||
{!noneFunded && (
|
||
<Tabs.TabPane tab={TabTitle('Funded', contributions.length)} key="funded">
|
||
{renderTabMsg(publicMsg)}
|
||
<div>
|
||
{contributions.map(c => (
|
||
<ProfileContribution
|
||
key={c.id}
|
||
userId={user.userid}
|
||
contribution={c}
|
||
showSendInstructions={this.openContributionModal}
|
||
/>
|
||
))}
|
||
</div>
|
||
</Tabs.TabPane>
|
||
)}
|
||
<Tabs.TabPane tab={TabTitle('Comments', comments.length)} key="comments">
|
||
{renderTabMsg(publicMsg)}
|
||
<div>
|
||
{noneCommented && (
|
||
<Placeholder
|
||
loading={isLoading}
|
||
title="No comments"
|
||
subtitle="There have not been any comments made"
|
||
/>
|
||
)}
|
||
{comments.map(c => (
|
||
<ProfileComment key={c.id} userName={user.displayName} comment={c} />
|
||
))}
|
||
</div>
|
||
</Tabs.TabPane>
|
||
{isAuthedUser && (
|
||
<Tabs.TabPane
|
||
tab={TabTitle('Invites', invites.length)}
|
||
key="invites"
|
||
disabled={!user.hasFetchedInvites}
|
||
>
|
||
{renderTabMsg(privateMsg)}
|
||
<div>
|
||
{noneInvites && (
|
||
<Placeholder
|
||
loading={isLoading}
|
||
title="No invitations"
|
||
subtitle="You’ll be notified when you’ve been invited to join a proposal"
|
||
/>
|
||
)}
|
||
{invites.map(invite => (
|
||
<ProfileInvite key={invite.id} userId={user.userid} invite={invite} />
|
||
))}
|
||
</div>
|
||
</Tabs.TabPane>
|
||
)}
|
||
{isAuthedUser && (
|
||
<Tabs.TabPane
|
||
tab={TabTitle('Arbitrations', arbitrated.length)}
|
||
key="arbitrations"
|
||
>
|
||
{renderTabMsg(privateMsg)}
|
||
{noneArbitrated && (
|
||
<Placeholder
|
||
loading={isLoading}
|
||
title="No arbitrations"
|
||
subtitle="You are not an arbiter of any proposals"
|
||
/>
|
||
)}
|
||
{arbitrated.map(arb => (
|
||
<ProfileArbitrated key={arb.proposal.proposalId} arbiter={arb} />
|
||
))}
|
||
</Tabs.TabPane>
|
||
)}
|
||
{isAuthedUser &&
|
||
!noneRejectedPermanently && (
|
||
<Tabs.TabPane
|
||
tab={TabTitle('Rejected', rejectedPermanentlyCount)}
|
||
key="rejected"
|
||
>
|
||
{renderTabMsg(privateMsg)}
|
||
<div>
|
||
<ProfileRejectedPermanentlyList
|
||
proposals={rejectedPermanentlyProposals}
|
||
requests={rejectedPermanentlyRequests}
|
||
/>
|
||
</div>
|
||
</Tabs.TabPane>
|
||
)}
|
||
</LinkableTabs>
|
||
</div>
|
||
|
||
<ContributionModal
|
||
isVisible={!!activeContribution}
|
||
proposalId={
|
||
activeContribution ? activeContribution.proposal.proposalId : undefined
|
||
}
|
||
contributionId={activeContribution ? activeContribution.id : undefined}
|
||
hasNoButtons
|
||
handleClose={this.closeContributionModal}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
private fetchData() {
|
||
const { match } = this.props;
|
||
const userLookupId = match.params.id;
|
||
if (userLookupId) {
|
||
this.props.fetchUser(userLookupId);
|
||
this.props.fetchUserInvites(userLookupId);
|
||
}
|
||
}
|
||
|
||
private openContributionModal = (c: UserContribution) =>
|
||
this.setState({ activeContribution: c });
|
||
private closeContributionModal = () => this.setState({ activeContribution: null });
|
||
}
|
||
|
||
const TabTitle = (disp: string, count: number) => (
|
||
<div>
|
||
{disp}
|
||
<Badge
|
||
className={`Profile-tabBadge ${count > 0 ? 'is-not-zero' : 'is-zero'}`}
|
||
showZero={true}
|
||
count={count}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
const withConnect = connect<StateProps, DispatchProps, {}, AppState>(
|
||
state => ({
|
||
usersMap: state.users.map,
|
||
authUser: state.auth.user,
|
||
hasCheckedUser: state.auth.hasCheckedUser,
|
||
}),
|
||
{
|
||
fetchUser: usersActions.fetchUser,
|
||
fetchUserInvites: usersActions.fetchUserInvites,
|
||
},
|
||
);
|
||
|
||
export default compose<Props, {}>(
|
||
withRouter,
|
||
withConnect,
|
||
)(Profile);
|