├── .flowconfig
├── .gitignore
├── .jshintrc
├── .babelrc
├── src
├── components
│ ├── List
│ │ └── index.js
│ ├── ListItem
│ │ └── index.js
│ ├── Nav
│ │ └── index.js
│ ├── Title
│ │ └── index.js
│ ├── Header
│ │ ├── LogoutLink.js
│ │ ├── LoginLink.js
│ │ ├── index.js
│ │ └── Nav.js
│ ├── J
│ │ └── index.js
│ ├── SlickButton
│ │ └── index.js
│ ├── requireAuth.js
│ └── Button
│ │ └── index.js
├── config
│ ├── index.js
│ ├── firebase.js
│ └── constants.js
├── index.test.js
├── store
│ ├── configure.js
│ ├── configure.prod.js
│ └── configure.dev.js
├── reducers
│ ├── routesPermissions.js
│ ├── initialState.js
│ ├── user.js
│ ├── index.js
│ ├── ajaxStatus.js
│ ├── notifications.js
│ └── auth.js
├── actions
│ ├── ajaxStatus.js
│ ├── notifications.js
│ ├── user.js
│ └── auth.js
├── containers
│ ├── NotFoundPage
│ │ └── index.js
│ ├── AccountPage
│ │ └── index.js
│ ├── HomePage
│ │ └── index.js
│ ├── AboutPage
│ │ └── index.js
│ ├── App
│ │ └── index.js
│ ├── Notifications
│ │ └── index.js
│ ├── RepoList
│ │ └── index.js
│ └── Layout.js
├── selectors
│ └── index.js
├── index.html
├── routes.js
├── styles
│ └── global.js
├── api
│ └── firebase.js
├── index.js
└── logo.svg
├── esdoc.json
├── tools
├── startMessage.js
├── srcServer.js
└── testSetup.js
├── .editorconfig
├── .vscode
└── launch.json
├── LICENSE
├── webpack.config.dev.js
├── .eslintrc
├── package.json
└── README.md
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 | dist
5 | .idea
6 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "newcap": false
6 | }
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": ["react-hot-loader/babel"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/List/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.ul`
4 | list-style: none;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/components/ListItem/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.li`
4 | margin: 0;
5 | `;
6 |
--------------------------------------------------------------------------------
/esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./docs",
4 | "plugins": [
5 | {"name": "esdoc-es7-plugin"}
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | import firebase from './firebase';
2 | import constants from './constants';
3 |
4 | export const firebaseConfig = firebase;
5 |
--------------------------------------------------------------------------------
/tools/startMessage.js:
--------------------------------------------------------------------------------
1 | import colors from 'colors';
2 |
3 | /*eslint-disable no-console */
4 |
5 | console.log('Starting app in dev mode...'.green);
6 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 |
3 | describe('Our first test', () => {
4 | it('should pass', () => {
5 | expect(true).toEqual(true);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/store/configure.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./configure.prod');
3 | } else {
4 | module.exports = require('./configure.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Nav/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.nav`
4 | > * {
5 | margin: 0 5px 5px 0;
6 | }
7 | > *:last-child {
8 | margin-right: inherit;
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/src/reducers/routesPermissions.js:
--------------------------------------------------------------------------------
1 | import initialState from './initialState';
2 |
3 | export default function routesPermissions(state = initialState.auth, action) {
4 | switch (action.type) {
5 | default:
6 | return state;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/actions/ajaxStatus.js:
--------------------------------------------------------------------------------
1 | import * as types from '../config/constants';
2 |
3 | export function beginAjaxCall() {
4 | return { type: types.BEGIN_AJAX_CALL };
5 | }
6 |
7 | export function ajaxCallError() {
8 | return { type: types.AJAX_CALL_ERROR };
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/src/components/Title/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Title = styled.h2`
4 | border-bottom: 1px solid rgba(0, 0, 0, .1);
5 | margin: 35px 0 15px 0;
6 | &:first-child {
7 | margin-top: 0;
8 | }
9 | `;
10 |
11 | export default Title;
12 |
--------------------------------------------------------------------------------
/src/config/firebase.js:
--------------------------------------------------------------------------------
1 | export default {
2 | apiKey: 'PUT_FIREBASE_API_KEY_HERE',
3 | authDomain: 'PUT_FIREBASE_AUTH_DOMAIN_HERE',
4 | databaseURL: 'PUT_FIREBASE_DATABASE_URL_HERE',
5 | storageBucket: 'PUT_FIREBASE_STORAGE_BUCKET_HERE',
6 | messagingSenderId: 'PUT_FIREBASE_MESSAGING_SENDER_ID_HERE',
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/Header/LogoutLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '../Button';
3 |
4 | const LogoutLink = ({ signOut }) => {
5 | return ;
6 | };
7 |
8 | LogoutLink.propTypes = {
9 | signOut: React.PropTypes.func.isRequired
10 | };
11 |
12 | export default LogoutLink;
13 |
--------------------------------------------------------------------------------
/src/store/configure.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import rootReducer from '../reducers';
3 | import thunk from 'redux-thunk';
4 |
5 | export default function configureStore(initialState) {
6 | return createStore(
7 | rootReducer,
8 | initialState,
9 | applyMiddleware(thunk)
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/actions/notifications.js:
--------------------------------------------------------------------------------
1 | export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';
2 | export const PUSH_NOTIFICATION = 'PUSH_NOTIFICATION';
3 |
4 | export function dismiss (notification) {
5 | return {
6 | type: DISMISS_NOTIFICATION,
7 | notification
8 | };
9 | }
10 |
11 | export function notify (message) {
12 | return {
13 | type: PUSH_NOTIFICATION,
14 | message
15 | };
16 | }
--------------------------------------------------------------------------------
/src/reducers/initialState.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ajaxCallsInProgress: 0,
3 | auth: {
4 | currentUserUID: null,
5 | initialized: false,
6 | isLogged: false,
7 | },
8 | routesPermissions: {
9 | requireAuth: [
10 | '/admin',
11 | ],
12 | routesRequireAdmin: [
13 | '/admin',
14 | ],
15 | },
16 | routing: {},
17 | user: {
18 | isAdmin: undefined,
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import styled from 'styled-components';
4 |
5 | import { Row, Column } from 'hedron';
6 |
7 | const HomePage = () => {
8 | return (
9 |
10 |
11 | Page Not Found
12 |
13 |
14 | );
15 | };
16 |
17 | export default HomePage;
18 |
--------------------------------------------------------------------------------
/src/selectors/index.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getAlertsBase = state => state.get('alerts');
4 |
5 | export const getAlerts = createSelector([getAlertsBase], (base) => {
6 | let arr = [];
7 |
8 | base.forEach(item => {
9 | arr.push({
10 | message: item.get('message'),
11 | title: item.get('title'),
12 | key: item.get('key'),
13 | dismissAfter: 5000
14 | });
15 | });
16 |
17 | return arr;
18 | });
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Hot Redux Firebase Starter
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Header/LoginLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import FontAwesome from 'react-fontawesome';
4 | import Button from '../Button';
5 |
6 | const LoginLink = (props) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | LoginLink.propTypes = {
15 | action: React.PropTypes.func.isRequired,
16 | };
17 |
18 | export default LoginLink;
19 |
--------------------------------------------------------------------------------
/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import * as types from '../config/constants';
2 | import initialState from './initialState';
3 |
4 | export default function userReducer(state = initialState.user, action) {
5 | switch (action.type) {
6 | case types.USER_LOADED_SUCCESS:
7 | return Object.assign({}, state, action.user);
8 | case types.PROVIDER_LOGIN_SUCCESS:
9 | return Object.assign({}, state, action.token);
10 | case types.AUTH_LOGGED_OUT_SUCCESS:
11 | return initialState.user;
12 | default:
13 | return state;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/containers/AccountPage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { Row, Column } from 'hedron';
4 |
5 | import checkAuth from '../../components/requireAuth';
6 | import Title from '../../components/Title';
7 |
8 | const AccountPage = () => {
9 | return (
10 |
11 |
12 | You will only see this page if you are logged in
13 | GitHub Token: CLICK TO SHOW
14 |
15 |
16 | );
17 | };
18 |
19 | export default checkAuth(AccountPage);
20 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 |
4 | import ajaxCallsInProgress from './ajaxStatus';
5 | import auth from './auth';
6 | import notifications from './notifications';
7 | import routesPermissions from './routesPermissions';
8 | import user from './user';
9 |
10 |
11 | const rootReducer = combineReducers({
12 | ajaxCallsInProgress,
13 | auth,
14 | notifications,
15 | routesPermissions,
16 | routing: routerReducer,
17 | user,
18 | });
19 |
20 | export default rootReducer;
21 |
--------------------------------------------------------------------------------
/src/containers/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'react-router';
4 | import { Row, Column } from 'hedron';
5 |
6 | import J from '../../components/J';
7 | import Title from '../../components/Title';
8 |
9 | const HomePage = () => {
10 | return (
11 |
12 |
13 | Welcome to the React Firebase Boilerplate!
14 | Time to start putting in your own content
15 |
16 |
17 | );
18 | };
19 |
20 | export default HomePage;
21 |
--------------------------------------------------------------------------------
/src/components/J/index.js:
--------------------------------------------------------------------------------
1 | import emojione from 'emojione';
2 | import React from 'react';
3 | import styled from 'styled-components';
4 |
5 | const Emoji = styled.img`
6 | width: ${props => props.s ? props.s : 22}px;
7 | `;
8 |
9 | const J = ({ id, s }) => {
10 | const shortname = `:${id}:`;
11 | const unicode = emojione.emojioneList[shortname].unicode;
12 | return ;
13 | };
14 |
15 | J.propTypes = {
16 | id: React.PropTypes.string,
17 | s: React.PropTypes.number,
18 | };
19 |
20 | export default J;
--------------------------------------------------------------------------------
/src/reducers/ajaxStatus.js:
--------------------------------------------------------------------------------
1 | import * as types from '../config/constants';
2 | import initialState from './initialState';
3 |
4 | function actionTypeEndsInSuccess(type) {
5 | return type.substring(type.length - 8) == '_SUCCESS';
6 | }
7 |
8 | export default function ajaxStatusReducer(state = initialState.ajaxCallsInProgress, action) {
9 | if (action.type == types.BEGIN_AJAX_CALL) {
10 | return state + 1;
11 | } else if (action.type == types.AJAX_CALL_ERROR ||
12 | actionTypeEndsInSuccess(action.type)) {
13 | return state - 1;
14 | }
15 |
16 |
17 | return state;
18 | }
19 |
--------------------------------------------------------------------------------
/src/reducers/notifications.js:
--------------------------------------------------------------------------------
1 | import { DISMISS_NOTIFICATION, PUSH_NOTIFICATION } from '../actions/notifications';
2 | import { OrderedSet } from 'immutable';
3 |
4 | let _currentKey = 0;
5 | export default function notifications (state = OrderedSet(), action) {
6 |
7 | switch (action.type) {
8 | case DISMISS_NOTIFICATION:
9 | return OrderedSet(state).delete(action.notification);
10 |
11 | case PUSH_NOTIFICATION:
12 | return OrderedSet(state).add({
13 | message: action.message,
14 | key: _currentKey++,
15 | });
16 | }
17 |
18 | return OrderedSet(state);
19 | }
20 |
--------------------------------------------------------------------------------
/src/store/configure.dev.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import { routerMiddleware } from 'react-router-redux';
3 | import rootReducer from '../reducers';
4 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
5 | import thunk from 'redux-thunk';
6 | import { browserHistory } from "react-router";
7 |
8 | export default function configureStore(initialState) {
9 | return createStore(
10 | rootReducer,
11 | initialState,
12 | compose(
13 | applyMiddleware(thunk, reduxImmutableStateInvariant(), routerMiddleware(browserHistory)),
14 | window.devToolsExtension ? window.devToolsExtension() : f => f
15 | )
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceRoot}/lint:watch",
12 | "cwd": "${workspaceRoot}"
13 | },
14 | {
15 | "type": "node",
16 | "request": "attach",
17 | "name": "Attach to Process",
18 | "port": 5858
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/src/containers/AboutPage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { Row, Column } from 'hedron';
4 |
5 | import Title from '../../components/Title';
6 |
7 | const AboutPage = () => {
8 | return (
9 |
10 |
11 | About
12 | Created by @garetmckinley
13 | Links
14 |
15 | -
16 | GitHub Repo
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default AboutPage;
25 |
--------------------------------------------------------------------------------
/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import * as types from '../config/constants';
2 | import initialState from './initialState';
3 |
4 | export default function authReducer(state = initialState.auth, action) {
5 | switch (action.type) {
6 | case types.AUTH_INITIALIZATION_DONE:
7 | return Object.assign({}, state, { initialized: true });
8 |
9 | case types.AUTH_LOGGED_IN_SUCCESS:
10 | return Object.assign({}, state, {
11 | isLogged: true,
12 | currentUserUID: action.userUID
13 | });
14 |
15 | case types.AUTH_LOGGED_OUT_SUCCESS:
16 | return Object.assign({}, state, {
17 | isLogged: false,
18 | currentUserUID: null
19 | });
20 | default:
21 | return state;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import AboutPage from './containers/AboutPage';
5 | import HomePage from './containers/HomePage';
6 | import NotFound from './containers/NotFoundPage';
7 | import Layout from './containers/Layout';
8 | import ProtectedPage from './containers/AccountPage';
9 |
10 | export default function Routes(store) {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/tools/srcServer.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import webpack from 'webpack';
3 | import path from 'path';
4 | import config from '../webpack.config.dev';
5 | import open from 'open';
6 |
7 | /* eslint-disable no-console */
8 |
9 | const port = 3000;
10 | const app = express();
11 | const compiler = webpack(config);
12 |
13 | app.use(require('webpack-dev-middleware')(compiler, {
14 | noInfo: true,
15 | publicPath: config.output.publicPath
16 | }));
17 |
18 | app.use(require('webpack-hot-middleware')(compiler));
19 |
20 | app.get('*', function(req, res) {
21 | res.sendFile(path.join( __dirname, '../src/index.html'));
22 | });
23 |
24 | app.listen(port, function(err) {
25 | if (err) {
26 | console.log(err);
27 | } else {
28 | open(`http://localhost:${port}`);
29 | }
30 | });
31 |
32 | export default app;
33 |
--------------------------------------------------------------------------------
/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Router } from 'react-router';
3 | import routes from '../../routes';
4 |
5 | // If you use React Router, make this component
6 | // render with your routes. Currently,
7 | // only synchronous routes are hot reloaded, and
8 | // you will see a warning from on every reload.
9 | // You can ignore this warning. For details, see:
10 | // https://github.com/reactjs/react-router/issues/2182
11 |
12 | class App extends Component {
13 | render() {
14 | const { history, store } = this.props;
15 | return (
16 |
17 | );
18 | }
19 | }
20 |
21 | App.propTypes = {
22 | history: React.PropTypes.object.isRequired,
23 | store: React.PropTypes.object.isRequired,
24 | };
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/src/containers/Notifications/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { NotificationStack } from 'react-notification';
4 | import { dismiss } from '../../actions/notifications';
5 |
6 | class Notifications extends React.Component {
7 | render() {
8 | return (
9 | this.props.dispatch(dismiss(notification))}
13 | />
14 | );
15 | }
16 | }
17 |
18 | Notifications.propTypes = {
19 | dispatch: React.PropTypes.func.isRequired,
20 | notifications: React.PropTypes.object.isRequired,
21 | };
22 |
23 | function mapStateToProps(state, ownProps) {
24 | return {
25 | notifications: state.notifications
26 | };
27 | }
28 |
29 | export default connect(mapStateToProps)(Notifications);
30 |
--------------------------------------------------------------------------------
/src/containers/RepoList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | class RepoList extends React.PureComponent {
7 | constructor(props, context) {
8 | super(props, context);
9 | }
10 | render() {
11 | const { actions, user, loading, ...props } = this.props;
12 | return (
13 | {user.username}
14 | );
15 | }
16 | }
17 |
18 | RepoList.propTypes = {
19 | actions: React.PropTypes.object.isRequired,
20 | loading: React.PropTypes.bool.isRequired,
21 | user: React.PropTypes.object.isRequired,
22 | };
23 |
24 |
25 | function mapStateToProps(state, ownProps) {
26 | return {
27 | loading: state.ajaxCallsInProgress > 0,
28 | user: state.user,
29 | };
30 | }
31 |
32 | function mapDispatchToProps(dispatch) {
33 | return {
34 | actions: bindActionCreators({ }, dispatch)
35 | };
36 | }
37 |
38 | export default connect(mapStateToProps, mapDispatchToProps)(RepoList);
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Garet McKinley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/SlickButton/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link, browserHistory } from 'react-router';
4 |
5 | const Wrapper = styled.span`
6 | display: inline-block;
7 | position: relative;
8 | overflow: hidden;
9 | `;
10 |
11 | const StyledButton = styled.button`
12 | padding: 4px 8px;
13 | background: transparent;
14 | border: 1px solid deepskyblue;
15 | border-radius: 3px;
16 | color: deepskyblue;
17 | font-size: 22px;
18 | font-weight: 200;
19 | letter-spacing: 1px;
20 | &::before {
21 | content: '${props => props.children}';
22 | position: absolute;
23 | margin-top: -50px;
24 | }
25 | &:hover{
26 | background: deepskyblue;
27 | color: white;
28 | }
29 | &:focus {
30 | outline: 2px solid deepskyblue;
31 | }
32 | `;
33 |
34 | function Button({ to, ...props }) {
35 | function navigate() {
36 | browserHistory.push(to);
37 | }
38 | return (
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | Button.propTypes = {
46 | to: React.PropTypes.string,
47 | };
48 |
49 | export default Button;
50 |
--------------------------------------------------------------------------------
/src/components/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { notify } from '../actions/notifications';
5 |
6 | import { store } from '../index';
7 |
8 | export default function (ComposedComponent){
9 | class Authentication extends Component {
10 | componentWillMount(){
11 | if(!this.props.authenticated) {
12 | this.context.router.push('/');
13 | store.dispatch(notify('You need to be logged to access this page'));
14 | }
15 | }
16 | componentWillUpdate(nextProps){
17 | if(!nextProps.authenticated) {
18 | this.context.router.push('/');
19 | store.dispatch(notify('You need to be logged to access this page'));
20 | }
21 | }
22 | render(){
23 | return ;
24 | }
25 | }
26 | Authentication.contextTypes = {
27 | router : PropTypes.object
28 | };
29 | Authentication.propTypes = {
30 | authenticated : PropTypes.bool,
31 | };
32 | const mapStateToProps = (state) => ({
33 | authenticated : state.auth.isLogged,
34 | });
35 | return connect(mapStateToProps)(Authentication);
36 | }
37 |
--------------------------------------------------------------------------------
/src/config/constants.js:
--------------------------------------------------------------------------------
1 | // ajax and loading actions
2 | export const BEGIN_AJAX_CALL = 'BEGIN_AJAX_CALL';
3 | export const AJAX_CALL_ERROR = 'AJAX_CALL_ERROR';
4 |
5 | // Auth actions
6 | export const AUTH_INITIALIZATION_DONE = 'AUTH_INITIALIZATION_DONE';
7 | export const AUTH_LOGGED_IN_SUCCESS = 'AUTH_LOGGED_IN_SUCCESS';
8 | export const AUTH_LOGGED_OUT_SUCCESS = 'AUTH_LOGGED_OUT_SUCCESS';
9 |
10 | // User actions
11 | export const USER_CREATED_SUCCESS = 'USER_CREATED_SUCCESS';
12 | export const USER_LOADED_SUCCESS = 'USER_LOADED_SUCCESS';
13 | export const USER_IS_ADMIN_SUCCESS = 'USER_IS_ADMIN_SUCCESS';
14 | export const PROVIDER_LOGIN_SUCCESS = 'PROVIDER_LOGIN_SUCCESS';
15 |
16 | export const NOTIFICATION_ADDED_SUCCESS = 'NOTIFICATION_ADDED_SUCCESS';
17 | export const NOTIFICATION_REMOVED_SUCCESS = 'NOTIFICATION_REMOVED_SUCCESS';
18 |
19 | export default {
20 | BEGIN_AJAX_CALL,
21 | AJAX_CALL_ERROR,
22 | AUTH_INITIALIZATION_DONE,
23 | AUTH_LOGGED_IN_SUCCESS,
24 | AUTH_LOGGED_OUT_SUCCESS,
25 | USER_CREATED_SUCCESS,
26 | USER_LOADED_SUCCESS,
27 | USER_IS_ADMIN_SUCCESS,
28 | PROVIDER_LOGIN_SUCCESS,
29 | NOTIFICATION_ADDED_SUCCESS,
30 | NOTIFICATION_REMOVED_SUCCESS,
31 | };
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | let path = require('path');
2 | let webpack = require('webpack');
3 |
4 | const config = {
5 | devtool: 'source-map',
6 | entry: [
7 | 'react-hot-loader/patch',
8 | 'webpack-hot-middleware/client?reload=false',
9 | './src/index'
10 | ],
11 | output: {
12 | path: path.join(__dirname, 'dist'),
13 | filename: 'bundle.js',
14 | publicPath: '/static/'
15 | },
16 | plugins: [
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | { test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel'] },
22 | { test: /(\.css)$/, loaders: ['style', 'css'] },
23 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" },
24 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" },
25 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" },
26 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" },
27 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" }
28 | ]
29 | }
30 | };
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import emojione from 'emojione';
2 | import React, { PropTypes } from 'react';
3 | import ReactTooltip from 'react-tooltip';
4 | import styled from 'styled-components';
5 | import { bindActionCreators } from 'redux';
6 | import { connect } from 'react-redux';
7 | import { Link, IndexLink } from 'react-router';
8 | import { Row, Column } from 'hedron';
9 |
10 | import Button from '../Button';
11 | import J from '../J';
12 | import LoginLink from './LoginLink';
13 | import LogoutLink from './LogoutLink';
14 | import Nav from './Nav';
15 |
16 | const PageHeader = styled.section`
17 | text-align: center;
18 | `;
19 |
20 | const svg = styled.img`
21 | width: 250px;
22 | `;
23 |
24 | const Header = (props) => {
25 | return (
26 |
27 |
28 |
29 |
30 | React Firebase Boilerplate
31 |
32 | The perfect starting point for react apps with firebase backends.
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | Header.propTypes = {
41 | auth: React.PropTypes.object.isRequired,
42 | signIn: React.PropTypes.func.isRequired,
43 | signOut: React.PropTypes.func.isRequired,
44 | user: React.PropTypes.object.isRequired,
45 | };
46 |
47 | export default Header;
48 |
--------------------------------------------------------------------------------
/src/styles/global.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components';
2 |
3 | export default injectGlobal`
4 | html, body, #root, .container-loading {
5 | font-family :sans-serif;
6 | height: 100%;
7 | margin: 0;
8 | }
9 |
10 | #app {
11 | color: #4d4d4d;
12 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
13 | margin: 0 auto;
14 | max-width: 850px;
15 | min-width: 550px;
16 | }
17 |
18 | a:link, a:hover, a:visited {
19 | color: dodgerblue;
20 | }
21 |
22 | a.active {
23 | color: orange;
24 | }
25 |
26 | nav {
27 | padding-top: 20px;
28 | }
29 |
30 | .container-loading {
31 | display: table;
32 | table-layout: fixed;
33 | width: 100%;
34 | }
35 |
36 | .loading {
37 | height: 100px;
38 | margin: 0 auto;
39 | width: 100px;
40 | }
41 |
42 | .loading-row {
43 | background-color: #fff;
44 | display:table-cell;
45 | height: 100%;
46 | min-height: 100%;
47 | text-align:center;
48 | vertical-align:middle;
49 | width: 100%;
50 | }
51 |
52 | .loading div {
53 | width: inherit !important;
54 | }
55 |
56 | .loading svg {
57 | display: block;
58 | margin: 0 auto;
59 | width: 100px;
60 | }
61 |
62 | .h1-loading {
63 | color: #fff;
64 | font-size: 6rem;
65 | line-height: 8rem;
66 | }
67 |
68 | h1, h2, h3, h4, h5, h6 {
69 | font-weight: 300;
70 | }
71 |
72 | h1 {
73 | font-size: 28px;
74 | }
75 | `;
76 |
--------------------------------------------------------------------------------
/tools/testSetup.js:
--------------------------------------------------------------------------------
1 | // Tests are placed alongside files under test.
2 | // This file does the following:
3 | // 1. Registers babel for transpiling our code for testing
4 | // 2. Disables Webpack-specific features that Mocha doesn't understand.
5 | // 3. Requires jsdom so we can test via an in-memory DOM in Node
6 | // 4. Sets up global vars that mimic a browser.
7 |
8 | /*eslint-disable no-var*/
9 |
10 | // This assures the .babelrc dev config (which includes
11 | // hot module reloading code) doesn't apply for tests.
12 | process.env.NODE_ENV = 'test';
13 |
14 | // Register babel so that it will transpile ES6 to ES5
15 | // before our tests run.
16 | require('babel-register')();
17 |
18 | // Disable webpack-specific features for tests since
19 | // Mocha doesn't know what to do with them.
20 | require.extensions['.css'] = function () {return null;};
21 | require.extensions['.png'] = function () {return null;};
22 | require.extensions['.jpg'] = function () {return null;};
23 |
24 | // Configure JSDOM and set global variables
25 | // to simulate a browser environment for tests.
26 | var jsdom = require('jsdom').jsdom;
27 |
28 | var exposedProperties = ['window', 'navigator', 'document'];
29 |
30 | global.document = jsdom('');
31 | global.window = document.defaultView;
32 | Object.keys(document.defaultView).forEach((property) => {
33 | if (typeof global[property] === 'undefined') {
34 | exposedProperties.push(property);
35 | global[property] = document.defaultView[property];
36 | }
37 | });
38 |
39 | global.navigator = {
40 | userAgent: 'node.js'
41 | };
42 |
43 | documentRef = document; //eslint-disable-line no-undef
44 |
--------------------------------------------------------------------------------
/src/actions/user.js:
--------------------------------------------------------------------------------
1 | import firebaseApi from '../api/firebase';
2 | import * as types from '../config/constants';
3 |
4 | import { authLoggedIn } from './auth';
5 | import { ajaxCallError, beginAjaxCall } from './ajaxStatus';
6 |
7 | function extractUserProperties(firebaseUser) {
8 |
9 | const user = {};
10 | const userProperties = [
11 | 'displayName',
12 | 'email',
13 | 'emailVerified',
14 | 'isAnonymous',
15 | 'photoURL',
16 | 'providerData',
17 | 'providerId',
18 | 'refreshToken',
19 | 'uid',
20 | ];
21 |
22 | userProperties.map((prop) => {
23 | if (prop in firebaseUser) {
24 | user[prop] = firebaseUser[prop];
25 | }
26 | });
27 |
28 | return user;
29 | }
30 |
31 | export function userCreated(user) {
32 | return (dispatch) => {
33 | firebaseApi.databaseSet('/users/' + user.uid, extractUserProperties(user))
34 | .then(
35 | () => {
36 | dispatch(authLoggedIn(user.uid));
37 | dispatch(userCreatedSuccess());
38 | })
39 | .catch(
40 | error => {
41 | dispatch(ajaxCallError(error));
42 | // @TODO better error handling
43 | throw(error);
44 | });
45 | };
46 | }
47 |
48 | export function userCreatedSuccess() {
49 | return {
50 | type: types.USER_CREATED_SUCCESS
51 | };
52 | }
53 |
54 | export function providerLoginSuccess(credential) {
55 | return {
56 | type: types.PROVIDER_LOGIN_SUCCESS, credential
57 | };
58 | }
59 |
60 | export function userLoadedSuccess(user) {
61 | return {
62 | type: types.USER_LOADED_SUCCESS, user: extractUserProperties(user)
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Header/Nav.js:
--------------------------------------------------------------------------------
1 | import emojione from 'emojione';
2 | import React, { PropTypes } from 'react';
3 | import ReactTooltip from 'react-tooltip';
4 | import styled from 'styled-components';
5 | import { bindActionCreators } from 'redux';
6 | import { connect } from 'react-redux';
7 | import { Link, IndexLink } from 'react-router';
8 | import { Row, Column } from 'hedron';
9 |
10 | import Button from '../Button';
11 | import J from '../J';
12 | import LoginLink from './LoginLink';
13 | import LogoutLink from './LogoutLink';
14 | import Nav from '../Nav';
15 |
16 | const PageHeader = styled.section`
17 | text-align: center;
18 | `;
19 |
20 | const svg = styled.img`
21 | width: 250px;
22 | `;
23 |
24 | const Navigation = ({ signIn, signOut, auth, user }) => {
25 | let loginLogoutLink = auth.isLogged
26 | ?
27 | : ;
28 | return (
29 |
41 | );
42 | };
43 |
44 | Navigation.propTypes = {
45 | auth: React.PropTypes.object.isRequired,
46 | signIn: React.PropTypes.func.isRequired,
47 | signOut: React.PropTypes.func.isRequired,
48 | user: React.PropTypes.object.isRequired,
49 | };
50 |
51 | export default Navigation;
52 |
--------------------------------------------------------------------------------
/src/containers/Layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Page, Row, Column } from 'hedron';
4 | import { bindActionCreators } from 'redux';
5 | import { connect } from 'react-redux';
6 | import { OrderedSet } from 'immutable';
7 |
8 | import Header from '../components/Header';
9 | import Notifications from './Notifications';
10 | import { signInWithGitHub, signOut } from '../actions/auth';
11 |
12 | const Content = styled(Column)`
13 | background: rgba(0, 0, 0, .05);
14 | border-radius: 5px;
15 | `;
16 |
17 | class Layout extends React.Component {
18 |
19 | constructor(props, context) {
20 | super(props, context);
21 | }
22 |
23 | render() {
24 | const { auth, actions, loading, user } = this.props;
25 | return (
26 |
27 |
28 |
29 |
30 | {this.props.children}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | Layout.propTypes = {
40 | actions: React.PropTypes.object.isRequired,
41 | auth: React.PropTypes.object.isRequired,
42 | children: React.PropTypes.object,
43 | loading: React.PropTypes.bool.isRequired,
44 | user: React.PropTypes.object.isRequired,
45 | };
46 |
47 | function mapStateToProps(state, ownProps) {
48 | return {
49 | auth: state.auth,
50 | loading: state.ajaxCallsInProgress > 0,
51 | user: state.user,
52 | };
53 | }
54 |
55 | function mapDispatchToProps(dispatch) {
56 | return {
57 | actions: bindActionCreators({
58 | signIn: signInWithGitHub,
59 | signOut,
60 | }, dispatch)
61 | };
62 | }
63 |
64 | export default connect(mapStateToProps, mapDispatchToProps)(Layout);
65 |
--------------------------------------------------------------------------------
/src/api/firebase.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase/firebase-browser';
2 | import { firebaseConfig } from '../config/';
3 |
4 | export function provider() {
5 | const gh = new firebase.auth.GithubAuthProvider();
6 | gh.addScope('repo');
7 | gh.addScope('user');
8 | return gh;
9 | }
10 |
11 | class FirebaseApi {
12 |
13 | static initAuth() {
14 | firebase.initializeApp(firebaseConfig);
15 | return new Promise((resolve, reject) => {
16 | const unsub = firebase.auth().onAuthStateChanged(
17 | user => {
18 | unsub();
19 | resolve(user);
20 | },
21 | error => reject(error)
22 | );
23 | });
24 | }
25 |
26 | static auth() {
27 | return firebase.auth;
28 | }
29 |
30 | static signInWithGitHub() {
31 | return firebase.auth().signInWithPopup(provider());
32 | }
33 |
34 | static authSignOut(){
35 | return firebase.auth().signOut();
36 | }
37 |
38 | static databasePush(path, value) {
39 | return new Promise((resolve, reject) => {
40 | firebase
41 | .database()
42 | .ref(path)
43 | .push(value, (error) => {
44 | if (error) {
45 | reject(error);
46 | } else {
47 | resolve();
48 | }
49 | });
50 | });
51 | }
52 |
53 | static GetValueByKeyOnce(path, key) {
54 | return firebase
55 | .database()
56 | .ref(path)
57 | .orderByKey()
58 | .equalTo(key)
59 | .once('value');
60 | }
61 |
62 | static GetChildAddedByKeyOnce(path, key) {
63 | return firebase
64 | .database()
65 | .ref(path)
66 | .orderByKey()
67 | .equalTo(key)
68 | .once('child_added');
69 | }
70 |
71 | static databaseSet(path, value) {
72 | return firebase
73 | .database()
74 | .ref(path)
75 | .set(value);
76 | }
77 | }
78 |
79 | export default FirebaseApi;
80 |
--------------------------------------------------------------------------------
/src/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactTooltip from 'react-tooltip';
3 | import styled from 'styled-components';
4 | import uuid from 'node-uuid';
5 | import { IndexLink, Link, browserHistory } from 'react-router';
6 |
7 | const StyledButton = styled.button`
8 | background: transparent;
9 | border-radius: 3px;
10 | border: 1px solid dodgerblue;
11 | color: dodgerblue;
12 | cursor: pointer;
13 | font-size: 22px;
14 | font-weight: 200;
15 | height: 40px;
16 | letter-spacing: 1px;
17 | overflow: hidden;
18 | padding: 0 8px;
19 | position: relative;
20 | transition: all .15s linear;
21 | .active & {
22 | background-color: dodgerblue;
23 | color: white;
24 | }
25 | &::before {
26 | background-color: rgba(0, 0, 0, .0);
27 | content: ' ';
28 | height: 0;
29 | left: 0;
30 | position: absolute;
31 | top: 50%;
32 | transition: all .3s ease-in;
33 | width: 100%;
34 | }
35 | &:hover{
36 | background: dodgerblue;
37 | color: white;
38 | &::before {
39 | background-color: rgba(0, 0, 0, .075);
40 | top: 0;
41 | display: block;
42 | height: 100%;
43 | }
44 | }
45 | &:active {
46 | background: dodgerblue;
47 | color: white;
48 | &::before {
49 | background-color: rgba(0, 0, 0, .15);
50 | }
51 | }
52 | &:focus {
53 | outline: 2px solid dodgerblue;
54 | }
55 | `;
56 |
57 | function Button({ to, tooltip, ...props }) {
58 | const output = [];
59 | const id = uuid.v4();
60 | if (typeof tooltip !== 'undefined') {
61 | return (
62 |
63 |
64 |
65 | {tooltip}
66 |
67 |
68 | );
69 | }
70 | return (
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | Button.propTypes = {
78 | to: React.PropTypes.string,
79 | tooltip: React.PropTypes.string,
80 | };
81 |
82 | export default Button;
83 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:import/errors",
5 | "plugin:import/warnings"
6 | ],
7 | "plugins": [
8 | "react"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 6,
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "jsx": true,
15 | "experimentalObjectRestSpread": true
16 | }
17 | },
18 | "env": {
19 | "es6": true,
20 | "browser": true,
21 | "node": true,
22 | "jquery": true,
23 | "mocha": true
24 | },
25 | "settings": {
26 | "import/ignore": [
27 | "node_modules"
28 | ]
29 | },
30 | "rules": {
31 | "comma-dangle": ["error", "only-multiline"],
32 | "quotes": 0,
33 | "no-console": 1,
34 | "no-debugger": 1,
35 | "no-var": 1,
36 | "semi": [1, "always"],
37 | "no-trailing-spaces": 0,
38 | "eol-last": 0,
39 | "no-unused-vars": 0,
40 | "no-underscore-dangle": 0,
41 | "no-alert": 0,
42 | "no-lone-blocks": 0,
43 | "jsx-quotes": ["error", "prefer-single"],
44 | "object-curly-spacing": ["error", "always"],
45 | "react/display-name": [ 1, {"ignoreTranspilerName": false }],
46 | "react/forbid-prop-types": [1, {"forbid": ["any"]}],
47 | "react/jsx-boolean-value": 1,
48 | "react/jsx-closing-bracket-location": 0,
49 | "react/jsx-curly-spacing": 0,
50 | "react/jsx-indent-props": 0,
51 | "react/jsx-key": 1,
52 | "react/jsx-max-props-per-line": 0,
53 | "react/jsx-no-bind": 0,
54 | "react/jsx-no-duplicate-props": 1,
55 | "react/jsx-no-literals": 0,
56 | "react/jsx-no-undef": 1,
57 | "react/jsx-pascal-case": 1,
58 | "react/jsx-sort-prop-types": 0,
59 | "react/jsx-sort-props": 0,
60 | "react/jsx-uses-react": 1,
61 | "react/jsx-uses-vars": 1,
62 | "react/no-danger": 1,
63 | "react/no-did-mount-set-state": 1,
64 | "react/no-did-update-set-state": 1,
65 | "react/no-direct-mutation-state": 1,
66 | "react/no-multi-comp": 1,
67 | "react/no-set-state": 0,
68 | "react/no-unknown-property": 1,
69 | "react/prefer-es6-class": 1,
70 | "react/prop-types": 1,
71 | "react/react-in-jsx-scope": 1,
72 | "react/require-extension": 1,
73 | "react/self-closing-comp": 1,
74 | "react/sort-comp": 1,
75 | "react/wrap-multilines": 1
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // modules
2 | import { AppContainer } from 'react-hot-loader';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import { Provider } from 'react-redux';
6 | import { syncHistoryWithStore } from 'react-router-redux';
7 | import { browserHistory } from 'react-router';
8 |
9 | // api
10 | import FirebaseApi from './api/firebase';
11 |
12 | // actions
13 | import { authInitialized } from './actions/auth';
14 | import { ajaxCallError, beginAjaxCall } from './actions/ajaxStatus';
15 |
16 | // components
17 | import App from './containers/App';
18 |
19 | // Store
20 | import initialState from './reducers/initialState';
21 | import configureStore from './store/configure'; //eslint-disable-line import/default
22 |
23 | // Styles
24 | import global from './styles/global';
25 |
26 | // store initialization
27 | export const store = configureStore(initialState);
28 |
29 | // Create an enhanced history that syncs navigation events with the store
30 | const history = syncHistoryWithStore(browserHistory, store);
31 | const rootEl = document.getElementById('root');
32 |
33 | // Initialize Firebase Auth and then start the app
34 | store.dispatch(beginAjaxCall());
35 | FirebaseApi.initAuth()
36 | .then(
37 | user => {
38 | store.dispatch(authInitialized(user));
39 |
40 | ReactDOM.render(
41 |
42 |
43 |
44 |
45 | ,
46 | rootEl
47 | );
48 |
49 | if (module.hot) {
50 | module.hot.accept('./containers/App', () => {
51 | // If you use Webpack 2 in ES modules mode, you can
52 | // use here rather than require() a .
53 | const NextApp = require('./containers/App').default;
54 | ReactDOM.render(
55 |
56 |
57 |
58 |
59 | ,
60 | rootEl
61 | );
62 | });
63 | }
64 | })
65 | .catch(
66 | error => {
67 | store.dispatch(ajaxCallError());
68 | console.error('error while initializing Firebase Auth'); // eslint-disable-line no-console
69 | console.error(error); // eslint-disable-line no-console
70 | });
71 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-firebase-boilerplate",
3 | "version": "0.1.0",
4 | "description": "The perfect starting point for react apps with firebase backends.",
5 | "scripts": {
6 | "prestart": "babel-node tools/startMessage.js",
7 | "start": "npm-run-all --parallel test:watch open:src lint:watch",
8 | "open:src": "babel-node tools/srcServer.js",
9 | "test": "mocha --reporter progress tools/testSetup.js src/*.test.js",
10 | "test:watch": "npm run test -- --watch",
11 | "lint": "node_modules/.bin/esw webpack.config.* tools src",
12 | "lint:watch": "npm run lint -- --watch",
13 | "flow": "flow",
14 | "build:docs": "esdoc -c esdoc.json"
15 | },
16 | "keywords": [
17 | "react",
18 | "reactjs",
19 | "boilerplate",
20 | "hot",
21 | "reload",
22 | "hmr",
23 | "live",
24 | "edit",
25 | "webpack",
26 | "firebase"
27 | ],
28 | "author": "Garet McKinley (http://github.com/garetmckinley)",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/jsbros/react-firebase-boilerplate/issues"
32 | },
33 | "homepage": "https://github.com/jsbros/react-firebase-boilerplate",
34 | "devDependencies": {
35 | "babel-cli": "^6.9.0",
36 | "babel-core": "^6.9.1",
37 | "babel-eslint": "^7.1.1",
38 | "babel-loader": "^6.2.4",
39 | "babel-preset-es2015": "^6.9.0",
40 | "babel-preset-react": "^6.5.0",
41 | "babel-preset-stage-0": "^6.5.0",
42 | "babel-register": "^6.18.0",
43 | "colors": "^1.1.2",
44 | "css-loader": "^0.23.1",
45 | "enzyme": "^2.3.0",
46 | "esdoc": "^0.4.8",
47 | "esdoc-es7-plugin": "^0.0.3",
48 | "eslint": "^3.11.1",
49 | "eslint-plugin-flowtype": "^2.29.1",
50 | "eslint-plugin-import": "^2.2.0",
51 | "eslint-plugin-react": "^6.7.1",
52 | "eslint-watch": "^2.1.11",
53 | "expect": "^1.20.1",
54 | "express": "^4.13.4",
55 | "file-loader": "^0.9.0",
56 | "flow-bin": "^0.36.0",
57 | "jsdom": "^9.2.1",
58 | "mocha": "^2.5.3",
59 | "nock": "^8.0.0",
60 | "npm-run-all": "^2.1.1",
61 | "open": "0.0.5",
62 | "react-addons-test-utils": "^15.1.0",
63 | "react-hot-loader": "^3.0.0-beta.2",
64 | "redux-immutable-state-invariant": "^1.2.3",
65 | "redux-mock-store": "^1.0.4",
66 | "style-loader": "^0.13.1",
67 | "url-loader": "^0.5.7",
68 | "webpack": "^1.13.1",
69 | "webpack-dev-middleware": "^1.6.1",
70 | "webpack-hot-middleware": "^2.10.0"
71 | },
72 | "dependencies": {
73 | "emojione": "^2.2.6",
74 | "firebase": "^3.0.3",
75 | "hedron": "^0.3.0",
76 | "immutable": "^3.8.1",
77 | "node-uuid": "^1.4.7",
78 | "react": "^15.1.0",
79 | "react-dom": "^15.1.0",
80 | "react-emojione": "^2.0.0",
81 | "react-fontawesome": "^1.4.0",
82 | "react-loader": "^2.4.0",
83 | "react-notification": "^6.5.1",
84 | "react-redux": "^4.4.5",
85 | "react-router": "^3.0.0",
86 | "react-router-redux": "^4.0.4",
87 | "react-tooltip": "^3.2.2",
88 | "redux": "^3.5.2",
89 | "redux-thunk": "^2.1.0",
90 | "reselect": "^2.5.4",
91 | "styled-components": "^1.1.2"
92 | },
93 | "repository": {
94 | "type": "git",
95 | "url": "https://github.com/jsbros/react-firebase-boilerplate.git"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase';
2 | import { push } from 'react-router-redux';
3 |
4 | import firebaseApi from '../api/firebase';
5 | import * as types from '../config/constants';
6 | import { ajaxCallError, beginAjaxCall } from './ajaxStatus';
7 | import { notify } from './notifications';
8 | import { providerLoginSuccess, userLoadedSuccess, userCreated } from './user';
9 |
10 | export function authInitializedDone() {
11 | return {
12 | type: types.AUTH_INITIALIZATION_DONE
13 | };
14 | }
15 |
16 | export function authLoggedInSuccess(userUID) {
17 | return {
18 | type: types.AUTH_LOGGED_IN_SUCCESS, userUID
19 | };
20 | }
21 |
22 | export function authLoggedOutSuccess() {
23 | return { type: types.AUTH_LOGGED_OUT_SUCCESS };
24 | }
25 |
26 | export function authInitialized(user) {
27 | return (dispatch) => {
28 | dispatch(authInitializedDone());
29 | if (user) {
30 | dispatch(authLoggedIn(user.uid));
31 | } else {
32 | dispatch(authLoggedOutSuccess());
33 | }
34 | };
35 | }
36 |
37 | /**
38 | * Determines whether or not the user has a valid
39 | * GitHub authentication token.
40 | *
41 | * @export
42 | * @returns (dispatch) => {}
43 | */
44 | export function hasGithubToken() {
45 | // if we have a stored token, return it; else return null.
46 | return localStorage.token ? localStorage.token : null;
47 | }
48 |
49 |
50 | /**
51 | * Determines whether or not a user is authenticated.
52 | *
53 | * @export
54 | * @returns (dispatch) => {}
55 | */
56 | export function authLoggedIn() {
57 | // fetch a copy of the currently logged in user
58 | const user = firebase.auth().currentUser;
59 | return (dispatch) => {
60 | // a github account is required to use the site,
61 | // so confirm the user has an access token.
62 | const token = hasGithubToken();
63 | if (token) {
64 | dispatch(providerLoginSuccess(token));
65 | dispatch(authLoggedInSuccess(user.uid));
66 | dispatch(beginAjaxCall());
67 | dispatch(userLoadedSuccess(user));
68 | dispatch(push('/'));
69 | }
70 | };
71 | }
72 |
73 | export function signInWithGitHub() {
74 | return (dispatch) => {
75 | dispatch(beginAjaxCall());
76 | return firebaseApi.signInWithGitHub()
77 | .then(
78 | result => {
79 | localStorage.setItem("token", result.credential.accessToken);
80 | dispatch(notify(`Welcome ${result.user.displayName}`));
81 | dispatch(authLoggedIn());
82 | return;
83 | })
84 | .catch(error => {
85 | dispatch(ajaxCallError(error));
86 | // @TODO better error handling
87 | throw (error);
88 | });
89 | };
90 | }
91 |
92 | export function signOut() {
93 | return (dispatch, getState) => {
94 | dispatch(beginAjaxCall());
95 | return firebaseApi.authSignOut()
96 | .then(
97 | () => {
98 | localStorage.token = null;
99 | dispatch(notify(`Logged out.`));
100 | dispatch(authLoggedOutSuccess());
101 | if (getState().routesPermissions.requireAuth
102 | .filter(route => route === getState().routing.locationBeforeTransitions.pathname).toString()) {
103 | dispatch(push('/'));
104 | }
105 | })
106 | .catch(error => {
107 | dispatch(ajaxCallError(error));
108 | // @TODO better error handling
109 | throw (error);
110 | });
111 | };
112 | }
113 |
114 |
115 | function redirect(replace, pathname, nextPathName, error = false) {
116 | replace({
117 | pathname: pathname,
118 | state: { nextPathname: nextPathName }
119 | });
120 | if (error) {
121 | // toastr.error(error);
122 | }
123 | }
124 |
125 | export function requireAuth(nextState, replace) {
126 | return (dispatch, getState) => {
127 | if (!getState().auth.isLogged) {
128 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page');
129 | }
130 | };
131 | }
132 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/JSBros/react-firebase-boilerplate/issues) [](https://github.com/JSBros/react-firebase-boilerplate/issues) [](https://github.com/JSBros/react-firebase-boilerplate/pulls?q=is%3Apr+is%3Aclosed) [](https://slackin-xtuseyimsc.now.sh/)
6 |
7 | The React Firebase Boilerplate was originally based on the [react-hot-redux-firebase-starter](https://github.com/douglascorrea/react-hot-redux-firebase-starter). The structure was changed durastically when creating this, so I decided to start an entirely new boilerplate based off the fork.
8 |
9 | ## Stack
10 |
11 | - React
12 | - [X] React `15.1.0`
13 | - [X] React Hot Loader `3.0.0-beta.2`
14 | - [X] React Router `3.0.0`
15 | - Redux
16 | - [X] Redux `3.5.2`
17 | - [X] React Redux `4.4.5`
18 | - [X] React Router Redux `4.0.4`
19 | - [X] Redux Thunk `2.1.0`
20 | - [X] Redux Dev Tools
21 | - Webpack
22 | - [X] Webpack `1.13.1`
23 | - [X] Webpack Dev Middleware `1.6.1`
24 | - [X] Webpack Hot Middleware `2.10.0`
25 | - Firebase
26 | - [X] Firebase `3.0.3`
27 | - Linting
28 | - [ ] Flowtype `0.36.0`
29 | - [X] Eslint `3.11.1`
30 | - Styles
31 | - [X] Styled Components `1.1.2`
32 | - Extras
33 | - [X] Emojione `2.2.6`
34 | - Testing
35 | - [ ] Mocha `2.5.3`
36 | - [ ] Enzyme `2.3.0`
37 |
38 |
39 | ## Features
40 |
41 | - Firebase:
42 | - Auth
43 | - [X] Authentication setup (Registration/Login with GitHub—other providers easil added)
44 | - [X] state.user sync with Firebase Auth
45 | - [X] Protected routes (requires login)
46 | - Beautiful notification system via `react-notification`.
47 | - Cross browser emoji support via `emojione`.
48 |
49 |
50 | ## Usage Guide
51 |
52 | ### Boilerplate Setup
53 |
54 | ```
55 | git clone https://github.com/jsbros/react-firebase-boilerplate
56 | cd react-firebase-boilerplate
57 | npm install
58 | ```
59 |
60 | ### Firebase setup
61 |
62 | 1. Create a Firebase project in the [Firebase console](https://console.firebase.google.com/), if you don't already have one.
63 | - If you already have an existing Google project associated with your app, click `Import Google Project`. Otherwise, click `Create New Project`.
64 | - If you already have a Firebase project, click `Add App` from the project overview page.
65 | 2. Click `Add Firebase to your web app`.
66 | 3. Copy all the values keys in the `config` object over to the [config/firebase.js](config/firebase.js) file.
67 | 4. [Register your app](https://github.com/settings/applications/new) as a developer application on GitHub and get your app's OAuth 2.0 Client ID and Client Secret.
68 | 5. Enable GitHub authentication:
69 | 1. In the [Firebase console](https://console.firebase.google.com/), open the `Auth` section.
70 | 2. On the `Sign in method` tab, enable the `GitHub` sign-in method and specify the OAuth 2.0 `Client ID` and `Client Secret` you got from GitHub.
71 | 3. Then, make sure your Firebase `OAuth redirect URI` (e.g. `my-app-12345.firebaseapp.com/__/auth/handler`) is set as your `Authorization callback URL` in your app's settings page on your [GitHub app's config](https://github.com/settings/developers).
72 |
73 |
74 | ### Usage
75 |
76 | ```
77 | npm start
78 | ```
79 |
80 | ## Development Tasks
81 |
82 | - `npm start` run the web app with lint and tests in watch mode
83 | - `npm run lint` linting javascript code usig eslint
84 | - `npm run test` test using mocha and enzyme
85 |
86 | ## Roadmap
87 |
88 | Check our [roadmap issues](https://github.com/jsbros/react-firebase-boilerplate/issues?q=is%3Aissue+is%3Aopen+label%3Aroadmap)
89 |
90 | ## Contributing
91 |
92 | ### Code Linting
93 |
94 | All code must pass the linter 100% before getting merged into the master repo. It's highly recommended to install an eslint extension into your code editor/IDE. You can also run the linter from the command line using
95 |
96 | ```
97 | npm run lint
98 | ```
99 |
100 | ### Commit Styles
101 |
102 | All commit messages must follow the [Semantic Commit Message](https://seesparkbox.com/foundry/semantic_commit_messages) guidelines.
103 |
104 | ## Author
105 |
106 | 🔥 [Garet McKinley](https://twitter.com/garetmckinley)
107 |
108 | ## License
109 |
110 | The React Firebase Boilerplate is under the [MIT License](LICENSE)
111 |
--------------------------------------------------------------------------------