├── README.md ├── client ├── .gitignore ├── app │ ├── .htaccess │ ├── api │ │ ├── cart-api.js │ │ ├── catalog-api.js │ │ └── index.js │ ├── app.js │ ├── cart-page │ │ ├── action-types.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── route.js │ │ ├── selectors.js │ │ ├── template.js │ │ ├── tests.js │ │ └── use-cases.js │ ├── catalog-page │ │ ├── index.js │ │ ├── route.js │ │ └── template.js │ ├── favicon.ico │ ├── header │ │ ├── index.js │ │ ├── styles.css │ │ └── template.js │ ├── home-page │ │ ├── index.js │ │ ├── route.js │ │ └── template.js │ ├── index.html │ ├── index.js │ ├── manifest.json │ ├── nav-item │ │ ├── container.js │ │ ├── index.js │ │ ├── selectors.js │ │ ├── styles.css │ │ ├── template.js │ │ ├── tests.js │ │ └── use-cases.js │ ├── nav │ │ ├── container.js │ │ ├── index.js │ │ └── template.js │ ├── not-found │ │ ├── container.js │ │ ├── index.js │ │ ├── routes.js │ │ └── template.js │ ├── product-list-item │ │ ├── container.js │ │ ├── index.js │ │ ├── styles.css │ │ └── template.js │ ├── product-list │ │ ├── action-types.js │ │ ├── actions.js │ │ ├── container.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ ├── state.js │ │ ├── template.js │ │ ├── tests.js │ │ └── use-cases.js │ ├── product-page │ │ ├── action-types.js │ │ ├── actions.js │ │ ├── container.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── route.js │ │ ├── selectors.js │ │ ├── state.js │ │ ├── styles.css │ │ └── template.js │ ├── reducers.js │ ├── routes.js │ ├── selectors.js │ ├── selectors.test.js │ ├── spinner │ │ ├── index.js │ │ ├── styles.css │ │ └── template.js │ ├── store.js │ ├── styles.css │ ├── tests │ │ └── store.test.js │ └── utils │ │ ├── asyncInjectors.js │ │ ├── routeHelpers.js │ │ ├── tests │ │ └── asyncInjectors.test.js │ │ └── use-case-helper.js ├── appveyor.yml ├── internals │ ├── config.js │ ├── generators │ │ ├── component │ │ │ ├── es6.js.hbs │ │ │ ├── index.js │ │ │ ├── messages.js.hbs │ │ │ ├── stateless.js.hbs │ │ │ ├── styles.css.hbs │ │ │ └── test.js.hbs │ │ ├── container │ │ │ ├── actions.js.hbs │ │ │ ├── actions.test.js.hbs │ │ │ ├── constants.js.hbs │ │ │ ├── index.js │ │ │ ├── index.js.hbs │ │ │ ├── messages.js.hbs │ │ │ ├── reducer.js.hbs │ │ │ ├── reducer.test.js.hbs │ │ │ ├── sagas.js.hbs │ │ │ ├── sagas.test.js.hbs │ │ │ ├── selectors.js.hbs │ │ │ ├── selectors.test.js.hbs │ │ │ ├── styles.css.hbs │ │ │ └── test.js.hbs │ │ ├── index.js │ │ ├── language │ │ │ ├── add-locale-data.hbs │ │ │ ├── app-locale.hbs │ │ │ ├── format-translation-messages.hbs │ │ │ ├── index.js │ │ │ ├── intl-locale-data.hbs │ │ │ ├── polyfill-intl-locale.hbs │ │ │ ├── translation-messages.hbs │ │ │ └── translations-json.hbs │ │ ├── route │ │ │ ├── index.js │ │ │ ├── route.hbs │ │ │ └── routeWithReducer.hbs │ │ └── utils │ │ │ └── componentExists.js │ ├── scripts │ │ ├── analyze.js │ │ ├── clean.js │ │ ├── dependencies.js │ │ ├── extract-intl.js │ │ ├── helpers │ │ │ ├── checkmark.js │ │ │ └── progress.js │ │ ├── npmcheckversion.js │ │ └── pagespeed.js │ ├── testing │ │ ├── karma.conf.js │ │ └── test-bundler.js │ └── webpack │ │ ├── webpack.base.babel.js │ │ ├── webpack.dev.babel.js │ │ ├── webpack.dll.babel.js │ │ ├── webpack.prod.babel.js │ │ └── webpack.test.babel.js ├── package.json └── server │ ├── index.js │ ├── logger.js │ └── middlewares │ └── frontendMiddleware.js └── server ├── CleanCo.Common ├── .gitignore ├── IRecord.cs ├── IRepository.cs └── project.json ├── CleanCo.Ecomm.Catalog ├── .gitignore ├── Catalog.cs └── project.json ├── CleanCo.Ecomm.RepoAdapter ├── .gitignore ├── Repository.cs └── project.json ├── CleanCo.Ecomm.RestAdapter ├── .gitignore ├── Controllers │ └── CatalogController.cs ├── Dockerfile ├── Program.cs ├── Properties │ └── launchSettings.json ├── README.md ├── Startup.cs ├── appsettings.json ├── project.json └── web.config ├── CleanCo.JsonAdapter ├── JsonHelper.cs ├── bin │ └── Debug │ │ └── netstandard1.6 │ │ ├── Clean.Adapters.Json.deps.json │ │ ├── Clean.Adapters.Json.dll │ │ ├── Clean.Adapters.Json.pdb │ │ ├── CleanCo.JsonAdapter.dll │ │ └── CleanCo.JsonAdapter.pdb ├── obj │ └── Debug │ │ └── netstandard1.6 │ │ ├── .IncrementalCache │ │ ├── .SDKVersion │ │ ├── dotnet-compile-csc.rsp │ │ ├── dotnet-compile.assemblyinfo.cs │ │ └── dotnet-compile.rsp ├── project.json └── project.lock.json ├── CleanCo.Products ├── .gitignore ├── Product.cs └── project.json ├── CleanCo.Tests ├── .gitignore ├── AdapterTests │ └── RepoTests.cs ├── EntityTests │ └── ProductTest.cs ├── TestDoubles │ ├── DummyRecord.cs │ └── FakeRepository.cs ├── UseCaseTests │ └── CatalogTest.cs ├── project.json └── xunit.runner.json └── global.json /README.md: -------------------------------------------------------------------------------- 1 | #Web App Architecture Example 2 | This project is a partial implementation of a an e-commerce storefront for a fictional company called "CleanCo". It's purpose is to demonstrate key concepts of a highly stable, layered, web application architecture. 3 | Many of these concepts are explained by Robert C. Martin (aka "Uncle Bob") in The Clean Architecture* 4 | 5 | *This project is in no way endorsed by, or affiliated with, Robert C. Martin 6 | 7 | ##Prerequisites 8 | * Watch the webinar - https://youtu.be/D4d-cehphVY 9 | * git - https://git-scm.com 10 | * .Net Core - https://www.microsoft.com/net/core 11 | * Node JS + NPM - https://nodejs.org/ 12 | 13 | ##Setup 14 | From the command line: 15 | 16 | git clone https://github.com/dev-mastery/clean-architecture.git 17 | cd clean-architecture 18 | cd server 19 | dotnet restore 20 | cd ../client 21 | npm install 22 | 23 | ##Run 24 | After setup, from the command line, at `/clean-architecture`: 25 | 26 | cd server 27 | cd CleanCo.Ecomm.RestAdapter 28 | dotnet run 29 | 30 | Open a new terminal window and go to `/clean-architecture`: 31 | 32 | cd client 33 | npm run start 34 | 35 | **IMPORTANT:** Make sure the server code is running at `localhost:5000` 36 | 37 | ##License 38 | 39 | Copyright (c) 2016 Dev Mastery 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /client/app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/app/api/cart-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are the "repo adapters" related to the shopping cart. 3 | * For this demo app, localStorage is used in place of a RESTful endpoint. 4 | * 5 | * The point of this module is to abstract away any details about 6 | * the underlying datasource. This api will be passed to the appropriate 7 | * use case and can be leveraged by that use case without any explicit 8 | * knowledge or reference to localStorage on the part of the use case. 9 | **/ 10 | 11 | const cart = window.localStorage; 12 | 13 | // localStorage wants a key value pair so we store 14 | // product id and quantity. 15 | function addToCart(id) { 16 | if (cart[id]) { 17 | cart[id] = (Number(cart.getItem(id)) + 1); 18 | } else { 19 | cart[id] = 1; 20 | } 21 | } 22 | 23 | // I hate "for" loops, but the localStorage API is quite limited and 24 | // time was short. 25 | function getCart() { 26 | const items = []; 27 | for (let i = 0; i < cart.length; i++) { 28 | if (cart.key(i) !== 'debug') { 29 | items.push({ 30 | id: cart.key(i), 31 | qty: Number(cart[cart.key(i)]), 32 | }); 33 | } 34 | } 35 | return items; 36 | } 37 | 38 | export { addToCart, getCart }; 39 | -------------------------------------------------------------------------------- /client/app/api/catalog-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are the "repo adapters" related to the catalog. 3 | * 4 | * The point of this module is to abstract away any details about 5 | * the underlying datasource. This api will be passed to the appropriate 6 | * use case and can be leveraged by that use case without any explicit 7 | * knowledge or reference to the web service endpoint on the part of 8 | * the use case. 9 | * 10 | * Passing the uri as an optional argument makes this testatable independent of 11 | * the real backend. 12 | **/ 13 | 14 | // TODO: Move URI to an environment variable. 15 | const CATALOG_URI = 'http://localhost:5000/api/catalog'; 16 | 17 | function getProducts(uri = CATALOG_URI) { 18 | return fetch(uri) 19 | .then((response) => response.json() 20 | .then((data) => data)); 21 | } 22 | 23 | function getProductById(id, uri = CATALOG_URI) { 24 | return fetch(`${uri}/${id}`) 25 | .then((response) => response.json() 26 | .then((data) => data)); 27 | } 28 | 29 | export { getProducts, getProductById }; 30 | -------------------------------------------------------------------------------- /client/app/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit. 8 | **/ 9 | 10 | import { addToCart, getCart } from 'api/cart-api'; 11 | import { getProducts, getProductById } from 'api/catalog-api'; 12 | 13 | export { addToCart, getCart, getProducts, getProductById }; 14 | -------------------------------------------------------------------------------- /client/app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | /** 8 | * app.js 9 | * 10 | * This is the entry file for the application, only setup and boilerplate 11 | * code. 12 | */ 13 | import 'babel-polyfill'; 14 | 15 | /* eslint-disable import/no-unresolved */ 16 | // Load the manifest.json file and the .htaccess file 17 | import '!file?name=[name].[ext]!./manifest.json'; 18 | import 'file?name=[name].[ext]!./.htaccess'; 19 | /* eslint-enable import/no-unresolved */ 20 | 21 | // Import all the third party stuff 22 | import React from 'react'; 23 | import ReactDOM from 'react-dom'; 24 | import { Provider } from 'react-redux'; 25 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 26 | import { syncHistoryWithStore } from 'react-router-redux'; 27 | import useScroll from 'react-router-scroll'; 28 | import configureStore from './store'; 29 | 30 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 31 | import 'sanitize.css/sanitize.css'; 32 | import 'bootstrap-css-only/css/bootstrap.min.css'; 33 | 34 | // Create redux store with history 35 | // this uses the singleton browserHistory provided by react-router 36 | // Optionally, this could be changed to leverage a created history 37 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 38 | const initialState = {}; 39 | const store = configureStore(initialState, browserHistory); 40 | 41 | // Sync history and store, as the react-router-redux reducer 42 | // is under the non-default key ("routing"), selectLocationState 43 | // must be provided for resolving how to retrieve the "route" in the state 44 | import { selectLocationState } from 'selectors'; 45 | const history = syncHistoryWithStore(browserHistory, store, { 46 | selectLocationState: selectLocationState(), 47 | }); 48 | 49 | // Set up the router, wrapping all Routes in the App component 50 | import Global from '/'; 51 | import createRoutes from './routes'; 52 | const rootRoute = { 53 | component: Global, 54 | childRoutes: createRoutes(store), 55 | }; 56 | 57 | 58 | const render = () => { 59 | ReactDOM.render( 60 | 61 | 70 | , 71 | document.getElementById('app') 72 | ); 73 | }; 74 | 75 | render(); 76 | 77 | // Install ServiceWorker and AppCache in the end since 78 | // it's not most important operation and if main code fails, 79 | // we do not want it installed 80 | import { install } from 'offline-plugin/runtime'; 81 | install(); 82 | -------------------------------------------------------------------------------- /client/app/cart-page/action-types.js: -------------------------------------------------------------------------------- 1 | // makes action names easy to reference and keep unique. 2 | export const ADD_TO_CART = 'cart-page/ADD_TO_CART'; 3 | -------------------------------------------------------------------------------- /client/app/cart-page/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React-redux uses a command pattern (http://www.blackwasp.co.uk/Command.aspx) 3 | * We issue commands via objects that describe them. 4 | **/ 5 | 6 | import { ADD_TO_CART } from 'cart-page/action-types'; 7 | 8 | // all commands must have a "type". 9 | function addToCart(productId) { 10 | return { 11 | type: ADD_TO_CART, 12 | payload: productId, 13 | }; 14 | } 15 | 16 | export { addToCart }; 17 | -------------------------------------------------------------------------------- /client/app/cart-page/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out, because they have to be more explicit. 8 | **/ 9 | 10 | import CcCartTemplate from 'cart-page/template'; 11 | import createCartRoute from 'cart-page/route'; 12 | import { addToCart } from 'cart-page/actions'; 13 | import { selectTotalItemsInCart } from 'cart-page/selectors'; 14 | 15 | export default CcCartTemplate; 16 | export { createCartRoute, addToCart, selectTotalItemsInCart }; 17 | -------------------------------------------------------------------------------- /client/app/cart-page/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In Redux, the reducer is where we manage state for anything global. 3 | * Here, we manage the state of the cart. 4 | * Notice that all the business logic is actually coming from 5 | * our use case module. 6 | **/ 7 | 8 | import { fromJS } from 'immutable'; 9 | import { ADD_TO_CART } from 'cart-page/action-types'; 10 | import { addProductToCart, getCartProducts } from 'cart-page/use-cases'; 11 | import * as api from 'api'; 12 | 13 | const initialState = immutableCart(); 14 | 15 | function cartReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case ADD_TO_CART: 18 | addProductToCart(api, action.payload); 19 | return immutableCart(); 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | function immutableCart() { 26 | return fromJS(getCartProducts(api)); 27 | } 28 | 29 | export default cartReducer; 30 | -------------------------------------------------------------------------------- /client/app/cart-page/route.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The route belongs to the feature so we keep it with the feature code. 3 | * The drawback to this approach is that developers have to be cognicant 4 | * of name collisions. This app is too small to worry about. 5 | * 6 | * On the plus side, this code allows the client to only load 7 | * the bits he needs. 8 | */ 9 | 10 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 11 | import { errorLoading, loadModule } from 'utils/routeHelpers'; 12 | 13 | export default function createCartRoute(store) { 14 | // Create reusable async injectors using getAsyncInjectors factory 15 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 16 | return { 17 | path: '/cart', 18 | name: 'cart', 19 | getComponent(nextState, cb) { 20 | const importModules = Promise.all([ 21 | System.import('cart-page'), 22 | ]); 23 | 24 | const renderRoute = loadModule(cb); 25 | 26 | importModules.then(([component]) => { 27 | renderRoute(component); 28 | }); 29 | 30 | importModules.catch(errorLoading); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/app/cart-page/selectors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * selectors let us take advantage of simple caching. 3 | * More info: https://github.com/reactjs/reselect 4 | */ 5 | 6 | import { createSelector } from 'reselect'; 7 | import { totalItemsInCart } from 'cart-page/use-cases'; 8 | 9 | function selectTotalItemsInCart() { 10 | return createSelector( 11 | selectCartDomain, 12 | (cart) => { 13 | if (cart) { 14 | return totalItemsInCart(cart); 15 | } 16 | return 0; 17 | } 18 | ); 19 | } 20 | 21 | function selectCartDomain(state) { 22 | return state.get('cart').toJS(); 23 | } 24 | 25 | export { selectTotalItemsInCart }; 26 | -------------------------------------------------------------------------------- /client/app/cart-page/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | 7 | function CcCartTemplate() { 8 | return ( 9 |
cart
10 | ); 11 | } 12 | 13 | export default CcCartTemplate; 14 | -------------------------------------------------------------------------------- /client/app/cart-page/tests.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { totalItemsInCart } from 'cart-page/use-cases'; 3 | 4 | describe('cart-page', () => 5 | it('should return the total items in the cart', () => { 6 | const dummyItems = Array(5).fill({ qty: 2 }); 7 | expect(totalItemsInCart(dummyItems)).toBe(10); 8 | }) 9 | ); 10 | -------------------------------------------------------------------------------- /client/app/cart-page/use-cases.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The use case encapsulates the business logic and should be 3 | * free of dependencies. Along with the Entities, this is the 4 | * heart and sould of our app. 5 | **/ 6 | 7 | import { validateApi } from 'utils/use-case-helper'; 8 | 9 | // one could argue that this belongs with product list 10 | function addProductToCart(api, id) { 11 | validateApi(api, 'addToCart'); 12 | api.addToCart(id); 13 | } 14 | 15 | function getCartProducts(api) { 16 | validateApi(api, 'getCart'); 17 | return api.getCart(); 18 | } 19 | 20 | function totalItemsInCart(items) { 21 | // const below avoids eslint warning: 22 | // http://eslint.org/docs/rules/no-extra-boolean-cast 23 | const itemsValid = !!items; 24 | if (itemsValid) { 25 | return items.reduce( 26 | (prev, curr) => prev + curr.qty, 27 | 0 28 | ); 29 | } 30 | return 0; 31 | } 32 | 33 | export { addProductToCart, getCartProducts, totalItemsInCart }; 34 | -------------------------------------------------------------------------------- /client/app/catalog-page/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out, because they have to be more explicit. 8 | **/ 9 | 10 | import CcCatalog from 'catalog-page/template'; 11 | import createCatalogRoute from 'catalog-page/route'; 12 | 13 | export default CcCatalog; 14 | export { createCatalogRoute }; 15 | -------------------------------------------------------------------------------- /client/app/catalog-page/route.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The route belongs to the feature so we keep it with the feature code. 3 | * The drawback to this approach is that developers have to be cognicant 4 | * of name collisions. This app is too small to worry about. 5 | * 6 | * On the plus side, this code allows the client to only load 7 | * the bits he needs. 8 | */ 9 | 10 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 11 | import { errorLoading, loadModule } from 'utils/routeHelpers'; 12 | 13 | export default function createCatalogRoute(store) { 14 | // Create reusable async injectors using getAsyncInjectors factory 15 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 16 | return { 17 | path: '/catalog', 18 | name: 'catalog', 19 | getComponent(nextState, cb) { 20 | const importModules = Promise.all([ 21 | // System.import('product-list/reducer.js'), 22 | System.import('catalog-page'), 23 | ]); 24 | 25 | const renderRoute = loadModule(cb); 26 | 27 | importModules.then(([component]) => { 28 | renderRoute(component); 29 | }); 30 | 31 | importModules.catch(errorLoading); 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /client/app/catalog-page/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | import CcProductList from 'product-list'; 7 | 8 | function CcCatalogTemplate() { 9 | return ( 10 |
11 |

12 | There would be probably be some kind of search/filter/sort thing here. 13 |

14 | 15 |
16 | ); 17 | } 18 | 19 | export default CcCatalogTemplate; 20 | -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-mastery/clean-architecture/8da65aafa1c41272e7d8fb46b77558dbc9bf5d00/client/app/favicon.ico -------------------------------------------------------------------------------- /client/app/header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out, because they have to be more explicit. 8 | **/ 9 | 10 | import CcHeaderTemplate from 'header/template'; 11 | export default CcHeaderTemplate; 12 | -------------------------------------------------------------------------------- /client/app/header/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding-top: 5px; 3 | padding-bottom: 5px; 4 | margin-bottom: 10px; 5 | border-bottom: 1px solid #D9E7F4; 6 | min-height: 45px; 7 | } 8 | 9 | .brand { 10 | font-weight: bold; 11 | font-size: 25px; 12 | padding-left: 0; 13 | color: #214565; 14 | } 15 | 16 | @media only screen and (max-width: 767px) { 17 | .header { 18 | min-height: 80px; 19 | } 20 | 21 | .brand { 22 | font-weight: bold; 23 | font-size: 20px; 24 | width: 100%; 25 | border-bottom: solid 1px #D9E7F4; 26 | text-align: center; 27 | padding-bottom: 2px; 28 | margin-bottom: 6px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/app/header/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | import React from 'react'; 5 | import CcNav from 'nav'; 6 | import styles from 'header/styles.css'; 7 | 8 | function CcHeaderTemplate() { 9 | return ( 10 |
11 |
12 | CleanCo. 13 |
14 |
25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default CcHeaderTemplate; 32 | -------------------------------------------------------------------------------- /client/app/home-page/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out, because they have to be more explicit. 8 | **/ 9 | 10 | import CcHome from 'home-page/template'; 11 | import createHomeRoute from 'home-page/route'; 12 | 13 | export default CcHome; 14 | export { createHomeRoute }; 15 | -------------------------------------------------------------------------------- /client/app/home-page/route.js: -------------------------------------------------------------------------------- 1 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 2 | import { errorLoading, loadModule } from 'utils/routeHelpers'; 3 | 4 | export default function createHomeRoute(store) { 5 | // Create reusable async injectors using getAsyncInjectors factory 6 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 7 | return { 8 | path: '/', 9 | name: 'home', 10 | getComponent(nextState, cb) { 11 | const importModules = Promise.all([ 12 | System.import('home-page'), 13 | ]); 14 | 15 | const renderRoute = loadModule(cb); 16 | 17 | importModules.then(([component]) => { 18 | renderRoute(component); 19 | }); 20 | 21 | importModules.catch(errorLoading); 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /client/app/home-page/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | import React from 'react'; 5 | 6 | function CcHomeTemplate() { 7 | return ( 8 |
9 |

10 | Web App Architecture Example 11 |

12 |

13 | This site is a partial implementation of a an e-commerce 14 | storefront for a fictional company called "CleanCo". 15 | It's purpose is to demonstrate key concepts of a highly stable, 16 | layered, web application architecture. 17 |

18 |

19 | Many of these concepts are explained by Robert C. Martin (aka "Uncle 20 | Bob") in  21 | 22 | The Clean Architecture* 23 | 24 |

25 | 26 | * 27 | This site is in no way endrosed by, 28 | or affiliated with, Robert C. Martin 29 | 30 |
31 | ); 32 | } 33 | 34 | export default CcHomeTemplate; 35 | -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | CleanCo. 12 | 13 | 14 | 15 |
16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /client/app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | /** 8 | * 9 | * App.react.js 10 | * 11 | * This component is the skeleton around the actual pages, and should only 12 | * contain code that should be seen on all pages. (e.g. navigation bar) 13 | * 14 | * NOTE: while this component should technically be a stateless functional 15 | * component (SFC), hot reloading does not currently support SFCs. If hot 16 | * reloading is not a neccessity for you then you can refactor it and remove 17 | * the linting exception. 18 | */ 19 | 20 | import React from 'react'; 21 | import CcHeader from './header/template'; 22 | 23 | export default class Global extends React.Component { // eslint-disable-line react/prefer-stateless-function 24 | 25 | static propTypes = { 26 | children: React.PropTypes.node, 27 | }; 28 | 29 | render() { 30 | return ( 31 |
32 |
33 |
34 | 35 | {React.Children.toArray(this.props.children)} 36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } 34 | -------------------------------------------------------------------------------- /client/app/nav-item/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A container is used to generate a higher order component that connects 3 | * a template component to the state in the redux store. 4 | * Containers also connect events in the template to redux actions. 5 | * see: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 6 | **/ 7 | 8 | import React from 'react'; 9 | import { connect } from 'react-redux'; 10 | import { selectNavItemProps } from 'nav-item/selectors'; 11 | import NavItemTemplate from 'nav-item/template'; 12 | 13 | const CcNavItem = connect(mapStateToProps, 14 | mapDispatchToProps 15 | )(NavItemTemplate); 16 | 17 | CcNavItem.propTypes = { 18 | label: React.PropTypes.string.isRequired, 19 | route: React.PropTypes.string.isRequired, 20 | }; 21 | 22 | function mapStateToProps(state, ownProps) { 23 | return selectNavItemProps(state, ownProps)(state, ownProps); 24 | } 25 | 26 | function mapDispatchToProps(dispatch) { 27 | return { 28 | dispatch, 29 | }; 30 | } 31 | 32 | export default CcNavItem; 33 | -------------------------------------------------------------------------------- /client/app/nav-item/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit and we can easily find 8 | * and fix them later. 9 | **/ 10 | 11 | import CcNavItem from 'nav-item/container'; 12 | export default CcNavItem; 13 | -------------------------------------------------------------------------------- /client/app/nav-item/selectors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * selectors let us take advantage of simple caching ('memoization'). 3 | * More info: https://github.com/reactjs/reselect 4 | */ 5 | import { createSelector } from 'reselect'; 6 | import { setItemCssClass } from 'nav-item/use-cases'; 7 | 8 | function selectNavItemProps(state, ownProps) { 9 | return createSelector(selectCurrentPath, 10 | (path) => ({ 11 | ...ownProps, 12 | className: setItemCssClass(path, ownProps.route), 13 | })); 14 | } 15 | 16 | function selectCurrentPath(state) { 17 | return state.toJS().route.locationBeforeTransitions.pathname; 18 | } 19 | 20 | 21 | export { selectNavItemProps }; 22 | -------------------------------------------------------------------------------- /client/app/nav-item/styles.css: -------------------------------------------------------------------------------- 1 | .navItem { 2 | padding: 5px 15px !important; 3 | } 4 | -------------------------------------------------------------------------------- /client/app/nav-item/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | import React from 'react'; 5 | import { Link } from 'react-router'; 6 | import styles from 'nav-item/styles.css'; 7 | 8 | CcNavItemTemplate.propTypes = { 9 | className: React.PropTypes.string, 10 | label: React.PropTypes.string.isRequired, 11 | route: React.PropTypes.string.isRequired, 12 | }; 13 | 14 | function CcNavItemTemplate({ 15 | className, 16 | label, 17 | route, 18 | }) { 19 | return ( 20 |
  • 21 | {label} 22 |
  • 23 | ); 24 | } 25 | 26 | export default CcNavItemTemplate; 27 | -------------------------------------------------------------------------------- /client/app/nav-item/tests.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { setItemCssClass } from 'nav-item/use-cases'; 3 | 4 | describe('NavItem', () => { 5 | it('should set the current nav item CSS class to "active"', () => { 6 | const className = setItemCssClass('/same', '/same'); 7 | expect(className).toEqual('active'); 8 | }); 9 | 10 | it('should be an empty string for non-current nav items', () => { 11 | const className = setItemCssClass('/same', '/different'); 12 | expect(className).toEqual(''); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /client/app/nav-item/use-cases.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The use case encapsulates the business logic and should be 3 | * free of dependencies. Along with the Entities, this is the 4 | * heart and sould of our app. 5 | **/ 6 | 7 | function setItemCssClass(currentAppLocation, navItemDestination) { 8 | return currentAppLocation === navItemDestination ? 'active' : ''; 9 | } 10 | 11 | export { setItemCssClass }; 12 | -------------------------------------------------------------------------------- /client/app/nav/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A container is used to generate a higher order component that connects 3 | * a template component to the state in the redux store. 4 | * Containers also connect events in the template to redux actions. 5 | * see: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 6 | **/ 7 | 8 | import { connect } from 'react-redux'; 9 | import CcNavTemplate from 'nav/template'; 10 | import { totalItemsInCart } from 'cart-page/use-cases'; 11 | 12 | const CcNav = connect(mapStateToProps, mapDispatchToProps)(CcNavTemplate); 13 | 14 | function mapStateToProps(state) { 15 | const num = (!state) ? 0 : totalItemsInCart(state.get('cart').toJS()); 16 | return { 17 | itemsInCart: num, 18 | }; 19 | } 20 | 21 | function mapDispatchToProps(dispatch) { 22 | return { dispatch }; 23 | } 24 | 25 | export default CcNav; 26 | -------------------------------------------------------------------------------- /client/app/nav/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out, because they have to be more explicit. 8 | **/ 9 | 10 | import CcNav from 'nav/container'; 11 | export default CcNav; 12 | -------------------------------------------------------------------------------- /client/app/nav/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | import React from 'react'; 5 | import CcNavItem from 'nav-item'; 6 | 7 | CcNavTemplate.propTypes = { 8 | itemsInCart: React.PropTypes.number, 9 | }; 10 | 11 | function CcNavTemplate({ 12 | itemsInCart, 13 | }) { 14 | return ( 15 | 20 | ); 21 | } 22 | 23 | export default CcNavTemplate; 24 | -------------------------------------------------------------------------------- /client/app/not-found/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extending or inheriting from a base class supplied by a third party framework 3 | * is a terrible thing to do. We should strive to avoid this at all costs. 4 | * It tightly binds our code to the framework and will cause great pain in the 5 | * future as the frameworks change and/or need to be replaced. Not clean!! 6 | * 7 | * We are doing it here in order to gain access to React's component lifecycle 8 | * hooks (mount and update). 9 | * 10 | * Notice that the real business logic is still happening in our use cases. 11 | * Keeping the logic seperate protects us from this 12 | * terrible practice and keeps our architecture relatively clean. 13 | **/ 14 | 15 | import { connect } from 'react-redux'; 16 | import NotFoundTemplate from 'not-found/template'; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(NotFoundTemplate); 19 | 20 | function mapStateToProps(state) { 21 | return state.toJS(); 22 | } 23 | 24 | function mapDispatchToProps(dispatch) { 25 | return { dispatch }; 26 | } 27 | -------------------------------------------------------------------------------- /client/app/not-found/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit and we can easily find 8 | * and fix them later. 9 | **/ 10 | 11 | import NotFoundPage from 'not-found/container'; 12 | import createNotFoundRoute from 'not-found/routes.js'; 13 | 14 | export default NotFoundPage; 15 | export { createNotFoundRoute }; 16 | -------------------------------------------------------------------------------- /client/app/not-found/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The route belongs to the feature so we keep it with the feature code. 3 | * The drawback to this approach is that developers have to be cognicant 4 | * of name collisions. This app is too small to worry about. 5 | * 6 | * On the plus side, this code allows the client to only load 7 | * the bits he needs. 8 | */ 9 | 10 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 11 | import { errorLoading, loadModule } from 'utils/routeHelpers'; 12 | 13 | export default function createNotFoundRoute(store) { 14 | // Create reusable async injectors using getAsyncInjectors factory 15 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 16 | return { 17 | path: '*', 18 | name: 'notfound', 19 | getComponent(nextState, cb) { 20 | System.import('not-found') 21 | .then(loadModule(cb)) 22 | .catch(errorLoading); 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /client/app/not-found/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | 7 | function NotFoundTemplate() { 8 | return ( 9 |
    Not foud.
    10 | ); 11 | } 12 | 13 | export default NotFoundTemplate; 14 | -------------------------------------------------------------------------------- /client/app/product-list-item/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A container is used to generate a higher order component that connects 3 | * a template component to the state in the redux store. 4 | * Containers also connect events in the template to redux actions. 5 | * see: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 6 | **/ 7 | 8 | import React from 'react'; 9 | import { connect } from 'react-redux'; 10 | import { browserHistory } from 'react-router'; 11 | import { setImage } from 'product-page'; 12 | import CcProductListItemTemplate from 'product-list-item/template'; 13 | 14 | const CcProductListItem = connect( 15 | mapStateToProps, 16 | mapDispatchToProps 17 | )(CcProductListItemTemplate); 18 | 19 | CcProductListItem.propTypes = { 20 | price: React.PropTypes.string.isRequired, 21 | name: React.PropTypes.string.isRequired, 22 | }; 23 | 24 | function mapStateToProps(_, ownProps) { 25 | return ownProps; 26 | } 27 | 28 | function mapDispatchToProps(dispatch, ownProps) { 29 | return { 30 | onClickProduct: () => clickProduct(dispatch, ownProps), 31 | }; 32 | } 33 | 34 | function clickProduct(dispatch, ownProps) { 35 | dispatch(setImage(ownProps.primaryImage)); 36 | browserHistory.push(`/product/${ownProps.id}`); 37 | } 38 | 39 | export default CcProductListItem; 40 | -------------------------------------------------------------------------------- /client/app/product-list-item/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit and we can easily find 8 | * and fix them later. 9 | **/ 10 | 11 | import CcProductListItem from 'product-list-item/container'; 12 | 13 | export default CcProductListItem; 14 | -------------------------------------------------------------------------------- /client/app/product-list-item/styles.css: -------------------------------------------------------------------------------- 1 | .name { 2 | text-align: center; 3 | margin-bottom: 2px; 4 | } 5 | 6 | .price { 7 | text-align: center; 8 | font-size: 34px; 9 | } 10 | 11 | .link { 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /client/app/product-list-item/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | import styles from 'product-list-item/styles.css'; 7 | 8 | CcProductListItemTemplate.propTypes = { 9 | id: React.PropTypes.string, 10 | name: React.PropTypes.string, 11 | image: React.PropTypes.string, 12 | addToCart: React.PropTypes.func, 13 | price: React.PropTypes.string, 14 | shortDescription: React.PropTypes.string, 15 | onClickProduct: React.PropTypes.func, 16 | }; 17 | 18 | function CcProductListItemTemplate({ 19 | id, 20 | name, 21 | image, 22 | shortDescription, 23 | onClickProduct, 24 | price, 25 | }) { 26 | return ( 27 |
    32 | {name} 33 |
    {name}
    34 |
    {price}
    35 | {shortDescription} 36 |
    37 | ); 38 | } 39 | 40 | export default CcProductListItemTemplate; 41 | -------------------------------------------------------------------------------- /client/app/product-list/action-types.js: -------------------------------------------------------------------------------- 1 | // makes action names easy to reference and keep unique. 2 | export const PRODUCTS_LOADED = 'product-list/PRODUCTS_LOADED'; 3 | -------------------------------------------------------------------------------- /client/app/product-list/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React-redux uses a command pattern (http://www.blackwasp.co.uk/Command.aspx) 3 | * We issue commands via objects that describe them. 4 | **/ 5 | 6 | import { PRODUCTS_LOADED } from 'product-list/action-types'; 7 | 8 | export function productsLoaded(products) { 9 | return { 10 | type: PRODUCTS_LOADED, 11 | payload: products, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /client/app/product-list/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A container is used to generate a higher order component that connects 3 | * a template component to the state in the redux store. 4 | * Containers also connect events in the template to redux actions. 5 | * see: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 6 | **/ 7 | 8 | import React, { Component } from 'react'; 9 | import { connect } from 'react-redux'; 10 | import { productsLoaded } from 'product-list/actions'; 11 | import { selectProductListProps } from 'product-list/selectors'; 12 | import { sameProducts, listProducts } from 'product-list/use-cases'; 13 | import * as api from 'api'; 14 | import CcProductListTemplate from 'product-list/template'; 15 | 16 | /** 17 | * Extending or inheriting from a base class supplied by a third party framework 18 | * is a terrible thing to do. We should strive to avoid this at all costs. 19 | * It tightly binds our code to the framework and will cause great pain in the 20 | * future as the frameworks change and/or need to be replaced. Not clean!! 21 | * 22 | * We are doing it here in order to gain access to React's component lifecycle 23 | * hooks (mount and update). 24 | * 25 | * Notice that the real business logic is still happening in our use cases. 26 | * Keeping the logic seperate protects us from this 27 | * terrible practice and keeps our architecture relatively clean. 28 | **/ 29 | class CcProductList extends Component { 30 | componentDidMount() { 31 | this.fetchData(); 32 | } 33 | 34 | componentDidUpdate(prevProps) { 35 | this.fetchData(prevProps.products); 36 | } 37 | 38 | fetchData(oldList) { 39 | const newList = this.props.products; 40 | const callBack = this.props.onProductsLoaded; 41 | if ( 42 | !oldList || 43 | (!!oldList && !sameProducts(oldList, newList)) 44 | ) { 45 | listProducts(api).then((products) => { 46 | callBack(products); 47 | }); 48 | } 49 | } 50 | 51 | render() { 52 | return ; 53 | } 54 | } 55 | 56 | CcProductList.propTypes = { 57 | onProductsLoaded: React.PropTypes.func, 58 | products: React.PropTypes.array, 59 | }; 60 | 61 | /** 62 | * we quickly replace our CcProductList higher-order-component (HOC) with a 63 | * connected version. Watch Dan Abramov's free course on idiomatic React-Redux 64 | * for more information about this technique: 65 | * https://egghead.io/courses/building-react-applications-with-idiomatic-redux 66 | * see lesson 14 in particular 67 | **/ 68 | CcProductList = connect( // eslint-disable-line no-class-assign 69 | mapStateToProps, 70 | { onProductsLoaded: productsLoaded } 71 | )(CcProductList); 72 | 73 | function mapStateToProps(state) { 74 | return selectProductListProps(state); 75 | } 76 | 77 | // function mapDispatchToProps(dispatch) { 78 | // return { 79 | // onProductsLoaded: (products) => dispatch(productsLoaded(products)), 80 | // }; 81 | // } 82 | 83 | export default CcProductList; 84 | -------------------------------------------------------------------------------- /client/app/product-list/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit and we can easily find 8 | * and fix them later. 9 | **/ 10 | 11 | import { productsLoaded } from 'product-list/actions'; 12 | import { filterProductsById, getProductById, formatProduct } from 'product-list/use-cases'; 13 | import { selectProducts } from 'product-list/selectors'; 14 | import CcProductList from 'product-list/container'; 15 | 16 | export default CcProductList; 17 | export { 18 | selectProducts, 19 | filterProductsById, 20 | getProductById, 21 | formatProduct, 22 | productsLoaded, 23 | }; 24 | -------------------------------------------------------------------------------- /client/app/product-list/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In Redux, the reducer is where we manage state for anything global. 3 | * Here, we manage the state of the product list. 4 | **/ 5 | 6 | import { fromJS } from 'immutable'; 7 | import productList from 'product-list/state'; 8 | import { PRODUCTS_LOADED } from 'product-list/action-types'; 9 | 10 | const initialState = fromJS(productList); 11 | 12 | function productListReducer(state = initialState, action) { 13 | switch (action.type) { 14 | case PRODUCTS_LOADED: 15 | return state.set('products', action.payload); 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default productListReducer; 22 | -------------------------------------------------------------------------------- /client/app/product-list/selectors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * selectors let us take advantage of simple caching ('memoization'). 3 | * More info: https://github.com/reactjs/reselect 4 | */ 5 | 6 | import { createSelector } from 'reselect'; 7 | import { formatProductList } from 'product-list/use-cases'; 8 | import CcSpinner from 'spinner'; 9 | 10 | export { selectProductListProps, selectProducts }; 11 | 12 | function selectProductListProps() { 13 | return createSelector( 14 | selectSpinner(), 15 | selectProducts, 16 | (spinner, products) => ({ 17 | spinner, 18 | products: formatProductList(products), 19 | }) 20 | ); 21 | } 22 | 23 | function selectSpinner() { 24 | return createSelector( 25 | selectProducts, 26 | (products) => ((products.length === 0) ? new CcSpinner() : null) 27 | ); 28 | } 29 | 30 | function selectProducts(state) { 31 | return state.toJS().productList ? state.toJS().productList.products : []; 32 | } 33 | -------------------------------------------------------------------------------- /client/app/product-list/state.js: -------------------------------------------------------------------------------- 1 | // here, we define the shape of our initial state free from any dependencies. 2 | const productList = { 3 | products: [], 4 | }; 5 | 6 | export default productList; 7 | -------------------------------------------------------------------------------- /client/app/product-list/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | import CcProductListItem from 'product-list-item'; 7 | 8 | CcProductListTemplate.propTypes = { 9 | products: React.PropTypes.array.isRequired, 10 | spinner: React.PropTypes.element, 11 | }; 12 | 13 | export default CcProductListTemplate; 14 | 15 | function CcProductListTemplate({ 16 | products, 17 | spinner, 18 | }) { 19 | return ( 20 |
    21 | {/* 22 | The logic that determines when to show/hide the spinner is 23 | in the container. This keeps our template free of logic and state. 24 | */} 25 | {spinner} 26 | {/* 27 | The map function below is the only "logic" we allow in templates, 28 | because there is no other good way to loop over a list of items in React. 29 | */} 30 | {products.map((product) => 31 |
    32 | 39 |
    40 | )} 41 |
    42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/app/product-list/tests.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | filterProductsById, 4 | formatProduct, 5 | formatProductList, 6 | sameProducts, 7 | } from 'product-list/use-cases'; 8 | 9 | describe('product-list', () => 10 | it('should format a product', () => { 11 | const dummyProduct = makeDummyProduct(); 12 | isProductFormatted(dummyProduct); 13 | }), 14 | 15 | it('should format a list of products', () => { 16 | const productList = makeDummyProductList(); 17 | formatProductList(productList); 18 | productList.forEach((product) => isProductFormatted(product)); 19 | }), 20 | 21 | it('should compare products', () => { 22 | const firstProducts = makeDummyProductList(); 23 | const secondProducts = makeDummyProductList(); 24 | const same = sameProducts(firstProducts, firstProducts); 25 | const different = sameProducts(firstProducts, secondProducts); 26 | expect(same).toBe(true); 27 | expect(different).toBe(false); 28 | }), 29 | 30 | it('should filter products by id', () => { 31 | const productList = makeDummyProductList(); 32 | const filteredProducts = filterProductsById(productList, productList[0].id); 33 | expect(filteredProducts[0].id).toBe(productList[0].id); 34 | }) 35 | ); 36 | 37 | function makeDummyProductList() { 38 | const productList = []; 39 | for (let i = 0; i < 3; i++) { 40 | productList.push(makeDummyProduct()); 41 | } 42 | return productList; 43 | } 44 | 45 | function makeDummyProduct() { 46 | return { 47 | id: Math.floor((Math.random() * 10000) + 1), 48 | price: 6.99, 49 | description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 50 | 'Cras finibus ligula et lectus luctus condimentum. ' + 51 | 'Aenean rutrum purus ut lacus.', 52 | }; 53 | } 54 | 55 | function isProductFormatted(product) { 56 | const formattedProduct = formatProduct(product); 57 | const short = formattedProduct.shortDescription; 58 | const price = formattedProduct.price; 59 | 60 | expect(formattedProduct).toIncludeKey('shortDescription'); 61 | expect(short).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 62 | 'Cras finibus ...'); 63 | expect(price).toBeA('string') 64 | .toMatch(/(\$)(\d*)(.)(\d*)/); 65 | } 66 | -------------------------------------------------------------------------------- /client/app/product-list/use-cases.js: -------------------------------------------------------------------------------- 1 | import { validateApi } from 'utils/use-case-helper'; 2 | 3 | export { 4 | filterProductsById, 5 | formatProduct, 6 | formatProductList, 7 | getProductById, 8 | listProducts, 9 | sameProducts, 10 | }; 11 | 12 | function formatProduct(product) { 13 | validateProduct(product); 14 | // isValidDescription keeps eslint calm and happy. 15 | // http://eslint.org/docs/rules/no-extra-boolean-cast 16 | const isValidDescription = !!product.description; 17 | const description = isValidDescription && product.description; 18 | const shortDescription = isValidDescription ? 19 | description.split(' ') // eslint-disable-line prefer-template 20 | .slice(0, 10) 21 | .join(' ') + 22 | ' ...' : null; 23 | const price = product.price.toLocaleString('en-US', 24 | { style: 'currency', currency: 'USD' }); 25 | 26 | const images = product.additionalImages ? 27 | product.additionalImages.concat([product.primaryImage]) : 28 | [].concat([product.primaryImage]); 29 | 30 | return { ...product, shortDescription, price, images }; 31 | } 32 | 33 | function formatProductList(products) { 34 | validateProductList(products); 35 | return (products.length > 0) ? 36 | products.map(formatProduct) : 37 | []; 38 | } 39 | 40 | function filterProductsById(products, id) { 41 | validateProductList(products); 42 | return products.filter((product) => product.id === id); 43 | } 44 | 45 | function getProductById(api, id) { 46 | validateApi(api, 'getProductById'); 47 | return api.getProductById(id); 48 | } 49 | 50 | function listProducts(api) { 51 | validateApi(api, 'getProducts'); 52 | return api.getProducts(); 53 | } 54 | 55 | function sameProducts(oldList, newList) { 56 | let areEqual = false; 57 | 58 | validateProductList(oldList); 59 | validateProductList(newList); 60 | 61 | if (oldList.length !== newList.length) { 62 | return false; 63 | } 64 | 65 | oldList.forEach((product, index) => { 66 | areEqual = (product.id === newList[index].id); 67 | }); 68 | 69 | return areEqual; 70 | } 71 | 72 | function validateProduct(product) { 73 | if (!product || !product.id || !product.price) { 74 | throw new Error('invalid product'); 75 | } 76 | } 77 | 78 | function validateProductList(list) { 79 | if (!(Array.isArray(list)) || 80 | (!(list.length === 0) && 81 | !(list[0].id))) { 82 | throw new Error('invalid product list'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /client/app/product-page/action-types.js: -------------------------------------------------------------------------------- 1 | // makes action names easy to reference and keep unique. 2 | export const SET_IMAGE = 'product-page/SET_IMAGE'; 3 | -------------------------------------------------------------------------------- /client/app/product-page/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React-redux uses a command pattern (http://www.blackwasp.co.uk/Command.aspx) 3 | * We issue commands via objects that describe them. 4 | **/ 5 | import { SET_IMAGE } from 'product-page/action-types'; 6 | 7 | export { 8 | setImage, 9 | }; 10 | 11 | function setImage(image) { 12 | return { 13 | type: SET_IMAGE, 14 | payload: { 15 | image, 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /client/app/product-page/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A container is used to generate a higher order component that connects 3 | * a template component to the state in the redux store. 4 | * Containers also connect events in the template to redux actions. 5 | * see: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 6 | **/ 7 | 8 | import React, { Component } from 'react'; 9 | import { getProductById } from 'product-list/use-cases'; 10 | import * as api from 'api'; 11 | import CcProductTemplate from 'product-page/template'; 12 | import { connect } from 'react-redux'; 13 | import { withRouter } from 'react-router'; 14 | import { selectProductProps } from 'product-page/selectors'; 15 | import { setImage } from 'product-page/actions'; 16 | import { productsLoaded } from 'product-list'; 17 | import { addToCart } from 'cart-page'; 18 | 19 | /** 20 | * Extending or inheriting from a base class supplied by a third party framework 21 | * is a terrible thing to do. We should strive to avoid this at all costs. 22 | * It tightly binds our code to the framework and will cause great pain in the 23 | * future as frameworks change and/or need to be replaced. Not clean!! 24 | * 25 | * We are doing it here in order to gain access to React's component lifecycle 26 | * hooks (mount and update). 27 | * 28 | * Notice that the real business logic is still happening in our use cases. 29 | * Keeping the logic seperate protects us from this 30 | * terrible practice and keeps our architecture relatively clean. 31 | **/ 32 | class CcProductPage extends Component { 33 | componentDidMount() { 34 | this.fetchData(); 35 | } 36 | 37 | componentDidUpdate(prevProps) { 38 | this.fetchData(prevProps); 39 | } 40 | 41 | fetchData(oldProps) { 42 | const callback = this.props.onProductLoaded; 43 | const noId = !this.props.id; 44 | const oldPropsExist = !!oldProps; 45 | const gotNewId = oldPropsExist && this.props.id !== oldProps.id; 46 | 47 | if (noId || (oldPropsExist && gotNewId)) { 48 | getProductById(api, this.props.params.id) 49 | .then((product) => callback(product)); 50 | } 51 | } 52 | 53 | render() { 54 | return ; 55 | } 56 | } 57 | 58 | CcProductPage.propTypes = { 59 | onProductLoaded: React.PropTypes.func, 60 | id: React.PropTypes.string, 61 | params: React.PropTypes.object, 62 | }; 63 | 64 | /** 65 | * we quickly replace our CcProductPage higher-order-component (HOC) with a 66 | * connected version. Watch Dan Abramov's free course on idiomatic React-Redux 67 | * for more information about this technique: 68 | * https://egghead.io/courses/building-react-applications-with-idiomatic-redux 69 | * see lesson 14 in particular 70 | **/ 71 | CcProductPage = withRouter( // eslint-disable-line no-class-assign 72 | connect( 73 | mapStateToProps, 74 | mapDispatchToProps 75 | )(CcProductPage) 76 | ); 77 | 78 | function mapStateToProps(state, ownProps) { 79 | return selectProductProps(state, ownProps); 80 | } 81 | 82 | function mapDispatchToProps(dispatch, ownProps) { 83 | return { 84 | onAddToCart: () => dispatch(addToCart(ownProps.params.id)), 85 | // Notice how we destructure the event object passed into onImageClick below? 86 | // More info at: 87 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 88 | onImageClick: ({ target: img }) => dispatch(setImage(img.src)), 89 | onProductLoaded: (product) => dispatch(productsLoaded([product])), 90 | }; 91 | } 92 | 93 | export default CcProductPage; 94 | -------------------------------------------------------------------------------- /client/app/product-page/index.js: -------------------------------------------------------------------------------- 1 | import createProductRoute from 'product-page/route'; 2 | import { setImage } from 'product-page/actions'; 3 | import CcProductPage from 'product-page/container'; 4 | 5 | 6 | export default CcProductPage; 7 | export { createProductRoute, setImage }; 8 | -------------------------------------------------------------------------------- /client/app/product-page/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In Redux, the reducer is where we manage state for anything global. 3 | * Here, we manage the state of the product page. 4 | * 5 | * We don't hold a product id in state here because it already exists in the 6 | * route branch of the state. So, all that's left to track is the selected image. 7 | **/ 8 | 9 | import { fromJS } from 'immutable'; 10 | import { SET_IMAGE } from 'product-page/action-types'; 11 | import currentProductState from 'product-page/state'; 12 | 13 | const initialState = fromJS(currentProductState); 14 | 15 | function currentProductReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case SET_IMAGE: 18 | return state.set('selectedImage', action.payload.image); 19 | default: 20 | return state; 21 | } 22 | } 23 | 24 | export default currentProductReducer; 25 | -------------------------------------------------------------------------------- /client/app/product-page/route.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The route belongs to the feature so we keep it with the feature code. 3 | * The drawback to this approach is that developers have to be cognicant 4 | * of name collisions. This app is too small to worry about. 5 | * 6 | * On the plus side, this code allows the client to only load 7 | * the bits he needs. 8 | */ 9 | 10 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 11 | import { errorLoading, loadModule } from 'utils/routeHelpers'; 12 | 13 | export default function createProductRoute(store) { 14 | // Create reusable async injectors using getAsyncInjectors factory 15 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 16 | return { 17 | path: '/product/:id', 18 | name: 'product', 19 | getComponent(nextState, cb) { 20 | const importModules = Promise.all([ 21 | System.import('product-page'), 22 | ]); 23 | 24 | const renderRoute = loadModule(cb); 25 | 26 | importModules.then(([component]) => { 27 | renderRoute(component); 28 | }); 29 | 30 | importModules.catch(errorLoading); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/app/product-page/selectors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * selectors let us take advantage of simple caching ('memoization'). 3 | * More info: https://github.com/reactjs/reselect 4 | */ 5 | import { createSelector } from 'reselect'; 6 | import { 7 | selectProducts, 8 | filterProductsById, 9 | formatProduct, 10 | } 11 | from 'product-list'; 12 | 13 | export { selectProductProps }; 14 | 15 | function selectProductProps(state, ownProps) { 16 | return createSelector( 17 | selectProduct(state, ownProps), 18 | selectProductImage(state, ownProps), 19 | (product, image) => { 20 | if (!product) return ownProps; 21 | const selectedImage = image || product.primaryImage; 22 | return { ...product, selectedImage }; 23 | } 24 | ); 25 | } 26 | 27 | function selectProduct(_, ownProps) { 28 | return createSelector( 29 | selectProducts, 30 | (products) => { 31 | const id = ownProps.params ? ownProps.params.id : null; 32 | if (products && (products.length > 0) && id) { 33 | return formatProduct(filterProductsById(products, id)[0]); 34 | } 35 | return null; 36 | } 37 | ); 38 | } 39 | 40 | function selectProductImage() { 41 | return createSelector( 42 | selectCurrentProduct, 43 | (product) => product.selectedImage || null, 44 | ); 45 | } 46 | 47 | function selectCurrentProduct(state) { 48 | return state.toJS().currentProduct; 49 | } 50 | -------------------------------------------------------------------------------- /client/app/product-page/state.js: -------------------------------------------------------------------------------- 1 | // here, we define the shape of our initial state free from any dependencies. 2 | const currentProductState = { 3 | selectedImage: null, 4 | }; 5 | 6 | export default currentProductState; 7 | -------------------------------------------------------------------------------- /client/app/product-page/styles.css: -------------------------------------------------------------------------------- 1 | .price { 2 | font-size: 34px; 3 | } 4 | 5 | .link { 6 | cursor: pointer; 7 | } 8 | 9 | .featureImage { 10 | margin-top: 23px; 11 | } 12 | 13 | .thumb { 14 | margin-top: 5px; 15 | max-width: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /client/app/product-page/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates should be stateless and "dumb". No logic here please. 3 | **/ 4 | 5 | import React from 'react'; 6 | import styles from 'product-page/styles.css'; 7 | 8 | CcProductTemplate.propTypes = { 9 | description: React.PropTypes.string, 10 | selectedImage: React.PropTypes.string, 11 | id: React.PropTypes.string, 12 | images: React.PropTypes.array, 13 | name: React.PropTypes.string, 14 | onAddToCart: React.PropTypes.func, 15 | onImageClick: React.PropTypes.func, 16 | price: React.PropTypes.string, 17 | }; 18 | 19 | function CcProductTemplate( 20 | { 21 | description, 22 | selectedImage, 23 | images, 24 | name, 25 | onAddToCart, 26 | onImageClick, 27 | price, 28 | } 29 | ) { 30 | return ( 31 |
    32 |
    33 |
    34 | {name} 38 | {images && images.map((image, index) => 39 |
    40 | {name} 46 |
    47 | )} 48 |
    49 |
    50 |

    {name}

    51 |

    {description}

    52 |
    {price}
    53 | 54 |
    55 |
    56 |
    57 | ); 58 | } 59 | 60 | export default CcProductTemplate; 61 | -------------------------------------------------------------------------------- /client/app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | /** 8 | * Combine all reducers in this file and export the combined reducers. 9 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 10 | */ 11 | 12 | import { combineReducers } from 'redux-immutable'; 13 | import { fromJS } from 'immutable'; 14 | import { LOCATION_CHANGE } from 'react-router-redux'; 15 | import cartReducer from 'cart-page/reducer'; 16 | import productListReducer from 'product-list/reducer.js'; 17 | import productPageReducer from 'product-page/reducer.js'; 18 | 19 | /* 20 | * routeReducer 21 | * 22 | * The reducer merges route location changes into our immutable state. 23 | * The change is necessitated by moving to react-router-redux@4 24 | * 25 | */ 26 | 27 | // Initial routing state 28 | const routeInitialState = fromJS({ 29 | locationBeforeTransitions: null, 30 | }); 31 | 32 | /** 33 | * Merge route into the global application state 34 | */ 35 | function routeReducer(state = routeInitialState, action) { 36 | switch (action.type) { 37 | /* istanbul ignore next */ 38 | case LOCATION_CHANGE: 39 | return state.merge({ 40 | locationBeforeTransitions: action.payload, 41 | }); 42 | default: 43 | return state; 44 | } 45 | } 46 | 47 | /** 48 | * Creates the main reducer with the asynchronously loaded ones 49 | */ 50 | export default function createReducer(asyncReducers) { 51 | return combineReducers({ 52 | route: routeReducer, 53 | productList: productListReducer, 54 | cart: cartReducer, 55 | currentProduct: productPageReducer, 56 | ...asyncReducers, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /client/app/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { createHomeRoute } from 'home-page'; 6 | import { createCatalogRoute } from 'catalog-page'; 7 | import { createCartRoute } from 'cart-page'; 8 | import { createProductRoute } from 'product-page'; 9 | import { createNotFoundRoute } from 'not-found'; 10 | 11 | export default function createRoutes(store) { 12 | return [ 13 | createHomeRoute(store), 14 | createCatalogRoute(store), 15 | createProductRoute(store), 16 | createCartRoute(store), 17 | createNotFoundRoute(store), 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /client/app/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | 8 | // selectLocationState expects a plain JS object for the routing state 9 | const selectLocationState = () => { 10 | let prevRoutingState; 11 | let prevRoutingStateJS; 12 | 13 | return (state) => { 14 | const routingState = state.get('route'); // or state.route 15 | 16 | if (!routingState.equals(prevRoutingState)) { 17 | prevRoutingState = routingState; 18 | prevRoutingStateJS = routingState.toJS(); 19 | } 20 | 21 | return prevRoutingStateJS; 22 | }; 23 | }; 24 | 25 | export { 26 | selectLocationState, 27 | }; 28 | -------------------------------------------------------------------------------- /client/app/selectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | 8 | import { fromJS } from 'immutable'; 9 | import expect from 'expect'; 10 | 11 | import { selectLocationState } from 'selectors'; 12 | 13 | describe('selectLocationState', () => { 14 | it('should select the route as a plain JS object', () => { 15 | const route = fromJS({ 16 | locationBeforeTransitions: null, 17 | }); 18 | const mockedState = fromJS({ 19 | route, 20 | }); 21 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/spinner/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generally an index.js file should simply provide an index of public methods 3 | * and modules. This is a good way to signal to consumers of this feature code 4 | * which elements are meant to be referenced and used and which ones are for 5 | * internal use only. Of course, nothing stops code from reaching deeper into 6 | * the hierarchy and referencing internal modules, but this way, those imports 7 | * will stand out because they have to be more explicit and we can easily find 8 | * and fix them later. 9 | **/ 10 | 11 | 12 | import CcSpinner from 'spinner/template'; 13 | 14 | export default CcSpinner; 15 | -------------------------------------------------------------------------------- /client/app/spinner/styles.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | margin: 10vh auto 0; 3 | width: 70px; 4 | text-align: center; 5 | } 6 | 7 | .spinner > div { 8 | width: 18px; 9 | height: 18px; 10 | background-color: slategray; 11 | border-radius: 100%; 12 | display: inline-block; 13 | -moz-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 14 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 15 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 16 | } 17 | 18 | .spinner .bounce1 { 19 | -moz-animation-delay: -0.32s; 20 | -webkit-animation-delay: -0.32s; 21 | animation-delay: -0.32s; 22 | } 23 | 24 | .spinner .bounce2 { 25 | -moz-animation-delay: -0.16s; 26 | -webkit-animation-delay: -0.16s; 27 | animation-delay: -0.16s; 28 | } 29 | 30 | @-webkit-keyframes sk-bouncedelay { 31 | 0%, 32 | 80%, 33 | 100% { 34 | -webkit-transform: scale(0); 35 | transform: scale(0); 36 | } 37 | 38 | 40% { 39 | -webkit-transform: scale(1); 40 | transform: scale(1); 41 | } 42 | } 43 | 44 | @keyframes sk-bouncedelay { 45 | 0%, 46 | 80%, 47 | 100% { 48 | -webkit-transform: scale(0); 49 | transform: scale(0); 50 | } 51 | 52 | 40% { 53 | -webkit-transform: scale(1); 54 | transform: scale(1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/app/spinner/template.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from 'spinner/styles.css'; 3 | 4 | function CcSpinnerTemplate() { 5 | return ( 6 |
    7 |
    8 |
    9 |
    10 |
    11 | ); 12 | } 13 | 14 | export default CcSpinnerTemplate; 15 | -------------------------------------------------------------------------------- /client/app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | 8 | /** 9 | * Create the store with asynchronously loaded reducers 10 | */ 11 | 12 | import { createStore, applyMiddleware, compose } from 'redux'; 13 | import { fromJS } from 'immutable'; 14 | import { routerMiddleware } from 'react-router-redux'; 15 | import createSagaMiddleware from 'redux-saga'; 16 | import createReducer from './reducers'; 17 | 18 | const sagaMiddleware = createSagaMiddleware(); 19 | const devtools = window.devToolsExtension || (() => noop => noop); 20 | 21 | export default function configureStore(initialState = {}, history) { 22 | // Create the store with two middlewares 23 | // 1. sagaMiddleware: Makes redux-sagas work 24 | // 2. routerMiddleware: Syncs the location/URL path to the state 25 | const middlewares = [ 26 | sagaMiddleware, 27 | routerMiddleware(history), 28 | ]; 29 | 30 | const enhancers = [ 31 | applyMiddleware(...middlewares), 32 | devtools(), 33 | ]; 34 | 35 | const store = createStore( 36 | createReducer(), 37 | fromJS(initialState), 38 | compose(...enhancers) 39 | ); 40 | 41 | // Create hook for async sagas 42 | store.runSaga = sagaMiddleware.run; 43 | 44 | // Make reducers hot reloadable, see http://mxs.is/googmo 45 | /* istanbul ignore next */ 46 | if (module.hot) { 47 | System.import('./reducers').then((reducerModule) => { 48 | const createReducers = reducerModule.default; 49 | const nextReducers = createReducers(store.asyncReducers); 50 | 51 | store.replaceReducer(nextReducers); 52 | }); 53 | } 54 | 55 | // Initialize it with no other reducers 56 | store.asyncReducers = {}; 57 | return store; 58 | } 59 | -------------------------------------------------------------------------------- /client/app/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * styles.css 3 | * 4 | * Global styles 5 | */ 6 | .appRoot { 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /client/app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DevMastery Note: This is mostly code from React-boilerplate. 3 | * Not much was changed here. More info at: 4 | * https://github.com/mxstbr/react-boilerplate 5 | **/ 6 | 7 | /** 8 | * Test store addons 9 | */ 10 | 11 | import expect from 'expect'; 12 | import configureStore from '../store'; 13 | import { browserHistory } from 'react-router'; 14 | 15 | describe('configureStore', () => { 16 | let store; 17 | 18 | before(() => { 19 | store = configureStore({}, browserHistory); 20 | }); 21 | 22 | describe('asyncReducers', () => { 23 | it('should contain an object for async reducers', () => { 24 | expect(typeof store.asyncReducers).toEqual('object'); 25 | }); 26 | }); 27 | 28 | describe('runSaga', () => { 29 | it('should contain a hook for `sagaMiddleware.run`', () => { 30 | expect(typeof store.runSaga).toEqual('function'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /client/app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 37 | store.replaceReducer(createReducer(store.asyncReducers)); 38 | }; 39 | } 40 | 41 | /** 42 | * Inject an asynchronously loaded saga 43 | */ 44 | export function injectAsyncSagas(store, isValid) { 45 | return function injectSagas(sagas) { 46 | if (!isValid) checkStore(store); 47 | 48 | invariant( 49 | Array.isArray(sagas), 50 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 51 | ); 52 | 53 | warning( 54 | !isEmpty(sagas), 55 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 56 | ); 57 | 58 | sagas.map(store.runSaga); 59 | }; 60 | } 61 | 62 | /** 63 | * Helper for creating injectors 64 | */ 65 | export function getAsyncInjectors(store) { 66 | checkStore(store); 67 | 68 | return { 69 | injectReducer: injectAsyncReducer(store, true), 70 | injectSagas: injectAsyncSagas(store, true), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /client/app/utils/routeHelpers.js: -------------------------------------------------------------------------------- 1 | const errorLoading = (err) => { 2 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 3 | }; 4 | 5 | const loadModule = (cb) => (componentModule) => { 6 | cb(null, componentModule.default); 7 | }; 8 | 9 | export { errorLoading, loadModule }; 10 | -------------------------------------------------------------------------------- /client/app/utils/tests/asyncInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test async injectors 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store.js'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getAsyncInjectors, 15 | } from 'utils/asyncInjectors'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | function* testSaga() { 31 | yield put({ type: 'TEST', payload: 'yup' }); 32 | } 33 | 34 | const sagas = [ 35 | testSaga, 36 | ]; 37 | 38 | describe('asyncInjectors', () => { 39 | let store; 40 | 41 | describe('getAsyncInjectors', () => { 42 | before(() => { 43 | store = configureStore({}, memoryHistory); 44 | }); 45 | 46 | it('given a store, should return all async injectors', () => { 47 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 48 | 49 | injectReducer('test', reducer); 50 | injectSagas(sagas); 51 | 52 | const actual = store.getState().get('test'); 53 | const expected = initialState.merge({ reduced: 'yup' }); 54 | 55 | expect(actual.toJS()).toEqual(expected.toJS()); 56 | }); 57 | 58 | it('should throw if passed invalid store shape', () => { 59 | let result = false; 60 | 61 | Reflect.deleteProperty(store, 'dispatch'); 62 | 63 | try { 64 | getAsyncInjectors(store); 65 | } catch (err) { 66 | result = err.name === 'Invariant Violation'; 67 | } 68 | 69 | expect(result).toEqual(true); 70 | }); 71 | }); 72 | 73 | describe('helpers', () => { 74 | before(() => { 75 | store = configureStore({}, memoryHistory); 76 | }); 77 | 78 | describe('injectAsyncReducer', () => { 79 | it('given a store, it should provide a function to inject a reducer', () => { 80 | const injectReducer = injectAsyncReducer(store); 81 | 82 | injectReducer('test', reducer); 83 | 84 | const actual = store.getState().get('test'); 85 | const expected = initialState; 86 | 87 | expect(actual.toJS()).toEqual(expected.toJS()); 88 | }); 89 | 90 | it('should throw if passed invalid name', () => { 91 | let result = false; 92 | 93 | const injectReducer = injectAsyncReducer(store); 94 | 95 | try { 96 | injectReducer('', reducer); 97 | } catch (err) { 98 | result = err.name === 'Invariant Violation'; 99 | } 100 | 101 | try { 102 | injectReducer(999, reducer); 103 | } catch (err) { 104 | result = err.name === 'Invariant Violation'; 105 | } 106 | 107 | expect(result).toEqual(true); 108 | }); 109 | 110 | it('should throw if passed invalid reducer', () => { 111 | let result = false; 112 | 113 | const injectReducer = injectAsyncReducer(store); 114 | 115 | try { 116 | injectReducer('bad', 'nope'); 117 | } catch (err) { 118 | result = err.name === 'Invariant Violation'; 119 | } 120 | 121 | try { 122 | injectReducer('coolio', 12345); 123 | } catch (err) { 124 | result = err.name === 'Invariant Violation'; 125 | } 126 | 127 | expect(result).toEqual(true); 128 | }); 129 | }); 130 | 131 | describe('injectAsyncSagas', () => { 132 | it('given a store, it should provide a function to inject a saga', () => { 133 | const injectSagas = injectAsyncSagas(store); 134 | 135 | injectSagas(sagas); 136 | 137 | const actual = store.getState().get('test'); 138 | const expected = initialState.merge({ reduced: 'yup' }); 139 | 140 | expect(actual.toJS()).toEqual(expected.toJS()); 141 | }); 142 | 143 | it('should throw if passed invalid saga', () => { 144 | let result = false; 145 | 146 | const injectSagas = injectAsyncSagas(store); 147 | 148 | try { 149 | injectSagas({ testSaga }); 150 | } catch (err) { 151 | result = err.name === 'Invariant Violation'; 152 | } 153 | 154 | try { 155 | injectSagas(testSaga); 156 | } catch (err) { 157 | result = err.name === 'Invariant Violation'; 158 | } 159 | 160 | expect(result).toEqual(true); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /client/app/utils/use-case-helper.js: -------------------------------------------------------------------------------- 1 | // Plain JavaScript doesn't let us take advantage of Interfaces or static types. 2 | // This function just gives us a way to explicitely defend against invalid apis. 3 | export function validateApi(api, method) { 4 | if (!api || !api[method]) { 5 | throw new TypeError(`Invalid api! Expected object with method, ${method} `); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: "5.0" 15 | 16 | # Fix line endings in Windows. (runs before repo cloning) 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | # Install scripts--runs after repo cloning 21 | install: 22 | # Install chrome 23 | - choco install -y googlechrome 24 | # Install the latest stable version of Node 25 | - ps: Install-Product node $env:nodejs_version 26 | - npm -g install npm 27 | - set PATH=%APPDATA%\npm;%PATH% 28 | - npm install 29 | 30 | # Disable automatic builds 31 | build: off 32 | 33 | # Post-install test scripts 34 | test_script: 35 | # Output debugging info 36 | - node --version 37 | - npm --version 38 | # run build and run tests 39 | - npm run build 40 | 41 | # remove, as appveyor doesn't support secure variables on pr builds 42 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 43 | #on_success: 44 | #- npm run coveralls 45 | -------------------------------------------------------------------------------- /client/internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.1.0', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here. 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css', 31 | 'bootstrap-css-only', 32 | ], 33 | 34 | /** 35 | * Specify any additional dependencies here. We include core-js and lodash 36 | * since a lot of our dependencies depend on them and they get picked up by webpack. 37 | */ 38 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 39 | 40 | // The path where the DLL manifest and bundle will get built 41 | path: resolve('../node_modules/react-boilerplate-dlls'), 42 | }, 43 | 44 | entry(pkg) { 45 | const dependencyNames = Object.keys(pkg.dependencies); 46 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 47 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 48 | const includeDependencies = uniq(dependencyNames.concat(include)); 49 | 50 | return { 51 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 52 | }; 53 | }, 54 | }, 55 | }; 56 | 57 | module.exports = ReactBoilerplate; 58 | -------------------------------------------------------------------------------- /client/internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /client/internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'Stateless Function', 14 | choices: () => ['ES6 Class', 'Stateless Function'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: value => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantCSS', 30 | default: true, 31 | message: 'Does it have styling?', 32 | }, { 33 | type: 'confirm', 34 | name: 'wantMessages', 35 | default: true, 36 | message: 'Do you want i18n messages (i.e. will this component use text)?', 37 | }], 38 | actions: data => { 39 | // Generate index.js and index.test.js 40 | const actions = [{ 41 | type: 'add', 42 | path: '../../app/components/{{properCase name}}/index.js', 43 | templateFile: data.type === 'ES6 Class' ? './component/es6.js.hbs' : './component/stateless.js.hbs', 44 | abortOnFail: true, 45 | }, { 46 | type: 'add', 47 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 48 | templateFile: './component/test.js.hbs', 49 | abortOnFail: true, 50 | }]; 51 | 52 | // If they want a CSS file, add styles.css 53 | if (data.wantCSS) { 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/components/{{properCase name}}/styles.css', 57 | templateFile: './component/styles.css.hbs', 58 | abortOnFail: true, 59 | }); 60 | } 61 | 62 | // If they want a i18n messages file 63 | if (data.wantMessages) { 64 | actions.push({ 65 | type: 'add', 66 | path: '../../app/components/{{properCase name}}/messages.js', 67 | templateFile: './component/messages.js.hbs', 68 | abortOnFail: true, 69 | }); 70 | } 71 | 72 | return actions; 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /client/internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /client/internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | 14 | {{#if wantCSS}} 15 | import styles from './styles.css'; 16 | {{/if}} 17 | 18 | function {{ properCase name }}() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | 32 | export default {{ properCase name }}; 33 | -------------------------------------------------------------------------------- /client/internals/generators/component/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /client/internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /client/internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /client/internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /client/internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /client/internals/generators/container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a container component', 9 | prompts: [{ 10 | type: 'input', 11 | name: 'name', 12 | message: 'What should it be called?', 13 | default: 'Form', 14 | validate: value => { 15 | if ((/.+/).test(value)) { 16 | return componentExists(value) ? 'A component or container with this name already exists' : true; 17 | } 18 | 19 | return 'The name is required'; 20 | }, 21 | }, { 22 | type: 'confirm', 23 | name: 'wantHeaders', 24 | default: false, 25 | message: 'Do you want headers?', 26 | }, { 27 | type: 'confirm', 28 | name: 'wantCSS', 29 | default: false, 30 | message: 'Does it have styling?', 31 | }, { 32 | type: 'confirm', 33 | name: 'wantActionsAndReducer', 34 | default: true, 35 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?', 36 | }, { 37 | type: 'confirm', 38 | name: 'wantSagas', 39 | default: true, 40 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)', 41 | }, { 42 | type: 'confirm', 43 | name: 'wantMessages', 44 | default: true, 45 | message: 'Do you want i18n messages (i.e. will this component use text)?', 46 | }], 47 | actions: data => { 48 | // Generate index.js and index.test.js 49 | const actions = [{ 50 | type: 'add', 51 | path: '../../app/containers/{{properCase name}}/index.js', 52 | templateFile: './container/index.js.hbs', 53 | abortOnFail: true, 54 | }, { 55 | type: 'add', 56 | path: '../../app/containers/{{properCase name}}/tests/index.test.js', 57 | templateFile: './container/test.js.hbs', 58 | abortOnFail: true, 59 | }]; 60 | 61 | // If they want a CSS file, add styles.css 62 | if (data.wantCSS) { 63 | actions.push({ 64 | type: 'add', 65 | path: '../../app/containers/{{properCase name}}/styles.css', 66 | templateFile: './container/styles.css.hbs', 67 | abortOnFail: true, 68 | }); 69 | } 70 | 71 | // If component wants messages 72 | if (data.wantMessages) { 73 | actions.push({ 74 | type: 'add', 75 | path: '../../app/containers/{{properCase name}}/messages.js', 76 | templateFile: './container/messages.js.hbs', 77 | abortOnFail: true, 78 | }); 79 | } 80 | 81 | // If they want actions and a reducer, generate actions.js, constants.js, 82 | // reducer.js and the corresponding tests for actions and the reducer 83 | if (data.wantActionsAndReducer) { 84 | // Actions 85 | actions.push({ 86 | type: 'add', 87 | path: '../../app/containers/{{properCase name}}/actions.js', 88 | templateFile: './container/actions.js.hbs', 89 | abortOnFail: true, 90 | }); 91 | actions.push({ 92 | type: 'add', 93 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js', 94 | templateFile: './container/actions.test.js.hbs', 95 | abortOnFail: true, 96 | }); 97 | 98 | // Constants 99 | actions.push({ 100 | type: 'add', 101 | path: '../../app/containers/{{properCase name}}/constants.js', 102 | templateFile: './container/constants.js.hbs', 103 | abortOnFail: true, 104 | }); 105 | 106 | // Selectors 107 | actions.push({ 108 | type: 'add', 109 | path: '../../app/containers/{{properCase name}}/selectors.js', 110 | templateFile: './container/selectors.js.hbs', 111 | abortOnFail: true, 112 | }); 113 | actions.push({ 114 | type: 'add', 115 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js', 116 | templateFile: './container/selectors.test.js.hbs', 117 | abortOnFail: true, 118 | }); 119 | 120 | // Reducer 121 | actions.push({ 122 | type: 'add', 123 | path: '../../app/containers/{{properCase name}}/reducer.js', 124 | templateFile: './container/reducer.js.hbs', 125 | abortOnFail: true, 126 | }); 127 | actions.push({ 128 | type: 'add', 129 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js', 130 | templateFile: './container/reducer.test.js.hbs', 131 | abortOnFail: true, 132 | }); 133 | } 134 | 135 | // Sagas 136 | if (data.wantSagas) { 137 | actions.push({ 138 | type: 'add', 139 | path: '../../app/containers/{{properCase name}}/sagas.js', 140 | templateFile: './container/sagas.js.hbs', 141 | abortOnFail: true, 142 | }); 143 | actions.push({ 144 | type: 'add', 145 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js', 146 | templateFile: './container/sagas.test.js.hbs', 147 | abortOnFail: true, 148 | }); 149 | } 150 | 151 | return actions; 152 | }, 153 | }; 154 | -------------------------------------------------------------------------------- /client/internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | {{#if wantHeaders}} 10 | import Helmet from 'react-helmet'; 11 | {{/if}} 12 | {{#if wantActionsAndReducer}} 13 | import select{{properCase name}} from './selectors'; 14 | {{/if}} 15 | {{#if wantMessages}} 16 | import { FormattedMessage } from 'react-intl'; 17 | import messages from './messages'; 18 | {{/if}} 19 | {{#if wantCSS}} 20 | import styles from './styles.css'; 21 | {{/if}} 22 | 23 | export class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 24 | render() { 25 | return ( 26 | {{#if wantCSS}} 27 |
    28 | {{else}} 29 |
    30 | {{/if}} 31 | {{#if wantHeaders}} 32 | 38 | {{/if}} 39 | {{#if wantMessages}} 40 | 41 | {{/if}} 42 |
    43 | ); 44 | } 45 | } 46 | 47 | {{#if wantActionsAndReducer}} 48 | const mapStateToProps = select{{properCase name}}(); 49 | {{/if}} 50 | 51 | function mapDispatchToProps(dispatch) { 52 | return { 53 | dispatch, 54 | }; 55 | } 56 | 57 | {{#if wantActionsAndReducer}} 58 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 59 | {{else}} 60 | export default connect(mapDispatchToProps)({{ properCase name }}); 61 | {{/if}} 62 | -------------------------------------------------------------------------------- /client/internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /client/internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /client/internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /client/internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /client/internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /client/internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => state => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /client/internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { select{{ properCase name }}Domain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = select{{ properCase name}}Domain(); 6 | 7 | describe('select{{ properCase name }}Domain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /client/internals/generators/container/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /client/internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /client/internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const componentGenerator = require('./component/index.js'); 9 | const containerGenerator = require('./container/index.js'); 10 | const routeGenerator = require('./route/index.js'); 11 | const languageGenerator = require('./language/index.js'); 12 | 13 | module.exports = (plop) => { 14 | plop.setGenerator('component', componentGenerator); 15 | plop.setGenerator('container', containerGenerator); 16 | plop.setGenerator('route', routeGenerator); 17 | plop.setGenerator('language', languageGenerator); 18 | plop.addHelper('directory', (comp) => { 19 | try { 20 | fs.accessSync(`app/containers/${comp}`, fs.F_OK); 21 | return `containers/${comp}`; 22 | } catch (e) { 23 | return `components/${comp}`; 24 | } 25 | }); 26 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 27 | }; 28 | -------------------------------------------------------------------------------- /client/internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /client/internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | '{{language}}', 3 | -------------------------------------------------------------------------------- /client/internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages({{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /client/internals/generators/language/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Generator 3 | */ 4 | const exec = require('child_process').exec; 5 | 6 | module.exports = { 7 | description: 'Add a langauge', 8 | prompts: [{ 9 | type: 'input', 10 | name: 'language', 11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?', 12 | default: 'fr', 13 | validate: value => { 14 | if ((/.+/).test(value) && value.length === 2) { 15 | return true; 16 | } 17 | 18 | return '2 character language specifier is required'; 19 | }, 20 | }], 21 | 22 | actions: () => { 23 | const actions = []; 24 | actions.push({ 25 | type: 'modify', 26 | path: '../../app/i18n.js', 27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g, 28 | templateFile: './language/intl-locale-data.hbs', 29 | }); 30 | actions.push({ 31 | type: 'modify', 32 | path: '../../app/i18n.js', 33 | pattern: /([\n\s'[a-z]+',)(?!.*[\n\s'[a-z]+',)/g, 34 | templateFile: './language/app-locale.hbs', 35 | }); 36 | actions.push({ 37 | type: 'modify', 38 | path: '../../app/i18n.js', 39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g, 40 | templateFile: './language/translation-messages.hbs', 41 | }); 42 | actions.push({ 43 | type: 'modify', 44 | path: '../../app/i18n.js', 45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g, 46 | templateFile: './language/add-locale-data.hbs', 47 | }); 48 | actions.push({ 49 | type: 'modify', 50 | path: '../../app/i18n.js', 51 | pattern: /([a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),)/g, 52 | templateFile: './language/format-translation-messages.hbs', 53 | }); 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/translations/{{language}}.json', 57 | templateFile: './language/translations-json.hbs', 58 | abortOnFail: true, 59 | }); 60 | actions.push({ 61 | type: 'modify', 62 | path: '../../app/app.js', 63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g, 64 | templateFile: './language/polyfill-intl-locale.hbs', 65 | }); 66 | actions.push( 67 | () => { 68 | const cmd = 'npm run extract-intl'; 69 | exec(cmd, (err, result, stderr) => { 70 | if (err || stderr) { 71 | throw err || stderr; 72 | } 73 | process.stdout.write(result); 74 | }); 75 | } 76 | ); 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /client/internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /client/internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /client/internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /client/internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /client/internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | function reducerExists(comp) { 8 | try { 9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function sagasExists(comp) { 17 | try { 18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | function trimTemplateFile(template) { 26 | // Loads the template file and trims the whitespace and then returns the content as a string. 27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, ''); 28 | } 29 | 30 | module.exports = { 31 | description: 'Add a route', 32 | prompts: [{ 33 | type: 'input', 34 | name: 'component', 35 | message: 'Which component should the route show?', 36 | validate: value => { 37 | if ((/.+/).test(value)) { 38 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 39 | } 40 | 41 | return 'The path is required'; 42 | }, 43 | }, { 44 | type: 'input', 45 | name: 'path', 46 | message: 'Enter the path of the route.', 47 | default: '/about', 48 | validate: value => { 49 | if ((/.+/).test(value)) { 50 | return true; 51 | } 52 | 53 | return 'path is required'; 54 | }, 55 | }], 56 | 57 | // Add the route to the routes.js file above the error route 58 | // TODO smarter route adding 59 | actions: data => { 60 | const actions = []; 61 | if (reducerExists(data.component)) { 62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 63 | actions.push({ 64 | type: 'modify', 65 | path: '../../app/routes.js', 66 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 67 | template: trimTemplateFile('routeWithReducer.hbs'), 68 | }); 69 | } else { 70 | actions.push({ 71 | type: 'modify', 72 | path: '../../app/routes.js', 73 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 74 | template: trimTemplateFile('route.hbs'), 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /client/internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | System.import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /client/internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | System.import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | System.import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /client/internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /client/internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shelljs = require('shelljs'); 4 | var animateProgress = require('./helpers/progress'); 5 | var chalk = require('chalk'); 6 | var addCheckMark = require('./helpers/checkmark'); 7 | 8 | var progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback) // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(' ✓'); 8 | callback(); 9 | } 10 | 11 | if (!which('git')) { 12 | echo('Sorry, this script requires git'); 13 | exit(1); 14 | } 15 | 16 | if (!test('-e', 'internals/templates')) { 17 | echo('The example is deleted already.'); 18 | exit(1); 19 | } 20 | 21 | process.stdout.write('Cleanup started...'); 22 | 23 | // Cleanup components folder 24 | rm('-rf', 'app/components/*'); 25 | 26 | // Cleanup containers folder 27 | rm('-rf', 'app/containers/*'); 28 | mkdir('-p', 'app/containers/App'); 29 | mkdir('-p', 'app/containers/NotFoundPage'); 30 | mkdir('-p', 'app/containers/HomePage'); 31 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js'); 32 | cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js'); 33 | cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js'); 34 | cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js'); 35 | cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js'); 36 | 37 | // Handle Translations 38 | mkdir('-p', 'app/translations'); 39 | cp('internals/templates/translations/en.json', 40 | 'app/translations/en.json'); 41 | 42 | // move i18n file 43 | cp('internals/templates/i18n.js', 44 | 'app/i18n.js'); 45 | 46 | // Copy LanguageProvider 47 | mkdir('-p', 'app/containers/LanguageProvider'); 48 | mkdir('-p', 'app/containers/LanguageProvider/tests'); 49 | cp('internals/templates/languageProvider/actions.js', 50 | 'app/containers/LanguageProvider/actions.js'); 51 | cp('internals/templates/languageProvider/constants.js', 52 | 'app/containers/LanguageProvider/constants.js'); 53 | cp('internals/templates/languageProvider/languageProvider.js', 54 | 'app/containers/LanguageProvider/index.js'); 55 | cp('internals/templates/languageProvider/reducer.js', 56 | 'app/containers/LanguageProvider/reducer.js'); 57 | cp('internals/templates/languageProvider/selectors.js', 58 | 'app/containers/LanguageProvider/selectors.js'); 59 | cp('internals/templates/styles.css', 'app/containers/App/styles.css'); 60 | 61 | // Copy selectors 62 | mkdir('app/containers/App/tests'); 63 | cp('internals/templates/selectors.js', 64 | 'app/containers/App/selectors.js'); 65 | cp('internals/templates/selectors.test.js', 66 | 'app/containers/App/tests/selectors.test.js'); 67 | 68 | // Utils 69 | rm('-rf', 'app/utils'); 70 | mkdir('app/utils'); 71 | mkdir('app/utils/tests'); 72 | cp('internals/templates/asyncInjectors.js', 73 | 'app/utils/asyncInjectors.js'); 74 | cp('internals/templates/asyncInjectors.test.js', 75 | 'app/utils/tests/asyncInjectors.test.js'); 76 | 77 | // Replace the files in the root app/ folder 78 | cp('internals/templates/app.js', 'app/app.js'); 79 | cp('internals/templates/index.html', 'app/index.html'); 80 | cp('internals/templates/reducers.js', 'app/reducers.js'); 81 | cp('internals/templates/routes.js', 'app/routes.js'); 82 | cp('internals/templates/store.js', 'app/store.js'); 83 | cp('internals/templates/store.test.js', 'app/tests/store.test.js'); 84 | 85 | // Remove the templates folder 86 | rm('-rf', 'internals/templates'); 87 | 88 | process.stdout.write(' ✓'); 89 | 90 | // Commit the changes 91 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 92 | echo('\nError: Git commit failed'); 93 | exit(1); 94 | } 95 | 96 | echo('\nCleanup done. Happy Coding!!!'); 97 | -------------------------------------------------------------------------------- /client/internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | 3 | // No need to build the DLL in production 4 | if (process.env.NODE_ENV === 'production') { 5 | process.exit(0) 6 | } 7 | 8 | require('shelljs/global') 9 | 10 | const path = require('path') 11 | const fs = require('fs') 12 | const exists = fs.existsSync 13 | const writeFile = fs.writeFileSync 14 | 15 | const defaults = require('lodash/defaultsDeep') 16 | const pkg = require(path.join(process.cwd(), 'package.json')) 17 | const config = require('../config') 18 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults) 19 | const outputPath = path.join(process.cwd(), dllConfig.path) 20 | const dllManifestPath = path.join(outputPath, 'package.json') 21 | 22 | /** 23 | * I use node_modules/react-boilerplate-dlls by default just because 24 | * it isn't going to be version controlled and babel wont try to parse it. 25 | */ 26 | mkdir('-p', outputPath) 27 | 28 | echo('Building the Webpack DLL...') 29 | 30 | /** 31 | * Create a manifest so npm install doesnt warn us 32 | */ 33 | if (!exists(dllManifestPath)) { 34 | writeFile( 35 | dllManifestPath, 36 | JSON.stringify(defaults({ 37 | name: 'react-boilerplate-dlls', 38 | private: true, 39 | author: pkg.author, 40 | repository: pkg.repository, 41 | version: pkg.version 42 | }), null, 2), 43 | 44 | 'utf8' 45 | ) 46 | } 47 | 48 | // the BUILDING_DLL env var is set to avoid confusing the development environment 49 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js') 50 | -------------------------------------------------------------------------------- /client/internals/scripts/extract-intl.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * This script will extract the internationalization messages from all components 4 | and package them in the transalation json files in the translations file. 5 | */ 6 | const fs = require('fs'); 7 | const nodeGlob = require('glob'); 8 | const transform = require('babel-core').transform; 9 | 10 | const animateProgress = require('./helpers/progress'); 11 | const addCheckmark = require('./helpers/checkmark'); 12 | 13 | const pkg = require('../../package.json'); 14 | const i18n = require('../../app/i18n'); 15 | 16 | require('shelljs/global'); 17 | 18 | // Glob to match all js files except test files 19 | const FILES_TO_PARSE = 'app/**/!(*.test).js'; 20 | const locales = i18n.appLocales; 21 | 22 | const newLine = () => process.stdout.write('\n'); 23 | 24 | // Progress Logger 25 | let progress; 26 | const task = (message) => { 27 | progress = animateProgress(message); 28 | process.stdout.write(message); 29 | 30 | return (error) => { 31 | if (error) { 32 | process.stderr.write(error); 33 | } 34 | clearTimeout(progress); 35 | return addCheckmark(() => newLine()); 36 | } 37 | } 38 | 39 | // Wrap async functions below into a promise 40 | const glob = (pattern) => new Promise((resolve, reject) => { 41 | nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value))); 42 | }); 43 | 44 | const readFile = (fileName) => new Promise((resolve, reject) => { 45 | fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value))); 46 | }); 47 | 48 | const writeFile = (fileName, data) => new Promise((resolve, reject) => { 49 | fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value))); 50 | }); 51 | 52 | // Store existing translations into memory 53 | const oldLocaleMappings = []; 54 | const localeMappings = []; 55 | // Loop to run once per locale 56 | for (const locale of locales) { 57 | oldLocaleMappings[locale] = {}; 58 | localeMappings[locale] = {}; 59 | // File to store translation messages into 60 | const translationFileName = `app/translations/${locale}.json`; 61 | try { 62 | // Parse the old translation message JSON files 63 | const messages = JSON.parse(fs.readFileSync(translationFileName)); 64 | for (const message of messages) { 65 | oldLocaleMappings[locale][message.id] = message; 66 | } 67 | } catch (error) { 68 | if (error.code !== 'ENOENT') { 69 | process.stderr.write( 70 | `There was an error loading this translation file: ${translationFileName} 71 | \n${error}` 72 | ); 73 | } 74 | } 75 | } 76 | 77 | const extractFromFile = async (fileName) => { 78 | try { 79 | const code = await readFile(fileName); 80 | // Use babel plugin to extract instances where react-intl is used 81 | const { metadata: result } = await transform(code, { 82 | presets: pkg.babel.presets, 83 | plugins: [ 84 | ['react-intl'], 85 | ], 86 | }); 87 | for (const message of result['react-intl'].messages) { 88 | for (const locale of locales) { 89 | const oldLocaleMapping = oldLocaleMappings[locale][message.id]; 90 | // Merge old translations into the babel extracted instances where react-intl is used 91 | localeMappings[locale][message.id] = { 92 | id: message.id, 93 | description: message.description, 94 | defaultMessage: message.defaultMessage, 95 | message: (oldLocaleMapping && oldLocaleMapping.message) 96 | ? oldLocaleMapping.message 97 | : '', 98 | }; 99 | } 100 | } 101 | } catch (error) { 102 | process.stderr.write(`Error transforming file: ${fileName}\n${error}`); 103 | } 104 | }; 105 | 106 | (async function main() { 107 | const memoryTaskDone = task('Storing language files in memory'); 108 | const files = await glob(FILES_TO_PARSE); 109 | memoryTaskDone() 110 | 111 | const extractTaskDone = task('Run extraction on all files'); 112 | // Run extraction on all files that match the glob on line 16 113 | await Promise.all(files.map((fileName) => extractFromFile(fileName))); 114 | extractTaskDone() 115 | 116 | // Make the directory if it doesn't exist, especially for first run 117 | mkdir('-p', 'app/translations'); 118 | for (const locale of locales) { 119 | const translationFileName = `app/translations/${locale}.json`; 120 | 121 | try { 122 | const localeTaskDone = task( 123 | `Writing translation messages for ${locale} to: ${translationFileName}` 124 | ); 125 | 126 | // Sort the translation JSON file so that git diffing is easier 127 | // Otherwise the translation messages will jump around every time we extract 128 | let messages = Object.values(localeMappings[locale]).sort((a, b) => { 129 | a = a.id.toUpperCase(); 130 | b = b.id.toUpperCase(); 131 | return do { 132 | if (a < b) -1; 133 | else if (a > b) 1; 134 | else 0; 135 | }; 136 | }); 137 | 138 | // Write to file the JSON representation of the translation messages 139 | const prettified = `${JSON.stringify(messages, null, 2)}\n`; 140 | 141 | await writeFile(translationFileName, prettified); 142 | 143 | localeTaskDone(); 144 | } catch (error) { 145 | localeTaskDone( 146 | `There was an error saving this translation file: ${translationFileName} 147 | \n${error}` 148 | ); 149 | } 150 | } 151 | 152 | process.exit() 153 | }()); 154 | -------------------------------------------------------------------------------- /client/internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /client/internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | var readline = require('readline'); 2 | 3 | /** 4 | * Adds an animated progress indicator 5 | * 6 | * @param {string} message The message to write next to the indicator 7 | * @param {number} amountOfDots The amount of dots you want to animate 8 | */ 9 | function animateProgress(message, amountOfDots) { 10 | if (typeof amountOfDots !== 'number') { 11 | amountOfDots = 3; 12 | } 13 | 14 | var i = 0; 15 | return setInterval(function () { 16 | readline.cursorTo(process.stdout, 0); 17 | i = (i + 1) % (amountOfDots + 1); 18 | var dots = new Array(i + 1).join('.'); 19 | process.stdout.write(message + dots); 20 | }, 500); 21 | } 22 | 23 | module.exports = animateProgress; 24 | -------------------------------------------------------------------------------- /client/internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var exec = require('child_process').exec; 3 | exec('npm -v', function (err, stdout, stderr) { 4 | if (err) throw err; 5 | if (parseFloat(stdout) < 3) { 6 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 7 | process.exit(1); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /client/internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | var ngrok = require('ngrok'); 7 | var psi = require('psi'); 8 | var chalk = require('chalk'); 9 | 10 | log('\nStarting ngrok tunnel'); 11 | 12 | startTunnel(runPsi); 13 | 14 | function runPsi(url) { 15 | log('\nStarting PageSpeed Insights'); 16 | psi.output(url).then(function (err) { 17 | process.exit(0); 18 | }); 19 | } 20 | 21 | function startTunnel(cb) { 22 | ngrok.connect(3000, function (err, url) { 23 | if (err) { 24 | log(chalk.red('\nERROR\n' + err)); 25 | process.exit(0); 26 | } 27 | 28 | log('\nServing tunnel from: ' + chalk.magenta(url)); 29 | cb(url); 30 | }); 31 | } 32 | 33 | function log(string) { 34 | process.stdout.write(string); 35 | } 36 | -------------------------------------------------------------------------------- /client/internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 34 | }, 35 | 36 | webpack: webpackConfig, 37 | 38 | // make Webpack bundle generation quiet 39 | webpackMiddleware: { 40 | noInfo: true, 41 | stats: 'errors-only', 42 | }, 43 | 44 | customLaunchers: { 45 | ChromeTravis: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | coverageReporter: { 52 | dir: path.join(process.cwd(), 'coverage'), 53 | reporters: [ 54 | { type: 'lcov', subdir: 'lcov' }, 55 | { type: 'html', subdir: 'html' }, 56 | { type: 'text-summary' }, 57 | ], 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /client/internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | chai.use(chaiEnzyme()); 9 | 10 | // Include all .js files under `app`, except app.js, reducers.js, and routes.js. 11 | // This is for isparta code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /client/internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Transform our own .css files with PostCSS and CSS-modules 22 | test: /\.css$/, 23 | exclude: /node_modules/, 24 | loader: options.cssLoaders, 25 | }, { 26 | // Do not transform vendor's CSS with CSS-modules 27 | // The point is that they remain in global scope. 28 | // Since we require these CSS files in our JS or CSS files, 29 | // they will be a part of our compilation either way. 30 | // So, no need for ExtractTextPlugin here. 31 | test: /\.css$/, 32 | include: /node_modules/, 33 | loaders: ['style-loader', 'css-loader'], 34 | }, { 35 | test: /\.(eot|svg|ttf|woff|woff2)$/, 36 | loader: 'file-loader', 37 | }, { 38 | test: /\.(jpg|png|gif)$/, 39 | loaders: [ 40 | 'file-loader', 41 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 42 | ], 43 | }, { 44 | test: /\.html$/, 45 | loader: 'html-loader', 46 | }, { 47 | test: /\.json$/, 48 | loader: 'json-loader', 49 | }, { 50 | test: /\.(mp4|webm)$/, 51 | loader: 'url-loader?limit=10000', 52 | }], 53 | }, 54 | plugins: options.plugins.concat([ 55 | new webpack.ProvidePlugin({ 56 | // make fetch available 57 | fetch: 'exports?self.fetch!whatwg-fetch', 58 | }), 59 | 60 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 61 | // inside your code for any environment checks; UglifyJS will automatically 62 | // drop any unreachable code. 63 | new webpack.DefinePlugin({ 64 | 'process.env': { 65 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 66 | }, 67 | }), 68 | ]), 69 | postcss: () => options.postcssPlugins, 70 | resolve: { 71 | modules: ['app', 'node_modules'], 72 | extensions: [ 73 | '', 74 | '.js', 75 | '.jsx', 76 | '.react.js', 77 | ], 78 | mainFields: [ 79 | 'jsnext:main', 80 | 'main', 81 | ], 82 | }, 83 | devtool: options.devtool, 84 | target: 'web', // Make web variables accessible to webpack, e.g. window 85 | stats: false, // Don't show stats in the console 86 | progress: true, 87 | }); 88 | -------------------------------------------------------------------------------- /client/internals/webpack/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEVELOPMENT WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const webpack = require('webpack'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const logger = require('../../server/logger'); 10 | const cheerio = require('cheerio'); 11 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 12 | const dllPlugin = pkg.dllPlugin; 13 | 14 | // PostCSS plugins 15 | const cssnext = require('postcss-cssnext'); 16 | const postcssFocus = require('postcss-focus'); 17 | const postcssReporter = require('postcss-reporter'); 18 | 19 | const plugins = [ 20 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading 21 | new webpack.NoErrorsPlugin(), 22 | new HtmlWebpackPlugin({ 23 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js 24 | templateContent: templateContent(), // eslint-disable-line no-use-before-define 25 | }), 26 | ]; 27 | 28 | module.exports = require('./webpack.base.babel')({ 29 | // Add hot reloading in development 30 | entry: [ 31 | 'eventsource-polyfill', // Necessary for hot reloading with IE 32 | 'webpack-hot-middleware/client', 33 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js 34 | ], 35 | 36 | // Don't use hashes in dev mode for better performance 37 | output: { 38 | filename: '[name].js', 39 | chunkFilename: '[name].chunk.js', 40 | }, 41 | 42 | // Add development plugins 43 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define 44 | 45 | // Load the CSS in a style tag in development 46 | cssLoaders: 'style-loader!css-loader?localIdentName=[local]__[path][name]__[hash:base64:5]&modules&importLoaders=1&sourceMap!postcss-loader', 47 | 48 | // Process the CSS with PostCSS 49 | postcssPlugins: [ 50 | postcssFocus(), // Add a :focus to every :hover 51 | cssnext({ // Allow future CSS features to be used, also auto-prefixes the CSS... 52 | browsers: ['last 2 versions', 'IE > 10'], // ...based on this browser list 53 | }), 54 | postcssReporter({ // Posts messages from plugins to the terminal 55 | clearMessages: true, 56 | }), 57 | ], 58 | 59 | // Tell babel that we want to hot-reload 60 | babelQuery: { 61 | presets: ['react-hmre'], 62 | }, 63 | 64 | // Emit a source map for easier debugging 65 | devtool: 'cheap-module-eval-source-map', 66 | }); 67 | 68 | /** 69 | * Select which plugins to use to optimize the bundle's handling of 70 | * third party dependencies. 71 | * 72 | * If there is a dllPlugin key on the project's package.json, the 73 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin 74 | * will be used. 75 | * 76 | */ 77 | function dependencyHandlers() { 78 | // Don't do anything during the DLL Build step 79 | if (process.env.BUILDING_DLL) { return []; } 80 | 81 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin 82 | if (!dllPlugin) { 83 | return [ 84 | new webpack.optimize.CommonsChunkPlugin({ 85 | name: 'vendor', 86 | children: true, 87 | minChunks: 2, 88 | async: true, 89 | }), 90 | ]; 91 | } 92 | 93 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls'); 94 | 95 | /** 96 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json 97 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude 98 | * 99 | * @see https://github.com/mxstbr/react-boilerplate/tree/master/docs/general/webpack.md 100 | */ 101 | if (!dllPlugin.dlls) { 102 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json'); 103 | 104 | if (!fs.existsSync(manifestPath)) { 105 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`'); 106 | process.exit(0); 107 | } 108 | 109 | return [ 110 | new webpack.DllReferencePlugin({ 111 | context: process.cwd(), 112 | manifest: require(manifestPath), // eslint-disable-line global-require 113 | }), 114 | ]; 115 | } 116 | 117 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them. 118 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`)); 119 | 120 | return dllManifests.map((manifestPath) => { 121 | if (!fs.existsSync(path)) { 122 | if (!fs.existsSync(manifestPath)) { 123 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`); 124 | logger.error(`Expected to find it in ${dllPath}`); 125 | logger.error('Please run: npm run build:dll'); 126 | 127 | process.exit(0); 128 | } 129 | } 130 | 131 | return new webpack.DllReferencePlugin({ 132 | context: process.cwd(), 133 | manifest: require(manifestPath), // eslint-disable-line global-require 134 | }); 135 | }); 136 | } 137 | 138 | /** 139 | * We dynamically generate the HTML content in development so that the different 140 | * DLL Javascript files are loaded in script tags and available to our application. 141 | */ 142 | function templateContent() { 143 | const html = fs.readFileSync( 144 | path.resolve(process.cwd(), 'app/index.html') 145 | ).toString(); 146 | 147 | if (!dllPlugin) { return html; } 148 | 149 | const doc = cheerio(html); 150 | const body = doc.find('body'); 151 | const dllNames = !dllPlugin.dlls ? ['reactBoilerplateDeps'] : Object.keys(dllPlugin.dlls); 152 | 153 | dllNames.forEach(dllName => body.append(``)); 154 | 155 | return doc.toString(); 156 | } 157 | -------------------------------------------------------------------------------- /client/internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const dllPlugin = require('../config').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = { 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /client/internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const OfflinePlugin = require('offline-plugin'); 7 | 8 | // PostCSS plugins 9 | const cssnext = require('postcss-cssnext'); 10 | const postcssFocus = require('postcss-focus'); 11 | const postcssReporter = require('postcss-reporter'); 12 | 13 | module.exports = require('./webpack.base.babel')({ 14 | // In production, we skip all hot-reloading stuff 15 | entry: [ 16 | path.join(process.cwd(), 'app/app.js'), 17 | ], 18 | 19 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 20 | output: { 21 | filename: '[name].[chunkhash].js', 22 | chunkFilename: '[name].[chunkhash].chunk.js', 23 | }, 24 | 25 | // We use ExtractTextPlugin so we get a seperate CSS file instead 26 | // of the CSS being in the JS and injected as a style tag 27 | cssLoaders: ExtractTextPlugin.extract( 28 | 'style-loader', 29 | 'css-loader?modules&-autoprefixer&importLoaders=1!postcss-loader' 30 | ), 31 | 32 | // In production, we minify our CSS with cssnano 33 | postcssPlugins: [ 34 | postcssFocus(), 35 | cssnext({ 36 | browsers: ['last 2 versions', 'IE > 10'], 37 | }), 38 | postcssReporter({ 39 | clearMessages: true, 40 | }), 41 | ], 42 | plugins: [ 43 | new webpack.optimize.CommonsChunkPlugin({ 44 | name: 'vendor', 45 | children: true, 46 | minChunks: 2, 47 | async: true, 48 | }), 49 | 50 | // OccurrenceOrderPlugin is needed for long-term caching to work properly. 51 | // See http://mxs.is/googmv 52 | new webpack.optimize.OccurrenceOrderPlugin(true), 53 | 54 | // Merge all duplicate modules 55 | new webpack.optimize.DedupePlugin(), 56 | 57 | // Minify and optimize the JavaScript 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, // ...but do not show warnings in the console (there is a lot of them) 61 | }, 62 | }), 63 | 64 | // Minify and optimize the index.html 65 | new HtmlWebpackPlugin({ 66 | template: 'app/index.html', 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeRedundantAttributes: true, 71 | useShortDoctype: true, 72 | removeEmptyAttributes: true, 73 | removeStyleLinkTypeAttributes: true, 74 | keepClosingSlash: true, 75 | minifyJS: true, 76 | minifyCSS: true, 77 | minifyURLs: true, 78 | }, 79 | inject: true, 80 | }), 81 | 82 | // Extract the CSS into a seperate file 83 | new ExtractTextPlugin('[name].[contenthash].css'), 84 | 85 | // Put it in the end to capture all the HtmlWebpackPlugin's 86 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 87 | new OfflinePlugin({ 88 | relativePaths: false, 89 | publicPath: '/', 90 | 91 | // No need to cache .htaccess. See http://mxs.is/googmp, 92 | // this is applied before any match in `caches` section 93 | excludes: ['.htaccess'], 94 | 95 | caches: { 96 | main: [':rest:'], 97 | 98 | // All chunks marked as `additional`, loaded after main section 99 | // and do not prevent SW to install. Change to `optional` if 100 | // do not want them to be preloaded at all (cached only when first loaded) 101 | additional: ['*.chunk.js'], 102 | }, 103 | 104 | // Removes warning for about `additional` section usage 105 | safeToUseOptionalCaches: true, 106 | 107 | AppCache: false, 108 | }), 109 | ], 110 | }); 111 | -------------------------------------------------------------------------------- /client/internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const modules = [ 8 | 'app', 9 | 'node_modules', 10 | ]; 11 | 12 | module.exports = { 13 | devtool: 'inline-source-map', 14 | isparta: { 15 | babel: { 16 | presets: ['es2015', 'react', 'stage-0'], 17 | }, 18 | }, 19 | module: { 20 | // Some libraries don't like being run through babel. 21 | // If they gripe, put them here. 22 | noParse: [ 23 | /node_modules(\\|\/)sinon/, 24 | /node_modules(\\|\/)acorn/, 25 | ], 26 | preLoaders: [ 27 | { test: /\.js$/, 28 | loader: 'isparta', 29 | include: path.resolve('app/'), 30 | }, 31 | ], 32 | loaders: [ 33 | { test: /\.json$/, loader: 'json-loader' }, 34 | { test: /\.css$/, loader: 'null-loader' }, 35 | 36 | // sinon.js--aliased for enzyme--expects/requires global vars. 37 | // imports-loader allows for global vars to be injected into the module. 38 | // See https://github.com/webpack/webpack/issues/304 39 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 40 | loader: 'imports?define=>false,require=>false', 41 | }, 42 | { test: /\.js$/, 43 | loader: 'babel', 44 | exclude: [/node_modules/], 45 | }, 46 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 47 | loader: 'null-loader', 48 | }, 49 | ], 50 | }, 51 | 52 | plugins: [ 53 | 54 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 55 | // inside your code for any environment checks; UglifyJS will automatically 56 | // drop any unreachable code. 57 | new webpack.DefinePlugin({ 58 | 'process.env': { 59 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 60 | }, 61 | })], 62 | 63 | // Some node_modules pull in Node-specific dependencies. 64 | // Since we're running in a browser we have to stub them out. See: 65 | // https://webpack.github.io/docs/configuration.html#node 66 | // https://github.com/webpack/node-libs-browser/tree/master/mock 67 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 68 | node: { 69 | fs: 'empty', 70 | child_process: 'empty', 71 | net: 'empty', 72 | tls: 'empty', 73 | }, 74 | 75 | // required for enzyme to work properly 76 | externals: { 77 | jsdom: 'window', 78 | 'react/addons': true, 79 | 'react/lib/ExecutionEnvironment': true, 80 | 'react/lib/ReactContext': 'window', 81 | }, 82 | resolve: { 83 | modulesDirectories: modules, 84 | modules, 85 | alias: { 86 | // required for enzyme to work properly 87 | sinon: 'sinon/pkg/sinon', 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /client/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const app = express(); 12 | 13 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 14 | // app.use('/api', myApi); 15 | 16 | // In production we need to pass these values in instead of relying on webpack 17 | setup(app, { 18 | outputPath: resolve(process.cwd(), 'build'), 19 | publicPath: '/', 20 | }); 21 | 22 | // get the intended port number, use port 3000 if not provided 23 | const port = argv.port || process.env.PORT || 3000; 24 | 25 | // Start your app. 26 | app.listen(port, (err) => { 27 | if (err) { 28 | return logger.error(err.message); 29 | } 30 | 31 | // Connect to ngrok in dev mode 32 | if (ngrok) { 33 | ngrok.connect(port, (innerErr, url) => { 34 | if (innerErr) { 35 | return logger.error(innerErr); 36 | } 37 | 38 | logger.appStarted(port, url); 39 | }); 40 | } else { 41 | logger.appStarted(port); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /client/server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: err => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log(`Server started ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /client/server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | -------------------------------------------------------------------------------- /server/CleanCo.Common/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Common/IRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanCo.Common 4 | { 5 | public interface IRecord 6 | { 7 | Guid Id { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /server/CleanCo.Common/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace CleanCo.Common 6 | { 7 | public interface IRepository 8 | { 9 | List RetrieveAll(); 10 | List Find(string filter); 11 | T RetrieveById(Guid id); 12 | void Save(T record); 13 | void Delete(Guid id); 14 | int RecordCount{ get; } 15 | } 16 | } -------------------------------------------------------------------------------- /server/CleanCo.Common/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0" 6 | }, 7 | 8 | "frameworks": { 9 | "netstandard1.6": { 10 | "imports": "dnxcore50" 11 | } 12 | }, 13 | 14 | "tooling": { 15 | "defaultNamespace": "CleanCo.Common" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.Catalog/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.Catalog/Catalog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CleanCo.Common; 4 | using CleanCo.Products; 5 | 6 | namespace CleanCo.Ecomm 7 | { 8 | public class Catalog { 9 | 10 | private IRepository repo; 11 | 12 | // TODO: remove this when repo.Find is implemented. 13 | private List names = new List(); 14 | public Catalog(IRepository repo) 15 | { 16 | if (repo == null) throw new ArgumentNullException("repo can't be null"); 17 | this.repo = repo; 18 | seedRepo(); 19 | } 20 | 21 | public void AddProduct(Product product) 22 | { 23 | if(names.Contains(product.Name)) 24 | { 25 | throw new ArgumentOutOfRangeException("That name already exists!"); 26 | } 27 | repo.Save(product); 28 | // TODO: remove this when repo.Find is implemented. 29 | names.Add(product.Name); 30 | } 31 | 32 | public List GetProducts() 33 | { 34 | return repo.RetrieveAll(); 35 | } 36 | 37 | public Product GetProductById(Guid id) 38 | { 39 | return repo.RetrieveById(id); 40 | } 41 | 42 | private void seedRepo() 43 | { 44 | if (this.repo.RecordCount == 0) { 45 | Product lumberjack = new Product("Lumberjack Lather Soap"); 46 | lumberjack.Price = 6.95m; 47 | lumberjack.PrimaryImage = new Uri("http://ft.b5z.net/i/u/6131027/i/ec/lumberjack_lather1.jpg"); 48 | lumberjack.AdditionalImages = new List(); 49 | lumberjack.AdditionalImages.Add(new Uri("http://ft.b5z.net/zirw/805/i/u/6131027/i/ec/lumberjack_lather2.jpg")); 50 | lumberjack.Description = "Cedarwood, Fir Needle, Eucalyptus, Aloe, Black Spruce, Agave Root & Yellow Dock Root\n\n" + 51 | "An earthy woodsy soap with Cedarwood, Fir Needle, Eucalyptus, Aloe, Black Spruce, " + 52 | "Agave Root & Yellow Dock Root.\n\n" + 53 | "A soap that's as tough as wood. Because you want to be a lumberjack and you're ok. " + 54 | "A 2013 contest winning soap from Greg in Seattle, Washington."; 55 | this.AddProduct(lumberjack); 56 | 57 | Product matcha = new Product("Matcha Matcha Man"); 58 | matcha.Price = 6.95m; 59 | matcha.PrimaryImage = new Uri("http://ft.b5z.net/i/u/6131027/i/matcha_matcha_man_1.jpg"); 60 | matcha.AdditionalImages = new List(); 61 | matcha.AdditionalImages.Add(new Uri("http://ft.b5z.net/zirw/805/i/u/6131027/i/matcha_matcha_man_2.jpg")); 62 | matcha.Description = "Matcha Tea Powder, Willow Charcoal, French Green Clay, Jasmine, Tea Tree, Ylang Ylang & Orange\n\n" + 63 | "Angelica in Los Angeles, CA knows what she like in a man -- a real man, a manly man, a man who likes healthy moisturized " + 64 | "skin and a smooth firm bar of soap with lots of antioxidants. That's why her Matcha Matcha Man is one of our " + 65 | "2016 soap contest winners!"; 66 | this.AddProduct(matcha); 67 | 68 | Product boardwalk = new Product("Boardwalk Hempire"); 69 | boardwalk.Price = 6.95m; 70 | boardwalk.PrimaryImage = new Uri("http://ft.b5z.net/i/u/6131027/i/boardwalk_hempire2_1.jpg"); 71 | boardwalk.AdditionalImages = new List(); 72 | boardwalk.AdditionalImages.Add(new Uri("http://soaptopia.com/i/u/6131027/i/boardwalk_hempire2_2.jpg")); 73 | boardwalk.Description = "A wood patchouli blend with ruby red grapefruit, lemon & hemp oil\n\n" + 74 | "Equally inspired by the lush green pacific slopes of Humbolt and the drum circles of Venice Beach, " + 75 | "this soap has a grounded, earthy scent with fresh, crisp, light notes."; 76 | this.AddProduct(boardwalk); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.Catalog/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0", 6 | "CleanCo.Common": { 7 | "target": "project" 8 | }, 9 | "CleanCo.Products": { 10 | "target": "project" 11 | } 12 | }, 13 | 14 | "frameworks": { 15 | "netstandard1.6": { 16 | "imports": "dnxcore50" 17 | } 18 | }, 19 | 20 | "tooling": { 21 | "defaultNamespace": "CleanCo.Ecomm.Catalog" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RepoAdapter/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RepoAdapter/Repository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using CleanCo.JsonAdapter; 5 | using CleanCo.Common; 6 | 7 | namespace CleanCo.RepoAdapter 8 | { 9 | 10 | public class Repository: IRepository where T: IRecord 11 | { 12 | private static IDictionary store; 13 | 14 | static Repository() 15 | { 16 | store = new Dictionary(); 17 | } 18 | 19 | public void Delete(Guid id) 20 | { 21 | store.Remove(id); 22 | } 23 | public List Find(string filter) 24 | { 25 | throw new NotImplementedException("Test me first"); 26 | } 27 | public List RetrieveAll() 28 | { 29 | List list = new List(); 30 | foreach (string item in store.Values) 31 | { 32 | T itemAsT = JsonHelper.FromJson(item); 33 | list.Add(itemAsT); 34 | } 35 | return list; 36 | } 37 | public T RetrieveById(Guid id) 38 | { 39 | validateId(id); 40 | if(store.ContainsKey(id)) 41 | { 42 | string jsonRecord = store[id]; 43 | T record = JsonHelper.FromJson(jsonRecord); 44 | return record; 45 | } 46 | return default(T); 47 | 48 | } 49 | public List RetrieveNext(Guid last, int max) 50 | { 51 | throw new NotImplementedException("Test me first"); 52 | } 53 | public void Save(T record) 54 | { 55 | validateRecord(record); 56 | if(store.ContainsKey(record.Id)) 57 | { 58 | add(record); 59 | } 60 | else 61 | { 62 | update(record); 63 | } 64 | } 65 | 66 | 67 | private void add(T record) 68 | { 69 | string recordAsJson = JsonHelper.ToJson(record); 70 | store.Add(record.Id, recordAsJson); 71 | } 72 | private void update(T record) 73 | { 74 | string recordAsJson = JsonHelper.ToJson(record); 75 | store[record.Id] = recordAsJson; 76 | } 77 | private void validateRecord(T record) 78 | { 79 | if(record == null) 80 | { 81 | throw new ArgumentNullException("record can not be null"); 82 | } 83 | validateId(record.Id); 84 | } 85 | private void validateId(Guid id) 86 | { 87 | if(id == Guid.Empty) 88 | { 89 | throw new ArgumentException("Id can not be empty"); 90 | } 91 | } 92 | 93 | public int RecordCount 94 | { 95 | get 96 | { 97 | return store.Count; 98 | } 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RepoAdapter/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0", 6 | "Newtonsoft.Json": "9.0.1", 7 | "CleanCo.Common": { 8 | "target": "project" 9 | }, 10 | "CleanCo.JsonAdapter": { 11 | "target": "project" 12 | } 13 | }, 14 | 15 | "frameworks": { 16 | "netstandard1.6": { 17 | "imports": "dnxcore50" 18 | } 19 | }, 20 | 21 | "tooling": { 22 | "defaultNamespace": "Clean.RepoAdapter" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/Controllers/CatalogController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | using CleanCo.RepoAdapter; 8 | using CleanCo.Products; 9 | using CleanCo.Common; 10 | using CleanCo.Ecomm; 11 | 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace CleanCo.Ecomm.RestAdapter 15 | { 16 | [Route("api/[controller]")] 17 | public class CatalogController : Controller 18 | { 19 | // GET api/catalog 20 | [HttpGet] 21 | public IEnumerable Get() 22 | { 23 | Catalog catalog; 24 | IRepository repo; 25 | 26 | repo = new Repository(); 27 | catalog = new Catalog(repo); 28 | return (IEnumerable)catalog.GetProducts(); 29 | } 30 | 31 | // GET api/values/5 32 | [HttpGet("{id}")] 33 | public IActionResult Get(string id) 34 | { 35 | Catalog catalog; 36 | IRepository repo; 37 | Product product; 38 | 39 | repo = new Repository(); 40 | catalog = new Catalog(repo); 41 | 42 | product = (Product)catalog.GetProductById(Guid.Parse(id)); 43 | if(product == null) 44 | { 45 | return NotFound(); 46 | } 47 | return new ObjectResult(product); 48 | } 49 | 50 | // POST api/values 51 | [HttpPost] 52 | public void Post([FromBody]string value) 53 | { 54 | 55 | } 56 | 57 | // PUT api/values/5 58 | [HttpPut("{id}")] 59 | public void Put(int id, [FromBody]string value) 60 | { 61 | } 62 | 63 | // DELETE api/values/5 64 | [HttpDelete("{id}")] 65 | public void Delete(int id) 66 | { 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:latest 2 | 3 | COPY . /app 4 | 5 | WORKDIR /app 6 | 7 | RUN ["dotnet", "restore"] 8 | 9 | RUN ["dotnet", "build"] 10 | 11 | EXPOSE 5000/tcp 12 | 13 | CMD ["dotnet", "run", "--server.urls", "http://*:5000"] 14 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.Extensions.Configuration; 9 | 10 | namespace Clean.Adapters.Rest 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | var config = new ConfigurationBuilder() 17 | .AddCommandLine(args) 18 | .AddEnvironmentVariables(prefix: "ASPNETCORE_") 19 | .Build(); 20 | 21 | var host = new WebHostBuilder() 22 | .UseConfiguration(config) 23 | .UseKestrel() 24 | .UseContentRoot(Directory.GetCurrentDirectory()) 25 | .UseIISIntegration() 26 | .UseStartup() 27 | .Build(); 28 | 29 | host.Run(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:1479/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Clean.Adapters.Rest": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to ASP.NET Core 2 | 3 | We've made some big updates in this release, so it’s **important** that you spend a few minutes to learn what’s new. 4 | 5 | You've created a new ASP.NET Core project. [Learn what's new](https://go.microsoft.com/fwlink/?LinkId=518016) 6 | 7 | ## This application consists of: 8 | 9 | * Sample pages using ASP.NET Core MVC 10 | * [Bower](https://go.microsoft.com/fwlink/?LinkId=518004) for managing client-side libraries 11 | * Theming using [Bootstrap](https://go.microsoft.com/fwlink/?LinkID=398939) 12 | 13 | ## How to 14 | 15 | * [Add a Controller and View](https://go.microsoft.com/fwlink/?LinkID=398600) 16 | * [Add an appsetting in config and access it in app.](https://go.microsoft.com/fwlink/?LinkID=699562) 17 | * [Manage User Secrets using Secret Manager.](https://go.microsoft.com/fwlink/?LinkId=699315) 18 | * [Use logging to log a message.](https://go.microsoft.com/fwlink/?LinkId=699316) 19 | * [Add packages using NuGet.](https://go.microsoft.com/fwlink/?LinkId=699317) 20 | * [Add client packages using Bower.](https://go.microsoft.com/fwlink/?LinkId=699318) 21 | * [Target development, staging or production environment.](https://go.microsoft.com/fwlink/?LinkId=699319) 22 | 23 | ## Overview 24 | 25 | * [Conceptual overview of what is ASP.NET Core](https://go.microsoft.com/fwlink/?LinkId=518008) 26 | * [Fundamentals of ASP.NET Core such as Startup and middleware.](https://go.microsoft.com/fwlink/?LinkId=699320) 27 | * [Working with Data](https://go.microsoft.com/fwlink/?LinkId=398602) 28 | * [Security](https://go.microsoft.com/fwlink/?LinkId=398603) 29 | * [Client side development](https://go.microsoft.com/fwlink/?LinkID=699321) 30 | * [Develop on different platforms](https://go.microsoft.com/fwlink/?LinkID=699322) 31 | * [Read more on the documentation site](https://go.microsoft.com/fwlink/?LinkID=699323) 32 | 33 | ## Run & Deploy 34 | 35 | * [Run your app](https://go.microsoft.com/fwlink/?LinkID=517851) 36 | * [Run tools such as EF migrations and more](https://go.microsoft.com/fwlink/?LinkID=517853) 37 | * [Publish to Microsoft Azure Web Apps](https://go.microsoft.com/fwlink/?LinkID=398609) 38 | 39 | We would love to hear your [feedback](https://go.microsoft.com/fwlink/?LinkId=518015) 40 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Clean.Adapters.Rest 12 | { 13 | public class Startup 14 | { 15 | public Startup(IHostingEnvironment env) 16 | { 17 | var builder = new ConfigurationBuilder() 18 | .SetBasePath(env.ContentRootPath) 19 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 20 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 21 | .AddEnvironmentVariables(); 22 | Configuration = builder.Build(); 23 | } 24 | 25 | public IConfigurationRoot Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | // Add framework services. 31 | services.AddCors(); 32 | services.AddMvc(); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 37 | { 38 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 39 | loggerFactory.AddDebug(); 40 | 41 | app.UseCors(builder => 42 | builder.WithOrigins("http://localhost:3000") 43 | .AllowAnyHeader()); 44 | app.UseMvc(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.NETCore.App": { 4 | "version": "1.0.0", 5 | "type": "platform" 6 | }, 7 | "Microsoft.AspNetCore.Mvc": "1.0.1", 8 | "Microsoft.AspNetCore.Routing": "1.0.1", 9 | "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", 10 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", 11 | "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", 12 | "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", 13 | "Microsoft.Extensions.Configuration.Json": "1.0.0", 14 | "Microsoft.Extensions.Configuration.CommandLine": "1.0.0", 15 | "Microsoft.Extensions.Logging": "1.0.0", 16 | "Microsoft.Extensions.Logging.Console": "1.0.0", 17 | "Microsoft.Extensions.Logging.Debug": "1.0.0", 18 | "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", 19 | "CleanCo.Ecomm.RepoAdapter": { 20 | "target": "project" 21 | }, 22 | "CleanCo.Common": { 23 | "target": "project" 24 | }, 25 | "CleanCo.Products": { 26 | "target": "project" 27 | }, 28 | "CleanCo.Ecomm.Catalog": { 29 | "target": "project" 30 | } 31 | }, 32 | 33 | "tools": { 34 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" 35 | }, 36 | 37 | "frameworks": { 38 | "netcoreapp1.0": { 39 | "imports": [ 40 | "dotnet5.6", 41 | "portable-net45+win8" 42 | ] 43 | } 44 | }, 45 | 46 | "buildOptions": { 47 | "emitEntryPoint": true, 48 | "preserveCompilationContext": true 49 | }, 50 | 51 | "runtimeOptions": { 52 | "configProperties": { 53 | "System.GC.Server": true 54 | } 55 | }, 56 | 57 | "publishOptions": { 58 | "include": [ 59 | "wwwroot", 60 | "Views", 61 | "Areas/**/Views", 62 | "appsettings.json", 63 | "web.config" 64 | ] 65 | }, 66 | 67 | "scripts": { 68 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] 69 | }, 70 | 71 | "tooling": { 72 | "defaultNamespace": "Clean.Adapters.Rest" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/CleanCo.Ecomm.RestAdapter/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CleanCo.JsonAdapter 4 | { 5 | public static class JsonHelper 6 | { 7 | public static string ToJson(T obj) 8 | { 9 | return JsonConvert.SerializeObject(obj); 10 | } 11 | 12 | public static T FromJson(string json) 13 | { 14 | return JsonConvert.DeserializeObject(json); 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/Clean.Adapters.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-mastery/clean-architecture/8da65aafa1c41272e7d8fb46b77558dbc9bf5d00/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/Clean.Adapters.Json.dll -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/Clean.Adapters.Json.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-mastery/clean-architecture/8da65aafa1c41272e7d8fb46b77558dbc9bf5d00/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/Clean.Adapters.Json.pdb -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-mastery/clean-architecture/8da65aafa1c41272e7d8fb46b77558dbc9bf5d00/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.dll -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-mastery/clean-architecture/8da65aafa1c41272e7d8fb46b77558dbc9bf5d00/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.pdb -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/obj/Debug/netstandard1.6/.IncrementalCache: -------------------------------------------------------------------------------- 1 | {"inputs":["/Users/bill/code/clean-architecture/server/CleanCo.JsonAdapter/project.json","/Users/bill/code/clean-architecture/server/CleanCo.JsonAdapter/project.lock.json","/Users/bill/code/clean-architecture/server/CleanCo.JsonAdapter/JsonHelper.cs"],"outputs":["/Users/bill/code/clean-architecture/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.dll","/Users/bill/code/clean-architecture/server/CleanCo.JsonAdapter/bin/Debug/netstandard1.6/CleanCo.JsonAdapter.pdb"],"buildArguments":{"version-suffix":null}} -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/obj/Debug/netstandard1.6/.SDKVersion: -------------------------------------------------------------------------------- 1 | 635cf40e58ede8a53e8b9555e19a6e1ccd6f9fbe 2 | 1.0.0-preview2-003131 3 | 4 | osx.10.12-x64 -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/obj/Debug/netstandard1.6/dotnet-compile.assemblyinfo.cs: -------------------------------------------------------------------------------- 1 | // This file has been auto generated. 2 | [assembly:System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] 3 | [assembly:System.Reflection.AssemblyVersionAttribute("1.0.0.0")] 4 | [assembly:System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] 5 | [assembly:System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v1.6")] -------------------------------------------------------------------------------- /server/CleanCo.JsonAdapter/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0", 6 | "Newtonsoft.Json": "9.0.1" 7 | }, 8 | 9 | "frameworks": { 10 | "netstandard1.6": { 11 | "imports": "dnxcore50" 12 | } 13 | }, 14 | 15 | "tooling": { 16 | "defaultNamespace": "CleanCo.JsonAdapter" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/CleanCo.Products/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Products/Product.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CleanCo.Common; 4 | 5 | namespace CleanCo.Products 6 | { 7 | public class Product : IRecord 8 | { 9 | private Guid id; 10 | private decimal price; 11 | 12 | public Product(string name, Guid id = new Guid()) 13 | { 14 | validateName(name); 15 | 16 | this.Name = name.Trim(); 17 | this.Id = (id == Guid.Empty) ? Guid.NewGuid() : id; 18 | 19 | } 20 | 21 | private void validateName(string name) 22 | { 23 | if(name == null) 24 | { 25 | throw new ArgumentNullException("name can not be null"); 26 | } 27 | 28 | if(String.IsNullOrEmpty(name.Trim())) 29 | { 30 | throw new ArgumentException("name can not be empty"); 31 | } 32 | } 33 | 34 | private void validatePrice(decimal price) 35 | { 36 | if(price < 0.00m) 37 | { 38 | throw new ArgumentOutOfRangeException("price must be greater than 0"); 39 | } 40 | } 41 | 42 | private void validateId(Guid id) 43 | { 44 | if(id == Guid.Empty) 45 | { 46 | throw new ArgumentException("id can not be an empty Guid"); 47 | } 48 | } 49 | 50 | public Guid Id 51 | { 52 | get { return id; } 53 | set 54 | { 55 | validateId(value); 56 | id = value; 57 | } 58 | } 59 | 60 | public string Name { get; } 61 | 62 | public decimal Price 63 | { 64 | get 65 | { 66 | return price; 67 | } 68 | set 69 | { 70 | validatePrice(value); 71 | price = value; 72 | } 73 | } 74 | public string Description { get; set; } 75 | public Uri PrimaryImage { get; set; } 76 | public List AdditionalImages { get; set; } 77 | public Dictionary> Attributes { get; set; } 78 | } 79 | } -------------------------------------------------------------------------------- /server/CleanCo.Products/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0", 6 | "CleanCo.Common": { 7 | "target": "project" 8 | } 9 | }, 10 | 11 | "frameworks": { 12 | "netstandard1.6": { 13 | "imports": "dnxcore50" 14 | } 15 | }, 16 | 17 | "tooling": { 18 | "defaultNamespace": "CleanCo.Products" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/CleanCo.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | node_modules/ 185 | orleans.codegen.cs 186 | 187 | # RIA/Silverlight projects 188 | Generated_Code/ 189 | 190 | # Backup & report files from converting an old project file 191 | # to a newer Visual Studio version. Backup files are not needed, 192 | # because we have git ;-) 193 | _UpgradeReport_Files/ 194 | Backup*/ 195 | UpgradeLog*.XML 196 | UpgradeLog*.htm 197 | 198 | # SQL Server files 199 | *.mdf 200 | *.ldf 201 | 202 | # Business Intelligence projects 203 | *.rdl.data 204 | *.bim.layout 205 | *.bim_*.settings 206 | 207 | # Microsoft Fakes 208 | FakesAssemblies/ 209 | 210 | # GhostDoc plugin setting file 211 | *.GhostDoc.xml 212 | 213 | # Node.js Tools for Visual Studio 214 | .ntvs_analysis.dat 215 | 216 | # Visual Studio 6 build log 217 | *.plg 218 | 219 | # Visual Studio 6 workspace options file 220 | *.opt 221 | 222 | # Visual Studio LightSwitch build output 223 | **/*.HTMLClient/GeneratedArtifacts 224 | **/*.DesktopClient/GeneratedArtifacts 225 | **/*.DesktopClient/ModelManifest.xml 226 | **/*.Server/GeneratedArtifacts 227 | **/*.Server/ModelManifest.xml 228 | _Pvt_Extensions 229 | 230 | # Paket dependency manager 231 | .paket/paket.exe 232 | 233 | # FAKE - F# Make 234 | .fake/ 235 | -------------------------------------------------------------------------------- /server/CleanCo.Tests/AdapterTests/RepoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | using CleanCo.RepoAdapter; 8 | using CleanCo.Common; 9 | 10 | namespace CleanCo.Tests 11 | { 12 | // see example explanation on xUnit.net website: 13 | // https://xunit.github.io/docs/getting-started-dotnet-core.html 14 | public class RepoTests 15 | { 16 | private IRepository repo = new Repository(); 17 | 18 | [Fact] 19 | public void Should_Save_A_New_Entity() 20 | { 21 | DummyRecord savedRecord; 22 | DummyRecord dummyRecord = new DummyRecord("save me"); 23 | 24 | repo.Save(dummyRecord); 25 | savedRecord = repo.RetrieveById(dummyRecord.Id); 26 | 27 | Assert.NotNull(savedRecord); 28 | Assert.IsType(dummyRecord.GetType(), savedRecord); 29 | Assert.Equal(dummyRecord.Id, savedRecord.Id); 30 | Assert.Equal(dummyRecord.Name, savedRecord.Name); 31 | 32 | repo.Delete(dummyRecord.Id); 33 | } 34 | 35 | [Fact] 36 | public void Should_Delete_An_Entity() 37 | { 38 | DummyRecord dummyRecord = new DummyRecord("delete me"); 39 | DummyRecord deletedRecord; 40 | 41 | repo.Save(dummyRecord); 42 | repo.Delete(dummyRecord.Id); 43 | deletedRecord = repo.RetrieveById(dummyRecord.Id); 44 | 45 | Assert.Null(deletedRecord); 46 | } 47 | 48 | [Fact] 49 | public void Should_Return_A_List_Of_Entities() 50 | { 51 | List dummyRecords; 52 | DummyRecord one = new DummyRecord("One"); 53 | DummyRecord two = new DummyRecord("Two"); 54 | DummyRecord three = new DummyRecord("Three"); 55 | 56 | List savedRecords; 57 | DummyRecord savedRecordOne; 58 | DummyRecord savedRecordTwo; 59 | DummyRecord savedRecordThree; 60 | 61 | dummyRecords = new List(); 62 | dummyRecords.Add(one); 63 | dummyRecords.Add(two); 64 | dummyRecords.Add(three); 65 | dummyRecords.ForEach((dummyRecord) => repo.Save(dummyRecord)); 66 | 67 | Assert.Equal(repo.RecordCount, dummyRecords.Count); 68 | 69 | savedRecords = repo.RetrieveAll(); 70 | savedRecordOne = savedRecords.Find((savedRecord) => savedRecord.Id == one.Id); 71 | savedRecordTwo = savedRecords.Find((savedRecord) => savedRecord.Id == two.Id); 72 | savedRecordThree = savedRecords.Find((savedRecord) => savedRecord.Id == three.Id); 73 | 74 | Assert.Equal(savedRecordOne.Name, one.Name); 75 | Assert.Equal(savedRecordTwo.Name, two.Name); 76 | Assert.Equal(savedRecordThree.Name, three.Name); 77 | 78 | dummyRecords.ForEach((entity) => repo.Delete(entity.Id)); 79 | } 80 | 81 | [Fact] 82 | public void Should_Return_A_Single_Entity() 83 | { 84 | DummyRecord one = new DummyRecord("One"); 85 | repo.Save(one); 86 | DummyRecord saved = repo.RetrieveById(one.Id); 87 | Assert.Equal(saved.Id, one.Id); 88 | Assert.Equal(saved.Name, one.Name); 89 | repo.Delete(one.Id); 90 | } 91 | 92 | [Fact] 93 | public void Should_Return_An_Empty_List() 94 | { 95 | List noRecords = repo.RetrieveAll(); 96 | Assert.NotNull(noRecords); 97 | Assert.Empty(noRecords); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /server/CleanCo.Tests/EntityTests/ProductTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using CleanCo.Products; 4 | 5 | namespace CleanCo.Tests 6 | { 7 | // see example explanation on xUnit.net website: 8 | // https://xunit.github.io/docs/getting-started-dotnet-core.html 9 | public class ProductTest 10 | { 11 | 12 | [Fact] 13 | public void Should_Automatically_Set_A_Valid_Id() 14 | { 15 | Product testProduct = new Product("Soap"); 16 | 17 | Assert.NotNull(testProduct.Id); 18 | Assert.True(testProduct.Id != Guid.Empty); 19 | } 20 | 21 | [Fact] 22 | public void Should_Manually_Set_A_Valid_Id() 23 | { 24 | Guid id = Guid.NewGuid(); 25 | Product testProduct = new Product("Soap", id); 26 | 27 | Assert.Equal(id, testProduct.Id); 28 | } 29 | 30 | [Fact] 31 | public void Should_Throw_On_Setting_An_Invalid_Id() 32 | { 33 | Product testProduct = new Product("Soap"); 34 | Assert.Throws(() => testProduct.Id = Guid.Empty); 35 | } 36 | 37 | [Theory, 38 | InlineData("good"), 39 | InlineData("still good"), 40 | InlineData(" also good ")] 41 | public void Should_Construct_A_Valid_Product(string name) 42 | { 43 | Product testProduct = new Product(name); 44 | 45 | Assert.Equal(testProduct.Name, name.Trim()); 46 | } 47 | 48 | [Theory, 49 | InlineData(""), 50 | InlineData(" ")] 51 | public void Should_Throw_On_Constructing_With_An_Empty_Name(string invalidName) 52 | { 53 | Assert.Throws(() => new Product(invalidName)); 54 | } 55 | 56 | [Fact] 57 | public void Should_Throw_On_Constructing_With_A_Null_Name() 58 | { 59 | string nullName = null; 60 | Assert.Throws(() => new Product(nullName)); 61 | } 62 | 63 | [Theory, 64 | InlineData(-1.99), 65 | InlineData(-10), 66 | InlineData(1-2)] 67 | public void Should_Throw_Setting_A_Negative_Price(decimal invalidPrice) 68 | { 69 | Product testProduct = new Product("Soap"); 70 | Assert.Throws(() => testProduct.Price = invalidPrice); 71 | } 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/CleanCo.Tests/TestDoubles/DummyRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using CleanCo.Common; 4 | 5 | namespace CleanCo.Tests 6 | { 7 | // An IRecord implementation we can use for testing. 8 | public class DummyRecord: IRecord 9 | { 10 | public DummyRecord(string name) { 11 | this.Id = Guid.NewGuid(); 12 | this.Name = name; 13 | } 14 | public Guid Id { get; set; } 15 | public string Name { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /server/CleanCo.Tests/TestDoubles/FakeRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using CleanCo.Common; 5 | 6 | namespace CleanCo.Tests 7 | { 8 | public class FakeRepository: IRepository where T: IRecord 9 | { 10 | private static IDictionary store; 11 | 12 | static FakeRepository() 13 | { 14 | store = new Dictionary(); 15 | } 16 | 17 | public void Delete(Guid id) 18 | { 19 | store.Remove(id); 20 | } 21 | public List Find(string filter) 22 | { 23 | throw new NotImplementedException("Test me first"); 24 | } 25 | public List RetrieveAll() 26 | { 27 | List list = new List(); 28 | foreach (T item in store.Values) 29 | { 30 | list.Add(item); 31 | } 32 | return list; 33 | } 34 | public T RetrieveById(Guid id) 35 | { 36 | if(store.ContainsKey(id)) 37 | { 38 | return store[id]; 39 | } 40 | else 41 | { 42 | return default(T); 43 | } 44 | } 45 | public List RetrieveNext(Guid last, int max) 46 | { 47 | throw new NotImplementedException("Test me first"); 48 | } 49 | public void Save(T record) 50 | { 51 | if(store.ContainsKey(record.Id)) 52 | { 53 | store[record.Id] = record; 54 | } 55 | else 56 | { 57 | store.Add(record.Id, record); 58 | } 59 | } 60 | public int RecordCount 61 | { 62 | get 63 | { 64 | return store.Count; 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /server/CleanCo.Tests/UseCaseTests/CatalogTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using CleanCo.Ecomm; 4 | using CleanCo.Products; 5 | 6 | namespace CleanCo.Tests 7 | { 8 | public class CatalogTests 9 | { 10 | private Catalog testCatalog = new Catalog(new FakeRepository()); 11 | [Fact] 12 | public void Should_Disallow_Products_With_Same_Name() 13 | { 14 | Product productA = new Product("same"); 15 | Product productB = new Product("same"); 16 | testCatalog.AddProduct(productA); 17 | Assert.Throws(() => testCatalog.AddProduct(productB)); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /server/CleanCo.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "testRunner": "xunit", 4 | "dependencies": { 5 | "dotnet-test-xunit": "2.2.0-preview2-build1029", 6 | "xunit": "2.2.0-beta2-build3300", 7 | "CleanCo.Common": { 8 | "target": "project" 9 | }, 10 | "CleanCo.Products": { 11 | "target": "project" 12 | }, 13 | "CleanCo.Ecomm.RepoAdapter": { 14 | "target": "project" 15 | }, 16 | "CleanCo.Ecomm.Catalog": { 17 | "target": "project" 18 | } 19 | }, 20 | "frameworks": { 21 | "netcoreapp1.0": { 22 | "dependencies": { 23 | "Microsoft.NETCore.App": { 24 | "type": "platform", 25 | "version": "1.0.0" 26 | } 27 | } 28 | } 29 | }, 30 | "buildOptions": { 31 | "copyToOutput": { 32 | "include": [ "xunit.runner.json" ] 33 | } 34 | }, 35 | "tooling": { 36 | "defaultNamespace": "Clean.Tests" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/CleanCo.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "diagnosticMessages": false, 3 | "methodDisplay": "classAndMethod", 4 | "parallelizeTestCollections": true 5 | } 6 | -------------------------------------------------------------------------------- /server/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | "CleanCo.JsonAdapter", 4 | "CleanCo.Ecomm.RepoAdapters", 5 | "CleanCo.Ecomm.RestAdapter", 6 | "CleanCo.Common", 7 | "CleanCo.Products", 8 | "CleanCo.Tests", 9 | "CleanCo.Ecomm.Catalog" 10 | ] 11 | } --------------------------------------------------------------------------------