Redesign Campaign Block (#74)

* init profile tipjar backend

* init profile tipjar frontend

* fix lint

* implement tip jar block

* fix wrapping, hide tip block on self

* init backend proposal tipjar

* init frontend proposal tipjar

* add hide title, fix bug

* uncomment rate limit

* rename vars, use null check

* allow address and view key to be unset

* add api tests

* fix tsc errors

* fix lint

* fix CopyInput styling

* fix migrations

* hide tipping in proposal if address not set

* add tip address to create flow

* redesign campaign block

* fix typo

* update campaign block

* add tip jar to dedicated card

* fix tipjar bug

* use zf light logo

* switch to zf grants logo

* hide profile tip jar if address not set

* add comment, run prettier
This commit is contained in:
Danny Skubak 2019-11-24 10:05:08 -05:00 committed by Daniel Ternyak
parent 8f187ad775
commit 213595cfba
9 changed files with 169 additions and 59 deletions

View File

@ -225,8 +225,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
description={
<div>
<p>
This proposal has changes requested. The team will be able to re-submit it for
approval should they desire to do so.
This proposal has changes requested. The team will be able to re-submit it
for approval should they desire to do so.
</p>
<b>Reason:</b>
<br />

View File

@ -61,7 +61,8 @@ class ProfileUser extends React.Component<Props, State> {
</div>
)}
</div>
{!isSelf && <TipJarBlock address={user.tipJarAddress} type="user" />}
{!isSelf &&
user.tipJarAddress && <TipJarBlock address={user.tipJarAddress} type="user" />}
</div>
);
}

View File

@ -8,9 +8,9 @@ import { compose } from 'recompose';
import { AppState } from 'store/reducers';
import { withRouter } from 'react-router';
import UnitDisplay from 'components/UnitDisplay';
import { TipJarBlock } from 'components/TipJar';
import Loader from 'components/Loader';
import { PROPOSAL_STAGE } from 'api/constants';
import ZFGrantsLogo from 'static/images/logo-name-light.svg'
import './style.less';
interface OwnProps {
@ -70,13 +70,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
}
const isAcceptedWithFunding = proposal.acceptedWithFunding === true;
const isAcceptedWithoutFunding = proposal.acceptedWithFunding === false;
const isAccepted = isAcceptedWithFunding || isAcceptedWithoutFunding;
const isCancelled = proposal.stage === PROPOSAL_STAGE.CANCELED;
const isJudged = isAccepted || isCancelled;
const displayBountyFunding =
!isVersionTwo || (isVersionTwo && isAcceptedWithFunding);
const isCanceled = proposal.stage === PROPOSAL_STAGE.CANCELED;
content = (
<React.Fragment>
@ -97,23 +91,48 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
</div>
</div>
)}
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Funding</div>
<div className="ProposalCampaignBlock-info-value">
<UnitDisplay value={funded} /> / <UnitDisplay value={target} symbol="ZEC" />
{!isVersionTwo && (
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Funding</div>
<div className="ProposalCampaignBlock-info-value">
<UnitDisplay value={funded} /> /{' '}
<UnitDisplay value={target} symbol="ZEC" />
</div>
</div>
</div>
)}
{isVersionTwo && (
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">
{isAcceptedWithFunding ? 'Funding' : 'Requested Funding'}
</div>
<div className="ProposalCampaignBlock-info-value">
<UnitDisplay value={target} symbol="ZEC" />
</div>
</div>
)}
{bounty &&
displayBountyFunding && (
!isVersionTwo && (
<div className="ProposalCampaignBlock-bounty">
Awarded with <UnitDisplay value={bounty} symbol="ZEC" /> bounty
</div>
)}
{isAcceptedWithoutFunding && (
<div className="ProposalCampaignBlock-bounty">Accepted without funding</div>
)}
{isVersionTwo &&
isAcceptedWithFunding && (
<div className="ProposalCampaignBlock-with-funding">
Funded through &nbsp;
<ZFGrantsLogo style={{ height: '1.5rem' }} />
</div>
)}
{isVersionTwo &&
!isAcceptedWithFunding && (
<div className="ProposalCampaignBlock-without-funding">
Open for Community Donations
</div>
)}
{!isVersionTwo &&
proposal.contributionMatching > 0 && (
@ -144,7 +163,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
['is-success']: isRaiseGoalReached,
})}
>
{isCancelled ? (
{isCanceled ? (
<>
<Icon type="close-circle-o" />
<span>Proposal was canceled</span>
@ -175,32 +194,12 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
))}
{isVersionTwo &&
isJudged && (
<div
className={classnames({
['ProposalCampaignBlock-fundingOver']: true,
['is-success']: isAccepted,
})}
>
{proposal.stage === PROPOSAL_STAGE.CANCELED ? (
<>
<Icon type="close-circle-o" />
<span>Proposal was canceled</span>
</>
) : (
<>
<Icon type="check-circle-o" />
<span>Proposal has been accepted</span>
</>
)}
isCanceled && (
<div className="ProposalCampaignBlock-fundingOver">
<Icon type="close-circle-o" />
<span>Proposal was canceled</span>
</div>
)}
{proposal.tipJarAddress && (
<div className="ProposalCampaignBlock-tipJarWrapper">
<TipJarBlock address={proposal.tipJarAddress} type="proposal" />
</div>
)}
</React.Fragment>
);
} else {

View File

@ -57,6 +57,25 @@
margin-top: 0;
}
&-with-funding,
&-without-funding {
margin: 0.5rem -1.5rem;
padding: 0.75rem 1rem;
font-size: 1rem;
color: #fff;
align-items: center;
display: flex;
justify-content: center;
}
&-with-funding {
background: @secondary-color;
}
&-without-funding {
background: @info-color;
}
&-popover {
&-overlay {
max-width: 400px;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Tooltip, Icon } from 'antd';
import { Proposal } from 'types';
import Loader from 'components/Loader';
import { TipJarBlock } from 'components/TipJar';
import { PROPOSAL_STAGE } from 'api/constants';
import './style.less';
interface Props {
proposal: Proposal;
}
const TippingBlock: React.SFC<Props> = ({ proposal }) => {
let content;
if (proposal) {
if (!proposal.tipJarAddress || proposal.stage === PROPOSAL_STAGE.CANCELED) {
return null;
}
content = (
<>
<div className="TippingBlock-info">
<div className="TippingBlock-info-label">Tips Received</div>
<div className="TippingBlock-info-value">
??? &nbsp;
<Tooltip
placement="left"
title="Made possible if a proposal owner supplies a view key with their tip address."
>
<Icon type="info-circle" />
</Tooltip>
</div>
</div>
<div className="TippingBlock-tipJarWrapper">
<TipJarBlock address={proposal.tipJarAddress} type="proposal" />
</div>
</>
);
} else {
content = <Loader />;
}
return (
<div className="Proposal-top-side-block">
<h2 className="Proposal-top-main-block-title">Tipping</h2>
<div className="Proposal-top-main-block">{content}</div>
</div>
);
};
export default TippingBlock;

View File

@ -0,0 +1,32 @@
@import '~styles/variables.less';
.TippingBlock {
&-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
overflow: hidden;
line-height: 1.7rem;
&-label {
font-size: 0.95rem;
font-weight: 300;
opacity: 0.8;
letter-spacing: 0.05rem;
flex: 0 0 auto;
margin-right: 1.5rem;
}
&-value {
font-size: 1.1rem;
flex-basis: 0;
flex-grow: 1;
overflow: hidden;
text-align: right;
}
}
&-tipJarWrapper {
margin-top: 1rem;
}
}

View File

@ -205,9 +205,16 @@
margin-left: 0;
}
&:last-child {
&:nth-child(2) {
width: calc(50% - 1rem);
}
&:nth-child(3) {
width: calc(50% - 1rem);
}
&:nth-child(4) {
width: calc(50% - 1rem);
margin-left: 1rem;
}
}
}
@ -218,7 +225,9 @@
> * {
&,
&:first-child,
&:last-child {
&:nth-child(2),
&:nth-child(3),
&:nth-child(4) {
width: auto;
margin-left: 0;
}

View File

@ -14,6 +14,7 @@ import { AlertProps } from 'antd/lib/alert';
import ExceptionPage from 'components/ExceptionPage';
import HeaderDetails from 'components/HeaderDetails';
import CampaignBlock from './CampaignBlock';
import TippingBlock from './TippingBlock'
import TeamBlock from './TeamBlock';
import RFPBlock from './RFPBlock';
import Milestones from './Milestones';
@ -246,6 +247,7 @@ export class ProposalDetail extends React.Component<Props, State> {
</div>
<div className="Proposal-top-side">
<CampaignBlock proposal={proposal} isPreview={!isLive} />
<TippingBlock proposal={proposal} />
<TeamBlock proposal={proposal} />
{proposal.rfp && <RFPBlock rfp={proposal.rfp} />}
</div>

View File

@ -3,7 +3,7 @@ import { Modal, Icon, Button, Form, Input } from 'antd';
import classnames from 'classnames';
import QRCode from 'qrcode.react';
import { formatZcashCLI, formatZcashURI } from 'utils/formatters';
import { getAmountErrorFromString } from 'utils/validators'
import { getAmountErrorFromString } from 'utils/validators';
import Loader from 'components/Loader';
import './TipJarModal.less';
import CopyInput from 'components/CopyInput';
@ -17,27 +17,25 @@ interface Props {
}
interface State {
amount: string | null;
amount: string;
}
export class TipJarModal extends React.Component<Props, State> {
static getDerivedStateFromProps = (nextProps: Props, prevState: State) => {
return prevState.amount === null ? { amount: nextProps.amount } : {};
static getDerivedStateFromProps = (nextProps: Props) => {
// while modal is closed, set amount state via props
return !nextProps.isOpen ? { amount: nextProps.amount } : {};
};
state: State = {
amount: null,
amount: '',
};
render() {
const { isOpen, onClose, type, address } = this.props;
const { amount } = this.state;
// should not be possible due to derived state, but makes TS happy
if (amount === null) return;
const amountError = getAmountErrorFromString(amount)
const amountIsValid = !amountError
const amountError = getAmountErrorFromString(amount);
const amountIsValid = !amountError;
const cli = amountIsValid ? formatZcashCLI(address, amount) : '';
const uri = amountIsValid ? formatZcashURI(address, amount) : '';
@ -122,5 +120,5 @@ export class TipJarModal extends React.Component<Props, State> {
amount: e.currentTarget.value,
});
private handleAfterClose = () => this.setState({ amount: null });
private handleAfterClose = () => this.setState({ amount: '' });
}