Merge pull request #179 from grant-project/rfp-fields
RFP Fields (matching, bounty, dates)
This commit is contained in:
commit
fe5632dc75
|
@ -91,6 +91,9 @@ class RFPDetail extends React.Component<Props> {
|
|||
{renderDeetItem('created', formatDateSeconds(rfp.dateCreated))}
|
||||
{renderDeetItem('status', rfp.status)}
|
||||
{renderDeetItem('category', rfp.category)}
|
||||
{renderDeetItem('matching', String(rfp.matching))}
|
||||
{renderDeetItem('bounty', `${rfp.bounty} ZEC`)}
|
||||
{renderDeetItem('dateCloses', rfp.dateCloses && formatDateSeconds(rfp.dateCloses))}
|
||||
</Card>
|
||||
|
||||
{/* PROPOSALS */}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
.RFPForm {
|
||||
max-width: 880px;
|
||||
|
||||
&-content {
|
||||
&-preview {
|
||||
font-size: 1rem;
|
||||
|
@ -13,6 +15,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-bounty {
|
||||
&-matching.ant-checkbox-wrapper {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-date {
|
||||
padding-left: 1.5rem;
|
||||
|
||||
.ant-calendar-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-form-explain {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
.ant-btn {
|
||||
margin-right: 0.5rem;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { view } from 'react-easy-state';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { Form, Input, Select, Icon, Button, message, Spin } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Form, Input, Select, Icon, Button, message, Spin, Checkbox, Row, Col, DatePicker } from 'antd';
|
||||
import Exception from 'ant-design-pro/lib/Exception';
|
||||
import { FormComponentProps } from 'antd/lib/form';
|
||||
import { PROPOSAL_CATEGORY, RFP_STATUS, RFPArgs } from 'src/types';
|
||||
|
@ -34,7 +36,7 @@ class RFPForm extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
const { isShowingPreview } = this.state;
|
||||
const { getFieldDecorator, getFieldValue } = this.props.form;
|
||||
const { getFieldDecorator, getFieldValue, isFieldsTouched } = this.props.form;
|
||||
|
||||
let defaults: RFPArgs = {
|
||||
title: '',
|
||||
|
@ -42,6 +44,9 @@ class RFPForm extends React.Component<Props, State> {
|
|||
content: '',
|
||||
category: '',
|
||||
status: '',
|
||||
matching: false,
|
||||
bounty: undefined,
|
||||
dateCloses: undefined,
|
||||
};
|
||||
const rfpId = this.getRFPId();
|
||||
if (rfpId) {
|
||||
|
@ -57,11 +62,19 @@ class RFPForm extends React.Component<Props, State> {
|
|||
content: rfp.content,
|
||||
category: rfp.category,
|
||||
status: rfp.status,
|
||||
matching: rfp.matching,
|
||||
bounty: rfp.bounty,
|
||||
dateCloses: rfp.dateCloses || undefined,
|
||||
};
|
||||
} else {
|
||||
return <Exception type="404" desc="This RFP does not exist" />;
|
||||
}
|
||||
}
|
||||
|
||||
const dateCloses = isFieldsTouched(['dateCloses'])
|
||||
? getFieldValue('dateCloses')
|
||||
: defaults.dateCloses && moment(defaults.dateCloses * 1000);
|
||||
const forceClosed = dateCloses && dateCloses.isBefore(moment.now());
|
||||
|
||||
return (
|
||||
<Form className="RFPForm" layout="vertical" onSubmit={this.handleSubmit}>
|
||||
|
@ -85,12 +98,15 @@ class RFPForm extends React.Component<Props, State> {
|
|||
</Form.Item>
|
||||
|
||||
{rfpId && (
|
||||
<Form.Item label="Status">
|
||||
<Form.Item
|
||||
label="Status"
|
||||
help={forceClosed && 'Status is forced to "Closed" when close date is in the past'}
|
||||
>
|
||||
{getFieldDecorator('status', {
|
||||
initialValue: defaults.status,
|
||||
rules: [{ required: true, message: 'Status is required' }],
|
||||
})(
|
||||
<Select size="large" placeholder="Select a status">
|
||||
<Select size="large" placeholder="Select a status" disabled={forceClosed}>
|
||||
{typedKeys(RFP_STATUS).map(c => (
|
||||
<Select.Option value={c} key={c}>
|
||||
{getStatusById(RFP_STATUSES, c).tagDisplay}
|
||||
|
@ -164,13 +180,57 @@ class RFPForm extends React.Component<Props, State> {
|
|||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Row>
|
||||
<Col sm={12} xs={12}>
|
||||
<Form.Item className="RFPForm-bounty" label="Bounty">
|
||||
{getFieldDecorator('bounty', {
|
||||
initialValue: defaults.bounty,
|
||||
})(
|
||||
<Input
|
||||
autoComplete="off"
|
||||
name="bounty"
|
||||
placeholder="100"
|
||||
addonAfter="ZEC"
|
||||
size="large"
|
||||
/>,
|
||||
)}
|
||||
{getFieldDecorator('matching', {
|
||||
initialValue: defaults.matching,
|
||||
})(
|
||||
<Checkbox
|
||||
className="RFPForm-bounty-matching"
|
||||
name="matching"
|
||||
defaultChecked={defaults.matching}
|
||||
>
|
||||
Match community contributions for approved proposals
|
||||
</Checkbox>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col sm={12} xs={24}>
|
||||
<Form.Item
|
||||
className="RFPForm-date"
|
||||
label="Close date"
|
||||
help="Date that proposals will stop being submittable by"
|
||||
>
|
||||
{getFieldDecorator('dateCloses', {
|
||||
initialValue: defaults.dateCloses ? moment(defaults.dateCloses * 1000) : undefined,
|
||||
})(
|
||||
<DatePicker size="large" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="RFPForm-buttons">
|
||||
<Button type="primary" htmlType="submit" size="large">
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="ghost" size="large">
|
||||
Cancel
|
||||
</Button>
|
||||
<Link to="/rfps">
|
||||
<Button type="ghost" size="large">
|
||||
Cancel
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
@ -189,10 +249,14 @@ class RFPForm extends React.Component<Props, State> {
|
|||
|
||||
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll(async (err: any, values: any) => {
|
||||
this.props.form.validateFieldsAndScroll(async (err: any, rawValues: any) => {
|
||||
if (err) return;
|
||||
|
||||
const rfpId = this.getRFPId();
|
||||
const values = {
|
||||
...rawValues,
|
||||
dateCloses: rawValues.dateCloses && rawValues.dateCloses.unix(),
|
||||
};
|
||||
let msg;
|
||||
if (rfpId) {
|
||||
await store.editRFP(rfpId, values);
|
||||
|
|
|
@ -36,19 +36,27 @@ export enum RFP_STATUS {
|
|||
export interface RFP {
|
||||
id: number;
|
||||
dateCreated: number;
|
||||
dateOpened: number | null;
|
||||
dateClosed: number | null;
|
||||
title: string;
|
||||
brief: string;
|
||||
content: string;
|
||||
category: string;
|
||||
status: string;
|
||||
proposals: Proposal[];
|
||||
matching: boolean;
|
||||
bounty: string | null;
|
||||
dateCloses: number | null;
|
||||
}
|
||||
export interface RFPArgs {
|
||||
title: string;
|
||||
brief: string;
|
||||
content: string;
|
||||
category: string;
|
||||
status?: string;
|
||||
matching: boolean;
|
||||
dateCloses: number | null | undefined;
|
||||
bounty: string | null | undefined;
|
||||
status: string;
|
||||
}
|
||||
// NOTE: sync with backend/grant/utils/enums.py ProposalArbiterStatus
|
||||
export enum PROPOSAL_ARBITER_STATUS {
|
||||
|
|
|
@ -2,6 +2,7 @@ from functools import reduce
|
|||
from flask import Blueprint, request
|
||||
from flask_yoloapi import endpoint, parameter
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from grant.comment.models import Comment, user_comments_schema
|
||||
from grant.email.send import generate_email, send_email
|
||||
from grant.extensions import db
|
||||
|
@ -19,7 +20,14 @@ from grant.user.models import User, admin_users_schema, admin_user_schema
|
|||
from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema
|
||||
from grant.utils.admin import admin_auth_required, admin_is_authed, admin_login, admin_logout
|
||||
from grant.utils.misc import make_url
|
||||
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus, ProposalArbiterStatus, MilestoneStage
|
||||
from grant.utils.enums import (
|
||||
ProposalStatus,
|
||||
ProposalStage,
|
||||
ContributionStatus,
|
||||
ProposalArbiterStatus,
|
||||
MilestoneStage,
|
||||
RFPStatus,
|
||||
)
|
||||
from grant.utils import pagination
|
||||
from grant.settings import EXPLORER_URL
|
||||
from sqlalchemy import func, or_
|
||||
|
@ -332,14 +340,15 @@ def get_rfps():
|
|||
parameter('brief', type=str),
|
||||
parameter('content', type=str),
|
||||
parameter('category', type=str),
|
||||
parameter('bounty', type=str),
|
||||
parameter('matching', type=bool, default=False),
|
||||
parameter('dateCloses', type=int),
|
||||
)
|
||||
@admin_auth_required
|
||||
def create_rfp(title, brief, content, category):
|
||||
def create_rfp(date_closes, **kwargs):
|
||||
rfp = RFP(
|
||||
title=title,
|
||||
brief=brief,
|
||||
content=content,
|
||||
category=category,
|
||||
**kwargs,
|
||||
date_closes=datetime.fromtimestamp(date_closes) if date_closes else None,
|
||||
)
|
||||
db.session.add(rfp)
|
||||
db.session.commit()
|
||||
|
@ -363,19 +372,33 @@ def get_rfp(rfp_id):
|
|||
parameter('brief', type=str),
|
||||
parameter('content', type=str),
|
||||
parameter('category', type=str),
|
||||
parameter('bounty', type=str),
|
||||
parameter('matching', type=bool, default=False),
|
||||
parameter('dateCloses', type=int),
|
||||
parameter('status', type=str),
|
||||
)
|
||||
@admin_auth_required
|
||||
def update_rfp(rfp_id, title, brief, content, category, status):
|
||||
def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
|
||||
rfp = RFP.query.filter(RFP.id == rfp_id).first()
|
||||
if not rfp:
|
||||
return {"message": "No RFP matching that id"}, 404
|
||||
|
||||
# Update fields
|
||||
rfp.title = title
|
||||
rfp.brief = brief
|
||||
rfp.content = content
|
||||
rfp.category = category
|
||||
rfp.status = status
|
||||
rfp.bounty = bounty
|
||||
rfp.matching = matching
|
||||
rfp.date_closes = datetime.fromtimestamp(date_closes) if date_closes else None
|
||||
|
||||
# Update timestamps if status changed
|
||||
if rfp.status != status:
|
||||
if status == RFPStatus.LIVE and not rfp.date_opened:
|
||||
rfp.date_opened = datetime.now()
|
||||
if status == RFPStatus.CLOSED:
|
||||
rfp.date_closed = datetime.now()
|
||||
rfp.status = status
|
||||
|
||||
db.session.add(rfp)
|
||||
db.session.commit()
|
||||
|
|
|
@ -164,9 +164,9 @@ def make_proposal_draft(rfp_id):
|
|||
rfp = RFP.query.filter_by(id=rfp_id).first()
|
||||
if not rfp:
|
||||
return {"message": "The request this proposal was made for doesn’t exist"}, 400
|
||||
proposal.title = rfp.title
|
||||
proposal.brief = rfp.brief
|
||||
proposal.category = rfp.category
|
||||
if rfp.matching:
|
||||
proposal.contribution_matching = 1.0
|
||||
rfp.proposals.append(proposal)
|
||||
db.session.add(rfp)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import datetime
|
||||
from datetime import datetime
|
||||
from grant.extensions import ma, db
|
||||
from grant.utils.enums import RFPStatus
|
||||
from grant.utils.misc import dt_to_unix
|
||||
|
@ -15,6 +15,11 @@ class RFP(db.Model):
|
|||
content = db.Column(db.Text, nullable=False)
|
||||
category = db.Column(db.String(255), nullable=False)
|
||||
status = db.Column(db.String(255), nullable=False)
|
||||
matching = db.Column(db.Boolean, default=False, nullable=False)
|
||||
bounty = db.Column(db.String(255), nullable=True)
|
||||
date_closes = db.Column(db.DateTime, nullable=True)
|
||||
date_opened = db.Column(db.DateTime, nullable=True)
|
||||
date_closed = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
proposals = db.relationship(
|
||||
|
@ -36,13 +41,19 @@ class RFP(db.Model):
|
|||
brief: str,
|
||||
content: str,
|
||||
category: str,
|
||||
bounty: str,
|
||||
date_closes: datetime,
|
||||
matching: bool = False,
|
||||
status: str = RFPStatus.DRAFT,
|
||||
):
|
||||
self.date_created = datetime.datetime.now()
|
||||
self.date_created = datetime.now()
|
||||
self.title = title
|
||||
self.brief = brief
|
||||
self.content = content
|
||||
self.category = category
|
||||
self.bounty = bounty
|
||||
self.date_closes = date_closes
|
||||
self.matching = matching
|
||||
self.status = status
|
||||
|
||||
|
||||
|
@ -57,15 +68,35 @@ class RFPSchema(ma.Schema):
|
|||
"content",
|
||||
"category",
|
||||
"status",
|
||||
"matching",
|
||||
"bounty",
|
||||
"date_created",
|
||||
"date_closes",
|
||||
"date_opened",
|
||||
"date_closed",
|
||||
"accepted_proposals",
|
||||
)
|
||||
|
||||
date_created = ma.Method("get_date_created")
|
||||
status = ma.Method("get_status")
|
||||
date_closes = ma.Method("get_date_closes")
|
||||
date_opened = ma.Method("get_date_opened")
|
||||
date_closed = ma.Method("get_date_closed")
|
||||
accepted_proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
||||
|
||||
def get_date_created(self, obj):
|
||||
return dt_to_unix(obj.date_created)
|
||||
def get_status(self, obj):
|
||||
# Force it into closed state if date_closes is in the past
|
||||
if obj.date_closes and obj.date_closes <= datetime.today():
|
||||
return RFPStatus.CLOSED
|
||||
return obj.status
|
||||
|
||||
def get_date_closes(self, obj):
|
||||
return dt_to_unix(obj.date_closes) if obj.date_closes else None
|
||||
|
||||
def get_date_opened(self, obj):
|
||||
return dt_to_unix(obj.date_opened) if obj.date_opened else None
|
||||
|
||||
def get_date_closed(self, obj):
|
||||
return dt_to_unix(obj.date_closed) if obj.date_closed else None
|
||||
|
||||
rfp_schema = RFPSchema()
|
||||
rfps_schema = RFPSchema(many=True)
|
||||
|
@ -82,15 +113,39 @@ class AdminRFPSchema(ma.Schema):
|
|||
"content",
|
||||
"category",
|
||||
"status",
|
||||
"matching",
|
||||
"bounty",
|
||||
"date_created",
|
||||
"date_closes",
|
||||
"date_opened",
|
||||
"date_closed",
|
||||
"proposals",
|
||||
)
|
||||
|
||||
status = ma.Method("get_status")
|
||||
date_created = ma.Method("get_date_created")
|
||||
date_closes = ma.Method("get_date_closes")
|
||||
date_opened = ma.Method("get_date_opened")
|
||||
date_closed = ma.Method("get_date_closed")
|
||||
proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
|
||||
|
||||
def get_status(self, obj):
|
||||
# Force it into closed state if date_closes is in the past
|
||||
if obj.date_closes and obj.date_closes <= datetime.today():
|
||||
return RFPStatus.CLOSED
|
||||
return obj.status
|
||||
|
||||
def get_date_created(self, obj):
|
||||
return dt_to_unix(obj.date_created)
|
||||
|
||||
def get_date_closes(self, obj):
|
||||
return dt_to_unix(obj.date_closes) if obj.date_closes else None
|
||||
|
||||
def get_date_opened(self, obj):
|
||||
return dt_to_unix(obj.date_opened) if obj.date_opened else None
|
||||
|
||||
def get_date_closed(self, obj):
|
||||
return dt_to_unix(obj.date_closes) if obj.date_closes else None
|
||||
|
||||
admin_rfp_schema = AdminRFPSchema()
|
||||
admin_rfps_schema = AdminRFPSchema(many=True)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
"""Adds RFP bounty, matching, and date fields
|
||||
|
||||
Revision ID: d39bb526eef4
|
||||
Revises: 3793d9a71e27
|
||||
Create Date: 2019-02-07 15:09:11.548655
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import expression
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd39bb526eef4'
|
||||
down_revision = '3793d9a71e27'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('rfp', sa.Column('bounty', sa.String(length=255), nullable=True))
|
||||
op.add_column('rfp', sa.Column('date_closed', sa.DateTime(), nullable=True))
|
||||
op.add_column('rfp', sa.Column('date_closes', sa.DateTime(), nullable=True))
|
||||
op.add_column('rfp', sa.Column('date_opened', sa.DateTime(), nullable=True))
|
||||
op.add_column('rfp', sa.Column('matching', sa.Boolean(), nullable=False, server_default=expression.false()))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# Set columns for times based on status.
|
||||
connection = op.get_bind()
|
||||
connection.execute("UPDATE rfp SET date_opened = now() - INTERVAL '1 DAY' WHERE status = 'LIVE' OR status = 'CLOSED'")
|
||||
connection.execute("UPDATE rfp SET date_closed = now() WHERE status = 'CLOSED'")
|
||||
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('rfp', 'matching')
|
||||
op.drop_column('rfp', 'date_opened')
|
||||
op.drop_column('rfp', 'date_closes')
|
||||
op.drop_column('rfp', 'date_closed')
|
||||
op.drop_column('rfp', 'bounty')
|
||||
op.create_table('rfp_proposal',
|
||||
sa.Column('rfp_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('proposal_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['proposal_id'], ['proposal.id'], name='rfp_proposal_proposal_id_fkey'),
|
||||
sa.ForeignKeyConstraint(['rfp_id'], ['rfp.id'], name='rfp_proposal_rfp_id_fkey'),
|
||||
sa.UniqueConstraint('proposal_id', name='rfp_proposal_proposal_id_key')
|
||||
)
|
||||
# ### end Alembic commands ###
|
|
@ -32,9 +32,23 @@
|
|||
|
||||
&-content {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&-rules {
|
||||
padding-bottom: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid rgba(#000, 0.08);
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1rem;
|
||||
|
||||
li {
|
||||
font-size: 0.9rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-proposals {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { AppState } from 'store/reducers';
|
|||
import Loader from 'components/Loader';
|
||||
import Markdown from 'components/Markdown';
|
||||
import ProposalCard from 'components/Proposals/ProposalCard';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import './index.less';
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -53,11 +54,34 @@ class RFPDetail extends React.Component<Props> {
|
|||
<Icon type="arrow-left" /> Back to Requests
|
||||
</Link>
|
||||
<div className="RFPDetail-top-date">
|
||||
Opened {moment(rfp.dateCreated * 1000).format('LL')}
|
||||
Opened {moment(rfp.dateOpened * 1000).format('LL')}
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="RFPDetail-title">{rfp.title}</h1>
|
||||
<Markdown className="RFPDetail-content" source={rfp.content} />
|
||||
<div className="RFPDetail-rules">
|
||||
<ul>
|
||||
{rfp.bounty && (
|
||||
<li>
|
||||
Accepted proposals will be funded up to{' '}
|
||||
<strong>
|
||||
<UnitDisplay value={rfp.bounty} symbol="ZEC" />
|
||||
</strong>
|
||||
</li>
|
||||
)}
|
||||
{rfp.matching && (
|
||||
<li>
|
||||
Contributions will have their <strong>funding matched</strong> by the
|
||||
Zcash Foundation
|
||||
</li>
|
||||
)}
|
||||
{rfp.dateCloses && (
|
||||
<li>
|
||||
Proposal submissions end {moment(rfp.dateCloses * 1000).format('LL')}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{!!rfp.acceptedProposals.length && (
|
||||
<div className="RFPDetail-proposals">
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
font-size: 1.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: @primary-color;
|
||||
|
||||
.ant-tag {
|
||||
vertical-align: top;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-brief {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import classnames from 'classnames';
|
||||
import { Tag } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import { RFP } from 'types';
|
||||
import './RFPItem.less';
|
||||
|
||||
|
@ -13,19 +15,52 @@ interface Props {
|
|||
export default class RFPItem extends React.Component<Props> {
|
||||
render() {
|
||||
const { rfp, isSmall } = this.props;
|
||||
const { id, title, brief, acceptedProposals, dateCreated } = rfp;
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
brief,
|
||||
acceptedProposals,
|
||||
dateOpened,
|
||||
dateCloses,
|
||||
dateClosed,
|
||||
bounty,
|
||||
matching,
|
||||
} = rfp;
|
||||
const closeDate = dateCloses || dateClosed;
|
||||
|
||||
const tags = [];
|
||||
if (!isSmall) {
|
||||
if (bounty) {
|
||||
tags.push(
|
||||
<Tag key="bounty" color="#CF8A00">
|
||||
<UnitDisplay value={bounty} symbol="ZEC" /> bounty
|
||||
</Tag>,
|
||||
);
|
||||
}
|
||||
if (matching) {
|
||||
tags.push(
|
||||
<Tag key="matching" color="#1890ff">
|
||||
x2 matching
|
||||
</Tag>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classnames('RFPItem', isSmall && 'is-small')}
|
||||
to={`/requests/${id}`}
|
||||
>
|
||||
<h3 className="RFPItem-title">{title}</h3>
|
||||
<h3 className="RFPItem-title">
|
||||
{title}
|
||||
{tags}
|
||||
</h3>
|
||||
<p className="RFPItem-brief">{brief}</p>
|
||||
|
||||
<div className="RFPItem-details">
|
||||
<div className="RFPItem-details-detail">
|
||||
{moment(dateCreated * 1000).format('LL')}
|
||||
{moment(dateOpened * 1000).format('LL')}
|
||||
{closeDate && <> – {moment(closeDate * 1000).format('LL')}</>}
|
||||
</div>
|
||||
<div className="RFPItem-details-detail">
|
||||
{acceptedProposals.length} proposals approved
|
||||
|
|
|
@ -101,6 +101,9 @@ export function formatProposalFromGet(p: any): Proposal {
|
|||
}
|
||||
|
||||
export function formatRFPFromGet(rfp: RFP): RFP {
|
||||
if (rfp.bounty) {
|
||||
rfp.bounty = toZat(rfp.bounty as any);
|
||||
}
|
||||
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
|
||||
return rfp;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { Proposal } from './proposal';
|
||||
import { PROPOSAL_CATEGORY, RFP_STATUS } from 'api/constants';
|
||||
import { Zat } from 'utils/units';
|
||||
|
||||
export interface RFP {
|
||||
id: number;
|
||||
dateCreated: number;
|
||||
title: string;
|
||||
brief: string;
|
||||
content: string;
|
||||
category: PROPOSAL_CATEGORY;
|
||||
status: RFP_STATUS;
|
||||
acceptedProposals: Proposal[];
|
||||
bounty: Zat | null;
|
||||
matching: boolean;
|
||||
dateOpened: number;
|
||||
dateClosed?: number;
|
||||
dateCloses?: number;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue