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

318 lines
10 KiB
TypeScript

import React, { ReactNode } from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Markdown from 'components/Markdown';
import LinkableTabs from 'components/LinkableTabs';
import Loader from 'components/Loader';
import { proposalActions } from 'modules/proposals';
import { bindActionCreators, Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { STATUS } from 'types';
import { Tabs, Icon, Dropdown, Menu, Button, Alert } from 'antd';
import { AlertProps } from 'antd/lib/alert';
import ExceptionPage from 'components/ExceptionPage';
import CampaignBlock from './CampaignBlock';
import TeamBlock from './TeamBlock';
import RFPBlock from './RFPBlock';
import Milestones from './Milestones';
import CommentsTab from './Comments';
import UpdatesTab from './Updates';
import ContributorsTab from './Contributors';
import UpdateModal from './UpdateModal';
import CancelModal from './CancelModal';
import classnames from 'classnames';
import { withRouter } from 'react-router';
import SocialShare from 'components/SocialShare';
import './index.less';
interface OwnProps {
proposalId: number;
isPreview?: boolean;
}
interface StateProps {
detail: AppState['proposal']['detail'];
isFetchingDetail: AppState['proposal']['isFetchingDetail'];
detailError: AppState['proposal']['detailError'];
user: AppState['auth']['user'];
}
interface DispatchProps {
fetchProposal: proposalActions.TFetchProposal;
}
type Props = StateProps & DispatchProps & OwnProps;
interface State {
isBodyExpanded: boolean;
isBodyOverflowing: boolean;
isUpdateOpen: boolean;
isCancelOpen: boolean;
}
export class ProposalDetail extends React.Component<Props, State> {
state: State = {
isBodyExpanded: false,
isBodyOverflowing: false,
isUpdateOpen: false,
isCancelOpen: false,
};
bodyEl: HTMLElement | null = null;
componentDidMount() {
// always refresh from server
this.props.fetchProposal(this.props.proposalId);
if (this.props.detail) {
this.checkBodyOverflow();
}
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.checkBodyOverflow);
}
}
componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.checkBodyOverflow);
}
}
componentDidUpdate() {
if (this.props.detail) {
this.checkBodyOverflow();
}
}
render() {
const { user, detail: proposal, isPreview, detailError } = this.props;
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, isUpdateOpen } = this.state;
const showExpand = !isBodyExpanded && isBodyOverflowing;
const wrongProposal = proposal && proposal.proposalId !== this.props.proposalId;
if (detailError) {
return <ExceptionPage code="404" desc="Could not find that proposal" />;
}
if (!proposal || wrongProposal) {
return <Loader size="large" />;
}
const isTrustee = !!proposal.team.find(tm => tm.userid === (user && user.userid));
const isLive = proposal.status === STATUS.LIVE;
const adminMenu = (
<Menu>
<Menu.Item disabled={!isLive} onClick={this.openUpdateModal}>
Post an Update
</Menu.Item>
<Menu.Item disabled={!isLive} onClick={this.openCancelModal}>
Cancel proposal
</Menu.Item>
</Menu>
);
// BANNER
const statusBanner = {
[STATUS.PENDING]: {
blurb: (
<>
Your proposal is being reviewed and is only visible to the team. You will get
an email when it is complete.
</>
),
type: 'warning',
},
[STATUS.APPROVED]: {
blurb: (
<>
Your proposal has been approved! It is currently only visible to the team.
Visit your <Link to="/profile?tab=pending">profile's pending tab</Link> to
publish.
</>
),
type: 'success',
},
[STATUS.REJECTED]: {
blurb: (
<>
Your proposal was rejected and is only visible to the team. Visit your{' '}
<Link to="/profile?tab=pending">profile's pending tab</Link> for more
information.
</>
),
type: 'error',
},
[STATUS.STAKING]: {
blurb: (
<>
Your proposal is awaiting a staking contribution. Visit your{' '}
<Link to="/profile?tab=pending">profile's pending tab</Link> for more
information.
</>
),
type: 'warning',
},
} as { [key in STATUS]: { blurb: ReactNode; type: AlertProps['type'] } };
let banner = statusBanner[proposal.status];
if (isPreview) {
banner = {
blurb: 'This is a preview of your proposal. It has not yet been published.',
type: 'info',
};
}
return (
<div className="Proposal">
{banner && (
<div className="Proposal-banner">
<Alert type={banner.type} message={banner.blurb} showIcon={false} banner />
</div>
)}
<div className="Proposal-top">
{isLive && (
<div className="Proposal-top-social">
<SocialShare
url={(typeof window !== 'undefined' && window.location.href) || ''}
title={`${proposal.title} needs funding on Grant-io!`}
text={`${
proposal.title
} needs funding on ZF Grants! Come help make this proposal a reality by funding it.`}
/>
</div>
)}
<div className="Proposal-top-main">
<h1 className="Proposal-top-main-title">
{proposal ? proposal.title : <span>&nbsp;</span>}
</h1>
<div className="Proposal-top-main-block" style={{ flexGrow: 1 }}>
<div
ref={el => (this.bodyEl = el)}
className={classnames({
['Proposal-top-main-block-bodyText']: true,
['is-expanded']: isBodyExpanded,
})}
>
{proposal ? <Markdown source={proposal.content} /> : <Loader />}
</div>
{showExpand && (
<button
className="Proposal-top-main-block-bodyExpand"
onClick={this.expandBody}
>
Read more <Icon type="arrow-down" style={{ fontSize: '0.7rem' }} />
</button>
)}
</div>
{isLive &&
isTrustee && (
<div className="Proposal-top-main-menu">
<Dropdown
overlay={adminMenu}
trigger={['click']}
placement="bottomRight"
>
<Button>
<span>Actions</span>
<Icon type="down" style={{ marginRight: '-0.25rem' }} />
</Button>
</Dropdown>
</div>
)}
</div>
<div className="Proposal-top-side">
<CampaignBlock proposal={proposal} isPreview={!isLive} />
<TeamBlock proposal={proposal} />
{proposal.rfp && <RFPBlock rfp={proposal.rfp} />}
</div>
</div>
<div className="Proposal-bottom">
<LinkableTabs scrollToTabs defaultActiveKey="milestones">
<Tabs.TabPane tab="Milestones" key="milestones">
<div style={{ marginTop: '1.5rem', padding: '0 2rem' }}>
<Milestones proposal={proposal} />
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="Discussion" key="discussions" disabled={!isLive}>
<CommentsTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
<Tabs.TabPane tab="Updates" key="updates" disabled={!isLive}>
<UpdatesTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
<Tabs.TabPane tab="Contributors" key="contributors" disabled={!isLive}>
<ContributorsTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
</LinkableTabs>
</div>
{isTrustee && (
<>
<UpdateModal
proposalId={proposal.proposalId}
isVisible={isUpdateOpen}
handleClose={this.closeUpdateModal}
/>
<CancelModal
proposal={proposal}
isVisible={isCancelOpen}
handleClose={this.closeCancelModal}
/>
</>
)}
</div>
);
}
private expandBody = () => {
this.setState({ isBodyExpanded: true });
};
private checkBodyOverflow = () => {
const { isBodyExpanded, isBodyOverflowing } = this.state;
if (isBodyExpanded || !this.bodyEl) {
return;
}
if (isBodyOverflowing && this.bodyEl.scrollHeight <= this.bodyEl.clientHeight) {
this.setState({ isBodyOverflowing: false });
} else if (
!isBodyOverflowing &&
this.bodyEl.scrollHeight > this.bodyEl.clientHeight
) {
this.setState({ isBodyOverflowing: true });
}
};
private openUpdateModal = () => this.setState({ isUpdateOpen: true });
private closeUpdateModal = () => this.setState({ isUpdateOpen: false });
private openCancelModal = () => this.setState({ isCancelOpen: true });
private closeCancelModal = () => this.setState({ isCancelOpen: false });
}
function mapStateToProps(state: AppState, _: OwnProps) {
return {
detail: state.proposal.detail,
isFetchingDetail: state.proposal.isFetchingDetail,
detailError: state.proposal.detailError,
user: state.auth.user,
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return bindActionCreators({ ...proposalActions }, dispatch);
}
const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
mapStateToProps,
mapDispatchToProps,
);
const ConnectedProposal = compose<Props, OwnProps>(
withRouter,
withConnect,
)(ProposalDetail);
export default ConnectedProposal;