├── .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 |
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 |
--------------------------------------------------------------------------------