Merge branch 'develop' into server-side-drafts
This commit is contained in:
commit
a95f693467
|
@ -1 +1,2 @@
|
||||||
.idea
|
.idea
|
||||||
|
contract/build
|
12
.travis.yml
12
.travis.yml
|
@ -2,7 +2,7 @@ matrix:
|
||||||
include:
|
include:
|
||||||
# Frontend
|
# Frontend
|
||||||
- language: node_js
|
- language: node_js
|
||||||
node_js: 8.11.4
|
node_js: 8.13.0
|
||||||
before_install:
|
before_install:
|
||||||
- cd frontend/
|
- cd frontend/
|
||||||
install: yarn
|
install: yarn
|
||||||
|
@ -15,17 +15,23 @@ matrix:
|
||||||
before_install:
|
before_install:
|
||||||
- cd backend/
|
- cd backend/
|
||||||
- cp .env.example .env
|
- cp .env.example .env
|
||||||
|
env:
|
||||||
|
- FLASK_APP=app.py FLASK_DEBUG=1 CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund
|
||||||
|
CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory
|
||||||
install: pip install -r requirements/dev.txt
|
install: pip install -r requirements/dev.txt
|
||||||
script:
|
script:
|
||||||
- flask test
|
- flask test
|
||||||
# Contracts
|
# Contracts
|
||||||
- language: node_js
|
- language: node_js
|
||||||
node_js: 8.11.4
|
node_js: 8.13.0
|
||||||
before_install:
|
before_install:
|
||||||
- cd contract/
|
- cd contract/
|
||||||
install: yarn && yarn add global truffle ganache-cli
|
install: yarn && yarn add global truffle ganache-cli@6.1.8
|
||||||
before_script:
|
before_script:
|
||||||
- ganache-cli > /dev/null &
|
- ganache-cli > /dev/null &
|
||||||
- sleep 10
|
- sleep 10
|
||||||
script:
|
script:
|
||||||
- yarn run test
|
- yarn run test
|
||||||
|
env:
|
||||||
|
- CROWD_FUND_URL=https://eip-712.herokuapp.com/contract/crowd-fund
|
||||||
|
CROWD_FUND_FACTORY_URL=https://eip-712.herokuapp.com/contract/factory
|
||||||
|
|
|
@ -9,3 +9,8 @@ SENDGRID_API_KEY="optional, but emails won't send without it"
|
||||||
# for ropsten use the following
|
# for ropsten use the following
|
||||||
# ETHEREUM_ENDPOINT_URI = "https://ropsten.infura.io/API_KEY"
|
# ETHEREUM_ENDPOINT_URI = "https://ropsten.infura.io/API_KEY"
|
||||||
ETHEREUM_ENDPOINT_URI = "http://localhost:8545"
|
ETHEREUM_ENDPOINT_URI = "http://localhost:8545"
|
||||||
|
CROWD_FUND_URL = "https://eip-712.herokuapp.com/contract/crowd-fund"
|
||||||
|
CROWD_FUND_FACTORY_URL = "https://eip-712.herokuapp.com/contract/factory"
|
||||||
|
UPLOAD_DIRECTORY = "/tmp"
|
||||||
|
UPLOAD_URL = "http://localhost:5000" # for constructing download url
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Config file for automatic testing at travis-ci.org
|
|
||||||
sudo: false # http://docs.travis-ci.com/user/migrating-from-legacy/
|
|
||||||
language: python
|
|
||||||
env:
|
|
||||||
- FLASK_APP=app.py FLASK_DEBUG=1
|
|
||||||
python:
|
|
||||||
- 2.7
|
|
||||||
- 3.4
|
|
||||||
- 3.5
|
|
||||||
- 3.6
|
|
||||||
install:
|
|
||||||
- pip install -r requirements/dev.txt
|
|
||||||
- nvm install 6.10
|
|
||||||
- nvm use 6.10
|
|
||||||
- npm install
|
|
||||||
before_script:
|
|
||||||
- npm run lint
|
|
||||||
- npm run build
|
|
||||||
- flask lint
|
|
||||||
script: flask test
|
|
|
@ -11,6 +11,8 @@ from grant.user.models import User, SocialMedia, Avatar
|
||||||
from grant.utils.auth import requires_sm, requires_team_member_auth
|
from grant.utils.auth import requires_sm, requires_team_member_auth
|
||||||
from grant.utils.exceptions import ValidationException
|
from grant.utils.exceptions import ValidationException
|
||||||
from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, proposal_team, db
|
from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, proposal_team, db
|
||||||
|
from grant.web3.proposal import read_proposal
|
||||||
|
|
||||||
|
|
||||||
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
||||||
|
|
||||||
|
@ -21,6 +23,10 @@ def get_proposal(proposal_id):
|
||||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||||
if proposal:
|
if proposal:
|
||||||
dumped_proposal = proposal_schema.dump(proposal)
|
dumped_proposal = proposal_schema.dump(proposal)
|
||||||
|
proposal_contract = read_proposal(dumped_proposal['proposal_address'])
|
||||||
|
if not proposal_contract:
|
||||||
|
return {"message": "Proposal retired"}, 404
|
||||||
|
dumped_proposal['crowd_fund'] = proposal_contract
|
||||||
return dumped_proposal
|
return dumped_proposal
|
||||||
else:
|
else:
|
||||||
return {"message": "No proposal matching id"}, 404
|
return {"message": "No proposal matching id"}, 404
|
||||||
|
@ -76,7 +82,11 @@ def get_proposals(stage):
|
||||||
else:
|
else:
|
||||||
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
|
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
|
||||||
dumped_proposals = proposals_schema.dump(proposals)
|
dumped_proposals = proposals_schema.dump(proposals)
|
||||||
return dumped_proposals
|
for p in dumped_proposals:
|
||||||
|
proposal_contract = read_proposal(p['proposal_address'])
|
||||||
|
p['crowd_fund'] = proposal_contract
|
||||||
|
filtered_proposals = list(filter(lambda p: p['crowd_fund'] is not None, dumped_proposals))
|
||||||
|
return filtered_proposals
|
||||||
|
|
||||||
@blueprint.route("/drafts", methods=["POST"])
|
@blueprint.route("/drafts", methods=["POST"])
|
||||||
@requires_sm
|
@requires_sm
|
||||||
|
|
|
@ -15,6 +15,8 @@ ENV = env.str("FLASK_ENV", default="production")
|
||||||
DEBUG = ENV == "development"
|
DEBUG = ENV == "development"
|
||||||
SITE_URL = env.str('SITE_URL', default='https://grant.io')
|
SITE_URL = env.str('SITE_URL', default='https://grant.io')
|
||||||
AUTH_URL = env.str('AUTH_URL', default='https://eip-712.herokuapp.com')
|
AUTH_URL = env.str('AUTH_URL', default='https://eip-712.herokuapp.com')
|
||||||
|
CROWD_FUND_FACTORY_URL = env.str('CROWD_FUND_FACTORY_URL', default=None)
|
||||||
|
CROWD_FUND_URL = env.str('CROWD_FUND_URL', default=None)
|
||||||
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
|
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
|
||||||
QUEUES = ["default"]
|
QUEUES = ["default"]
|
||||||
SECRET_KEY = env.str("SECRET_KEY")
|
SECRET_KEY = env.str("SECRET_KEY")
|
||||||
|
@ -27,3 +29,6 @@ SENDGRID_API_KEY = env.str("SENDGRID_API_KEY", default="")
|
||||||
SENDGRID_DEFAULT_FROM = "noreply@grant.io"
|
SENDGRID_DEFAULT_FROM = "noreply@grant.io"
|
||||||
ETHEREUM_PROVIDER = "http"
|
ETHEREUM_PROVIDER = "http"
|
||||||
ETHEREUM_ENDPOINT_URI = env.str("ETHEREUM_ENDPOINT_URI")
|
ETHEREUM_ENDPOINT_URI = env.str("ETHEREUM_ENDPOINT_URI")
|
||||||
|
UPLOAD_DIRECTORY = env.str("UPLOAD_DIRECTORY")
|
||||||
|
UPLOAD_URL = env.str("UPLOAD_URL")
|
||||||
|
MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5MB (limits file uploads, raises RequestEntityTooLarge)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from flask import Blueprint, g
|
from flask import Blueprint, g, request
|
||||||
from flask_yoloapi import endpoint, parameter
|
from flask_yoloapi import endpoint, parameter
|
||||||
|
|
||||||
from grant.proposal.models import Proposal, proposal_team
|
from grant.proposal.models import Proposal, proposal_team
|
||||||
from grant.utils.auth import requires_sm, requires_same_user_auth, verify_signed_auth, BadSignatureException
|
from grant.utils.auth import requires_sm, requires_same_user_auth, verify_signed_auth, BadSignatureException
|
||||||
|
from grant.utils.upload import save_avatar, send_upload, remove_avatar
|
||||||
|
from grant.settings import UPLOAD_URL
|
||||||
from .models import User, SocialMedia, Avatar, users_schema, user_schema, db
|
from .models import User, SocialMedia, Avatar, users_schema, user_schema, db
|
||||||
|
|
||||||
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
|
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
|
||||||
|
@ -119,6 +121,38 @@ def auth_user(account_address, signed_message, raw_typed_data):
|
||||||
return user_schema.dump(existing_user)
|
return user_schema.dump(existing_user)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/avatar", methods=["POST"])
|
||||||
|
@requires_sm
|
||||||
|
@endpoint.api()
|
||||||
|
def upload_avatar():
|
||||||
|
user = g.current_user
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return {"message": "No file in post"}, 400
|
||||||
|
file = request.files['file']
|
||||||
|
if file.filename == '':
|
||||||
|
return {"message": "No selected file"}, 400
|
||||||
|
try:
|
||||||
|
filename = save_avatar(file, user.id)
|
||||||
|
return {"url": "{0}/api/v1/users/avatar/{1}".format(UPLOAD_URL, filename)}
|
||||||
|
except Exception as e:
|
||||||
|
return {"message": str(e)}, 400
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/avatar/<filename>", methods=["GET"])
|
||||||
|
def get_avatar(filename):
|
||||||
|
return send_upload(filename)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/avatar", methods=["DELETE"])
|
||||||
|
@requires_sm
|
||||||
|
@endpoint.api(
|
||||||
|
parameter('url', type=str, required=True)
|
||||||
|
)
|
||||||
|
def delete_avatar(url):
|
||||||
|
user = g.current_user
|
||||||
|
remove_avatar(url, user.id)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/<user_identity>", methods=["PUT"])
|
@blueprint.route("/<user_identity>", methods=["PUT"])
|
||||||
@requires_sm
|
@requires_sm
|
||||||
@requires_same_user_auth
|
@requires_same_user_auth
|
||||||
|
@ -148,6 +182,7 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
|
||||||
else:
|
else:
|
||||||
SocialMedia.query.filter_by(user_id=user.id).delete()
|
SocialMedia.query.filter_by(user_id=user.id).delete()
|
||||||
|
|
||||||
|
old_avatar = Avatar.query.filter_by(user_id=user.id).first()
|
||||||
if avatar is not None:
|
if avatar is not None:
|
||||||
Avatar.query.filter_by(user_id=user.id).delete()
|
Avatar.query.filter_by(user_id=user.id).delete()
|
||||||
avatar_link = avatar.get('link')
|
avatar_link = avatar.get('link')
|
||||||
|
@ -157,6 +192,11 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
|
||||||
else:
|
else:
|
||||||
Avatar.query.filter_by(user_id=user.id).delete()
|
Avatar.query.filter_by(user_id=user.id).delete()
|
||||||
|
|
||||||
|
old_avatar_url = old_avatar and old_avatar.image_url
|
||||||
|
new_avatar_url = avatar and avatar['link']
|
||||||
|
if old_avatar_url and old_avatar_url != new_avatar_url:
|
||||||
|
remove_avatar(old_avatar_url, user.id)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
result = user_schema.dump(user)
|
result = user_schema.dump(user)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from hashlib import md5
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from flask import send_from_directory
|
||||||
|
from grant.settings import UPLOAD_DIRECTORY
|
||||||
|
|
||||||
|
IMAGE_MIME_TYPES = set(['image/png', 'image/jpg', 'image/gif'])
|
||||||
|
AVATAR_MAX_SIZE = 2 * 1024 * 1024 # 2MB
|
||||||
|
|
||||||
|
|
||||||
|
class FileValidationException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_avatar_file(file):
|
||||||
|
if file.mimetype not in IMAGE_MIME_TYPES:
|
||||||
|
raise FileValidationException("Unacceptable file type: {0}".format(file.mimetype))
|
||||||
|
file.seek(0, os.SEEK_END)
|
||||||
|
size = file.tell()
|
||||||
|
file.seek(0)
|
||||||
|
if size > AVATAR_MAX_SIZE:
|
||||||
|
raise FileValidationException(
|
||||||
|
"File size is too large ({0}KB), max size is {1}KB".format(size / 1024, AVATAR_MAX_SIZE / 1024)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def hash_file(file):
|
||||||
|
hasher = md5()
|
||||||
|
buf = file.read()
|
||||||
|
hasher.update(buf)
|
||||||
|
file.seek(0)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def save_avatar(file, user_id):
|
||||||
|
if file and allowed_avatar_file(file):
|
||||||
|
ext = file.mimetype.replace('image/', '')
|
||||||
|
filename = "{0}.{1}.{2}".format(user_id, hash_file(file), ext)
|
||||||
|
file.save(os.path.join(UPLOAD_DIRECTORY, filename))
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def remove_avatar(url, user_id):
|
||||||
|
match = re.search(r'/api/v1/users/avatar/(\d+.\w+.\w+)', url)
|
||||||
|
if match:
|
||||||
|
filename = match.group(1)
|
||||||
|
if filename.startswith(str(user_id) + '.'):
|
||||||
|
os.remove(os.path.join(UPLOAD_DIRECTORY, filename))
|
||||||
|
|
||||||
|
|
||||||
|
def send_upload(filename):
|
||||||
|
return send_from_directory(UPLOAD_DIRECTORY, secure_filename(filename))
|
|
@ -1,7 +1,9 @@
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from flask_web3 import current_web3
|
from flask_web3 import current_web3
|
||||||
from .util import batch_call, call_array
|
from .util import batch_call, call_array, RpcError
|
||||||
|
import requests
|
||||||
|
from grant.settings import CROWD_FUND_URL
|
||||||
|
|
||||||
|
|
||||||
crowd_fund_abi = None
|
crowd_fund_abi = None
|
||||||
|
@ -11,12 +13,20 @@ def get_crowd_fund_abi():
|
||||||
global crowd_fund_abi
|
global crowd_fund_abi
|
||||||
if crowd_fund_abi:
|
if crowd_fund_abi:
|
||||||
return crowd_fund_abi
|
return crowd_fund_abi
|
||||||
|
|
||||||
|
if CROWD_FUND_URL:
|
||||||
|
crowd_fund_json = requests.get(CROWD_FUND_URL).json()
|
||||||
|
crowd_fund_abi = crowd_fund_json['abi']
|
||||||
|
return crowd_fund_abi
|
||||||
|
|
||||||
with open("../contract/build/contracts/CrowdFund.json", "r") as read_file:
|
with open("../contract/build/contracts/CrowdFund.json", "r") as read_file:
|
||||||
crowd_fund_abi = json.load(read_file)['abi']
|
crowd_fund_abi = json.load(read_file)['abi']
|
||||||
return crowd_fund_abi
|
return crowd_fund_abi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_proposal(address):
|
def read_proposal(address):
|
||||||
|
current_web3.eth.defaultAccount = current_web3.eth.accounts[0]
|
||||||
crowd_fund_abi = get_crowd_fund_abi()
|
crowd_fund_abi = get_crowd_fund_abi()
|
||||||
contract = current_web3.eth.contract(address=address, abi=crowd_fund_abi)
|
contract = current_web3.eth.contract(address=address, abi=crowd_fund_abi)
|
||||||
|
|
||||||
|
@ -34,7 +44,11 @@ def read_proposal(address):
|
||||||
|
|
||||||
# batched
|
# batched
|
||||||
calls = list(map(lambda x: [x, None], methods))
|
calls = list(map(lambda x: [x, None], methods))
|
||||||
|
try:
|
||||||
crowd_fund = batch_call(current_web3, address, crowd_fund_abi, calls, contract)
|
crowd_fund = batch_call(current_web3, address, crowd_fund_abi, calls, contract)
|
||||||
|
# catch dead contracts here
|
||||||
|
except RpcError:
|
||||||
|
return None
|
||||||
|
|
||||||
# balance (sync)
|
# balance (sync)
|
||||||
crowd_fund['balance'] = current_web3.eth.getBalance(address)
|
crowd_fund['balance'] = current_web3.eth.getBalance(address)
|
||||||
|
|
|
@ -9,6 +9,10 @@ from web3.utils.abi import get_abi_output_types, map_abi_data
|
||||||
from web3.utils.normalizers import BASE_RETURN_NORMALIZERS
|
from web3.utils.normalizers import BASE_RETURN_NORMALIZERS
|
||||||
|
|
||||||
|
|
||||||
|
class RpcError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def call_array(fn):
|
def call_array(fn):
|
||||||
results = []
|
results = []
|
||||||
no_error = True
|
no_error = True
|
||||||
|
@ -51,6 +55,8 @@ def batch_call(w3, address, abi, calls, contract):
|
||||||
# this implements batched rpc calls using web3py helper methods
|
# this implements batched rpc calls using web3py helper methods
|
||||||
# web3py doesn't support this out-of-box yet
|
# web3py doesn't support this out-of-box yet
|
||||||
# issue: https://github.com/ethereum/web3.py/issues/832
|
# issue: https://github.com/ethereum/web3.py/issues/832
|
||||||
|
if not calls:
|
||||||
|
return []
|
||||||
if type(w3.providers[0]) is EthereumTesterProvider:
|
if type(w3.providers[0]) is EthereumTesterProvider:
|
||||||
return tester_batch(calls, contract)
|
return tester_batch(calls, contract)
|
||||||
inputs = []
|
inputs = []
|
||||||
|
@ -60,6 +66,9 @@ def batch_call(w3, address, abi, calls, contract):
|
||||||
prepared = prepare_transaction(address, w3, name, abi, None, tx, args)
|
prepared = prepare_transaction(address, w3, name, abi, None, tx, args)
|
||||||
inputs.append([prepared, 'latest'])
|
inputs.append([prepared, 'latest'])
|
||||||
responses = batch(ETHEREUM_ENDPOINT_URI, inputs)
|
responses = batch(ETHEREUM_ENDPOINT_URI, inputs)
|
||||||
|
if 'error' in responses[0]:
|
||||||
|
message = responses[0]['error']['message'] if 'message' in responses[0]['error'] else 'No error message found.'
|
||||||
|
raise RpcError("rpc error: {0}".format(message))
|
||||||
results = {}
|
results = {}
|
||||||
for r in zip(calls, responses):
|
for r in zip(calls, responses):
|
||||||
result = HexBytes(r[1]['result'])
|
result = HexBytes(r[1]['result'])
|
||||||
|
|
|
@ -60,4 +60,3 @@ flask-yolo2API==0.2.6
|
||||||
#web3
|
#web3
|
||||||
flask-web3==0.1.1
|
flask-web3==0.1.1
|
||||||
web3==4.8.1
|
web3==4.8.1
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import copy
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from grant.extensions import web3
|
|
||||||
from ..config import BaseTestConfig
|
|
||||||
from grant.web3.proposal import read_proposal
|
|
||||||
from flask_web3 import current_web3
|
|
||||||
import eth_tester.backends.pyevm.main as py_evm_main
|
import eth_tester.backends.pyevm.main as py_evm_main
|
||||||
|
from flask_web3 import current_web3
|
||||||
|
from grant.extensions import web3
|
||||||
|
from grant.settings import CROWD_FUND_URL, CROWD_FUND_FACTORY_URL
|
||||||
|
from grant.web3.proposal import read_proposal
|
||||||
|
|
||||||
|
from ..config import BaseTestConfig
|
||||||
|
import requests
|
||||||
# increase gas limit on eth-tester
|
# increase gas limit on eth-tester
|
||||||
# https://github.com/ethereum/web3.py/issues/1013
|
# https://github.com/ethereum/web3.py/issues/1013
|
||||||
# https://gitter.im/ethereum/py-evm?at=5b7eb68c4be56c5918854337
|
# https://gitter.im/ethereum/py-evm?at=5b7eb68c4be56c5918854337
|
||||||
|
@ -23,9 +24,16 @@ class TestWeb3ProposalRead(BaseTestConfig):
|
||||||
BaseTestConfig.setUp(self)
|
BaseTestConfig.setUp(self)
|
||||||
# the following will properly configure web3 with test config
|
# the following will properly configure web3 with test config
|
||||||
web3.init_app(self.real_app)
|
web3.init_app(self.real_app)
|
||||||
with open("../contract/build/contracts/CrowdFundFactory.json", "r") as read_file:
|
if CROWD_FUND_FACTORY_URL:
|
||||||
|
crowd_fund_factory_json = requests.get(CROWD_FUND_FACTORY_URL).json()
|
||||||
|
else:
|
||||||
|
with open("../frontend/client/lib/contracts/CrowdFundFactory.json", "r") as read_file:
|
||||||
crowd_fund_factory_json = json.load(read_file)
|
crowd_fund_factory_json = json.load(read_file)
|
||||||
with open("../contract/build/contracts/CrowdFund.json", "r") as read_file:
|
|
||||||
|
if CROWD_FUND_URL:
|
||||||
|
self.crowd_fund_json = requests.get(CROWD_FUND_URL).json()
|
||||||
|
else:
|
||||||
|
with open("../frontend/client/lib/contracts/CrowdFund.json", "r") as read_file:
|
||||||
self.crowd_fund_json = json.load(read_file)
|
self.crowd_fund_json = json.load(read_file)
|
||||||
current_web3.eth.defaultAccount = current_web3.eth.accounts[0]
|
current_web3.eth.defaultAccount = current_web3.eth.accounts[0]
|
||||||
CrowdFundFactory = current_web3.eth.contract(
|
CrowdFundFactory = current_web3.eth.contract(
|
||||||
|
@ -58,9 +66,7 @@ class TestWeb3ProposalRead(BaseTestConfig):
|
||||||
"isImmediatePayout": True
|
"isImmediatePayout": True
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"trustees": [
|
"trustees": [current_web3.eth.accounts[0]],
|
||||||
current_web3.eth.accounts[0]
|
|
||||||
],
|
|
||||||
"contributors": [],
|
"contributors": [],
|
||||||
"target": "5000000000000000000",
|
"target": "5000000000000000000",
|
||||||
"isFrozen": False,
|
"isFrozen": False,
|
||||||
|
@ -74,9 +80,7 @@ class TestWeb3ProposalRead(BaseTestConfig):
|
||||||
"contributionAmount": str(c[1] * 1000000000000000000),
|
"contributionAmount": str(c[1] * 1000000000000000000),
|
||||||
"refundVote": False,
|
"refundVote": False,
|
||||||
"refunded": False,
|
"refunded": False,
|
||||||
"milestoneNoVotes": [
|
"milestoneNoVotes": [False]
|
||||||
False
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
return mock_proposal_read
|
return mock_proposal_read
|
||||||
|
|
||||||
|
@ -111,7 +115,7 @@ class TestWeb3ProposalRead(BaseTestConfig):
|
||||||
deadline = proposal_read.pop('deadline')
|
deadline = proposal_read.pop('deadline')
|
||||||
deadline_diff = deadline - time.time() * 1000
|
deadline_diff = deadline - time.time() * 1000
|
||||||
self.assertGreater(60000, deadline_diff)
|
self.assertGreater(60000, deadline_diff)
|
||||||
self.assertGreater(deadline_diff, 58000)
|
self.assertGreater(deadline_diff, 50000)
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertEqual(proposal_read, self.get_mock_proposal_read())
|
self.assertEqual(proposal_read, self.get_mock_proposal_read())
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,4 @@ node_modules
|
||||||
.idea/
|
.idea/
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.env
|
.env
|
||||||
build/abi
|
build
|
||||||
build/typedefs
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -6,3 +6,6 @@ NO_DEV_TS_CHECK=true
|
||||||
|
|
||||||
# Set the public host url (no trailing slash)
|
# Set the public host url (no trailing slash)
|
||||||
PUBLIC_HOST_URL=https://demo.grant.io
|
PUBLIC_HOST_URL=https://demo.grant.io
|
||||||
|
|
||||||
|
CROWD_FUND_URL = "https://eip-712.herokuapp.com/contract/crowd-fund"
|
||||||
|
CROWD_FUND_FACTORY_URL = "https://eip-712.herokuapp.com/contract/factory"
|
|
@ -1 +1 @@
|
||||||
8.11.4
|
8.13.0
|
|
@ -0,0 +1 @@
|
||||||
|
web: yarn start
|
|
@ -74,7 +74,7 @@ const routeConfigs: RouteConfig[] = [
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
title: 'Browse proposals',
|
title: 'Browse proposals',
|
||||||
requiresWeb3: true,
|
requiresWeb3: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -99,7 +99,7 @@ const routeConfigs: RouteConfig[] = [
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
title: 'Proposal',
|
title: 'Proposal',
|
||||||
requiresWeb3: true,
|
requiresWeb3: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
import { Proposal, ProposalDraft, TeamMember, Update } from 'types';
|
import { Proposal, ProposalDraft, TeamMember, Update } from 'types';
|
||||||
import {
|
import {
|
||||||
|
formatProposalFromGet,
|
||||||
formatTeamMemberForPost,
|
formatTeamMemberForPost,
|
||||||
formatTeamMemberFromGet,
|
formatTeamMemberFromGet,
|
||||||
generateProposalUrl,
|
|
||||||
} from 'utils/api';
|
} from 'utils/api';
|
||||||
|
|
||||||
export function getProposals(): Promise<{ data: Proposal[] }> {
|
export function getProposals(): Promise<{ data: Proposal[] }> {
|
||||||
return axios.get('/api/v1/proposals/').then(res => {
|
return axios.get('/api/v1/proposals/').then(res => {
|
||||||
res.data = res.data.map((proposal: any) => {
|
res.data = res.data.map(formatProposalFromGet);
|
||||||
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
|
||||||
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
|
||||||
return proposal;
|
|
||||||
});
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProposal(proposalId: number | string): Promise<{ data: Proposal }> {
|
export function getProposal(proposalId: number | string): Promise<{ data: Proposal }> {
|
||||||
return axios.get(`/api/v1/proposals/${proposalId}`).then(res => {
|
return axios.get(`/api/v1/proposals/${proposalId}`).then(res => {
|
||||||
res.data.team = res.data.team.map(formatTeamMemberFromGet);
|
res.data = formatProposalFromGet(res.data);
|
||||||
res.data.proposalUrlId = generateProposalUrl(res.data.proposalId, res.data.title);
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -86,6 +81,16 @@ export function verifyEmail(code: string): Promise<any> {
|
||||||
return axios.post(`/api/v1/email/${code}/verify`);
|
return axios.post(`/api/v1/email/${code}/verify`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCrowdFundFactoryJSON(): Promise<any> {
|
||||||
|
const res = await axios.get(process.env.CROWD_FUND_FACTORY_URL as string);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCrowdFundJSON(): Promise<any> {
|
||||||
|
const res = await axios.get(process.env.CROWD_FUND_URL as string);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
export function postProposalUpdate(
|
export function postProposalUpdate(
|
||||||
proposalId: number,
|
proposalId: number,
|
||||||
title: string,
|
title: string,
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.MetaMaskRequiredButton {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
height: 3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #fff;
|
||||||
|
background: #f88500;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-logo {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
padding: 0.4rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
margin: 0 1rem 0 0;
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
display: block;
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { web3Actions } from 'modules/web3';
|
||||||
|
import { Alert } from 'antd';
|
||||||
|
import metaMaskImgSrc from 'static/images/metamask.png';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
message: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
isMissingWeb3: boolean;
|
||||||
|
isWeb3Locked: boolean;
|
||||||
|
isWrongNetwork: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
setAccounts: typeof web3Actions['setAccounts'];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = OwnProps & StateProps & DispatchProps;
|
||||||
|
|
||||||
|
class MetaMaskRequiredButton extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { isMissingWeb3, isWeb3Locked, isWrongNetwork, children, message } = this.props;
|
||||||
|
const displayMessage =
|
||||||
|
((isMissingWeb3 || isWeb3Locked || isWrongNetwork) && message) || null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{displayMessage}
|
||||||
|
{isMissingWeb3 ? (
|
||||||
|
<a
|
||||||
|
className="MetaMaskRequiredButton"
|
||||||
|
href="https://metamask.io/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener nofollow"
|
||||||
|
>
|
||||||
|
<div className="MetaMaskRequiredButton-logo">
|
||||||
|
<img src={metaMaskImgSrc} />
|
||||||
|
</div>
|
||||||
|
MetaMask required
|
||||||
|
</a>
|
||||||
|
) : isWeb3Locked ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message={
|
||||||
|
<>
|
||||||
|
It looks like your MetaMask account is locked. Please unlock it and{' '}
|
||||||
|
<a onClick={this.props.setAccounts}>click here to continue</a>.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : isWrongNetwork ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message={
|
||||||
|
<>
|
||||||
|
The Grant.io smart contract is currently only supported on the{' '}
|
||||||
|
<strong>Ropsten</strong> network. Please change your network to continue.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
|
state => ({
|
||||||
|
isMissingWeb3: state.web3.isMissingWeb3,
|
||||||
|
isWeb3Locked: state.web3.isWeb3Locked,
|
||||||
|
isWrongNetwork: state.web3.isWrongNetwork,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
setAccounts: web3Actions.setAccounts,
|
||||||
|
},
|
||||||
|
)(MetaMaskRequiredButton);
|
|
@ -0,0 +1,74 @@
|
||||||
|
@small-query: ~'(max-width: 500px)';
|
||||||
|
|
||||||
|
.AvatarEdit {
|
||||||
|
&-avatar {
|
||||||
|
position: relative;
|
||||||
|
height: 10.5rem;
|
||||||
|
width: 10.5rem;
|
||||||
|
margin-right: 1.25rem;
|
||||||
|
align-self: start;
|
||||||
|
|
||||||
|
@media @small-query {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-change {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.2rem;
|
||||||
|
right: 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffffff;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:hover:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
import React from 'react';
|
||||||
|
import axios from 'api/axios';
|
||||||
|
import { Upload, Icon, Modal, Button, Alert } from 'antd';
|
||||||
|
import Cropper from 'react-cropper';
|
||||||
|
import 'cropperjs/dist/cropper.css';
|
||||||
|
import { UploadFile } from 'antd/lib/upload/interface';
|
||||||
|
import { TeamMember } from 'types';
|
||||||
|
import { getBase64 } from 'utils/blob';
|
||||||
|
import UserAvatar from 'components/UserAvatar';
|
||||||
|
import './AvatarEdit.less';
|
||||||
|
|
||||||
|
const FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
|
||||||
|
const FILE_MAX_LOAD_MB = 10;
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
user: TeamMember;
|
||||||
|
onDelete(): void;
|
||||||
|
onDone(url: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isUploading: false,
|
||||||
|
showModal: false,
|
||||||
|
newAvatarUrl: '',
|
||||||
|
loadError: '',
|
||||||
|
uploadError: '',
|
||||||
|
};
|
||||||
|
type State = typeof initialState;
|
||||||
|
|
||||||
|
type Props = OwnProps;
|
||||||
|
|
||||||
|
export default class AvatarEdit extends React.PureComponent<Props, State> {
|
||||||
|
state = initialState;
|
||||||
|
cropperRef: React.RefObject<any>;
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.cropperRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { newAvatarUrl, showModal, loadError, uploadError, isUploading } = this.state;
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
user: { avatarUrl },
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<div className="AvatarEdit-avatar">
|
||||||
|
<UserAvatar className="AvatarEdit-avatar-img" user={user} />
|
||||||
|
<Upload
|
||||||
|
name="avatar"
|
||||||
|
showUploadList={false}
|
||||||
|
action={this.handleLoad}
|
||||||
|
beforeUpload={this.beforeLoad}
|
||||||
|
onChange={this.handleLoadChange}
|
||||||
|
>
|
||||||
|
<Button className="AvatarEdit-avatar-change">
|
||||||
|
<Icon
|
||||||
|
className="AvatarEdit-avatar-change-icon"
|
||||||
|
type={avatarUrl ? 'picture' : 'plus-circle'}
|
||||||
|
/>
|
||||||
|
<div>{avatarUrl ? 'Change photo' : 'Add photo'}</div>
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
{avatarUrl && (
|
||||||
|
<Button
|
||||||
|
className="AvatarEdit-avatar-delete"
|
||||||
|
icon="delete"
|
||||||
|
shape="circle"
|
||||||
|
onClick={this.props.onDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{loadError && (
|
||||||
|
<Alert message={loadError} type="error" style={{ margin: '0.5rem 0 0 0' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
title="Prepare your avatar"
|
||||||
|
visible={showModal}
|
||||||
|
footer={[
|
||||||
|
<Button key="back" onClick={this.handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={isUploading}
|
||||||
|
onClick={this.handleUpload}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Cropper
|
||||||
|
ref={this.cropperRef}
|
||||||
|
src={newAvatarUrl}
|
||||||
|
style={{ height: 300 }}
|
||||||
|
aspectRatio={1}
|
||||||
|
guides={false}
|
||||||
|
viewMode={1}
|
||||||
|
/>
|
||||||
|
{uploadError && (
|
||||||
|
<Alert
|
||||||
|
message={uploadError}
|
||||||
|
type="error"
|
||||||
|
style={{ margin: '0.5rem 0 0 0' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClose = () => {
|
||||||
|
this.setState({
|
||||||
|
isUploading: false,
|
||||||
|
showModal: false,
|
||||||
|
newAvatarUrl: '',
|
||||||
|
uploadError: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleLoadChange = (info: any) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
getBase64(info.file.originFileObj, newAvatarUrl =>
|
||||||
|
this.setState({
|
||||||
|
newAvatarUrl,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private beforeLoad = (file: UploadFile) => {
|
||||||
|
this.setState({ loadError: '' });
|
||||||
|
const isTypeOk = !!FILE_TYPES.find(t => t === file.type);
|
||||||
|
if (!isTypeOk) {
|
||||||
|
this.setState({ loadError: 'File must be a jpg, png or gif' });
|
||||||
|
}
|
||||||
|
const isSizeOk = file.size / 1024 / 1024 < FILE_MAX_LOAD_MB;
|
||||||
|
if (!isSizeOk) {
|
||||||
|
this.setState({
|
||||||
|
loadError: `File size must be less than ${FILE_MAX_LOAD_MB}MB`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return isTypeOk && isSizeOk;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleLoad = () => {
|
||||||
|
this.setState({ showModal: true });
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleUpload = () => {
|
||||||
|
this.cropperRef.current
|
||||||
|
.getCroppedCanvas({ width: 400, height: 400 })
|
||||||
|
.toBlob((blob: Blob) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', blob);
|
||||||
|
this.setState({ isUploading: true });
|
||||||
|
axios
|
||||||
|
.post('/api/v1/users/avatar', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.props.onDone(res.data.url);
|
||||||
|
this.handleClose();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.setState({ isUploading: false, uploadError: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -7,12 +7,12 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
z-index: 1000;
|
z-index: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProfileEdit {
|
.ProfileEdit {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1001;
|
z-index: 901;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
@ -28,77 +28,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-avatar {
|
|
||||||
position: relative;
|
|
||||||
height: 10.5rem;
|
|
||||||
width: 10.5rem;
|
|
||||||
margin-right: 1.25rem;
|
|
||||||
align-self: start;
|
|
||||||
|
|
||||||
@media @small-query {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-change {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:hover:focus {
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-delete {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.2rem;
|
|
||||||
right: 0.2rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #ffffff;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:hover:focus {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { Input, Form, Col, Row, Button, Icon, Alert } from 'antd';
|
import axios from 'api/axios';
|
||||||
|
import { Input, Form, Col, Row, Button, Alert } from 'antd';
|
||||||
import { SOCIAL_INFO } from 'utils/social';
|
import { SOCIAL_INFO } from 'utils/social';
|
||||||
import { SOCIAL_TYPE, TeamMember } from 'types';
|
import { SOCIAL_TYPE, TeamMember } from 'types';
|
||||||
import { UserState } from 'modules/users/reducers';
|
import { UserState } from 'modules/users/reducers';
|
||||||
import { getCreateTeamMemberError } from 'modules/create/utils';
|
import { getCreateTeamMemberError } from 'modules/create/utils';
|
||||||
import UserAvatar from 'components/UserAvatar';
|
import AvatarEdit from './AvatarEdit';
|
||||||
import './ProfileEdit.less';
|
import './ProfileEdit.less';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -54,27 +55,12 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="ProfileEdit">
|
<div className="ProfileEdit">
|
||||||
<div className="ProfileEdit-avatar">
|
<AvatarEdit
|
||||||
<UserAvatar className="ProfileEdit-avatar-img" user={fields} />
|
user={fields}
|
||||||
<Button
|
onDone={this.handleChangePhoto}
|
||||||
className="ProfileEdit-avatar-change"
|
onDelete={this.handleDeletePhoto}
|
||||||
onClick={this.handleChangePhoto}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className="ProfileEdit-avatar-change-icon"
|
|
||||||
type={fields.avatarUrl ? 'picture' : 'plus-circle'}
|
|
||||||
/>
|
/>
|
||||||
<div>{fields.avatarUrl ? 'Change photo' : 'Add photo'}</div>
|
|
||||||
</Button>
|
|
||||||
{fields.avatarUrl && (
|
|
||||||
<Button
|
|
||||||
className="ProfileEdit-avatar-delete"
|
|
||||||
icon="delete"
|
|
||||||
shape="circle"
|
|
||||||
onClick={this.handleDeletePhoto}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="ProfileEdit-info">
|
<div className="ProfileEdit-info">
|
||||||
<Form
|
<Form
|
||||||
className="ProfileEdit-info-form"
|
className="ProfileEdit-info-form"
|
||||||
|
@ -187,6 +173,13 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleCancel = () => {
|
private handleCancel = () => {
|
||||||
|
const { avatarUrl } = this.state.fields;
|
||||||
|
// cleanup uploaded file if we cancel
|
||||||
|
if (this.props.user.avatarUrl !== avatarUrl && avatarUrl) {
|
||||||
|
axios.delete('/api/v1/users/avatar', {
|
||||||
|
params: { url: avatarUrl },
|
||||||
|
});
|
||||||
|
}
|
||||||
this.props.onDone();
|
this.props.onDone();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -226,13 +219,10 @@ export default class ProfileEdit extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleChangePhoto = () => {
|
private handleChangePhoto = (url: string) => {
|
||||||
// TODO: Actual file uploading
|
|
||||||
const gender = ['men', 'women'][Math.floor(Math.random() * 2)];
|
|
||||||
const num = Math.floor(Math.random() * 80);
|
|
||||||
const fields = {
|
const fields = {
|
||||||
...this.state.fields,
|
...this.state.fields,
|
||||||
avatarUrl: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
|
avatarUrl: url,
|
||||||
};
|
};
|
||||||
const isChanged = this.isChangedCheck(fields);
|
const isChanged = this.isChangedCheck(fields);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -4,17 +4,17 @@ import { Spin, Form, Input, Button, Icon } from 'antd';
|
||||||
import { ProposalWithCrowdFund } from 'types';
|
import { ProposalWithCrowdFund } from 'types';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { fromWei } from 'utils/units';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { compose } from 'recompose';
|
import { compose } from 'recompose';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { web3Actions } from 'modules/web3';
|
import { web3Actions } from 'modules/web3';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
|
||||||
import ShortAddress from 'components/ShortAddress';
|
import ShortAddress from 'components/ShortAddress';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
import { getAmountError } from 'utils/validators';
|
import { getAmountError } from 'utils/validators';
|
||||||
import { CATEGORY_UI } from 'api/constants';
|
import { CATEGORY_UI } from 'api/constants';
|
||||||
|
import MetaMaskRequiredButton from 'components/MetaMaskRequiredButton';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
proposal: ProposalWithCrowdFund;
|
proposal: ProposalWithCrowdFund;
|
||||||
|
@ -29,11 +29,7 @@ interface ActionProps {
|
||||||
fundCrowdFund: typeof web3Actions['fundCrowdFund'];
|
fundCrowdFund: typeof web3Actions['fundCrowdFund'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Web3Props {
|
type Props = OwnProps & StateProps & ActionProps;
|
||||||
web3: Web3RenderProps['web3'];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = OwnProps & StateProps & ActionProps & Web3Props;
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
amountToRaise: string;
|
amountToRaise: string;
|
||||||
|
@ -58,7 +54,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { proposal, web3 } = this.props;
|
const { proposal } = this.props;
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
const remainingTarget = crowdFund.target.sub(crowdFund.funded);
|
const remainingTarget = crowdFund.target.sub(crowdFund.funded);
|
||||||
const amount = parseFloat(value);
|
const amount = parseFloat(value);
|
||||||
|
@ -67,7 +63,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
if (Number.isNaN(amount)) {
|
if (Number.isNaN(amount)) {
|
||||||
// They're entering some garbage, they’ll work it out
|
// They're entering some garbage, they’ll work it out
|
||||||
} else {
|
} else {
|
||||||
const remainingEthNum = parseFloat(web3.utils.fromWei(remainingTarget, 'ether'));
|
const remainingEthNum = parseFloat(fromWei(remainingTarget, 'ether'));
|
||||||
amountError = getAmountError(amount, remainingEthNum);
|
amountError = getAmountError(amount, remainingEthNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +78,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { proposal, sendLoading, web3, isPreview } = this.props;
|
const { proposal, sendLoading, isPreview } = this.props;
|
||||||
const { amountToRaise, amountError } = this.state;
|
const { amountToRaise, amountError } = this.state;
|
||||||
const amountFloat = parseFloat(amountToRaise) || 0;
|
const amountFloat = parseFloat(amountToRaise) || 0;
|
||||||
let content;
|
let content;
|
||||||
|
@ -94,7 +90,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
crowdFund.isFrozen;
|
crowdFund.isFrozen;
|
||||||
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
|
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
|
||||||
const remainingEthNum = parseFloat(
|
const remainingEthNum = parseFloat(
|
||||||
web3.utils.fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
|
||||||
);
|
);
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
|
@ -166,7 +162,21 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
|
<MetaMaskRequiredButton
|
||||||
|
message={
|
||||||
|
<Form.Item style={{ marginBottom: '0.5rem', paddingBottom: 0 }}>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
type="number"
|
||||||
|
placeholder="0.5"
|
||||||
|
addonAfter="ETH"
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
validateStatus={amountError ? 'error' : undefined}
|
validateStatus={amountError ? 'error' : undefined}
|
||||||
help={amountError}
|
help={amountError}
|
||||||
|
@ -186,7 +196,6 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
disabled={isPreview}
|
disabled={isPreview}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={this.sendTransaction}
|
onClick={this.sendTransaction}
|
||||||
size="large"
|
size="large"
|
||||||
|
@ -197,6 +206,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
>
|
>
|
||||||
Fund this project
|
Fund this project
|
||||||
</Button>
|
</Button>
|
||||||
|
</MetaMaskRequiredButton>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -226,21 +236,9 @@ const withConnect = connect(
|
||||||
{ fundCrowdFund: web3Actions.fundCrowdFund },
|
{ fundCrowdFund: web3Actions.fundCrowdFund },
|
||||||
);
|
);
|
||||||
|
|
||||||
const ConnectedProposalCampaignBlock = compose<Props, OwnProps & Web3Props>(
|
const ConnectedProposalCampaignBlock = compose<Props, OwnProps>(
|
||||||
withRouter,
|
withRouter,
|
||||||
withConnect,
|
withConnect,
|
||||||
)(ProposalCampaignBlock);
|
)(ProposalCampaignBlock);
|
||||||
|
|
||||||
export default (props: OwnProps) => (
|
export default ConnectedProposalCampaignBlock;
|
||||||
<Web3Container
|
|
||||||
renderLoading={() => (
|
|
||||||
<div className="ProposalCampaignBlock Proposal-top-side-block">
|
|
||||||
<h1 className="Proposal-top-main-block-title">Campaign</h1>
|
|
||||||
<div className="Proposal-top-main-block">
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
render={({ web3 }) => <ConnectedProposalCampaignBlock {...props} web3={web3} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
|
@ -18,12 +18,11 @@ import ContributorsTab from './Contributors';
|
||||||
// import CommunityTab from './Community';
|
// import CommunityTab from './Community';
|
||||||
import UpdateModal from './UpdateModal';
|
import UpdateModal from './UpdateModal';
|
||||||
import CancelModal from './CancelModal';
|
import CancelModal from './CancelModal';
|
||||||
import './style.less';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import Web3Container from 'lib/Web3Container';
|
|
||||||
import { web3Actions } from 'modules/web3';
|
import { web3Actions } from 'modules/web3';
|
||||||
import SocialShare from 'components/SocialShare';
|
import SocialShare from 'components/SocialShare';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
proposalId: number;
|
proposalId: number;
|
||||||
|
@ -32,17 +31,14 @@ interface OwnProps {
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
proposal: ProposalWithCrowdFund | null;
|
proposal: ProposalWithCrowdFund | null;
|
||||||
|
account: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
fetchProposal: proposalActions.TFetchProposal;
|
fetchProposal: proposalActions.TFetchProposal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Web3Props {
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
account: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & Web3Props & OwnProps;
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
isBodyExpanded: boolean;
|
isBodyExpanded: boolean;
|
||||||
|
@ -95,7 +91,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
return <Spin />;
|
return <Spin />;
|
||||||
} else {
|
} else {
|
||||||
const { crowdFund } = proposal;
|
const { crowdFund } = proposal;
|
||||||
const isTrustee = crowdFund.trustees.includes(account);
|
const isTrustee = !!account && crowdFund.trustees.includes(account);
|
||||||
const isContributor = !!crowdFund.contributors.find(c => c.address === account);
|
const isContributor = !!crowdFund.contributors.find(c => c.address === account);
|
||||||
const hasBeenFunded = crowdFund.isRaiseGoalReached;
|
const hasBeenFunded = crowdFund.isRaiseGoalReached;
|
||||||
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
|
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
|
||||||
|
@ -255,6 +251,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
||||||
function mapStateToProps(state: AppState, ownProps: OwnProps) {
|
function mapStateToProps(state: AppState, ownProps: OwnProps) {
|
||||||
return {
|
return {
|
||||||
proposal: getProposal(state, ownProps.proposalId),
|
proposal: getProposal(state, ownProps.proposalId),
|
||||||
|
account: (state.web3.accounts.length && state.web3.accounts[0]) || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,22 +264,9 @@ const withConnect = connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
mapDispatchToProps,
|
mapDispatchToProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ConnectedProposal = compose<Props, OwnProps & Web3Props>(
|
const ConnectedProposal = compose<Props, OwnProps>(
|
||||||
withRouter,
|
withRouter,
|
||||||
withConnect,
|
withConnect,
|
||||||
)(ProposalDetail);
|
)(ProposalDetail);
|
||||||
|
|
||||||
export default (props: OwnProps) => (
|
export default ConnectedProposal;
|
||||||
<Web3Container
|
|
||||||
renderLoading={() => (
|
|
||||||
<div className="Proposal">
|
|
||||||
<div className="Proposal-top">
|
|
||||||
<div className="Proposal-top-main">
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
render={({ accounts }) => <ConnectedProposal account={accounts[0]} {...props} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Progress, Icon, Spin } from 'antd';
|
import { Progress, Icon } from 'antd';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { CATEGORY_UI } from 'api/constants';
|
import { CATEGORY_UI } from 'api/constants';
|
||||||
import { ProposalWithCrowdFund } from 'types';
|
import { ProposalWithCrowdFund } from 'types';
|
||||||
import './style.less';
|
|
||||||
import { Dispatch, bindActionCreators } from 'redux';
|
|
||||||
import * as web3Actions from 'modules/web3/actions';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import UserAvatar from 'components/UserAvatar';
|
import UserAvatar from 'components/UserAvatar';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
interface Props extends ProposalWithCrowdFund {
|
export class ProposalCard extends React.Component<ProposalWithCrowdFund> {
|
||||||
web3: AppState['web3']['web3'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProposalCard extends React.Component<Props> {
|
|
||||||
state = { redirect: '' };
|
state = { redirect: '' };
|
||||||
render() {
|
render() {
|
||||||
if (this.state.redirect) {
|
if (this.state.redirect) {
|
||||||
|
@ -29,14 +21,10 @@ export class ProposalCard extends React.Component<Props> {
|
||||||
proposalUrlId,
|
proposalUrlId,
|
||||||
category,
|
category,
|
||||||
dateCreated,
|
dateCreated,
|
||||||
web3,
|
|
||||||
crowdFund,
|
crowdFund,
|
||||||
team,
|
team,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!web3) {
|
|
||||||
return <Spin />;
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="ProposalCard"
|
className="ProposalCard"
|
||||||
|
@ -45,8 +33,8 @@ export class ProposalCard extends React.Component<Props> {
|
||||||
<h3 className="ProposalCard-title">{title}</h3>
|
<h3 className="ProposalCard-title">{title}</h3>
|
||||||
<div className="ProposalCard-funding">
|
<div className="ProposalCard-funding">
|
||||||
<div className="ProposalCard-funding-raised">
|
<div className="ProposalCard-funding-raised">
|
||||||
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small>{' '}
|
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small> of{' '}
|
||||||
of <UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
|
<UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames({
|
className={classnames({
|
||||||
|
@ -94,19 +82,5 @@ export class ProposalCard extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
export default ProposalCard;
|
||||||
return bindActionCreators(web3Actions, dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
|
||||||
return {
|
|
||||||
web3: state.web3.web3,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(ProposalCard);
|
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { getProposals } from 'modules/proposals/selectors';
|
||||||
import { ProposalWithCrowdFund } from 'types';
|
import { ProposalWithCrowdFund } from 'types';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Input, Divider, Spin, Drawer, Icon, Button } from 'antd';
|
import { Input, Divider, Drawer, Icon, Button } from 'antd';
|
||||||
import ProposalResults from './Results';
|
import ProposalResults from './Results';
|
||||||
import ProposalFilters, { Filters } from './Filters';
|
import ProposalFilters, { Filters } from './Filters';
|
||||||
import { PROPOSAL_SORT } from 'api/constants';
|
import { PROPOSAL_SORT } from 'api/constants';
|
||||||
import Web3Container from 'lib/Web3Container';
|
|
||||||
import './style.less';
|
import './style.less';
|
||||||
|
|
||||||
type ProposalSortFn = (p1: ProposalWithCrowdFund, p2: ProposalWithCrowdFund) => number;
|
type ProposalSortFn = (p1: ProposalWithCrowdFund, p2: ProposalWithCrowdFund) => number;
|
||||||
|
@ -246,6 +245,4 @@ const ConnectedProposals = connect(
|
||||||
mapDispatchToProps,
|
mapDispatchToProps,
|
||||||
)(Proposals);
|
)(Proposals);
|
||||||
|
|
||||||
export default () => (
|
export default ConnectedProposals;
|
||||||
<Web3Container renderLoading={() => <Spin />} render={() => <ConnectedProposals />} />
|
|
||||||
);
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Web3 from 'web3';
|
||||||
|
import getContractInstance from './getContract';
|
||||||
|
import { fetchCrowdFundJSON } from 'api/api';
|
||||||
|
|
||||||
|
const contractCache = {} as { [key: string]: any };
|
||||||
|
|
||||||
|
export async function getCrowdFundContract(web3: Web3 | null, deployedAddress: string) {
|
||||||
|
if (!web3) {
|
||||||
|
throw new Error('getCrowdFundAddress: web3 was null but is required!');
|
||||||
|
}
|
||||||
|
if (!contractCache[deployedAddress]) {
|
||||||
|
let CrowdFund;
|
||||||
|
if (process.env.CROWD_FUND_FACTORY_URL) {
|
||||||
|
CrowdFund = await fetchCrowdFundJSON();
|
||||||
|
} else {
|
||||||
|
CrowdFund = await import('./contracts/CrowdFund.json');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
contractCache[deployedAddress] = await getContractInstance(
|
||||||
|
web3,
|
||||||
|
CrowdFund,
|
||||||
|
deployedAddress,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Could not lookup crowdFund contract @ ${deployedAddress}: `, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contractCache[deployedAddress];
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ const resolveWeb3 = (resolve: (web3: Web3) => void, reject: (err: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { web3 } = window as Web3Window;
|
let { web3 } = window as Web3Window;
|
||||||
const localProvider = `http://localhost:8545`;
|
|
||||||
|
|
||||||
// To test what it's like to not have web3, uncomment the reject. Otherwise
|
// To test what it's like to not have web3, uncomment the reject. Otherwise
|
||||||
// localProvider will always kick in.
|
// localProvider will always kick in.
|
||||||
|
@ -19,10 +18,6 @@ const resolveWeb3 = (resolve: (web3: Web3) => void, reject: (err: Error) => void
|
||||||
if (typeof web3 !== 'undefined') {
|
if (typeof web3 !== 'undefined') {
|
||||||
console.info(`Injected web3 detected.`);
|
console.info(`Injected web3 detected.`);
|
||||||
web3 = new Web3(web3.currentProvider);
|
web3 = new Web3(web3.currentProvider);
|
||||||
} else if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.info(`No web3 instance injected, using Local web3.`);
|
|
||||||
const provider = new Web3.providers.HttpProvider(localProvider);
|
|
||||||
web3 = new Web3(provider);
|
|
||||||
} else {
|
} else {
|
||||||
return reject(new Error('No web3 instance available'));
|
return reject(new Error('No web3 instance available'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import types from './types';
|
import types from './types';
|
||||||
|
import usersTypes from 'modules/users/types';
|
||||||
// TODO: Use a common User type instead of this
|
// TODO: Use a common User type instead of this
|
||||||
import { TeamMember, AuthSignatureData } from 'types';
|
import { TeamMember, AuthSignatureData } from 'types';
|
||||||
|
|
||||||
|
@ -56,6 +57,14 @@ export default function createReducer(
|
||||||
authSignatureAddress: action.payload.user.ethAddress,
|
authSignatureAddress: action.payload.user.ethAddress,
|
||||||
isAuthingUser: false,
|
isAuthingUser: false,
|
||||||
};
|
};
|
||||||
|
case usersTypes.UPDATE_USER_FULFILLED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user:
|
||||||
|
state.user && state.user.ethAddress === action.payload.user.ethAddress
|
||||||
|
? action.payload.user
|
||||||
|
: state.user,
|
||||||
|
};
|
||||||
case types.AUTH_USER_REJECTED:
|
case types.AUTH_USER_REJECTED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -255,6 +255,5 @@ export function makeProposalPreviewFromDraft(
|
||||||
isFrozen: false,
|
isFrozen: false,
|
||||||
isRaiseGoalReached: false,
|
isRaiseGoalReached: false,
|
||||||
},
|
},
|
||||||
crowdFundContract: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,64 +6,16 @@ import {
|
||||||
getProposalUpdates,
|
getProposalUpdates,
|
||||||
} from 'api/api';
|
} from 'api/api';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import Web3 from 'web3';
|
import { ProposalWithCrowdFund, Comment } from 'types';
|
||||||
import { ProposalWithCrowdFund, Proposal, Comment } from 'types';
|
|
||||||
import { signData } from 'modules/web3/actions';
|
import { signData } from 'modules/web3/actions';
|
||||||
import getContract from 'lib/getContract';
|
|
||||||
import CrowdFund from 'lib/contracts/CrowdFund.json';
|
|
||||||
import { getCrowdFundState } from 'web3interact/crowdFund';
|
|
||||||
|
|
||||||
async function getMergedCrowdFundProposal(
|
|
||||||
proposal: Proposal,
|
|
||||||
web3: Web3,
|
|
||||||
account: string,
|
|
||||||
) {
|
|
||||||
const crowdFundContract = await getContract(web3, CrowdFund, proposal.proposalAddress);
|
|
||||||
const crowdFundData = {
|
|
||||||
crowdFundContract,
|
|
||||||
crowdFund: await getCrowdFundState(crowdFundContract, account, web3),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < crowdFundData.crowdFund.milestones.length; i++) {
|
|
||||||
proposal.milestones[i] = {
|
|
||||||
...proposal.milestones[i],
|
|
||||||
...crowdFundData.crowdFund.milestones[i],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...crowdFundData,
|
|
||||||
...proposal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// valid as defined by crowdFund contract existing on current network
|
|
||||||
export async function getValidProposals(
|
|
||||||
proposals: { data: Proposal[] },
|
|
||||||
web3: Web3,
|
|
||||||
account: string,
|
|
||||||
) {
|
|
||||||
return (await Promise.all(
|
|
||||||
proposals.data.map(async (proposal: Proposal) => {
|
|
||||||
try {
|
|
||||||
return await getMergedCrowdFundProposal(proposal, web3, account);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Could not lookup crowdFund contract', e);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// remove proposals that except since they cannot be retrieved via getContract
|
|
||||||
)).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TFetchProposals = typeof fetchProposals;
|
export type TFetchProposals = typeof fetchProposals;
|
||||||
export function fetchProposals() {
|
export function fetchProposals() {
|
||||||
return (dispatch: Dispatch<any>, getState: any) => {
|
return (dispatch: Dispatch<any>) => {
|
||||||
const state = getState();
|
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: types.PROPOSALS_DATA,
|
type: types.PROPOSALS_DATA,
|
||||||
payload: async () => {
|
payload: async () => {
|
||||||
const proposals = await getProposals();
|
return (await getProposals()).data;
|
||||||
return getValidProposals(proposals, state.web3.web3, state.web3.accounts[0]);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -71,17 +23,11 @@ export function fetchProposals() {
|
||||||
|
|
||||||
export type TFetchProposal = typeof fetchProposal;
|
export type TFetchProposal = typeof fetchProposal;
|
||||||
export function fetchProposal(proposalId: ProposalWithCrowdFund['proposalId']) {
|
export function fetchProposal(proposalId: ProposalWithCrowdFund['proposalId']) {
|
||||||
return (dispatch: Dispatch<any>, getState: any) => {
|
return (dispatch: Dispatch<any>) => {
|
||||||
const state = getState();
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: types.PROPOSAL_DATA,
|
type: types.PROPOSAL_DATA,
|
||||||
payload: async () => {
|
payload: async () => {
|
||||||
const proposal = await getProposal(proposalId);
|
return (await getProposal(proposalId)).data;
|
||||||
return await getMergedCrowdFundProposal(
|
|
||||||
proposal.data,
|
|
||||||
state.web3.web3,
|
|
||||||
state.web3.accounts[0],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { proposalToContractData } from 'modules/create/utils';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Wei } from 'utils/units';
|
import { Wei } from 'utils/units';
|
||||||
import { AuthSignatureData, ProposalDraft, ProposalWithCrowdFund } from 'types';
|
import { AuthSignatureData, ProposalDraft, ProposalWithCrowdFund } from 'types';
|
||||||
|
import { getCrowdFundContract } from 'lib/crowdFundContracts';
|
||||||
|
import { TeamMember, AuthSignatureData, ProposalDraft, ProposalWithCrowdFund } from 'types';
|
||||||
|
|
||||||
type GetState = () => AppState;
|
type GetState = () => AppState;
|
||||||
|
|
||||||
|
@ -170,7 +172,12 @@ export function requestMilestonePayout(proposal: ProposalWithCrowdFund, index: n
|
||||||
});
|
});
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await crowdFundContract.methods
|
await crowdFundContract.methods
|
||||||
.requestMilestonePayout(index)
|
.requestMilestonePayout(index)
|
||||||
|
@ -200,7 +207,11 @@ export function payMilestonePayout(proposal: ProposalWithCrowdFund, index: numbe
|
||||||
});
|
});
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await crowdFundContract.methods
|
await crowdFundContract.methods
|
||||||
|
@ -234,7 +245,8 @@ export function fundCrowdFund(proposal: ProposalWithCrowdFund, value: number | s
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const web3 = state.web3.web3;
|
const web3 = state.web3.web3;
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(web3, proposalAddress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!web3) {
|
if (!web3) {
|
||||||
|
@ -270,7 +282,11 @@ export function voteMilestonePayout(
|
||||||
dispatch({ type: types.VOTE_AGAINST_MILESTONE_PAYOUT_PENDING });
|
dispatch({ type: types.VOTE_AGAINST_MILESTONE_PAYOUT_PENDING });
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await crowdFundContract.methods
|
await crowdFundContract.methods
|
||||||
|
@ -297,7 +313,11 @@ export function voteRefund(proposal: ProposalWithCrowdFund, vote: boolean) {
|
||||||
dispatch({ type: types.VOTE_REFUND_PENDING });
|
dispatch({ type: types.VOTE_REFUND_PENDING });
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await crowdFundContract.methods
|
await crowdFundContract.methods
|
||||||
|
@ -346,7 +366,11 @@ export function triggerRefund(proposal: ProposalWithCrowdFund) {
|
||||||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await freezeContract(crowdFundContract, account);
|
await freezeContract(crowdFundContract, account);
|
||||||
|
@ -367,7 +391,11 @@ export function withdrawRefund(proposal: ProposalWithCrowdFund, address: string)
|
||||||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.web3.accounts[0];
|
const account = state.web3.accounts[0];
|
||||||
const { crowdFundContract, proposalId } = proposal;
|
const { proposalAddress, proposalId } = proposal;
|
||||||
|
const crowdFundContract = await getCrowdFundContract(
|
||||||
|
state.web3.web3,
|
||||||
|
proposalAddress,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await freezeContract(crowdFundContract, account);
|
await freezeContract(crowdFundContract, account);
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { put, all, fork, take, takeLatest, select, call } from 'redux-saga/effects';
|
import { all, call, fork, put, select, take, takeLatest } from 'redux-saga/effects';
|
||||||
import { setWeb3, setAccounts, setContract } from './actions';
|
import { setAccounts, setContract, setWeb3 } from './actions';
|
||||||
import { selectWeb3 } from './selectors';
|
import { selectWeb3 } from './selectors';
|
||||||
import { safeEnable } from 'utils/web3';
|
import { safeEnable } from 'utils/web3';
|
||||||
import types from './types';
|
import types from './types';
|
||||||
|
import { fetchCrowdFundFactoryJSON } from 'api/api';
|
||||||
|
|
||||||
/* tslint:disable no-var-requires --- TODO: find a better way to import contract */
|
/* tslint:disable no-var-requires --- TODO: find a better way to import contract */
|
||||||
const CrowdFundFactory = require('lib/contracts/CrowdFundFactory.json');
|
let CrowdFundFactory = require('lib/contracts/CrowdFundFactory.json');
|
||||||
|
|
||||||
export function* bootstrapWeb3(): SagaIterator {
|
export function* bootstrapWeb3(): SagaIterator {
|
||||||
// Don't attempt to bootstrap web3 on SSR
|
// Don't attempt to bootstrap web3 on SSR
|
||||||
if (process.env.SERVER_SIDE_RENDER) {
|
if (process.env.SERVER_SIDE_RENDER) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (process.env.CROWD_FUND_FACTORY_URL) {
|
||||||
|
CrowdFundFactory = yield call(fetchCrowdFundFactoryJSON);
|
||||||
|
}
|
||||||
|
|
||||||
yield put<any>(setWeb3());
|
yield put<any>(setWeb3());
|
||||||
yield take(types.WEB3_FULFILLED);
|
yield take(types.WEB3_FULFILLED);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { TeamMember } from 'types';
|
import BN from 'bn.js';
|
||||||
|
import { TeamMember, CrowdFund, ProposalWithCrowdFund } from 'types';
|
||||||
import { socialAccountsToUrls, socialUrlsToAccounts } from 'utils/social';
|
import { socialAccountsToUrls, socialUrlsToAccounts } from 'utils/social';
|
||||||
|
|
||||||
export function formatTeamMemberForPost(user: TeamMember) {
|
export function formatTeamMemberForPost(user: TeamMember) {
|
||||||
|
@ -27,6 +28,38 @@ export function formatTeamMemberFromGet(user: any): TeamMember {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCrowdFundFromGet(crowdFund: CrowdFund): CrowdFund {
|
||||||
|
const bnKeys = ['amountVotingForRefund', 'balance', 'funded', 'target'] as Array<
|
||||||
|
keyof CrowdFund
|
||||||
|
>;
|
||||||
|
bnKeys.forEach(k => {
|
||||||
|
crowdFund[k] = new BN(crowdFund[k] as string);
|
||||||
|
});
|
||||||
|
crowdFund.milestones = crowdFund.milestones.map(ms => {
|
||||||
|
ms.amount = new BN(ms.amount);
|
||||||
|
ms.amountAgainstPayout = new BN(ms.amountAgainstPayout);
|
||||||
|
return ms;
|
||||||
|
});
|
||||||
|
crowdFund.contributors = crowdFund.contributors.map(c => {
|
||||||
|
c.contributionAmount = new BN(c.contributionAmount);
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
return crowdFund;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatProposalFromGet(proposal: ProposalWithCrowdFund) {
|
||||||
|
for (let i = 0; i < proposal.crowdFund.milestones.length; i++) {
|
||||||
|
proposal.milestones[i] = {
|
||||||
|
...proposal.milestones[i],
|
||||||
|
...proposal.crowdFund.milestones[i],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
||||||
|
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
||||||
|
proposal.crowdFund = formatCrowdFundFromGet(proposal.crowdFund);
|
||||||
|
return proposal;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: i18n on case-by-case basis
|
// TODO: i18n on case-by-case basis
|
||||||
export function generateProposalUrl(id: number, title: string) {
|
export function generateProposalUrl(id: number, title: string) {
|
||||||
const slug = title
|
const slug = title
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
export function getBase64(img: Blob, callback: (base64: string) => void) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => callback(reader.result as string));
|
||||||
|
reader.readAsDataURL(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataUrlToBlob(dataUrl: string) {
|
||||||
|
const base64ImageContent = dataUrl.replace(/^data:[a-z]+\/[a-z]+;base64,/, '');
|
||||||
|
const mimeSearch = dataUrl.match(/^data:([a-z]+\/[a-z]+)(;base64,)/);
|
||||||
|
if (!mimeSearch || mimeSearch.length !== 3) {
|
||||||
|
throw new Error(
|
||||||
|
'dataUrlToBlob could not find mime type, or base64 was missing: ' +
|
||||||
|
dataUrl.substring(0, 200),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return base64ToBlob(base64ImageContent, mimeSearch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToBlob(base64: string, mime: string) {
|
||||||
|
mime = mime || '';
|
||||||
|
const sliceSize = 1024;
|
||||||
|
const byteChars = window.atob(base64);
|
||||||
|
const byteArrays = [];
|
||||||
|
for (let offset = 0, len = byteChars.length; offset < len; offset += sliceSize) {
|
||||||
|
const slice = byteChars.slice(offset, offset + sliceSize);
|
||||||
|
const byteNumbers = new Array(slice.length);
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
byteArrays.push(byteArray);
|
||||||
|
}
|
||||||
|
return new Blob(byteArrays, { type: mime });
|
||||||
|
}
|
|
@ -26,14 +26,29 @@ dotenvFiles.forEach(dotenvFile => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!process.env.PUBLIC_HOST_URL) {
|
const envProductionRequiredHandler = (envVariable, fallbackValue) => {
|
||||||
|
if (!process.env[envVariable]) {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The process.env.PUBLIC_HOST_URL environment variable is required but was not specified.',
|
`The process.env.${envVariable} environment variable is required but was not specified.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
process.env.PUBLIC_HOST_URL = 'http://localhost:' + (process.env.PORT || 3000);
|
process.env[envVariable] = fallbackValue;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
envProductionRequiredHandler(
|
||||||
|
'PUBLIC_HOST_URL',
|
||||||
|
'http://localhost:' + (process.env.PORT || 3000),
|
||||||
|
);
|
||||||
|
envProductionRequiredHandler(
|
||||||
|
'CROWD_FUND_URL',
|
||||||
|
'https://eip-712.herokuapp.com/contract/crowd-fund',
|
||||||
|
);
|
||||||
|
envProductionRequiredHandler(
|
||||||
|
'CROWD_FUND_FACTORY_URL',
|
||||||
|
'https://eip-712.herokuapp.com/contract/factory',
|
||||||
|
);
|
||||||
|
|
||||||
const appDirectory = fs.realpathSync(process.cwd());
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "grant",
|
"name": "grant",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -10,12 +10,16 @@
|
||||||
"lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\"",
|
"lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\"",
|
||||||
"start": "NODE_ENV=production node ./build/server/server.js",
|
"start": "NODE_ENV=production node ./build/server/server.js",
|
||||||
"now": "npm run build && now -e BACKEND_URL=https://grant-stage.herokuapp.com",
|
"now": "npm run build && now -e BACKEND_URL=https://grant-stage.herokuapp.com",
|
||||||
|
"heroku-postbuild": "yarn build",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"link-contracts": "cd client/lib && ln -s ../../build/contracts contracts",
|
"link-contracts": "cd client/lib && ln -s ../../build/contracts contracts",
|
||||||
"ganache": "ganache-cli -b 5",
|
"ganache": "ganache-cli -b 5",
|
||||||
"truffle": "truffle exec ./bin/init-truffle.js && cd client/lib/contracts && truffle console",
|
"truffle": "truffle exec ./bin/init-truffle.js && cd client/lib/contracts && truffle console",
|
||||||
"storybook": "start-storybook -p 9001 -c .storybook"
|
"storybook": "start-storybook -p 9001 -c .storybook"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "8.13.0"
|
||||||
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
|
@ -55,6 +59,7 @@
|
||||||
"@types/node": "^10.3.1",
|
"@types/node": "^10.3.1",
|
||||||
"@types/numeral": "^0.0.25",
|
"@types/numeral": "^0.0.25",
|
||||||
"@types/react": "16.4.18",
|
"@types/react": "16.4.18",
|
||||||
|
"@types/react-cropper": "^0.10.3",
|
||||||
"@types/react-dom": "16.0.9",
|
"@types/react-dom": "16.0.9",
|
||||||
"@types/react-helmet": "^5.0.7",
|
"@types/react-helmet": "^5.0.7",
|
||||||
"@types/react-redux": "^6.0.2",
|
"@types/react-redux": "^6.0.2",
|
||||||
|
@ -120,6 +125,7 @@
|
||||||
"prettier-package-json": "^1.6.0",
|
"prettier-package-json": "^1.6.0",
|
||||||
"query-string": "6.1.0",
|
"query-string": "6.1.0",
|
||||||
"react": "16.5.2",
|
"react": "16.5.2",
|
||||||
|
"react-cropper": "^1.0.1",
|
||||||
"react-dev-utils": "^5.0.2",
|
"react-dev-utils": "^5.0.2",
|
||||||
"react-dom": "16.5.2",
|
"react-dom": "16.5.2",
|
||||||
"react-helmet": "^5.2.0",
|
"react-helmet": "^5.2.0",
|
||||||
|
|
|
@ -212,13 +212,13 @@ export function getProposalWithCrowdFund({
|
||||||
amountVotingForRefund: new BN(0),
|
amountVotingForRefund: new BN(0),
|
||||||
percentVotingForRefund: 0,
|
percentVotingForRefund: 0,
|
||||||
},
|
},
|
||||||
crowdFundContract: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
sendLoading: false,
|
sendLoading: false,
|
||||||
fundCrowdFund,
|
fundCrowdFund,
|
||||||
web3: new Web3(),
|
web3: new Web3(),
|
||||||
|
isMissingWeb3: false,
|
||||||
proposal,
|
proposal,
|
||||||
...proposal, // yeah...
|
...proposal, // yeah...
|
||||||
};
|
};
|
||||||
|
|
|
@ -68,7 +68,6 @@ export interface Proposal {
|
||||||
|
|
||||||
export interface ProposalWithCrowdFund extends Proposal {
|
export interface ProposalWithCrowdFund extends Proposal {
|
||||||
crowdFund: CrowdFund;
|
crowdFund: CrowdFund;
|
||||||
crowdFundContract: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProposalComments {
|
export interface ProposalComments {
|
||||||
|
|
|
@ -1858,6 +1858,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/express" "*"
|
"@types/express" "*"
|
||||||
|
|
||||||
|
"@types/cropperjs@*":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cropperjs/-/cropperjs-1.1.3.tgz#8b2264fb45e933c3eda149cbd08a4f1926dfd8e2"
|
||||||
|
|
||||||
"@types/dotenv@^4.0.3":
|
"@types/dotenv@^4.0.3":
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67"
|
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67"
|
||||||
|
@ -1967,6 +1971,13 @@
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d"
|
||||||
|
|
||||||
|
"@types/react-cropper@^0.10.3":
|
||||||
|
version "0.10.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-cropper/-/react-cropper-0.10.3.tgz#d7ca18667d9cdad9469d3de6469104924d8217d5"
|
||||||
|
dependencies:
|
||||||
|
"@types/cropperjs" "*"
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-dom@16.0.9":
|
"@types/react-dom@16.0.9":
|
||||||
version "16.0.9"
|
version "16.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.9.tgz#73ceb7abe6703822eab6600e65c5c52efd07fb91"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.9.tgz#73ceb7abe6703822eab6600e65c5c52efd07fb91"
|
||||||
|
@ -4772,6 +4783,10 @@ create-react-context@0.2.3:
|
||||||
fbjs "^0.8.0"
|
fbjs "^0.8.0"
|
||||||
gud "^1.0.0"
|
gud "^1.0.0"
|
||||||
|
|
||||||
|
cropperjs@v1.0.0-rc.3:
|
||||||
|
version "1.0.0-rc.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.0.0-rc.3.tgz#50a7c7611befc442702f845ede77d7df4572e82b"
|
||||||
|
|
||||||
cross-env@^5.2.0:
|
cross-env@^5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
|
||||||
|
@ -12519,6 +12534,13 @@ react-copy-to-clipboard@^5.0.1:
|
||||||
copy-to-clipboard "^3"
|
copy-to-clipboard "^3"
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
|
react-cropper@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-cropper/-/react-cropper-1.0.1.tgz#6e5595abb8576088ab3e51ecfdcfe5f865d0340f"
|
||||||
|
dependencies:
|
||||||
|
cropperjs v1.0.0-rc.3
|
||||||
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
react-dev-utils@6.0.0-next.3e165448:
|
react-dev-utils@6.0.0-next.3e165448:
|
||||||
version "6.0.0-next.3e165448"
|
version "6.0.0-next.3e165448"
|
||||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.0-next.3e165448.tgz#d573ed0ba692f6cee23166f99204e5761df0897c"
|
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.0-next.3e165448.tgz#d573ed0ba692f6cee23166f99204e5761df0897c"
|
||||||
|
|
Loading…
Reference in New Issue