281 lines
8.9 KiB
TypeScript
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> </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} />}
|
|
/>
|
|
);
|