├── README.md ├── api ├── routes.json ├── db.json └── db.json.example ├── ui ├── common │ ├── components │ │ ├── App │ │ │ ├── App.scss │ │ │ ├── App.js │ │ │ ├── Header.scss │ │ │ └── Header.js │ │ ├── Home.scss │ │ ├── index.js │ │ ├── Home.js │ │ └── Pages │ │ │ ├── Show.js │ │ │ ├── Page.js │ │ │ ├── Index.js │ │ │ └── Form.js │ ├── containers │ │ ├── index.js │ │ ├── Pages │ │ │ ├── New.js │ │ │ ├── Index.js │ │ │ ├── Form.js │ │ │ └── Show.js │ │ └── Root.js │ ├── constants │ │ ├── endpoints.js │ │ └── actionTypes.js │ ├── theme │ │ ├── _variables.scss │ │ └── elements.scss │ ├── reducers │ │ ├── index.js │ │ └── pages.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ └── actions │ │ └── page.js ├── config.js ├── server │ ├── index.js │ ├── fetchComponent.js │ ├── server.js │ └── ssr.js ├── .babelrc ├── lib │ └── processSass.js ├── client │ └── index.js └── webpack │ └── webpack.config.js ├── .gitignore └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # series-wiki -------------------------------------------------------------------------------- /api/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/v1/": "/" 3 | } 4 | -------------------------------------------------------------------------------- /ui/common/components/App/App.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-top: 4rem; 3 | } 4 | -------------------------------------------------------------------------------- /ui/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: '127.0.0.1', 3 | apiPort: 5000, 4 | serverPort: 8080, 5 | clientPort: 8081 6 | } 7 | -------------------------------------------------------------------------------- /ui/common/components/Home.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | } 7 | -------------------------------------------------------------------------------- /ui/common/containers/index.js: -------------------------------------------------------------------------------- 1 | export Pages from './Pages/Index' 2 | export ShowPage from './Pages/Show' 3 | export NewPage from './Pages/New' 4 | -------------------------------------------------------------------------------- /ui/server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | ignore: [/processSass\.js/, /node_modules/] 3 | }) 4 | 5 | module.exports = require('./server.js') 6 | -------------------------------------------------------------------------------- /api/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "id": 1, 5 | "title": "test page#1", 6 | "content": "TEST PAGE CONTENT" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /api/db.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "id": 1, 5 | "title": "test page#1", 6 | "content": "TEST PAGE CONTENT" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /ui/common/constants/endpoints.js: -------------------------------------------------------------------------------- 1 | import config from '../../config' 2 | const API_ROOT = `http://${config.host}:${config.serverPort}/api/v1` 3 | 4 | export const PAGES_ENDPOINT = `${API_ROOT}/pages` 5 | -------------------------------------------------------------------------------- /ui/common/components/index.js: -------------------------------------------------------------------------------- 1 | export App from './App/App' 2 | export Home from './Home' 3 | export Pages from './Pages/Index' 4 | export ShowPage from './Pages/Show' 5 | export PageForm from './Pages/Form' 6 | -------------------------------------------------------------------------------- /ui/common/theme/_variables.scss: -------------------------------------------------------------------------------- 1 | $gray1-color: #cbcbcb; 2 | $gray2-color: #e0e0e0; 3 | 4 | $dark-gray1-color: #2d3e50; 5 | 6 | $green1-color: #18d8a9; 7 | 8 | $red1-color: #da4453; 9 | 10 | $white-color: #fff; 11 | $black-color: #000; 12 | -------------------------------------------------------------------------------- /ui/common/containers/Pages/New.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Form from './Form' 3 | 4 | export default class NewPageContainer extends Component { 5 | render() { 6 | return ( 7 |
8 | ) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": [ 4 | "react-hot-loader/babel", 5 | [ 6 | "css-modules-transform", { 7 | "preprocessCss": "./ui/lib/processSass.js", 8 | "extensions": [".css", ".scss"] 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ui/lib/processSass.js: -------------------------------------------------------------------------------- 1 | var sass = require('node-sass'); 2 | var path = require('path'); 3 | 4 | module.exports = function processSass(data, filename) { 5 | var result; 6 | 7 | result = sass.renderSync({ 8 | data: data, 9 | file: filename 10 | }).css; 11 | 12 | return result.toString('utf8'); 13 | }; 14 | -------------------------------------------------------------------------------- /ui/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { reducer as formReducer } from 'redux-form' 3 | import { routerReducer } from 'react-router-redux' 4 | import pages from './pages' 5 | 6 | export default combineReducers({ 7 | form: formReducer, 8 | routing: routerReducer, 9 | pages 10 | }) 11 | -------------------------------------------------------------------------------- /ui/common/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './Home.scss' 3 | 4 | class Home extends Component { 5 | render() { 6 | return ( 7 |

8 | Welcome to BabelCoder Wiki! 9 |

10 | ) 11 | } 12 | } 13 | 14 | export default Home 15 | -------------------------------------------------------------------------------- /ui/common/components/Pages/Show.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const ShowPage = ({ 4 | title, 5 | content 6 | }) => { 7 | return ( 8 |
9 |

{title}

10 |

11 | {content} 12 |

13 |
14 | ) 15 | } 16 | 17 | ShowPage.propTypes = { 18 | title: PropTypes.string.isRequired, 19 | content: PropTypes.string.isRequired 20 | } 21 | 22 | export default ShowPage 23 | -------------------------------------------------------------------------------- /ui/common/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Header from './Header' 3 | import styles from './App.scss' 4 | 5 | export default class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 |
11 |
12 | {this.props.children} 13 |
14 |
15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/common/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | import configureStore from '../store/configureStore' 4 | import routes from '../routes' 5 | 6 | export default class Root extends Component { 7 | render() { 8 | const { history, initialState } = this.props 9 | const store = configureStore(history, initialState) 10 | 11 | return ( 12 | 13 | {routes(store, history)} 14 | 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/server/fetchComponent.js: -------------------------------------------------------------------------------- 1 | export function fetchComponent(dispatch, components, params) { 2 | const needs = 3 | components 4 | .filter(component => component) 5 | .reduce((prev, current) => { 6 | const wrappedComponent = current.WrappedComponent 7 | 8 | return (current.need || []) 9 | .concat( 10 | (wrappedComponent && wrappedComponent.need) || [] 11 | ) 12 | .concat(prev) 13 | }, [] 14 | ) 15 | 16 | return Promise.all(needs.map(need => dispatch(need(params)))) 17 | } 18 | -------------------------------------------------------------------------------- /ui/common/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD_PAGES_REQUEST = 'LOAD_PAGES_REQUEST' 2 | export const LOAD_PAGES_SUCCESS = 'LOAD_PAGES_SUCCESS' 3 | export const LOAD_PAGES_FAILURE = 'LOAD_PAGES_FAILURE' 4 | 5 | export const LOAD_PAGE_REQUEST = 'LOAD_PAGE_REQUEST' 6 | export const LOAD_PAGE_SUCCESS = 'LOAD_PAGE_SUCCESS' 7 | export const LOAD_PAGE_FAILURE = 'LOAD_PAGE_FAILURE' 8 | 9 | export const CREATE_PAGE_REQUEST = 'CREATE_PAGE_REQUEST' 10 | export const CREATE_PAGE_SUCCESS = 'CREATE_PAGE_SUCCESS' 11 | export const CREATE_PAGE_FAILURE = 'CREATE_PAGE_FAILURE' 12 | -------------------------------------------------------------------------------- /ui/common/reducers/pages.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOAD_PAGES_SUCCESS, 3 | LOAD_PAGE_SUCCESS 4 | } from '../constants/actionTypes' 5 | 6 | const initialState = [] 7 | 8 | export default (state = initialState, action) => { 9 | switch(action.type) { 10 | case LOAD_PAGES_SUCCESS: 11 | return action.payload 12 | case LOAD_PAGE_SUCCESS: 13 | return [action.payload] 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export const getPageById = (state, id) => ( 20 | state.pages.find((page) => page.id === +id) || { title: '', content: '' } 21 | ) 22 | -------------------------------------------------------------------------------- /ui/common/components/Pages/Page.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class Page extends Component { 5 | static propTypes = { 6 | id: PropTypes.number.isRequired, 7 | title: PropTypes.string.isRequired 8 | } 9 | 10 | render() { 11 | const { id, title } = this.props 12 | 13 | return ( 14 | 15 | {id} 16 | {title} 17 | 18 | Show 19 | 20 | 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import httpProxy from 'http-proxy' 3 | import ssr from './ssr' 4 | import config from '../config' 5 | 6 | const PORT = config.serverPort 7 | const app = express() 8 | const targetUrl = `http://${config.host}:${config.apiPort}` 9 | const proxy = httpProxy.createProxyServer({ 10 | target: targetUrl 11 | }) 12 | 13 | app.use('/api', (req, res) => { 14 | proxy.web(req, res, { target: `${targetUrl}/api` }); 15 | }) 16 | app.use(ssr) 17 | 18 | app.listen(PORT, error => { 19 | if (error) { 20 | console.error(error) 21 | } else { 22 | console.info(`==> Listening on port ${PORT}.`) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /ui/common/components/App/Header.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme/variables'; 2 | 3 | .header { 4 | background: $dark-gray1-color; 5 | padding: 1rem; 6 | width: 100%; 7 | position: fixed; 8 | left: 0; 9 | top: 0; 10 | box-sizing: border-box; 11 | } 12 | 13 | .brand { 14 | color: $white-color; 15 | text-decoration: none; 16 | } 17 | 18 | .menu { 19 | float: right; 20 | display: inline-block; 21 | list-style: none; 22 | margin: 0; 23 | padding: 0; 24 | 25 | &__item { 26 | display: inline-block; 27 | vertical-align: middle; 28 | } 29 | 30 | &__link { 31 | padding: 1rem; 32 | color: $green1-color; 33 | text-decoration: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # build directory 36 | static 37 | 38 | # API database 39 | api/db.json 40 | -------------------------------------------------------------------------------- /ui/common/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { syncHistoryWithStore } from 'react-router-redux' 3 | import { Router, Route, IndexRoute } from 'react-router' 4 | import { 5 | Pages, 6 | ShowPage, 7 | NewPage 8 | } from './containers' 9 | import { 10 | App, 11 | Home 12 | } from './components' 13 | 14 | export default (store, history) => { 15 | return ( 16 | 17 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /ui/client/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory } from 'react-router' 4 | import { AppContainer } from 'react-hot-loader' 5 | import Root from '../common/containers/Root' 6 | 7 | const initialState = window.__INITIAL_STATE__ 8 | const rootEl = document.getElementById('app') 9 | 10 | render( 11 | 12 | 15 | , 16 | rootEl 17 | ) 18 | 19 | if (module.hot) { 20 | module.hot.accept('../common/containers/Root', () => { 21 | const NextRootApp = require('../common/containers/Root').default 22 | 23 | render( 24 | 25 | 28 | , 29 | rootEl 30 | ) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /ui/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import thunk from 'redux-thunk' 4 | import { apiMiddleware } from 'redux-api-middleware' 5 | import createLogger from 'redux-logger' 6 | import rootReducer from '../reducers' 7 | 8 | export default (history, initialState) => { 9 | const middlewares = [thunk, apiMiddleware, routerMiddleware(history)] 10 | 11 | if(process.env.NODE_ENV !== 'production') 12 | middlewares.push(createLogger()) 13 | 14 | const store = createStore( 15 | rootReducer, 16 | initialState, 17 | applyMiddleware(...middlewares) 18 | ) 19 | 20 | if (module.hot) { 21 | module.hot.accept('../reducers', () => { 22 | System.import('../reducers').then(nextRootReducer => 23 | store.replaceReducer(nextRootReducer.default) 24 | ) 25 | }) 26 | } 27 | 28 | return store 29 | } 30 | -------------------------------------------------------------------------------- /ui/common/containers/Pages/Index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { loadPages } from '../../actions/page' 4 | import { Pages } from '../../components' 5 | 6 | class PagesContainer extends Component { 7 | static propTypes = { 8 | pages: PropTypes.array.isRequired, 9 | onLoadPages: PropTypes.func.isRequired 10 | } 11 | 12 | static need = [ 13 | loadPages 14 | ] 15 | 16 | shouldComponentUpdate(nextProps) { 17 | return this.props.pages !== nextProps.pages; 18 | } 19 | 20 | onReloadPages = () => { 21 | this.props.onLoadPages() 22 | } 23 | 24 | componentDidMount() { 25 | this.onReloadPages() 26 | } 27 | 28 | render() { 29 | return ( 30 | 33 | ) 34 | } 35 | } 36 | 37 | export default connect( 38 | (state) => ({ pages: state.pages }), 39 | { onLoadPages: loadPages } 40 | )(PagesContainer) 41 | -------------------------------------------------------------------------------- /ui/common/containers/Pages/Form.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { reduxForm } from 'redux-form' 3 | import { createPage } from '../../actions/page' 4 | import { PageForm } from '../../components' 5 | 6 | const FIELDS = ['title', 'content'] 7 | 8 | class PageFormContainer extends Component { 9 | static propTypes = { 10 | fields: PropTypes.object.isRequired, 11 | handleSubmit: PropTypes.func.isRequired 12 | } 13 | 14 | render() { 15 | const { fields, handleSubmit } = this.props 16 | 17 | return ( 18 | 21 | ) 22 | } 23 | } 24 | 25 | export default reduxForm({ 26 | form: 'page', 27 | fields: FIELDS, 28 | validate: (values, props) => 29 | FIELDS.reduce((errors, field) => 30 | values[field] ? errors : { ...errors, [field]: 'Required' }, {}) 31 | }, 32 | (state) => ({}), 33 | (dispatch) => ({ 34 | onSubmit: (values) => 35 | dispatch(createPage(values)) 36 | }) 37 | )(PageFormContainer) 38 | -------------------------------------------------------------------------------- /ui/common/components/App/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | import styles from './Header.scss' 4 | 5 | export default class Header extends Component { 6 | render() { 7 | return ( 8 |
9 | 32 |
33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/common/components/Pages/Index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import fetch from 'isomorphic-fetch' 4 | import Page from './Page' 5 | 6 | const Pages = ({ 7 | pages, 8 | onReloadPages 9 | }) => ( 10 |
11 | 16 | Create New Page 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | { 28 | pages.map((page) => ( 29 | 33 | )) 34 | } 35 | 36 |
IDTitleAction
37 |
38 | ) 39 | 40 | Pages.propTypes = { 41 | pages: PropTypes.array.isRequired, 42 | onReloadPages: PropTypes.func.isRequired 43 | } 44 | 45 | export default Pages 46 | -------------------------------------------------------------------------------- /ui/common/containers/Pages/Show.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { loadPage } from '../../actions/page' 4 | import { getPageById } from '../../reducers/pages' 5 | import { ShowPage } from '../../components' 6 | 7 | class ShowPageContainer extends Component { 8 | static propTypes = { 9 | page: PropTypes.object.isRequired, 10 | onLoadPage: PropTypes.func.isRequired 11 | } 12 | 13 | static need = [ 14 | (params) => (loadPage(params.id)) 15 | ] 16 | 17 | shouldComponentUpdate(nextProps) { 18 | return this.props.page !== nextProps.page; 19 | } 20 | 21 | componentDidMount() { 22 | const { onLoadPage, params: { id } } = this.props 23 | 24 | onLoadPage(id) 25 | } 26 | 27 | render() { 28 | const { id, title, content } = this.props.page 29 | 30 | return ( 31 | 35 | ) 36 | } 37 | } 38 | 39 | export default connect( 40 | (state, ownProps) => ({ page: getPageById(state, ownProps.params.id) }), 41 | { onLoadPage: loadPage } 42 | )(ShowPageContainer) 43 | -------------------------------------------------------------------------------- /ui/common/components/Pages/Form.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const errorMessageElement = (field) => ( 4 | field['touched'] && 5 | field['error'] && 6 |
{field['error']}
7 | ) 8 | const PageForm = ({ 9 | fields, 10 | handleSubmit 11 | }) => { 12 | const { title, content } = fields 13 | 14 | return ( 15 | 18 |
19 | 20 | 21 | {errorMessageElement(title)} 22 |
23 |
24 | 25 | 29 | {errorMessageElement(content)} 30 |
31 | 36 | 37 | ) 38 | } 39 | 40 | PageForm.propTypes = { 41 | fields: PropTypes.shape({ 42 | title: PropTypes.object.isRequired, 43 | content: PropTypes.object.isRequired 44 | }).isRequired, 45 | handleSubmit: PropTypes.func.isRequired 46 | } 47 | 48 | export default PageForm 49 | -------------------------------------------------------------------------------- /ui/common/theme/elements.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | $border-style: 1px solid $gray1-color; 4 | 5 | :global { 6 | body { 7 | font-size: 16px; 8 | } 9 | 10 | .container { 11 | width: 50%; 12 | margin: 0 auto; 13 | } 14 | 15 | // Table 16 | .table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | empty-cells: show; 20 | border: $border-style; 21 | 22 | td, th { 23 | border-left: $border-style; 24 | border-width: 0 0 0 1px; 25 | font-size: inherit; 26 | margin: 0; 27 | overflow: visible; 28 | padding: 0.5rem 1rem; 29 | } 30 | 31 | thead { 32 | background-color: $gray2-color; 33 | color: $black-color; 34 | text-align: left; 35 | vertical-align: bottom; 36 | } 37 | } 38 | 39 | // Button 40 | .button { 41 | display: inline-block; 42 | vertical-align: middle; 43 | text-align: center; 44 | cursor: pointer; 45 | user-select: none; 46 | box-sizing: border-box; 47 | padding: 0.5rem 1rem; 48 | text-decoration: none; 49 | background-color: $gray2-color; 50 | color: $dark-gray1-color; 51 | border: none; 52 | } 53 | 54 | // Form 55 | .form { 56 | fieldset { 57 | margin: 0; 58 | padding: 0.35rem 0 0.75rem; 59 | border: 0; 60 | } 61 | 62 | input[type='text'], 63 | textarea, 64 | label { 65 | display: block; 66 | margin: 0.25rem 0; 67 | width: 100%; 68 | } 69 | 70 | .error { 71 | color: $red1-color; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/common/actions/page.js: -------------------------------------------------------------------------------- 1 | import { CALL_API } from 'redux-api-middleware' 2 | import { push } from 'react-router-redux' 3 | import { PAGES_ENDPOINT } from '../constants/endpoints' 4 | import { 5 | LOAD_PAGES_REQUEST, 6 | LOAD_PAGES_SUCCESS, 7 | LOAD_PAGES_FAILURE, 8 | 9 | LOAD_PAGE_REQUEST, 10 | LOAD_PAGE_SUCCESS, 11 | LOAD_PAGE_FAILURE, 12 | 13 | CREATE_PAGE_REQUEST, 14 | CREATE_PAGE_SUCCESS, 15 | CREATE_PAGE_FAILURE 16 | } from '../constants/actionTypes' 17 | 18 | export const loadPages = () => ({ 19 | [CALL_API]: { 20 | endpoint: PAGES_ENDPOINT, 21 | method: 'GET', 22 | types: [LOAD_PAGES_REQUEST, LOAD_PAGES_SUCCESS, LOAD_PAGES_FAILURE] 23 | } 24 | }) 25 | 26 | export const loadPage = (id) => ({ 27 | [CALL_API]: { 28 | endpoint: `${PAGES_ENDPOINT}/${id}`, 29 | method: 'GET', 30 | types: [LOAD_PAGE_REQUEST, LOAD_PAGE_SUCCESS, LOAD_PAGE_FAILURE] 31 | } 32 | }) 33 | 34 | export const createPage = (values) => ( 35 | (dispatch) => 36 | dispatch({ 37 | [CALL_API]: { 38 | endpoint: PAGES_ENDPOINT, 39 | headers: { 40 | 'Accept': 'application/json', 41 | 'Content-Type': 'application/json' 42 | }, 43 | method: 'POST', 44 | body: JSON.stringify(values), 45 | types: [ 46 | CREATE_PAGE_REQUEST, 47 | { 48 | type: CREATE_PAGE_SUCCESS, 49 | payload: (_action, _state, res) => { 50 | return res.json().then((page) => { 51 | dispatch(push(`/pages/${page.id}`)) 52 | return page 53 | }) 54 | } 55 | }, 56 | CREATE_PAGE_FAILURE 57 | ] 58 | } 59 | } 60 | ) 61 | ) 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babelcoder-wiki", 3 | "version": "1.0.0", 4 | "description": "BabelCoder Wiki!", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm-run-all --parallel start-dev-api start-dev-ui start-dev-ssr", 8 | "start-dev-api": "json-server --watch api/db.json --routes api/routes.json --port 5000", 9 | "start-dev-ui": "webpack-dev-server --config ui/webpack/webpack.config.js", 10 | "start-dev-ssr": "nodemon ./ui/server/index.js" 11 | }, 12 | "author": "Nuttavut Thongjor", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "autoprefixer": "^6.3.6", 16 | "babel-core": "^6.8.0", 17 | "babel-loader": "^6.2.4", 18 | "babel-plugin-css-modules-transform": "^0.1.1", 19 | "babel-plugin-transform-runtime": "^6.8.0", 20 | "babel-preset-es2015": "^6.6.0", 21 | "babel-preset-react": "^6.5.0", 22 | "babel-preset-stage-0": "^6.5.0", 23 | "css-loader": "^0.23.1", 24 | "express": "^4.14.0", 25 | "json-server": "^0.8.12", 26 | "node-sass": "^3.7.0", 27 | "nodemon": "^1.9.2", 28 | "npm-run-all": "^1.8.0", 29 | "postcss-loader": "^0.9.1", 30 | "react-hot-loader": "^3.0.0-beta.1", 31 | "redux-logger": "^2.6.1", 32 | "sass-loader": "^3.2.0", 33 | "style-loader": "^0.13.1", 34 | "webpack": "^2.1.0-beta.7", 35 | "webpack-dev-server": "^2.0.0-beta" 36 | }, 37 | "dependencies": { 38 | "babel-runtime": "^6.6.1", 39 | "http-proxy": "^1.14.0", 40 | "isomorphic-fetch": "^2.2.1", 41 | "react": "^15.0.2", 42 | "react-dom": "^15.0.2", 43 | "react-redux": "^4.4.5", 44 | "react-router": "^3.0.0-alpha.1 ", 45 | "react-router-redux": "^4.0.4", 46 | "redux": "^3.5.2", 47 | "redux-api-middleware": "^1.0.2", 48 | "redux-form": "^5.2.5", 49 | "redux-thunk": "^2.1.0" 50 | }, 51 | "peerDependencies": { 52 | "react": "^15.0.2", 53 | "react-dom": "^15.0.2" 54 | }, 55 | "engines": { 56 | "node": "6.0.0", 57 | "npm": "3.8.6" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const autoprefixer = require('autoprefixer'); 4 | const config = require('../config'); 5 | 6 | module.exports = { 7 | devtool: 'eval', 8 | entry: [ 9 | 'react-hot-loader/patch', 10 | `webpack-dev-server/client?http://${config.host}:${config.clientPort}`, 11 | 'webpack/hot/only-dev-server', 12 | './ui/common/theme/elements.scss', 13 | './ui/client/index.js' 14 | ], 15 | output: { 16 | publicPath: `http://${config.host}:${config.clientPort}/static/`, 17 | path: path.join(__dirname, 'static'), 18 | filename: 'bundle.js' 19 | }, 20 | plugins: [ 21 | new webpack.HotModuleReplacementPlugin() 22 | ], 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.jsx?$/, 27 | exclude: /node_modules/, 28 | loaders: [ 29 | { 30 | loader: 'babel-loader', 31 | query: { 32 | babelrc: false, 33 | presets: ["es2015", "stage-0", "react"] 34 | } 35 | } 36 | ] 37 | }, 38 | { 39 | test: /\.css$/, 40 | loaders: [ 41 | 'style-loader', 42 | 'css-loader' 43 | ] 44 | }, { 45 | test: /\.scss$/, 46 | exclude: /node_modules/, 47 | loaders: [ 48 | 'style-loader', 49 | { 50 | loader: 'css-loader', 51 | query: { 52 | sourceMap: true, 53 | module: true, 54 | localIdentName: '[name]__[local]___[hash:base64:5]' 55 | } 56 | }, 57 | { 58 | loader: 'sass-loader', 59 | query: { 60 | outputStyle: 'expanded', 61 | sourceMap: true 62 | } 63 | }, 64 | 'postcss-loader' 65 | ] 66 | } 67 | ] 68 | }, 69 | postcss: function () { 70 | return [autoprefixer]; 71 | }, 72 | devServer: { 73 | port: config.clientPort, 74 | hot: true, 75 | inline: false, 76 | historyApiFallback: true 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /ui/server/ssr.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { match, RouterContext } from 'react-router' 3 | import { renderToString } from 'react-dom/server' 4 | import { Provider } from 'react-redux' 5 | import createMemoryHistory from 'react-router/lib/createMemoryHistory' 6 | import { syncHistoryWithStore } from 'react-router-redux' 7 | import configureStore from '../common/store/configureStore' 8 | import Root from '../common/containers/Root' 9 | import getRoutes from '../common/routes' 10 | import { fetchComponent } from './fetchComponent.js' 11 | 12 | const renderHtml = (html, initialState) => (` 13 | 14 | 15 | 16 | 17 | Wiki! 18 | 19 | 20 |
${html}
21 | 24 | 25 | 26 | 27 | `) 28 | 29 | export default function(req, res) { 30 | const memoryHistory = createMemoryHistory(req.originalUrl) 31 | const store = configureStore(memoryHistory) 32 | const history = syncHistoryWithStore(memoryHistory, store) 33 | 34 | match({ 35 | routes: getRoutes(store, history), 36 | location: req.originalUrl 37 | }, (error, redirectLocation, renderProps) => { 38 | if (error) { 39 | console.log(error) 40 | res.status(500).send('Internal Server Error') 41 | } else if (redirectLocation) { 42 | res.redirect(302, `${redirectLocation.pathname}${redirectLocation.search}`) 43 | } else if (renderProps) { 44 | const { components, params } = renderProps 45 | 46 | fetchComponent(store.dispatch, components, params) 47 | .then(html => { 48 | const componentHTML = renderToString( 49 | 50 | 51 | 52 | ) 53 | const initialState = store.getState() 54 | 55 | res.status(200).send( 56 | renderHtml(componentHTML, initialState) 57 | ) 58 | }) 59 | .catch(error => { 60 | console.log(error) 61 | res.status(500).send('Internal Server Error') 62 | }) 63 | } else { 64 | res.status(404).send('Not found') 65 | } 66 | }) 67 | } 68 | --------------------------------------------------------------------------------