├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── client ├── README.md └── index.js ├── common ├── App.jsx ├── Auth │ ├── Auth.jsx │ ├── index.js │ └── index.styl ├── Cart │ ├── Cart.jsx │ ├── Empty-Cart.jsx │ ├── index.js │ └── index.styl ├── Nav │ ├── Nav.jsx │ └── index.styl ├── Products │ ├── Product.jsx │ ├── Products.jsx │ └── index.styl ├── api.js ├── index.styl ├── utils │ ├── make-fetch.js │ └── serialize-form.js └── vars.styl ├── gulpfile.js ├── images ├── Cart.png └── Store.png ├── package.json ├── public ├── fonts │ └── roboto │ │ ├── Apache License.txt │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-ThinItalic.ttf │ │ ├── RobotoCondensed-Bold.ttf │ │ ├── RobotoCondensed-BoldItalic.ttf │ │ ├── RobotoCondensed-Italic.ttf │ │ ├── RobotoCondensed-Light.ttf │ │ ├── RobotoCondensed-LightItalic.ttf │ │ └── RobotoCondensed-Regular.ttf ├── images │ ├── AddToCartSelected.png │ ├── AddToCartUnselected.png │ ├── HeartItemSelected.png │ ├── HeartItemUnselected.png │ ├── SearchIcon.png │ ├── SearchIconActive.png │ ├── cart │ │ ├── AddOneItem.png │ │ ├── DeleteItem.png │ │ └── SubtractOneItem.png │ ├── navbar │ │ ├── CartIcon.png │ │ ├── Logo.png │ │ ├── UserLogo.png │ │ ├── UserProfile.png │ │ └── rwr-logo.png │ └── products │ │ ├── apple.png │ │ ├── apricot.png │ │ ├── banana.png │ │ ├── broccoli.png │ │ ├── carrot.png │ │ ├── cherry.png │ │ ├── dill.png │ │ ├── eggplant.png │ │ ├── garlic.png │ │ ├── grape.png │ │ ├── honeydew.png │ │ ├── kiwi.png │ │ ├── mango.png │ │ ├── mushroom.png │ │ ├── nectarine.png │ │ ├── orange.png │ │ ├── pear.png │ │ └── pineapple.png └── mocks │ ├── cart.html │ ├── log-in.html │ ├── products.html │ └── sign-up.html ├── sample.env ├── seed ├── index.js └── products.json ├── server ├── boot │ ├── authentication.js │ ├── root.js │ ├── seed-data.js │ └── services.js ├── component-config.json ├── config.json ├── datasources.json ├── middleware.development.json ├── middleware.json ├── model-config.json ├── models │ ├── product.js │ ├── product.json │ ├── user.js │ └── user.json ├── server.js └── views │ └── index.pug └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["babel-plugin-add-module-exports"], 4 | "env": { 5 | "test": { 6 | "plugins": ["istanbul"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOption": { 3 | "ecmaVersion": 6, 4 | "ecmaFeatures": { 5 | "jsx": true 6 | } 7 | }, 8 | "env": { 9 | "es6": true, 10 | "browser": true, 11 | "mocha": true, 12 | "node": true 13 | }, 14 | "parser": "babel-eslint", 15 | "plugins": [ 16 | "react", 17 | "import" 18 | ], 19 | "settings": { 20 | "import/ignore": [ 21 | "node_modules", 22 | "\\.json$" 23 | ], 24 | "import/extensions": [ 25 | ".js", 26 | ".jsx" 27 | ] 28 | }, 29 | "globals": { 30 | "Promise": true, 31 | "window": true, 32 | "$": true, 33 | "ga": true, 34 | "jQuery": true, 35 | "router": true 36 | }, 37 | "rules": { 38 | "comma-dangle": 2, 39 | "no-cond-assign": 2, 40 | "no-console": 0, 41 | "no-constant-condition": 2, 42 | "no-control-regex": 2, 43 | "no-debugger": 2, 44 | "no-dupe-keys": 2, 45 | "no-empty": 2, 46 | "no-empty-character-class": 2, 47 | "no-ex-assign": 2, 48 | "no-extra-boolean-cast": 2, 49 | "no-extra-parens": 0, 50 | "no-extra-semi": 2, 51 | "no-func-assign": 2, 52 | "no-inner-declarations": 2, 53 | "no-invalid-regexp": 2, 54 | "no-irregular-whitespace": 2, 55 | "no-negated-in-lhs": 2, 56 | "no-obj-calls": 2, 57 | "no-regex-spaces": 2, 58 | "no-reserved-keys": 0, 59 | "no-sparse-arrays": 2, 60 | "no-unreachable": 2, 61 | "use-isnan": 2, 62 | "valid-jsdoc": 2, 63 | "valid-typeof": 2, 64 | 65 | "block-scoped-var": 0, 66 | "complexity": 0, 67 | "consistent-return": 2, 68 | "curly": 2, 69 | "default-case": 2, 70 | "dot-notation": 0, 71 | "eqeqeq": 2, 72 | "guard-for-in": 2, 73 | "no-alert": 2, 74 | "no-caller": 2, 75 | "no-div-regex": 2, 76 | "no-else-return": 0, 77 | "no-eq-null": 2, 78 | "no-eval": 2, 79 | "no-extend-native": 2, 80 | "no-extra-bind": 2, 81 | "no-fallthrough": 2, 82 | "no-floating-decimal": 2, 83 | "no-implied-eval": 2, 84 | "no-iterator": 2, 85 | "no-labels": 2, 86 | "no-lone-blocks": 2, 87 | "no-loop-func": 2, 88 | "no-multi-spaces": 2, 89 | "no-multi-str": 2, 90 | "no-native-reassign": 2, 91 | "no-new": 2, 92 | "no-new-func": 2, 93 | "no-new-wrappers": 2, 94 | "no-octal": 2, 95 | "no-octal-escape": 2, 96 | "no-process-env": 0, 97 | "no-proto": 2, 98 | "no-redeclare": 2, 99 | "no-return-assign": 2, 100 | "no-script-url": 2, 101 | "no-self-compare": 2, 102 | "no-sequences": 2, 103 | "no-unused-expressions": 2, 104 | "no-void": 0, 105 | "no-warning-comments": [ 106 | 2, 107 | { 108 | "terms": [ 109 | "fixme" 110 | ], 111 | "location": "start" 112 | } 113 | ], 114 | "no-with": 2, 115 | "radix": 2, 116 | "vars-on-top": 0, 117 | "wrap-iife": [2, "any"], 118 | "yoda": 0, 119 | 120 | "strict": 0, 121 | 122 | "no-catch-shadow": 2, 123 | "no-delete-var": 2, 124 | "no-label-var": 2, 125 | "no-shadow": 0, 126 | "no-shadow-restricted-names": 2, 127 | "no-undef": 2, 128 | "no-undef-init": 2, 129 | "no-undefined": 2, 130 | "no-unused-vars": 2, 131 | "no-use-before-define": 0, 132 | 133 | "handle-callback-err": 2, 134 | "no-mixed-requires": 0, 135 | "no-new-require": 2, 136 | "no-path-concat": 2, 137 | "no-process-exit": 2, 138 | "no-restricted-modules": 0, 139 | "no-sync": 0, 140 | 141 | "brace-style": [ 142 | 2, 143 | "1tbs", 144 | { "allowSingleLine": true } 145 | ], 146 | "camelcase": 2, 147 | "comma-spacing": [ 148 | 2, 149 | { 150 | "before": false, 151 | "after": true 152 | } 153 | ], 154 | "comma-style": [ 155 | 2, "last" 156 | ], 157 | "consistent-this": 0, 158 | "eol-last": 2, 159 | "func-names": 0, 160 | "func-style": 0, 161 | "key-spacing": [ 162 | 2, 163 | { 164 | "beforeColon": false, 165 | "afterColon": true 166 | } 167 | ], 168 | "max-nested-callbacks": 0, 169 | "new-cap": 0, 170 | "new-parens": 2, 171 | "no-array-constructor": 2, 172 | "no-inline-comments": 2, 173 | "no-lonely-if": 2, 174 | "no-mixed-spaces-and-tabs": 2, 175 | "no-multiple-empty-lines": [ 176 | 2, 177 | { "max": 2 } 178 | ], 179 | "no-nested-ternary": 2, 180 | "no-new-object": 2, 181 | "semi-spacing": [2, { "before": false, "after": true }], 182 | "no-spaced-func": 2, 183 | "no-ternary": 0, 184 | "no-trailing-spaces": 2, 185 | "no-underscore-dangle": 0, 186 | "one-var": 0, 187 | "operator-assignment": 0, 188 | "padded-blocks": 0, 189 | "quote-props": [2, "as-needed"], 190 | "quotes": [ 191 | 2, 192 | "single", 193 | "avoid-escape" 194 | ], 195 | "semi": [ 196 | 2, 197 | "always" 198 | ], 199 | "sort-vars": 0, 200 | "keyword-spacing": [ 2 ], 201 | "space-before-function-paren": [ 202 | 2, 203 | "never" 204 | ], 205 | "space-before-blocks": [ 206 | 2, 207 | "always" 208 | ], 209 | "space-in-brackets": 0, 210 | "space-in-parens": 0, 211 | "space-infix-ops": 2, 212 | "space-unary-ops": [ 213 | 2, 214 | { 215 | "words": true, 216 | "nonwords": false 217 | } 218 | ], 219 | "spaced-comment": [ 220 | 2, 221 | "always", 222 | { "exceptions": ["-"] } 223 | ], 224 | "wrap-regex": 2, 225 | 226 | "max-depth": 0, 227 | "max-len": [ 228 | 2, 229 | 80, 230 | 2 231 | ], 232 | "max-params": 0, 233 | "max-statements": 0, 234 | "no-bitwise": 2, 235 | "no-plusplus": 0, 236 | 237 | "jsx-quotes": [2, "prefer-single"], 238 | "react/display-name": 2, 239 | "react/jsx-boolean-value": [2, "always"], 240 | "react/jsx-no-undef": 2, 241 | "react/jsx-sort-props": [2, { "ignoreCase": true }], 242 | "react/jsx-uses-react": 2, 243 | "react/jsx-uses-vars": 2, 244 | "react/no-did-mount-set-state": 2, 245 | "react/no-did-update-set-state": 2, 246 | "react/no-multi-comp": [2, { "ignoreStateless": true } ], 247 | "react/prop-types": 2, 248 | "react/react-in-jsx-scope": 2, 249 | "react/self-closing-comp": 2, 250 | "react/wrap-multilines": 2, 251 | "react/jsx-closing-bracket-location": [ 2, { "selfClosing": "line-aligned", "nonEmpty": "props-aligned" } ], 252 | 253 | "import/no-unresolved": 2, 254 | "import/named": 2, 255 | "import/namespace": 2, 256 | "import/default": 2, 257 | "import/export": 2, 258 | "import/imports-first": 2, 259 | "import/no-duplicates": 2, 260 | "import/newline-after-import": 2 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | coverage 18 | node_modules 19 | npm-debug.log 20 | !public/images 21 | !public/mocks 22 | public/css 23 | public/js 24 | *.env 25 | !sample.env 26 | .tern-project 27 | .nyc_output 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro-React 2 | 3 | ## User Stories 4 | 5 | #### As a user, I can... 6 | * sign up 7 | * log in 8 | * view all the products on one page 9 | * search the products 10 | * favorite specific products 11 | * add products to my cart 12 | * see the product count in the cart icon 13 | * view my cart 14 | * increase the item quantity of a specific product from my cart 15 | * see price changes reactively with quantity changes in my cart 16 | * remove an item from my cart 17 | * As a user, I should see a fully hydrated page on first load 18 | 19 | 20 | ## Backend API 21 | 22 | Our API is built using [Loopback.js](https://github.com/strongloop/loopback) 23 | To see all of the available REST API endpoints take a look at the explorer [localhost:3000/explorer](localhost:3000/explorer) 24 | 25 | Below are the main API endpoints you will use for this app 26 | 27 | ### Auth 28 | 29 | Create a new user (will also provide an access token) 30 | `POST /api/users` 31 | 32 | BODY 33 | 34 | ```js 35 | { 36 | username: String, 37 | email: String, 38 | password: String 39 | } 40 | ``` 41 | 42 | returns the user object on success (see: [server/models/user.json](server/models/user.json) for the schema) with attached access token 43 | 44 | RETURNS 45 | 46 | ``` 47 | { 48 | id: String, 49 | email: String, 50 | username: String, 51 | cart: [], 52 | accessToken: String 53 | } 54 | ``` 55 | 56 | sign in `POST /api/users/login?include=user` 57 | BODY 58 | 59 | ```js 60 | { 61 | email, 62 | password 63 | } 64 | ``` 65 | 66 | RETURNS 67 | 68 | ```js 69 | { 70 | user: User, 71 | id: String, // accessToken 72 | } 73 | ``` 74 | 75 | ### Products 76 | 77 | see [server/models/product.json](server/models/product.json) for Product schema 78 | 79 | Get the list of products available 80 | 81 | `GET /api/products` 82 | 83 | RETURNS 84 | 85 | ```js 86 | [Product, Product, ...Product] 87 | 88 | ``` 89 | 90 | ### Cart 91 | 92 | Access users cart through the users API 93 | 94 | **Add an item to the users cart** 95 | `GET /api/users/:userId/add-to-cart?access_token=${accessToken}` 96 | 97 | BODY 98 | 99 | 100 | ```js 101 | { 102 | itemId: String // the id of the product to add to the cart 103 | } 104 | ``` 105 | 106 | RETURNS 107 | 108 | ```js 109 | { 110 | cart: [ ...{ id: String, count: Number } ] 111 | } 112 | ``` 113 | 114 | returns the updated cart. Use this information to ensure your clients cart is up 115 | to date with your server 116 | 117 | 118 | **Remove an item from the users cart** 119 | `GET /api/users/:userId/remove-from-cart?access_token=${accessToken}` 120 | 121 | BODY 122 | 123 | ```js 124 | { 125 | itemId: String // the id of the product 126 | } 127 | ``` 128 | 129 | RETURNS 130 | 131 | ``` 132 | { 133 | cart: [ ...{ id: String, count: Number } ] 134 | } 135 | ``` 136 | 137 | returns the updated cart. Use this information to ensure your clients cart is up 138 | to date with your server 139 | 140 | **delete an item from the users cart** 141 | `GET /api/users/:userId/delete-form-cart?access_token=${accessToken}` 142 | 143 | BODY 144 | 145 | ```js 146 | { 147 | itemId: String // the id of the product 148 | } 149 | ``` 150 | 151 | RETURNS 152 | 153 | ```js 154 | { 155 | cart: [ ...{ id: String, count: Number } ] 156 | } 157 | ``` 158 | 159 | returns the updated cart. Use this information to ensure your clients cart is up 160 | to date with your server 161 | 162 | 163 | ## To start development 164 | 165 | ```bash 166 | npm start 167 | ``` 168 | 169 | The default gulp task will 170 | 171 | * Compile stylus files to css. 172 | * Compile React app with WebPack 173 | * Launch nodemon which will intern manage the LoopBack server 174 | * Launch `webpack-dev-server` with Hot Reloading and React Hot Loader 175 | * Launch BrowserSync which will manage injecting css and webpack build 176 | 177 | 178 | ## Useful docs 179 | 180 | * [loopback user docs](https://docs.strongloop.com/display/APIC/User+REST+API) 181 | * [redux](http://redux.js.org/docs/) 182 | * [redux-thunk](https://github.com/gaearon/redux-thunk) 183 | * [learnrx (based on v4)](http://reactivex.io/learnrx/) 184 | * [rxjs@5](http://reactivex.io/rxjs/) 185 | * [react-redux-router](https://github.com/reactjs/react-router-redux) 186 | * [react-observable](https://redux-observable.js.org/) 187 | * [express-state](https://github.com/yahoo/express-state) 188 | * [fetchr](https://github.com/yahoo/fetchr) 189 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 3 | This is the place for your application front-end files. 4 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | 5 | import App from '../common/App.jsx'; 6 | 7 | const win = typeof window !== 'undefined' ? window : {}; 8 | const container = win.document.getElementById('app'); 9 | 10 | // 11 | // 12 | // 13 | render( 14 | createElement( 15 | Router, 16 | null, 17 | createElement(App) 18 | ), 19 | container 20 | ); 21 | -------------------------------------------------------------------------------- /common/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import _ from 'lodash'; 4 | 5 | import Nav from './Nav/Nav.jsx'; 6 | import Products from './Products/Products.jsx'; 7 | import Auth from './Auth/Auth.jsx'; 8 | import Cart from './Cart/Cart.jsx'; 9 | 10 | import { fetchUser, fetchProducts, makeCartApiCall } from './api.js'; 11 | 12 | const propTypes = {}; 13 | 14 | export default class App extends Component { 15 | constructor(...args) { 16 | super(...args); 17 | this.state = { 18 | products: [], 19 | user: {}, 20 | token: '', 21 | userId: '', 22 | cart: [] 23 | }; 24 | this.handleAuth = this.handleAuth.bind(this); 25 | this.addToCart = this.addToCart.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | const id = localStorage.getItem('userId'); 30 | const token = localStorage.getItem('token'); 31 | if (id && token) { 32 | fetchUser(id, token).then(user => { 33 | console.log('user: ', user); 34 | this.handleAuth(user); 35 | }); 36 | } 37 | fetchProducts().then((products) => { 38 | this.setState({ products }); 39 | }); 40 | } 41 | 42 | handleAuth(user) { 43 | localStorage.setItem('userId', user.id); 44 | localStorage.setItem('token', user.accessToken); 45 | this.setState({ 46 | user, 47 | token: user.accessToken, 48 | userId: user.id, 49 | cart: user.cart 50 | }); 51 | } 52 | 53 | addToCart(itemId) { 54 | const method = 'ADD_TO_CART'; 55 | const { token, userId } = this.state; 56 | console.log('attemt to add: ', itemId); 57 | console.log('userId: ', userId); 58 | makeCartApiCall( 59 | method, 60 | userId, 61 | token, 62 | itemId 63 | ).then(({ cart }) => { 64 | console.log('cart: ', cart); 65 | this.setState({ 66 | cart 67 | }); 68 | }); 69 | } 70 | 71 | render() { 72 | const { 73 | products, 74 | user, 75 | cart 76 | } = this.state; 77 | const { username } = user; 78 | return ( 79 |
80 |
144 | ); 145 | } 146 | } 147 | 148 | App.displayName = 'App'; 149 | App.propTypes = propTypes; 150 | -------------------------------------------------------------------------------- /common/Auth/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import { auth } from '../api.js'; 5 | 6 | const propTypes = { 7 | isSignUp: PropTypes.bool, 8 | onSubmit: PropTypes.func 9 | }; 10 | 11 | export default class Auth extends Component { 12 | constructor(...args) { 13 | super(...args); 14 | this.state = { 15 | isAuthed: false 16 | }; 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | } 19 | 20 | handleSubmit(e) { 21 | e.preventDefault(); 22 | auth(this.props.isSignUp, e.target) 23 | .then(user => { 24 | console.log('user: ', user); 25 | this.props.onSubmit(user); 26 | this.setState({ isAuthed: true }); 27 | }); 28 | } 29 | 30 | render() { 31 | const { isSignUp } = this.props; 32 | const { isAuthed } = this.state; 33 | if (isAuthed) { 34 | return ; 35 | } 36 | return ( 37 |
38 |
39 | 42 | { isSignUp ? 43 | 46 | : null 47 | } 48 | 51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | Auth.propTypes = propTypes; 59 | -------------------------------------------------------------------------------- /common/Auth/index.js: -------------------------------------------------------------------------------- 1 | import Auth from './Auth.jsx'; 2 | 3 | export default Auth; 4 | -------------------------------------------------------------------------------- /common/Auth/index.styl: -------------------------------------------------------------------------------- 1 | .auth 2 | display flex 3 | justify-content center 4 | margin-top 160px 5 | 6 | form 7 | color blue 8 | 9 | label 10 | display inline 11 | font-size 18px 12 | font-weight 300 13 | &:after 14 | content "\a" 15 | white-space pre 16 | 17 | input 18 | width 400px 19 | border none 20 | border-bottom 1px solid blue 21 | font-size 28px 22 | font-weight 100 23 | padding 12px 0 14px 20px 24 | color purple-dark 25 | &:focus 26 | box-shadow none 27 | outline none 28 | 29 | ::-webkit-input-placeholder { 30 | color: blue; 31 | } 32 | 33 | :-moz-placeholder { /* Firefox 18- */ 34 | color: blue; 35 | } 36 | 37 | ::-moz-placeholder { /* Firefox 19+ */ 38 | color: blue; 39 | } 40 | 41 | :-ms-input-placeholder { 42 | color: blue; 43 | } 44 | 45 | button 46 | background-color purple 47 | color white 48 | border-top none 49 | border-left none 50 | border-right 3px solid purple-dark 51 | border-bottom 3px solid purple-dark 52 | border-radius 2px 53 | margin 25px 0 54 | padding 8px 20px 55 | font-size 24px 56 | font-weight 300 57 | &:hover 58 | background-color purple-dark 59 | -------------------------------------------------------------------------------- /common/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | 4 | export default class Cart extends Component { 5 | render() { 6 | const { cart } = this.props; 7 | const totalSum = cart.reduce((sum, item) => { 8 | return sum + item.totalItem; 9 | }, 0); 10 | return ( 11 |
12 |
13 |

My Cart

14 |
15 |
16 |
17 |
18 | Item 19 |
20 |
21 | Qty 22 |
23 |
24 | Price 25 |
26 |
27 |
28 | { cart.map(({ count, image, name, price, totalItem }) => { 29 | return ( 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | { name } 38 |
39 |
40 | $ { price } 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 | { count } 50 |
51 |
52 |
53 |
54 | $ { totalItem } 55 |
56 |
57 |
58 | ); 59 | }) } 60 |
61 |
62 |
63 |
64 | Total 65 |
66 |
67 | $ { totalSum } 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /common/Cart/Empty-Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | export default class EmptyCart extends PureComponent { 4 | render() { 5 | return ( 6 |
7 |
8 |

Your cart is empty

9 |
10 |
11 | ); 12 | } 13 | } 14 | EmptyCart.displayName = 'EmptyCart'; 15 | -------------------------------------------------------------------------------- /common/Cart/index.js: -------------------------------------------------------------------------------- 1 | import Cart from './Cart.jsx'; 2 | 3 | export default Cart; 4 | -------------------------------------------------------------------------------- /common/Cart/index.styl: -------------------------------------------------------------------------------- 1 | .cart { 2 | padding: 40px 100px; 3 | } 4 | 5 | .cart-empty-link { 6 | text-decoration: none; 7 | .cart-empty-link-text { 8 | color: purple; 9 | font-size: 25px; 10 | height: 50px; 11 | text-align: center; 12 | margin-top: 50px; 13 | } 14 | } 15 | 16 | .cart-title { 17 | display: flex; 18 | & h2 { 19 | padding-top: 20px; 20 | margin: 0 auto; 21 | font-weight: 300; 22 | color: blue; 23 | font-size: 36px; 24 | 25 | } 26 | } 27 | 28 | .cart-list { 29 | max-width: 900px; 30 | min-width: 800px; 31 | margin-left: auto; 32 | margin-right: auto; 33 | } 34 | 35 | .cart-list > div:nth-of-type(1) { 36 | margin-bottom: -10px; 37 | color: blue; 38 | font-size: 24px; 39 | font-weight: 300; 40 | } 41 | 42 | .cart-list > div:last-of-type { 43 | background-color: blue; 44 | color: white; 45 | } 46 | 47 | .cart-list-row { 48 | display: flex; 49 | height: 110px; 50 | margin-bottom: 30px; 51 | 52 | .cart-list-item:first-child { 53 | flex: 1 50%; 54 | } 55 | 56 | .cart-list-item { 57 | flex: 3; 58 | text-align: center; 59 | align-self: center; 60 | } 61 | } 62 | 63 | .cart-list-product { 64 | display: flex; 65 | border: 1px solid blue; 66 | height: 110px; 67 | 68 | .cart-list-stock-photo { 69 | flex: 2; 70 | overflow: hidden; 71 | } 72 | 73 | .cart-list-info { 74 | flex: 1; 75 | flex-flow: column nowrap; 76 | display: flex; 77 | 78 | .cart-list-info-name { 79 | flex: 1; 80 | color: blue; 81 | font-size: 20px; 82 | margin-top: 15px; 83 | margin-right: 30px; 84 | align-self: flex-end; 85 | } 86 | .cart-list-info-price { 87 | flex: 1; 88 | align-self: flex-end; 89 | margin-right: 30px; 90 | } 91 | } 92 | } 93 | 94 | .cart-count-down 95 | img 96 | max-width 25px 97 | &:hover 98 | cursor pointer 99 | 100 | .cart-count-up 101 | img 102 | max-width 25px 103 | &:hover 104 | cursor pointer 105 | 106 | .cart-delete-item 107 | img 108 | max-width 20px 109 | &:hover 110 | cursor pointer 111 | 112 | .cart-count-count 113 | margin-bottom 6px 114 | 115 | .cart-list-count { 116 | display: flex; 117 | flex-flow: column nowrap; 118 | padding: 10px 0; 119 | .cart-count-item { 120 | flex: 1; 121 | color: purple; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /common/Nav/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const propTypes = { 5 | isSignedIn: PropTypes.bool, 6 | name: PropTypes.string, 7 | numOfItems: PropTypes.number 8 | }; 9 | 10 | export default class Nav extends Component { 11 | render() { 12 | const { isSignedIn, name, numOfItems } = this.props; 13 | let navListClassName = 'nav-list'; 14 | let leftNav = null; 15 | if (isSignedIn) { 16 | navListClassName = navListClassName + ' nav-list-named'; 17 | leftNav = ( 18 |
    19 |
  • 20 | 21 |
  • 22 |
  • { name }
  • 23 |
  • 24 | 25 | 26 | { numOfItems } 27 | 28 |
  • 29 |
30 | ); 31 | } else { 32 | leftNav = ( 33 |
    34 |
  • Sign Up
  • 35 |
  • Log In
  • 36 |
37 | ); 38 | } 39 | return ( 40 | 51 | ); 52 | } 53 | } 54 | 55 | Nav.propTypes = propTypes; 56 | Nav.displayName = 'Nav'; 57 | -------------------------------------------------------------------------------- /common/Nav/index.styl: -------------------------------------------------------------------------------- 1 | .nav { 2 | min-height: 20%; 3 | padding-top: 10px; 4 | padding-left: 20px; 5 | padding-right: 20px; 6 | border-bottom: 1px solid blue; 7 | display: flex; 8 | align-items: flex-start; 9 | flex-direction: column; 10 | justify-content: center; 11 | clearfix(); 12 | } 13 | 14 | .nav-logo 15 | img 16 | height 50px 17 | min-width 100% 18 | max-width 100% 19 | margin-top: 10px 20 | +below(800px) 21 | height 100% 22 | width 100% 23 | 24 | .nav-list 25 | align-self flex-end 26 | list-style-type none 27 | margin -50px 0 0 0 28 | padding 20px 29 | li 30 | display inline 31 | padding 30px 32 | color blue 33 | &:hover 34 | color purple-dark 35 | 36 | .nav-list-named { 37 | display: flex; 38 | flex-flow: row wrap; 39 | justify-content: space-around; 40 | padding: 7.5px 10px; 41 | li { 42 | padding: 0 30px; 43 | align-items: flex-end; 44 | } 45 | } 46 | 47 | .nav-list-user-name { 48 | padding: 0; 49 | align-self: flex-end; 50 | } 51 | 52 | .nav-list-user-logo 53 | padding 0 54 | img 55 | width 30px 56 | 57 | .nav-list-cart 58 | padding 0 59 | img 60 | width 40px 61 | a 62 | color purple-dark 63 | 64 | .nav-list-login 65 | a 66 | color blue 67 | text-decoration none 68 | &:hover 69 | color purple-dark 70 | 71 | .nav-list-sign-up 72 | a 73 | color blue 74 | text-decoration none 75 | &:hover 76 | color purple-dark 77 | -------------------------------------------------------------------------------- /common/Products/Product.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | id: PropTypes.number, 5 | image: PropTypes.string, 6 | isInCart: PropTypes.bool, 7 | name: PropTypes.string, 8 | description: PropTypes.string, 9 | handleClick: PropTypes.func 10 | }; 11 | 12 | export default function Product({ 13 | id, 14 | image, 15 | isInCart, 16 | name, 17 | description, 18 | handleClick 19 | }) { 20 | const cartImage = isInCart ? 'Selected' : 'Unselected'; 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 | { name } 28 |
29 |
30 | { description } 31 |
32 |
33 |
34 | 37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | Product.propTypes = propTypes; 44 | -------------------------------------------------------------------------------- /common/Products/Products.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | import Product from './Product.jsx'; 4 | 5 | const propTypes = { 6 | addToCart: PropTypes.func, 7 | products: PropTypes.array 8 | }; 9 | 10 | export default class Products extends Component { 11 | constructor(...args) { 12 | super(...args); 13 | this.state = { 14 | search: '' 15 | }; 16 | } 17 | 18 | render() { 19 | const { search } = this.state; 20 | const { addToCart, products } = this.props; 21 | const searchRegex = new RegExp(search); 22 | return ( 23 |
24 |
25 | { 28 | console.log('searching for ', e.target.value); 29 | this.setState({ 30 | search: e.target.value 31 | }); 32 | } } 33 | value={ search } 34 | /> 35 |
36 |
37 | { products 38 | .filter(({ name }) => { 39 | if (search.length > 2) { 40 | return searchRegex.test(name); 41 | } 42 | return true; 43 | }) 44 | .map(({ id, name, description, image, isInCart }) => { 45 | return ( 46 | 55 | ); 56 | }) } 57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | Products.propTypes = propTypes; 64 | -------------------------------------------------------------------------------- /common/Products/index.styl: -------------------------------------------------------------------------------- 1 | .products { 2 | margin: 0 auto; 3 | max-width: 1200px; 4 | } 5 | 6 | .products-search { 7 | row(); 8 | display: flex; 9 | justify-content: center; 10 | margin: 50px 0; 11 | } 12 | 13 | .products-search_input 14 | width 400px 15 | border none 16 | border-bottom 1px solid blue 17 | font-size 28px 18 | font-weight 100 19 | padding 12px 0 14px 50px 20 | color blue 21 | background url(/images/SearchIcon.png) no-repeat 0 12px 22 | background-size 45px auto 23 | &:focus 24 | box-shadow none 25 | outline none 26 | background url(/images/SearchIconActive.png) no-repeat 0 12px 27 | background-size 45px auto 28 | border-bottom 1px solid purple-dark 29 | 30 | 31 | 32 | .products-lists 33 | display flex 34 | flex-flow row wrap 35 | justify-content space-around 36 | margin-bottom 100px 37 | 38 | .empty 39 | display flex 40 | & h2 41 | padding-top 20px 42 | margin 0 auto 43 | font-weight 300x 44 | color blue 45 | font-size 36px 46 | 47 | .products-item 48 | border 1px solid blue 49 | border-radius 2px 50 | margin 40px 20px 51 | overflow hidden 52 | width 300px 53 | height 375px 54 | 55 | .products-item-name 56 | padding 5px 5px 0 10px 57 | color blue 58 | font-size 26px 59 | 60 | .products-item-description 61 | color fontGray 62 | font-weight 300 63 | height 88px 64 | margin 10px 5px 65 | overflow hidden 66 | padding 5px 67 | 68 | 69 | .products-item-footer 70 | display flex 71 | justify-content space-between 72 | align-items flex-end 73 | 74 | .products-item-cart 75 | .products-item-favorite 76 | padding 10px 12px 77 | background-color: transparent; 78 | border: none; 79 | img 80 | width 35px 81 | 82 | button 83 | background-color transparent 84 | &:focus 85 | outline none 86 | 87 | 88 | .products-item-stock-photo { 89 | overflow: hidden; 90 | } 91 | -------------------------------------------------------------------------------- /common/api.js: -------------------------------------------------------------------------------- 1 | import makeFetch from './utils/make-fetch.js'; 2 | import serializeForm from './utils/serialize-form.js'; 3 | 4 | const api = '/api/users'; 5 | const defaultOptions = { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | } 10 | }; 11 | export function fetchProducts() { 12 | return makeFetch('/api/products'); 13 | } 14 | 15 | export const cartMethods = { 16 | ADD_TO_CART: 'add-to-cart', 17 | REMOVE_FROM_CART: 'remove-from-cart', 18 | DELETE_FROM_CART: 'delete-from-cart' 19 | }; 20 | export function makeCartApiCall(type, id, token, itemId) { 21 | const method = cartMethods[type]; 22 | const url = `${api}/${id}/${method}?access_token=${token}`; 23 | const options = { 24 | ...defaultOptions, 25 | body: JSON.stringify({ itemId }) 26 | }; 27 | return makeFetch(url, options); 28 | } 29 | 30 | makeCartApiCall.cartMethods = cartMethods; 31 | 32 | export function fetchUser(id, token) { 33 | const options = { 34 | ...defaultOptions, 35 | method: 'GET' 36 | }; 37 | return makeFetch(api + `/${id}?access_token=${token}`, options) 38 | // normalize user data 39 | .then(user => ({ ...user, accessToken: token })); 40 | } 41 | 42 | export function auth(isSignUp, form) { 43 | const options = { 44 | ...defaultOptions, 45 | body: serializeForm(form) 46 | }; 47 | const url = isSignUp ? 48 | api : 49 | api + '/login?include=user'; 50 | return makeFetch(url, options) 51 | .then(res => ( 52 | // normalize server response 53 | isSignUp ? 54 | res : 55 | { ...res.user, accessToken: res.id } 56 | )); 57 | } 58 | -------------------------------------------------------------------------------- /common/index.styl: -------------------------------------------------------------------------------- 1 | @import "kouto-swiss" 2 | normalize(); 3 | box-sizing-reset(); 4 | 5 | @font-face 6 | font-family roboto 7 | src url(/fonts/roboto/Roboto-Regular.ttf) 8 | body { 9 | width: 100vw; 10 | font-family: roboto; 11 | } 12 | 13 | .app { 14 | gs('fluid'); 15 | } 16 | 17 | .app-child { 18 | row(); 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | } 24 | 25 | @import './vars.styl'; 26 | @import './Auth'; 27 | @import './Cart'; 28 | @import './Nav'; 29 | @import './Products'; 30 | -------------------------------------------------------------------------------- /common/utils/make-fetch.js: -------------------------------------------------------------------------------- 1 | export default function makeFetch(uri, options) { 2 | return fetch(uri, options).then(res => { 3 | if (!res.ok) { 4 | return Promise.reject(new Error(res.statusText)); 5 | } 6 | return res.json(); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /common/utils/serialize-form.js: -------------------------------------------------------------------------------- 1 | export default function serializeForm(form) { 2 | const data = [].filter.call(form.elements, node => !!node.name) 3 | .reduce((data, node) => { 4 | data[node.name] = node.value; 5 | return data; 6 | }, {}); 7 | 8 | return JSON.stringify(data); 9 | } 10 | -------------------------------------------------------------------------------- /common/vars.styl: -------------------------------------------------------------------------------- 1 | blue = #02AEEF; 2 | blue-dark = #009AD4; 3 | purple = #BD10E0; 4 | purple-dark = #9F0CBE; 5 | gray = #C1C1C1; 6 | gray-dark = #565656; 7 | fontGray = #888888; 8 | roboto = "roboto", sans-serif; 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | process.env.DEBUG = process.env.DEBUG || 'ar:*'; 2 | require('dotenv').load(); 3 | var gulp = require('gulp'); 4 | // var gutil = require('gulp-util'); 5 | var notify = require('gulp-notify'); 6 | var plumber = require('gulp-plumber'); 7 | var sourcemaps = require('gulp-sourcemaps'); 8 | var stylus = require('gulp-stylus'); 9 | 10 | var swiss = require('kouto-swiss'); 11 | var nodemon = require('nodemon'); 12 | var debugFactory = require('debug'); 13 | 14 | var webpack = require('webpack'); 15 | var webpackDevMiddleware = require('webpack-dev-middleware'); 16 | var webpackHotMiddleware = require('webpack-hot-middleware'); 17 | var browserSync = require('browser-sync'); 18 | 19 | var yargs = require('yargs'); 20 | 21 | var pckg = require('./package.json'); 22 | var webpackConfig = require('./webpack.config'); 23 | 24 | var debug = debugFactory('ar:gulp'); 25 | 26 | var sync = browserSync.create('ar-sync-server'); 27 | var reload = sync.reload.bind(sync); 28 | 29 | // user definable ports 30 | var port = yargs.argv.port || process.env.PORT || '3001'; 31 | var syncPort = yargs.argv['sync-port'] || process.env.SYNC_PORT || '3000'; 32 | // make sure sync ui port does not interfere with proxy port 33 | var syncUIPort = yargs.argv['sync-ui-port'] || 34 | process.env.SYNC_UI_PORT || 35 | parseInt(syncPort, 10) + 2; 36 | var isNotSSR = !process.env.SSR; 37 | var paths = { 38 | server: pckg.main, 39 | serverIgnore: [ 40 | 'client/**/*', 41 | 'package.json', 42 | 'gulpfile.js' 43 | ], 44 | stylus: './common/index.styl', 45 | stylusFiles: [ 46 | './client/**/*.styl', 47 | './common/**/*.styl' 48 | ], 49 | public: './public', 50 | syncWatch: [ 51 | './server/views/**.jade', 52 | './public/main.css' 53 | ] 54 | }; 55 | 56 | if (isNotSSR) { 57 | debug('no ssr'); 58 | paths.serverIgnore.push('common/**/*'); 59 | } 60 | 61 | function errorHandler() { 62 | var args = Array.prototype.slice.call(arguments); 63 | 64 | // Send error to notification center with gulp-notify 65 | notify.onError({ 66 | title: 'Compile Error', 67 | message: '<%= error %>' 68 | }).apply(this, args); 69 | 70 | // Keep gulp from hanging on this task 71 | this.emit('end'); 72 | } 73 | 74 | gulp.task('serve', function(cb) { 75 | var called = false; 76 | const monitor = nodemon({ 77 | script: paths.server, 78 | ext: '.jsx .js .json', 79 | ignore: paths.serverIgnore, 80 | // exec: path.join(__dirname, 'node_modules/.bin/babel-node'), 81 | env: { 82 | NODE_ENV: process.env.NODE_ENV || 'development', 83 | DEBUG: process.env.DEBUG || 'ar:*', 84 | PORT: port 85 | } 86 | }) 87 | .on('start', function() { 88 | if (!called) { 89 | called = true; 90 | setTimeout(function() { 91 | cb(); 92 | }); 93 | } 94 | }) 95 | .on('restart', function(files) { 96 | if (files) { debug('Files that changes: ', files); } 97 | }); 98 | // add clean hear to prevent nodemon from running after 99 | // exit 100 | // see: JacksonGariety/gulp-nodemon/issues/77 101 | process.once('SIGINT', () => { 102 | monitor.once('exit', () => { 103 | /* eslint-disable no-process-exit */ 104 | process.exit(0); 105 | /* eslint-enable no-process-exit */ 106 | }); 107 | }); 108 | }); 109 | 110 | gulp.task('stylus', function() { 111 | return gulp.src(paths.stylus) 112 | .pipe(plumber({ errorHandler: errorHandler })) 113 | .pipe(sourcemaps.init()) 114 | .pipe(stylus({ 115 | use: swiss() 116 | })) 117 | .pipe(sourcemaps.write()) 118 | .pipe(gulp.dest(paths.public + '/css')) 119 | .pipe(reload({ stream: true })); 120 | }); 121 | 122 | var syncDependents = [ 123 | 'serve', 124 | 'stylus' 125 | ]; 126 | 127 | gulp.task('dev-server', syncDependents, function() { 128 | webpackConfig.entry.bundle = [ 129 | 'webpack/hot/dev-server', 130 | 'webpack-hot-middleware/client' 131 | ].concat(webpackConfig.entry.bundle); 132 | 133 | var bundler = webpack(webpackConfig); 134 | 135 | sync.init(null, { 136 | ui: { 137 | port: syncUIPort 138 | }, 139 | proxy: 'http://localhost:' + port, 140 | logLeval: 'debug', 141 | files: paths.syncWatch, 142 | port: syncPort, 143 | open: false, 144 | middleware: [ 145 | webpackDevMiddleware(bundler, { 146 | publicPath: webpackConfig.output.publicPath, 147 | stats: 'errors-only' 148 | }), 149 | webpackHotMiddleware(bundler) 150 | ] 151 | }); 152 | }); 153 | 154 | gulp.task('watch', function() { 155 | gulp.watch(paths.stylusFiles, ['stylus']); 156 | }); 157 | 158 | gulp.task('default', [ 'serve', 'stylus', 'dev-server', 'watch' ]); 159 | -------------------------------------------------------------------------------- /images/Cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/images/Cart.png -------------------------------------------------------------------------------- /images/Store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/images/Store.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-redux", 3 | "version": "0.0.1", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "test": "NODE_ENV=test nyc ava", 7 | "test:watch": "ava --watch", 8 | "cover": "nyc report --reporter=html", 9 | "cover:watch": "nodemon --watch test/ --exec 'npm run test && npm run cover'", 10 | "cover:show": "open coverage/index.html", 11 | "cover:alls": "nyc report --reporter=text-lcov | coveralls", 12 | "lint": "eslint .", 13 | "start": "npm run create-env && gulp", 14 | "seed": "npm run create-env && node seed", 15 | "create-env": "node -e \"var fs = require('fs'); fs.access('./.env', function(err) { if (err) { console.log('\\n\\ncreating .env file\\n\\n'); return fs.writeFileSync('./.env', ''); } console.log('\\n\\n.env file present\\n\\n'); });\"", 16 | "static": "httpster -p 3003 -d public" 17 | }, 18 | "dependencies": { 19 | "babel-core": "^6.14.0", 20 | "babel-plugin-add-module-exports": "^0.2.1", 21 | "babel-preset-es2015": "^6.14.0", 22 | "babel-preset-react": "^6.11.1", 23 | "babel-preset-stage-0": "^6.5.0", 24 | "babel-register": "^6.14.0", 25 | "compression": "^1.0.3", 26 | "cors": "^2.5.2", 27 | "debug": "^2.2.0", 28 | "dotenv": "^2.0.0", 29 | "express-state": "^1.4.0", 30 | "fetchr": "^0.5.37", 31 | "helmet": "^1.3.0", 32 | "lodash": "^4.15.0", 33 | "loopback": "^2.22.0", 34 | "loopback-boot": "^2.6.5", 35 | "loopback-component-explorer": "^2.4.0", 36 | "loopback-datasource-juggler": "^2.39.0", 37 | "morgan": "^1.7.0", 38 | "normalizr": "^2.2.1", 39 | "pug": "^2.0.0-beta6", 40 | "react": "15.3.2", 41 | "react-dom": "15.3.2", 42 | "react-redux": "^5.0.4", 43 | "react-redux-epic": "^0.3.1", 44 | "react-router": "^2.7.0", 45 | "react-router-dom": "^4.1.1", 46 | "react-router-redux": "^4.0.5", 47 | "redux": "^3.6.0", 48 | "redux-actions": "^1.1.0", 49 | "redux-observable": "^0.14.1", 50 | "redux-promise": "^0.5.3", 51 | "redux-thunk": "^2.1.0", 52 | "reselect": "^2.5.4", 53 | "rxjs": "^5.4.0", 54 | "serve-favicon": "^2.0.1", 55 | "strong-error-handler": "^1.0.1" 56 | }, 57 | "devDependencies": { 58 | "ava": "^0.19.1", 59 | "babel-eslint": "^7.2.3", 60 | "babel-loader": "^6.2.5", 61 | "babel-plugin-istanbul": "^4.1.4", 62 | "browser-sync": "^2.15.0", 63 | "eslint": "^3.4.0", 64 | "eslint-config-loopback": "^4.0.0", 65 | "eslint-plugin-import": "^1.14.0", 66 | "eslint-plugin-react": "^6.2.0", 67 | "gulp": "^3.9.1", 68 | "gulp-notify": "^2.2.0", 69 | "gulp-plumber": "^1.1.0", 70 | "gulp-sourcemaps": "^1.6.0", 71 | "gulp-stylus": "^2.5.0", 72 | "gulp-util": "^3.0.7", 73 | "httpster": "^1.0.3", 74 | "json-loader": "^0.5.4", 75 | "kouto-swiss": "^0.12.0", 76 | "nodemon": "^1.10.2", 77 | "nyc": "^11.0.1", 78 | "react-hot-loader": "^1.3.0", 79 | "webpack": "^1.13.2", 80 | "webpack-dev-middleware": "^1.6.1", 81 | "webpack-hot-middleware": "^2.12.2", 82 | "yargs": "^5.0.0" 83 | }, 84 | "ava": { 85 | "require": [ 86 | "babel-register" 87 | ], 88 | "babel": "inherit", 89 | "failFast": true 90 | }, 91 | "nyc": { 92 | "sourceMap": false, 93 | "instrument": false 94 | }, 95 | "repository": { 96 | "type": "", 97 | "url": "" 98 | }, 99 | "license": "BSD-3-Clause", 100 | "description": "advanced-redux" 101 | } 102 | -------------------------------------------------------------------------------- /public/fonts/roboto/Apache License.txt: -------------------------------------------------------------------------------- 1 | Font data copyright Google 2012 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-BoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-Light.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-LightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/roboto/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/fonts/roboto/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /public/images/AddToCartSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/AddToCartSelected.png -------------------------------------------------------------------------------- /public/images/AddToCartUnselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/AddToCartUnselected.png -------------------------------------------------------------------------------- /public/images/HeartItemSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/HeartItemSelected.png -------------------------------------------------------------------------------- /public/images/HeartItemUnselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/HeartItemUnselected.png -------------------------------------------------------------------------------- /public/images/SearchIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/SearchIcon.png -------------------------------------------------------------------------------- /public/images/SearchIconActive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/SearchIconActive.png -------------------------------------------------------------------------------- /public/images/cart/AddOneItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/cart/AddOneItem.png -------------------------------------------------------------------------------- /public/images/cart/DeleteItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/cart/DeleteItem.png -------------------------------------------------------------------------------- /public/images/cart/SubtractOneItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/cart/SubtractOneItem.png -------------------------------------------------------------------------------- /public/images/navbar/CartIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/navbar/CartIcon.png -------------------------------------------------------------------------------- /public/images/navbar/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/navbar/Logo.png -------------------------------------------------------------------------------- /public/images/navbar/UserLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/navbar/UserLogo.png -------------------------------------------------------------------------------- /public/images/navbar/UserProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/navbar/UserProfile.png -------------------------------------------------------------------------------- /public/images/navbar/rwr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/navbar/rwr-logo.png -------------------------------------------------------------------------------- /public/images/products/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/apple.png -------------------------------------------------------------------------------- /public/images/products/apricot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/apricot.png -------------------------------------------------------------------------------- /public/images/products/banana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/banana.png -------------------------------------------------------------------------------- /public/images/products/broccoli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/broccoli.png -------------------------------------------------------------------------------- /public/images/products/carrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/carrot.png -------------------------------------------------------------------------------- /public/images/products/cherry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/cherry.png -------------------------------------------------------------------------------- /public/images/products/dill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/dill.png -------------------------------------------------------------------------------- /public/images/products/eggplant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/eggplant.png -------------------------------------------------------------------------------- /public/images/products/garlic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/garlic.png -------------------------------------------------------------------------------- /public/images/products/grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/grape.png -------------------------------------------------------------------------------- /public/images/products/honeydew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/honeydew.png -------------------------------------------------------------------------------- /public/images/products/kiwi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/kiwi.png -------------------------------------------------------------------------------- /public/images/products/mango.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/mango.png -------------------------------------------------------------------------------- /public/images/products/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/mushroom.png -------------------------------------------------------------------------------- /public/images/products/nectarine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/nectarine.png -------------------------------------------------------------------------------- /public/images/products/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/orange.png -------------------------------------------------------------------------------- /public/images/products/pear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/pear.png -------------------------------------------------------------------------------- /public/images/products/pineapple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworldreact/react-shoppe/43e5b85d32dc21a569f261c7f60b3d7cf4f8a39f/public/images/products/pineapple.png -------------------------------------------------------------------------------- /public/mocks/cart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cart 5 | 6 | 7 | 8 |
9 | 23 |
24 |
25 |
26 |

My Cart

27 |
28 |
29 |
30 |
31 | Item 32 |
33 |
34 | Qty 35 |
36 |
37 | Price 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Apricots 47 |
48 |
49 | $ 1.50 50 |
51 |
52 |
53 |
54 |
55 |
56 | 3 57 |
58 |
59 |
60 |
61 | $4.50 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Total 70 |
71 |
72 | $4.50 73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /public/mocks/log-in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Auth 5 | 6 | 7 | 8 |
9 |
10 | 25 |
26 |
27 |
28 | 31 | 34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /public/mocks/products.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Products Page 5 | 6 | 7 | 8 |
9 | 24 |
25 |
26 | 29 |
30 |
31 |
32 |
33 | Apples 34 |
35 |
36 | The apple tree is a deciduous tree in the rose family best known for its sweet, pomaceous fruit, the apple. Don't accept from snakes. 37 |
38 | 43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/mocks/sign-up.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Auth 5 | 6 | 7 | 8 |
9 |
10 | 25 |
26 |
27 |
28 | 31 | 34 | 37 | 38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # This is the port for our Loopback server 2 | PORT=3001 3 | # Make BrowserSync main entry during dev work 4 | # It is in charge of bundling the app, 5 | # will inject css when it compiles, 6 | # and will proxy the main server 7 | SYNC_PORT=3000 8 | -------------------------------------------------------------------------------- /seed/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | const { bindNodeCallback } = require('rxjs/observable/bindNodeCallback'); 3 | require('rxjs/add/operator/switchMap'); 4 | require('rxjs/add/operator/do'); 5 | const products = require('./products.json'); 6 | const app = require('../server/server'); 7 | 8 | const Product = app.models.Product; 9 | 10 | const destroyProducts = bindNodeCallback(Product.destroyAll.bind(Product)); 11 | const createProducts = bindNodeCallback(Product.create.bind(Product)); 12 | 13 | console.log('\nclear previous products\n'); 14 | destroyProducts() 15 | .do(() => console.log('\ncreating new products\n')) 16 | .switchMap(() => createProducts(products)) 17 | .subscribe( 18 | () => {}, 19 | err => { throw err; }, 20 | () => process.exit(0) 21 | ); 22 | -------------------------------------------------------------------------------- /seed/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "Apples", 5 | "description": "The apple tree is a deciduous tree in the rose family best known for its sweet, pomaceous fruit, the apple. Don't accept from snakes.", 6 | "image": "apple.png", 7 | "nutrition": ["Vitamin C", "Fiber"], 8 | "price": 0.50 9 | }, 10 | { 11 | "id": "2", 12 | "name": "Apricots", 13 | "description": "An apricot is a fruit or the tree that bears the fruit of several species in the genus Prunus. Grind the pits for a facial exfoliant.", 14 | "image": "apricot.png", 15 | "nutrition": ["Vitamin A", "Vitamin C"], 16 | "price": 1.50 17 | }, 18 | { 19 | "id": "3", 20 | "name": "Bananas", 21 | "description": "The banana is an edible fruit, botanically a berry, produced by several kinds of large herbaceous flowering plants in the genus Musa.", 22 | "image": "banana.png", 23 | "nutrition": ["Potassium", "Vitamin C", "Vitamin B-6"], 24 | "price": 2.50 25 | }, 26 | { 27 | "id": "4", 28 | "name": "Broccoli", 29 | "description": "Broccoli is an edible green plant in the cabbage family whose large flowering head is eaten as a vegetable. Try with cheese.", 30 | "image": "broccoli.png", 31 | "nutrition": ["Potassium", "Vitamin C", "Fiber"], 32 | "price": 1.75 33 | }, 34 | { 35 | "id": "5", 36 | "name": "Carrots", 37 | "description": "The carrot is a root vegetable, usually orange in colour, though purple, black, red, white, and yellow varieties exist. Carrots are a domesticated form...", 38 | "image": "carrot.png", 39 | "nutrition": ["Vitamin A", "Vitamin C"], 40 | "price": 0.15 41 | }, 42 | { 43 | "id": "6", 44 | "name": "Cherries", 45 | "description": "A cherry is the fruit of many plants of the genus Prunus, and is a fleshy drupe. Try eating the seeds, they're delicious.", 46 | "image": "cherry.png", 47 | "nutrition": ["Vitamin A", "Vitamin C", "Potassium"], 48 | "price": 2.05 49 | }, 50 | { 51 | "id": "7", 52 | "name": "Dill", 53 | "description": "Dill is an annual herb in the celery family Apiaceae. A great addition to soups. Not to be confused with pickles.", 54 | "image": "dill.png", 55 | "nutrition": ["Calcium", "Magnesium"], 56 | "price": 0.99 57 | }, 58 | { 59 | "id": "8", 60 | "name": "Eggplant", 61 | "description": "Eggplant or aubergine is a species of nightshade grown for its edible fruit. Eggplant is the common name in North American and Australian English but..", 62 | "image": "eggplant.png", 63 | "nutrition": ["Fiber"], 64 | "price": 3.50 65 | }, 66 | { 67 | "id": "9", 68 | "name": "Garlic", 69 | "description": "Allium sativum, commonly known as garlic, is a species in the onion genus, Allium. Its close relatives include the onion, shallot, leek, chive, and...", 70 | "image": "garlic.png", 71 | "nutrition": ["Potassium"], 72 | "price": 1.52 73 | }, 74 | { 75 | "id": "10", 76 | "name": "Grapes", 77 | "description": "A grape is a fruiting berry of the deciduous woody vines of the botanical genus Vitis. Grapes can be eaten fresh as table grapes or they can be...", 78 | "image": "grape.png", 79 | "nutrition": ["Vitamin C", "Potassium", "Magnesium"], 80 | "price": 0.57 81 | }, 82 | { 83 | "id": "11", 84 | "name": "Honeydew", 85 | "description": "a cultivar group of the muskmelon, Cucumis melo Inodorus group, which includes crenshaw, casaba, Persian, winter, and other mixed melons.", 86 | "image": "honeydew.png", 87 | "nutrition": ["Vitamin C", "Potassium", "Sodium"], 88 | "price": 2.40 89 | }, 90 | { 91 | "id": "12", 92 | "name": "Kiwi", 93 | "description": "Kiwifruit or Chinese gooseberry is the name given to the edible berries of several species of woody vines in the genus Actinidia.", 94 | "image": "kiwi.png", 95 | "nutrition": ["Vitamin C", "Potassium", "Fiber"], 96 | "price": 3.53 97 | }, 98 | { 99 | "id": "13", 100 | "name": "Mango", 101 | "description": "The mango is a juicy stone fruit belonging to the genus Mangifera, consisting of numerous tropical fruiting trees, cultivated mostly for edible fruit.", 102 | "image": "mango.png", 103 | "nutrition": ["Vitamin C", "Vitamin A", "Fiber", "Vitamin B-6"], 104 | "price": 10.00 105 | }, 106 | { 107 | "id": "14", 108 | "name": "Mushrooms", 109 | "description": "A mushroom is the fleshy, spore-bearing fruiting body of a fungus, typically produced above ground on soil or on its food source.", 110 | "image": "mushroom.png", 111 | "nutrition": [], 112 | "price": 5.58 113 | }, 114 | { 115 | "id": "15", 116 | "name": "Nectarines", 117 | "description": "A deciduous tree native to the region of Northwest China between the Tarim Basin and the north slopes of the Kunlun Shan mountains, where...", 118 | "image": "nectarine.png", 119 | "nutrition": ["Vitamin C", "Vitamin A", "Fiber", "Potassium"], 120 | "price": 1.83 121 | }, 122 | { 123 | "id": "16", 124 | "name": "Oranges", 125 | "description": "The orange is the fruit of the citrus species Citrus × sinensis in the family Rutaceae. The fruit of the Citrus × sinensis is considered a sweet...", 126 | "image": "orange.png", 127 | "nutrition": ["Vitamin C", "Calcium", "Potassium"], 128 | "price": 0.50 129 | }, 130 | { 131 | "id": "17", 132 | "name": "Pears", 133 | "description": "Pears are broadly classified based up on their place of origin as Asian-pears and European-pears. Asian varieties feature crispy texture and firm...", 134 | "image": "pear.png", 135 | "nutrition": ["Vitamin C", "Fiber", "Potassium"], 136 | "price": 1.30 137 | }, 138 | { 139 | "id": "18", 140 | "name": "Pineapple", 141 | "description": "A tropical plant with edible multiple fruit consisting of coalesced berries, also called pineapples, and the most economically significant plant...", 142 | "image": "pineapple.png", 143 | "nutrition": ["Vitamin C", "Fiber", "Potassium", "Calcium", "Vitamin A", "Iron", "Vitamin B-6", "Magnesium"], 144 | "price": 0.20 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function enableAuthentication(server) { 4 | // enable authentication 5 | server.enableAuth(); 6 | }; 7 | -------------------------------------------------------------------------------- /server/boot/root.js: -------------------------------------------------------------------------------- 1 | const reactRoutes = [ 2 | '/', 3 | '/cart', 4 | '/sign-up', 5 | '/log-in' 6 | ]; 7 | 8 | export default function rootScript(app) { 9 | const router = app.loopback.Router(); 10 | reactRoutes.forEach(route => { 11 | router.get(route, renderHome); 12 | }); 13 | 14 | function renderHome(req, res) { 15 | return res.render('index', { title: 'react-shoppe' }); 16 | } 17 | 18 | app.use(router); 19 | } 20 | -------------------------------------------------------------------------------- /server/boot/seed-data.js: -------------------------------------------------------------------------------- 1 | const products = require('../../seed/products.json'); 2 | 3 | const user = { 4 | id: '58c01fd125fd5d99b0d5640f', 5 | username: 'john doe', 6 | email: 'john@example.com', 7 | password: 'foobar' 8 | }; 9 | 10 | module.exports = function seed(app) { 11 | const Product = app.models.Product; 12 | const User = app.models.User; 13 | // const AccessToken = app.models.AccessToken; 14 | 15 | Product.create(products, (err) => { 16 | if (err) { throw err; } 17 | console.log('products seeded'); 18 | }); 19 | 20 | User.create(user, (err) => { 21 | if (err) { throw err; } 22 | console.log('user created: ', user); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /server/boot/services.js: -------------------------------------------------------------------------------- 1 | import Fetcher from 'fetchr'; 2 | 3 | export default function services(app) { 4 | const router = app.loopback.Router(); 5 | const Product = app.models.Product; 6 | router.use('/services', Fetcher.middleware()); 7 | const productServices = { 8 | name: 'products', 9 | read: function(req, resource, params, config, callback) { 10 | Product.find({}) 11 | .then(products => products.map(product => product.toJSON())) 12 | .then(products => { 13 | callback(null, products); 14 | }) 15 | .catch(err => callback(err)); 16 | } 17 | }; 18 | Fetcher.registerService(productServices); 19 | 20 | app.use(router); 21 | } 22 | -------------------------------------------------------------------------------- /server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "logoutSessionsOnSensitiveChanges": true, 6 | "remoting": { 7 | "context": false, 8 | "rest": { 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false, 21 | "handleErrors": false 22 | }, 23 | "legacyExplorer": false 24 | } 25 | -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "morgan": { 8 | "params": ":status :method :response-time ms - :url" 9 | }, 10 | "cors": { 11 | "params": { 12 | "origin": true, 13 | "credentials": true, 14 | "maxAge": 86400 15 | } 16 | }, 17 | "helmet#xssFilter": {}, 18 | "helmet#frameguard": { 19 | "params": [ 20 | "deny" 21 | ] 22 | }, 23 | "helmet#hsts": { 24 | "params": { 25 | "maxAge": 0, 26 | "includeSubdomains": true 27 | } 28 | }, 29 | "helmet#hidePoweredBy": {}, 30 | "helmet#ieNoOpen": {}, 31 | "helmet#noSniff": {}, 32 | "helmet#noCache": { 33 | "enabled": false 34 | } 35 | }, 36 | "session": {}, 37 | "auth": {}, 38 | "parse": { 39 | "body-parser#json":{} 40 | }, 41 | "routes": { 42 | "loopback#rest": { 43 | "paths": [ 44 | "${restApiRoot}" 45 | ] 46 | } 47 | }, 48 | "files": { 49 | "loopback#static": { 50 | "params": [ 51 | "$!../public", 52 | { 53 | "maxAge": "86400000" 54 | } 55 | ] 56 | } 57 | }, 58 | "final": { 59 | "loopback#urlNotFound": {} 60 | }, 61 | "final:after": { 62 | "strong-error-handler": {} 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db", 18 | "public": false 19 | }, 20 | "AccessToken": { 21 | "dataSource": "db", 22 | "public": false 23 | }, 24 | "ACL": { 25 | "dataSource": "db", 26 | "public": false 27 | }, 28 | "RoleMapping": { 29 | "dataSource": "db", 30 | "public": false 31 | }, 32 | "Role": { 33 | "dataSource": "db", 34 | "public": false 35 | }, 36 | "product": { 37 | "dataSource": "db", 38 | "public": true 39 | }, 40 | "user": { 41 | "dataSource": "db", 42 | "public": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/models/product.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /server/models/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "price": { 14 | "type": "number", 15 | "required": true, 16 | "default": 0 17 | }, 18 | "description": { 19 | "type": "string", 20 | "required": true 21 | }, 22 | "image": { 23 | "type": "string", 24 | "required": true 25 | }, 26 | "nutrition": { 27 | "type": [ 28 | "string" 29 | ], 30 | "default": [ 31 | "[]" 32 | ] 33 | } 34 | }, 35 | "validations": [], 36 | "relations": {}, 37 | "acls": [ 38 | { 39 | "accessType": "*", 40 | "principalType": "ROLE", 41 | "principalId": "$everyone", 42 | "permission": "DENY" 43 | }, 44 | { 45 | "accessType": "READ", 46 | "principalType": "ROLE", 47 | "principalId": "$everyone", 48 | "permission": "ALLOW" 49 | } 50 | ], 51 | "methods": {} 52 | } 53 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | module.exports = function(User) { 4 | User.prototype.updateTo = function update$(updateData) { 5 | const id = this.getId(); 6 | const updateOptions = { allowExtendedOperators: true }; 7 | if ( 8 | !updateData || 9 | typeof updateData !== 'object' || 10 | !Object.keys(updateData).length 11 | ) { 12 | return Promise.reject(new Error( 13 | ` 14 | updateData must be an object with at least one key, 15 | but got ${updateData} with ${Object.keys(updateData).length} 16 | `.split('\n').join(' ') 17 | )); 18 | } 19 | console.log('updating user: ', id); 20 | console.log('update data: ', updateData); 21 | return this.constructor.updateAll({ id }, updateData, updateOptions); 22 | }; 23 | 24 | User.prototype.fav = function fav(itemId) { 25 | const user = this; 26 | const favs = this.favs; 27 | if (_.includes(favs, itemId)) { 28 | // item already in favs 29 | // remove it 30 | const newFavs = [ ...favs ]; 31 | const index = _.sortedIndexOf(favs, itemId); 32 | newFavs.splice(index, 1); 33 | return user.updateTo({ favs: newFavs }).then(() => newFavs); 34 | } 35 | 36 | const { Product } = User.app.models; 37 | // validate item is actual product 38 | return Product.find({ fields: [ 'id' ]}) 39 | .then(products => { 40 | if (!products.length) { 41 | throw new Error('no products found.'); 42 | } 43 | if (_.find(products, ({ id }) => id === itemId) === -1) { 44 | throw new Error(`no product found with id ${itemId}.`); 45 | } 46 | return products.map(({ id }) => id).sort(); 47 | }) 48 | .then(() => { 49 | const newFavs = [ ...favs ]; 50 | const index = _.sortedIndex(favs, itemId); 51 | newFavs.splice(index, 0, itemId); 52 | const updateData = { favs: newFavs }; 53 | return user.updateTo(updateData).then(() => newFavs); 54 | }); 55 | }; 56 | 57 | User.prototype.addToCart = function addToCart(itemId) { 58 | const user = this; 59 | const { Product } = User.app.models; 60 | // validate item is actual product 61 | return Product.findById(itemId) 62 | .then(product => { 63 | if (!product) { 64 | throw new Error(`no product found with id ${itemId}.`); 65 | } 66 | return product; 67 | }) 68 | .then(product => { 69 | console.log('item: ', product.getId()); 70 | const newCart = [ ...user.cart ]; 71 | const index = _.findIndex(newCart, item => item.id === product.id); 72 | if (index !== -1) { 73 | const oldItem = newCart[index]; 74 | newCart[index] = { ...oldItem, count: oldItem.count + 1 }; 75 | } else { 76 | newCart.push({ 77 | id: product.getId(), 78 | count: 1 79 | }); 80 | } 81 | const updateData = { cart: newCart }; 82 | return user.updateTo(updateData).then(() => newCart); 83 | }); 84 | }; 85 | 86 | User.prototype.removeFromCart = function removeFromCart(itemId) { 87 | const user = this; 88 | const { Product } = User.app.models; 89 | // validate item is actual product 90 | return Product.findById(itemId) 91 | .then(product => { 92 | if (!product) { 93 | throw new Error(`no product found with id ${itemId}.`); 94 | } 95 | return product; 96 | }) 97 | .then(product => { 98 | console.log('item: ', product.getId()); 99 | const newCart = [ ...user.cart ]; 100 | const index = _.findIndex(newCart, item => item.id === product.id); 101 | if (index !== -1) { 102 | const oldItem = newCart[index]; 103 | newCart[index] = { 104 | ...oldItem, 105 | count: Math.max(oldItem.count - 1, 0) 106 | }; 107 | const updateData = { cart: newCart}; 108 | return user.updateTo(updateData).then(() => newCart); 109 | } 110 | // item is not in cart so we do not save any data 111 | return newCart; 112 | }); 113 | }; 114 | 115 | User.prototype.deleteFromCart = function deleteFromCart(itemId) { 116 | const user = this; 117 | const { Product } = User.app.models; 118 | // validate item is actual product 119 | return Product.findById(itemId) 120 | .then(product => { 121 | if (!product) { 122 | throw new Error(`no product found with id ${itemId}.`); 123 | } 124 | return product; 125 | }) 126 | .then(product => { 127 | console.log('item: ', product.getId()); 128 | const newCart = [ ...user.cart ]; 129 | const index = _.findIndex(newCart, item => item.id === product.id); 130 | if (index !== -1) { 131 | newCart.splice(index, 1); 132 | const updateData = { cart: newCart}; 133 | return user.updateTo(updateData).then(() => newCart); 134 | } 135 | // we do not need to update user list here 136 | // since item was not in cart 137 | return newCart; 138 | }); 139 | }; 140 | 141 | const getItemId = ({ req }) => req.body.itemId; 142 | 143 | User.remoteMethod( 144 | 'fav', 145 | { 146 | isStatic: false, 147 | description: 'toggle item in favs array', 148 | accepts: [ 149 | { 150 | arg: 'itemId', 151 | type: 'string', 152 | required: true, 153 | http: getItemId 154 | } 155 | ], 156 | returns: [ 157 | { 158 | arg: 'favs', 159 | type: 'object' 160 | } 161 | ], 162 | http: { 163 | path: '/fav', 164 | verb: 'POST' 165 | } 166 | } 167 | ); 168 | 169 | User.remoteMethod( 170 | 'addToCart', 171 | { 172 | isStatic: false, 173 | description: 'updates the users cart with item', 174 | accepts: [ 175 | { 176 | arg: 'itemId', 177 | type: 'string', 178 | required: true, 179 | http: getItemId 180 | } 181 | ], 182 | returns: [ 183 | { 184 | arg: 'cart', 185 | type: 'object' 186 | } 187 | ], 188 | http: { 189 | path: '/add-to-cart', 190 | verb: 'POST' 191 | } 192 | } 193 | ); 194 | 195 | User.remoteMethod( 196 | 'removeFromCart', 197 | { 198 | isStatic: false, 199 | description: 'removes the item from users cart', 200 | accepts: [ 201 | { 202 | arg: 'itemId', 203 | type: 'string', 204 | required: true, 205 | http: getItemId 206 | } 207 | ], 208 | returns: [ 209 | { 210 | arg: 'cart', 211 | type: 'object' 212 | } 213 | ], 214 | http: { 215 | path: '/remove-from-cart', 216 | verb: 'POST' 217 | } 218 | } 219 | ); 220 | 221 | User.remoteMethod( 222 | 'deleteFromCart', 223 | { 224 | isStatic: false, 225 | description: 'deletes the item from users cart', 226 | accepts: [ 227 | { 228 | arg: 'itemId', 229 | type: 'string', 230 | required: true, 231 | http: getItemId 232 | } 233 | ], 234 | returns: [ 235 | { 236 | arg: 'cart', 237 | type: 'object' 238 | } 239 | ], 240 | http: { 241 | path: '/delete-from-cart', 242 | verb: 'POST' 243 | } 244 | } 245 | ); 246 | 247 | User.afterRemote('create', function afterRemoteCreate(ctx, user, next) { 248 | return user.createAccessToken() 249 | .then(token => { 250 | user.accessToken = token.id; 251 | return next(); 252 | }) 253 | .catch(next); 254 | }); 255 | }; 256 | -------------------------------------------------------------------------------- /server/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "base": "User", 4 | "strict": true, 5 | "idInjection": true, 6 | "injectOptionsFromRemoteContext": true, 7 | "options": { 8 | "validateUpsert": true 9 | }, 10 | "properties": { 11 | "username": { 12 | "type": "string", 13 | "required": true 14 | }, 15 | "cart": { 16 | "type": [ 17 | "object" 18 | ], 19 | "required": false, 20 | "default": [] 21 | }, 22 | "favs": { 23 | "type": [ 24 | "string" 25 | ], 26 | "required": false, 27 | "default": [] 28 | } 29 | }, 30 | "validations": [], 31 | "relations": {}, 32 | "acls": [ 33 | { 34 | "accessType": "EXECUTE", 35 | "principalType": "ROLE", 36 | "principalId": "$owner", 37 | "permission": "ALLOW", 38 | "property": "fav" 39 | }, 40 | { 41 | "accessType": "EXECUTE", 42 | "principalType": "ROLE", 43 | "principalId": "$owner", 44 | "permission": "ALLOW", 45 | "property": "addToCart" 46 | }, 47 | { 48 | "accessType": "EXECUTE", 49 | "principalType": "ROLE", 50 | "principalId": "$owner", 51 | "permission": "ALLOW", 52 | "property": "removeFromCart" 53 | }, 54 | { 55 | "accessType": "EXECUTE", 56 | "principalType": "ROLE", 57 | "principalId": "$owner", 58 | "permission": "ALLOW", 59 | "property": "deleteFromCart" 60 | } 61 | ], 62 | "methods": {} 63 | } 64 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('dotenv').load(); 3 | require('babel-register'); 4 | 5 | var loopback = require('loopback'); 6 | var boot = require('loopback-boot'); 7 | var path = require('path'); 8 | var expressState = require('express-state'); 9 | 10 | var app = loopback(); 11 | 12 | expressState.extend(app); 13 | app.set('state namespace', '__ar__'); 14 | app.set('port', process.env.PORT || 3000); 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'pug'); 17 | app.use(loopback.token()); 18 | app.disable('x-powered-by'); 19 | 20 | app.start = function() { 21 | // start the web server 22 | return app.listen(function() { 23 | app.emit('started'); 24 | var baseUrl = app.get('url').replace(/\/$/, ''); 25 | console.log('Web server listening at: %s', baseUrl); 26 | if (app.get('loopback-component-explorer')) { 27 | var explorerPath = app.get('loopback-component-explorer').mountPath; 28 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 29 | } 30 | }); 31 | }; 32 | 33 | // Bootstrap the application, configure models, datasources and middleware. 34 | // Sub-apps like REST API are mounted via boot scripts. 35 | boot( 36 | app, 37 | { 38 | appRootDir: __dirname, 39 | dev: process.env.NODE_ENV 40 | }, 41 | function(err) { if (err) { throw err; } } 42 | ); 43 | 44 | module.exports = app; 45 | 46 | // start the server if `$ node server/server.js` 47 | if (require.main === module) { 48 | app.start(); 49 | } 50 | -------------------------------------------------------------------------------- /server/views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet' type='text/css' href='/css/index.css') 6 | body 7 | div#app!= markup 8 | script!= state 9 | script(src='/js/bundle.js') 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var __DEV__ = process.env.NODE_ENV !== 'production'; 5 | 6 | module.exports = { 7 | devtool: __DEV__ ? 'inline-source-map' : null, 8 | 9 | entry: { 10 | bundle: './client' 11 | }, 12 | 13 | output: { 14 | filename: 'bundle.js', 15 | path: path.join(__dirname, 'public', 'js'), 16 | publicPath: '/js/' 17 | }, 18 | 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': { 22 | NODE_ENV: JSON.stringify(__DEV__ ? 'development' : 'production') 23 | }, 24 | __DEVTOOLS__: !__DEV__ 25 | }), 26 | // Use browser version of visionmedia-debug 27 | new webpack.NormalModuleReplacementPlugin( 28 | /debug\/node/, 29 | 'debug/browser' 30 | ), 31 | new webpack.optimize.OccurenceOrderPlugin(true), 32 | new webpack.HotModuleReplacementPlugin(), 33 | new webpack.NoErrorsPlugin() 34 | ], 35 | 36 | module: { 37 | loaders: [ 38 | { 39 | test: /\.jsx?$/, 40 | include: [ 41 | path.join(__dirname, 'client/'), 42 | path.join(__dirname, 'common/') 43 | ], 44 | loaders: __DEV__ ? ['react-hot', 'babel'] : [ 'babel' ] 45 | }, 46 | { 47 | test: /\.json$/, 48 | loaders: [ 49 | 'json-loader' 50 | ] 51 | } 52 | ] 53 | } 54 | }; 55 | --------------------------------------------------------------------------------