├── .gitignore ├── .babelrc ├── CHECKS ├── app-entry.js ├── screencast.gif ├── config.js ├── index.html ├── .eslintrc ├── app-server.js ├── webpack.config.js ├── models └── Message.js ├── package.json ├── README.md └── app-client.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public/dist -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } -------------------------------------------------------------------------------- /CHECKS: -------------------------------------------------------------------------------- 1 | WAIT=30 2 | ATTEMPTS=10 3 | / React Chat App 4 | 5 | -------------------------------------------------------------------------------- /app-entry.js: -------------------------------------------------------------------------------- 1 | // app-entry.js 2 | require('babel-register') 3 | require('./app-server.js') -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmicapp-react-chat/master/screencast.gif -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // config.js 2 | export default { 3 | bucket: { 4 | slug: process.env.COSMIC_BUCKET || 'cosmic-js-chat', 5 | type_slug: 'messages', 6 | read_key: process.env.COSMIC_READ_KEY, 7 | write_key: process.env.COSMIC_WRITE_KEY 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Chat App 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "react" 10 | ], 11 | "rules": { 12 | "react/jsx-no-bind": 0, 13 | "no-new": 0, 14 | "curly": [2, "multi-or-nest"], 15 | "comma-dangle": [2, "never"], 16 | "semi": 0, 17 | "camelcase": 0, 18 | "new-cap": 0, 19 | "strict": 0, 20 | "no-underscore-dangle": 0, 21 | "no-use-before-define": 0, 22 | "eol-last": 0, 23 | "quotes": [2, "single"], 24 | "jsx-quotes": 1, 25 | "react/jsx-no-undef": 1, 26 | "react/jsx-uses-react": 1, 27 | "react/jsx-uses-vars": 1 28 | } 29 | } -------------------------------------------------------------------------------- /app-server.js: -------------------------------------------------------------------------------- 1 | // app-server.js 2 | import express from 'express' 3 | const app = express() 4 | // Set port 5 | app.set('port', process.env.PORT || 3000) 6 | // Static files 7 | app.use(express.static('public')) 8 | const http = require('http').Server(app) 9 | const io = require('socket.io')(http) 10 | 11 | // Config 12 | import config from './config' 13 | 14 | // Models 15 | import Message from './models/Message' 16 | 17 | // Listen for a connection 18 | io.on('connection', socket => { 19 | // Create message 20 | socket.on('chat message', params => { 21 | Message.create(config, params, (message) => { 22 | io.emit('chat message', message); 23 | }) 24 | }) 25 | }) 26 | 27 | // Route 28 | app.get('/', (req, res) => { 29 | res.sendFile(__dirname + '/index.html') 30 | }) 31 | 32 | http.listen(app.get('port'), () => { 33 | console.log('React Chat App listening on ' + app.get('port')) 34 | }) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | var webpack = require('webpack') 3 | 4 | var loaders = []; 5 | // JS loaders 6 | if (process.env.NODE_ENV === 'development') { 7 | var js_loaders = { 8 | test: /\.js$/, 9 | loaders: ['react-hot','babel'], 10 | exclude: /node_modules/ 11 | } 12 | } else { 13 | var js_loaders = { 14 | test: /\.js$/, 15 | loaders: ['babel'], 16 | exclude: /node_modules/ 17 | } 18 | } 19 | loaders.push(js_loaders) 20 | // Development loaders 21 | if(process.env.NODE_ENV === 'development'){ 22 | var es_lint = { 23 | test: /\.js$/, 24 | loader: 'eslint-loader', 25 | exclude: /node_modules/ 26 | } 27 | loaders.push(es_lint) 28 | } 29 | module.exports = { 30 | devtool: 'source-map', 31 | entry: './app-client.js', 32 | output: { 33 | path: __dirname + '/public/dist', 34 | filename: 'bundle.js', 35 | publicPath: '/dist/' 36 | }, 37 | module: { 38 | loaders: loaders 39 | }, 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env.COSMIC_BUCKET': JSON.stringify(process.env.COSMIC_BUCKET), 43 | 'process.env.APP_URL': JSON.stringify(process.env.APP_URL) 44 | }) 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /models/Message.js: -------------------------------------------------------------------------------- 1 | // models/Message.js 2 | import Cosmic from 'cosmicjs' 3 | 4 | export default { 5 | create: (config, params, callback) => { 6 | const object = { 7 | title: params.author + ': ' + params.message, 8 | type_slug: config.bucket.type_slug, 9 | metafields: [ 10 | { 11 | title: 'Message', 12 | key: 'message', 13 | value: params.message, 14 | type: 'textarea', 15 | edit: 1 16 | }, 17 | { 18 | title: 'Author', 19 | key: 'author', 20 | value: params.author, 21 | type: 'text', 22 | edit: 1 23 | } 24 | ], 25 | options: { 26 | slug_field: 0, 27 | content_editor: 0, 28 | add_metafields: 0, 29 | metafields_title: 0, 30 | metafields_key: 0 31 | } 32 | } 33 | Cosmic.addObject(config, object, (err, res) => { 34 | const new_object = res.object 35 | const message = { 36 | _id: new_object._id, 37 | metafield: { 38 | message: { 39 | value: new_object.metafields[0].value 40 | }, 41 | author: { 42 | value: new_object.metafields[1].value 43 | } 44 | } 45 | } 46 | callback(message) 47 | }) 48 | } 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-js-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app-client.js", 6 | "dependencies": { 7 | "babel": "^6.3.26", 8 | "babel-core": "^6.4.5", 9 | "babel-loader": "^6.2.1", 10 | "babel-preset-es2015": "^6.3.13", 11 | "babel-preset-react": "^6.3.13", 12 | "babel-preset-stage-2": "^6.3.13", 13 | "babel-register": "^6.4.3", 14 | "cosmicjs": "^2.0.0", 15 | "express": "^4.13.3", 16 | "lodash": "^4.0.0", 17 | "node-uuid": "^1.4.7", 18 | "react": "^0.14.1", 19 | "react-bootstrap": "^0.28.2", 20 | "react-dom": "^0.14.1", 21 | "shorti": "^1.1.3", 22 | "socket.io": "^1.3.7", 23 | "socket.io-client": "^1.3.7", 24 | "webpack": "^1.12.2" 25 | }, 26 | "scripts": { 27 | "start": "npm run production", 28 | "development": "npm run webpack-dev-server", 29 | "production": "webpack -p && node app-entry.js", 30 | "server": "nodemon app-entry.js", 31 | "webpack-dev-server": "NODE_ENV=development webpack-dev-server --hot --inline --devtool inline-source-map --history-api-fallback" 32 | }, 33 | "author": "", 34 | "license": "ISC", 35 | "devDependencies": { 36 | "babel-eslint": "^5.0.0-beta6", 37 | "babel-preset-react": "^6.3.13", 38 | "eslint": "^1.10.3", 39 | "eslint-config-airbnb": "^3.0.2", 40 | "eslint-loader": "^1.2.0", 41 | "eslint-plugin-react": "^3.14.0", 42 | "react-hot-loader": "^1.3.0", 43 | "webpack-dev-server": "^1.12.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Chat App 2 | ![Screencast](https://github.com/tonyspiro/react-chat-app/blob/master/screencast.gif) 3 | 4 | This is an example of a basic real time chat app using React, Socket.io and Cosmic JS. This example consists of the following: 5 | 6 | 1. [React](https://facebook.github.io/react/) for UI 7 | 2. [Babel](https://babeljs.io/) for ES6 and JSX transformation 8 | 3. [Webpack](https://webpack.github.io/) for bundling 9 | 4. [Socket.io](http://socket.io/) for real-time communication 10 | 5. [Cosmic JS](https://cosmicjs.com) for saving and returning messages from a cloud-hosted API 11 | 12 | The following dev tools are used: 13 | 14 | 1. [ESLint](http://eslint.org/) to make sure our code is consistent 15 | 2. [React Hot Loader](https://github.com/gaearon/react-hot-loader) for instant updates on save 16 | 17 | ### Install 18 | Run the following commands to install the app: 19 | ``` 20 | git clone https://github.com/tonyspiro/react-chat-app 21 | cd react-chat-app 22 | npm install 23 | ``` 24 | #### Run in production 25 | Run the following command to run the app in production: 26 | ``` 27 | npm start 28 | ``` 29 | View the app running in production at [http://localhost:3000](http://localhost:3000) 30 | 31 | #### Run in development 32 | Run the following commands to run the app in development with hot reloading: 33 | ``` 34 | npm start server 35 | ``` 36 | and in another terminal tab run: 37 | ``` 38 | npm run development 39 | ``` 40 | View the app running in development at [http://localhost:8080](http://localhost:8080) 41 | 42 | ### Configure your own chat app 43 | 1. Set up a bucket in [Cosmic JS](https://cosmicjs.com) with an object type of `messages`. 44 | 2. Edit config.js: 45 | ```javascript 46 | // config.js 47 | export default { 48 | bucket: { 49 | slug: 'your-bucket-slug', 50 | type_slug: 'messages' 51 | }, 52 | server: { 53 | host: process.env.APP_URL || 'http://localhost:3000' 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /app-client.js: -------------------------------------------------------------------------------- 1 | // app-client.js 2 | import React, { Component } from 'react' 3 | import { render } from 'react-dom' 4 | import Cosmic from 'cosmicjs' 5 | import io from 'socket.io-client' 6 | import config from './config' 7 | import uuid from 'node-uuid' 8 | import S from 'shorti' 9 | import _ from 'lodash' 10 | import { Input } from 'react-bootstrap' 11 | 12 | class App extends Component { 13 | 14 | constructor() { 15 | super() 16 | this.state = { 17 | data: { 18 | messages: [] 19 | } 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | let data = this.state.data 25 | setTimeout(() => { 26 | this.refs.author.refs.input.focus() 27 | }, 100) 28 | const socket = io() 29 | Cosmic.getObjects(config, (err, res) => { 30 | const messages = res.objects.type.messages 31 | if (messages) { 32 | messages.reverse() 33 | this.setState({ 34 | data: { 35 | author: data.author, 36 | messages 37 | } 38 | }) 39 | } 40 | }) 41 | // Listen for messages coming in 42 | socket.on('chat message', message => { 43 | data = this.state.data 44 | const messages = this.state.data.messages 45 | if (data.author !== message.metafield.author.value) { 46 | messages.push(message) 47 | this.setState({ 48 | data: { 49 | author: data.author, 50 | messages 51 | } 52 | }) 53 | } 54 | }) 55 | } 56 | 57 | componentDidUpdate() { 58 | if (this.refs.message) 59 | this.refs.message.refs.input.focus() 60 | if (this.refs.messages_scroll_area) 61 | this.refs.messages_scroll_area.scrollTop = this.refs.messages_scroll_area.scrollHeight 62 | } 63 | 64 | setAuthor() { 65 | const author = this.refs.author.refs.input.value.trim() 66 | if (!author) 67 | return 68 | this.refs.author.refs.input.value = '' 69 | const messages = this.state.data.messages 70 | this.setState({ 71 | data: { 72 | author, 73 | messages 74 | } 75 | }) 76 | } 77 | 78 | createMessage() { 79 | const data = this.state.data 80 | const messages = data.messages 81 | const socket = io() 82 | const message_text = this.refs.message.refs.input.value.trim() 83 | if (!message_text) 84 | return 85 | const message_emit = { 86 | message: message_text, 87 | author: data.author 88 | } 89 | // Send message out 90 | socket.emit('chat message', message_emit) 91 | // Render to browser 92 | const message_browser = { 93 | _id: uuid.v1(), 94 | metafield: { 95 | author: { 96 | value: data.author 97 | }, 98 | message: { 99 | value: message_text 100 | } 101 | } 102 | } 103 | messages.push(message_browser) 104 | this.setState({ 105 | data: { 106 | author: data.author, 107 | messages 108 | } 109 | }) 110 | this.refs.message.refs.input.value = '' 111 | } 112 | 113 | handleSubmit(e) { 114 | e.preventDefault() 115 | const data = this.state.data 116 | if (data.author) 117 | this.createMessage() 118 | else 119 | this.setAuthor() 120 | } 121 | 122 | render() { 123 | const data = this.state.data 124 | let form_input 125 | if (!data.author) { 126 | form_input = ( 127 |
128 | Hi, what is your name?
129 | 130 |
131 | ) 132 | } else { 133 | form_input = ( 134 |
135 | Hello { data.author }, type a message:
136 | 137 |
138 | ) 139 | } 140 | const messages = data.messages 141 | let messages_list 142 | if (messages) { 143 | // order by created 144 | const sorted_messages = _.sortBy(messages, message => { 145 | return message.created 146 | }) 147 | messages_list = sorted_messages.map(message_object => { 148 | if (message_object) { 149 | return ( 150 |
  • 151 | { message_object.metafield.author.value }
    152 | { message_object.metafield.message.value } 153 |
  • 154 | ) 155 | } 156 | }) 157 | } 158 | const scroll_area_style = { 159 | ...S('h-' + (window.innerHeight - 140)), 160 | overflowY: 'scroll' 161 | } 162 | return ( 163 |
    164 |
    165 |

    React Chat App

    166 |
    167 |
      { messages_list }
    168 |
    169 |
    170 |
    171 |
    172 | { form_input } 173 |
    174 |
    175 |
    176 | ) 177 | } 178 | } 179 | const app = document.getElementById('app') 180 | render(, app) 181 | --------------------------------------------------------------------------------