├── requirements.txt ├── .gitignore ├── comments.json ├── .editorconfig ├── app.json ├── LICENSE ├── public ├── scripts │ ├── polyfill.js │ └── example.js ├── css │ └── base.css └── index.html ├── package.json ├── README.md ├── server.pl ├── server.py ├── server.rb ├── server.php ├── server.js └── server.go /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1388534400000, 4 | "author": "Pete Hunt", 5 | "text": "Hey there!" 6 | }, 7 | { 8 | "id": 1420070400000, 9 | "author": "Paul O’Shannessy", 10 | "text": "React is *great*!" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{js,rb,css,html}] 13 | indent_size = 2 14 | 15 | [*.go] 16 | indent_size = 8 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Redux Tutorial Server", 3 | "description": "Code from the React tutorial, modified to use basic Redux", 4 | "keywords": [ "react", "reactjs", "redux", "tutorial" ], 5 | "repository": "https://github.com/firasd/react-redux-tutorial", 6 | "logo": "https://facebook.github.io/react/img/logo.svg", 7 | "website": "https://github.com/firasd/react-redux-tutorial", 8 | "success_url": "/", 9 | "env" : { 10 | "BUILDPACK_URL": "https://github.com/heroku/heroku-buildpack-nodejs.git" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The examples provided by Facebook are for non-commercial testing and evaluation 2 | purposes only. Facebook reserves all rights not expressly granted. 3 | 4 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 7 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 8 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 9 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /public/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | if (typeof Object.assign != 'function') { 2 | (function () { 3 | Object.assign = function (target) { 4 | 'use strict'; 5 | if (target === undefined || target === null) { 6 | throw new TypeError('Cannot convert undefined or null to object'); 7 | } 8 | 9 | var output = Object(target); 10 | for (var index = 1; index < arguments.length; index++) { 11 | var source = arguments[index]; 12 | if (source !== undefined && source !== null) { 13 | for (var nextKey in source) { 14 | if (source.hasOwnProperty(nextKey)) { 15 | output[nextKey] = source[nextKey]; 16 | } 17 | } 18 | } 19 | } 20 | return output; 21 | }; 22 | })(); 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-tutorial", 3 | "version": "0.0.0", 4 | "description": "Code from the React tutorial, modified to use basic Redux.", 5 | "main": "server.js", 6 | "dependencies": { 7 | "body-parser": "^1.4.3", 8 | "express": "^4.4.5" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "start": "node server.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/firasd/react-redux-tutorial.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "redux", 22 | "tutorial", 23 | "comment", 24 | "example" 25 | ], 26 | "author": "petehunt", 27 | "bugs": { 28 | "url": "https://github.com/firasd/react-redux-tutorial/issues" 29 | }, 30 | "homepage": "https://github.com/firasd/react-redux-tutorial", 31 | "engines" : { 32 | "node" : "0.12.x" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 15px; 5 | line-height: 1.7; 6 | margin: 0; 7 | padding: 30px; 8 | } 9 | 10 | a { 11 | color: #4183c4; 12 | text-decoration: none; 13 | } 14 | 15 | a:hover { 16 | text-decoration: underline; 17 | } 18 | 19 | code { 20 | background-color: #f8f8f8; 21 | border: 1px solid #ddd; 22 | border-radius: 3px; 23 | font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; 24 | font-size: 12px; 25 | margin: 0 2px; 26 | padding: 0 5px; 27 | } 28 | 29 | h1, h2, h3, h4 { 30 | font-weight: bold; 31 | margin: 0 0 15px; 32 | padding: 0; 33 | } 34 | 35 | h1 { 36 | border-bottom: 1px solid #ddd; 37 | font-size: 2.5em; 38 | font-weight: bold; 39 | margin: 0 0 15px; 40 | padding: 0; 41 | } 42 | 43 | h2 { 44 | border-bottom: 1px solid #eee; 45 | font-size: 2em; 46 | } 47 | 48 | h3 { 49 | font-size: 1.5em; 50 | } 51 | 52 | h4 { 53 | font-size: 1.2em; 54 | } 55 | 56 | p, ul { 57 | margin: 15px 0; 58 | } 59 | 60 | ul { 61 | padding-left: 30px; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 2 | 3 | # Simple React Redux Tutorial 4 | 5 | 6 | This is a modified version of the React comment box example from [the React tutorial](http://facebook.github.io/react/docs/tutorial.html). The example app has been modified to use Redux. 7 | 8 | ## To use 9 | 10 | There are several simple server implementations included. They all serve static files from `public/` and handle requests to `/api/comments` to fetch or add data. Start a server with one of the following: 11 | 12 | ### Node 13 | 14 | ```sh 15 | npm install 16 | node server.js 17 | ``` 18 | 19 | ### Python 20 | 21 | ```sh 22 | pip install -r requirements.txt 23 | python server.py 24 | ``` 25 | 26 | ### Ruby 27 | ```sh 28 | ruby server.rb 29 | ``` 30 | 31 | ### PHP 32 | ```sh 33 | php server.php 34 | ``` 35 | 36 | ### Go 37 | ```sh 38 | go run server.go 39 | ``` 40 | 41 | ### Perl 42 | 43 | ```sh 44 | cpan Mojolicious 45 | perl server.pl 46 | ``` 47 | 48 | And visit . Try opening multiple tabs! 49 | 50 | ## Changing the port 51 | 52 | You can change the port number by setting the `$PORT` environment variable before invoking any of the scripts above, e.g., 53 | 54 | ```sh 55 | PORT=3001 node server.js 56 | ``` 57 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server.pl: -------------------------------------------------------------------------------- 1 | # This file provided by Facebook is for non-commercial testing and evaluation 2 | # purposes only. Facebook reserves all rights not expressly granted. 3 | # 4 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 7 | # FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 8 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 9 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | use Time::HiRes qw(gettimeofday); 12 | use Mojolicious::Lite; 13 | use Mojo::JSON qw(encode_json decode_json); 14 | 15 | app->static->paths->[0] = './public'; 16 | 17 | any '/' => sub { $_[0]->reply->static('index.html') }; 18 | 19 | any [qw(GET POST)] => '/api/comments' => sub { 20 | my $self = shift; 21 | my $comments = decode_json (do { local(@ARGV,$/) = 'comments.json';<> }); 22 | $self->res->headers->cache_control('no-cache'); 23 | $self->res->headers->access_control_allow_origin('*'); 24 | 25 | if ($self->req->method eq 'POST') 26 | { 27 | push @$comments, { 28 | id => int(gettimeofday * 1000), 29 | author => $self->param('author'), 30 | text => $self->param('text'), 31 | }; 32 | open my $FILE, '>', 'comments.json'; 33 | print $FILE encode_json($comments); 34 | } 35 | $self->render(json => $comments); 36 | }; 37 | my $port = $ENV{PORT} || 3000; 38 | app->start('daemon', '-l', "http://*:$port"); 39 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | # This file provided by Facebook is for non-commercial testing and evaluation 2 | # purposes only. Facebook reserves all rights not expressly granted. 3 | # 4 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 7 | # FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 8 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 9 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | import json 12 | import os 13 | import time 14 | from flask import Flask, Response, request 15 | 16 | app = Flask(__name__, static_url_path='', static_folder='public') 17 | app.add_url_rule('/', 'root', lambda: app.send_static_file('index.html')) 18 | 19 | @app.route('/api/comments', methods=['GET', 'POST']) 20 | def comments_handler(): 21 | 22 | with open('comments.json', 'r') as file: 23 | comments = json.loads(file.read()) 24 | 25 | if request.method == 'POST': 26 | newComment = request.form.to_dict() 27 | newComment['id'] = int(time.time() * 1000) 28 | comments.append(newComment) 29 | 30 | with open('comments.json', 'w') as file: 31 | file.write(json.dumps(comments, indent=4, separators=(',', ': '))) 32 | 33 | return Response(json.dumps(comments), mimetype='application/json', headers={'Cache-Control': 'no-cache', 'Access-Control-Allow-Origin': '*'}) 34 | 35 | if __name__ == '__main__': 36 | app.run(port=int(os.environ.get("PORT",3000))) 37 | -------------------------------------------------------------------------------- /server.rb: -------------------------------------------------------------------------------- 1 | # This file provided by Facebook is for non-commercial testing and evaluation 2 | # purposes only. Facebook reserves all rights not expressly granted. 3 | # 4 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 7 | # FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 8 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 9 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | require 'webrick' 12 | require 'json' 13 | 14 | # default port to 3000 or overwrite with PORT variable by running 15 | # $ PORT=3001 ruby server.rb 16 | port = ENV['PORT'] ? ENV['PORT'].to_i : 3000 17 | 18 | puts "Server started: http://localhost:#{port}/" 19 | 20 | root = File.expand_path './public' 21 | server = WEBrick::HTTPServer.new Port: port, DocumentRoot: root 22 | 23 | server.mount_proc '/api/comments' do |req, res| 24 | comments = JSON.parse(File.read('./comments.json', encoding: 'UTF-8')) 25 | 26 | if req.request_method == 'POST' 27 | # Assume it's well formed 28 | comment = { id: (Time.now.to_f * 1000).to_i } 29 | req.query.each do |key, value| 30 | comment[key] = value.force_encoding('UTF-8') unless key == 'id' 31 | end 32 | comments << comment 33 | File.write( 34 | './comments.json', 35 | JSON.pretty_generate(comments, indent: ' '), 36 | encoding: 'UTF-8' 37 | ) 38 | end 39 | 40 | # always return json 41 | res['Content-Type'] = 'application/json' 42 | res['Cache-Control'] = 'no-cache' 43 | res['Access-Control-Allow-Origin'] = '*' 44 | res.body = JSON.generate(comments) 45 | end 46 | 47 | trap('INT') { server.shutdown } 48 | 49 | server.start 50 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | round(microtime(true) * 1000), 39 | 'author' => $_POST['author'], 40 | 'text' => $_POST['text'] 41 | ]; 42 | 43 | $comments = json_encode($commentsDecoded, JSON_PRETTY_PRINT); 44 | file_put_contents('comments.json', $comments); 45 | } 46 | header('Content-Type: application/json'); 47 | header('Cache-Control: no-cache'); 48 | header('Access-Control-Allow-Origin: *'); 49 | echo $comments; 50 | } else { 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 10 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | var fs = require('fs'); 14 | var path = require('path'); 15 | var express = require('express'); 16 | var bodyParser = require('body-parser'); 17 | var app = express(); 18 | 19 | var COMMENTS_FILE = path.join(__dirname, 'comments.json'); 20 | 21 | app.set('port', (process.env.PORT || 3000)); 22 | 23 | app.use('/', express.static(path.join(__dirname, 'public'))); 24 | app.use(bodyParser.json()); 25 | app.use(bodyParser.urlencoded({extended: true})); 26 | 27 | // Additional middleware which will set headers that we need on each request. 28 | app.use(function(req, res, next) { 29 | // Set permissive CORS header - this allows this server to be used only as 30 | // an API server in conjunction with something like webpack-dev-server. 31 | res.setHeader('Access-Control-Allow-Origin', '*'); 32 | 33 | // Disable caching so we'll always get the latest comments. 34 | res.setHeader('Cache-Control', 'no-cache'); 35 | next(); 36 | }); 37 | 38 | app.get('/api/comments', function(req, res) { 39 | fs.readFile(COMMENTS_FILE, function(err, data) { 40 | if (err) { 41 | console.error(err); 42 | process.exit(1); 43 | } 44 | res.json(JSON.parse(data)); 45 | }); 46 | }); 47 | 48 | app.post('/api/comments', function(req, res) { 49 | fs.readFile(COMMENTS_FILE, function(err, data) { 50 | if (err) { 51 | console.error(err); 52 | process.exit(1); 53 | } 54 | var comments = JSON.parse(data); 55 | // NOTE: In a real implementation, we would likely rely on a database or 56 | // some other approach (e.g. UUIDs) to ensure a globally unique id. We'll 57 | // treat Date.now() as unique-enough for our purposes. 58 | var newComment = { 59 | id: Date.now(), 60 | author: req.body.author, 61 | text: req.body.text, 62 | }; 63 | comments.push(newComment); 64 | fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) { 65 | if (err) { 66 | console.error(err); 67 | process.exit(1); 68 | } 69 | res.json(comments); 70 | }); 71 | }); 72 | }); 73 | 74 | 75 | app.listen(app.get('port'), function() { 76 | console.log('Server started: http://localhost:' + app.get('port') + '/'); 77 | }); 78 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 10 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "bytes" 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "io/ioutil" 21 | "log" 22 | "net/http" 23 | "os" 24 | "sync" 25 | "time" 26 | ) 27 | 28 | type comment struct { 29 | ID int64 `json:"id"` 30 | Author string `json:"author"` 31 | Text string `json:"text"` 32 | } 33 | 34 | const dataFile = "./comments.json" 35 | 36 | var commentMutex = new(sync.Mutex) 37 | 38 | // Handle comments 39 | func handleComments(w http.ResponseWriter, r *http.Request) { 40 | // Since multiple requests could come in at once, ensure we have a lock 41 | // around all file operations 42 | commentMutex.Lock() 43 | defer commentMutex.Unlock() 44 | 45 | // Stat the file, so we can find its current permissions 46 | fi, err := os.Stat(dataFile) 47 | if err != nil { 48 | http.Error(w, fmt.Sprintf("Unable to stat the data file (%s): %s", dataFile, err), http.StatusInternalServerError) 49 | return 50 | } 51 | 52 | // Read the comments from the file. 53 | commentData, err := ioutil.ReadFile(dataFile) 54 | if err != nil { 55 | http.Error(w, fmt.Sprintf("Unable to read the data file (%s): %s", dataFile, err), http.StatusInternalServerError) 56 | return 57 | } 58 | 59 | switch r.Method { 60 | case "POST": 61 | // Decode the JSON data 62 | var comments []comment 63 | if err := json.Unmarshal(commentData, &comments); err != nil { 64 | http.Error(w, fmt.Sprintf("Unable to Unmarshal comments from data file (%s): %s", dataFile, err), http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | // Add a new comment to the in memory slice of comments 69 | comments = append(comments, comment{ID: time.Now().UnixNano() / 1000000, Author: r.FormValue("author"), Text: r.FormValue("text")}) 70 | 71 | // Marshal the comments to indented json. 72 | commentData, err = json.MarshalIndent(comments, "", " ") 73 | if err != nil { 74 | http.Error(w, fmt.Sprintf("Unable to marshal comments to json: %s", err), http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | // Write out the comments to the file, preserving permissions 79 | err := ioutil.WriteFile(dataFile, commentData, fi.Mode()) 80 | if err != nil { 81 | http.Error(w, fmt.Sprintf("Unable to write comments to data file (%s): %s", dataFile, err), http.StatusInternalServerError) 82 | return 83 | } 84 | 85 | w.Header().Set("Content-Type", "application/json") 86 | w.Header().Set("Cache-Control", "no-cache") 87 | w.Header().Set("Access-Control-Allow-Origin", "*") 88 | io.Copy(w, bytes.NewReader(commentData)) 89 | 90 | case "GET": 91 | w.Header().Set("Content-Type", "application/json") 92 | w.Header().Set("Cache-Control", "no-cache") 93 | w.Header().Set("Access-Control-Allow-Origin", "*") 94 | // stream the contents of the file to the response 95 | io.Copy(w, bytes.NewReader(commentData)) 96 | 97 | default: 98 | // Don't know the method, so error 99 | http.Error(w, fmt.Sprintf("Unsupported method: %s", r.Method), http.StatusMethodNotAllowed) 100 | } 101 | } 102 | 103 | func main() { 104 | port := os.Getenv("PORT") 105 | if port == "" { 106 | port = "3000" 107 | } 108 | http.HandleFunc("/api/comments", handleComments) 109 | http.Handle("/", http.FileServer(http.Dir("./public"))) 110 | log.Println("Server started: http://localhost:" + port) 111 | log.Fatal(http.ListenAndServe(":"+port, nil)) 112 | } 113 | -------------------------------------------------------------------------------- /public/scripts/example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 10 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | var Comment = React.createClass({ 14 | rawMarkup: function() { 15 | var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); 16 | return { __html: rawMarkup }; 17 | }, 18 | 19 | render: function() { 20 | return ( 21 |
22 |

23 | {this.props.author} 24 |

25 | 26 |
27 | ); 28 | } 29 | }); 30 | 31 | var CommentBox = React.createClass({ 32 | loadCommentsFromServer: function() { 33 | $.ajax({ 34 | url: this.props.url, 35 | dataType: 'json', 36 | cache: false, 37 | success: function(data) { 38 | this.props.setComments(data); 39 | }.bind(this), 40 | error: function(xhr, status, err) { 41 | console.error(this.props.url, status, err.toString()); 42 | }.bind(this) 43 | }); 44 | }, 45 | handleCommentSubmit: function(comment) { 46 | var comments = this.props.data; 47 | // Optimistically set an id on the new comment. It will be replaced by an 48 | // id generated by the server. In a production application you would likely 49 | // not use Date.now() for this and would have a more robust system in place. 50 | this.props.addComment(comment); 51 | $.ajax({ 52 | url: this.props.url, 53 | dataType: 'json', 54 | type: 'POST', 55 | data: comment, 56 | success: function(data) { 57 | this.props.setComments(data); 58 | }.bind(this), 59 | error: function(xhr, status, err) { 60 | this.props.setComments(comments); 61 | console.error(this.props.url, status, err.toString()); 62 | }.bind(this) 63 | }); 64 | }, 65 | componentDidMount: function() { 66 | this.loadCommentsFromServer(); 67 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 68 | }, 69 | render: function() { 70 | return ( 71 |
72 |

Comments

73 | 74 | 75 |
76 | ); 77 | } 78 | }); 79 | 80 | var CommentList = React.createClass({ 81 | render: function() { 82 | var commentNodes = this.props.data.map(function(comment) { 83 | return ( 84 | 85 | {comment.text} 86 | 87 | ); 88 | }); 89 | return ( 90 |
91 | {commentNodes} 92 |
93 | ); 94 | } 95 | }); 96 | 97 | var CommentForm = React.createClass({ 98 | getInitialState: function() { 99 | return {author: '', text: ''}; 100 | }, 101 | handleAuthorChange: function(e) { 102 | this.setState({author: e.target.value}); 103 | }, 104 | handleTextChange: function(e) { 105 | this.setState({text: e.target.value}); 106 | }, 107 | handleSubmit: function(e) { 108 | e.preventDefault(); 109 | var author = this.state.author.trim(); 110 | var text = this.state.text.trim(); 111 | if (!text || !author) { 112 | return; 113 | } 114 | this.props.onCommentSubmit({author: author, text: text}); 115 | this.setState({author: '', text: ''}); 116 | }, 117 | render: function() { 118 | return ( 119 |
120 | 126 | 132 | 133 |
134 | ); 135 | } 136 | }); 137 | 138 | var createStore = Redux.createStore; 139 | var Provider = ReactRedux.Provider; 140 | var connect = ReactRedux.connect; 141 | 142 | var initialState = { 143 | data: [], 144 | url: "/api/comments", 145 | pollInterval: 2000 146 | } 147 | 148 | var reducer = function(state, action) { 149 | if(state === undefined) { 150 | return initialState; 151 | } 152 | var newState = state; 153 | switch(action.type) { 154 | case 'add_comment': 155 | var newComments = state.data.concat([action.comment]); 156 | newState = Object.assign({}, state, {data: newComments}); 157 | break; 158 | case 'set_comments': 159 | newState = Object.assign({}, state, {data: action.data}) 160 | break; 161 | } 162 | return newState; 163 | } 164 | 165 | var store = createStore(reducer, initialState); 166 | 167 | var CommentBoxState = function(state) { 168 | return { 169 | data: state.data, 170 | url: state.url, 171 | pollInterval: state.pollInterval 172 | } 173 | } 174 | 175 | var CommentListState = function(state) { 176 | return { 177 | data: state.data 178 | } 179 | } 180 | 181 | var CommentBoxDispatch = function(dispatch) { 182 | return { 183 | addComment: function(comment) { 184 | comment.id = Date.now(); 185 | dispatch({ 186 | type: 'add_comment', 187 | comment: comment, 188 | }) 189 | }, 190 | setComments: function(data) { 191 | dispatch({ 192 | type: 'set_comments', 193 | data: data 194 | }) 195 | } 196 | } 197 | } 198 | 199 | CommentBox = connect( 200 | CommentBoxState, 201 | CommentBoxDispatch 202 | )(CommentBox) 203 | CommentList = connect( 204 | CommentListState 205 | )(CommentList) 206 | 207 | ReactDOM.render( 208 | 209 | 210 | , 211 | document.getElementById('content') 212 | ); 213 | --------------------------------------------------------------------------------