├── .babelrc ├── .gitignore ├── README.md ├── bundle-stats.js ├── package.json ├── server ├── bundle.js └── server.js ├── src ├── Components │ └── chat.js ├── Reducers │ ├── app.js │ ├── index.js │ └── messages.js ├── actions │ └── actions.js ├── containers │ └── App.js ├── index.js └── styles.styl ├── webpack.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "experimental": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc 2 | node_modules 3 | build 4 | dist 5 | 6 | # Compiled source # 7 | ################### 8 | *.com 9 | *.class 10 | *.dll 11 | *.exe 12 | *.o 13 | *.so 14 | 15 | # Packages # 16 | ############ 17 | # it's better to unpack these files and commit the raw source 18 | # git has its own built in compression methods 19 | *.7z 20 | *.dmg 21 | *.gz 22 | *.iso 23 | *.jar 24 | *.rar 25 | *.tar 26 | *.zip 27 | 28 | # Logs and databases # 29 | ###################### 30 | *.log 31 | *.sql 32 | *.sqlite 33 | 34 | # OS generated files # 35 | ###################### 36 | .DS_Store 37 | .DS_Store? 38 | ._* 39 | .Spotlight-V100 40 | .Trashes 41 | ehthumbs.db 42 | Thumbs.db 43 | ======= 44 | node_modules 45 | *.log 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This tiny project is my first real experinent with React, Redux and Firebase. 2 | 3 | Tips: 4 | * Analyze: 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gp-boilerplate", 3 | "version": "1.0.0", 4 | "description": "React + Stylus boileplate", 5 | "main": "app/app.js", 6 | "author": "George Papadakis", 7 | "scripts": { 8 | "start": "node server/server.js", 9 | "deploy": "NODE_ENV=development webpack -p --display-modules --config webpack.production.config.js", 10 | "watch": "webpack --watch" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "webpack" 15 | ], 16 | "license": "MIT", 17 | "devDependencies": { 18 | "autoprefixer-loader": "^2.0.0", 19 | "babel-core": "^5.7.4", 20 | "babel-loader": "^5.3.2", 21 | "babel-runtime": "^5.8.19", 22 | "css-loader": "^0.15.5", 23 | "cssnext-loader": "^1.0.1", 24 | "express": "^4.13.1", 25 | "extract-text-webpack-plugin": "^0.8.2", 26 | "html-webpack-plugin": "^1.6.0", 27 | "http-proxy": "^1.11.1", 28 | "node-libs-browser": "^0.5.2", 29 | "open": "0.0.5", 30 | "re-base": "^1.1.1", 31 | "react": "^0.13.3", 32 | "react-hot-loader": "^1.2.8", 33 | "react-redux": "^0.2.2", 34 | "reactfire": "^0.5.0", 35 | "redux": "^1.0.0-rc", 36 | "redux-devtools": "^0.1.2", 37 | "redux-thunk": "^0.1.0", 38 | "style-loader": "^0.12.3", 39 | "stylus-loader": "^1.2.1", 40 | "webpack": "^1.10.1", 41 | "webpack-dev-server": "^1.10.1" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "not yet" 46 | }, 47 | "dependencies": { 48 | "webpack-stats-plugin": "^0.1.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/bundle.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var webpackConfig = require('./../webpack.config.js'); 4 | 5 | module.exports = function () { 6 | 7 | var bundleStart = null, 8 | compiler = webpack(webpackConfig); 9 | 10 | compiler.plugin('compile', function() { 11 | console.log('Bundling...'); 12 | bundleStart = Date.now(); 13 | }); 14 | 15 | compiler.plugin('done', function() { 16 | console.log('Bundled in ' + (Date.now() - bundleStart) + 'ms!'); 17 | }); 18 | 19 | var bundler = new WebpackDevServer(compiler, { 20 | publicPath: '/build/', 21 | inline: true, 22 | hot: true, 23 | quiet: false, 24 | noInfo: true, 25 | stats: { 26 | colors: true 27 | } 28 | }); 29 | 30 | bundler.listen(3001, 'localhost', function () { 31 | console.log('Bundling project, please wait...'); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var http = require('http'); 3 | var express = require('express'); 4 | var httpProxy = require('http-proxy'); 5 | var proxy = httpProxy.createProxyServer({ 6 | changeOrigin: true, 7 | ws: true 8 | }); 9 | var app = express(); 10 | var isProduction = process.env.NODE_ENV === 'production'; 11 | var port = isProduction ? process.env.PORT : 3000; 12 | var publicPath = path.resolve(__dirname, '../build'); 13 | 14 | app.use(express.static(publicPath)); 15 | 16 | app.all('/api/*', function (req, res) { 17 | proxy.web(req, res, { 18 | target: 'http://192.168.10.250/api' 19 | }); 20 | }); 21 | 22 | if (!isProduction) { 23 | 24 | var bundle = require('./bundle.js'); 25 | bundle(); 26 | app.all('/build/*', function (req, res) { 27 | proxy.web(req, res, { 28 | target: 'http://127.0.0.1:3001' 29 | }); 30 | }); 31 | app.all('/socket.io*', function (req, res) { 32 | proxy.web(req, res, { 33 | target: 'http://127.0.0.1:3001' 34 | }); 35 | }); 36 | 37 | 38 | proxy.on('error', function(e) { 39 | // Just catch it 40 | }); 41 | 42 | // We need to use basic HTTP service to proxy 43 | // websocket requests from webpack 44 | var server = http.createServer(app); 45 | 46 | server.on('upgrade', function (req, socket, head) { 47 | proxy.ws(req, socket, head); 48 | }); 49 | 50 | server.listen(port, function () { 51 | console.log('Server running on port ' + port); 52 | }); 53 | 54 | } else { 55 | // And run the server 56 | app.listen(port, function () { 57 | console.log('Server running on port ' + port); 58 | }); 59 | } 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Components/chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Rebase from 're-base'; 3 | import { TransitionSpring } from 'react-motion'; 4 | 5 | const base = Rebase.createClass('https://reduxchat.firebaseio.com'); 6 | 7 | export default class Chat extends Component { 8 | constructor (props) { 9 | super(props); 10 | this.state = { 11 | content: '', 12 | username: this.props.username 13 | }; 14 | } 15 | 16 | componentWillReceiveProps () { 17 | setTimeout(::this.gotoBottom, 1); 18 | } 19 | 20 | handleSubmit (event) { 21 | event.preventDefault(); 22 | // this.props.dispatch(beginAdding(this.state.content, this.props.username)); 23 | this.setState({ 24 | content: null 25 | }); 26 | 27 | 28 | base.post('chats', { 29 | data: this.props.messages.concat([{ 30 | user: this.props.username, 31 | content: this.state.content 32 | }]), 33 | context: this, 34 | /* 35 | * This 'then' method will run after the 36 | * post has finished. 37 | */ 38 | then: () => { 39 | console.log('POSTED'); 40 | } 41 | }); 42 | } 43 | 44 | handleChange (event) { 45 | this.setState({content: event.target.value}); 46 | } 47 | 48 | gotoBottom () { 49 | let list = React.findDOMNode(this.refs.list).querySelector('ol'); 50 | list.scrollTop = list.scrollHeight; 51 | } 52 | 53 | render () { 54 | const willEnter = (key) => ({ 55 | opacity: { val: 1, config: [100, 20]}, 56 | text: key 57 | }); 58 | 59 | const { messages } = this.props; 60 | 61 | const endValue = () => { 62 | let config = {}; 63 | 64 | messages.forEach(function (message, index) { 65 | config[`index-${index}`] = { 66 | opacity: { val: 1} 67 | }; 68 | }); 69 | 70 | return config; 71 | }; 72 | 73 | const defaultValue = () => { 74 | let config = {}; 75 | 76 | messages.forEach(function (message, index) { 77 | config[`index-${index}`] = { 78 | opacity: { val: 0 } 79 | }; 80 | }); 81 | 82 | return config; 83 | }; 84 | 85 | 86 | return ( 87 |
88 |
89 | 93 | {currentValue => 94 |
    95 | {this.props.messages.map((message, index) => { 96 | return ( 97 |
  1. 102 | {message.user} {currentValue[`index-${index}`].opacity} 103 | {message.content} 104 |
  2. 105 | ); 106 | })} 107 |
108 | } 109 |
110 |
111 |
112 | 120 |
121 | 122 |

Click here to set your username (current: {this.props.username})

123 |
124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Reducers/app.js: -------------------------------------------------------------------------------- 1 | const initialAppState = { 2 | username: 'phaistonian' 3 | }; 4 | 5 | export default function app(state = initialAppState, action) { 6 | switch(action.type) { 7 | case 'CHANGE_USERNAME': 8 | return {...state, username: action.username}; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as messages } from './messages'; 2 | export { default as app } from './app'; 3 | -------------------------------------------------------------------------------- /src/Reducers/messages.js: -------------------------------------------------------------------------------- 1 | const initialState = []; 2 | 3 | export default function messages(state = initialState, action) { 4 | switch(action.type) { 5 | case 'LOAD': 6 | return action.messages; 7 | case 'ADD': 8 | return [...state, { 9 | content: action.content, 10 | user: action.user, 11 | timestamp: Date.now() 12 | }]; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | export function add (content, user) { 2 | return { 3 | type: 'ADD', 4 | content, 5 | user 6 | }; 7 | } 8 | 9 | export function changeUsername (username) { 10 | return { 11 | type: 'CHANGE_USERNAME', 12 | username 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { changeUsername } from '../actions/actions.js'; 3 | import { connect } from 'react-redux'; 4 | import Rebase from 're-base'; 5 | import Chat from '../components/Chat'; 6 | import '../styles.styl'; 7 | 8 | 9 | // const allMessages = localStorage.messages ? JSON.parse(localStorage.messages) : []; 10 | const base = Rebase.createClass('https://reduxchat.firebaseio.com'); 11 | 12 | @connect(state => state) 13 | export default class Root extends Component { 14 | constructor (props) { 15 | super(props); 16 | this.state = { 17 | messages: [] 18 | }; 19 | } 20 | 21 | componentWillMount () { 22 | if (!localStorage.username) { 23 | this.promptForUsername(); 24 | } 25 | 26 | base.bindToState('chats', { 27 | context: this, 28 | state: 'messages', 29 | asArray: true 30 | }); 31 | 32 | this.ref = base.syncState('chats', { 33 | context: this, 34 | state: 'messages', 35 | asArray: true 36 | }); 37 | 38 | //this.props.dispatch(load(allMessages)); 39 | } 40 | 41 | componentWillUnmount () { 42 | base.removeBinding(this.ref); 43 | } 44 | 45 | promptForUsername () { 46 | let username; 47 | 48 | while (!username) { 49 | username = prompt('Enter username, please', this.props.app.username); 50 | } 51 | 52 | localStorage.username = username; 53 | this.props.dispatch(changeUsername(username)); 54 | } 55 | 56 | render () { 57 | return ; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import thunk from 'redux-thunk'; 5 | import { messages, app } from './reducers'; 6 | 7 | 8 | import App from './Containers/App'; 9 | 10 | const reducer = combineReducers({messages, app}); 11 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 12 | const store = createStoreWithMiddleware(reducer); 13 | 14 | React.render(( 15 | 16 | { () => } 17 | 18 | ), document.body 19 | ); 20 | 21 | -------------------------------------------------------------------------------- /src/styles.styl: -------------------------------------------------------------------------------- 1 | $avatar_size = 36px; 2 | 3 | * { 4 | font: 14px normal 'Avenir Next', 'Helvetica'; 5 | box-sizing: border-box; 6 | } 7 | html, body { 8 | height: 100%; 9 | } 10 | 11 | body > div { 12 | padding: 50px; 13 | } 14 | 15 | ol { 16 | height: 500px; 17 | overflow: auto; 18 | } 19 | ol, li { 20 | list-style-type: none; 21 | padding-left: 0; 22 | } 23 | li { 24 | margin: 10px 0; 25 | padding-left: ($avatar_size + 10); 26 | position: relative; 27 | min-height: $avatar_size + 10; 28 | 29 | &:before { 30 | content: ""; 31 | width: $avatar_size; 32 | height: $avatar_size; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | background-color: #eee; 37 | } 38 | 39 | &:hover { 40 | &:before { 41 | background-color: red; 42 | } 43 | } 44 | 45 | &.me { 46 | strong { 47 | color: blue !important; 48 | } 49 | 50 | &:before { 51 | background-color: #ccc; 52 | } 53 | } 54 | } 55 | 56 | 57 | input { 58 | width: 100%; 59 | padding: 10px; 60 | border: 1px solid #ddd; 61 | 62 | &:focus { 63 | outline: none; 64 | border-color: #888; 65 | } 66 | } 67 | strong { 68 | display: block; 69 | font-weight: bold; 70 | } 71 | 72 | p { 73 | margin-top: 20px; 74 | 75 | u { 76 | color: lighten(blue, 40%); 77 | cursor: pointer; 78 | 79 | &:hover { 80 | color: blue; 81 | } 82 | } 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var Webpack = require('webpack'); 2 | var path = require('path'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | var config = { 6 | devtool: 'eval-source-map', 7 | entry: [ 8 | 'webpack-dev-server/client?http://localhost:3000', 9 | 'webpack/hot/dev-server', 10 | './src/index.js' 11 | ], 12 | output: { 13 | path: path.resolve(__dirname, 'build'), 14 | filename: 'bundle.js', 15 | publicPath: '/build/' // This HAS to point to the build path in order for hot reload to work 16 | }, 17 | module: { 18 | loaders: [{ 19 | test: /\.js$/, 20 | loaders: ['react-hot', 'babel?stage=0&optional=runtime'], 21 | include: path.join(__dirname, 'src') 22 | }, { 23 | test: /\.css$/, 24 | loader: 'style-loader!css-loader!autoprefixer-loader', 25 | include: path.join(__dirname, 'src') 26 | }, { 27 | test: /\.styl$/, 28 | loader: 'style-loader!css-loader!autoprefixer-loader!stylus-loader', 29 | include: path.join(__dirname, 'src') 30 | }] 31 | }, 32 | resolve: { 33 | extensions: ['', '.js', '.json', '.css', 'styl'] 34 | }, 35 | plugins: [ 36 | new Webpack.HotModuleReplacementPlugin(), 37 | new Webpack.NoErrorsPlugin(), 38 | new HtmlWebpackPlugin({ 39 | title: 'My App', 40 | filename: 'index.html' 41 | }) 42 | ] 43 | }; 44 | 45 | module.exports = config; 46 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var config = { 6 | devtool: 'source-map', 7 | entry: './src/index', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'bundle.js' 11 | }, 12 | module: { 13 | loaders : [ 14 | { 15 | test: /\.css$/, 16 | loader: ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader"), 17 | include: path.join(__dirname, 'src') 18 | }, 19 | { 20 | test: /\.styl$/, 21 | loader: ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader!stylus-loader"), 22 | include: path.join(__dirname, 'src') 23 | }, 24 | { 25 | test: /\.jsx?$/, 26 | loaders: ['babel'], 27 | include: path.join(__dirname, 'src') 28 | } 29 | ] 30 | }, 31 | plugins: [ 32 | new ExtractTextPlugin('app.css', { allChunks: true }), 33 | new webpack.optimize.UglifyJsPlugin(), 34 | new webpack.optimize.DedupePlugin(), 35 | new webpack.optimize.OccurenceOrderPlugin(), 36 | new webpack.DefinePlugin({ 37 | 'process.env': { 38 | // This has effect on the react lib size 39 | 'NODE_ENV': JSON.stringify('production') 40 | } 41 | }) 42 | ] 43 | }; 44 | 45 | module.exports = config; 46 | --------------------------------------------------------------------------------