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

334 lines
10 KiB
TypeScript
Raw Normal View History

import React, { ReactNode } from 'react';
2018-09-10 09:55:26 -07:00
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';
2019-01-23 07:15:59 -08:00
import Loader from 'components/Loader';
2018-09-10 09:55:26 -07:00
import { proposalActions } from 'modules/proposals';
import { bindActionCreators, Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { Proposal, STATUS } from 'types';
2018-09-10 09:55:26 -07:00
import { getProposal } from 'modules/proposals/selectors';
2019-01-23 07:15:59 -08:00
import { Tabs, Icon, Dropdown, Menu, Button, Alert } from 'antd';
import { AlertProps } from 'antd/lib/alert';
2018-09-10 09:55:26 -07:00
import CampaignBlock from './CampaignBlock';
import TeamBlock from './TeamBlock';
import RFPBlock from './RFPBlock';
2018-09-10 09:55:26 -07:00
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';
2018-09-10 09:55:26 -07:00
interface OwnProps {
proposalId: number;
isPreview?: boolean;
2018-09-10 09:55:26 -07:00
}
interface StateProps {
proposal: Proposal | null;
user: AppState['auth']['user'];
2018-09-10 09:55:26 -07:00
}
interface DispatchProps {
fetchProposal: proposalActions.TFetchProposal;
}
type Props = StateProps & DispatchProps & OwnProps;
2018-09-10 09:55:26 -07:00
interface State {
isBodyExpanded: boolean;
isBodyOverflowing: boolean;
isUpdateOpen: boolean;
isCancelOpen: boolean;
bodyId: string;
2018-09-10 09:55:26 -07:00
}
export class ProposalDetail extends React.Component<Props, State> {
2018-09-10 09:55:26 -07:00
state: State = {
isBodyExpanded: false,
isBodyOverflowing: false,
isUpdateOpen: false,
isCancelOpen: false,
bodyId: `body-${Math.floor(Math.random() * 1000000)}`,
2018-09-10 09:55:26 -07:00
};
componentDidMount() {
// always refresh from server
this.props.fetchProposal(this.props.proposalId);
if (this.props.proposal) {
this.checkBodyOverflow();
2018-09-10 09:55:26 -07:00
}
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.checkBodyOverflow);
}
}
componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.checkBodyOverflow);
}
}
componentDidUpdate() {
if (this.props.proposal) {
this.checkBodyOverflow();
}
2018-09-10 09:55:26 -07:00
}
render() {
const { user, proposal, isPreview } = this.props;
const {
isBodyExpanded,
isBodyOverflowing,
isCancelOpen,
isUpdateOpen,
bodyId,
} = this.state;
const showExpand = !isBodyExpanded && isBodyOverflowing;
2018-09-10 09:55:26 -07:00
if (!proposal) {
2019-01-23 08:35:03 -08:00
return <Loader size="large" />;
}
const deadline = 0; // TODO: Use actual date for deadline
// TODO: isTrustee - determine rework to isAdmin?
// for now: check if authed user in member of proposal team
const isTrustee = !!proposal.team.find(tm => tm.userid === (user && user.userid));
const hasBeenFunded = false; // TODO: deterimne if proposal has reached funding
const isProposalActive = !hasBeenFunded && deadline > Date.now();
const canCancel = false; // TODO: Allow canceling if proposal hasn't gone live yet
const isLive = proposal.status === STATUS.LIVE;
const adminMenu = (
<Menu>
<Menu.Item disabled={!isLive} 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: canCancel ? '#e74c3c' : undefined }}
onClick={this.openCancelModal}
disabled={!canCancel}
>
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
2019-01-22 10:40:20 -08:00
} 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
id={bodyId}
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>
<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}>
2019-01-09 13:57:15 -08:00
<ContributorsTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
</LinkableTabs>
{isTrustee && (
<>
<UpdateModal
proposalId={proposal.proposalId}
isVisible={isUpdateOpen}
handleClose={this.closeUpdateModal}
/>
<CancelModal
proposal={proposal}
isVisible={isCancelOpen}
handleClose={this.closeCancelModal}
/>
</>
)}
</div>
);
2018-09-10 09:55:26 -07:00
}
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 });
2018-09-10 09:55:26 -07:00
}
function mapStateToProps(state: AppState, ownProps: OwnProps) {
2018-12-14 11:36:22 -08:00
console.warn('TODO - new redux user-proposal-role/account');
2018-09-10 09:55:26 -07:00
return {
proposal: getProposal(state, ownProps.proposalId),
user: state.auth.user,
2018-09-10 09:55:26 -07:00
};
}
function mapDispatchToProps(dispatch: Dispatch) {
2018-12-14 11:36:22 -08:00
return bindActionCreators({ ...proposalActions }, dispatch);
2018-09-10 09:55:26 -07:00
}
const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
2018-09-10 09:55:26 -07:00
mapStateToProps,
mapDispatchToProps,
);
const ConnectedProposal = compose<Props, OwnProps>(
withRouter,
withConnect,
)(ProposalDetail);
2018-09-10 09:55:26 -07:00
export default ConnectedProposal;