├── .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 | -
102 | {message.user} {currentValue[`index-${index}`].opacity}
103 | {message.content}
104 |
105 | );
106 | })}
107 |
108 | }
109 |
110 |
111 |
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 |
--------------------------------------------------------------------------------