From fe1e2a8df345e77274c6b0f8f5c25769163756ee Mon Sep 17 00:00:00 2001 From: AMStrix Date: Mon, 17 Sep 2018 15:55:49 -0500 Subject: [PATCH] Replace nextjs (#54) * add new deps * remove .babelrc * add main files * package scripts + add missing typings * tslint ignore json * replace next/router * replace next/link * HMR + configureStore + fontawsome header link * Use Link instead of Redirect to solve same page redirect problem. * Home svg import. * hide filter button even if ant styles load first * Integrate Helmet * adjust style loading + fix font-face url format * import style higher in render tree for improved SSR * dev.js - nodemon only watch build/server dir * precedence order fixed * keep_fnames=true to keep uglifyjs from mangling BN * small cleanup --- frontend/.babelrc | 29 - frontend/bin/build.js | 29 + frontend/bin/dev.js | 96 + frontend/bin/truffle-util.js | 140 + frontend/bin/utils.js | 27 + frontend/client/Routes.tsx | 50 + frontend/client/components/BasicHead.tsx | 14 +- .../CreateProposal/CreateSuccess.tsx | 8 +- .../CreateProposal/MilestoneFields.tsx | 1 + frontend/client/components/Footer/index.tsx | 8 +- frontend/client/components/Footer/styled.tsx | 24 +- frontend/client/components/Header/index.tsx | 32 +- frontend/client/components/Header/styled.ts | 70 +- frontend/client/components/Home/index.tsx | 32 +- frontend/client/components/Home/styled.ts | 2 +- .../Proposal/CampaignBlock/index.tsx | 2 +- frontend/client/components/Proposal/index.tsx | 2 +- .../Proposals/ProposalCard/index.tsx | 85 +- frontend/client/index.tsx | 25 + frontend/client/pages/proposal.tsx | 6 +- frontend/client/store/configure.tsx | 8 + frontend/client/styles/fonts.less | 6 +- .../typings/express-manifest-helpers.d.ts | 1 + .../client/typings/loadable-components.d.ts | 1 + frontend/config/env.js | 52 + frontend/config/paths.js | 24 + .../config/webpack.config.js/client.base.js | 74 + .../config/webpack.config.js/client.dev.js | 28 + .../config/webpack.config.js/client.prod.js | 21 + frontend/config/webpack.config.js/index.js | 8 + frontend/config/webpack.config.js/loaders.js | 229 ++ .../module-dependency-warning.js | 24 + frontend/config/webpack.config.js/plugins.js | 60 + .../config/webpack.config.js/resolvers.js | 20 + .../config/webpack.config.js/server.base.js | 36 + .../config/webpack.config.js/server.dev.js | 19 + .../config/webpack.config.js/server.prod.js | 6 + frontend/logs/.gitkeep | 0 frontend/package.json | 80 +- frontend/server/components/HTML.tsx | 61 + frontend/server/index.tsx | 71 + frontend/server/log.ts | 55 + frontend/server/render.tsx | 141 + frontend/tsconfig.json | 21 +- frontend/tslint.json | 3 + frontend/yarn.lock | 3005 ++++++++++++++++- 46 files changed, 4444 insertions(+), 292 deletions(-) delete mode 100644 frontend/.babelrc create mode 100644 frontend/bin/build.js create mode 100644 frontend/bin/dev.js create mode 100644 frontend/bin/truffle-util.js create mode 100644 frontend/bin/utils.js create mode 100644 frontend/client/Routes.tsx create mode 100644 frontend/client/index.tsx create mode 100644 frontend/client/typings/express-manifest-helpers.d.ts create mode 100644 frontend/client/typings/loadable-components.d.ts create mode 100644 frontend/config/env.js create mode 100644 frontend/config/paths.js create mode 100644 frontend/config/webpack.config.js/client.base.js create mode 100644 frontend/config/webpack.config.js/client.dev.js create mode 100644 frontend/config/webpack.config.js/client.prod.js create mode 100644 frontend/config/webpack.config.js/index.js create mode 100644 frontend/config/webpack.config.js/loaders.js create mode 100644 frontend/config/webpack.config.js/module-dependency-warning.js create mode 100644 frontend/config/webpack.config.js/plugins.js create mode 100644 frontend/config/webpack.config.js/resolvers.js create mode 100644 frontend/config/webpack.config.js/server.base.js create mode 100644 frontend/config/webpack.config.js/server.dev.js create mode 100644 frontend/config/webpack.config.js/server.prod.js create mode 100644 frontend/logs/.gitkeep create mode 100644 frontend/server/components/HTML.tsx create mode 100644 frontend/server/index.tsx create mode 100644 frontend/server/log.ts create mode 100644 frontend/server/render.tsx diff --git a/frontend/.babelrc b/frontend/.babelrc deleted file mode 100644 index 5666c917..00000000 --- a/frontend/.babelrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "presets": ["next/babel", "@zeit/next-typescript/babel"], - "env": { - "development": { - "plugins": ["inline-dotenv"] - }, - "production": { - "plugins": ["transform-inline-environment-variables"] - } - }, - "plugins": [ - ["import", { "libraryName": "antd", "style": false }], - [ - "module-resolver", - { - "root": ["client"], - "extensions": [".js", ".tsx", ".ts"] - } - ], - [ - "styled-components", - { - "ssr": true, - "displayName": true, - "preprocess": false - } - ] - ] -} diff --git a/frontend/bin/build.js b/frontend/bin/build.js new file mode 100644 index 00000000..c33e0d89 --- /dev/null +++ b/frontend/bin/build.js @@ -0,0 +1,29 @@ +const webpack = require('webpack'); +const rimraf = require('rimraf'); + +const webpackConfig = require('../config/webpack.config.js')( + process.env.NODE_ENV || 'production', +); +const paths = require('../config/paths'); +const { logMessage } = require('./utils'); + +const build = async () => { + rimraf.sync(paths.clientBuild); + rimraf.sync(paths.serverBuild); + + logMessage('Compiling, please wait...'); + + const [clientConfig, serverConfig] = webpackConfig; + const multiCompiler = webpack([clientConfig, serverConfig]); + multiCompiler.run((error, stats) => { + if (stats) { + console.log(stats.toString(clientConfig.stats)); + } + if (error) { + logMessage('Compile error', error); + console.error(error); + } + }); +}; + +build(); diff --git a/frontend/bin/dev.js b/frontend/bin/dev.js new file mode 100644 index 00000000..8b6034f2 --- /dev/null +++ b/frontend/bin/dev.js @@ -0,0 +1,96 @@ +const fs = require('fs'); +const webpack = require('webpack'); +const nodemon = require('nodemon'); +const rimraf = require('rimraf'); +const webpackConfig = require('../config/webpack.config.js')( + process.env.NODE_ENV || 'development', +); +const webpackDevMiddleware = require('webpack-dev-middleware'); +const webpackHotMiddleware = require('webpack-hot-middleware'); +const express = require('express'); +const paths = require('../config/paths'); +const truffleUtil = require('./truffle-util'); +const { logMessage } = require('./utils'); + +const app = express(); + +const WEBPACK_PORT = + process.env.WEBPACK_PORT || + (!isNaN(Number(process.env.PORT)) ? Number(process.env.PORT) + 1 : 3001); + +const start = async () => { + rimraf.sync(paths.clientBuild); + rimraf.sync(paths.serverBuild); + + await truffleUtil.ethereumCheck(); + + const [clientConfig, serverConfig] = webpackConfig; + clientConfig.entry.bundle = [ + `webpack-hot-middleware/client?path=http://localhost:${WEBPACK_PORT}/__webpack_hmr`, + ...clientConfig.entry.bundle, + ]; + + clientConfig.output.hotUpdateMainFilename = 'updates/[hash].hot-update.json'; + clientConfig.output.hotUpdateChunkFilename = 'updates/[id].[hash].hot-update.js'; + + const publicPath = clientConfig.output.publicPath; + clientConfig.output.publicPath = `http://localhost:${WEBPACK_PORT}${publicPath}`; + serverConfig.output.publicPath = `http://localhost:${WEBPACK_PORT}${publicPath}`; + + const multiCompiler = webpack([clientConfig, serverConfig]); + const clientCompiler = multiCompiler.compilers[0]; + const serverCompiler = multiCompiler.compilers[1]; + + serverCompiler.hooks.compile.tap('_', () => logMessage('Server compiling...', 'info')); + clientCompiler.hooks.compile.tap('_', () => logMessage('Client compiling...', 'info')); + + const watchOptions = { + ignored: /node_modules/, + stats: clientConfig.stats, + }; + + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + return next(); + }); + + const devMiddleware = webpackDevMiddleware(multiCompiler, { + publicPath: clientConfig.output.publicPath, + stats: clientConfig.stats, + watchOptions, + }); + app.use(devMiddleware); + app.use(webpackHotMiddleware(clientCompiler)); + app.use('/static', express.static(paths.clientBuild)); + app.listen(WEBPACK_PORT); + + // await first build... + await new Promise((res, rej) => devMiddleware.waitUntilValid(() => res())); + + const script = nodemon({ + script: `${paths.serverBuild}/server.js`, + watch: [paths.serverBuild], + verbose: true, + }); + + // uncomment to see nodemon details + // script.on('log', x => console.log(`LOG `, x.colour)); + + script.on('crash', () => + logMessage( + 'Server crashed, will attempt to restart after changes. Waiting...', + 'error', + ), + ); + + script.on('restart', () => { + logMessage('Server restarted.', 'warning'); + }); + + script.on('error', () => { + logMessage('An error occured attempting to run the server. Exiting', 'error'); + process.exit(1); + }); +}; + +start(); diff --git a/frontend/bin/truffle-util.js b/frontend/bin/truffle-util.js new file mode 100644 index 00000000..8ddca4b4 --- /dev/null +++ b/frontend/bin/truffle-util.js @@ -0,0 +1,140 @@ +const rimraf = require('rimraf'); +const path = require('path'); +const fs = require('fs'); +const childProcess = require('child_process'); +const Web3 = require('web3'); + +const paths = require('../config/paths'); +const truffleConfig = require('../truffle'); +const { logMessage } = require('./utils'); + +require('../config/env'); + +module.exports = {}; + +const clean = (module.exports.clean = () => { + rimraf.sync(paths.contractsBuild); +}); + +const compile = (module.exports.compile = () => { + childProcess.execSync('yarn build', { cwd: paths.contractsBase }); +}); + +const migrate = (module.exports.migrate = () => { + childProcess.execSync('truffle migrate', { cwd: paths.contractsBase }); +}); + +const makeWeb3Conn = () => { + const { host, port } = truffleConfig.networks.development; + return `ws://${host}:${port}`; +}; + +const createWeb3 = () => { + return new Web3(makeWeb3Conn()); +}; + +const isGanacheUp = (module.exports.isGanacheUp = verbose => + new Promise((res, rej) => { + verbose && logMessage(`Testing ganache @ ${makeWeb3Conn()}...`, 'info'); + // console.log('curProv', web3.eth.currentProvider); + const web3 = createWeb3(); + web3.eth.net + .isListening() + .then(() => { + verbose && logMessage('Ganache is UP!', 'info'); + res(true); + web3.currentProvider.connection.close(); + }) + .catch(e => { + logMessage('Ganache appears to be down, unable to connect.', 'error'); + res(false); + }); + })); + +const getGanacheNetworkId = (module.exports.getGanacheNetworkId = () => { + const web3 = createWeb3(); + return web3.eth.net + .getId() + .then(id => { + web3.currentProvider.connection.close(); + return id; + }) + .catch(() => -1); +}); + +const checkContractsNetworkIds = (module.exports.checkContractsNetworkIds = id => + new Promise((res, rej) => { + const buildDir = paths.contractsBuild; + fs.readdir(buildDir, (err, names) => { + if (err) { + logMessage(`No contracts build directory @ ${buildDir}`, 'error'); + res(false); + } else { + const allHaveId = names.reduce((ok, name) => { + const contract = require(path.join(buildDir, name)); + if (Object.keys(contract.networks).length > 0 && !contract.networks[id]) { + const actual = Object.keys(contract.networks).join(', '); + logMessage(`${name} should have networks[${id}], it has ${actual}`, 'error'); + return false; + } + return true && ok; + }, true); + res(allHaveId); + } + }); + })); + +const fundWeb3v1 = (module.exports.fundWeb3v1 = () => { + // Fund ETH accounts + const ethAccounts = process.env.FUND_ETH_ADDRESSES + ? process.env.FUND_ETH_ADDRESSES.split(',').map(a => a.trim()) + : []; + const web3 = createWeb3(); + return web3.eth.getAccounts().then(accts => { + if (ethAccounts.length) { + logMessage('Sending 50% of ETH balance from accounts...', 'info'); + const txs = ethAccounts.map((addr, i) => { + return web3.eth + .getBalance(accts[i]) + .then(parseInt) + .then(bal => { + const amount = '' + Math.round(bal / 2); + const amountEth = web3.utils.fromWei(amount); + return web3.eth + .sendTransaction({ + to: addr, + from: accts[i], + value: amount, + }) + .then(() => logMessage(` ${addr} <- ${amountEth} from ${accts[i]}`)) + .catch(e => + logMessage(` Error sending funds to ${addr} : ${e}`, 'error'), + ); + }); + }); + return Promise.all(txs).then(() => web3.currentProvider.connection.close()); + } else { + logMessage('No accounts specified for funding in .env file...', 'warning'); + } + }); +}); + +module.exports.ethereumCheck = () => + isGanacheUp(true) + .then(isUp => !isUp && Promise.reject('network down')) + .then(getGanacheNetworkId) + .then(checkContractsNetworkIds) + .then(allHaveId => { + if (!allHaveId) { + logMessage('Contract problems, will compile & migrate.', 'warning'); + clean(); + logMessage('truffle compile, please wait...', 'info'); + compile(); + logMessage('truffle migrate, please wait...', 'info'); + migrate(); + fundWeb3v1(); + } else { + logMessage('OK, Contracts have correct network id.', 'info'); + } + }) + .catch(e => logMessage('WARNING: ethereum setup has a problem: ' + e, 'error')); diff --git a/frontend/bin/utils.js b/frontend/bin/utils.js new file mode 100644 index 00000000..6d06d6fb --- /dev/null +++ b/frontend/bin/utils.js @@ -0,0 +1,27 @@ +const chalk = require('chalk'); +const net = require('net'); + +const logMessage = (message, level = 'info') => { + const colors = { error: 'red', warning: 'yellow', info: 'blue' }; + colors[undefined] = 'white'; + console.log(`${chalk[colors[level]](message)}`); +}; + +const isPortTaken = port => + new Promise((res, rej) => { + const tester = net + .createServer() + .once('error', function(err) { + err.code != 'EADDRINUSE' && rej(); + err.code == 'EADDRINUSE' && res(true); + }) + .once('listening', () => { + tester.once('close', () => res(false)).close(); + }) + .listen(port, '127.0.0.1'); + }); + +module.exports = { + logMessage, + isPortTaken, +}; diff --git a/frontend/client/Routes.tsx b/frontend/client/Routes.tsx new file mode 100644 index 00000000..be42f697 --- /dev/null +++ b/frontend/client/Routes.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { Switch, Route, Redirect } from 'react-router'; +import { injectGlobal } from 'styled-components'; +import loadable from 'loadable-components'; + +// wrap components in loadable...import & they will be split +const Home = loadable(() => import('pages/index')); +const Create = loadable(() => import('pages/create')); +const Proposals = loadable(() => import('pages/proposals')); +const Proposal = loadable(() => import('pages/proposal')); + +import 'styles/style.less'; + +// tslint:disable-next-line:no-unused-expression +injectGlobal` + * { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + } + html { + font-size: 16px; + + @media (max-width: 900px) { + font-size: 14px; + } + @media (max-width: 600px) { + font-size: 12px; + } + + } +`; + +class Routes extends React.Component { + render() { + return ( + + + + + + } /> + + ); + } +} + +export default hot(module)(Routes); diff --git a/frontend/client/components/BasicHead.tsx b/frontend/client/components/BasicHead.tsx index 11af6c80..7898e9f4 100644 --- a/frontend/client/components/BasicHead.tsx +++ b/frontend/client/components/BasicHead.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import Head from 'next/head'; - -import 'styles/style.less'; +import { Helmet } from 'react-helmet'; interface Props { title: string; @@ -12,20 +10,16 @@ export default class BasicHead extends React.Component { const { children, title } = this.props; return (
- + Grant.io - {title} - {/*TODO - bundle*/} + - - - - - + {children}
); diff --git a/frontend/client/components/CreateProposal/CreateSuccess.tsx b/frontend/client/components/CreateProposal/CreateSuccess.tsx index c2ecca45..94f15468 100644 --- a/frontend/client/components/CreateProposal/CreateSuccess.tsx +++ b/frontend/client/components/CreateProposal/CreateSuccess.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import { Icon } from 'antd'; import * as Styled from './styled'; @@ -16,10 +16,8 @@ const CreateSuccess = ({ crowdFundCreatedAddress }: Props) => (

Contract was succesfully deployed!

Your proposal is now live and on the blockchain!{' '} - - Click here - {' '} - to check it out. + Click here to check it + out.
diff --git a/frontend/client/components/CreateProposal/MilestoneFields.tsx b/frontend/client/components/CreateProposal/MilestoneFields.tsx index bc249616..d3eda4a0 100644 --- a/frontend/client/components/CreateProposal/MilestoneFields.tsx +++ b/frontend/client/components/CreateProposal/MilestoneFields.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Input, DatePicker, Card, Icon, Alert, Checkbox } from 'antd'; import moment from 'moment'; diff --git a/frontend/client/components/Footer/index.tsx b/frontend/client/components/Footer/index.tsx index 991bd04e..3b249f7e 100644 --- a/frontend/client/components/Footer/index.tsx +++ b/frontend/client/components/Footer/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import * as Styled from './styled'; export default () => ( - - Grant.io - + + Grant.io + {/* about legal diff --git a/frontend/client/components/Footer/styled.tsx b/frontend/client/components/Footer/styled.tsx index 357b6061..9a651b22 100644 --- a/frontend/client/components/Footer/styled.tsx +++ b/frontend/client/components/Footer/styled.tsx @@ -10,18 +10,20 @@ export const Footer = styled.footer` height: 140px; `; -export const Title = styled.a` - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: #fff; - transition: transform 100ms ease; +export const Title = styled.span` + a { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: #fff; + transition: transform 100ms ease; - &:hover, - &:focus, - &:active { - transform: translateY(-1px); - color: inherit; + &:hover, + &:focus, + &:active { + transform: translateY(-1px); + color: inherit; + } } `; diff --git a/frontend/client/components/Header/index.tsx b/frontend/client/components/Header/index.tsx index 938b7c26..a83da839 100644 --- a/frontend/client/components/Header/index.tsx +++ b/frontend/client/components/Header/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Icon } from 'antd'; -import Link from 'next/link'; +import { Link } from 'react-router-dom'; import * as Styled from './styled'; interface OwnProps { @@ -15,30 +15,28 @@ export default class Header extends React.Component { return ( -
- - - - - - Explore - + + + + + + Explore -
+ - - Grant.io - + + Grant.io + - - + + Start a Proposal - - + + {!isTransparent && Alpha} diff --git a/frontend/client/components/Header/styled.ts b/frontend/client/components/Header/styled.ts index 24d6a09b..3138e041 100644 --- a/frontend/client/components/Header/styled.ts +++ b/frontend/client/components/Header/styled.ts @@ -24,44 +24,48 @@ export const Header = styled<{ isTransparent: boolean }, 'header'>('header')` box-shadow: ${(p: any) => (p.isTransparent ? 'none' : '0 1px 2px rgba(0, 0, 0, 0.3)')}; `; -export const Title = styled.a` - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - font-size: 2.2rem; - margin: 0; - color: inherit; - letter-spacing: 0.08rem; - font-weight: 500; - transition: transform 100ms ease; - flex-grow: 1; - text-align: center; - - &:hover, - &:focus, - &:active { +export const Title = styled.span` + a { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 2.2rem; + margin: 0; color: inherit; - transform: translateY(-2px) translate(-50%, -50%); + letter-spacing: 0.08rem; + font-weight: 500; + transition: transform 100ms ease; + flex-grow: 1; + text-align: center; + + &:hover, + &:focus, + &:active { + color: inherit; + transform: translateY(-2px) translate(-50%, -50%); + } } `; -export const Button = styled.a` - display: block; - background: none; - padding: 0; - font-size: 1.2rem; - font-weight: 300; - color: inherit; - letter-spacing: 0.05rem; - cursor: pointer; - transition: transform 100ms ease; - - &:hover, - &:focus, - &:active { - transform: translateY(-1px); +export const Button = styled.div` + a { + display: block; + background: none; + padding: 0; + font-size: 1.2rem; + font-weight: 300; color: inherit; + letter-spacing: 0.05rem; + cursor: pointer; + transition: transform 100ms ease; + + &:hover, + &:focus, + &:active { + transform: translateY(-1px); + color: inherit; + } } `; diff --git a/frontend/client/components/Home/index.tsx b/frontend/client/components/Home/index.tsx index 2e3f67eb..22fde7ec 100644 --- a/frontend/client/components/Home/index.tsx +++ b/frontend/client/components/Home/index.tsx @@ -1,26 +1,33 @@ import React from 'react'; import * as Styled from './styled'; -import Link from 'next/link'; +import { Redirect } from 'react-router-dom'; import { Icon } from 'antd'; import AntWrap from 'components/AntWrap'; +import TeamsSvg from 'static/images/intro-teams.svg'; +import FundingSvg from 'static/images/intro-funding.svg'; +import CommunitySvg from 'static/images/intro-community.svg'; const introBlobs = [ { - image: 'static/images/intro-teams.svg', + Svg: TeamsSvg, text: 'Developers and teams propose projects for improving the ecosystem', }, { - image: 'static/images/intro-funding.svg', + Svg: FundingSvg, text: 'Projects are funded by the community and paid as it’s built', }, { - image: 'static/images/intro-community.svg', + Svg: CommunitySvg, text: 'Open discussion and project updates bring devs and the community together', }, ]; export default class Home extends React.Component { + state = { redirect: '' }; render() { + if (this.state.redirect) { + return ; + } return ( @@ -29,12 +36,15 @@ export default class Home extends React.Component { - - Propose a Project - - - Explore Projects - + this.setState({ redirect: '/create' })} + isPrimary + > + Propose a Project + + this.setState({ redirect: '/proposals' })}> + Explore Projects + @@ -52,7 +62,7 @@ export default class Home extends React.Component { {introBlobs.map((blob, i) => ( - +

{blob.text}

))} diff --git a/frontend/client/components/Home/styled.ts b/frontend/client/components/Home/styled.ts index 47b0fb93..71c405a9 100644 --- a/frontend/client/components/Home/styled.ts +++ b/frontend/client/components/Home/styled.ts @@ -142,7 +142,7 @@ export const IntroBlob = styled.div` max-width: 320px; } - img { + svg { margin-bottom: 1rem; opacity: 0.75; height: 100px; diff --git a/frontend/client/components/Proposal/CampaignBlock/index.tsx b/frontend/client/components/Proposal/CampaignBlock/index.tsx index fa6fbe04..3873a00d 100644 --- a/frontend/client/components/Proposal/CampaignBlock/index.tsx +++ b/frontend/client/components/Proposal/CampaignBlock/index.tsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { AppState } from 'store/reducers'; import { web3Actions } from 'modules/web3'; -import { withRouter } from 'next/router'; +import { withRouter } from 'react-router'; import Web3Container, { Web3RenderProps } from 'lib/Web3Container'; import ShortAddress from 'components/ShortAddress'; import UnitDisplay from 'components/UnitDisplay'; diff --git a/frontend/client/components/Proposal/index.tsx b/frontend/client/components/Proposal/index.tsx index a4f3a060..8573a514 100644 --- a/frontend/client/components/Proposal/index.tsx +++ b/frontend/client/components/Proposal/index.tsx @@ -18,7 +18,7 @@ import GovernanceTab from './Governance'; import ContributorsTab from './Contributors'; // import CommunityTab from './Community'; import * as Styled from './styled'; -import { withRouter } from 'next/router'; +import { withRouter } from 'react-router'; import Web3Container from 'lib/Web3Container'; import { web3Actions } from 'modules/web3'; diff --git a/frontend/client/components/Proposals/ProposalCard/index.tsx b/frontend/client/components/Proposals/ProposalCard/index.tsx index e9f74e8a..73ae6bbe 100644 --- a/frontend/client/components/Proposals/ProposalCard/index.tsx +++ b/frontend/client/components/Proposals/ProposalCard/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Progress, Icon, Spin } from 'antd'; import moment from 'moment'; -import Link from 'next/link'; +import { Redirect } from 'react-router-dom'; import { CATEGORY_UI } from 'api/constants'; import { ProposalWithCrowdFund } from 'modules/proposals/reducers'; import * as Styled from './styled'; @@ -18,7 +18,11 @@ interface Props extends ProposalWithCrowdFund { } class ProposalCard extends React.Component { + state = { redirect: '' }; render() { + if (this.state.redirect) { + return ; + } const { title, proposalId, category, dateCreated, web3, crowdFund } = this.props; const team = [...this.props.team].reverse(); @@ -26,48 +30,47 @@ class ProposalCard extends React.Component { return ; } else { return ( - - - {title} - - - {' '} - raised of{' '} - goal - - = 100}> - {crowdFund.percentFunded}% - - - = 100 ? 'success' : 'active'} - showInfo={false} - /> + this.setState({ redirect: `/proposals/${proposalId}` })} + > + {title} + + + raised{' '} + of goal + + = 100}> + {crowdFund.percentFunded}% + + + = 100 ? 'success' : 'active'} + showInfo={false} + /> - - - {team[0].accountAddress}{' '} - {team.length > 1 && +{team.length - 1} other} - - - {team.reverse().map(u => ( - - ))} - - - {proposalId} + + + {team[0].accountAddress}{' '} + {team.length > 1 && +{team.length - 1} other} + + + {team.reverse().map(u => ( + + ))} + + + {proposalId} - - - {CATEGORY_UI[category].label} - - - {moment(dateCreated * 1000).fromNow()} - - - - + + + {CATEGORY_UI[category].label} + + + {moment(dateCreated * 1000).fromNow()} + + + ); } } diff --git a/frontend/client/index.tsx b/frontend/client/index.tsx new file mode 100644 index 00000000..02772ddd --- /dev/null +++ b/frontend/client/index.tsx @@ -0,0 +1,25 @@ +import '@babel/polyfill'; +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { hydrate } from 'react-dom'; +import { loadComponents } from 'loadable-components'; +import { Provider } from 'react-redux'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { configureStore } from 'store/configure'; +import Routes from './Routes'; + +const initialState = window && (window as any).__PRELOADED_STATE__; +const store = configureStore(initialState); + +const App = hot(module)(() => ( + + + + + +)); + +loadComponents().then(() => { + hydrate(, document.getElementById('app')); +}); diff --git a/frontend/client/pages/proposal.tsx b/frontend/client/pages/proposal.tsx index 74a8a7bd..e0619a56 100644 --- a/frontend/client/pages/proposal.tsx +++ b/frontend/client/pages/proposal.tsx @@ -2,16 +2,16 @@ import React, { Component } from 'react'; import Web3Page from 'components/Web3Page'; import Proposal from 'components/Proposal'; -import { WithRouterProps, withRouter } from 'next/router'; +import { withRouter, RouteComponentProps } from 'react-router'; -type RouteProps = WithRouterProps; +type RouteProps = RouteComponentProps; class ProposalPage extends Component { constructor(props: RouteProps) { super(props); } render() { - const proposalId = this.props.router.query.id as string; + const proposalId = this.props.match.params.id; return ( + store.replaceReducer(require('./reducers').default), + ); + } + } return store; } diff --git a/frontend/client/styles/fonts.less b/frontend/client/styles/fonts.less index cb53d9dd..20684179 100644 --- a/frontend/client/styles/fonts.less +++ b/frontend/client/styles/fonts.less @@ -1,17 +1,17 @@ @font-face { font-family: 'Nunito Sans'; font-weight: 400; - src: url('/_next/static/fonts/NunitoSans-Regular.ttf') format('ttf'); + src: url('../static/fonts/NunitoSans-Regular.ttf') format('truetype'); } @font-face { font-family: 'Nunito Sans'; font-weight: 600; - src: url('/_next/static/fonts/NunitoSans-SemiBold.ttf') format('ttf'); + src: url('../static/fonts/NunitoSans-SemiBold.ttf') format('truetype'); } @font-face { font-family: 'Nunito Sans'; font-weight: 300; - src: url('/_next/static/fonts/NunitoSans-Light.ttf') format('ttf'); + src: url('../static/fonts/NunitoSans-Light.ttf') format('truetype'); } diff --git a/frontend/client/typings/express-manifest-helpers.d.ts b/frontend/client/typings/express-manifest-helpers.d.ts new file mode 100644 index 00000000..9c80eb20 --- /dev/null +++ b/frontend/client/typings/express-manifest-helpers.d.ts @@ -0,0 +1 @@ +declare module 'express-manifest-helpers'; diff --git a/frontend/client/typings/loadable-components.d.ts b/frontend/client/typings/loadable-components.d.ts new file mode 100644 index 00000000..37cbe34e --- /dev/null +++ b/frontend/client/typings/loadable-components.d.ts @@ -0,0 +1 @@ +declare module 'loadable-components/server'; diff --git a/frontend/config/env.js b/frontend/config/env.js new file mode 100644 index 00000000..3c9941be --- /dev/null +++ b/frontend/config/env.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +delete require.cache[require.resolve('./paths')]; + +if (!process.env.NODE_ENV) { + throw new Error( + 'The process.env.NODE_ENV environment variable is required but was not specified.', + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${process.env.NODE_ENV}.local`, + `${paths.dotenv}.${process.env.NODE_ENV}`, + process.env.NODE_ENV !== 'test' && `${paths.dotenv}.local`, + paths.dotenv, +].filter(Boolean); + +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv').config({ + path: dotenvFile, + }); + } +}); + +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +module.exports = () => { + const raw = { + PORT: process.env.PORT || 3000, + NODE_ENV: process.env.NODE_ENV || 'development', + BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:5000', + }; + + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +}; diff --git a/frontend/config/paths.js b/frontend/config/paths.js new file mode 100644 index 00000000..51364326 --- /dev/null +++ b/frontend/config/paths.js @@ -0,0 +1,24 @@ +const path = require('path'); +const fs = require('fs'); +const findRoot = require('find-root'); + +const appDirectory = fs.realpathSync(process.cwd()); +// truffle exec cwd moves to called js, so make sure we are on root +const appRoot = findRoot(appDirectory); +const resolveApp = relativePath => path.resolve(appRoot, relativePath); + +const paths = { + clientBuild: resolveApp('build/client'), + contractsBase: resolveApp('../contract'), + contractsBuild: resolveApp('../contract/build/contracts'), + dotenv: resolveApp('.env'), + logs: resolveApp('logs'), + publicPath: '/static/', + serverBuild: resolveApp('build/server'), + srcClient: resolveApp('client'), + srcServer: resolveApp('server'), +}; + +paths.resolveModules = [paths.srcClient, paths.srcServer, 'node_modules']; + +module.exports = paths; diff --git a/frontend/config/webpack.config.js/client.base.js b/frontend/config/webpack.config.js/client.base.js new file mode 100644 index 00000000..9b05bb32 --- /dev/null +++ b/frontend/config/webpack.config.js/client.base.js @@ -0,0 +1,74 @@ +const path = require('path'); +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const paths = require('../paths'); +const { client: clientLoaders } = require('./loaders'); +const resolvers = require('./resolvers'); +const plugins = require('./plugins'); + +const isDev = process.env.NODE_ENV === 'development'; + +module.exports = { + name: 'client', + target: 'web', + entry: { + bundle: [path.join(paths.srcClient, 'index.tsx')], + }, + output: { + path: path.join(paths.clientBuild, paths.publicPath), + filename: 'bundle.js', + publicPath: paths.publicPath, + chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js', + }, + module: { + rules: clientLoaders, + }, + resolve: { ...resolvers }, + plugins: [...plugins.shared, ...plugins.client], + node: { + dgram: 'empty', + fs: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty', + }, + optimization: { + minimizer: [ + new UglifyJsPlugin({ + cache: true, + parallel: true, + sourceMap: true, + uglifyOptions: { + // otherwise BN typecheck gets mangled during minification + keep_fnames: true, + }, + }), + ], + namedModules: true, + noEmitOnErrors: false, + // concatenateModules: true, + // below settings bundle all vendor css in one file + // this allows SSR to render a reference to the hashed css + // if commons is split by module then flickering may occur on load + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: 'vendor', + chunks: 'all', + }, + }, + }, + }, + stats: { + cached: false, + cachedAssets: false, + chunks: false, + chunkModules: false, + colors: true, + hash: false, + modules: false, + reasons: false, + timings: true, + version: false, + }, +}; diff --git a/frontend/config/webpack.config.js/client.dev.js b/frontend/config/webpack.config.js/client.dev.js new file mode 100644 index 00000000..83afbb19 --- /dev/null +++ b/frontend/config/webpack.config.js/client.dev.js @@ -0,0 +1,28 @@ +const baseConfig = require('./client.base'); +const webpack = require('webpack'); +const WriteFileWebpackPlugin = require('write-file-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const config = { + ...baseConfig, + plugins: [ + new WriteFileWebpackPlugin(), + new webpack.HotModuleReplacementPlugin(), + ...baseConfig.plugins, + new ForkTsCheckerWebpackPlugin(), + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: '../../dev-client-bundle-analysis.html', + defaultSizes: 'gzip', + openAnalyzer: false, + }), + ], + mode: 'development', + devtool: 'cheap-module-inline-source-map', + performance: { + hints: false, + }, +}; + +module.exports = config; diff --git a/frontend/config/webpack.config.js/client.prod.js b/frontend/config/webpack.config.js/client.prod.js new file mode 100644 index 00000000..7d1c1e8f --- /dev/null +++ b/frontend/config/webpack.config.js/client.prod.js @@ -0,0 +1,21 @@ +const baseConfig = require('./client.base'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const config = { + ...baseConfig, + plugins: [ + ...baseConfig.plugins, + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: '../../prod-client-bundle-analysis.html', + defaultSizes: 'gzip', + openAnalyzer: false, + }), + ], + mode: 'production', + devtool: 'source-map', +}; + +config.output.filename = 'bundle.[hash:8].js'; + +module.exports = config; diff --git a/frontend/config/webpack.config.js/index.js b/frontend/config/webpack.config.js/index.js new file mode 100644 index 00000000..65ec3574 --- /dev/null +++ b/frontend/config/webpack.config.js/index.js @@ -0,0 +1,8 @@ +module.exports = (env = 'production') => { + if (env === 'development' || env === 'dev') { + process.env.NODE_ENV = 'development'; + return [require('./client.dev'), require('./server.dev')]; + } + process.env.NODE_ENV = 'production'; + return [require('./client.prod'), require('./server.prod')]; +}; diff --git a/frontend/config/webpack.config.js/loaders.js b/frontend/config/webpack.config.js/loaders.js new file mode 100644 index 00000000..9bb315a2 --- /dev/null +++ b/frontend/config/webpack.config.js/loaders.js @@ -0,0 +1,229 @@ +const _ = require('lodash'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const isDev = process.env.NODE_ENV === 'development'; + +const babelPresets = [ + '@babel/react', + // '@babel/typescript', (using ts-loader) + ['@babel/env', { useBuiltIns: 'entry', modules: false }], +]; + +const lessLoader = { + loader: 'less-loader', + options: { javascriptEnabled: true }, +}; + +const tsBabelLoaderClient = { + test: /\.tsx?$/, + use: [ + { + loader: 'babel-loader', + options: { + plugins: [ + ['styled-components', { ssr: true, displayName: true }], + 'dynamic-import-webpack', // for client + 'loadable-components/babel', + 'react-hot-loader/babel', + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-proposal-class-properties', + ['import', { libraryName: 'antd', style: false }], + ], + presets: ['@babel/react', ['@babel/env', { useBuiltIns: 'entry' }]], + }, + }, + { + loader: 'ts-loader', + options: { transpileOnly: isDev }, + }, + ], +}; + +const tsBabelLoaderServer = { + test: /\.tsx?$/, + use: [ + { + loader: 'babel-loader', + options: { + plugins: [ + ['styled-components', { ssr: true, displayName: true }], + 'dynamic-import-node', // for server + 'loadable-components/babel', + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-proposal-class-properties', + ['import', { libraryName: 'antd', style: false }], + ], + presets: [ + '@babel/react', + ['@babel/env', { useBuiltIns: 'entry', targets: { node: 'current' } }], + ], + }, + }, + { + loader: 'ts-loader', + options: { transpileOnly: isDev }, + }, + ], +}; + +const cssLoaderClient = { + test: /\.css$/, + exclude: [/node_modules/], + use: [ + isDev && 'style-loader', + !isDev && MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + }, + ].filter(Boolean), +}; + +const lessLoaderClient = { + test: /\.less$/, + exclude: [/node_modules/], + use: [...cssLoaderClient.use, lessLoader], +}; + +const cssLoaderServer = { + test: /\.css$/, + exclude: [/node_modules/], + use: [ + { + loader: 'css-loader/locals', + }, + ], +}; + +const lessLoaderServer = { + test: /\.less$/, + exclude: [/node_modules/], + use: [...cssLoaderServer.use, lessLoader], +}; + +const urlLoaderClient = { + test: /\.(png|jpe?g|gif)$/, + loader: require.resolve('url-loader'), + options: { + limit: 2048, + name: 'assets/[name].[hash:8].[ext]', + }, +}; + +const urlLoaderServer = { + ...urlLoaderClient, + options: { + ...urlLoaderClient.options, + emitFile: false, + }, +}; + +const fileLoaderClient = { + // WARNING: this will catch all files except those below + exclude: [/\.(js|ts|tsx|css|less|mjs|html|json|ejs)$/], + use: [ + { + loader: 'file-loader', + options: { + name: 'assets/[name].[hash:8].[ext]', + }, + }, + ], +}; + +const fileLoaderServer = _.defaultsDeep( + { + use: [{ options: { emitFile: false } }], + }, + fileLoaderClient, +); + +const svgLoaderClient = { + test: /\.svg$/, + issuer: { + test: /\.tsx?$/, + }, + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [{ inlineStyles: { onlyMatchedOnce: false } }], + }, + }, + }, + ], // svg -> react component +}; + +const svgLoaderServer = svgLoaderClient; + +// Write css files from node_modules to its own vendor.css file +const externalCssLoaderClient = { + test: /\.css$/, + include: [/node_modules/], + use: [ + isDev && 'style-loader', + !isDev && MiniCssExtractPlugin.loader, + 'css-loader', + ].filter(Boolean), +}; + +const externalLessLoaderClient = { + test: /\.less$/, + include: [/node_modules/], + use: [ + isDev && 'style-loader', + !isDev && MiniCssExtractPlugin.loader, + 'css-loader', + lessLoader, + ].filter(Boolean), +}; + +// Server build needs a loader to handle external .css files +const externalCssLoaderServer = { + test: /\.css$/, + include: [/node_modules/], + loader: 'css-loader/locals', +}; + +const externalLessLoaderServer = { + test: /\.less$/, + include: [/node_modules/], + use: ['css-loader/locals', lessLoader], +}; + +const client = [ + { + // oneOf: first matching rule takes all + oneOf: [ + tsBabelLoaderClient, + cssLoaderClient, + lessLoaderClient, + svgLoaderClient, + urlLoaderClient, + fileLoaderClient, + externalCssLoaderClient, + externalLessLoaderClient, + ], + }, +]; + +const server = [ + { + // oneOf: first matching rule takes all + oneOf: [ + tsBabelLoaderServer, + cssLoaderServer, + lessLoaderServer, + svgLoaderServer, + urlLoaderServer, + fileLoaderServer, + externalCssLoaderServer, + externalLessLoaderServer, + ], + }, +]; + +module.exports = { + client, + server, +}; diff --git a/frontend/config/webpack.config.js/module-dependency-warning.js b/frontend/config/webpack.config.js/module-dependency-warning.js new file mode 100644 index 00000000..f1b49560 --- /dev/null +++ b/frontend/config/webpack.config.js/module-dependency-warning.js @@ -0,0 +1,24 @@ +const ModuleDependencyWarning = require('webpack/lib/ModuleDependencyWarning'); + +// supress unfortunate warnings due to transpileOnly=true and certain ts export patterns +// https://github.com/TypeStrong/ts-loader/issues/653#issuecomment-390889335 +// https://github.com/TypeStrong/ts-loader/issues/751 + +module.exports = class IgnoreNotFoundExportPlugin { + apply(compiler) { + const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/; + function doneHook(stats) { + stats.compilation.warnings = stats.compilation.warnings.filter(function(warn) { + if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) { + return false; + } + return true; + }); + } + if (compiler.hooks) { + compiler.hooks.done.tap('IgnoreNotFoundExportPlugin', doneHook); + } else { + compiler.plugin('done', doneHook); + } + } +}; diff --git a/frontend/config/webpack.config.js/plugins.js b/frontend/config/webpack.config.js/plugins.js new file mode 100644 index 00000000..d138ae3a --- /dev/null +++ b/frontend/config/webpack.config.js/plugins.js @@ -0,0 +1,60 @@ +const webpack = require('webpack'); +const ManifestPlugin = require('webpack-manifest-plugin'); +const { StatsWriterPlugin } = require('webpack-stats-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const ModuleDependencyWarning = require('./module-dependency-warning'); + +const env = require('../env')(); + +const shared = [new ModuleDependencyWarning()]; + +const client = [ + new webpack.DefinePlugin(env.stringified), + new webpack.DefinePlugin({ + __SERVER__: 'false', + __CLIENT__: 'true', + }), + new MiniCssExtractPlugin({ + filename: + process.env.NODE_ENV === 'development' ? '[name].css' : '[name].[hash:8].css', + chunkFilename: + process.env.NODE_ENV === 'development' + ? '[name].chunk.css' + : '[name].[chunkhash:8].chunk.css', + }), + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + new ManifestPlugin({ + fileName: 'manifest.json', + writeToFileEmit: true, // fixes initial-only writing from WriteFileWebpackPlugin + }), + new StatsWriterPlugin({ + fileName: 'stats.json', + fields: null, + transform(data) { + const trans = {}; + trans.modules = data.modules.map(m => ({ + id: m.id, + chunks: m.chunks, + reasons: m.reasons, + })); + trans.chunks = data.chunks.map(c => ({ + id: c.id, + files: c.files, + })); + return JSON.stringify(trans, null, 2); + }, + }), +]; + +const server = [ + new webpack.DefinePlugin({ + __SERVER__: 'true', + __CLIENT__: 'false', + }), +]; + +module.exports = { + shared, + client, + server, +}; diff --git a/frontend/config/webpack.config.js/resolvers.js b/frontend/config/webpack.config.js/resolvers.js new file mode 100644 index 00000000..23c9d2bb --- /dev/null +++ b/frontend/config/webpack.config.js/resolvers.js @@ -0,0 +1,20 @@ +const paths = require('../paths'); + +module.exports = { + extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'], + modules: paths.resolveModules, + // tsconfig.compilerOptions.paths should sync with these + alias: { + contracts: paths.contractsBuild, // truffle build contracts dir + api: `${paths.srcClient}/api`, + components: `${paths.srcClient}/components`, + lib: `${paths.srcClient}/lib`, + modules: `${paths.srcClient}/modules`, + pages: `${paths.srcClient}/pages`, + store: `${paths.srcClient}/store`, + styles: `${paths.srcClient}/styles`, + typings: `${paths.srcClient}/typings`, + utils: `${paths.srcClient}/utils`, + web3interact: `${paths.srcClient}/web3interact`, + }, +}; diff --git a/frontend/config/webpack.config.js/server.base.js b/frontend/config/webpack.config.js/server.base.js new file mode 100644 index 00000000..25fdd9bf --- /dev/null +++ b/frontend/config/webpack.config.js/server.base.js @@ -0,0 +1,36 @@ +const path = require('path'); +const nodeExternals = require('webpack-node-externals'); + +const paths = require('../paths'); +const { server: serverLoaders } = require('./loaders'); +const resolvers = require('./resolvers'); +const plugins = require('./plugins'); + +module.exports = { + name: 'server', + target: 'node', + entry: { + server: [path.join(paths.srcServer, 'index.tsx')], + }, + externals: [ + nodeExternals({ + // we still want imported css from external files to be bundled otherwise 3rd party packages + // which require us to include their own css would not work properly + whitelist: [/\.css$/, /^antd.*style$/], + }), + ], + output: { + path: paths.serverBuild, + filename: 'server.js', + publicPath: paths.publicPath, + // libraryTarget: 'commonjs2', + }, + resolve: { ...resolvers }, + module: { + rules: serverLoaders, + }, + plugins: [...plugins.shared, ...plugins.server], + stats: { + colors: true, + }, +}; diff --git a/frontend/config/webpack.config.js/server.dev.js b/frontend/config/webpack.config.js/server.dev.js new file mode 100644 index 00000000..e7b33a50 --- /dev/null +++ b/frontend/config/webpack.config.js/server.dev.js @@ -0,0 +1,19 @@ +const baseConfig = require('./server.base'); +const webpack = require('webpack'); +const WriteFileWebpackPlugin = require('write-file-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +const config = { + ...baseConfig, + plugins: [ + new WriteFileWebpackPlugin(), + ...baseConfig.plugins, + new ForkTsCheckerWebpackPlugin(), + ], + mode: 'development', + performance: { + hints: false, + }, +}; + +module.exports = config; diff --git a/frontend/config/webpack.config.js/server.prod.js b/frontend/config/webpack.config.js/server.prod.js new file mode 100644 index 00000000..8d950fbc --- /dev/null +++ b/frontend/config/webpack.config.js/server.prod.js @@ -0,0 +1,6 @@ +const config = require('./server.base'); + +module.exports = { + ...config, + mode: 'production', +}; diff --git a/frontend/logs/.gitkeep b/frontend/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/package.json b/frontend/package.json index 02eec4b3..01a90670 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,10 +5,10 @@ "license": "MIT", "scripts": { "analyze": "NODE_ENV=production ANALYZE=true next build ./client", - "build": "NODE_ENV=production next build ./client", - "dev": "rm -rf ./node_modules/.cache && cross-env BACKEND_URL=http://localhost:5000 node server.js", + "build": "cross-env NODE_ENV=production node bin/build.js", + "dev": "rm -rf ./node_modules/.cache && cross-env NODE_ENV=development BACKEND_URL=http://localhost:5000 node bin/dev.js", "lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\"", - "start": "NODE_ENV=production node server.js", + "start": "NODE_ENV=production node build/server/server.js", "tsc": "tsc", "link-contracts": "cd client/lib && ln -s ../../build/contracts contracts", "ganache": "ganache-cli -b 5", @@ -27,7 +27,25 @@ ] }, "dependencies": { + "@babel/core": "^7.0.1", + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/polyfill": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "@babel/preset-typescript": "^7.0.0", + "@babel/register": "^7.0.0", + "@svgr/webpack": "^2.4.0", + "@types/classnames": "^2.2.6", + "@types/cors": "^2.8.4", + "@types/dotenv": "^4.0.3", + "@types/express": "^4.16.0", + "@types/express-winston": "^3.0.0", "@types/headroom": "^0.7.31", + "@types/history": "^4.7.0", + "@types/i18next": "^8.4.5", "@types/js-cookie": "2.1.0", "@types/jwt-decode": "^2.2.1", "@types/lodash": "^4.14.112", @@ -39,8 +57,17 @@ "@types/react": "16.3.16", "@types/react-document-title": "^2.0.2", "@types/react-dom": "^16.0.6", + "@types/react-helmet": "^5.0.7", + "@types/react-i18next": "^7.8.2", "@types/react-redux": "^6.0.2", + "@types/react-router": "^4.0.31", + "@types/react-router-dom": "^4.3.1", "@types/recompose": "^0.26.1", + "@types/redux-actions": "^2.3.0", + "@types/redux-logger": "^3.0.6", + "@types/webpack": "^4.4.11", + "@types/webpack-env": "^1.13.6", + "@types/winston": "^2.4.4", "@zeit/next-css": "^0.2.0", "@zeit/next-less": "^0.3.0", "@zeit/next-sass": "^0.2.0", @@ -48,6 +75,10 @@ "ant-design-pro": "^2.0.0-beta.2", "antd": "^3.7.1", "axios": "^0.18.0", + "babel-core": "^6.26.3", + "babel-loader": "^8.0.2", + "babel-plugin-dynamic-import-node": "^2.1.0", + "babel-plugin-dynamic-import-webpack": "^1.0.2", "babel-plugin-import": "^1.8.0", "babel-plugin-inline-dotenv": "^1.1.2", "babel-plugin-module-resolver": "^3.1.1", @@ -56,26 +87,37 @@ "babel-register": "^6.26.0", "babel-runtime": "^6.26.0", "bn.js": "4.11.8", + "body-parser": "^1.18.3", + "chalk": "^2.4.1", "cookie-parser": "^1.4.3", + "cors": "^2.8.4", "cross-env": "^5.2.0", - "dotenv": "6.0.0", + "css-loader": "^1.0.0", + "dotenv": "^6.0.0", "drizzle": "^1.2.2", "ethereum-blockies-base64": "1.0.2", "ethereumjs-util": "5.2.0", "express": "^4.16.3", - "file-loader": "^1.1.11", + "express-manifest-helpers": "^0.5.0", + "express-winston": "^3.0.0", + "file-loader": "^2.0.0", + "find-root": "^1.1.0", "font-awesome": "^4.7.0", "fork-ts-checker-webpack-plugin": "^0.4.2", "fs-extra": "^7.0.0", "http-proxy-middleware": "^0.18.0", "husky": "^1.0.0-rc.8", + "i18next": "^11.9.0", "is-mobile": "^1.0.0", "isomorphic-unfetch": "^2.0.0", "js-cookie": "^2.2.0", "jwt-decode": "^2.2.0", "less": "^3.7.1", - "lint-staged": "^7.1.3", + "less-loader": "^4.1.0", + "lint-staged": "^7.2.2", + "loadable-components": "^2.2.3", "lodash": "^4.17.10", + "mini-css-extract-plugin": "^0.4.2", "moment": "^2.22.2", "next": "^6.1.1", "next-compose-plugins": "^2.1.1", @@ -84,6 +126,7 @@ "next-redux-wrapper": "^2.0.0-beta.6", "next-routes": "^1.4.2", "node-sass": "^4.9.2", + "nodemon": "^1.18.4", "nprogress": "^0.2.0", "openzeppelin-solidity": "^1.12.0", "prettier": "^1.13.4", @@ -92,11 +135,17 @@ "rc-scroll-anim": "^2.0.2", "rc-tween-one": "^1.5.5", "react": "^16.4.0", + "react-dev-utils": "^5.0.2", "react-document-title": "^2.0.3", "react-dom": "^16.4.0", "react-headroom": "^2.2.2", + "react-helmet": "^5.2.0", + "react-hot-loader": "^4.3.8", + "react-i18next": "^7.12.0", "react-mde": "^5.8.0", "react-redux": "^5.0.7", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", "recompose": "^0.27.1", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.2", @@ -107,17 +156,30 @@ "showdown": "^1.8.6", "slate": "^0.37.3", "slate-react": "^0.15.4", - "styled-components": "^3.3.3", + "stats-webpack-plugin": "^0.7.0", + "style-loader": "^0.23.0", + "styled-components": "^3.4.6", "truffle-hdwallet-provider": "0.0.6", + "ts-loader": "^5.1.1", "tslint": "^5.10.0", "tslint-config-airbnb": "^5.9.2", "tslint-config-prettier": "^1.13.0", "tslint-eslint-rules": "^5.3.1", "tslint-react": "^3.6.0", "typescript": "3.0.3", - "url-loader": "^1.0.1", + "uglifyjs-webpack-plugin": "^2.0.0", + "url-loader": "^1.1.1", "web3": "^1.0.0-beta.34", - "webpack-bundle-analyzer": "^2.13.1", + "webpack": "^4.19.0", + "webpack-bundle-analyzer": "^3.0.2", + "webpack-cli": "^3.1.0", + "webpack-dev-server": "^3.1.8", + "webpack-hot-middleware": "^2.24.0", + "webpack-manifest-plugin": "^2.0.4", + "webpack-node-externals": "^1.7.2", + "webpack-stats-plugin": "^0.2.1", + "winston": "^3.1.0", + "write-file-webpack-plugin": "^4.4.0", "xss": "1.0.3" }, "devDependencies": { diff --git a/frontend/server/components/HTML.tsx b/frontend/server/components/HTML.tsx new file mode 100644 index 00000000..49e47cea --- /dev/null +++ b/frontend/server/components/HTML.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; + +export interface Props { + children: any; + css: string[]; + scripts: string[]; + state: string; + loadableStateScript: string; + styleElements: any; +} + +export default class HTML extends React.Component { + render() { + const head = Helmet.renderStatic(); + const { + children, + scripts, + css, + state, + loadableStateScript, + styleElements, + } = this.props; + return ( + + + + + {/* TODO: import from @fortawesome */} + + {head.base.toComponent()} + {head.title.toComponent()} + {head.meta.toComponent()} + {head.link.toComponent()} + {head.script.toComponent()} + {css.map(href => { + return ; + })} + {styleElements.map((styleEl: any) => styleEl)} +