├── .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 |
5 |
--------------------------------------------------------------------------------
/assets/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/img/pizza.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/img/toppings/chili.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/img/toppings/pepper.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/bacon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
58 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/basil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
73 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/chili.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
56 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/mozzarella.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/mushroom.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
72 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/olive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
88 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/onion.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
87 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/pepper.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/pepperoni.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
195 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/prawn.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
113 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/sweetcorn.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
93 |
--------------------------------------------------------------------------------
/assets/img/toppings/singles/tomato.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
11 |
12 |
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 |
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 |
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 |
--------------------------------------------------------------------------------