Address TODOs (#349)

* todos: simple ones, removals & modifications to NOTE

* rem-todo: reduced markdown images are removed by sanitizer

* be todo: add user validation to create

* be todo: improve test_invide_api tests

* be todo: remove todo comment

* fe todo: set error messages on reducers

* fe todo: upgrade and enable react-helmet

* todos - remove uneeded

* fe todos: remove unecessary

* be: fix remaining staking contribution calculation
This commit is contained in:
AMStrix 2019-03-13 16:39:50 -05:00 committed by Daniel Ternyak
parent 25a71ced1b
commit 1ae519e251
29 changed files with 159 additions and 137 deletions

View File

@ -401,7 +401,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Markdown source={p.content} />
</Collapse.Panel>
{/* TODO - comments, milestones, updates &etc. */}
<Collapse.Panel key="json" header="json">
<pre>{JSON.stringify(p, null, 4)}</pre>
</Collapse.Panel>
@ -469,8 +468,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
</div>
))}
</Card>
{/* TODO: contributors here? */}
</Col>
</Row>
</div>

View File

@ -22,7 +22,6 @@ class Milestone(db.Model):
content = db.Column(db.Text, nullable=False)
payout_percent = db.Column(db.String(255), nullable=False)
immediate_payout = db.Column(db.Boolean)
# TODO: change to estimated_duration (sec or ms) -- FE can calc from dates on draft
date_estimated = db.Column(db.DateTime, nullable=False)
stage = db.Column(db.String(255), nullable=False)

View File

@ -376,14 +376,14 @@ class Proposal(db.Model):
def get_staking_contribution(self, user_id: int):
contribution = None
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.contributed)
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.amount_staked)
# check funding
if remaining > 0:
# find pending contribution for any user of remaining amount
# TODO: Filter by staking=True?
contribution = ProposalContribution.query.filter_by(
proposal_id=self.id,
status=ProposalStatus.PENDING,
staking=True,
).first()
if not contribution:
contribution = self.create_contribution(
@ -537,6 +537,14 @@ class Proposal(db.Model):
funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
return str(funded)
@hybrid_property
def amount_staked(self):
contributions = ProposalContribution.query \
.filter_by(proposal_id=self.id, status=ContributionStatus.CONFIRMED, staking=True) \
.all()
amount = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
return str(amount)
@hybrid_property
def funded(self):
target = Decimal(self.target)
@ -716,9 +724,6 @@ proposal_team_invite_schema = ProposalTeamInviteSchema()
proposal_team_invites_schema = ProposalTeamInviteSchema(many=True)
# TODO: Find a way to extend ProposalTeamInviteSchema instead of redefining
class InviteWithProposalSchema(ma.Schema):
class Meta:
model = ProposalTeamInvite
@ -768,7 +773,7 @@ class ProposalContributionSchema(ma.Schema):
def get_addresses(self, obj):
# Omit 'memo' and 'sprout' for now
# TODO: Add back in 'sapling' when ready
# NOTE: Add back in 'sapling' when ready
addresses = blockchain_get('/contribution/addresses', {'contributionId': obj.id})
return {
'transparent': addresses['transparent'],

View File

@ -4,6 +4,7 @@ from decimal import Decimal
from flask import Blueprint, g, request, current_app
from marshmallow import fields, validate
from sqlalchemy import or_
from sentry_sdk import capture_message
from grant.extensions import limiter
from grant.comment.models import Comment, comment_schema, comments_schema
@ -136,7 +137,7 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
db.session.commit()
dumped_comment = comment_schema.dump(comment)
# TODO: Email proposal team if top-level comment
# Email proposal team if top-level comment
if not parent:
for member in proposal.team:
send_email(member.email_address, 'proposal_comment', {
@ -407,7 +408,6 @@ def post_proposal_team_invite(proposal_id, address):
db.session.commit()
# Send email
# TODO: Move this to some background task / after request action
email = address
user = User.get_by_email(email_address=address)
if user:
@ -531,8 +531,9 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
id=contribution_id).first()
if not contribution:
# TODO: Log in sentry
current_app.logger.warn(f'Unknown contribution {contribution_id} confirmed with txid {txid}')
msg = f'Unknown contribution {contribution_id} confirmed with txid {txid}, amount {amount}'
capture_message(msg)
current_app.logger.warn(msg)
return {"message": "No contribution matching id"}, 404
if contribution.status == ContributionStatus.CONFIRMED:
@ -578,8 +579,6 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
})
# TODO: Once we have a task queuer in place, queue emails to everyone
# on funding target reached.
contribution.proposal.set_funded_when_ready()

View File

@ -60,7 +60,7 @@ class RFP(db.Model):
matching: bool = False,
status: str = RFPStatus.DRAFT,
):
# TODO add status assert
assert RFPStatus.includes(status)
assert Category.includes(category)
self.id = gen_random_id(RFP)
self.date_created = datetime.now()

View File

@ -30,7 +30,6 @@ class ProposalReminder:
assert task.job_type == 1, "Job type: {} is incorrect for ProposalReminder".format(task.job_type)
from grant.proposal.models import Proposal
proposal = Proposal.query.filter_by(id=task.blob["proposal_id"]).first()
# TODO - replace with email
print(proposal)
task.completed = True
db.session.add(task)

View File

@ -1,6 +1,7 @@
from flask_security import UserMixin, RoleMixin
from flask_security.core import current_user
from flask_security.utils import hash_password, verify_and_update_password, login_user
from sqlalchemy.ext.hybrid import hybrid_property
from grant.comment.models import Comment
from grant.email.models import EmailVerification, EmailRecovery
from grant.email.send import send_email
@ -10,11 +11,11 @@ from grant.email.subscription_settings import (
email_subscriptions_to_dict
)
from grant.extensions import ma, db, security
from grant.utils.misc import make_url, gen_random_id
from grant.utils.misc import make_url, gen_random_id, is_email
from grant.utils.social import generate_social_url
from grant.utils.upload import extract_avatar_filename, construct_avatar_url
from grant.utils import totp_2fa
from sqlalchemy.ext.hybrid import hybrid_property
from grant.utils.exceptions import ValidationException
def is_current_authed_user_id(user_id):
@ -133,8 +134,6 @@ class User(db.Model, UserMixin):
backref=db.backref('users', lazy='dynamic'))
arbiter_proposals = db.relationship("ProposalArbiter", lazy=True, back_populates="user")
# TODO - add create and validate methods
def __init__(
self,
email_address,
@ -150,6 +149,22 @@ class User(db.Model, UserMixin):
self.title = title
self.password = password
@staticmethod
def validate(user):
em = user.get('email_address')
if not em:
raise ValidationException('Must have email address')
if not is_email(em):
raise ValidationException('Email address looks invalid')
t = user.get('title')
if t and len(t) > 255:
raise ValidationException('Title is too long')
dn = user.get('display_name')
if dn and len(dn) > 255:
raise ValidationException('Display name is too long')
@staticmethod
def create(email_address=None, password=None, display_name=None, title=None, _send_email=True):
user = security.datastore.create_user(
@ -158,6 +173,7 @@ class User(db.Model, UserMixin):
display_name=display_name,
title=title
)
User.validate(vars(user))
security.datastore.commit()
# user settings
@ -249,7 +265,6 @@ class User(db.Model, UserMixin):
db.session.flush()
def set_admin(self, is_admin: bool):
# TODO: audit entry & possibly email user
self.is_admin = is_admin
db.session.add(self)
db.session.flush()

View File

@ -245,10 +245,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def test_update_proposal(self):
self.login_admin()
# set to 1 (on)
resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}",
data=json.dumps({"contributionMatching": 1}))
self.assert200(resp_on)
self.assertEqual(resp_on.json['contributionMatching'], 1)
resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 0}))
resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}",
data=json.dumps({"contributionMatching": 0}))
self.assert200(resp_off)
self.assertEqual(resp_off.json['contributionMatching'], 0)
@ -307,7 +309,6 @@ class TestAdminAPI(BaseProposalCreatorConfig):
})
)
self.assert200(resp)
# TODO - more tests
def test_create_rfp_succeeds(self):
self.login_admin()
@ -315,12 +316,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
resp = self.app.post(
"/api/v1/admin/rfps",
data=json.dumps({
"brief": "Some brief",
"category": "CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
"brief": "Some brief",
"category": "CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
})
)
self.assert200(resp)
@ -331,13 +332,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
resp = self.app.post(
"/api/v1/admin/rfps",
data=json.dumps({
"brief": "Some brief",
"category": "NOT_CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
"brief": "Some brief",
"category": "NOT_CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
})
)
self.assert400(resp)

View File

@ -47,7 +47,10 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
self.assertStatus(invites_res, 200)
# Make sure we made the team, coach
self.assertTrue(len(self.other_proposal.team) == 2) # TODO: More thorough check than length
print(self.other_proposal.team)
self.assertTrue(len(self.other_proposal.team) == 2)
team_ids = [t.id for t in self.other_proposal.team]
self.assertIn(self.user.id, team_ids, 'user should be in team')
def test_put_user_invite_response_reject(self):
invite = ProposalTeamInvite(
@ -67,7 +70,9 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
self.assertStatus(invites_res, 200)
# Make sure we made the team, coach
self.assertTrue(len(self.other_proposal.team) == 1) # TODO: More thorough check than length
self.assertTrue(len(self.other_proposal.team) == 1)
team_ids = [t.id for t in self.other_proposal.team]
self.assertNotIn(self.user.id, team_ids, 'user should NOT be in team')
def test_no_auth_put_user_invite_response(self):
invite = ProposalTeamInvite(

View File

@ -144,7 +144,7 @@ class TestUserAPI(BaseUserConfig):
self.login_default_user()
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
updated_user["displayName"] = 'new display name'
updated_user["avatar"] = '' # TODO confirm avatar is no longer a dict
updated_user["avatar"] = ''
updated_user["socialMedias"] = []
user_update_resp = self.app.put(

View File

@ -33,7 +33,6 @@ export interface VOut {
scriptPubKey: ScriptPubKey;
}
export interface Transaction {
txid: string;
hex: string;
@ -46,7 +45,7 @@ export interface Transaction {
time: number;
vin: VIn[];
vout: VOut[];
// TODO: fill me out, what is this?
// unclear what vjoinsplit is
vjoinsplit: any[];
}
@ -74,7 +73,6 @@ export interface BlockWithTransactionIds extends Block {
tx: string[];
}
export interface BlockWithTransactions extends Block {
tx: Transaction[];
}
@ -107,33 +105,44 @@ export interface ValidationResponse {
isvalid: boolean;
}
// TODO: Type all methods with signatures from
// https://github.com/zcash/zcash/blob/master/doc/payment-api.md
interface ZCashNode {
getblockchaininfo: () => Promise<BlockChainInfo>;
getblockcount: () => Promise<number>;
getblock: {
(numberOrHash: string | number, verbosity?: 1): Promise<BlockWithTransactionIds>;
(numberOrHash: string | number, verbosity: 2): Promise<BlockWithTransactions>;
(numberOrHash: string | number, verbosity?: 1): Promise<
BlockWithTransactionIds
>;
(numberOrHash: string | number, verbosity: 2): Promise<
BlockWithTransactions
>;
(numberOrHash: string | number, verbosity: 0): Promise<string>;
}
};
gettransaction: (txid: string) => Promise<Transaction>;
validateaddress: (address: string) => Promise<ValidationResponse>;
z_getbalance: (address: string, minConf?: number) => Promise<number>;
z_getnewaddress: (type?: 'sprout' | 'sapling') => Promise<string>;
z_getnewaddress: (type?: "sprout" | "sapling") => Promise<string>;
z_listaddresses: () => Promise<string[]>;
z_listreceivedbyaddress: (address: string, minConf?: number) => Promise<Receipt[]>;
z_importviewingkey: (key: string, rescan?: 'yes' | 'no' | 'whenkeyisnew', startHeight?: number) => Promise<void>;
z_listreceivedbyaddress: (
address: string,
minConf?: number
) => Promise<Receipt[]>;
z_importviewingkey: (
key: string,
rescan?: "yes" | "no" | "whenkeyisnew",
startHeight?: number
) => Promise<void>;
z_exportviewingkey: (zaddr: string) => Promise<string>;
z_validatepaymentdisclosure: (disclosure: string) => Promise<DisclosedPayment>;
z_validatepaymentdisclosure: (
disclosure: string
) => Promise<DisclosedPayment>;
z_validateaddress: (address: string) => Promise<ValidationResponse>;
}
export const rpcOptions = {
url: env.ZCASH_NODE_URL,
username: env.ZCASH_NODE_USERNAME,
password: env.ZCASH_NODE_PASSWORD,
password: env.ZCASH_NODE_PASSWORD
};
const node: ZCashNode = stdrpc(rpcOptions);
@ -152,28 +161,31 @@ export async function initNode() {
}
if (info.chain.includes("test")) {
network = bitcore.Networks.testnet;
}
else {
} else {
network = bitcore.Networks.mainnet;
}
}
catch(err) {
} catch (err) {
captureException(err);
log.error(err.response ? err.response.data : err);
log.error('Failed to connect to zcash node with the following credentials:\r\n', rpcOptions);
log.error(
"Failed to connect to zcash node with the following credentials:\r\n",
rpcOptions
);
process.exit(1);
}
// Check if sprout address is readable
try {
if (!env.SPROUT_ADDRESS) {
console.error('Missing SPROUT_ADDRESS environment variable, exiting');
console.error("Missing SPROUT_ADDRESS environment variable, exiting");
process.exit(1);
}
await node.z_getbalance(env.SPROUT_ADDRESS as string);
} catch(err) {
} catch (err) {
if (!env.SPROUT_VIEWKEY) {
log.error('Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting');
log.error(
"Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting"
);
process.exit(1);
}
await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
@ -183,7 +195,7 @@ export async function initNode() {
export function getNetwork() {
if (!network) {
throw new Error('Called getNetwork before initNode');
throw new Error("Called getNetwork before initNode");
}
return network;
}
@ -194,11 +206,15 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
try {
const tx = await node.gettransaction(txid);
const block = await node.getblock(tx.blockhash);
const height = block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
const height =
block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
return height.toString();
} catch(err) {
console.warn(`Attempted to get block height for tx ${txid} but failed with the following error:\n`, err);
console.warn('Falling back to hard-coded starter blocks');
} catch (err) {
console.warn(
`Attempted to get block height for tx ${txid} but failed with the following error:\n`,
err
);
console.warn("Falling back to hard-coded starter blocks");
}
}
@ -207,11 +223,10 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
const net = getNetwork();
if (net === bitcore.Networks.mainnet) {
return env.MAINNET_START_BLOCK;
}
else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
} else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
return env.TESTNET_START_BLOCK;
}
// Regtest or otherwise unknown networks should start at the bottom
return '0';
return "0";
}

View File

@ -21,8 +21,7 @@ export function authenticate(secret: string) {
return hash === sha256(secret);
}
// TODO: Not fully confident in compatibility with most bip32 wallets,
// do more work to ensure this is reliable.
// NOTE: this is just one way to derive t-addrs
export function deriveTransparentAddress(index: number, network: any) {
const root = new HDPublicKey(env.BIP32_XPUB);
const child = root.derive(`m/0/${index}`);
@ -39,14 +38,16 @@ export function removeItem<T>(arr: T[], remove: T) {
}
export function encodeHexMemo(memo: string) {
return new Buffer(memo, 'utf8').toString('hex');
return new Buffer(memo, "utf8").toString("hex");
}
export function decodeHexMemo(memoHex: string) {
return new Buffer(memoHex, 'hex')
.toString()
// Remove null bytes from zero padding
.replace(/\0.*$/g, '');
return (
new Buffer(memoHex, "hex")
.toString()
// Remove null bytes from zero padding
.replace(/\0.*$/g, "")
);
}
export function makeContributionMemo(contributionId: number) {
@ -54,14 +55,15 @@ export function makeContributionMemo(contributionId: number) {
}
export function getContributionIdFromMemo(memoHex: string) {
const matches = decodeHexMemo(memoHex).match(/Contribution ([0-9]+) on Grant\.io/);
const matches = decodeHexMemo(memoHex).match(
/Contribution ([0-9]+) on Grant\.io/
);
if (matches && matches[1]) {
return parseInt(matches[1], 10);
}
return false;
}
// TODO: Make this more robust
export function toBaseUnit(unit: number) {
return Math.floor(100000000 * unit);
}

View File

@ -21,4 +21,4 @@ Tests can be found in `cypress/integration`. Cypress will hot-reload open tests
### CI
TODO
Coming soon.

View File

@ -79,7 +79,6 @@ describe("create.fund.ms2.no-vote.re-vote", () => {
"Request milestone payout",
{ timeout: 20000 }
).click();
// TODO: fix this bug (the following fails)
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
timeout: 20000
});

View File

@ -25,7 +25,6 @@ class BasicHead extends React.Component<Props> {
name="keywords"
content="Zcash, Zcash Foundation, Zcash Foundation Grants, Zcash Grants, Zcash Grant, ZF Grants, ZFGrants"
/>
<meta name={`${title} page`} content={`${title} page stuff`} />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
@ -41,7 +40,6 @@ class BasicHead extends React.Component<Props> {
<meta property="og:url" content={defaultOgpUrl} />
<meta property="og:image" content={defaultOgpImage} />
<meta property="og:locale" content="en_US" />
{/* TODO: i18n */}
{/* <meta property="og:locale:alternate" content="en_US" /> */}
{/* <meta property="og:locale:alternate" content="de_DE" /> */}

View File

@ -41,7 +41,6 @@ class Comment extends React.Component<Props> {
};
componentDidUpdate(prevProps: Props) {
// TODO: Come up with better check on if our comment post was a success
const { isPostCommentPending, postCommentError } = this.props;
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
this.setState({ reply: '', isReplying: false });

View File

@ -1,6 +1,6 @@
import React from 'react';
// import { Helmet } from 'react-helmet';
// import { urlToPublic } from 'utils/helpers';
import { Helmet } from 'react-helmet';
import { urlToPublic } from 'utils/helpers';
interface Props {
title: string;
@ -12,25 +12,22 @@ interface Props {
export default class HeaderDetails extends React.Component<Props> {
render() {
// TODO: Uncomment once helmet is fixed
// https://github.com/nfl/react-helmet/issues/373
return null;
// const { title, image, url, type, description } = this.props;
// return (
// <Helmet>
// <title>{`ZF Grants - ${title}`}</title>
// {/* open graph protocol */}
// {type && <meta property="og:type" content="website" />}
// <meta property="og:title" content={title} />
// {description && <meta property="og:description" content={description} />}
// {url && <meta property="og:url" content={urlToPublic(url)} />}
// {image && <meta property="og:image" content={urlToPublic(image)} />}
// {/* twitter card */}
// <meta property="twitter:title" content={title} />
// {description && <meta property="twitter:description" content={description} />}
// {url && <meta property="twitter:url" content={urlToPublic(url)} />}
// {image && <meta property="twitter:image" content={urlToPublic(image)} />}
// </Helmet>
// );
const { title, image, url, type, description } = this.props;
return (
<Helmet>
<title>{`ZF Grants - ${title}`}</title>
{/* open graph protocol */}
{type && <meta property="og:type" content="website" />}
<meta property="og:title" content={title} />
{description && <meta property="og:description" content={description} />}
{url && <meta property="og:url" content={urlToPublic(url)} />}
{image && <meta property="og:image" content={urlToPublic(image)} />}
{/* twitter card */}
<meta property="twitter:title" content={title} />
{description && <meta property="twitter:description" content={description} />}
{url && <meta property="twitter:url" content={urlToPublic(url)} />}
{image && <meta property="twitter:image" content={urlToPublic(image)} />}
</Helmet>
);
}
}

View File

@ -106,7 +106,6 @@ class Profile extends React.Component<Props, State> {
return (
<div className="Profile">
{/* TODO: customize details for funders/creators */}
<HeaderDetails
title={`${user.displayName} is funding projects on ZF Grants`}
description={`Join ${user.displayName} in funding the future!`}

View File

@ -240,7 +240,6 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
return;
}
// TODO: Get values from proposal
const { target, funded } = this.props.proposal;
const remainingTarget = target.sub(funded);
const amount = parseFloat(value);

View File

@ -55,7 +55,6 @@ class ProposalComments extends React.Component<Props, State> {
}
componentDidUpdate(prevProps: Props) {
// TODO: Come up with better check on if our comment post was a success
const { isPostCommentPending, postCommentError } = this.props;
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
this.setState({ comment: '' });

View File

@ -386,8 +386,7 @@ export default (state = INITIAL_STATE, action: any) => {
case types.PROPOSAL_UPDATES_REJECTED:
return {
...state,
// TODO: Get action to send real error
updatesError: 'Failed to fetch updates',
updatesError: (payload && payload.message) || payload.toString(),
isFetchingUpdates: false,
};
@ -402,8 +401,7 @@ export default (state = INITIAL_STATE, action: any) => {
case types.PROPOSAL_CONTRIBUTIONS_REJECTED:
return {
...state,
// TODO: Get action to send real error
fetchContributionsError: 'Failed to fetch updates',
fetchContributionsError: (payload && payload.message) || payload.toString(),
isFetchingContributions: false,
};

View File

@ -47,8 +47,7 @@ export default (state: RFPState = INITIAL_STATE, action: any): RFPState => {
case types.FETCH_RFP_REJECTED:
return {
...state,
// TODO: Get action to send real error
fetchRfpsError: 'Failed to fetch rfps',
fetchRfpsError: (payload && payload.message) || payload.toString(),
isFetchingRfps: false,
};
case types.FETCH_RFPS_FULFILLED:

View File

@ -114,7 +114,7 @@ export function formatRFPFromGet(rfp: RFP): RFP {
return rfp;
}
// TODO: i18n on case-by-case basis
// NOTE: i18n on case-by-case basis
export function generateSlugUrl(id: number, title: string) {
const slug = title
.toLowerCase()

View File

@ -27,7 +27,6 @@ const converters: { [key in MARKDOWN_TYPE]: Showdown.Converter } = {
...sharedOptions,
noHeaderId: true,
headerLevelStart: 4,
// TODO: Find a way to disable images
}),
};

View File

@ -60,7 +60,7 @@
"@types/react": "16.4.18",
"@types/react-cropper": "^0.10.3",
"@types/react-dom": "16.0.9",
"@types/react-helmet": "^5.0.7",
"@types/react-helmet": "^5.0.8",
"@types/react-redux": "^6.0.2",
"@types/react-router": "4.4.3",
"@types/react-router-dom": "^4.3.1",
@ -126,7 +126,7 @@
"react-cropper": "^1.0.1",
"react-dev-utils": "^5.0.2",
"react-dom": "16.5.2",
"react-helmet": "^5.2.0",
"react-helmet": "6.0.0-beta",
"react-hot-loader": "^4.3.8",
"react-i18next": "^8.3.5",
"react-mde": "7.0.4",

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { Helmet } from 'react-helmet';
import { ChunkExtractor } from '@loadable/server';
export interface Props {
@ -34,13 +34,6 @@ const HTML: React.SFC<Props> = ({
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-TileColor" content="#fff" />
<meta name="theme-color" content="#fff" />
{/* TODO: import from @fortawesome */}
{/* <link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
crossOrigin="anonymous"
/> */}
{/* Custom link & meta tags from webpack */}
{extractor.getLinkElements()}
{linkTags.map((l, idx) => (

View File

@ -13,7 +13,7 @@ export interface Contribution {
export interface ContributionWithAddresses extends Contribution {
addresses: {
transparent: string;
// TODO: Add sapling and memo in when ready
// NOTE: Add sapling and memo in when ready
// sprout: string;
// memo: string;
};

View File

@ -2,7 +2,7 @@ import { SocialMedia } from 'types';
export interface User {
userid: number;
emailAddress?: string; // TODO: Split into full user type
emailAddress?: string;
emailVerified?: boolean;
displayName: string;
title: string;

View File

@ -1940,9 +1940,10 @@
"@types/node" "*"
"@types/react" "*"
"@types/react-helmet@^5.0.7":
version "5.0.7"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.7.tgz#1cae65b2c37fe54cf56f40cd388836d4619dbc51"
"@types/react-helmet@^5.0.8":
version "5.0.8"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.8.tgz#f080eea6652e44f60b4574463d238f268d81d9af"
integrity sha512-ZTr12eDAYI0yUiMx1K82EHqRYa8J1BOOLus+0gL+AkksUiIPwLE0wLiXa9FNqD8r9GXAi+yRPZImkRh1JNlTkQ==
dependencies:
"@types/react" "*"
@ -9601,6 +9602,11 @@ react-error-overlay@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
react-fast-compare@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fittext@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/react-fittext/-/react-fittext-1.0.0.tgz#836a1c04f9322f6c94cb69e45c66006fc42d37a5"
@ -9617,13 +9623,14 @@ react-fuzzy@^0.5.2:
fuse.js "^3.0.1"
prop-types "^15.5.9"
react-helmet@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.0.tgz#a81811df21313a6d55c5f058c4aeba5d6f3d97a7"
react-helmet@6.0.0-beta:
version "6.0.0-beta"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0-beta.tgz#1f2ac04521951486e4fce3296d0c88aae8cabd5c"
integrity sha512-GnNWsokebTe7fe8MH2I/a2dl4THYWhthLBoMaQSRYqW5XbPo881WAJGi+lqRBjyOFryW6zpQluEkBy70zh+h9w==
dependencies:
deep-equal "^1.0.1"
object-assign "^4.1.1"
prop-types "^15.5.4"
react-fast-compare "^2.0.2"
react-side-effect "^1.1.0"
react-hot-loader@^4.3.8: