add server rendering of loaded compares and clean up saving of compares, switch to using isomorphic fetch (and save much compiled size)

This commit is contained in:
Adam Brown 2016-12-14 19:06:14 -05:00
parent 0a3a37e64e
commit 6bb76ccd17
17 changed files with 2456 additions and 19660 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@ data/*
browser-bundle.js.map browser-bundle.js.map
npm-debug.log.* npm-debug.log.*
stats.json
stats.analyzed.txt

21781
dist/browser-bundle.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "src/server/babel.index.js", "main": "src/server/babel.index.js",
"scripts": { "scripts": {
"copy-css": "copyfiles -f ./node_modules/semantic-ui-css/semantic.min.css ./dist", "copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist",
"build": "npm run copy-css && webpack --colors", "build": "npm run copy-css && webpack --colors",
"start": "npm run copy-css && webpack --progress --colors --watch", "start": "npm run copy-css && webpack --progress --colors --watch",
"serve": "node src/server/babel.index.js", "serve": "node src/server/babel.index.js",
@ -20,15 +20,15 @@
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"diff": "^3.0.1", "diff": "^3.0.1",
"express": "^4.14.0", "express": "^4.14.0",
"isomorphic-fetch": "^2.2.1",
"jsonfile": "^2.4.0", "jsonfile": "^2.4.0",
"markdown-it": "^5.1.0", "markdown-it": "^5.1.0",
"markdown-to-jsx": "^4.0.3", "markdown-to-jsx": "^4.0.3",
"react": "^0.14.5", "react": "^0.14.5",
"react-dom": "^0.14.5", "react-dom": "^0.14.5",
"react-redux": "^4.4.6", "react-redux": "^4.4.6",
"react-router": "^1.0.0", "react-router": "~3.0.0",
"redux": "^3.5.1", "redux": "^3.5.1",
"redux-router": "^1.0.0-beta5",
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"request": "^2.79.0", "request": "^2.79.0",
"request-promise-native": "^1.0.3", "request-promise-native": "^1.0.3",

View File

@ -5,8 +5,8 @@ import * as Redux from 'redux'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import createBrowserHistory from 'history/lib/createBrowserHistory' //import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, Route, IndexRoute, Redirect } from 'react-router' import {Router, Route, IndexRoute, Redirect, browserHistory } from 'react-router'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
@ -38,8 +38,11 @@ const store = Redux.createStore(
Redux.applyMiddleware(...middlewares) Redux.applyMiddleware(...middlewares)
) )
console.log(store) //this way of reading local input isn't working:
// it's just overriding what comes from the server
// and it's not respecting the comparison that is loaded from the server
/*
const localInput = localStore.get('dubdiff') const localInput = localStore.get('dubdiff')
if (localInput.input) { if (localInput.input) {
//dispatch localStore data to store //dispatch localStore data to store
@ -47,6 +50,7 @@ if (localInput.input) {
store.dispatch(Actions.updateFinalInput(localInput.input.final)) store.dispatch(Actions.updateFinalInput(localInput.input.final))
//should this be done after the first render? //should this be done after the first render?
} }
*/
//save the state whenever the state changes //save the state whenever the state changes
function saveState() { function saveState() {
@ -60,7 +64,7 @@ store.subscribe(saveState)
function render() { function render() {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<Router history={createBrowserHistory()}> <Router history={browserHistory}>
{routes} {routes}
</Router> </Router>
</Provider> </Provider>

View File

@ -1,4 +1,4 @@
import requestPromise from 'request-promise-native' import fetch from 'isomorphic-fetch'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
export const updateOriginalInput = (text) => export const updateOriginalInput = (text) =>
@ -24,43 +24,82 @@ export const clearInput = () =>
dispatch({ type: 'SAVE_STATUS_EMPTY'}) dispatch({ type: 'SAVE_STATUS_EMPTY'})
} }
export const updateOriginalCompare = (text) => ({ type: 'UPDATE_ORIGINAL_COMPARE', data:text})
export const updateFinalCompare = (text) => ({ type: 'UPDATE_FINAL_COMPARE', data:text})
export const clearCompare = () => ({ type: 'CLEAR_COMPARE'})
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT'}) export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT'})
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT'}) export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT'})
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL'}) export const showOriginal = () => ({ type: 'SHOW_ORIGINAL'})
export const showFinal = () => ({ type: 'SHOW_FINAL'}) export const showFinal = () => ({ type: 'SHOW_FINAL'})
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE'}) export const showDifference = () => ({ type: 'SHOW_DIFFERENCE'})
//saves the current input fields to the server
//creates and returns a new id for the
export const save = () => export const save = () =>
(dispatch, getState) => { (dispatch, getState) => {
console.log("!!! SAVING")
//generate an id //generate an id
const id = uuid() const id = uuid()
dispatch( {type: 'SAVE_STATUS_ASSIGN_ID', id})
//set waiting state //set waiting state
dispatch( {type: 'SAVE_STATUS_WAITING'}) dispatch( {type: 'SAVE_STATUS_WAITING'})
const reqOptions = { const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'POST', method: 'POST',
uri: `${location.origin}/api/compare/${id}`, body: JSON.stringify({
body: {
a: getState().input.original, a: getState().input.original,
b: getState().input.final b: getState().input.final
}),
headers: {
"Content-Type": "application/json"
}, },
json: true
} }
//dispatch post request //dispatch post request
requestPromise(reqOptions) fetch(endpointUri, fetchOptions)
.then(returnBodyJson => { .then(response => {
dispatch( {type: 'SAVE_STATUS_SAVED'}) dispatch( {type: 'SAVE_STATUS_SAVED'})
}) })
.catch(error => { .catch(error => {
dispatch( {type: 'SAVE_STATUS_FAILED', error}) dispatch( {type: 'SAVE_STATUS_FAILED', error})
}) })
//return the id after the request has been sent
return id;
} }
/*
const load = (id) =>
(dispatch, getState) => {
//set waiting state
dispatch( {type: 'SAVE_STATUS_WAITING'})
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'GET'
}
//dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => response.json())
.then(json => {
dispatch( {type: 'UPDATE_ORIGINAL_INPUT', data:json.a})
dispatch( {type: 'UPDATE_FINAL_INPUT', data:json.b})
dispatch( {type: 'LOAD_STATUS_LOADED'})
})
.catch(error => {
dispatch( {type: 'LOAD_STATUS_FAILED', error})
})
//return the id after the request has been sent
return id;
}
export const loadIfNeeded = (id) =>
(dispatch, getState) => {
if
}
*/

View File

@ -18,19 +18,24 @@ const mapStateToProps = (state) => ({
isShowOriginal: Selectors.isShowOriginal(state), isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state), isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state), isShowDifference: Selectors.isShowDifference(state),
compare: state.compare, safeInput: Selectors.safeInput(state),
diff: Selectors.diff(state) diff: Selectors.diff(state)
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
//loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
}) })
class Compare extends React.Component { class Compare extends React.Component {
/*
componentDidMount() {
this.props.loadIfNeeded(this.props.routeParams.compareId)
}
*/
render() { render() {
console.log({isMarkdownFormat: this.props.isMarkdownFormat, isShowDifference: this.props.isShowDifference})
return ( return (
<div> <div>
<Header/> <Header/>
@ -49,11 +54,11 @@ class Compare extends React.Component {
<ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>: <ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>:
(!this.props.isMarkdownFormat && !this.props.isShowDifference) ? (!this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowPlaintext <ShowPlaintext
text={this.props.isShowOriginal? this.props.compare.original: this.props.compare.final} text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> : /> :
(this.props.isMarkdownFormat && !this.props.isShowDifference) ? (this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowMarkdown <ShowMarkdown
text={this.props.isShowOriginal? this.props.compare.original: this.props.compare.final} text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> : /> :
null null
} }

View File

@ -12,7 +12,6 @@ const mapStateToProps = (state) => ({
isShowOriginal: Selectors.isShowOriginal(state), isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state), isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state), isShowDifference: Selectors.isShowDifference(state),
compare: state.compare
}) })
@ -22,17 +21,14 @@ const mapDispatchToProps = dispatch => ({
onShowOriginal: () => dispatch(Actions.showOriginal()), onShowOriginal: () => dispatch(Actions.showOriginal()),
onShowFinal: () => dispatch(Actions.showFinal()), onShowFinal: () => dispatch(Actions.showFinal()),
onShowDifference: () => dispatch(Actions.showDifference()), onShowDifference: () => dispatch(Actions.showDifference()),
onEdit: (compare) => { onEdit: () => {
dispatch(Actions.updateOriginalInput(compare.original))
dispatch(Actions.updateFinalInput(compare.final))
dispatch(Actions.clearCompare())
} }
}) })
class CompareControls extends React.Component { class CompareControls extends React.Component {
onClickEdit() { onClickEdit() {
this.props.onEdit(this.props.compare) this.props.onEdit()
} }
onClickMarkdownFormat() { onClickMarkdownFormat() {

View File

@ -1,10 +1,11 @@
import React from 'react' import React from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {Segment, Header} from 'semantic-ui-react' import {Segment, Header, Rail, Container} from 'semantic-ui-react'
import {Link} from 'react-router' import {Link} from 'react-router'
import * as Actions from '../actions' import * as Actions from '../actions'
import SaveStatus from './SaveStatus'
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
}) })
@ -18,8 +19,20 @@ const mapDispatchToProps = dispatch => ({
}) })
const SiteHeader = (props) => ( const SiteHeader = (props) => (
<Segment basic >
<Segment basic padded textAlign="center" as="header" id='masthead'> <Segment basic padded textAlign="center" as="header" id='masthead'>
<Link to="/"><Header onClick={props.onClear}>dubdiff</Header></Link> <Header><Link onClick={props.onClear} to="/">dubdiff</Link></Header>
</Segment>
<Rail internal position="right">
<Segment basic padded>
<SaveStatus/>
</Segment>
</Rail>
</Segment> </Segment>
) )

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {Link} from 'react-router' import {browserHistory} from 'react-router'
import {Button, Icon, Segment} from 'semantic-ui-react' import {Button, Icon, Segment} from 'semantic-ui-react'
@ -10,27 +10,30 @@ import * as Selectors from '../selectors'
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
format: state.format, format: state.format,
isMarkdownFormat: Selectors.isMarkdownFormat(state), isMarkdownFormat: Selectors.isMarkdownFormat(state),
safeInput: Selectors.safeInput(state) saveStatus: state.saveStatus
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()), onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()), onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
onCompare: (safeInput) => {
dispatch(Actions.save()) //returns an id for the record to be saved
dispatch(Actions.updateOriginalCompare(safeInput.original)) startSaveAsync: () => {
dispatch(Actions.updateFinalCompare(safeInput.final)) return dispatch(Actions.save())
} }
}) })
class MainControls extends React.Component { class MainControls extends React.Component {
onClickCompare() { onClickCompare() {
//generate new id? (or should the id be baked into the link route?) //start saving the input to the server
//post safeInput to db const id = this.props.startSaveAsync()
//we can use the id created by the save method to build a path
const comparePath = `/${id}`
browserHistory.replace(comparePath)
this.props.onCompare(this.props.safeInput)
return false return false
} }
@ -46,7 +49,7 @@ class MainControls extends React.Component {
return ( return (
<Segment.Group> <Segment.Group>
<Segment > <Segment >
<Link to="compare"><Button fluid onClick={this.onClickCompare.bind(this)}>Compare</Button></Link> <Button fluid onClick={this.onClickCompare.bind(this)}>Compare</Button>
</Segment> </Segment>
<Segment > <Segment >

View File

@ -0,0 +1,52 @@
import React from 'react'
import {connect} from 'react-redux'
import { Message, Icon, Button} from 'semantic-ui-react'
import { browserHistory} from 'react-router'
import * as Actions from '../actions'
const mapStateToProps = (state) => ({
saveStatus: state.saveStatus
})
const mapDispatchToProps = dispatch => ({
retrySave: () => dispatch(Actions.save())
})
const onRetrySaveClick = (props) => {
//we can use the id created by the save method to build a path
const id = props.retrySave()
const comparePath = `/${id}`
browserHistory.replace(comparePath)
return false
}
const SaveStatus = (props) => {
if (props.saveStatus.waiting) return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Saving diff</Message.Header>
</Message.Content>
</Message>
)
else if (props.saveStatus.failed) return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error saving diff</Message.Header>
The server returned {props.saveStatus.error.message}.
<Button onClick={()=>onRetrySaveClick(props)}>Retry</Button>
</Message.Content>
</Message>
)
else return ( <div></div> )
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)

10
src/common/constants.js Normal file
View File

@ -0,0 +1,10 @@
export const Format = {
PLAINTEXT: 'PLAINTEXT',
MARKDOWN: 'MARKDOWN'
}
export const Show = {
ORIGINAL:'ORIGINAL',
FINAL:'FINAL',
DIFFERENCE:'DIFFERENCE'
}

View File

@ -14,20 +14,6 @@ export function input (state, action ) {
} }
} }
export function compare (state, action ) {
switch (action.type) {
case 'UPDATE_ORIGINAL_COMPARE':
return Object.assign({}, state, {original:action.data})
case 'UPDATE_FINAL_COMPARE':
return Object.assign({}, state, {final:action.data})
case 'CLEAR_COMPARE':
return {original:'', final:''}
default:
return state || {original:'', final:''}
}
}
export function format (state, action) { export function format (state, action) {
switch (action.type) { switch (action.type) {
case 'SET_PLAINTEXT_FORMAT': case 'SET_PLAINTEXT_FORMAT':
@ -57,18 +43,30 @@ export function show (state, action) {
export function saveStatus (state, action) { export function saveStatus (state, action) {
switch (action.type) { switch (action.type) {
case 'SAVE_STATUS_DIRTY': case 'SAVE_STATUS_DIRTY':
return {dirty:true, id:null} return {dirty: true}
case 'SAVE_STATUS_EMPTY': case 'SAVE_STATUS_EMPTY':
return {dirty:false, id:null} return {dirty: false, empty: true}
case 'SAVE_STATUS_SAVED': case 'SAVE_STATUS_SAVED':
return Object.assign({}, state, {waiting: false, dirty:false, failed: false, error:null}) return {dirty: false, saved: true}
case 'SAVE_STATUS_FAILED' : case 'SAVE_STATUS_FAILED' :
return Object.assign({}, state, {waiting: false, failed: true, error: action.error}) return Object.assign({}, state, {waiting: false, failed: true, error: action.error})
case 'SAVE_STATUS_WAITING' : case 'SAVE_STATUS_WAITING' :
return Object.assign({}, state, {waiting: true, failed: false, error: null}) return Object.assign({}, state, {waiting: true, failed: false, error: null})
case 'SAVE_STATUS_ASSIGN_ID':
return Object.assign({}, state, {id: action.id})
default: default:
return state || {empty: true, dirty:false, id:null} return state || {empty: true, dirty:false}
} }
} }
/*
export function loadStatus (state, action) {
switch (action.type) {
case 'LOAD_STATUS_WAITING':
return {waiting: true}
case 'LOAD_STATUS_FAILED':
return {failed: true, error: action.error }
case 'LOAD_STATUS_LOADED':
return {loaded: true}
default:
return state || {waiting: false}
}
}
*/

View File

@ -13,7 +13,6 @@ import * as Dubdiff from './util/dubdiff'
const input = (state) => state.input const input = (state) => state.input
const format = (state) => state.format const format = (state) => state.format
const show = (state) => state.show const show = (state) => state.show
const compare = (state) => state.compare
export const safeInput = createSelector( export const safeInput = createSelector(
[input], [input],
@ -43,9 +42,9 @@ export const isShowDifference= isShow(Show.DIFFERENCE)
export const diff = createSelector( export const diff = createSelector(
[format, compare], [format, safeInput],
(format, compare) => { (format, safeInput) => {
return Dubdiff.plaintextDiff(compare.original, compare.final) return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
/* /*
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' ')) let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' '))
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => ( return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => (

View File

@ -31,7 +31,6 @@ function createComparisonWithId(req, res) {
const id = req.params.id const id = req.params.id
const {a, b} = req.body const {a, b} = req.body
return writeRecord(res, id, {a, b, id}) return writeRecord(res, id, {a, b, id})
} }
@ -82,12 +81,12 @@ function writeRecord(res, id, data) {
module.exports = router module.exports = router
function handleError(res, err) { function handleError(res, err) {
console.log(err); console.log(err)
return res.send(500, err); return res.send(500, err)
} }
// returns a filename for the given comparison // returns a filename for the given comparison
function fnData (id) { function fnData (id) {
return "./data/" + "id-" + id + ".json"; return `./data/${id}.json`
} }

View File

@ -3,6 +3,8 @@ import path from 'path'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import * as Redux from 'redux' import * as Redux from 'redux'
import fetch from 'isomorphic-fetch'
import comparisonRouter from './comparison' import comparisonRouter from './comparison'
@ -11,6 +13,8 @@ import * as actions from '../common/actions'
import render from './render' import render from './render'
const PORT = 8080
const app = express() const app = express()
//serve the dist static files at /dist //serve the dist static files at /dist
@ -24,27 +28,40 @@ app.use('/api/compare', comparisonRouter);
//the following routes are for server-side rendering of the app //the following routes are for server-side rendering of the app
//eventually, we should render the comparison directly from the server //we should render the comparison directly from the server
/*
//this is garbage, we should use a robust method for loading comparisons, parallel to how saving works
//comparisons should be loaded isomorphically
app.route('/:comparisonId') app.route('/:comparisonId')
.get((req, res) => { .get((req, res) => {
const store = createSessionStore() const store = createSessionStore()
...
//fetch the comparison
fetchComparison(req.params.comparisonId)
.then( ({a,b}) => {
store.dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: a})
store.dispatch({type: 'UPDATE_FINAL_INPUT', data: b})
render(store, req, res)
})
.catch( error => {
//... what to do here?
console.log(`Error fetching comparison with id ${req.params.comparisonId}`, error)
})
}) })
app.route('/'). ... app.route('/')
*/ .get((req, res) => {
render(createSessionStore(), req, res)
})
//but for now, let's just render the app and let it fetch comparison data app.listen(PORT, function () {
app.use((req, res) => render(createSessionStore(), req, res))
app.listen(8080, function () {
console.log('Server listening on port 8080.') console.log('Server listening on port 8080.')
}) })
//this is pretty much redundant at this point //creates the session store
function createSessionStore() { function createSessionStore() {
//create the redux store //create the redux store
return Redux.createStore( return Redux.createStore(
@ -53,7 +70,16 @@ function createSessionStore() {
} }
function fetchComparison(id) {
const endpointUri = `http://localhost:${PORT}/api/compare/${id}`
const fetchOptions = {
method: 'GET'
}
//dispatch post request
return fetch(endpointUri, fetchOptions)
.then(response => response.json())
}
//router.get('/', controller.index); //router.get('/', controller.index);

View File

@ -1,18 +1,17 @@
import React from 'react' import React from 'react'
import { renderToString } from 'react-dom/server' import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { match, RoutingContext } from 'react-router' import { match, RouterContext } from 'react-router'
import routes from '../common/routes.js' import routes from '../common/routes.js'
export default function render(store, req, res) { export default function render(store, req, res) {
console.log(store.getState())
// Send the rendered page back to the client // Send the rendered page back to the client
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) { if (error) {
res.status(500).send(renderError('Routing Error', error.message)) console.log(error)
res.status(500).send(renderError('Routing Error:', error.message))
} else if (redirectLocation) { } else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search) res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) { } else if (renderProps) {
@ -20,7 +19,7 @@ export default function render(store, req, res) {
try { try {
const html = renderToString( const html = renderToString(
<Provider store={store}> <Provider store={store}>
<RoutingContext {...renderProps} /> <RouterContext {...renderProps} />
</Provider> </Provider>
) )
@ -30,6 +29,7 @@ export default function render(store, req, res) {
res.status(200).send(appTemplate(html, initialState)) res.status(200).send(appTemplate(html, initialState))
} }
catch(ex) { catch(ex) {
console.log("Render Exception:",ex)
res.status(500).send(errorTemplate('Render Exception', ex.message, ex)) res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
} }