├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── comments.json ├── index.html ├── package.json ├── server.js ├── src └── index.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Jewei Mak 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # my-react-tutorial 2 | ES6/7 version of [CommentBox component](https://facebook.github.io/react/docs/tutorial.html) based on react-transform-boilerplate. 3 | 4 | ## Features 5 | * Facebook React official tutorial example 6 | * ESMAScript 2015 (ES6) / ES7 7 | * Webpack 8 | * Hot loading (Reflects code changes on the fly) 9 | * Follows Airbnb JavaScript/React/JSX style guides 10 | * Look ma! No semicolon! 11 | 12 | ## Setup 13 | git clone https://github.com/jewei/my-react-tutorial.git 14 | cd my-react-tutorial 15 | npm install 16 | npm run dev 17 | 18 | View the application on `http://localhost:3000`. 19 | 20 | ## Roadmap 21 | * Replace jQuery with [Fetch](https://github.com/github/fetch) for AJAX calls. 22 | * Implements Redux. 23 | * CSS loading. 24 | * Lint 25 | -------------------------------------------------------------------------------- /comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "author": "Pete Hunt", 5 | "text": "This is one comment" 6 | }, 7 | { 8 | "id": 2, 9 | "author": "Jordan Walke", 10 | "text": "This is *another* comment" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My React Tutorial 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-tutorial", 3 | "version": "1.0.0", 4 | "description": "My code on React tutorial", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 8 | "build": "npm run clean && npm run build:webpack", 9 | "clean": "rm -rf dist", 10 | "dev": "node server.js", 11 | "lint": "eslint src", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jewei/my-react-tutorial.git" 17 | }, 18 | "keywords": [ 19 | "React", 20 | "tutorial", 21 | "react-transform-boilerplate" 22 | ], 23 | "author": "Jewei Mak ", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/jewei/my-react-tutorial/issues" 27 | }, 28 | "homepage": "https://github.com/jewei/my-react-tutorial#readme", 29 | "dependencies": { 30 | "body-parser": "^1.15.0", 31 | "jquery": "^2.2.0", 32 | "marked": "^0.3.5", 33 | "react": "^0.14.7", 34 | "react-dom": "^0.14.7" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.5.2", 38 | "babel-eslint": "^5.0.0", 39 | "babel-loader": "^6.2.3", 40 | "babel-preset-es2015": "^6.5.0", 41 | "babel-preset-react": "^6.5.0", 42 | "babel-preset-react-hmre": "^1.1.0", 43 | "babel-preset-stage-0": "^6.5.0", 44 | "cross-env": "^1.0.7", 45 | "eslint": "^2.1.0", 46 | "eslint-plugin-babel": "^3.1.0", 47 | "eslint-plugin-react": "^3.16.1", 48 | "eventsource-polyfill": "^0.9.6", 49 | "express": "^4.13.4", 50 | "webpack": "^1.12.13", 51 | "webpack-dev-middleware": "^1.5.1", 52 | "webpack-hot-middleware": "^2.7.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config.dev') 5 | 6 | const app = express() 7 | const compiler = webpack(config) 8 | 9 | // Webpack. 10 | 11 | app.use(require('webpack-dev-middleware')(compiler, { 12 | noInfo: true, 13 | publicPath: config.output.publicPath 14 | })); 15 | 16 | app.use(require('webpack-hot-middleware')(compiler)); 17 | 18 | app.get('/', function(req, res) { 19 | res.sendFile(path.join(__dirname, 'index.html')); 20 | }); 21 | 22 | // Comment server. 23 | 24 | const fs = require('fs') 25 | const bodyParser = require('body-parser') 26 | const COMMENTS_FILE = path.join(__dirname, 'comments.json') 27 | 28 | app.use('/', express.static(path.join(__dirname, 'public'))) 29 | app.use(bodyParser.json()) 30 | app.use(bodyParser.urlencoded({extended: true})) 31 | 32 | // Additional middleware which will set headers that we need on each request. 33 | app.use(function(req, res, next) { 34 | // Set permissive CORS header - this allows this server to be used only as 35 | // an API server in conjunction with something like webpack-dev-server. 36 | res.setHeader('Access-Control-Allow-Origin', '*'); 37 | 38 | // Disable caching so we'll always get the latest comments. 39 | res.setHeader('Cache-Control', 'no-cache'); 40 | next(); 41 | }); 42 | 43 | app.get('/api/comments', function(req, res) { 44 | fs.readFile(COMMENTS_FILE, function(err, data) { 45 | if (err) { 46 | console.error(err); 47 | process.exit(1); 48 | } 49 | res.json(JSON.parse(data)); 50 | }); 51 | }); 52 | 53 | app.post('/api/comments', function(req, res) { 54 | fs.readFile(COMMENTS_FILE, function(err, data) { 55 | if (err) { 56 | console.error(err); 57 | process.exit(1); 58 | } 59 | var comments = JSON.parse(data); 60 | // NOTE: In a real implementation, we would likely rely on a database or 61 | // some other approach (e.g. UUIDs) to ensure a globally unique id. We'll 62 | // treat Date.now() as unique-enough for our purposes. 63 | var newComment = { 64 | id: Date.now(), 65 | author: req.body.author, 66 | text: req.body.text, 67 | }; 68 | comments.push(newComment); 69 | fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) { 70 | if (err) { 71 | console.error(err); 72 | process.exit(1); 73 | } 74 | res.json(comments); 75 | }); 76 | }); 77 | }); 78 | 79 | app.listen(3000, 'localhost', function(err) { 80 | if (err) { 81 | console.log(err); 82 | return; 83 | } 84 | console.log('Server started: http://localhost:3000'); 85 | }); 86 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | const marked = require('marked') 4 | const $ = require ('jquery') 5 | 6 | class CommentBox extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = {data: []} 10 | } 11 | 12 | loadCommentsFromServer() { 13 | $.ajax({ 14 | url: this.props.url, 15 | dataType: 'json', 16 | cache: false, 17 | success: (data) => { 18 | this.setState({data}) 19 | }, 20 | error: (xhr, status, err) => { 21 | console.error(this.props.url, status, err.toString()) 22 | }, 23 | }) 24 | } 25 | 26 | componentDidMount() { 27 | this.loadCommentsFromServer() 28 | setInterval(this.loadCommentsFromServer(), this.props.pollInterval) 29 | } 30 | 31 | handleCommentSubmit(comment) { 32 | let comments = this.state.data 33 | // Optimistically set an id on the new comment. It will be replaced by an 34 | // id generated by the server. In a production application you would likely 35 | // not use Date.now() for this and would have a more robust system in place. 36 | comment.id = Date.now() 37 | let data = [...comments, comment] 38 | this.setState({ data }) 39 | 40 | $.ajax({ 41 | url: this.props.url, 42 | dataType: 'json', 43 | type: 'POST', 44 | data: comment, 45 | success: (data) => { 46 | this.setState({data}) 47 | }, 48 | error: (xhr, status, err) => { 49 | this.setState({data: comments}) 50 | console.error(this.props.url, status, err.toString()) 51 | } 52 | }) 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 |

Comments

59 | 60 | 61 |
62 | ) 63 | } 64 | } 65 | 66 | const CommentList = (props) => ( 67 |
68 | { props.data.map(comment => ) } 69 |
70 | ) 71 | 72 | class CommentForm extends Component { 73 | constructor(props) { 74 | super(props) 75 | this.state = {author: '', text: ''} 76 | } 77 | 78 | handleAuthorChange(e) { 79 | this.setState({author: e.target.value}) 80 | } 81 | 82 | handleTextChange(e) { 83 | this.setState({text: e.target.value}) 84 | } 85 | 86 | handleSubmit(e) { 87 | e.preventDefault() 88 | let author = this.state.author.trim() 89 | let text = this.state.text.trim() 90 | if (!text || !author) { 91 | return 92 | } 93 | this.props.onCommentSubmit({ author, text }) 94 | this.setState({author: '', text: ''}) 95 | } 96 | 97 | render () { 98 | return ( 99 |
100 | 106 | 112 | 113 |
114 | ) 115 | } 116 | } 117 | 118 | const Comment = (props) => ( 119 |
120 |

121 | {props.author} 122 |

123 | 124 |
125 | ) 126 | 127 | ReactDOM.render( 128 | , 129 | document.getElementById('root') 130 | ) 131 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'eventsource-polyfill', // necessary for hot reloading with IE 8 | 'webpack-hot-middleware/client', 9 | path.join(__dirname, 'src/index') 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.jsx?/, 23 | loaders: ['babel'], 24 | include: path.join(__dirname, 'src') 25 | }] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | './src/index' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | publicPath: '/static/' 13 | }, 14 | plugins: [ 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | new webpack.DefinePlugin({ 17 | 'process.env': { 18 | 'NODE_ENV': JSON.stringify('production') 19 | } 20 | }), 21 | new webpack.optimize.UglifyJsPlugin({ 22 | compressor: { 23 | warnings: false 24 | } 25 | }) 26 | ], 27 | module: { 28 | loaders: [{ 29 | test: /\.js$/, 30 | loaders: ['babel'], 31 | include: path.join(__dirname, 'src') 32 | }] 33 | } 34 | }; 35 | --------------------------------------------------------------------------------