├── 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 | {
8 | this.props.messages.map( message =>{
9 | return (
10 |
11 | )
12 | })
13 | }
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 | {
8 | this.props.users.map( user =>{
9 | return (
10 | )
14 | })
15 | }
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 | {
8 | this.props.channels.map( chan =>{
9 | return
14 | })
15 | }
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------