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

357 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="Youll be notified when youve 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);