Price in Usd (#91)
* 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 * init backend changes * init admin changes * init frontend changes * fix backend tests * update campaign block * be - init rfp usd changes * admin - init rfp usd changes * fe - fully adapt api util functions to usd * fe - init rfp usd changes * adapt profile created to usd * misc usd changes * 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 * conditionally add info icon and tooltip to funding line * admin - disallow decimals in RFPs * fe - cover usd string edge case * add Usd as rfp bounty type
This commit is contained in:
parent
6f4e1b779b
commit
4a0e23e9c7
|
@ -29,6 +29,7 @@ import Markdown from 'components/Markdown';
|
||||||
import ArbiterControl from 'components/ArbiterControl';
|
import ArbiterControl from 'components/ArbiterControl';
|
||||||
import { toZat, fromZat } from 'src/util/units';
|
import { toZat, fromZat } from 'src/util/units';
|
||||||
import FeedbackModal from '../FeedbackModal';
|
import FeedbackModal from '../FeedbackModal';
|
||||||
|
import { formatUsd } from 'util/formatters';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
type Props = RouteComponentProps<any>;
|
type Props = RouteComponentProps<any>;
|
||||||
|
@ -285,11 +286,23 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ms = p.currentMilestone;
|
const ms = p.currentMilestone;
|
||||||
const amount = fromZat(
|
|
||||||
toZat(p.target)
|
let paymentMsg;
|
||||||
.mul(new BN(ms.payoutPercent))
|
if (p.isVersionTwo) {
|
||||||
.divn(100),
|
const target = parseFloat(p.target.toString());
|
||||||
);
|
const payoutPercent = parseFloat(ms.payoutPercent);
|
||||||
|
const amountNum = (target * payoutPercent) / 100;
|
||||||
|
const amount = formatUsd(amountNum, true, 2);
|
||||||
|
paymentMsg = `${amount} in ZEC`;
|
||||||
|
} else {
|
||||||
|
const amount = fromZat(
|
||||||
|
toZat(p.target)
|
||||||
|
.mul(new BN(ms.payoutPercent))
|
||||||
|
.divn(100),
|
||||||
|
);
|
||||||
|
paymentMsg = `${amount} ZEC`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className="ProposalDetail-alert"
|
className="ProposalDetail-alert"
|
||||||
|
@ -306,7 +319,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{' '}
|
{' '}
|
||||||
Please make a payment of <b>{amount.toString()} ZEC</b> to:
|
Please make a payment of <b>{paymentMsg}</b> to:
|
||||||
</p>{' '}
|
</p>{' '}
|
||||||
<pre>{p.payoutAddress}</pre>
|
<pre>{p.payoutAddress}</pre>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
|
@ -445,9 +458,12 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
||||||
{renderDeetItem('isFailed', JSON.stringify(p.isFailed))}
|
{renderDeetItem('isFailed', JSON.stringify(p.isFailed))}
|
||||||
{renderDeetItem('status', p.status)}
|
{renderDeetItem('status', p.status)}
|
||||||
{renderDeetItem('stage', p.stage)}
|
{renderDeetItem('stage', p.stage)}
|
||||||
{renderDeetItem('target', p.target)}
|
{renderDeetItem('target', p.isVersionTwo ? formatUsd(p.target) : p.target)}
|
||||||
{renderDeetItem('contributed', p.contributed)}
|
{renderDeetItem('contributed', p.contributed)}
|
||||||
{renderDeetItem('funded (inc. matching)', p.funded)}
|
{renderDeetItem(
|
||||||
|
'funded (inc. matching)',
|
||||||
|
p.isVersionTwo ? formatUsd(p.funded) : p.funded,
|
||||||
|
)}
|
||||||
{renderDeetItem('matching', p.contributionMatching)}
|
{renderDeetItem('matching', p.contributionMatching)}
|
||||||
{renderDeetItem('bounty', p.contributionBounty)}
|
{renderDeetItem('bounty', p.contributionBounty)}
|
||||||
{renderDeetItem('rfpOptIn', JSON.stringify(p.rfpOptIn))}
|
{renderDeetItem('rfpOptIn', JSON.stringify(p.rfpOptIn))}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Markdown from 'components/Markdown';
|
||||||
import { formatDateSeconds } from 'util/time';
|
import { formatDateSeconds } from 'util/time';
|
||||||
import store from 'src/store';
|
import store from 'src/store';
|
||||||
import { PROPOSAL_STATUS } from 'src/types';
|
import { PROPOSAL_STATUS } from 'src/types';
|
||||||
|
import { formatUsd } from 'src/util/formatters';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
type Props = RouteComponentProps<{ id?: string }>;
|
type Props = RouteComponentProps<{ id?: string }>;
|
||||||
|
@ -93,7 +94,10 @@ class RFPDetail extends React.Component<Props> {
|
||||||
{renderDeetItem('created', formatDateSeconds(rfp.dateCreated))}
|
{renderDeetItem('created', formatDateSeconds(rfp.dateCreated))}
|
||||||
{renderDeetItem('status', rfp.status)}
|
{renderDeetItem('status', rfp.status)}
|
||||||
{renderDeetItem('matching', String(rfp.matching))}
|
{renderDeetItem('matching', String(rfp.matching))}
|
||||||
{renderDeetItem('bounty', `${rfp.bounty} ZEC`)}
|
{renderDeetItem(
|
||||||
|
'bounty',
|
||||||
|
rfp.isVersionTwo ? formatUsd(rfp.bounty) : `${rfp.bounty} ZEC`,
|
||||||
|
)}
|
||||||
{renderDeetItem(
|
{renderDeetItem(
|
||||||
'dateCloses',
|
'dateCloses',
|
||||||
rfp.dateCloses && formatDateSeconds(rfp.dateCloses),
|
rfp.dateCloses && formatDateSeconds(rfp.dateCloses),
|
||||||
|
|
|
@ -47,6 +47,8 @@ class RFPForm extends React.Component<Props, State> {
|
||||||
dateCloses: undefined,
|
dateCloses: undefined,
|
||||||
};
|
};
|
||||||
const rfpId = this.getRFPId();
|
const rfpId = this.getRFPId();
|
||||||
|
let isVersionTwo = true;
|
||||||
|
|
||||||
if (rfpId) {
|
if (rfpId) {
|
||||||
if (!store.rfpsFetched) {
|
if (!store.rfpsFetched) {
|
||||||
return <Spin />;
|
return <Spin />;
|
||||||
|
@ -63,6 +65,7 @@ class RFPForm extends React.Component<Props, State> {
|
||||||
bounty: rfp.bounty,
|
bounty: rfp.bounty,
|
||||||
dateCloses: rfp.dateCloses || undefined,
|
dateCloses: rfp.dateCloses || undefined,
|
||||||
};
|
};
|
||||||
|
isVersionTwo = rfp.isVersionTwo;
|
||||||
} else {
|
} else {
|
||||||
return <Exception type="404" desc="This RFP does not exist" />;
|
return <Exception type="404" desc="This RFP does not exist" />;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +76,10 @@ class RFPForm extends React.Component<Props, State> {
|
||||||
: defaults.dateCloses && moment(defaults.dateCloses * 1000);
|
: defaults.dateCloses && moment(defaults.dateCloses * 1000);
|
||||||
const forceClosed = dateCloses && dateCloses.isBefore(moment.now());
|
const forceClosed = dateCloses && dateCloses.isBefore(moment.now());
|
||||||
|
|
||||||
|
const bountyMatchRule = isVersionTwo
|
||||||
|
? { pattern: /^[^.]*$/, message: 'Cannot contain a decimal' }
|
||||||
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="RFPForm" layout="vertical" onSubmit={this.handleSubmit}>
|
<Form className="RFPForm" layout="vertical" onSubmit={this.handleSubmit}>
|
||||||
<Back to="/rfps" text="RFPs" />
|
<Back to="/rfps" text="RFPs" />
|
||||||
|
@ -162,12 +169,17 @@ class RFPForm extends React.Component<Props, State> {
|
||||||
<Form.Item className="RFPForm-bounty" label="Bounty">
|
<Form.Item className="RFPForm-bounty" label="Bounty">
|
||||||
{getFieldDecorator('bounty', {
|
{getFieldDecorator('bounty', {
|
||||||
initialValue: defaults.bounty,
|
initialValue: defaults.bounty,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: 'Bounty is required' },
|
||||||
|
bountyMatchRule,
|
||||||
|
],
|
||||||
})(
|
})(
|
||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="bounty"
|
name="bounty"
|
||||||
placeholder="100"
|
placeholder="1000"
|
||||||
addonAfter="ZEC"
|
addonBefore={isVersionTwo ? '$' : undefined}
|
||||||
|
addonAfter={isVersionTwo ? undefined : 'ZEC'}
|
||||||
size="large"
|
size="large"
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -47,6 +47,7 @@ export interface RFP {
|
||||||
matching: boolean;
|
matching: boolean;
|
||||||
bounty: string | null;
|
bounty: string | null;
|
||||||
dateCloses: number | null;
|
dateCloses: number | null;
|
||||||
|
isVersionTwo: boolean;
|
||||||
}
|
}
|
||||||
export interface RFPArgs {
|
export interface RFPArgs {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
const toFixed = (num: string, digits: number = 3) => {
|
||||||
|
const [integerPart, fractionPart = ''] = num.split('.');
|
||||||
|
if (fractionPart.length === digits) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
if (fractionPart.length < digits) {
|
||||||
|
return `${integerPart}.${fractionPart.padEnd(digits, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decimalPoint = integerPart.length;
|
||||||
|
|
||||||
|
const formattedFraction = fractionPart.slice(0, digits);
|
||||||
|
|
||||||
|
const integerArr = `${integerPart}${formattedFraction}`.split('').map(str => +str);
|
||||||
|
|
||||||
|
let carryOver = Math.floor((+fractionPart[digits] + 5) / 10);
|
||||||
|
|
||||||
|
// grade school addition / rounding
|
||||||
|
for (let i = integerArr.length - 1; i >= 0; i--) {
|
||||||
|
const currVal = integerArr[i] + carryOver;
|
||||||
|
const newVal = currVal % 10;
|
||||||
|
carryOver = Math.floor(currVal / 10);
|
||||||
|
integerArr[i] = newVal;
|
||||||
|
if (i === 0 && carryOver > 0) {
|
||||||
|
integerArr.unshift(0);
|
||||||
|
decimalPoint++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const strArr = integerArr.map(n => n.toString());
|
||||||
|
|
||||||
|
strArr.splice(decimalPoint, 0, '.');
|
||||||
|
|
||||||
|
if (strArr[strArr.length - 1] === '.') {
|
||||||
|
strArr.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return strArr.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatNumber(num: string, digits?: number): string {
|
||||||
|
const parts = toFixed(num, digits).split('.');
|
||||||
|
|
||||||
|
// Remove trailing zeroes on decimal (If there is a decimal)
|
||||||
|
if (parts[1]) {
|
||||||
|
parts[1] = parts[1].replace(/0+$/, '');
|
||||||
|
|
||||||
|
// If there's nothing left, remove decimal altogether
|
||||||
|
if (!parts[1]) {
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commafy the whole numbers
|
||||||
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
|
||||||
|
return parts.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUsd(
|
||||||
|
amount: number | string | undefined | null,
|
||||||
|
includeDollarSign: boolean = true,
|
||||||
|
digits: number = 0,
|
||||||
|
) {
|
||||||
|
if (!amount) return includeDollarSign ? '$0' : '0';
|
||||||
|
const a = typeof amount === 'number' ? amount.toString() : amount;
|
||||||
|
const str = formatNumber(a, digits);
|
||||||
|
return includeDollarSign ? `$${str}` : str;
|
||||||
|
}
|
||||||
|
|
|
@ -39,4 +39,4 @@ EXPLORER_URL="https://chain.so/tx/ZECTEST/<txid>"
|
||||||
PROPOSAL_STAKING_AMOUNT=0.025
|
PROPOSAL_STAKING_AMOUNT=0.025
|
||||||
|
|
||||||
# Maximum amount for a proposal target, keep in sync with frontend .env
|
# Maximum amount for a proposal target, keep in sync with frontend .env
|
||||||
PROPOSAL_TARGET_MAX=10000
|
PROPOSAL_TARGET_MAX=500000
|
||||||
|
|
|
@ -363,9 +363,11 @@ class Proposal(db.Model):
|
||||||
if len(self.content) > 250000:
|
if len(self.content) > 250000:
|
||||||
raise ValidationException("Content cannot be longer than 250,000 characters")
|
raise ValidationException("Content cannot be longer than 250,000 characters")
|
||||||
if Decimal(self.target) > PROPOSAL_TARGET_MAX:
|
if Decimal(self.target) > PROPOSAL_TARGET_MAX:
|
||||||
raise ValidationException("Target cannot be more than {} ZEC".format(PROPOSAL_TARGET_MAX))
|
raise ValidationException("Target cannot be more than {} USD".format(PROPOSAL_TARGET_MAX))
|
||||||
if Decimal(self.target) < 0.0001:
|
if Decimal(self.target) < 0:
|
||||||
raise ValidationException("Target cannot be less than 0.0001")
|
raise ValidationException("Target cannot be less than 0")
|
||||||
|
if not self.target.isdigit():
|
||||||
|
raise ValidationException("Target must be a whole number")
|
||||||
if self.deadline_duration > 7776000:
|
if self.deadline_duration > 7776000:
|
||||||
raise ValidationException("Deadline duration cannot be more than 90 days")
|
raise ValidationException("Deadline duration cannot be more than 90 days")
|
||||||
|
|
||||||
|
@ -863,6 +865,7 @@ user_fields = [
|
||||||
"date_published",
|
"date_published",
|
||||||
"reject_reason",
|
"reject_reason",
|
||||||
"team",
|
"team",
|
||||||
|
"accepted_with_funding",
|
||||||
"is_version_two",
|
"is_version_two",
|
||||||
"authed_follows",
|
"authed_follows",
|
||||||
"authed_liked"
|
"authed_liked"
|
||||||
|
|
|
@ -32,6 +32,7 @@ class RFP(db.Model):
|
||||||
date_closes = db.Column(db.DateTime, nullable=True)
|
date_closes = db.Column(db.DateTime, nullable=True)
|
||||||
date_opened = db.Column(db.DateTime, nullable=True)
|
date_opened = db.Column(db.DateTime, nullable=True)
|
||||||
date_closed = db.Column(db.DateTime, nullable=True)
|
date_closed = db.Column(db.DateTime, nullable=True)
|
||||||
|
version = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
proposals = db.relationship(
|
proposals = db.relationship(
|
||||||
|
@ -111,6 +112,7 @@ class RFP(db.Model):
|
||||||
self.date_closes = date_closes
|
self.date_closes = date_closes
|
||||||
self.matching = matching
|
self.matching = matching
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self.version = '2'
|
||||||
|
|
||||||
|
|
||||||
class RFPSchema(ma.Schema):
|
class RFPSchema(ma.Schema):
|
||||||
|
@ -131,7 +133,8 @@ class RFPSchema(ma.Schema):
|
||||||
"date_closed",
|
"date_closed",
|
||||||
"accepted_proposals",
|
"accepted_proposals",
|
||||||
"authed_liked",
|
"authed_liked",
|
||||||
"likes_count"
|
"likes_count",
|
||||||
|
"is_version_two"
|
||||||
)
|
)
|
||||||
|
|
||||||
status = ma.Method("get_status")
|
status = ma.Method("get_status")
|
||||||
|
@ -139,6 +142,7 @@ class RFPSchema(ma.Schema):
|
||||||
date_opened = ma.Method("get_date_opened")
|
date_opened = ma.Method("get_date_opened")
|
||||||
date_closed = ma.Method("get_date_closed")
|
date_closed = ma.Method("get_date_closed")
|
||||||
accepted_proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
accepted_proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
||||||
|
is_version_two = ma.Method("get_is_version_two")
|
||||||
|
|
||||||
def get_status(self, obj):
|
def get_status(self, obj):
|
||||||
# Force it into closed state if date_closes is in the past
|
# Force it into closed state if date_closes is in the past
|
||||||
|
@ -155,6 +159,9 @@ class RFPSchema(ma.Schema):
|
||||||
def get_date_closed(self, obj):
|
def get_date_closed(self, obj):
|
||||||
return dt_to_unix(obj.date_closed) if obj.date_closed else None
|
return dt_to_unix(obj.date_closed) if obj.date_closed else None
|
||||||
|
|
||||||
|
def get_is_version_two(self, obj):
|
||||||
|
return True if obj.version == '2' else False
|
||||||
|
|
||||||
|
|
||||||
rfp_schema = RFPSchema()
|
rfp_schema = RFPSchema()
|
||||||
rfps_schema = RFPSchema(many=True)
|
rfps_schema = RFPSchema(many=True)
|
||||||
|
@ -177,6 +184,7 @@ class AdminRFPSchema(ma.Schema):
|
||||||
"date_opened",
|
"date_opened",
|
||||||
"date_closed",
|
"date_closed",
|
||||||
"proposals",
|
"proposals",
|
||||||
|
"is_version_two"
|
||||||
)
|
)
|
||||||
|
|
||||||
status = ma.Method("get_status")
|
status = ma.Method("get_status")
|
||||||
|
@ -185,6 +193,7 @@ class AdminRFPSchema(ma.Schema):
|
||||||
date_opened = ma.Method("get_date_opened")
|
date_opened = ma.Method("get_date_opened")
|
||||||
date_closed = ma.Method("get_date_closed")
|
date_closed = ma.Method("get_date_closed")
|
||||||
proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
||||||
|
is_version_two = ma.Method("get_is_version_two")
|
||||||
|
|
||||||
def get_status(self, obj):
|
def get_status(self, obj):
|
||||||
# Force it into closed state if date_closes is in the past
|
# Force it into closed state if date_closes is in the past
|
||||||
|
@ -204,6 +213,9 @@ class AdminRFPSchema(ma.Schema):
|
||||||
def get_date_closed(self, obj):
|
def get_date_closed(self, obj):
|
||||||
return dt_to_unix(obj.date_closes) if obj.date_closes else None
|
return dt_to_unix(obj.date_closes) if obj.date_closes else None
|
||||||
|
|
||||||
|
def get_is_version_two(self, obj):
|
||||||
|
return True if obj.version == '2' else False
|
||||||
|
|
||||||
|
|
||||||
admin_rfp_schema = AdminRFPSchema()
|
admin_rfp_schema = AdminRFPSchema()
|
||||||
admin_rfps_schema = AdminRFPSchema(many=True)
|
admin_rfps_schema = AdminRFPSchema(many=True)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 1e1460456ce4
|
||||||
|
Revises: c55f96720196
|
||||||
|
Create Date: 2019-11-21 20:36:37.504400
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1e1460456ce4'
|
||||||
|
down_revision = 'c55f96720196'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('rfp', sa.Column('version', sa.String(length=255), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('rfp', 'version')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -48,7 +48,7 @@ test_proposal = {
|
||||||
"brief": "$$$",
|
"brief": "$$$",
|
||||||
"milestones": test_milestones,
|
"milestones": test_milestones,
|
||||||
"category": Category.ACCESSIBILITY,
|
"category": Category.ACCESSIBILITY,
|
||||||
"target": "123.456",
|
"target": "12345",
|
||||||
"payoutAddress": "123",
|
"payoutAddress": "123",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ test_proposal = {
|
||||||
"brief": "$$$",
|
"brief": "$$$",
|
||||||
"milestones": milestones,
|
"milestones": milestones,
|
||||||
"category": Category.ACCESSIBILITY,
|
"category": Category.ACCESSIBILITY,
|
||||||
"target": "123.456",
|
"target": "12345",
|
||||||
"payoutAddress": "123",
|
"payoutAddress": "123",
|
||||||
"deadlineDuration": 100
|
"deadlineDuration": 100
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,4 @@ DISABLE_SSL=true
|
||||||
# TESTNET=true
|
# TESTNET=true
|
||||||
|
|
||||||
# Maximum amount for a proposal target, keep in sync with backend .env
|
# Maximum amount for a proposal target, keep in sync with backend .env
|
||||||
PROPOSAL_TARGET_MAX=10000
|
PROPOSAL_TARGET_MAX=500000
|
||||||
|
|
|
@ -169,7 +169,10 @@ class CreateFlowBasics extends React.Component<Props, State> {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Target amount"
|
label="Target amount"
|
||||||
validateStatus={errors.target ? 'error' : undefined}
|
validateStatus={errors.target ? 'error' : undefined}
|
||||||
help={errors.target || 'This cannot be changed once your proposal starts'}
|
help={
|
||||||
|
errors.target ||
|
||||||
|
'You will be paid out in ZEC at market price at payout time. This cannot be changed once your proposal starts'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
size="large"
|
size="large"
|
||||||
|
@ -178,7 +181,7 @@ class CreateFlowBasics extends React.Component<Props, State> {
|
||||||
type="number"
|
type="number"
|
||||||
value={target}
|
value={target}
|
||||||
onChange={this.handleInputChange}
|
onChange={this.handleInputChange}
|
||||||
addonAfter="ZEC"
|
addonBefore="$"
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import UserAvatar from 'components/UserAvatar';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { CREATE_STEP } from './index';
|
import { CREATE_STEP } from './index';
|
||||||
import { ProposalDraft } from 'types';
|
import { ProposalDraft } from 'types';
|
||||||
|
import { formatUsd } from 'utils/formatters';
|
||||||
import './Review.less';
|
import './Review.less';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
@ -59,7 +60,7 @@ class CreateReview extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'target',
|
key: 'target',
|
||||||
content: <div style={{ fontSize: '1.2rem' }}>{form.target} ZEC</div>,
|
content: <div style={{ fontSize: '1.2rem' }}>{formatUsd(form.target)}</div>,
|
||||||
error: errors.target,
|
error: errors.target,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { UserProposal } from 'types';
|
||||||
import './ProfileProposal.less';
|
import './ProfileProposal.less';
|
||||||
import UserRow from 'components/UserRow';
|
import UserRow from 'components/UserRow';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import { formatUsd } from 'utils/formatters'
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
proposal: UserProposal;
|
proposal: UserProposal;
|
||||||
|
@ -11,19 +13,47 @@ interface OwnProps {
|
||||||
|
|
||||||
export default class Profile extends React.Component<OwnProps> {
|
export default class Profile extends React.Component<OwnProps> {
|
||||||
render() {
|
render() {
|
||||||
const { title, brief, team, proposalId, funded, target } = this.props.proposal;
|
const {
|
||||||
|
title,
|
||||||
|
brief,
|
||||||
|
team,
|
||||||
|
proposalId,
|
||||||
|
funded,
|
||||||
|
target,
|
||||||
|
isVersionTwo,
|
||||||
|
acceptedWithFunding,
|
||||||
|
} = this.props.proposal;
|
||||||
|
|
||||||
|
// pulled from `variables.less`
|
||||||
|
const infoColor = '#1890ff'
|
||||||
|
const secondaryColor = '#2D2A26'
|
||||||
|
|
||||||
|
const tagColor = acceptedWithFunding
|
||||||
|
? secondaryColor
|
||||||
|
: infoColor
|
||||||
|
const tagMessage = acceptedWithFunding
|
||||||
|
? 'Funded by ZF'
|
||||||
|
: 'Open for Contributions'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ProfileProposal">
|
<div className="ProfileProposal">
|
||||||
<div className="ProfileProposal-block">
|
<div className="ProfileProposal-block">
|
||||||
<Link to={`/proposals/${proposalId}`} className="ProfileProposal-title">
|
<Link to={`/proposals/${proposalId}`} className="ProfileProposal-title">
|
||||||
{title}
|
{title} {isVersionTwo && (<Tag color={tagColor} style={{verticalAlign: 'text-top'}}>{tagMessage}</Tag>)}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ProfileProposal-brief">{brief}</div>
|
<div className="ProfileProposal-brief">{brief}</div>
|
||||||
<div className="ProfileProposal-raised">
|
{!isVersionTwo && (
|
||||||
<UnitDisplay value={funded} symbol="ZEC" displayShortBalance={4} />{' '}
|
<div className="ProfileProposal-raised">
|
||||||
<small>raised</small> of{' '}
|
<UnitDisplay value={funded} symbol="ZEC" displayShortBalance={4} />{' '}
|
||||||
<UnitDisplay value={target} symbol="ZEC" displayShortBalance={4} /> goal
|
<small>raised</small> of{' '}
|
||||||
</div>
|
<UnitDisplay value={target} symbol="ZEC" displayShortBalance={4} /> goal
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isVersionTwo && (
|
||||||
|
<div className="ProfileProposal-raised">
|
||||||
|
{formatUsd(target.toString())}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ProfileProposal-block">
|
<div className="ProfileProposal-block">
|
||||||
<h3>Team</h3>
|
<h3>Team</h3>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Icon, Popover } from 'antd';
|
import { Icon, Popover, Tooltip } from 'antd';
|
||||||
import { Proposal, STATUS } from 'types';
|
import { Proposal, STATUS } from 'types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -10,7 +10,8 @@ import { withRouter } from 'react-router';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { PROPOSAL_STAGE } from 'api/constants';
|
import { PROPOSAL_STAGE } from 'api/constants';
|
||||||
import ZFGrantsLogo from 'static/images/logo-name-light.svg'
|
import { formatUsd } from 'utils/formatters';
|
||||||
|
import ZFGrantsLogo from 'static/images/logo-name-light.svg';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
@ -107,7 +108,15 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
{isAcceptedWithFunding ? 'Funding' : 'Requested Funding'}
|
{isAcceptedWithFunding ? 'Funding' : 'Requested Funding'}
|
||||||
</div>
|
</div>
|
||||||
<div className="ProposalCampaignBlock-info-value">
|
<div className="ProposalCampaignBlock-info-value">
|
||||||
<UnitDisplay value={target} symbol="ZEC" />
|
{formatUsd(target.toString(10))}
|
||||||
|
{isAcceptedWithFunding && (
|
||||||
|
<Tooltip
|
||||||
|
placement="left"
|
||||||
|
title="Proposal owners will be paid out in ZEC at market price at payout time"
|
||||||
|
>
|
||||||
|
<Icon type="info-circle" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { proposalActions } from 'modules/proposals';
|
||||||
import { ProposalDetail } from 'modules/proposals/reducers';
|
import { ProposalDetail } from 'modules/proposals/reducers';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { formatUsd } from 'utils/formatters';
|
||||||
|
import { Zat } from 'utils/units';
|
||||||
|
|
||||||
enum STEP_STATUS {
|
enum STEP_STATUS {
|
||||||
WAIT = 'wait',
|
WAIT = 'wait',
|
||||||
|
@ -254,6 +256,7 @@ class ProposalMilestones extends React.Component<Props, State> {
|
||||||
!!proposal.arbiter.user &&
|
!!proposal.arbiter.user &&
|
||||||
proposal.arbiter.status === PROPOSAL_ARBITER_STATUS.ACCEPTED
|
proposal.arbiter.status === PROPOSAL_ARBITER_STATUS.ACCEPTED
|
||||||
}
|
}
|
||||||
|
isVersionTwo={proposal.isVersionTwo}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -329,12 +332,17 @@ interface MilestoneProps extends MSProps {
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
proposalId: number;
|
proposalId: number;
|
||||||
isFunded: boolean;
|
isFunded: boolean;
|
||||||
|
isVersionTwo: boolean;
|
||||||
}
|
}
|
||||||
const Milestone: React.SFC<MilestoneProps> = p => {
|
const Milestone: React.SFC<MilestoneProps> = p => {
|
||||||
const estimatedDate = p.dateEstimated
|
const estimatedDate = p.dateEstimated
|
||||||
? moment(p.dateEstimated * 1000).format('MMMM YYYY')
|
? moment(p.dateEstimated * 1000).format('MMMM YYYY')
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
const reward = <UnitDisplay value={p.amount} symbol="ZEC" displayShortBalance={4} />;
|
const reward = p.isVersionTwo ? (
|
||||||
|
formatUsd(p.amount as string, true, 2)
|
||||||
|
) : (
|
||||||
|
<UnitDisplay value={p.amount as Zat} symbol="ZEC" displayShortBalance={4} />
|
||||||
|
);
|
||||||
const getAlertProps = {
|
const getAlertProps = {
|
||||||
[MILESTONE_STAGE.IDLE]: () => null,
|
[MILESTONE_STAGE.IDLE]: () => null,
|
||||||
[MILESTONE_STAGE.REQUESTED]: () => ({
|
[MILESTONE_STAGE.REQUESTED]: () => ({
|
||||||
|
@ -370,7 +378,7 @@ const Milestone: React.SFC<MilestoneProps> = p => {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: (
|
message: (
|
||||||
<span>
|
<span>
|
||||||
The team was awarded <strong>{reward}</strong>{' '}
|
The team was awarded <strong>{reward}</strong> {p.isVersionTwo && `in ZEC`}
|
||||||
{p.immediatePayout && ` as an initial payout `} on {fmtDate(p.datePaid)}.
|
{p.immediatePayout && ` as an initial payout `} on {fmtDate(p.datePaid)}.
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Proposal } from 'types';
|
||||||
import Card from 'components/Card';
|
import Card from 'components/Card';
|
||||||
import UserAvatar from 'components/UserAvatar';
|
import UserAvatar from 'components/UserAvatar';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
|
import { formatUsd } from 'utils/formatters'
|
||||||
import './style.less';
|
import './style.less';
|
||||||
|
|
||||||
export class ProposalCard extends React.Component<Proposal> {
|
export class ProposalCard extends React.Component<Proposal> {
|
||||||
|
@ -41,7 +42,7 @@ export class ProposalCard extends React.Component<Proposal> {
|
||||||
{isVersionTwo && (
|
{isVersionTwo && (
|
||||||
<div className="ProposalCard-funding">
|
<div className="ProposalCard-funding">
|
||||||
<div className="ProposalCard-funding-raised">
|
<div className="ProposalCard-funding-raised">
|
||||||
<UnitDisplay value={target} symbol="ZEC" />
|
{formatUsd(target.toString(10))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import UnitDisplay from 'components/UnitDisplay';
|
||||||
import HeaderDetails from 'components/HeaderDetails';
|
import HeaderDetails from 'components/HeaderDetails';
|
||||||
import Like from 'components/Like';
|
import Like from 'components/Like';
|
||||||
import { RFP_STATUS } from 'api/constants';
|
import { RFP_STATUS } from 'api/constants';
|
||||||
|
import { formatUsd } from 'utils/formatters';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
@ -62,11 +63,19 @@ class RFPDetail extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rfp.bounty) {
|
if (rfp.bounty) {
|
||||||
tags.push(
|
if (rfp.isVersionTwo) {
|
||||||
<Tag key="bounty" color="#CF8A00">
|
tags.push(
|
||||||
<UnitDisplay value={rfp.bounty} symbol="ZEC" /> bounty
|
<Tag key="bounty" color="#CF8A00">
|
||||||
</Tag>,
|
{formatUsd(rfp.bounty.toString(10))} bounty
|
||||||
);
|
</Tag>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tags.push(
|
||||||
|
<Tag key="bounty" color="#CF8A00">
|
||||||
|
<UnitDisplay value={rfp.bounty} symbol="ZEC" /> bounty
|
||||||
|
</Tag>,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLive) {
|
if (!isLive) {
|
||||||
|
@ -98,14 +107,22 @@ class RFPDetail extends React.Component<Props> {
|
||||||
<Markdown className="RFPDetail-content" source={rfp.content} />
|
<Markdown className="RFPDetail-content" source={rfp.content} />
|
||||||
<div className="RFPDetail-rules">
|
<div className="RFPDetail-rules">
|
||||||
<ul>
|
<ul>
|
||||||
{rfp.bounty && (
|
{rfp.bounty &&
|
||||||
<li>
|
rfp.isVersionTwo && (
|
||||||
Accepted proposals will be funded up to{' '}
|
<li>
|
||||||
<strong>
|
Accepted proposals will be funded up to{' '}
|
||||||
<UnitDisplay value={rfp.bounty} symbol="ZEC" />
|
<strong>{formatUsd(rfp.bounty.toString(10))}</strong> in ZEC
|
||||||
</strong>
|
</li>
|
||||||
</li>
|
)}
|
||||||
)}
|
{rfp.bounty &&
|
||||||
|
!rfp.isVersionTwo && (
|
||||||
|
<li>
|
||||||
|
Accepted proposals will be funded up to{' '}
|
||||||
|
<strong>
|
||||||
|
<UnitDisplay value={rfp.bounty} symbol="ZEC" />
|
||||||
|
</strong>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{rfp.matching && (
|
{rfp.matching && (
|
||||||
<li>
|
<li>
|
||||||
Contributions will have their <strong>funding matched</strong> by the
|
Contributions will have their <strong>funding matched</strong> by the
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Tag } from 'antd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
import { RFP } from 'types';
|
import { RFP } from 'types';
|
||||||
|
import { formatUsd } from 'utils/formatters';
|
||||||
import './RFPItem.less';
|
import './RFPItem.less';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -25,17 +26,26 @@ export default class RFPItem extends React.Component<Props> {
|
||||||
dateClosed,
|
dateClosed,
|
||||||
bounty,
|
bounty,
|
||||||
matching,
|
matching,
|
||||||
|
isVersionTwo,
|
||||||
} = rfp;
|
} = rfp;
|
||||||
const closeDate = dateCloses || dateClosed;
|
const closeDate = dateCloses || dateClosed;
|
||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
if (!isSmall) {
|
if (!isSmall) {
|
||||||
if (bounty) {
|
if (bounty) {
|
||||||
tags.push(
|
if (isVersionTwo) {
|
||||||
<Tag key="bounty" color="#CF8A00">
|
tags.push(
|
||||||
<UnitDisplay value={bounty} symbol="ZEC" /> bounty
|
<Tag key="bounty" color="#CF8A00">
|
||||||
</Tag>,
|
{formatUsd(bounty.toString(10))} bounty
|
||||||
);
|
</Tag>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tags.push(
|
||||||
|
<Tag key="bounty" color="#CF8A00">
|
||||||
|
<UnitDisplay value={bounty} symbol="ZEC" /> bounty
|
||||||
|
</Tag>,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (matching) {
|
if (matching) {
|
||||||
tags.push(
|
tags.push(
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { ProposalDraft, STATUS, MILESTONE_STAGE, PROPOSAL_ARBITER_STATUS } from 'types';
|
import { ProposalDraft, STATUS, MILESTONE_STAGE, PROPOSAL_ARBITER_STATUS } from 'types';
|
||||||
import { User } from 'types';
|
import { User } from 'types';
|
||||||
import {
|
import {
|
||||||
getAmountError,
|
getAmountErrorUsd,
|
||||||
|
getAmountErrorUsdFromString,
|
||||||
isValidSaplingAddress,
|
isValidSaplingAddress,
|
||||||
isValidTAddress,
|
isValidTAddress,
|
||||||
isValidSproutAddress,
|
isValidSproutAddress,
|
||||||
|
@ -98,7 +99,8 @@ export function getCreateErrors(
|
||||||
const targetFloat = target ? parseFloat(target) : 0;
|
const targetFloat = target ? parseFloat(target) : 0;
|
||||||
if (target && !Number.isNaN(targetFloat)) {
|
if (target && !Number.isNaN(targetFloat)) {
|
||||||
const limit = parseFloat(process.env.PROPOSAL_TARGET_MAX as string);
|
const limit = parseFloat(process.env.PROPOSAL_TARGET_MAX as string);
|
||||||
const targetErr = getAmountError(targetFloat, limit, 0.001);
|
const targetErr =
|
||||||
|
getAmountErrorUsd(targetFloat, limit) || getAmountErrorUsdFromString(target);
|
||||||
if (targetErr) {
|
if (targetErr) {
|
||||||
errors.target = targetErr;
|
errors.target = targetErr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from 'types';
|
} from 'types';
|
||||||
import { UserState } from 'modules/users/reducers';
|
import { UserState } from 'modules/users/reducers';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { toZat } from './units';
|
import { toZat, toUsd } from './units';
|
||||||
|
|
||||||
export function formatUserForPost(user: User) {
|
export function formatUserForPost(user: User) {
|
||||||
return {
|
return {
|
||||||
|
@ -21,8 +21,14 @@ export function formatUserForPost(user: User) {
|
||||||
|
|
||||||
export function formatUserFromGet(user: UserState) {
|
export function formatUserFromGet(user: UserState) {
|
||||||
const bnUserProp = (p: any) => {
|
const bnUserProp = (p: any) => {
|
||||||
p.funded = toZat(p.funded);
|
if (p.isVersionTwo) {
|
||||||
p.target = toZat(p.target);
|
p.funded = toUsd(p.funded);
|
||||||
|
p.target = toUsd(p.target);
|
||||||
|
} else {
|
||||||
|
p.funded = toZat(p.funded);
|
||||||
|
p.target = toZat(p.target);
|
||||||
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
if (user.pendingProposals) {
|
if (user.pendingProposals) {
|
||||||
|
@ -84,17 +90,40 @@ export function formatProposalPageFromGet(page: any): ProposalPage {
|
||||||
export function formatProposalFromGet(p: any): Proposal {
|
export function formatProposalFromGet(p: any): Proposal {
|
||||||
const proposal = { ...p } as Proposal;
|
const proposal = { ...p } as Proposal;
|
||||||
proposal.proposalUrlId = generateSlugUrl(proposal.proposalId, proposal.title);
|
proposal.proposalUrlId = generateSlugUrl(proposal.proposalId, proposal.title);
|
||||||
proposal.target = toZat(p.target);
|
|
||||||
proposal.funded = toZat(p.funded);
|
if (proposal.isVersionTwo) {
|
||||||
proposal.contributionBounty = toZat(p.contributionBounty);
|
proposal.target = toUsd(p.target);
|
||||||
proposal.percentFunded = proposal.target.isZero()
|
proposal.funded = toUsd(p.funded);
|
||||||
? 0
|
|
||||||
: proposal.funded.div(proposal.target.divn(100)).toNumber();
|
// not used in v2 proposals, but populated for completeness
|
||||||
|
proposal.contributionBounty = toUsd(p.contributionBounty);
|
||||||
|
proposal.percentFunded = 0;
|
||||||
|
} else {
|
||||||
|
proposal.target = toZat(p.target);
|
||||||
|
proposal.funded = toZat(p.funded);
|
||||||
|
proposal.contributionBounty = toZat(p.contributionBounty);
|
||||||
|
proposal.percentFunded = proposal.target.isZero()
|
||||||
|
? 0
|
||||||
|
: proposal.funded.div(proposal.target.divn(100)).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
if (proposal.milestones) {
|
if (proposal.milestones) {
|
||||||
const msToFe = (m: any) => ({
|
const msToFe = (m: any) => {
|
||||||
...m,
|
let amount;
|
||||||
amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
|
|
||||||
});
|
if (proposal.isVersionTwo) {
|
||||||
|
const target = parseFloat(proposal.target.toString());
|
||||||
|
const payoutPercent = parseFloat(m.payoutPercent);
|
||||||
|
amount = ((target * payoutPercent) / 100).toFixed(2);
|
||||||
|
} else {
|
||||||
|
amount = proposal.target.mul(new BN(m.payoutPercent)).divn(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
amount,
|
||||||
|
};
|
||||||
|
};
|
||||||
proposal.milestones = proposal.milestones.map(msToFe);
|
proposal.milestones = proposal.milestones.map(msToFe);
|
||||||
proposal.currentMilestone = msToFe(proposal.currentMilestone);
|
proposal.currentMilestone = msToFe(proposal.currentMilestone);
|
||||||
}
|
}
|
||||||
|
@ -107,7 +136,7 @@ export function formatProposalFromGet(p: any): Proposal {
|
||||||
export function formatRFPFromGet(rfp: RFP): RFP {
|
export function formatRFPFromGet(rfp: RFP): RFP {
|
||||||
rfp.urlId = generateSlugUrl(rfp.id, rfp.title);
|
rfp.urlId = generateSlugUrl(rfp.id, rfp.title);
|
||||||
if (rfp.bounty) {
|
if (rfp.bounty) {
|
||||||
rfp.bounty = toZat(rfp.bounty as any);
|
rfp.bounty = rfp.isVersionTwo ? toUsd(rfp.bounty as any) : toZat(rfp.bounty as any);
|
||||||
}
|
}
|
||||||
if (rfp.acceptedProposals) {
|
if (rfp.acceptedProposals) {
|
||||||
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
|
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
|
||||||
|
@ -139,42 +168,53 @@ export function extractIdFromSlug(slug: string) {
|
||||||
export function massageSerializedState(state: AppState) {
|
export function massageSerializedState(state: AppState) {
|
||||||
// proposal detail
|
// proposal detail
|
||||||
if (state.proposal.detail) {
|
if (state.proposal.detail) {
|
||||||
|
const { isVersionTwo } = state.proposal.detail
|
||||||
|
const base = isVersionTwo ? 10 : 16;
|
||||||
|
|
||||||
state.proposal.detail.target = new BN(
|
state.proposal.detail.target = new BN(
|
||||||
(state.proposal.detail.target as any) as string,
|
(state.proposal.detail.target as any) as string,
|
||||||
16,
|
base,
|
||||||
);
|
);
|
||||||
state.proposal.detail.funded = new BN(
|
state.proposal.detail.funded = new BN(
|
||||||
(state.proposal.detail.funded as any) as string,
|
(state.proposal.detail.funded as any) as string,
|
||||||
16,
|
base,
|
||||||
);
|
);
|
||||||
state.proposal.detail.contributionBounty = new BN((state.proposal.detail
|
state.proposal.detail.contributionBounty = new BN((state.proposal.detail
|
||||||
.contributionBounty as any) as string);
|
.contributionBounty as any) as string);
|
||||||
state.proposal.detail.milestones = state.proposal.detail.milestones.map(m => ({
|
state.proposal.detail.milestones = state.proposal.detail.milestones.map(m => ({
|
||||||
...m,
|
...m,
|
||||||
amount: new BN((m.amount as any) as string, 16),
|
amount: isVersionTwo
|
||||||
|
? m.amount
|
||||||
|
: new BN((m.amount as any) as string, 16),
|
||||||
}));
|
}));
|
||||||
if (state.proposal.detail.rfp && state.proposal.detail.rfp.bounty) {
|
if (state.proposal.detail.rfp && state.proposal.detail.rfp.bounty) {
|
||||||
state.proposal.detail.rfp.bounty = new BN(
|
state.proposal.detail.rfp.bounty = new BN(
|
||||||
(state.proposal.detail.rfp.bounty as any) as string,
|
(state.proposal.detail.rfp.bounty as any) as string,
|
||||||
16,
|
base,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// proposals
|
// proposals
|
||||||
state.proposal.page.items = state.proposal.page.items.map(p => ({
|
state.proposal.page.items = state.proposal.page.items.map(p => {
|
||||||
...p,
|
const base = p.isVersionTwo ? 10 : 16;
|
||||||
target: new BN((p.target as any) as string, 16),
|
return {
|
||||||
funded: new BN((p.funded as any) as string, 16),
|
...p,
|
||||||
contributionBounty: new BN((p.contributionMatching as any) as string, 16),
|
target: new BN((p.target as any) as string, base),
|
||||||
milestones: p.milestones.map(m => ({
|
funded: new BN((p.funded as any) as string, base),
|
||||||
...m,
|
contributionBounty: new BN((p.contributionMatching as any) as string, base),
|
||||||
amount: new BN((m.amount as any) as string, 16),
|
milestones: p.milestones.map(m => ({
|
||||||
})),
|
...m,
|
||||||
}));
|
amount: p.isVersionTwo
|
||||||
|
? m.amount
|
||||||
|
: new BN((m.amount as any) as string, 16),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
// users
|
// users
|
||||||
const bnUserProp = (p: UserProposal) => {
|
const bnUserProp = (p: UserProposal) => {
|
||||||
p.funded = new BN(p.funded, 16);
|
const base = p.isVersionTwo ? 10 : 16;
|
||||||
p.target = new BN(p.target, 16);
|
p.funded = new BN(p.funded, base);
|
||||||
|
p.target = new BN(p.target, base);
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
Object.values(state.users.map).forEach(user => {
|
Object.values(state.users.map).forEach(user => {
|
||||||
|
@ -190,8 +230,9 @@ export function massageSerializedState(state: AppState) {
|
||||||
});
|
});
|
||||||
// RFPs
|
// RFPs
|
||||||
state.rfps.rfps = state.rfps.rfps.map(rfp => {
|
state.rfps.rfps = state.rfps.rfps.map(rfp => {
|
||||||
|
const base = rfp.isVersionTwo ? 10 : 16;
|
||||||
if (rfp.bounty) {
|
if (rfp.bounty) {
|
||||||
rfp.bounty = new BN(rfp.bounty, 16);
|
rfp.bounty = new BN(rfp.bounty, base);
|
||||||
}
|
}
|
||||||
return rfp;
|
return rfp;
|
||||||
});
|
});
|
||||||
|
|
|
@ -92,3 +92,14 @@ export function formatTxExplorerUrl(txid: string) {
|
||||||
}
|
}
|
||||||
throw new Error('EXPLORER_URL env variable needs to be set!');
|
throw new Error('EXPLORER_URL env variable needs to be set!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatUsd(
|
||||||
|
amount: number | string | undefined | null,
|
||||||
|
includeDollarSign: boolean = true,
|
||||||
|
digits: number = 0,
|
||||||
|
) {
|
||||||
|
if (!amount) return includeDollarSign ? '$0' : '0';
|
||||||
|
const a = typeof amount === 'number' ? amount.toString() : amount;
|
||||||
|
const str = formatNumber(a, digits);
|
||||||
|
return includeDollarSign ? `$${str}` : str;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const Units = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Zat = BN;
|
export type Zat = BN;
|
||||||
|
export type Usd = BN;
|
||||||
export type UnitKey = keyof typeof Units;
|
export type UnitKey = keyof typeof Units;
|
||||||
|
|
||||||
export const handleValues = (input: string | BN) => {
|
export const handleValues = (input: string | BN) => {
|
||||||
|
@ -61,4 +62,14 @@ export const toZat = (value: string | number): Zat => {
|
||||||
return Zat(zat);
|
return Zat(zat);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toUsd = (value: string | number): Usd => {
|
||||||
|
value = value.toString();
|
||||||
|
const hasDecimal = value.indexOf('.') !== -1;
|
||||||
|
|
||||||
|
// decimals aren't allowed for proposal targets,
|
||||||
|
// but remove decimal if it exists
|
||||||
|
value = hasDecimal ? value.split('.')[0] : value;
|
||||||
|
return new BN(value, 10);
|
||||||
|
};
|
||||||
|
|
||||||
export const getDecimalFromUnitKey = (key: UnitKey) => Units[key].length - 1;
|
export const getDecimalFromUnitKey = (key: UnitKey) => Units[key].length - 1;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { formatUsd } from './formatters';
|
||||||
|
|
||||||
export function getAmountError(amount: number, max: number = Infinity, min?: number) {
|
export function getAmountError(amount: number, max: number = Infinity, min?: number) {
|
||||||
if (amount < 0) {
|
if (amount < 0) {
|
||||||
return 'Amount must be a positive number';
|
return 'Amount must be a positive number';
|
||||||
|
@ -15,16 +17,38 @@ export function getAmountError(amount: number, max: number = Infinity, min?: num
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAmountErrorUsd(amount: number, max: number = Infinity, min?: number) {
|
||||||
|
if (amount < 0) {
|
||||||
|
return 'Amount must be a positive number';
|
||||||
|
} else if (!Number.isInteger(amount)) {
|
||||||
|
return 'Amount must be a whole number';
|
||||||
|
} else if (amount > max) {
|
||||||
|
return `Cannot exceed maximum (${formatUsd(max)})`;
|
||||||
|
} else if (min && amount < min) {
|
||||||
|
return `Must be at least ${formatUsd(min)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Covers the edge case where whole decimals (eg. 100.00) is valid in getAmountErrorUsd
|
||||||
|
export function getAmountErrorUsdFromString(amount: string) {
|
||||||
|
return amount.indexOf('.') !== -1
|
||||||
|
? 'Amount must be a whole number'
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
export function getAmountErrorFromString(amount: string, max?: number, min?: number) {
|
export function getAmountErrorFromString(amount: string, max?: number, min?: number) {
|
||||||
const parsedAmount = parseFloat(amount)
|
const parsedAmount = parseFloat(amount);
|
||||||
if (Number.isNaN(parsedAmount)) {
|
if (Number.isNaN(parsedAmount)) {
|
||||||
return 'Not a valid number'
|
return 'Not a valid number';
|
||||||
}
|
}
|
||||||
// prevents "-0" from being valid...
|
// prevents "-0" from being valid...
|
||||||
if (amount[0] === '-') {
|
if (amount[0] === '-') {
|
||||||
return 'Amount must be a positive number'
|
return 'Amount must be a positive number';
|
||||||
}
|
}
|
||||||
return getAmountError(parsedAmount, max, min)
|
return getAmountError(parsedAmount, max, min);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidEmail(email: string): boolean {
|
export function isValidEmail(email: string): boolean {
|
||||||
|
|
|
@ -19,7 +19,7 @@ export enum MILESTONE_STAGE {
|
||||||
export interface Milestone {
|
export interface Milestone {
|
||||||
index: number;
|
index: number;
|
||||||
stage: MILESTONE_STAGE;
|
stage: MILESTONE_STAGE;
|
||||||
amount: Zat;
|
amount: Zat | string;
|
||||||
immediatePayout: boolean;
|
immediatePayout: boolean;
|
||||||
dateEstimated?: number;
|
dateEstimated?: number;
|
||||||
daysEstimated?: string;
|
daysEstimated?: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Zat } from 'utils/units';
|
import { Zat, Usd } from 'utils/units';
|
||||||
import { PROPOSAL_STAGE } from 'api/constants';
|
import { PROPOSAL_STAGE } from 'api/constants';
|
||||||
import { CreateMilestone, Update, User, Comment, ContributionWithUser } from 'types';
|
import { CreateMilestone, Update, User, Comment, ContributionWithUser } from 'types';
|
||||||
import { ProposalMilestone } from './milestone';
|
import { ProposalMilestone } from './milestone';
|
||||||
|
@ -52,8 +52,8 @@ export interface ProposalDraft {
|
||||||
export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
||||||
proposalAddress: string;
|
proposalAddress: string;
|
||||||
proposalUrlId: string;
|
proposalUrlId: string;
|
||||||
target: Zat;
|
target: Zat | Usd;
|
||||||
funded: Zat;
|
funded: Zat | Usd;
|
||||||
percentFunded: number;
|
percentFunded: number;
|
||||||
contributionMatching: number;
|
contributionMatching: number;
|
||||||
contributionBounty: Zat;
|
contributionBounty: Zat;
|
||||||
|
@ -99,13 +99,15 @@ export interface UserProposal {
|
||||||
status: STATUS;
|
status: STATUS;
|
||||||
title: string;
|
title: string;
|
||||||
brief: string;
|
brief: string;
|
||||||
funded: Zat;
|
funded: Zat | Usd;
|
||||||
target: Zat;
|
target: Zat | Usd;
|
||||||
dateCreated: number;
|
dateCreated: number;
|
||||||
dateApproved: number;
|
dateApproved: number;
|
||||||
datePublished: number;
|
datePublished: number;
|
||||||
team: User[];
|
team: User[];
|
||||||
rejectReason: string;
|
rejectReason: string;
|
||||||
|
acceptedWithFunding: boolean | null;
|
||||||
|
isVersionTwo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: sync with backend/grant/proposal/models.py STATUSES
|
// NOTE: sync with backend/grant/proposal/models.py STATUSES
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Proposal } from './proposal';
|
import { Proposal } from './proposal';
|
||||||
import { RFP_STATUS } from 'api/constants';
|
import { RFP_STATUS } from 'api/constants';
|
||||||
import { Zat } from 'utils/units';
|
import { Zat, Usd } from 'utils/units';
|
||||||
|
|
||||||
export interface RFP {
|
export interface RFP {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -10,11 +10,12 @@ export interface RFP {
|
||||||
content: string;
|
content: string;
|
||||||
status: RFP_STATUS;
|
status: RFP_STATUS;
|
||||||
acceptedProposals: Proposal[];
|
acceptedProposals: Proposal[];
|
||||||
bounty: Zat | null;
|
bounty: Zat | Usd | null;
|
||||||
matching: boolean;
|
matching: boolean;
|
||||||
dateOpened: number;
|
dateOpened: number;
|
||||||
dateClosed?: number;
|
dateClosed?: number;
|
||||||
dateCloses?: number;
|
dateCloses?: number;
|
||||||
authedLiked: boolean;
|
authedLiked: boolean;
|
||||||
likesCount: number;
|
likesCount: number;
|
||||||
|
isVersionTwo: boolean;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue