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

281 lines
8.9 KiB
TypeScript

import React from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import Markdown from 'components/Markdown';
import { proposalActions } from 'modules/proposals';
import { bindActionCreators, Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { ProposalWithCrowdFund } from 'types';
import { getProposal } from 'modules/proposals/selectors';
import { Spin, Tabs, Icon, Dropdown, Menu, Button } from 'antd';
import CampaignBlock from './CampaignBlock';
import TeamBlock from './TeamBlock';
import Milestones from './Milestones';
import CommentsTab from './Comments';
import UpdatesTab from './Updates';
import GovernanceTab from './Governance';
import ContributorsTab from './Contributors';
// import CommunityTab from './Community';
import UpdateModal from './UpdateModal';
import CancelModal from './CancelModal';
import './style.less';
import classnames from 'classnames';
import { withRouter } from 'react-router';
import Web3Container from 'lib/Web3Container';
import { web3Actions } from 'modules/web3';
import SocialShare from 'components/SocialShare';
interface OwnProps {
proposalId: string;
isPreview?: boolean;
}
interface StateProps {
proposal: ProposalWithCrowdFund | null;
}
interface DispatchProps {
fetchProposal: proposalActions.TFetchProposal;
}
interface Web3Props {
account: string;
}
type Props = StateProps & DispatchProps & Web3Props & OwnProps;
interface State {
isBodyExpanded: boolean;
isBodyOverflowing: boolean;
isUpdateOpen: boolean;
isCancelOpen: boolean;
bodyId: string;
}
export class ProposalDetail extends React.Component<Props, State> {
state: State = {
isBodyExpanded: false,
isBodyOverflowing: false,
isUpdateOpen: false,
isCancelOpen: false,
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
};
componentDidMount() {
if (!this.props.proposal) {
this.props.fetchProposal(this.props.proposalId);
} else {
this.checkBodyOverflow();
}
window.addEventListener('resize', this.checkBodyOverflow);
}
componentWillUnmount() {
window.removeEventListener('resize', this.checkBodyOverflow);
}
componentDidUpdate() {
if (this.props.proposal) {
this.checkBodyOverflow();
}
}
render() {
const { proposal, isPreview, account } = this.props;
const { isBodyExpanded, isBodyOverflowing, isCancelOpen, isUpdateOpen, bodyId } = this.state;
const showExpand = !isBodyExpanded && isBodyOverflowing;
if (!proposal) {
return <Spin />;
} else {
const { crowdFund } = proposal;
const isTrustee = crowdFund.trustees.includes(account);
const isContributor = !!crowdFund.contributors.find(c => c.address === account);
const hasBeenFunded = crowdFund.isRaiseGoalReached;
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
const canRefund = (hasBeenFunded || isProposalActive) && !crowdFund.isFrozen;
const adminMenu = isTrustee && (
<Menu>
<Menu.Item onClick={this.openUpdateModal}>
Post an Update
</Menu.Item>
<Menu.Item
onClick={() => alert('Sorry, not yet implemented!')}
disabled={!isProposalActive}
>
Edit proposal
</Menu.Item>
<Menu.Item
style={{ color: canRefund ? '#e74c3c' : undefined }}
onClick={this.openCancelModal}
disabled={!canRefund}
>
{hasBeenFunded ? 'Refund contributors' : 'Cancel proposal'}
</Menu.Item>
</Menu>
);
return (
<div className="Proposal">
<div className="Proposal-top">
<div className="Proposal-top-social">
<SocialShare
url={window.location.href}
title={`${proposal.title} needs funding on Grant-io!`}
text={`${
proposal.title
} needs funding on Grant.io! 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
id={bodyId}
className={classnames({
['Proposal-top-main-block-bodyText']: true,
['is-expanded']: isBodyExpanded,
})}
>
{proposal ? <Markdown source={proposal.body} /> : <Spin size="large" />}
</div>
{showExpand && (
<button
className="Proposal-top-main-block-bodyExpand"
onClick={this.expandBody}
>
Read more <Icon type="arrow-down" style={{ fontSize: '0.7rem' }} />
</button>
)}
</div>
{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={isPreview} />
<TeamBlock proposal={proposal} />
</div>
</div>
{proposal && (
<Tabs>
<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={isPreview}>
<CommentsTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
<Tabs.TabPane tab="Updates" key="updates" disabled={isPreview}>
<UpdatesTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
{isContributor && (
<Tabs.TabPane tab="Refund" key="refund">
<GovernanceTab proposal={proposal} />
</Tabs.TabPane>
)}
<Tabs.TabPane tab="Contributors" key="contributors">
<ContributorsTab crowdFund={proposal.crowdFund} />
</Tabs.TabPane>
</Tabs>
)}
{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, bodyId, isBodyOverflowing } = this.state;
if (isBodyExpanded) {
return;
}
// Use id instead of ref because styled component ref doesn't return html element
const bodyEl = document.getElementById(bodyId);
if (!bodyEl) {
return;
}
if (isBodyOverflowing && bodyEl.scrollHeight <= bodyEl.clientHeight) {
this.setState({ isBodyOverflowing: false });
} else if (!isBodyOverflowing && bodyEl.scrollHeight > 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: OwnProps) {
return {
proposal: getProposal(state, ownProps.proposalId),
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return bindActionCreators({ ...proposalActions, ...web3Actions }, dispatch);
}
const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
mapStateToProps,
mapDispatchToProps,
);
const ConnectedProposal = compose<Props, OwnProps & Web3Props>(
withRouter,
withConnect,
)(ProposalDetail);
export default (props: OwnProps) => (
<Web3Container
renderLoading={() => (
<div className="Proposal">
<div className="Proposal-top">
<div className="Proposal-top-main">
<Spin />
</div>
</div>
</div>
)}
render={({ accounts }) => <ConnectedProposal account={accounts[0]} {...props} />}
/>
);