├── .babelrc ├── src ├── main.js ├── alt │ └── index.js ├── main.scss ├── components │ ├── Message.jsx │ ├── Channel.jsx │ ├── Chat.jsx │ ├── App.jsx │ ├── Login.jsx │ ├── MessageBox.jsx │ ├── MessageList.jsx │ └── ChannelList.jsx ├── routes │ └── index.js ├── sources │ ├── ChannelSource.js │ └── MessageSource.js ├── actions │ └── index.js └── stores │ └── ChatStore.js ├── index.html ├── server.js ├── README.md ├── package.json ├── webpack.config.js └── data └── messages.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | require('./main.scss'); 2 | require('./routes'); 3 | -------------------------------------------------------------------------------- /src/alt/index.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | export default new Alt(); 3 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | $color: blue; 2 | 3 | body { 4 | //color: $color; 5 | margin: 0; 6 | background-color: #F9F9F9; 7 | } 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebPackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebPackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(8080, 'localhost', function(err, result){ 10 | if(err){ 11 | return console.log(err); 12 | } 13 | 14 | console.log('Listening on localhost:8080') 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mui from 'material-ui'; 3 | 4 | var {ListItem, Avatar} = mui; 5 | 6 | class Message extends React.Component { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | render(){ 12 | return ( 13 | } 15 | >{this.props.message.message} 16 | ); 17 | } 18 | } 19 | 20 | export default Message; 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-stack 2 | Code used for a Pluralsight.com course about a real-time React stack with Flux, Webpack, and Firebase 3 | 4 | Go check out the course at https://app.pluralsight.com/library/courses/build-isomorphic-app-react-flux-webpack-firebase! 5 | 6 | # Get up and running 7 | 8 | * From the command line, clone this repo: `git clone https://github.com/hendrikswan/react-stack && cd react-stack` 9 | * Run `npm install` to install all the modules (pegged to specific NPM modules to ensure that it works for you) 10 | * Run `node server` to run the webpack dev server 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../components/App.jsx'; 3 | import Chat from '../components/Chat.jsx'; 4 | import Login from '../components/Login.jsx'; 5 | import Router from 'react-router'; 6 | let Route = Router.Route; 7 | let DefaultRoute = Router.DefaultRoute; 8 | 9 | let routes = ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | Router.run(routes, Router.HashLocation, (Root)=> { 19 | React.render(, document.getElementById('container')); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Channel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mui from 'material-ui'; 3 | import Actions from '../actions'; 4 | 5 | var {ListItem} = mui; 6 | 7 | class Channel extends React.Component { 8 | constructor(props){ 9 | super(props); 10 | } 11 | 12 | onClick(){ 13 | Actions.channelOpened(this.props.channel); 14 | } 15 | 16 | render(){ 17 | let style = {}; 18 | 19 | if(this.props.channel.selected){ 20 | style.backgroundColor = '#f0f0f0'; 21 | } 22 | 23 | return ( 24 | {this.props.channel.name} 29 | ); 30 | } 31 | } 32 | 33 | export default Channel; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stack", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "alt": "0.17.1", 13 | "babel-core": "5.8.14", 14 | "babel-loader": "5.3.2", 15 | "css-loader": "0.15.6", 16 | "firebase": "2.2.9", 17 | "lodash": "3.10.1", 18 | "material-ui": "0.10.2", 19 | "node-sass": "3.4.1", 20 | "react": "0.13.3", 21 | "react-hot-loader": "1.2.8", 22 | "react-router": "0.13.3", 23 | "react-tap-event-plugin": "0.1.7", 24 | "sass-loader": "1.0.3", 25 | "style-loader": "0.12.3", 26 | "trim": "0.0.1", 27 | "webpack": "1.10.5", 28 | "webpack-dev-server": "1.10.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/sources/ChannelSource.js: -------------------------------------------------------------------------------- 1 | import Actions from '../actions'; 2 | import Firebase from 'firebase'; 3 | 4 | let firebaseRef = new Firebase('https://react-stack.firebaseio.com/channels'); 5 | 6 | let ChannelSource = { 7 | getChannels: { 8 | remote(state, selectedChannelKey){ 9 | return new Promise((resolve, reject) => { 10 | firebaseRef.once("value", (dataSnapshot)=> { 11 | var channels = dataSnapshot.val(); 12 | selectedChannelKey = selectedChannelKey || _.keys(channels)[0]; 13 | var selectedChannel = channels[selectedChannelKey]; 14 | if(selectedChannel){ 15 | selectedChannel.selected = true; 16 | } 17 | resolve(channels); 18 | }); 19 | }); 20 | }, 21 | success: Actions.channelsReceived, 22 | error: Actions.channelsFailed 23 | } 24 | } 25 | 26 | export default ChannelSource; 27 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import Firebase from 'firebase'; 3 | 4 | class Actions { 5 | constructor(){ 6 | this.generateActions( 7 | 'channelsReceived', 8 | 'channelsFailed', 9 | 'messagesReceived', 10 | 'messagesFailed', 11 | 'channelOpened', 12 | 'messagesLoading', 13 | 'sendMessage', 14 | 'messageSendSuccess', 15 | 'messageSendError', 16 | 'messageReceived' 17 | ); 18 | } 19 | 20 | login(router){ 21 | return (dispatch) => { 22 | var firebaseRef = new Firebase('https://react-stack.firebaseio.com'); 23 | firebaseRef.authWithOAuthPopup("google", (error, user)=> { 24 | if(error){ 25 | return; 26 | } 27 | 28 | dispatch(user); 29 | 30 | router.transitionTo('/chat'); 31 | }); 32 | } 33 | } 34 | } 35 | 36 | export default alt.createActions(Actions); 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: { 7 | main: [ 8 | 'webpack-dev-server/client?http://localhost:8080', 9 | 'webpack/hot/only-dev-server', 10 | './src/main.js' 11 | ] 12 | }, 13 | output: { 14 | filename: '[name].js', 15 | path: path.join(__dirname, 'public'), 16 | publicPath: '/public/' 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | module: { 23 | loaders: [ 24 | { 25 | test: /\.jsx?$/, 26 | include: path.join(__dirname, 'src'), 27 | loader: 'react-hot!babel' 28 | }, 29 | { 30 | test: /\.scss$/, 31 | include: path.join(__dirname, 'src'), 32 | loader: 'style!css!sass' 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Chat.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MessageList from './MessageList.jsx'; 3 | import ChannelList from './ChannelList.jsx'; 4 | import MessageBox from './MessageBox.jsx'; 5 | import ChatStore from '../stores/ChatStore'; 6 | 7 | class Chat extends React.Component { 8 | render(){ 9 | return ( 10 |
11 |
18 | 19 | 20 |
21 | 22 |
23 | ); 24 | } 25 | 26 | static willTransitionTo(transition){ 27 | var state = ChatStore.getState(); 28 | if(!state.user){ 29 | transition.redirect('/login'); 30 | } 31 | } 32 | } 33 | 34 | export default Chat; 35 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mui from 'material-ui'; 3 | import {RouteHandler} from 'react-router'; 4 | 5 | var ThemeManager = new mui.Styles.ThemeManager(); 6 | var Colors = mui.Styles.Colors; 7 | var AppBar = mui.AppBar; 8 | 9 | class App extends React.Component { 10 | constructor(){ 11 | super(); 12 | 13 | ThemeManager.setPalette({ 14 | primary1Color: Colors.blue500, 15 | primary2Color: Colors.blue700, 16 | primary3Color: Colors.blue100, 17 | accent1Color: Colors.pink400 18 | }); 19 | } 20 | 21 | 22 | static childContextTypes = { 23 | muiTheme: React.PropTypes.object 24 | } 25 | 26 | getChildContext(){ 27 | return { 28 | muiTheme: ThemeManager.getCurrentTheme() 29 | }; 30 | } 31 | 32 | render(){ 33 | 34 | return ( 35 |
36 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mui from 'material-ui'; 3 | import Actions from '../actions'; 4 | 5 | var { 6 | Card, 7 | CardText, 8 | RaisedButton 9 | } = mui; 10 | 11 | 12 | class Login extends React.Component { 13 | 14 | onClick(){ 15 | Actions.login(this.context.router); 16 | } 17 | 18 | static contextTypes = { 19 | router: React.PropTypes.func.isRequired 20 | } 21 | 22 | render(){ 23 | 24 | return ( 25 | 30 | 33 | To start chatting away, please log in with your Google account. 34 | 35 | 36 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | 47 | module.exports = Login; 48 | -------------------------------------------------------------------------------- /src/components/MessageBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mui from 'material-ui'; 3 | import trim from 'trim'; 4 | import Actions from '../actions'; 5 | 6 | var {Card} = mui; 7 | 8 | class MessageBox extends React.Component { 9 | constructor(props){ 10 | super(props); 11 | this.state = { 12 | message: '' 13 | } 14 | } 15 | 16 | onChange(evt){ 17 | this.setState({ 18 | message: evt.target.value 19 | }); 20 | } 21 | 22 | onKeyUp(evt){ 23 | if(evt.keyCode === 13 && trim(evt.target.value) != ''){ 24 | evt.preventDefault(); 25 | 26 | Actions.sendMessage(this.state.message); 27 | 28 | this.setState({ 29 | message: '' 30 | }); 31 | 32 | } 33 | } 34 | 35 | render(){ 36 | return ( 37 | 42 |