├── .editorconfig ├── .gitignore ├── README.md ├── assets ├── css │ └── style.css ├── fonts │ ├── cornerstone.woff │ └── cornerstone.woff2 └── img │ ├── actions │ └── checked.svg │ ├── logo.svg │ ├── pizza.svg │ └── toppings │ ├── anchovy.svg │ ├── bacon.svg │ ├── basil.svg │ ├── chili.svg │ ├── mozzarella.svg │ ├── mushroom.svg │ ├── olive.svg │ ├── onion.svg │ ├── pepper.svg │ ├── pepperoni.svg │ ├── prawn.svg │ ├── singles │ ├── anchovy.svg │ ├── bacon.svg │ ├── basil.svg │ ├── chili.svg │ ├── mozzarella.svg │ ├── mushroom.svg │ ├── olive.svg │ ├── onion.svg │ ├── pepper.svg │ ├── pepperoni.svg │ ├── prawn.svg │ ├── sweetcorn.svg │ └── tomato.svg │ ├── sweetcorn.svg │ └── tomato.svg ├── db.json ├── favicon.ico ├── index.html ├── karma.conf.js ├── karma.entry.js ├── package.json ├── src ├── app │ ├── app.module.ts │ └── containers │ │ └── app │ │ ├── app.component.scss │ │ └── app.component.ts ├── main.ts └── products │ ├── components │ ├── index.ts │ ├── pizza-display │ │ ├── pizza-display.component.scss │ │ └── pizza-display.component.ts │ ├── pizza-form │ │ ├── pizza-form.component.scss │ │ └── pizza-form.component.ts │ ├── pizza-item │ │ ├── pizza-item.component.scss │ │ └── pizza-item.component.ts │ └── pizza-toppings │ │ ├── pizza-toppings.component.scss │ │ └── pizza-toppings.component.ts │ ├── containers │ ├── index.ts │ ├── product-item │ │ ├── product-item.component.scss │ │ └── product-item.component.ts │ └── products │ │ ├── products.component.scss │ │ └── products.component.ts │ ├── models │ ├── pizza.model.ts │ └── topping.model.ts │ ├── products.module.ts │ └── services │ ├── index.ts │ ├── pizzas.service.ts │ └── toppings.service.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | .idea 3 | .vscode 4 | 5 | # Node 6 | node_modules 7 | 8 | # macOS 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | Icon 13 | ._* 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | ## Windows 18 | Thumbs.db 19 | ehthumbs.db 20 | Desktop.ini 21 | $RECYCLE.BIN/ 22 | 23 | # Package Managers 24 | yarn-error.log 25 | npm-debug.log 26 | 27 | # Build 28 | build 29 | vendor/*-manifest.json 30 | 31 | # Docs 32 | gh-pages 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NGRX: Store + Effects app 4 |

5 |

Project seed app for our NGRX application using Angular, NGRX Store, Effects, Router Store.

6 | 7 | --- 8 | 9 | 10 | 11 | --- 12 | 13 | > This repo serves as the seed project for the Ultimate Angular NGRX Store + 14 | > Effects course as well as the final solution in stepped branches, come and 15 | > [learn NGRX](https://ultimatecourses.com/learn/ngrx-store-effects) with us! 16 | 17 | [Setup and install](#setup-and-install) | [Tasks](#tasks) | 18 | [Resources](#resources) 19 | 20 | ## Setup and install 21 | 22 | Fork this repo from inside GitHub so you can commit directly to your account, or 23 | simply download the `.zip` bundle with the contents inside. 24 | 25 | #### Dependency installation 26 | 27 | During the time building this project, you'll need development dependencies of 28 | which run on Node.js, follow the steps below for setting everything up (if you 29 | have some of these already, skip to the next step where appropriate): 30 | 31 | 1. Download and install [Node.js here](https://nodejs.org/en/download/) for 32 | Windows or for Mac. 33 | 34 | That's about it for tooling you'll need to run the project, let's move onto the 35 | project install. 36 | 37 | #### Project installation and server 38 | 39 | Now you've pulled down the repo and have everything setup, using the terminal 40 | you'll need to `cd` into the directory that you cloned the repo into and run 41 | some quick tasks: 42 | 43 | ``` 44 | cd 45 | npm install --legacy-peer-deps 46 | ``` 47 | 48 | This will then setup all the development and production dependencies we need. 49 | 50 | Now simply run this to boot up the server: 51 | 52 | ``` 53 | npm start 54 | ``` 55 | 56 | Visit `localhost:3000` to start building. 57 | 58 | ## Tasks 59 | 60 | A quick reminder of all tasks available: 61 | 62 | #### Development server 63 | 64 | ``` 65 | yarn start 66 | # OR 67 | npm start 68 | ``` 69 | 70 | ## Resources 71 | 72 | There are several resources used inside this project, of which you can read 73 | further about to dive deeper or understand in more detail what they are: 74 | 75 | * [Angular](https://angular.io) 76 | * [ngrx/store](https://github.com/ngrx/platform/blob/master/docs/store/README.md) 77 | docs 78 | * [ngrx/effects](https://github.com/ngrx/platform/blob/master/docs/effects/README.md) 79 | docs 80 | * [npm](https://www.npmjs.com/) 81 | * [Webpack](https://webpack.js.org/) 82 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | } 8 | html, 9 | body { 10 | height: 100%; 11 | width: 100%; 12 | margin: 0; 13 | padding: 0; 14 | color: #333; 15 | background: #23292d; 16 | -webkit-font-smoothing: antialiased; 17 | font: 300 16px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 18 | Arial, sans-serif; 19 | display: flex; 20 | } 21 | a { 22 | text-decoration: none; 23 | outline: 0; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5 { 31 | font-weight: normal; 32 | margin: 0; 33 | padding: 0; 34 | font-family: 'cornerstone'; 35 | } 36 | 37 | h3 { 38 | font-size: 24px; 39 | } 40 | h4 { 41 | font-size: 20px; 42 | } 43 | 44 | @font-face { 45 | font-family: 'cornerstone'; 46 | src: url('../fonts/cornerstone.woff2') format('woff2'), 47 | url('../fonts/cornerstone.woff') format('woff'); 48 | font-weight: normal; 49 | font-style: normal; 50 | } 51 | 52 | .btn { 53 | display: inline-block; 54 | padding: 10px 15px; 55 | margin: 0; 56 | outline: 0; 57 | border: 0; 58 | border-radius: 3px; 59 | font-size: 16px; 60 | font-family: 'cornerstone'; 61 | cursor: pointer; 62 | transition: all 0.2s ease; 63 | } 64 | .btn__ok { 65 | background: #0f9675; 66 | color: #fff; 67 | } 68 | .btn__ok:hover { 69 | background: #0a7d61; 70 | } 71 | .btn__warning { 72 | background: #ab131c; 73 | color: #fff; 74 | } 75 | .btn__warning:hover { 76 | background: #880c14; 77 | } 78 | -------------------------------------------------------------------------------- /assets/fonts/cornerstone.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultimatecourses/ngrx-store-effects-app/2c39c6e87fc1cc9ab76b590aac88deb88068073c/assets/fonts/cornerstone.woff -------------------------------------------------------------------------------- /assets/fonts/cornerstone.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultimatecourses/ngrx-store-effects-app/2c39c6e87fc1cc9ab76b590aac88deb88068073c/assets/fonts/cornerstone.woff2 -------------------------------------------------------------------------------- /assets/img/actions/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /assets/img/pizza.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /assets/img/toppings/chili.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets/img/toppings/pepper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/bacon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 17 | 18 | 19 | 20 | 21 | 26 | 31 | 36 | 41 | 42 | 43 | 44 | 49 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/basil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 17 | 23 | 47 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/chili.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 20 | 29 | 40 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/mozzarella.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 20 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/mushroom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 24 | 30 | 42 | 46 | 47 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/olive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 23 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 49 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 75 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/onion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 21 | 22 | 23 | 30 | 34 | 42 | 44 | 46 | 47 | 54 | 58 | 60 | 69 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/pepper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 22 | 36 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/pepperoni.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 21 | 30 | 112 | 149 | 176 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/prawn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 24 | 63 | 91 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/sweetcorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 19 | 22 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 49 | 54 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 78 | 81 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /assets/img/toppings/singles/tomato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 20 | 29 | 40 | 83 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "toppings": [ 3 | { 4 | "id": 1, 5 | "name": "anchovy" 6 | }, 7 | { 8 | "id": 2, 9 | "name": "bacon" 10 | }, 11 | { 12 | "id": 3, 13 | "name": "basil" 14 | }, 15 | { 16 | "id": 4, 17 | "name": "chili" 18 | }, 19 | { 20 | "id": 5, 21 | "name": "mozzarella" 22 | }, 23 | { 24 | "id": 6, 25 | "name": "mushroom" 26 | }, 27 | { 28 | "id": 7, 29 | "name": "olive" 30 | }, 31 | { 32 | "id": 8, 33 | "name": "onion" 34 | }, 35 | { 36 | "id": 9, 37 | "name": "pepper" 38 | }, 39 | { 40 | "id": 10, 41 | "name": "pepperoni" 42 | }, 43 | { 44 | "id": 11, 45 | "name": "sweetcorn" 46 | }, 47 | { 48 | "id": 12, 49 | "name": "tomato" 50 | } 51 | ], 52 | "pizzas": [ 53 | { 54 | "name": "Blazin' Inferno", 55 | "toppings": [ 56 | { 57 | "id": 10, 58 | "name": "pepperoni" 59 | }, 60 | { 61 | "id": 9, 62 | "name": "pepper" 63 | }, 64 | { 65 | "id": 3, 66 | "name": "basil" 67 | }, 68 | { 69 | "id": 4, 70 | "name": "chili" 71 | }, 72 | { 73 | "id": 7, 74 | "name": "olive" 75 | }, 76 | { 77 | "id": 2, 78 | "name": "bacon" 79 | } 80 | ], 81 | "id": 1 82 | }, 83 | { 84 | "name": "Seaside Surfin'", 85 | "toppings": [ 86 | { 87 | "id": 6, 88 | "name": "mushroom" 89 | }, 90 | { 91 | "id": 7, 92 | "name": "olive" 93 | }, 94 | { 95 | "id": 2, 96 | "name": "bacon" 97 | }, 98 | { 99 | "id": 3, 100 | "name": "basil" 101 | }, 102 | { 103 | "id": 1, 104 | "name": "anchovy" 105 | }, 106 | { 107 | "id": 8, 108 | "name": "onion" 109 | }, 110 | { 111 | "id": 11, 112 | "name": "sweetcorn" 113 | }, 114 | { 115 | "id": 9, 116 | "name": "pepper" 117 | }, 118 | { 119 | "id": 5, 120 | "name": "mozzarella" 121 | } 122 | ], 123 | "id": 2 124 | }, 125 | { 126 | "name": "Plain Ol' Pepperoni", 127 | "toppings": [ 128 | { 129 | "id": 10, 130 | "name": "pepperoni" 131 | } 132 | ], 133 | "id": 3 134 | } 135 | ] 136 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultimatecourses/ngrx-store-effects-app/2c39c6e87fc1cc9ab76b590aac88deb88068073c/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ultimate Angular 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = config => { 4 | config.set({ 5 | browsers: ['Chrome'], 6 | files: ['./node_modules/es6-shim/es6-shim.min.js', 'karma.entry.js'], 7 | frameworks: ['jasmine'], 8 | mime: { 'text/x-typescript': ['ts'] }, 9 | preprocessors: { 10 | 'karma.entry.js': ['webpack', 'sourcemap'], 11 | '*.js': ['sourcemap'], 12 | '**/*.spec.ts': ['sourcemap', 'webpack'], 13 | }, 14 | reporters: ['spec'], 15 | webpack: { 16 | context: __dirname, 17 | devtool: 'sourcemap', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.html$/, 22 | loaders: ['raw-loader'], 23 | }, 24 | { 25 | test: /\.scss$/, 26 | loaders: ['raw-loader', 'sass-loader'], 27 | }, 28 | { 29 | test: /\.ts$/, 30 | loaders: ['awesome-typescript-loader', 'angular2-template-loader'], 31 | }, 32 | ], 33 | }, 34 | plugins: [ 35 | new webpack.NamedModulesPlugin(), 36 | new webpack.SourceMapDevToolPlugin({ 37 | filename: null, 38 | test: /\.(ts|js)($|\?)/i, 39 | }), 40 | ], 41 | resolve: { 42 | extensions: ['.ts', '.js'], 43 | }, 44 | }, 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /karma.entry.js: -------------------------------------------------------------------------------- 1 | require('es6-shim'); 2 | require('reflect-metadata'); 3 | require('zone.js/dist/zone'); 4 | require('zone.js/dist/long-stack-trace-zone'); 5 | require('zone.js/dist/async-test'); 6 | require('zone.js/dist/fake-async-test'); 7 | require('zone.js/dist/sync-test'); 8 | require('zone.js/dist/proxy'); 9 | require('zone.js/dist/jasmine-patch'); 10 | 11 | const browserTesting = require('@angular/platform-browser-dynamic/testing'); 12 | const coreTesting = require('@angular/core/testing'); 13 | const context = require.context('./src/', true, /\.spec\.ts$/); 14 | 15 | Error.stackTraceLimit = Infinity; 16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; 17 | 18 | coreTesting.TestBed.resetTestEnvironment(); 19 | coreTesting.TestBed.initTestEnvironment( 20 | browserTesting.BrowserDynamicTestingModule, 21 | browserTesting.platformBrowserDynamicTesting() 22 | ); 23 | 24 | context.keys().forEach(context); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-store-effects-app", 3 | "version": "0.0.0", 4 | "author": "Ultimate Angular", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production webpack -p", 8 | "build:dev": 9 | "cross-env NODE_ENV=development webpack-dev-server --inline --hot", 10 | "build:production": "npm run clean && npm run build", 11 | "clean": "rimraf build", 12 | "start": "npm run build:dev", 13 | "test": "karma start ./karma.conf.js" 14 | }, 15 | "devDependencies": { 16 | "@angular/compiler": "5.0.3", 17 | "@angular/compiler-cli": "5.0.3", 18 | "@ngtools/webpack": "1.7.1", 19 | "@types/core-js": "0.9.43", 20 | "@types/jasmine": "2.6.0", 21 | "@types/karma": "0.13.36", 22 | "@types/node": "8.0.28", 23 | "angular-router-loader": "0.6.0", 24 | "angular2-template-loader": "0.6.2", 25 | "awesome-typescript-loader": "3.2.3", 26 | "chalk": "1.1.3", 27 | "cross-env": "5.0.5", 28 | "es6-shim": "0.35.3", 29 | "file-loader": "1.1.5", 30 | "html-loader": "0.5.1", 31 | "jasmine-core": "2.8.0", 32 | "jasmine-marbles": "0.2.0", 33 | "json-server": "0.12.0", 34 | "karma": "1.7.1", 35 | "karma-chrome-launcher": "2.2.0", 36 | "karma-jasmine": "1.1.0", 37 | "karma-sourcemap-loader": "0.3.7", 38 | "karma-spec-reporter": "0.0.31", 39 | "karma-webpack": "2.0.4", 40 | "ngrx-store-freeze": "^0.2.0", 41 | "sass": "^1.70.0", 42 | "node-sass": "^9.0.0", 43 | "progress-bar-webpack-plugin": "1.9.3", 44 | "raw-loader": "0.5.1", 45 | "rimraf": "2.6.2", 46 | "sass-loader": "6.0.6", 47 | "typescript": "2.6.1", 48 | "webpack": "3.6.0", 49 | "webpack-dev-server": "2.8.2" 50 | }, 51 | "dependencies": { 52 | "@angular/animations": "5.0.3", 53 | "@angular/common": "5.0.3", 54 | "@angular/core": "5.0.3", 55 | "@angular/forms": "5.0.3", 56 | "@angular/http": "5.0.3", 57 | "@angular/platform-browser": "5.0.3", 58 | "@angular/platform-browser-dynamic": "5.0.3", 59 | "@angular/router": "5.0.3", 60 | "@ngrx/effects": "4.1.0", 61 | "@ngrx/router-store": "4.1.1", 62 | "@ngrx/store": "4.1.0", 63 | "@ngrx/store-devtools": "4.0.0", 64 | "core-js": "2.5.1", 65 | "reflect-metadata": "0.1.10", 66 | "rxjs": "5.5.2", 67 | "ts-helpers": "1.1.2", 68 | "tslib": "1.8.0", 69 | "zone.js": "0.8.18" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { StoreModule, MetaReducer } from '@ngrx/store'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | 9 | // not used in production 10 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 11 | import { storeFreeze } from 'ngrx-store-freeze'; 12 | 13 | // this would be done dynamically with webpack for builds 14 | const environment = { 15 | development: true, 16 | production: false, 17 | }; 18 | 19 | export const metaReducers: MetaReducer[] = !environment.production 20 | ? [storeFreeze] 21 | : []; 22 | 23 | // bootstrap 24 | import { AppComponent } from './containers/app/app.component'; 25 | 26 | // routes 27 | export const ROUTES: Routes = [ 28 | { path: '', pathMatch: 'full', redirectTo: 'products' }, 29 | { 30 | path: 'products', 31 | loadChildren: '../products/products.module#ProductsModule', 32 | }, 33 | ]; 34 | 35 | @NgModule({ 36 | imports: [ 37 | BrowserModule, 38 | BrowserAnimationsModule, 39 | RouterModule.forRoot(ROUTES), 40 | StoreModule.forRoot({}, { metaReducers }), 41 | EffectsModule.forRoot([]), 42 | environment.development ? StoreDevtoolsModule.instrument() : [], 43 | ], 44 | declarations: [AppComponent], 45 | bootstrap: [AppComponent], 46 | }) 47 | export class AppModule {} 48 | -------------------------------------------------------------------------------- /src/app/containers/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | } 5 | .app { 6 | &__header { 7 | position: relative; 8 | text-align: center; 9 | padding: 35px 0; 10 | } 11 | &__logo { 12 | width: 75px; 13 | } 14 | &__nav { 15 | border-radius: 4px 4px 0 0; 16 | text-align: center; 17 | background: #ab131b; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | a { 22 | color: #fff; 23 | padding: 15px 35px; 24 | font-family: 'cornerstone'; 25 | &.active { 26 | background: #921217; 27 | } 28 | } 29 | } 30 | &__content { 31 | margin: 0 auto 50px; 32 | background: #fff; 33 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 34 | max-width: 1000px; 35 | border-radius: 4px; 36 | } 37 | &__container { 38 | padding: 35px; 39 | } 40 | &__footer { 41 | border-radius: 0 0 4px 4px; 42 | background: #0f9675; 43 | color: #fff; 44 | padding: 10px; 45 | text-align: center; 46 | p { 47 | margin: 0; 48 | font-weight: 600; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/containers/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | styleUrls: ['app.component.scss'], 6 | template: ` 7 |
8 |
9 | 10 |
11 |
12 |
13 | Products 14 |
15 |
16 | 17 |
18 | 21 |
22 |
23 | `, 24 | }) 25 | export class AppComponent {} 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { AppModule } from './app/app.module'; 6 | 7 | if (process.env.NODE_ENV === 'production') { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/products/components/index.ts: -------------------------------------------------------------------------------- 1 | import { PizzaItemComponent } from './pizza-item/pizza-item.component'; 2 | import { PizzaFormComponent } from './pizza-form/pizza-form.component'; 3 | import { PizzaDisplayComponent } from './pizza-display/pizza-display.component'; 4 | import { PizzaToppingsComponent } from './pizza-toppings/pizza-toppings.component'; 5 | 6 | export const components: any[] = [ 7 | PizzaItemComponent, 8 | PizzaFormComponent, 9 | PizzaDisplayComponent, 10 | PizzaToppingsComponent, 11 | ]; 12 | 13 | export * from './pizza-item/pizza-item.component'; 14 | export * from './pizza-form/pizza-form.component'; 15 | export * from './pizza-display/pizza-display.component'; 16 | export * from './pizza-toppings/pizza-toppings.component'; 17 | -------------------------------------------------------------------------------- /src/products/components/pizza-display/pizza-display.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | .pizza-display { 5 | background: #f5f5f5; 6 | border-radius: 4px; 7 | padding: 15px 0; 8 | &__base { 9 | position: relative; 10 | text-align: center; 11 | } 12 | &__topping { 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | bottom: 0; 18 | height: 100%; 19 | width: 100%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/products/components/pizza-display/pizza-display.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | import { transition, style, animate, trigger } from '@angular/animations'; 3 | 4 | import { Pizza } from '../../models/pizza.model'; 5 | 6 | export const DROP_ANIMATION = trigger('drop', [ 7 | transition(':enter', [ 8 | style({ transform: 'translateY(-200px)', opacity: 0 }), 9 | animate( 10 | '300ms cubic-bezier(1.000, 0.000, 0.000, 1.000)', 11 | style({ transform: 'translateY(0)', opacity: 1 }) 12 | ), 13 | ]), 14 | transition(':leave', [ 15 | style({ transform: 'translateY(0)', opacity: 1 }), 16 | animate( 17 | '200ms cubic-bezier(1.000, 0.000, 0.000, 1.000)', 18 | style({ transform: 'translateY(-200px)', opacity: 0 }) 19 | ), 20 | ]), 21 | ]); 22 | 23 | @Component({ 24 | selector: 'pizza-display', 25 | animations: [DROP_ANIMATION], 26 | changeDetection: ChangeDetectionStrategy.OnPush, 27 | styleUrls: ['pizza-display.component.scss'], 28 | template: ` 29 |
30 |
31 | 32 | 38 |
39 |
40 | `, 41 | }) 42 | export class PizzaDisplayComponent { 43 | @Input() pizza: Pizza; 44 | } 45 | -------------------------------------------------------------------------------- /src/products/components/pizza-form/pizza-form.component.scss: -------------------------------------------------------------------------------- 1 | .pizza-form { 2 | ::ng-deep pizza-display { 3 | margin: 0 0 35px; 4 | } 5 | label { 6 | margin: 0 0 35px; 7 | display: block; 8 | h4 { 9 | margin: 0 0 15px; 10 | } 11 | } 12 | &__error { 13 | padding: 10px; 14 | border-radius: 0 0 4px 4px; 15 | display: flex; 16 | align-items: center; 17 | background: #aa141b; 18 | color: #fff; 19 | p { 20 | font: { 21 | size: 14px; 22 | } 23 | margin: 0; 24 | } 25 | } 26 | &__input { 27 | border: 0; 28 | margin: 0; 29 | padding: 15px; 30 | outline: 0; 31 | width: 100%; 32 | border-radius: 4px; 33 | font-size: 20px; 34 | font-weight: 600; 35 | background: #f5f5f5; 36 | border: 1px solid transparent; 37 | &.error { 38 | border-radius: 4px 4px 0 0; 39 | border-color: #b54846; 40 | } 41 | } 42 | &__actions { 43 | margin: 35px 0 0; 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | button { 48 | &:last-child { 49 | margin-left: auto; 50 | } 51 | } 52 | } 53 | &__list { 54 | margin: -20px 0 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/products/components/pizza-form/pizza-form.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | OnChanges, 7 | SimpleChanges, 8 | ChangeDetectionStrategy, 9 | } from '@angular/core'; 10 | import { 11 | FormControl, 12 | FormGroup, 13 | FormArray, 14 | FormBuilder, 15 | Validators, 16 | } from '@angular/forms'; 17 | 18 | import { map } from 'rxjs/operators'; 19 | 20 | import { Pizza } from '../../models/pizza.model'; 21 | import { Topping } from '../../models/topping.model'; 22 | 23 | @Component({ 24 | selector: 'pizza-form', 25 | styleUrls: ['pizza-form.component.scss'], 26 | template: ` 27 |
28 |
29 | 30 | 44 | 45 | 46 | 47 | 50 |
51 | 52 | 55 | 56 | 57 |
58 | 59 |
60 | 67 | 68 | 75 | 76 | 83 |
84 | 85 |
86 |
87 | `, 88 | }) 89 | export class PizzaFormComponent implements OnChanges { 90 | exists = false; 91 | 92 | @Input() pizza: Pizza; 93 | @Input() toppings: Topping[]; 94 | 95 | @Output() selected = new EventEmitter(); 96 | @Output() create = new EventEmitter(); 97 | @Output() update = new EventEmitter(); 98 | @Output() remove = new EventEmitter(); 99 | 100 | form = this.fb.group({ 101 | name: ['', Validators.required], 102 | toppings: [[]], 103 | }); 104 | 105 | constructor(private fb: FormBuilder) {} 106 | 107 | get nameControl() { 108 | return this.form.get('name') as FormControl; 109 | } 110 | 111 | get nameControlInvalid() { 112 | return this.nameControl.hasError('required') && this.nameControl.touched; 113 | } 114 | 115 | ngOnChanges(changes: SimpleChanges) { 116 | if (this.pizza && this.pizza.id) { 117 | this.exists = true; 118 | this.form.patchValue(this.pizza); 119 | } 120 | this.form 121 | .get('toppings') 122 | .valueChanges.pipe( 123 | map(toppings => toppings.map((topping: Topping) => topping.id)) 124 | ) 125 | .subscribe(value => this.selected.emit(value)); 126 | } 127 | 128 | createPizza(form: FormGroup) { 129 | const { value, valid } = form; 130 | if (valid) { 131 | this.create.emit(value); 132 | } 133 | } 134 | 135 | updatePizza(form: FormGroup) { 136 | const { value, valid, touched } = form; 137 | if (touched && valid) { 138 | this.update.emit({ ...this.pizza, ...value }); 139 | } 140 | } 141 | 142 | removePizza(form: FormGroup) { 143 | const { value } = form; 144 | this.remove.emit({ ...this.pizza, ...value }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/products/components/pizza-item/pizza-item.component.scss: -------------------------------------------------------------------------------- 1 | .pizza-item { 2 | text-align: center; 3 | margin: 0 10px; 4 | padding: 20px 10px; 5 | border-radius: 4px; 6 | background: #f5f5f5; 7 | a { 8 | color: #333; 9 | } 10 | h4 { 11 | margin: 10px 0; 12 | } 13 | &__base { 14 | position: relative; 15 | } 16 | &__topping { 17 | position: absolute; 18 | top: 0; 19 | right: 0; 20 | left: 0; 21 | bottom: 0; 22 | height: 100%; 23 | width: 100%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/products/components/pizza-item/pizza-item.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | ChangeDetectionStrategy, 7 | } from '@angular/core'; 8 | 9 | @Component({ 10 | selector: 'pizza-item', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | styleUrls: ['pizza-item.component.scss'], 13 | template: ` 14 | 25 | `, 26 | }) 27 | export class PizzaItemComponent { 28 | @Input() pizza: any; 29 | } 30 | -------------------------------------------------------------------------------- /src/products/components/pizza-toppings/pizza-toppings.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | .pizza-toppings { 5 | display: flex; 6 | justify-content: space-between; 7 | flex-wrap: wrap; 8 | &-item { 9 | position: relative; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | padding: 8px; 14 | margin: 0 0 10px; 15 | border-radius: 4px; 16 | font-size: 15px; 17 | font-family: 'cornerstone'; 18 | border: 1px solid #e4e4e4; 19 | flex: 0 0 23%; 20 | transition: all 0.2s ease; 21 | cursor: pointer; 22 | &.active { 23 | background: #f5f5f5; 24 | &:after { 25 | content: ''; 26 | border-radius: 50%; 27 | background: #19b55f url(/assets/img/actions/checked.svg) no-repeat 28 | center center; 29 | width: 16px; 30 | height: 16px; 31 | position: absolute; 32 | top: -5px; 33 | right: -5px; 34 | background-size: 10px; 35 | } 36 | } 37 | img { 38 | width: 22px; 39 | margin: 0 10px 0 0; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/products/components/pizza-toppings/pizza-toppings.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | forwardRef, 5 | ChangeDetectionStrategy, 6 | } from '@angular/core'; 7 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 8 | import { Topping } from '../../models/topping.model'; 9 | 10 | const PIZZA_TOPPINGS_ACCESSOR = { 11 | provide: NG_VALUE_ACCESSOR, 12 | useExisting: forwardRef(() => PizzaToppingsComponent), 13 | multi: true, 14 | }; 15 | 16 | @Component({ 17 | selector: 'pizza-toppings', 18 | providers: [PIZZA_TOPPINGS_ACCESSOR], 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | styleUrls: ['pizza-toppings.component.scss'], 21 | template: ` 22 |
23 |
28 | 29 | {{ topping.name }} 30 |
31 |
32 | `, 33 | }) 34 | export class PizzaToppingsComponent implements ControlValueAccessor { 35 | @Input() toppings: Topping[] = []; 36 | 37 | value: Topping[] = []; 38 | 39 | private onTouch: Function; 40 | private onModelChange: Function; 41 | 42 | registerOnChange(fn: Function) { 43 | this.onModelChange = fn; 44 | } 45 | 46 | registerOnTouched(fn: Function) { 47 | this.onTouch = fn; 48 | } 49 | 50 | writeValue(value: Topping[]) { 51 | this.value = value; 52 | } 53 | 54 | selectTopping(topping: Topping) { 55 | if (this.existsInToppings(topping)) { 56 | this.value = this.value.filter(item => item.id !== topping.id); 57 | } else { 58 | this.value = [...this.value, topping]; 59 | } 60 | this.onTouch(); 61 | this.onModelChange(this.value); 62 | } 63 | 64 | existsInToppings(topping: Topping) { 65 | return this.value.some(val => val.id === topping.id); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/products/containers/index.ts: -------------------------------------------------------------------------------- 1 | import { ProductsComponent } from './products/products.component'; 2 | import { ProductItemComponent } from './product-item/product-item.component'; 3 | 4 | export const containers: any[] = [ProductsComponent, ProductItemComponent]; 5 | 6 | export * from './products/products.component'; 7 | export * from './product-item/product-item.component'; 8 | -------------------------------------------------------------------------------- /src/products/containers/product-item/product-item.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | .product-item { 5 | display: flex; 6 | justify-content: space-between; 7 | } 8 | -------------------------------------------------------------------------------- /src/products/containers/product-item/product-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { Pizza } from '../../models/pizza.model'; 5 | import { PizzasService } from '../../services/pizzas.service'; 6 | 7 | import { Topping } from '../../models/topping.model'; 8 | import { ToppingsService } from '../../services/toppings.service'; 9 | 10 | @Component({ 11 | selector: 'product-item', 12 | styleUrls: ['product-item.component.scss'], 13 | template: ` 14 |
16 | 23 | 25 | 26 | 27 |
28 | `, 29 | }) 30 | export class ProductItemComponent implements OnInit { 31 | pizza: Pizza; 32 | visualise: Pizza; 33 | toppings: Topping[]; 34 | 35 | constructor( 36 | private pizzaService: PizzasService, 37 | private toppingsService: ToppingsService, 38 | private route: ActivatedRoute, 39 | private router: Router 40 | ) {} 41 | 42 | ngOnInit() { 43 | this.pizzaService.getPizzas().subscribe(pizzas => { 44 | const param = this.route.snapshot.params.id; 45 | let pizza; 46 | if (param === 'new') { 47 | pizza = {}; 48 | } else { 49 | pizza = pizzas.find(pizza => pizza.id == parseInt(param, 10)); 50 | } 51 | this.pizza = pizza; 52 | this.toppingsService.getToppings().subscribe(toppings => { 53 | this.toppings = toppings; 54 | this.onSelect(toppings.map(topping => topping.id)); 55 | }); 56 | }); 57 | } 58 | 59 | onSelect(event: number[]) { 60 | let toppings; 61 | if (this.toppings && this.toppings.length) { 62 | toppings = event.map(id => 63 | this.toppings.find(topping => topping.id === id) 64 | ); 65 | } else { 66 | toppings = this.pizza.toppings; 67 | } 68 | this.visualise = { ...this.pizza, toppings }; 69 | } 70 | 71 | onCreate(event: Pizza) { 72 | this.pizzaService.createPizza(event).subscribe(pizza => { 73 | this.router.navigate([`/products/${pizza.id}`]); 74 | }); 75 | } 76 | 77 | onUpdate(event: Pizza) { 78 | this.pizzaService.updatePizza(event).subscribe(() => { 79 | this.router.navigate([`/products`]); 80 | }); 81 | } 82 | 83 | onRemove(event: Pizza) { 84 | const remove = window.confirm('Are you sure?'); 85 | if (remove) { 86 | this.pizzaService.removePizza(event).subscribe(() => { 87 | this.router.navigate([`/products`]); 88 | }); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/products/containers/products/products.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | .products { 5 | position: relative; 6 | &__new { 7 | margin: -35px -35px 35px; 8 | background: #f9f9f9; 9 | padding: 35px; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | &__list { 15 | display: flex; 16 | flex-wrap: wrap; 17 | pizza-item { 18 | background: #fff; 19 | flex: 0 0 33%; 20 | margin: 0 0 55px; 21 | transition: 0.2s all ease; 22 | &:hover { 23 | transform: scale(1.05); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/products/containers/products/products.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Pizza } from '../../models/pizza.model'; 4 | import { PizzasService } from '../../services/pizzas.service'; 5 | 6 | @Component({ 7 | selector: 'products', 8 | styleUrls: ['products.component.scss'], 9 | template: ` 10 |
11 | 18 |
19 |
20 | No pizzas, add one to get started. 21 |
22 | 25 | 26 |
27 |
28 | `, 29 | }) 30 | export class ProductsComponent implements OnInit { 31 | pizzas: Pizza[]; 32 | 33 | constructor(private pizzaService: PizzasService) {} 34 | 35 | ngOnInit() { 36 | this.pizzaService.getPizzas().subscribe(pizzas => { 37 | this.pizzas = pizzas; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/products/models/pizza.model.ts: -------------------------------------------------------------------------------- 1 | import { Topping } from '../models/topping.model'; 2 | 3 | export interface Pizza { 4 | id?: number; 5 | name?: string; 6 | toppings?: Topping[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/products/models/topping.model.ts: -------------------------------------------------------------------------------- 1 | export interface Topping { 2 | id?: number; 3 | name?: string; 4 | [key: string]: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | 7 | // components 8 | import * as fromComponents from './components'; 9 | 10 | // containers 11 | import * as fromContainers from './containers'; 12 | 13 | // services 14 | import * as fromServices from './services'; 15 | 16 | // routes 17 | export const ROUTES: Routes = [ 18 | { 19 | path: '', 20 | component: fromContainers.ProductsComponent, 21 | }, 22 | { 23 | path: ':id', 24 | component: fromContainers.ProductItemComponent, 25 | }, 26 | { 27 | path: 'new', 28 | component: fromContainers.ProductItemComponent, 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [ 34 | CommonModule, 35 | ReactiveFormsModule, 36 | HttpClientModule, 37 | RouterModule.forChild(ROUTES), 38 | ], 39 | providers: [...fromServices.services], 40 | declarations: [...fromContainers.containers, ...fromComponents.components], 41 | exports: [...fromContainers.containers, ...fromComponents.components], 42 | }) 43 | export class ProductsModule {} 44 | -------------------------------------------------------------------------------- /src/products/services/index.ts: -------------------------------------------------------------------------------- 1 | import { PizzasService } from './pizzas.service'; 2 | import { ToppingsService } from './toppings.service'; 3 | 4 | export const services: any[] = [PizzasService, ToppingsService]; 5 | 6 | export * from './pizzas.service'; 7 | export * from './toppings.service'; 8 | -------------------------------------------------------------------------------- /src/products/services/pizzas.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | import { Observable } from 'rxjs/Observable'; 5 | import { catchError } from 'rxjs/operators'; 6 | import 'rxjs/add/observable/throw'; 7 | 8 | import { Pizza } from '../models/pizza.model'; 9 | 10 | @Injectable() 11 | export class PizzasService { 12 | constructor(private http: HttpClient) {} 13 | 14 | getPizzas(): Observable { 15 | return this.http 16 | .get(`/api/pizzas`) 17 | .pipe(catchError((error: any) => Observable.throw(error.json()))); 18 | } 19 | 20 | createPizza(payload: Pizza): Observable { 21 | return this.http 22 | .post(`/api/pizzas`, payload) 23 | .pipe(catchError((error: any) => Observable.throw(error.json()))); 24 | } 25 | 26 | updatePizza(payload: Pizza): Observable { 27 | return this.http 28 | .put(`/api/pizzas/${payload.id}`, payload) 29 | .pipe(catchError((error: any) => Observable.throw(error.json()))); 30 | } 31 | 32 | removePizza(payload: Pizza): Observable { 33 | return this.http 34 | .delete(`/api/pizzas/${payload.id}`) 35 | .pipe(catchError((error: any) => Observable.throw(error.json()))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/products/services/toppings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | import { Observable } from 'rxjs/Observable'; 5 | import { catchError } from 'rxjs/operators'; 6 | import 'rxjs/add/observable/throw'; 7 | 8 | import { Topping } from '../models/topping.model'; 9 | 10 | @Injectable() 11 | export class ToppingsService { 12 | constructor(private http: HttpClient) {} 13 | 14 | getToppings(): Observable { 15 | return this.http 16 | .get(`/api/toppings`) 17 | .pipe(catchError((error: any) => Observable.throw(error.json()))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "entryModule": "app/app.module#AppModule", 4 | "genDir": "./ngfactory" 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": ".", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "lib": ["dom", "es2016"], 12 | "module": "es2015", 13 | "moduleResolution": "node", 14 | "noEmitHelpers": true, 15 | "noImplicitAny": true, 16 | "outDir": "lib", 17 | "rootDir": ".", 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "target": "es5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const typescript = require('typescript'); 4 | const { AotPlugin } = require('@ngtools/webpack'); 5 | const jsonServer = require('json-server'); 6 | 7 | const rules = [ 8 | { test: /\.html$/, loader: 'html-loader' }, 9 | { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader'] }, 10 | { test: /\.(jpe?g|png|gif|svg)$/i, loader: 'file-loader' }, 11 | ]; 12 | 13 | const plugins = [ 14 | new webpack.DefinePlugin({ 15 | 'process.env': { 16 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 17 | }, 18 | }), 19 | new webpack.optimize.CommonsChunkPlugin({ 20 | name: 'vendor', 21 | minChunks: module => module.context && /node_modules/.test(module.context), 22 | }), 23 | ]; 24 | 25 | if (process.env.NODE_ENV === 'production') { 26 | rules.push({ 27 | test: /\.ts$/, 28 | loaders: ['@ngtools/webpack'], 29 | }); 30 | plugins.push( 31 | new AotPlugin({ 32 | tsConfigPath: './tsconfig.json', 33 | entryModule: 'src/app/app.module#AppModule', 34 | }), 35 | new webpack.LoaderOptionsPlugin({ 36 | minimize: true, 37 | debug: false, 38 | }), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | sourceMap: true, 41 | beautify: false, 42 | mangle: { 43 | screw_ie8: true, 44 | }, 45 | compress: { 46 | unused: true, 47 | dead_code: true, 48 | drop_debugger: true, 49 | conditionals: true, 50 | evaluate: true, 51 | drop_console: true, 52 | sequences: true, 53 | booleans: true, 54 | screw_ie8: true, 55 | warnings: false, 56 | }, 57 | comments: false, 58 | }) 59 | ); 60 | } else { 61 | rules.push({ 62 | test: /\.ts$/, 63 | loaders: [ 64 | 'awesome-typescript-loader', 65 | 'angular-router-loader', 66 | 'angular2-template-loader', 67 | ], 68 | }); 69 | plugins.push( 70 | new webpack.NamedModulesPlugin(), 71 | new webpack.ContextReplacementPlugin( 72 | /angular(\\|\/)core(\\|\/)@angular/, 73 | path.resolve(__dirname, './notfound') 74 | ) 75 | ); 76 | } 77 | 78 | module.exports = { 79 | cache: true, 80 | context: __dirname, 81 | devServer: { 82 | contentBase: __dirname, 83 | historyApiFallback: true, 84 | stats: { 85 | chunks: false, 86 | chunkModules: false, 87 | chunkOrigins: false, 88 | errors: true, 89 | errorDetails: false, 90 | hash: false, 91 | timings: false, 92 | modules: false, 93 | warnings: false, 94 | }, 95 | publicPath: '/build/', 96 | port: 3000, 97 | setup: function(app) { 98 | app.use('/api', jsonServer.router('db.json')); 99 | }, 100 | }, 101 | devtool: 'sourcemap', 102 | entry: { 103 | app: ['zone.js/dist/zone', './src/main.ts'], 104 | }, 105 | output: { 106 | filename: '[name].js', 107 | chunkFilename: '[name]-chunk.js', 108 | publicPath: '/build/', 109 | path: path.resolve(__dirname, 'build'), 110 | }, 111 | node: { 112 | console: false, 113 | global: true, 114 | process: true, 115 | Buffer: false, 116 | setImmediate: false, 117 | }, 118 | module: { 119 | rules, 120 | }, 121 | resolve: { 122 | extensions: ['.ts', '.js'], 123 | modules: ['src', 'node_modules'], 124 | }, 125 | plugins, 126 | }; 127 | --------------------------------------------------------------------------------