├── .gitignore ├── README.md ├── app ├── App.js ├── actions │ └── quotes.js ├── components │ ├── Footer.js │ ├── Main.js │ ├── Navbar.js │ ├── QuoteCell.js │ ├── QuoteList.js │ └── SubmitQuote.js ├── config │ ├── routes.js │ └── store.js ├── containers │ ├── QuoteList.js │ └── SubmitQuote.js ├── epics │ ├── index.js │ └── quotes.js ├── reducers │ ├── index.js │ └── quotes.js └── utils │ └── API.js ├── controllers └── quotesController.js ├── models └── quote.js ├── nodemon.json ├── package.json ├── public ├── bundle.js ├── css │ └── style.css ├── image │ ├── react-quotes-1.png │ └── react-quotes-2.png └── index.html ├── routes ├── apiRoutes.js └── routes.js ├── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React redux-observable exmaple app 2 | React + [redux-observable](https://redux-observable.js.org/) example app. 3 | 4 | [Live app](https://react-quotes-app.herokuapp.com) 5 | 6 | ## Quick Start 7 | ```bash 8 | #clone the repo 9 | git clone https://github.com/monad98/redux-observable-example.git 10 | 11 | #change directory to repo 12 | cd redux-observable-example 13 | 14 | # install dependencies 15 | npm install 16 | 17 | #start 18 | npm run serve 19 | //and 20 | npm run dev 21 | ``` 22 | 23 | ## Packages used 24 | - React 25 | - redux 26 | - [redux-observable](https://redux-observable.js.org/) 27 | - [react-router 4.x](https://github.com/ReactTraining/react-router) 28 | - react-router-redux 29 | 30 | 31 | ## Screenshots 32 | 33 | react-quotes-1 34 | react-quotes-2 35 | -------------------------------------------------------------------------------- /app/App.js: -------------------------------------------------------------------------------- 1 | // Importing ReactDOM and our routes 2 | import ReactDOM from "react-dom"; 3 | import routes from "./config/routes"; 4 | 5 | // Rendering our router to the "app" div in index.html 6 | ReactDOM.render(routes, document.getElementById("app")); 7 | -------------------------------------------------------------------------------- /app/actions/quotes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Hyungwu Pae on 6/12/17. 3 | */ 4 | 5 | /** 6 | * Action types 7 | */ 8 | export const FETCH_QUOTES = 'FETCH_QUOTES'; 9 | export const LOAD_QUOTES = 'LOAD_QUOTES'; 10 | export const SAVE_QUOTE = 'SAVE_QUOTE'; 11 | export const LOAD_QUOTE = 'LOAD_QUOTE'; 12 | export const TOGGLE_FAVORITE = 'TOGGLE_FAVORITE'; 13 | export const DELETE_QUOTE = 'DELETE_QUOTE'; 14 | export const DELETE_SUCCESS = 'DELETE_SUCCESS'; 15 | 16 | 17 | /** 18 | * Actions 19 | */ 20 | export function fetchQuotes () { 21 | return { 22 | type: FETCH_QUOTES, 23 | payload: null 24 | } 25 | } 26 | 27 | export function loadQuotes (quotes) { 28 | return { 29 | type: LOAD_QUOTES, 30 | payload: quotes 31 | } 32 | } 33 | 34 | export function saveQuote (quote) { 35 | return { 36 | type: SAVE_QUOTE, 37 | payload: quote 38 | }; 39 | } 40 | 41 | export function loadQuote (quote) { 42 | return { 43 | type: LOAD_QUOTE, 44 | payload: quote 45 | }; 46 | } 47 | 48 | export function toggleFavorite (quote) { 49 | return { 50 | type: TOGGLE_FAVORITE, 51 | payload: quote 52 | }; 53 | } 54 | 55 | export function deleteQuote (id) { 56 | return { 57 | type: DELETE_QUOTE, 58 | payload: id 59 | }; 60 | } 61 | 62 | export function deleteSuccess (id) { 63 | return { 64 | type: DELETE_SUCCESS, 65 | payload: id 66 | }; 67 | } -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Footer = () => ( 3 | 9 | ); 10 | 11 | export default Footer; -------------------------------------------------------------------------------- /app/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from 'prop-types' 3 | import { Route } from 'react-router'; 4 | import Navbar from '../components/Navbar'; 5 | import Footer from '../components/Footer'; 6 | import QuoteListContainer from '../containers/QuoteList'; 7 | import SubmitQuoteContainer from '../containers/SubmitQuote'; 8 | import {fetchQuotes} from '../actions/quotes'; 9 | import { connect } from 'react-redux'; 10 | import { NavLink } from 'react-router-dom' 11 | 12 | class Main extends React.Component{ 13 | 14 | constructor(props) { 15 | super(props); 16 | props.fetchQuotes(); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | 23 |
24 |
25 |
26 | SUBMIT 27 | QUOTES 28 |
29 |
30 | 31 | 32 | 33 |
34 |
36 | ); 37 | } 38 | } 39 | Main.propTypes = { 40 | fetchQuotes: PropTypes.func.isRequired, 41 | location: PropTypes.any 42 | }; 43 | 44 | export default connect( 45 | ({routing}) => ({location: routing.location}), // NavLink update 46 | {fetchQuotes} 47 | )(Main); -------------------------------------------------------------------------------- /app/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, Link } from 'react-router-dom' 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const Navbar = ({count}) => ( 7 | 20 | ); 21 | 22 | Navbar.propTypes = { 23 | count: PropTypes.number.isRequired, 24 | location: PropTypes.any 25 | }; 26 | 27 | // 28 | export default connect( 29 | ({ quotes, routing }) => ({ count: quotes.ids.length, location: routing.location }) //location for NavLink activeClass update 30 | )(Navbar); -------------------------------------------------------------------------------- /app/components/QuoteCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const QuoteCellComponent = ({deleteQuote, toggleFavorite, quote}) => ( 5 |
6 |
7 |

8 |
9 | 10 |
11 |

12 | {quote.text} 13 |

14 |
15 |
16 | 17 |
18 | 19 |
20 | ); 21 | 22 | QuoteCellComponent.propTypes = { 23 | deleteQuote: PropTypes.func.isRequired, 24 | toggleFavorite: PropTypes.func.isRequired, 25 | quote: PropTypes.shape({ 26 | _id: PropTypes.string.isRequired, 27 | text: PropTypes.string.isRequired, 28 | favorited: PropTypes.bool.isRequired 29 | }).isRequired 30 | }; 31 | 32 | 33 | export default QuoteCellComponent; -------------------------------------------------------------------------------- /app/components/QuoteList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import QuoteCellComponent from './QuoteCell'; 4 | 5 | const QuoteListComponent = ({deleteQuote, toggleFavorite, quotes}) => ( 6 |
7 |
8 |

Quote List

9 |
10 |
11 |
12 |

Quote List

13 |
14 |
15 | {quotes.map((quote, idx) => 16 | ( 17 |
18 |
19 | 20 |
21 |
22 | ) 23 | )} 24 |
25 |
26 |
27 | ); 28 | 29 | QuoteListComponent.propTypes = { 30 | deleteQuote: PropTypes.func.isRequired, 31 | toggleFavorite: PropTypes.func.isRequired, 32 | quotes: PropTypes.arrayOf( 33 | PropTypes.shape({ 34 | _id: PropTypes.string.isRequired, 35 | text: PropTypes.string.isRequired, 36 | favorited: PropTypes.bool.isRequired 37 | }).isRequired 38 | ) 39 | }; 40 | 41 | export default QuoteListComponent; -------------------------------------------------------------------------------- /app/components/SubmitQuote.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | 4 | export default class SubmitQuote extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {text: ''}; 8 | } 9 | 10 | update(e) { 11 | this.setState({ 12 | text: e.target.value, 13 | favorited: false 14 | }) 15 | } 16 | 17 | submit(e) { 18 | e.preventDefault(); 19 | this.state.text = this.state.text.trim(); 20 | this.props.saveQuote(this.state); 21 | this.setState({text: ''}); 22 | 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |
29 |

Submit Quotes

30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 | 38 |
39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | SubmitQuote.propTypes = { 46 | saveQuote: PropTypes.func.isRequired 47 | }; 48 | -------------------------------------------------------------------------------- /app/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import configureStore, {history} from './store'; 4 | import { Route } from 'react-router' 5 | import { ConnectedRouter } from 'react-router-redux' 6 | const store = configureStore(); 7 | import Main from '../components/Main' 8 | 9 | 10 | const routes = ( 11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | ); 19 | 20 | export default routes; 21 | -------------------------------------------------------------------------------- /app/config/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Hyungwu Pae on 6/12/17. 3 | */ 4 | import { createStore, applyMiddleware, compose } from 'redux'; 5 | import { createEpicMiddleware } from 'redux-observable'; 6 | import { routerMiddleware } from 'react-router-redux'; 7 | import rootReducer from '../reducers'; 8 | import rootEpic from '../epics'; 9 | import createHistory from 'history/createBrowserHistory'; 10 | const epicMiddleware = createEpicMiddleware(rootEpic); 11 | import logger from 'redux-logger'; 12 | 13 | // Create a history of your choosing (we're using a browser history in this case) 14 | export const history = createHistory(); 15 | 16 | export default function configureStore() { 17 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 18 | return createStore( 19 | rootReducer, 20 | composeEnhancers( 21 | applyMiddleware( 22 | epicMiddleware, 23 | routerMiddleware(history), 24 | logger 25 | ) 26 | ) 27 | ); 28 | } -------------------------------------------------------------------------------- /app/containers/QuoteList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import {deleteQuote, toggleFavorite} from '../actions/quotes'; 3 | import {getQuotesArray} from '../reducers/index'; 4 | import QuoteListComponent from '../components/QuoteList'; 5 | 6 | export default connect( 7 | (state) => ({quotes: getQuotesArray(state)}), 8 | { deleteQuote, toggleFavorite } 9 | )(QuoteListComponent); 10 | 11 | -------------------------------------------------------------------------------- /app/containers/SubmitQuote.js: -------------------------------------------------------------------------------- 1 | import {saveQuote} from '../actions/quotes'; 2 | import { connect } from 'react-redux'; 3 | import SubmitQuoteComponent from '../components/SubmitQuote'; 4 | 5 | export default connect( 6 | null, 7 | { saveQuote } 8 | )(SubmitQuoteComponent); -------------------------------------------------------------------------------- /app/epics/index.js: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-observable'; 2 | import {fetchQuotes, saveQuote, toggleFavorite, deleteQuote} from "./quotes"; 3 | // import * as quotes from "./quotes"; 4 | 5 | 6 | export default combineEpics( 7 | fetchQuotes, 8 | saveQuote, 9 | toggleFavorite, 10 | deleteQuote 11 | ); -------------------------------------------------------------------------------- /app/epics/quotes.js: -------------------------------------------------------------------------------- 1 | import { ajax } from 'rxjs/observable/dom/ajax'; 2 | import * as fromQuotes from '../actions/quotes'; 3 | import 'rxjs/add/operator/map'; 4 | import 'rxjs/add/operator/switchMap'; 5 | import 'rxjs/add/operator/catch'; 6 | 7 | export const fetchQuotes = (action$) => 8 | action$.ofType(fromQuotes.FETCH_QUOTES) 9 | .switchMap(() => 10 | ajax.get(`/api/quotes`) 11 | .map(ajaxRes => ajaxRes.response) 12 | .map(fromQuotes.loadQuotes) 13 | .catch(e => console.log(e)) 14 | ); 15 | 16 | export const saveQuote = (action$) => 17 | action$.ofType(fromQuotes.SAVE_QUOTE) 18 | .map(action => action.payload) 19 | .switchMap(quote => 20 | ajax.post(`/api/quotes`, quote) 21 | .map(ajaxRes => ajaxRes.response) 22 | .map(fromQuotes.loadQuote) 23 | ); 24 | 25 | 26 | export const toggleFavorite = (action$) => 27 | action$.ofType(fromQuotes.TOGGLE_FAVORITE) 28 | .map(action => action.payload) 29 | .switchMap(({_id, text, favorited}) => 30 | ajax.patch(`/api/quotes/${_id}`, {_id, text, favorited: !favorited}) 31 | .map(ajaxRes => ajaxRes.response) 32 | .map(() => fromQuotes.loadQuote({_id, text, favorited: !favorited})) 33 | ); 34 | 35 | export const deleteQuote = (action$) => 36 | action$.ofType(fromQuotes.DELETE_QUOTE) 37 | .map(action => action.payload) 38 | .switchMap(quote => 39 | ajax.delete(`/api/quotes/${quote._id}`) 40 | .map(ajaxRes => ajaxRes.response) 41 | .map(() => fromQuotes.deleteSuccess(quote._id)) 42 | ); 43 | 44 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import * as fromQuotes from './quotes'; 4 | import { createSelector } from 'reselect' 5 | 6 | /** 7 | * root reduces 8 | */ 9 | export default combineReducers({ 10 | quotes: fromQuotes.reducer, 11 | routing: routerReducer 12 | }); 13 | 14 | 15 | /** 16 | * selectors 17 | */ 18 | export const getQuotes = state => state.quotes; 19 | export const getQuotesArray = createSelector(getQuotes, fromQuotes.getQuotesArray); -------------------------------------------------------------------------------- /app/reducers/quotes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Hyungwu Pae on 6/12/17. 3 | */ 4 | import * as fromQuotes from '../actions/quotes'; 5 | import { createSelector } from 'reselect' 6 | 7 | const initialState = { 8 | ids: [], 9 | entities: {}, 10 | }; 11 | 12 | /** 13 | * Reduces 14 | */ 15 | export function reducer(state = initialState, action) { 16 | switch (action.type) { 17 | case fromQuotes.LOAD_QUOTES: { 18 | const quotes = action.payload; 19 | const ids = quotes.map(q => q._id); 20 | const entities = quotes.reduce((acc, q) => Object.assign(acc, {[q._id]: q}), {}); 21 | return {ids, entities}; 22 | } 23 | 24 | case fromQuotes.LOAD_QUOTE: { 25 | const quote = action.payload; 26 | const entities = Object.assign({}, state.entities, {[quote._id]: quote}); 27 | const ids = Object.keys(entities); 28 | return {ids, entities}; 29 | } 30 | 31 | case fromQuotes.DELETE_SUCCESS: { 32 | const deletedId = action.payload; 33 | const ids = state.ids.filter(id => id !== deletedId); 34 | const entities = ids.reduce((acc, id) => Object.assign(acc, {[id]: state.entities[id]}), {}); 35 | return {ids, entities}; 36 | } 37 | default: 38 | return state; 39 | } 40 | } 41 | 42 | /** 43 | * Selectors 44 | */ 45 | const getIds = state => state.ids; 46 | const getEntities = state => state.entities; 47 | export const getQuotesArray = createSelector([getIds, getEntities], (ids, entities) => ids.map(id => entities[id])); 48 | -------------------------------------------------------------------------------- /app/utils/API.js: -------------------------------------------------------------------------------- 1 | // import axios from "axios"; 2 | 3 | const API = { 4 | // getQuotes returns all quotes from out db 5 | // getQuotes: function() { 6 | // return axios.get("/api/quotes"); 7 | // }, 8 | // // Save quote saves a quote to the db, 9 | // // expects to be passed the new quotes text as an argument 10 | // saveQuote: function(text) { 11 | // return axios.post("/api/quotes", { text }); 12 | // }, 13 | // // deleteQuote deletes a quote from the db, 14 | // // expects the id of the quote to delete as an argument 15 | // deleteQuote: function(id) { 16 | // return axios.delete(`/api/quotes/${id}`); 17 | // }, 18 | // // favorite quote toggle's a quote's 'favorite' status in the db, 19 | // // expects the quote object as an argument 20 | // favoriteQuote: function(quote) { 21 | // quote.favorited = !quote.favorited; 22 | // const { _id, favorited } = quote; 23 | // return axios.patch(`/api/quotes/${_id}`, { favorited }); 24 | // } 25 | }; 26 | 27 | export default API; 28 | -------------------------------------------------------------------------------- /controllers/quotesController.js: -------------------------------------------------------------------------------- 1 | var Quote = require("../models/quote"); 2 | 3 | module.exports = { 4 | // This method handles retrieving quotes from the db 5 | index: function(req, res) { 6 | var query; 7 | if (req.query) { 8 | query = req.query; 9 | } 10 | else { 11 | query = req.params.id ? { _id: req.params.id } : {}; 12 | } 13 | Quote.find(query) 14 | .then(function(doc) { 15 | res.json(doc); 16 | }).catch(function(err) { 17 | res.json(err); 18 | }); 19 | }, 20 | // This method handles creating new quotes 21 | create: function(req, res) { 22 | Quote.create(req.body).then(function(doc) { 23 | res.json(doc); 24 | }).catch(function(err) { 25 | res.json(err); 26 | }); 27 | }, 28 | // This method handles updating quotes 29 | update: function(req, res) { 30 | Quote.update({ 31 | _id: req.params.id 32 | }, 33 | req.body 34 | ).then(function(doc) { 35 | res.json(doc); 36 | }).catch(function(err) { 37 | res.json(err); 38 | }); 39 | }, 40 | // This method handles deleting quotes 41 | destroy: function(req, res) { 42 | Quote.remove({ 43 | _id: req.params.id 44 | }).then(function(doc) { 45 | res.json(doc); 46 | }).catch(function(err) { 47 | res.json(err); 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /models/quote.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | var quoteSchema = new Schema({ 6 | text: String, 7 | favorited: { 8 | type: Boolean, 9 | default: false 10 | } 11 | }); 12 | 13 | var Quote = mongoose.model("Quote", quoteSchema); 14 | 15 | module.exports = Quote; -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["app/*", "node_modules/**/node_modules"] 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solved", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev": "webpack -w", 8 | "start": "node server.js", 9 | "serve": "nodemon server.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bluebird": "^3.4.7", 16 | "body-parser": "^1.15.2", 17 | "express": "^4.14.0", 18 | "history": "^4.6.1", 19 | "mongoose": "^4.7.5", 20 | "prop-types": "^15.5.10", 21 | "react": "^15.5.4", 22 | "react-dom": "^15.5.4", 23 | "react-redux": "^5.0.4", 24 | "react-router": "^4.1.1", 25 | "react-router-dom": "^4.1.1", 26 | "react-router-redux": "^5.0.0-alpha.6", 27 | "redux": "^3.6.0", 28 | "redux-logger": "^3.0.6", 29 | "redux-observable": "^0.14.1", 30 | "reselect": "^3.0.1", 31 | "rxjs": "^5.4.0" 32 | }, 33 | "devDependencies": { 34 | "babel-core": "^6.25.0", 35 | "babel-loader": "^7.0.0", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-preset-react": "^6.24.1", 38 | "html-webpack-plugin": "^2.28.0", 39 | "nodemon": "^1.11.0", 40 | "webpack": "^2.6.1", 41 | "webpack-dev-server": "^2.4.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Styles go here */ 2 | html { 3 | position: relative; 4 | min-height: 100%; 5 | } 6 | body .main.container { 7 | padding: 60px 15px 0; 8 | position: relative; 9 | margin-bottom: 60px; 10 | } 11 | 12 | footer { 13 | position: absolute; 14 | bottom: 0; 15 | width: 100%; 16 | height: 60px; 17 | display: block; 18 | } 19 | 20 | .navbar-default .navbar-nav > li > a.active { 21 | color: #4da5f4; 22 | background-color: transparent; 23 | font-weight: bold; 24 | } 25 | 26 | .btn-space { 27 | margin-right: 5px; 28 | } 29 | 30 | .label.label-primary { 31 | border-radius: 50%; 32 | } 33 | 34 | .fa.fa-star, .fa.fa-star-o { 35 | cursor: pointer; 36 | font-size: 18px; 37 | } 38 | .btn.btn-lg { 39 | margin-right: 10px; 40 | } -------------------------------------------------------------------------------- /public/image/react-quotes-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monad98/redux-observable-example/ed267dccdc8258075ece4d65de960efe4cdcf30a/public/image/react-quotes-1.png -------------------------------------------------------------------------------- /public/image/react-quotes-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monad98/redux-observable-example/ed267dccdc8258075ece4d65de960efe4cdcf30a/public/image/react-quotes-2.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LearnReact! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /routes/apiRoutes.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | 3 | var quotesController = require("../controllers/quotesController"); 4 | 5 | var router = new express.Router(); 6 | 7 | // Get all quotes (or optionally a specific quote with an id) 8 | router.get("/quotes/:id?", quotesController.index); 9 | // Create a new quote using data passed in req.body 10 | router.post("/quotes", quotesController.create); 11 | // Update an existing quote with a speicified id param, using data in req.body 12 | router.patch("/quotes/:id", quotesController.update); 13 | // Delete a specific quote using the id in req.params.id 14 | router.delete("/quotes/:id", quotesController.destroy); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /routes/routes.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var path = require("path"); 3 | 4 | var apiRoutes = require("./apiRoutes"); 5 | 6 | var router = new express.Router(); 7 | 8 | // Use the apiRoutes module for any routes starting with "/api" 9 | router.use("/api", apiRoutes); 10 | 11 | // Otherwise send all other requests the index.html page 12 | // React router will handle routing withing the app 13 | router.get("*", function(req, res) { 14 | res.sendFile(path.join(__dirname, "../public/index.html")); 15 | }); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Require our dependecies 2 | var express = require("express"); 3 | var mongoose = require("mongoose"); 4 | var bluebird = require("bluebird"); 5 | var bodyParser = require("body-parser"); 6 | var routes = require("./routes/routes"); 7 | 8 | // Set up a default port, configure mongoose, configure our middleware 9 | var PORT = process.env.PORT || 3000; 10 | mongoose.Promise = bluebird; 11 | var app = express(); 12 | app.use(bodyParser.urlencoded({ extended: true })); 13 | app.use(bodyParser.json()); 14 | app.use(express.static(__dirname + "/public")); 15 | app.use("/", routes); 16 | 17 | var db = process.env.MONGODB_URI || "mongodb://heroku_fb4hnl6j:sjldcihu9031t59269ko8pk4t@ds163360.mlab.com:63360/heroku_fb4hnl6j"; 18 | 19 | // Connect mongoose to our database 20 | mongoose.connect(db, function(error) { 21 | // Log any errors connecting with mongoose 22 | if (error) { 23 | console.error(error); 24 | } 25 | // Or log a success message 26 | else { 27 | console.log("mongoose connection is successful"); 28 | } 29 | }); 30 | 31 | // Start the server 32 | app.listen(PORT, function() { 33 | console.log("Now listening on port %s! Visit localhost:%s in your browser.", PORT, PORT); 34 | }); 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // This is the entry point or start of our react applicaton 4 | entry: "./app/app.js", 5 | 6 | // The plain compiled Javascript will be output into this file 7 | output: { 8 | filename: "public/bundle.js" 9 | }, 10 | 11 | // This section desribes the transformations we will perform 12 | module: { 13 | loaders: [ 14 | { 15 | // Only working with files that in in a .js or .jsx extension 16 | test: /\.jsx?$/, 17 | // Webpack will only process files in our app folder. This avoids processing 18 | // node modules and server files unnecessarily 19 | include: /app/, 20 | loader: "babel-loader", 21 | query: { 22 | // These are the specific transformations we'll be using. 23 | presets: ["react", "es2015"] 24 | } 25 | } 26 | ] 27 | }, 28 | // This lets us debug our react code in chrome dev tools. Errors will have lines and file names 29 | // Without this the console says all errors are coming from just coming from bundle.js 30 | devtool: "eval-source-map" 31 | }; 32 | --------------------------------------------------------------------------------