├── index.js ├── components ├── users │ ├── User.jsx │ ├── UserList.jsx │ ├── UserSection.jsx │ └── UserForm.jsx ├── messages │ ├── MessageList.jsx │ ├── Message.jsx │ ├── MessageSection.jsx │ └── MessageForm.jsx ├── channels │ ├── ChannelList.jsx │ ├── Channel.jsx │ ├── ChannelForm.jsx │ └── ChannelSection.jsx └── App.jsx ├── index.html ├── webpack.config.js ├── package.json ├── .gitignore ├── socket.js ├── app.css └── README.md /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App.jsx'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); -------------------------------------------------------------------------------- /components/users/User.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class User extends Component{ 4 | render(){ 5 | return ( 6 |
  • 7 | {this.props.user.name} 8 |
  • 9 | ) 10 | } 11 | } 12 | 13 | User.propTypes = { 14 | user: React.PropTypes.object.isRequired 15 | } 16 | 17 | export default User -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Acme Support 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './index.js', 3 | output: { 4 | path: __dirname, 5 | filename: 'bundle.js' 6 | }, 7 | module: { 8 | loaders: [ 9 | { 10 | test: /\.jsx?$/, 11 | loader: 'babel-loader', 12 | exclude: /node_modules/, 13 | query: { 14 | presets: ['es2015', 'react'] 15 | } 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/messages/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Message from './Message.jsx'; 3 | 4 | class MessageList extends Component{ 5 | render(){ 6 | return ( 7 | 14 | ) 15 | } 16 | } 17 | MessageList.propTypes = { 18 | messages: React.PropTypes.array.isRequired 19 | } 20 | export default MessageList -------------------------------------------------------------------------------- /components/users/UserList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import User from './User.jsx'; 3 | 4 | class UserList extends React.Component{ 5 | render(){ 6 | return ( 7 | 16 | ) 17 | } 18 | } 19 | 20 | UserList.propTypes = { 21 | users: React.PropTypes.array.isRequired 22 | } 23 | 24 | export default UserList -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtsupport", 3 | "version": "1.0.0", 4 | "description": "Realtime support frontend", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "James Moore ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "babel-core": "^6.8.0", 13 | "babel-loader": "^6.2.4", 14 | "babel-preset-es2015": "^6.6.0", 15 | "babel-preset-react": "^6.5.0", 16 | "fecha": "^1.0.0", 17 | "react": "^0.14.2", 18 | "react-dom": "^0.14.2", 19 | "webpack": "^1.12.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/channels/ChannelList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Channel from './Channel.jsx'; 3 | 4 | class ChannelList extends Component{ 5 | render(){ 6 | return ( 7 | 16 | ) 17 | } 18 | } 19 | 20 | ChannelList.propTypes = { 21 | channels: React.PropTypes.array.isRequired, 22 | setChannel: React.PropTypes.func.isRequired, 23 | activeChannel: React.PropTypes.object.isRequired 24 | } 25 | 26 | export default ChannelList -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 27 | node_modules 28 | 29 | dist 30 | 31 | .DS_Store 32 | 33 | bundle.js 34 | -------------------------------------------------------------------------------- /components/messages/Message.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import fecha from 'fecha'; 3 | 4 | class Message extends Component{ 5 | render(){ 6 | let {message} = this.props; 7 | let createdAt = fecha.format(new Date(message.createdAt), 'HH:mm:ss MM/DD/YY'); 8 | return ( 9 |
  • 10 |
    11 | {message.author} 12 | {createdAt} 13 |
    14 |
    {message.body}
    15 |
  • 16 | ) 17 | } 18 | } 19 | 20 | Message.propTypes = { 21 | message: React.PropTypes.object.isRequired 22 | } 23 | 24 | export default Message -------------------------------------------------------------------------------- /components/users/UserSection.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserForm from './UserForm.jsx'; 3 | import UserList from './UserList.jsx'; 4 | 5 | class UserSection extends React.Component{ 6 | render(){ 7 | return ( 8 |
    9 |
    10 | Users 11 |
    12 |
    13 | 14 | 15 |
    16 |
    17 | ) 18 | } 19 | } 20 | 21 | UserSection.propTypes = { 22 | users: React.PropTypes.array.isRequired, 23 | setUserName: React.PropTypes.func.isRequired 24 | } 25 | 26 | export default UserSection -------------------------------------------------------------------------------- /components/channels/Channel.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Channel extends Component{ 4 | onClick(e){ 5 | e.preventDefault(); 6 | const {setChannel, channel} = this.props; 7 | setChannel(channel); 8 | } 9 | render(){ 10 | const {channel, activeChannel} = this.props; 11 | const active = channel === activeChannel ? 'active' : ''; 12 | return ( 13 |
  • 14 | 15 | {channel.name} 16 | 17 |
  • 18 | ) 19 | } 20 | } 21 | 22 | Channel.propTypes = { 23 | channel: React.PropTypes.object.isRequired, 24 | setChannel: React.PropTypes.func.isRequired, 25 | activeChannel: React.PropTypes.object.isRequired 26 | } 27 | 28 | export default Channel -------------------------------------------------------------------------------- /components/users/UserForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class UserForm extends Component{ 4 | onSubmit(e){ 5 | e.preventDefault(); 6 | const node = this.refs.userName; 7 | const userName = node.value; 8 | this.props.setUserName(userName); 9 | node.value = ''; 10 | } 11 | render(){ 12 | return ( 13 |
    14 |
    15 | 20 |
    21 |
    22 | ) 23 | } 24 | } 25 | 26 | UserForm.propTypes = { 27 | setUserName: React.PropTypes.func.isRequired 28 | } 29 | 30 | export default UserForm -------------------------------------------------------------------------------- /components/channels/ChannelForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class ChannelForm extends Component{ 4 | onSubmit(e){ 5 | e.preventDefault(); 6 | const node = this.refs.channel; 7 | const channelName = node.value; 8 | this.props.addChannel(channelName); 9 | node.value = ''; 10 | } 11 | render(){ 12 | return ( 13 |
    14 |
    15 | 21 |
    22 | 23 |
    24 | ) 25 | } 26 | } 27 | 28 | ChannelForm.propTypes = { 29 | addChannel: React.PropTypes.func.isRequired 30 | } 31 | 32 | 33 | export default ChannelForm -------------------------------------------------------------------------------- /components/messages/MessageSection.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import MessageList from './MessageList.jsx'; 3 | import MessageForm from './MessageForm.jsx'; 4 | 5 | class MessageSection extends Component{ 6 | render(){ 7 | let {activeChannel} = this.props; 8 | return ( 9 |
    10 |
    {activeChannel.name || 'Select A Channel'}
    11 |
    12 | 13 | 14 |
    15 |
    16 | ) 17 | } 18 | } 19 | 20 | MessageSection.propTypes = { 21 | messages: React.PropTypes.array.isRequired, 22 | activeChannel: React.PropTypes.object.isRequired, 23 | addMessage: React.PropTypes.func.isRequired 24 | } 25 | 26 | export default MessageSection -------------------------------------------------------------------------------- /components/channels/ChannelSection.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ChannelForm from './ChannelForm.jsx'; 3 | import ChannelList from './ChannelList.jsx'; 4 | 5 | class ChannelSection extends Component{ 6 | render(){ 7 | return ( 8 |
    9 |
    10 | Channels 11 |
    12 |
    13 | 14 | 15 |
    16 |
    17 | 18 | ) 19 | } 20 | } 21 | 22 | ChannelSection.propTypes = { 23 | channels: React.PropTypes.array.isRequired, 24 | setChannel: React.PropTypes.func.isRequired, 25 | addChannel: React.PropTypes.func.isRequired, 26 | activeChannel: React.PropTypes.object.isRequired 27 | } 28 | 29 | export default ChannelSection -------------------------------------------------------------------------------- /socket.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | 3 | class Socket { 4 | constructor(ws = new WebSocket(), ee = new EventEmitter()){ 5 | this.ws = ws; 6 | this.ee = ee; 7 | ws.onmessage = this.message.bind(this); 8 | ws.onopen = this.open.bind(this); 9 | ws.onclose = this.close.bind(this); 10 | } 11 | on(name, fn){ 12 | this.ee.on(name, fn); 13 | } 14 | off(name, fn){ 15 | this.ee.removeListener(name, fn); 16 | } 17 | emit(name, data){ 18 | const message = JSON.stringify({name, data}); 19 | this.ws.send(message); 20 | } 21 | message(e){ 22 | try{ 23 | const message = JSON.parse(e.data); 24 | this.ee.emit(message.name, message.data); 25 | } 26 | catch(err){ 27 | this.ee.emit('error', err); 28 | } 29 | } 30 | open(){ 31 | this.ee.emit('connect'); 32 | } 33 | close(){ 34 | this.ee.emit('disconnect'); 35 | } 36 | } 37 | 38 | export default Socket; -------------------------------------------------------------------------------- /components/messages/MessageForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class MessageForm extends Component{ 4 | onSubmit(e){ 5 | e.preventDefault(); 6 | const node = this.refs.message; 7 | const message = node.value; 8 | this.props.addMessage(message); 9 | node.value = ''; 10 | } 11 | render(){ 12 | let input; 13 | if(this.props.activeChannel.id !== undefined){ 14 | input = ( 15 | 20 | ) 21 | } 22 | return ( 23 |
    24 |
    25 | {input} 26 |
    27 |
    28 | ) 29 | } 30 | } 31 | 32 | MessageForm.propTypes = { 33 | activeChannel: React.PropTypes.object.isRequired, 34 | addMessage: React.PropTypes.func.isRequired 35 | } 36 | 37 | export default MessageForm -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | .app{ 2 | display:flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | height: 100vh; 6 | font-size: 16px; 7 | } 8 | .nav{ 9 | flex-basis: 20%; 10 | } 11 | 12 | .messages-container{ 13 | flex-basis: 80%; 14 | margin-bottom: 0; 15 | height: 100vh; 16 | } 17 | .support, .users{ 18 | height: 50vh; 19 | margin-bottom: 0; 20 | } 21 | .channels, .users, .messages{ 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: space-between; 25 | } 26 | .channels, .users{ 27 | height: 88%; 28 | } 29 | .messages{ 30 | height: 94%; 31 | } 32 | .channels > ul, .messages > ul, .users > ul{ 33 | flex: 1; 34 | overflow-y: auto; 35 | } 36 | .message{ 37 | /*display: flex;*/ 38 | } 39 | .message > .author{ 40 | flex-basis: 100%; 41 | } 42 | .message > .body{ 43 | flex-basis: 100%; 44 | } 45 | .active { 46 | background-color: #E8E8E8; 47 | } 48 | .timestamp{ 49 | color: #808080; 50 | font-style: italic; 51 | padding-left: 10px; 52 | font-size: 13px; 53 | } 54 | .form-group{ 55 | margin-bottom: 0; 56 | } 57 | ul{ 58 | list-style-type:none; 59 | padding: 0; 60 | } 61 | li{ 62 | padding: 0 4px 0 4px; 63 | } 64 | a{ 65 | cursor: pointer; 66 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn How to Develop Realtime Web Apps 2 | This repo contains the source for the React Frontend for the following course on Building Realtime Web Apps with Reactjs, Golang & RethinkDB 3 | http://courses.knowthen.com/courses/learn-how-to-develop-realtime-web-apps 4 | 5 | This Course Teaches you everything you need to know to build Realtime Web Apps using React, Golang & RethinkDB. 6 | 7 | #### Why you should take this course? 8 | 9 | Software developers **will need to know** how to create Realtime apps in the **very near future**. It's the next wave in the webs evolution, in fact Realtime Apps/Features are already being created at companies like Twitter, Facebook and Google. 10 | 11 | **Get ahead** of the curve and learn how to make realtime apps now, using the following cool technologies: 12 | 13 | [React](https://facebook.github.io/react/) - Facebook's Open-Source Javascript Library for building user interfaces 14 | 15 | [Go](https://golang.org/) - Googles New Open-Source programming language that's great for Realtime apps 16 | 17 | [RethinkDB](http://rethinkdb.com/) - A cool, new Open-Source database for the realtime web 18 | 19 | #### Why this course? 20 | 21 | Do you ever feel overwhelmed with new technologies? I think most of us do, there is so much change constantly happening and the pace of change seems to be increasing. 22 | 23 | What can you do to manage the learning challenges facing software developers? 24 | 25 | #### Lean learning 26 | 27 | I don't want to waste your time, so you'll learn just what you need to know as quickly as possible. You'll start this course with the end in mind. What do I mean by that? We're going to be building a web application, and you'll learn just what you need to build the app. 28 | 29 | -------------------------------------------------------------------------------- /components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ChannelSection from './channels/ChannelSection.jsx'; 3 | import UserSection from './users/UserSection.jsx'; 4 | import MessageSection from './messages/MessageSection.jsx'; 5 | import Socket from '../socket.js'; 6 | 7 | class App extends Component{ 8 | constructor(props){ 9 | super(props); 10 | this.state = { 11 | channels: [], 12 | users: [], 13 | messages: [], 14 | activeChannel: {}, 15 | connected: false 16 | }; 17 | } 18 | componentDidMount(){ 19 | let ws = new WebSocket('ws://localhost:4000') 20 | let socket = this.socket = new Socket(ws); 21 | socket.on('connect', this.onConnect.bind(this)); 22 | socket.on('disconnect', this.onDisconnect.bind(this)); 23 | socket.on('channel add', this.onAddChannel.bind(this)); 24 | socket.on('user add', this.onAddUser.bind(this)); 25 | socket.on('user edit', this.onEditUser.bind(this)); 26 | socket.on('user remove', this.onRemoveUser.bind(this)); 27 | socket.on('message add', this.onMessageAdd.bind(this)); 28 | } 29 | onMessageAdd(message){ 30 | let {messages} = this.state; 31 | messages.push(message); 32 | this.setState({messages}); 33 | } 34 | onRemoveUser(removeUser){ 35 | let {users} = this.state; 36 | users = users.filter(user => { 37 | return user.id !== removeUser.id; 38 | }); 39 | this.setState({users}); 40 | } 41 | onAddUser(user){ 42 | let {users} = this.state; 43 | users.push(user); 44 | this.setState({users}); 45 | } 46 | onEditUser(editUser){ 47 | let {users} = this.state; 48 | users = users.map(user => { 49 | if(editUser.id === user.id){ 50 | return editUser; 51 | } 52 | return user; 53 | }); 54 | this.setState({users}); 55 | } 56 | onConnect(){ 57 | this.setState({connected: true}); 58 | this.socket.emit('channel subscribe'); 59 | this.socket.emit('user subscribe'); 60 | } 61 | onDisconnect(){ 62 | this.setState({connected: false}); 63 | } 64 | onAddChannel(channel){ 65 | let {channels} = this.state; 66 | channels.push(channel); 67 | this.setState({channels}); 68 | } 69 | addChannel(name){ 70 | this.socket.emit('channel add', {name}); 71 | } 72 | setChannel(activeChannel){ 73 | this.setState({activeChannel}); 74 | this.socket.emit('message unsubscribe'); 75 | this.setState({messages: []}); 76 | this.socket.emit('message subscribe', 77 | {channelId: activeChannel.id}); 78 | } 79 | setUserName(name){ 80 | this.socket.emit('user edit', {name}); 81 | } 82 | addMessage(body){ 83 | let {activeChannel} = this.state; 84 | this.socket.emit('message add', 85 | {channelId: activeChannel.id, body}); 86 | } 87 | render(){ 88 | return ( 89 |
    90 |
    91 | 96 | 100 |
    101 | 105 |
    106 | 107 | 108 | ) 109 | } 110 | } 111 | 112 | export default App 113 | --------------------------------------------------------------------------------