├── src
├── assets
│ ├── .gitkeep
│ ├── customers.json
│ └── orders.json
├── favicon.ico
├── app
│ ├── orders
│ │ ├── orders.component.css
│ │ ├── orders.preNg17.component.html
│ │ ├── orders.component.html
│ │ └── orders.component.ts
│ ├── customers
│ │ ├── customers.component.html
│ │ ├── customers.component.ts
│ │ └── customers-list
│ │ │ ├── filter-textbox.component.ts
│ │ │ ├── customers-list.component.html
│ │ │ └── customers-list.component.ts
│ ├── app.component.ts
│ ├── shared
│ │ ├── capitalize.pipe.ts
│ │ └── interfaces.ts
│ ├── app.routes.ts
│ ├── app.config.ts
│ └── core
│ │ ├── sorter.service.ts
│ │ └── data.service.ts
├── main.ts
├── styles.css
└── index.html
├── .dockerignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── server
├── package.json
├── node.dockerfile
├── data
│ ├── customers.json
│ └── orders.json
├── server.js
└── package-lock.json
├── tsconfig.app.json
├── tsconfig.spec.json
├── .editorconfig
├── config
└── nginx.conf
├── .gitignore
├── nginx.prod.dockerfile
├── nginx.dockerfile
├── tsconfig.json
├── package.json
├── README.md
├── docker-compose.prod.yml
├── docker-compose.yml
└── angular.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | .gitignore
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanWahlin/Angular-Core-Concepts/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/app/orders/orders.component.css:
--------------------------------------------------------------------------------
1 | .orders-table {
2 | margin-top: 20px;
3 | }
4 |
5 | .odd {
6 | background-color: #efefef;
7 | }
--------------------------------------------------------------------------------
/src/app/customers/customers.component.html:
--------------------------------------------------------------------------------
1 |
{{ title }}
2 |
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-core-concepts-server",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "express": "4.16.3"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/node.dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 |
3 | LABEL author="Dan Wahlin"
4 |
5 | WORKDIR /var/www/angular-node-service
6 |
7 | COPY package.json package.json
8 | RUN npm install
9 |
10 | COPY . .
11 |
12 | EXPOSE 3000
13 |
14 | ENTRYPOINT ["node", "server.js"]
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig)
6 | .catch((err) => console.error(err));
7 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterOutlet } from '@angular/router';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | standalone: true,
7 | imports: [RouterOutlet],
8 | template: `
9 |
10 | `
11 | })
12 | export class AppComponent {
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "include": [
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/src/app/shared/capitalize.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'capitalize',
5 | standalone: true
6 | })
7 | export class CapitalizePipe implements PipeTransform {
8 | transform(value: any) {
9 | if (value) {
10 | return value.charAt(0).toUpperCase() + value.slice(1);
11 | }
12 | return value;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/shared/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface Customer {
2 | id: number;
3 | name: string;
4 | city: string;
5 | orderTotal?: number;
6 | customerSince: any;
7 | }
8 |
9 | export interface Order {
10 | customerId: number;
11 | orderItems: OrderItem[];
12 | }
13 |
14 | export interface OrderItem {
15 | id: number;
16 | productName: string;
17 | itemCost: number;
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const routes: Routes = [
4 | { path: '', pathMatch: 'full', redirectTo: '/customers'},
5 | { path: 'customers', loadComponent: () => import('./customers/customers.component').then(m => m.CustomersComponent) },
6 | { path: 'orders/:id', loadComponent: () => import('./orders/orders.component').then(m => m.OrdersComponent) },
7 | { path: '**', pathMatch: 'full', redirectTo: '/customers' }
8 | ];
9 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core';
2 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
3 | import { provideRouter, withComponentInputBinding } from '@angular/router';
4 |
5 | import { routes } from './app.routes';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [
9 | provideRouter(routes, withComponentInputBinding()),
10 | provideHttpClient(withInterceptorsFromDi())
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow-y: scroll;
3 | overflow-x: hidden;
4 | }
5 |
6 | main {
7 | position: relative;
8 | padding-top: 70px;
9 | }
10 |
11 | /* Ensure display:flex and others don't override a [hidden] */
12 | [hidden] { display: none !important; }
13 |
14 | footer {
15 | margin-top: 15px;
16 | }
17 |
18 | th {
19 | cursor: pointer;
20 | }
21 |
22 | td {
23 | width:33%;
24 | }
25 |
26 | .app-title {
27 | line-height:50px;
28 | font-size:20px;
29 | color: white;
30 | }
31 |
32 | thead {
33 | background-color: #efefef;
34 | }
--------------------------------------------------------------------------------
/config/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 0.0.0.0:80;
3 | listen [::]:80;
4 | default_type application/octet-stream;
5 |
6 | gzip on;
7 | gzip_comp_level 6;
8 | gzip_vary on;
9 | gzip_min_length 1000;
10 | gzip_proxied any;
11 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
12 | gzip_buffers 16 8k;
13 | client_max_body_size 256M;
14 |
15 | root /usr/share/nginx/html;
16 |
17 | location / {
18 | try_files $uri $uri/ /index.html =404;
19 | }
20 | }
--------------------------------------------------------------------------------
/server/data/customers.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "ted James",
5 | "city": " Phoenix ",
6 | "orderTotal": 40.99
7 | },
8 | {
9 | "id": 2,
10 | "name": "Michelle Thompson",
11 | "city": "Los Angeles ",
12 | "orderTotal": 89.99
13 | },
14 | {
15 | "id": 3,
16 | "name": "James Thomas",
17 | "city": " Las Vegas ",
18 | "orderTotal": 29.99
19 | },
20 | {
21 | "id": 4,
22 | "name": "Tina Adams",
23 | "city": "Seattle",
24 | "orderTotal": 15.99
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/src/assets/customers.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "ted James",
5 | "city": " Phoenix ",
6 | "orderTotal": 29.98
7 | },
8 | {
9 | "id": 2,
10 | "name": "Michelle Thompson",
11 | "city": "Los Angeles ",
12 | "orderTotal": 207.98
13 | },
14 | {
15 | "id": 3,
16 | "name": "James Thomas",
17 | "city": " Las Vegas ",
18 | "orderTotal": 8.98
19 | },
20 | {
21 | "id": 4,
22 | "name": "Tina Adams",
23 | "city": "Seattle",
24 | "orderTotal": 229.97
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/src/app/orders/orders.preNg17.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Orders for {{ customer.name | capitalize }}
3 |
4 |
5 |
6 |
8 | | {{ orderItem.productName }} |
9 | {{ orderItem.itemCost | currency:'USD':'symbol' }} |
10 |
11 |
12 |
13 |
14 |
15 | No customer found
16 |
17 |
18 | View All Customers
--------------------------------------------------------------------------------
/src/app/customers/customers.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject } from '@angular/core';
2 |
3 | import { Customer } from '../shared/interfaces';
4 | import { DataService } from '../core/data.service';
5 | import { CommonModule } from '@angular/common';
6 | import { Observable, of } from 'rxjs';
7 | import { CustomersListComponent } from './customers-list/customers-list.component';
8 |
9 | @Component({
10 | selector: 'app-customers',
11 | standalone: true,
12 | imports: [ CommonModule, CustomersListComponent ],
13 | templateUrl: './customers.component.html'
14 | })
15 | export class CustomersComponent {
16 | title = 'Customers';
17 | dataService: DataService = inject(DataService);
18 | people$: Observable = this.dataService.getCustomers();
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/customers/customers-list/filter-textbox.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 |
4 | @Component({
5 | selector: 'app-filter-textbox',
6 | standalone: true,
7 | imports: [ FormsModule ],
8 | template: `
9 | Filter:
10 | `
11 | })
12 | export class FilterTextboxComponent{
13 |
14 | private _filter = '';
15 | @Input() get filter() {
16 | return this._filter;
17 | }
18 |
19 | set filter(val: string) {
20 | this._filter = val;
21 | this.changed.emit(this.filter); // Raise changed event
22 | }
23 |
24 | @Output() changed: EventEmitter = new EventEmitter();
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/nginx.prod.dockerfile:
--------------------------------------------------------------------------------
1 | ##### Stage 1
2 | FROM node:latest as node
3 | LABEL author="Dan Wahlin"
4 | WORKDIR /app
5 | COPY package.json package-lock.json ./
6 | RUN npm install
7 | COPY . .
8 | # Angular 12+ does a production build by default if you've enabled it using
9 | # ng update @angular/cli --migrate-only production-by-default
10 | # https://github.com/angular/angular-cli/issues/21073#issuecomment-855960826
11 | # RUN npm run build
12 |
13 | # Prod build if production-by-default hasn't been enabled
14 | RUN npm run build
15 |
16 | ##### Stage 2
17 | FROM nginx:alpine
18 | VOLUME /var/cache/nginx
19 | COPY --from=node /app/dist/angular-core-concepts/browser /usr/share/nginx/html
20 | COPY ./config/nginx.conf /etc/nginx/conf.d/default.conf
21 |
22 | # docker build -t nginx-angular -f nginx.prod.dockerfile .
23 | # docker run -p 8080:80 nginx-angular
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var express = require('express'),
3 | app = express(),
4 | customers = require('./data/customers'),
5 | orders = require('./data/orders');
6 |
7 | app.use(express.urlencoded({ extended: true }));
8 | app.use(express.json());
9 |
10 | //CORS
11 | app.use((req, res, next) => {
12 | res.header("Access-Control-Allow-Origin", "*");
13 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, X-XSRF-TOKEN, Content-Type, Accept");
14 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH');
15 | next();
16 | });
17 |
18 | app.get('/api/customers', (req, res) => {
19 | res.json(customers);
20 | });
21 |
22 | app.get('/api/orders', (req, res) => {
23 | res.json(orders);
24 | });
25 |
26 | app.listen(3000);
27 |
28 | console.log('Express listening on port 3000.');
29 |
30 |
31 |
--------------------------------------------------------------------------------
/nginx.dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:alpine
2 | LABEL author="Dan Wahlin"
3 | COPY ./config/nginx.conf /etc/nginx/conf.d/default.conf
4 |
5 |
6 |
7 |
8 |
9 | # Use the following commands to build the image and run the container (run from the root folder)
10 | # 1. You'll first need to build the project using `ng build`
11 |
12 | # 2. Now build the Docker image:
13 | # docker build -t nginx-angular -f nginx.dockerfile .
14 |
15 | #3. Run the Docker container:
16 | # To run the container we'll create a volume to point to our local source code. On Mac
17 | # you can use $(pwd) to reference your local folder where your running Docker commands from.
18 | # If you're on Windows there are several options to point to the folder. See my following post:
19 | # https://blog.codewithdan.com/2017/10/25/docker-volumes-and-print-working-directory-pwd/
20 |
21 | # docker run -p 8080:80 -v $(pwd)/dist:/usr/share/nginx/html nginx-angular
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularCoreConcepts
6 |
7 |
8 |
9 |
12 |
13 |
14 |
21 |
22 |
23 |
24 | Loading...
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "outDir": "./dist/out-tsc",
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "skipLibCheck": true,
13 | "esModuleInterop": true,
14 | "sourceMap": true,
15 | "declaration": false,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "ES2022",
21 | "useDefineForClassFields": false,
22 | "lib": [
23 | "ES2022",
24 | "dom"
25 | ]
26 | },
27 | "angularCompilerOptions": {
28 | "enableI18nLegacyMessageIdFormat": false,
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/orders/orders.component.html:
--------------------------------------------------------------------------------
1 | @if (this.customer$ | async; as customer) {
2 |
3 |
Orders for {{ customer.name | capitalize }}
4 |
5 |
6 | @for (order of orders$ | async; track order.customerId) {
7 | @for (orderItem of order.orderItems; track orderItem.id; let isOdd=$odd) {
8 |
9 | | {{ orderItem.productName }} |
10 | {{ orderItem.itemCost | currency:'USD':'symbol' }} |
11 |
12 | }
13 | }
14 |
15 |
16 | }
17 | @else {
18 |
19 | No customer found
20 |
21 | }
22 |
23 | @switch ((this.customer$ | async)?.city) {
24 | @case ('New York') {
25 |
26 | }
27 | @case ('Phoenix') {
28 |
29 | }
30 | @default {
31 |
32 | }
33 | }
34 |
35 |
36 | View All Customers
37 |
38 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-core-concepts",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test"
10 | },
11 | "private": true,
12 | "dependencies": {
13 | "@angular/animations": "^17.2.0",
14 | "@angular/common": "^17.2.0",
15 | "@angular/compiler": "^17.2.0",
16 | "@angular/core": "^17.2.0",
17 | "@angular/forms": "^17.2.0",
18 | "@angular/platform-browser": "^17.2.0",
19 | "@angular/platform-browser-dynamic": "^17.2.0",
20 | "@angular/router": "^17.2.0",
21 | "rxjs": "~7.8.0",
22 | "tslib": "^2.3.0",
23 | "zone.js": "~0.14.3"
24 | },
25 | "devDependencies": {
26 | "@angular-devkit/build-angular": "^17.2.2",
27 | "@angular/cli": "^17.2.2",
28 | "@angular/compiler-cli": "^17.2.0",
29 | "@types/jasmine": "~5.1.0",
30 | "jasmine-core": "~5.1.0",
31 | "karma": "~6.4.0",
32 | "karma-chrome-launcher": "~3.2.0",
33 | "karma-coverage": "~2.2.0",
34 | "karma-jasmine": "~5.1.0",
35 | "karma-jasmine-html-reporter": "~2.1.0",
36 | "typescript": "~5.3.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/orders/orders.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, inject } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { ActivatedRoute, RouterLink } from '@angular/router';
4 | import { Observable, of } from 'rxjs';
5 |
6 | import { DataService } from '../core/data.service';
7 | import { Customer, Order } from '../shared/interfaces';
8 | import { CapitalizePipe } from '../shared/capitalize.pipe';
9 |
10 | @Component({
11 | selector: 'app-orders',
12 | standalone: true,
13 | imports: [ CommonModule, RouterLink, CapitalizePipe ],
14 | templateUrl: './orders.component.html',
15 | styleUrls: [ './orders.component.css' ]
16 | })
17 | export class OrdersComponent {
18 |
19 | orders$: Observable = of([]);
20 | customer$: Observable = of();
21 | dataService: DataService = inject(DataService);
22 | route: ActivatedRoute = inject(ActivatedRoute);
23 |
24 | // Get the customer ID from the route - see `provideRouter(routes, withComponentInputBinding())` in app.config.ts
25 | @Input() set id(value: string) {
26 | const idParam = +value;
27 | this.orders$ = this.dataService.getOrders(idParam);
28 | this.customer$ = this.dataService.getCustomer(idParam);
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Core Concepts
2 |
3 | This project shows several core features of Angular including:
4 |
5 | * Components
6 | * Data Binding
7 | * Communication between components
8 | * Services
9 | * Routing
10 |
11 | ## Running the Project Locally
12 |
13 | 1. Install the Angular CLI
14 |
15 | `npm install -g @angular/cli`
16 |
17 | 1. Run `npm install` at the root of this project
18 |
19 | 1. Run `ng serve -o`
20 |
21 |
22 | ## Running the Project Using Docker Containers
23 |
24 | 1. Install the Angular CLI
25 |
26 | `npm install -g @angular/cli`
27 |
28 | 1. Run `npm install` at the root of this project
29 |
30 | 1. Build the project
31 |
32 | `ng build`
33 |
34 | 1. Ensure that you have volumes (file sharing) enabled in the Docker Desktop settings.
35 |
36 | 1. Run `docker-compose build`
37 |
38 | 1. Run `docker-compose up`
39 |
40 | 1. Visit `http://localhost`
41 |
42 | ## Running the `Production` Version in Containers
43 |
44 | 1. Run `docker-compose -f docker-compose.prod.yml build`. This uses a multi-stage Docker build process to create the nginx image for the Angular app.
45 |
46 | 1. Run `docker-compose -f docker-compose.prod.yml up` and visit `http://localhost`.
47 |
48 | 1. Run `docker-compose -f docker-compose.prod.yml down` once you're done.
--------------------------------------------------------------------------------
/src/app/customers/customers-list/customers-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | | Name |
8 | City |
9 | Order Total |
10 |
11 |
12 |
13 | @for (cust of filteredCustomers; track cust.id) {
14 |
15 | |
16 |
17 | {{ cust.name | capitalize }}
18 |
19 | |
20 | {{ cust.city }} |
21 | {{ cust.orderTotal | currency:currencyCode:'symbol' }}
22 | |
23 | }
24 | @empty {
25 |
26 | | There are no orders. |
27 |
28 | }
29 |
30 | @if (this.filteredCustomers.length) {
31 |
32 | | |
33 |
34 | {{ customersOrderTotal | currency:currencyCode:'symbol' }}
35 | |
36 |
37 | }
38 | @else {
39 |
40 | | No customers found |
41 |
42 | }
43 |
44 |
45 | Number of Customers: {{ filteredCustomers.length }}
46 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 |
2 | # This can be used to run a production version of the Angular and Node containers
3 | # See the readme.md for details on changes that are required in the Angular service
4 |
5 | # Run docker-compose -f docker-compose.prod.yml build
6 | # Run docker-compose -f docker-compose.prod.yml up
7 | # Live long and prosper
8 |
9 | version: '3.7'
10 |
11 | services:
12 |
13 | nginx:
14 | container_name: nginx-angular
15 | image: nginx-angular
16 | build:
17 | context: .
18 | dockerfile: nginx.prod.dockerfile
19 | ports:
20 | - "80:80"
21 | - "443:443"
22 | depends_on:
23 | - node
24 | networks:
25 | - app-network
26 |
27 | node:
28 | container_name: angular-node-service
29 | image: angular-node-service
30 | build:
31 | context: ./server
32 | dockerfile: node.dockerfile
33 | environment:
34 | - NODE_ENV=production
35 | ports:
36 | - "3000:3000"
37 | networks:
38 | - app-network
39 |
40 | # Disabled in case someone is running this on Windows (OK to uncomment if on Mac/Linux)
41 | # cadvisor:
42 | # container_name: cadvisor
43 | # image: google/cadvisor
44 | # volumes:
45 | # - /:/rootfs:ro
46 | # - /var/run:/var/run:rw
47 | # - /sys:/sys:ro
48 | # - /var/lib/docker/:/var/lib/docker:ro
49 | # ports:
50 | # - "8080:8080"
51 | # networks:
52 | # - app-network
53 |
54 | networks:
55 | app-network:
56 | driver: bridge
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # This can be used to run a development version of the Angular and Node containers
2 | # See the readme.md for details on changes that are required in the Angular service
3 |
4 | # Run docker-compose build
5 | # Run docker-compose up
6 | # Live long and prosper
7 |
8 | version: '3.7'
9 |
10 | services:
11 |
12 | nginx:
13 | container_name: nginx-angular
14 | image: nginx-angular
15 | build:
16 | context: .
17 | dockerfile: nginx.dockerfile
18 | volumes:
19 | - ./dist/angular-core-concepts/browser:/usr/share/nginx/html
20 | ports:
21 | - "80:80"
22 | - "443:443"
23 | depends_on:
24 | - node
25 | networks:
26 | - app-network
27 |
28 | node:
29 | container_name: angular-node-service
30 | image: angular-node-service
31 | build:
32 | context: ./server
33 | dockerfile: node.dockerfile
34 | environment:
35 | - NODE_ENV=development
36 | ports:
37 | - "3000:3000"
38 | networks:
39 | - app-network
40 |
41 | # Disabled in case someone is running this on Windows (OK to uncomment if on Mac/Linux)
42 | # cadvisor:
43 | # container_name: cadvisor
44 | # image: google/cadvisor
45 | # volumes:
46 | # - /:/rootfs:ro
47 | # - /var/run:/var/run:rw
48 | # - /sys:/sys:ro
49 | # - /var/lib/docker/:/var/lib/docker:ro
50 | # ports:
51 | # - "8080:8080"
52 | # networks:
53 | # - app-network
54 |
55 | networks:
56 | app-network:
57 | driver: bridge
--------------------------------------------------------------------------------
/src/app/core/sorter.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable({ providedIn: 'root' })
4 | export class SorterService {
5 |
6 | property = '';
7 | direction = 1;
8 |
9 | sort(collection: any[], prop: any) {
10 | this.property = prop;
11 | this.direction = this.direction * -1;
12 |
13 | collection.sort((a: any, b: any) => {
14 | let aVal: any;
15 | let bVal: any;
16 |
17 | // Handle resolving complex properties such as 'state.name' for prop value
18 | if (prop && prop.indexOf('.') > -1) {
19 | aVal = this.resolveProperty(prop, a);
20 | bVal = this.resolveProperty(prop, b);
21 | }
22 | else {
23 | aVal = a[prop];
24 | bVal = b[prop];
25 | }
26 |
27 | // Fix issues that spaces before/after string value can cause such as ' San Francisco'
28 | if (this.isString(aVal)) { aVal = aVal.trim().toUpperCase(); }
29 | if (this.isString(bVal)) { bVal = bVal.trim().toUpperCase(); }
30 |
31 | if (aVal === bVal) {
32 | return 0;
33 | }
34 | else if (aVal > bVal) {
35 | return this.direction * -1;
36 | }
37 | else {
38 | return this.direction * 1;
39 | }
40 | });
41 | }
42 |
43 | isString(val: any): boolean {
44 | return (val && (typeof val === 'string' || val instanceof String));
45 | }
46 |
47 | resolveProperty(path: string, obj: any) {
48 | return path.split('.').reduce(function(prev, curr) {
49 | return (prev ? prev[curr] : undefined);
50 | }, obj || self);
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/customers/customers-list/customers-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, inject } from '@angular/core';
2 |
3 | import { Customer } from '../../shared/interfaces';
4 | import { SorterService } from '../../core/sorter.service';
5 | import { FilterTextboxComponent } from './filter-textbox.component';
6 | import { RouterLink } from '@angular/router';
7 | import { CurrencyPipe } from '@angular/common';
8 | import { CapitalizePipe } from '../../shared/capitalize.pipe';
9 |
10 | @Component({
11 | selector: 'app-customers-list',
12 | standalone: true,
13 | imports: [ CurrencyPipe, CapitalizePipe, FilterTextboxComponent, RouterLink ],
14 | templateUrl: './customers-list.component.html'
15 | })
16 | export class CustomersListComponent {
17 | private _customers: Customer[] = [];
18 | @Input() get customers(): Customer[] {
19 | return this._customers;
20 | }
21 | set customers(value: Customer[]) {
22 | if (value) {
23 | this.filteredCustomers = this._customers = value;
24 | this.calculateOrders();
25 | }
26 | }
27 | filteredCustomers: Customer[] = [];
28 | customersOrderTotal = 0;
29 | currencyCode = 'USD';
30 | sorterService: SorterService = inject(SorterService);
31 |
32 | calculateOrders() {
33 | this.customersOrderTotal = 0;
34 | this.filteredCustomers.forEach((cust: Customer) => {
35 | this.customersOrderTotal += cust.orderTotal!;
36 | });
37 | }
38 |
39 | filter(data: string) {
40 | if (data) {
41 | this.filteredCustomers = this.customers.filter((cust: Customer) => {
42 | return cust.name.toLowerCase().indexOf(data.toLowerCase()) > -1 ||
43 | cust.city.toLowerCase().indexOf(data.toLowerCase()) > -1 ||
44 | cust.orderTotal!.toString().indexOf(data) > -1;
45 | });
46 | } else {
47 | this.filteredCustomers = this.customers;
48 | }
49 | this.calculateOrders();
50 | }
51 |
52 | sort(prop: string) {
53 | this.sorterService.sort(this.filteredCustomers, prop);
54 | }
55 |
56 | customerTrackBy(index: number, customer: Customer) {
57 | return customer.id;
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/core/data.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpClient, HttpErrorResponse } from '@angular/common/http';
3 |
4 | import { Observable, throwError } from 'rxjs';
5 | import { map, catchError } from 'rxjs/operators';
6 |
7 | import { Customer, Order } from '../../app/shared/interfaces';
8 |
9 | @Injectable({ providedIn: 'root' })
10 | export class DataService {
11 | // Use the following properties if running the Docker containers via Docker Compose
12 | // customersUrl = 'http://localhost:3000/api/customers';
13 | // ordersUrl = 'http://localhost:3000/api/orders';
14 |
15 | // Use the following properties if running the app stand-alone with no external dependencies
16 | customersUrl = 'assets/customers.json';
17 | ordersUrl = 'assets/orders.json';
18 |
19 | constructor(private http: HttpClient) { }
20 |
21 | getCustomers(): Observable {
22 | return this.http.get(this.customersUrl)
23 | .pipe(
24 | catchError(this.handleError)
25 | );
26 |
27 | }
28 |
29 | getCustomer(id: number): Observable {
30 | return this.http.get(this.customersUrl)
31 | .pipe(
32 | map(customers => {
33 | const customer = customers.filter((cust: Customer) => cust.id === id);
34 | return (customer && customer.length) ? customer[0] : {} as Customer;
35 | }),
36 | catchError(this.handleError)
37 | );
38 | }
39 |
40 | getOrders(id: number): Observable {
41 | return this.http.get(this.ordersUrl)
42 | .pipe(
43 | map(orders => {
44 | const custOrders = orders.filter((order: Order) => order.customerId === id);
45 | return custOrders;
46 | }),
47 | catchError(this.handleError)
48 | );
49 | }
50 |
51 | private handleError(error: HttpErrorResponse) {
52 | console.error('server error:', error);
53 | if (error.error instanceof Error) {
54 | const errMessage = error.error.message;
55 | return throwError(() => errMessage);
56 | // Use the following instead if using lite-server
57 | // return Observable.throw(err.text() || 'backend server error');
58 | }
59 | return throwError(() => error || 'Node.js server error');
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/server/data/orders.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "customerId": 1,
4 | "orderItems": [
5 | {"id": 1, "productName": "Baseball", "itemCost": 9.99},
6 | {"id": 2, "productName": "Bat", "itemCost": 19.99}
7 | ]
8 | },
9 | {
10 | "customerId": 2,
11 | "orderItems": [
12 | {"id": 3, "productName": "Basketball", "itemCost": 7.99},
13 | {"id": 4, "productName": "Shoes", "itemCost": 199.99}
14 | ]
15 | },
16 | {
17 | "customerId": 3,
18 | "orderItems": [
19 | {"id": 5, "productName": "Frisbee", "itemCost": 2.99},
20 | {"id": 6, "productName": "Hat", "itemCost": 5.99}
21 | ]
22 | },
23 | {
24 | "customerId": 4,
25 | "orderItems": [
26 | {"id": 7, "productName": "Boomerang", "itemCost": 29.99},
27 | {"id": 8, "productName": "Helmet", "itemCost": 19.99},
28 | {"id": 9, "productName": "Kangaroo Saddle", "itemCost": 179.99}
29 | ]
30 | },
31 | {
32 | "customerId": 5,
33 | "orderItems": [
34 | {"id": 10, "productName": "Budgie Smugglers", "itemCost": 19.99},
35 | {"id": 11, "productName": "Swimming Cap", "itemCost": 5.49}
36 | ]
37 | },
38 | {
39 | "customerId": 6,
40 | "orderItems": [
41 | {"id": 12, "productName": "Bow", "itemCost": 399.99},
42 | {"id": 13, "productName": "Arrows", "itemCost": 69.99}
43 | ]
44 | },
45 | {
46 | "customerId": 7,
47 | "orderItems": [
48 | {"id": 14, "productName": "Baseball", "itemCost": 9.99},
49 | {"id": 15, "productName": "Bat", "itemCost": 19.99}
50 | ]
51 | },
52 | {
53 | "customerId": 8,
54 | "orderItems": [
55 | {"id": 16, "productName": "Surfboard", "itemCost": 299.99},
56 | {"id": 17, "productName": "Wax", "itemCost": 5.99},
57 | {"id": 18, "productName": "Shark Repellent", "itemCost": 15.99}
58 | ]
59 | },
60 | {
61 | "customerId": 9,
62 | "orderItems": [
63 | {"id": 19, "productName": "Saddle", "itemCost": 599.99},
64 | {"id": 20, "productName": "Riding cap", "itemCost": 79.99}
65 | ]
66 | },
67 | {
68 | "customerId": 10,
69 | "orderItems": [
70 | {"id": 21, "productName": "Baseball", "itemCost": 9.99},
71 | {"id": 22, "productName": "Bat", "itemCost": 19.99}
72 | ]
73 | }
74 | ]
--------------------------------------------------------------------------------
/src/assets/orders.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "customerId": 1,
4 | "orderItems": [
5 | {"id": 1, "productName": "Baseball", "itemCost": 9.99},
6 | {"id": 2, "productName": "Bat", "itemCost": 19.99}
7 | ]
8 | },
9 | {
10 | "customerId": 2,
11 | "orderItems": [
12 | {"id": 3, "productName": "Basketball", "itemCost": 7.99},
13 | {"id": 4, "productName": "Shoes", "itemCost": 199.99}
14 | ]
15 | },
16 | {
17 | "customerId": 3,
18 | "orderItems": [
19 | {"id": 5, "productName": "Frisbee", "itemCost": 2.99},
20 | {"id": 6, "productName": "Hat", "itemCost": 5.99}
21 | ]
22 | },
23 | {
24 | "customerId": 4,
25 | "orderItems": [
26 | {"id": 7, "productName": "Boomerang", "itemCost": 29.99},
27 | {"id": 8, "productName": "Helmet", "itemCost": 19.99},
28 | {"id": 9, "productName": "Kangaroo Saddle", "itemCost": 179.99}
29 | ]
30 | },
31 | {
32 | "customerId": 5,
33 | "orderItems": [
34 | {"id": 10, "productName": "Budgie Smugglers", "itemCost": 19.99},
35 | {"id": 11, "productName": "Swimming Cap", "itemCost": 5.49}
36 | ]
37 | },
38 | {
39 | "customerId": 6,
40 | "orderItems": [
41 | {"id": 12, "productName": "Bow", "itemCost": 399.99},
42 | {"id": 13, "productName": "Arrows", "itemCost": 69.99}
43 | ]
44 | },
45 | {
46 | "customerId": 7,
47 | "orderItems": [
48 | {"id": 14, "productName": "Baseball", "itemCost": 9.99},
49 | {"id": 15, "productName": "Bat", "itemCost": 19.99}
50 | ]
51 | },
52 | {
53 | "customerId": 8,
54 | "orderItems": [
55 | {"id": 16, "productName": "Surfboard", "itemCost": 299.99},
56 | {"id": 17, "productName": "Wax", "itemCost": 5.99},
57 | {"id": 18, "productName": "Shark Repellent", "itemCost": 15.99}
58 | ]
59 | },
60 | {
61 | "customerId": 9,
62 | "orderItems": [
63 | {"id": 19, "productName": "Saddle", "itemCost": 599.99},
64 | {"id": 20, "productName": "Riding cap", "itemCost": 79.99}
65 | ]
66 | },
67 | {
68 | "customerId": 10,
69 | "orderItems": [
70 | {"id": 21, "productName": "Baseball", "itemCost": 9.99},
71 | {"id": 22, "productName": "Bat", "itemCost": 19.99}
72 | ]
73 | }
74 | ]
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-core-concepts": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:class": {
10 | "skipTests": true
11 | },
12 | "@schematics/angular:component": {
13 | "skipTests": true
14 | },
15 | "@schematics/angular:directive": {
16 | "skipTests": true
17 | },
18 | "@schematics/angular:guard": {
19 | "skipTests": true
20 | },
21 | "@schematics/angular:interceptor": {
22 | "skipTests": true
23 | },
24 | "@schematics/angular:pipe": {
25 | "skipTests": true
26 | },
27 | "@schematics/angular:resolver": {
28 | "skipTests": true
29 | },
30 | "@schematics/angular:service": {
31 | "skipTests": true
32 | }
33 | },
34 | "root": "",
35 | "sourceRoot": "src",
36 | "prefix": "app",
37 | "architect": {
38 | "build": {
39 | "builder": "@angular-devkit/build-angular:application",
40 | "options": {
41 | "outputPath": "dist/angular-core-concepts",
42 | "index": "src/index.html",
43 | "browser": "src/main.ts",
44 | "polyfills": [
45 | "zone.js"
46 | ],
47 | "tsConfig": "tsconfig.app.json",
48 | "assets": [
49 | "src/favicon.ico",
50 | "src/assets"
51 | ],
52 | "styles": [
53 | "src/styles.css"
54 | ],
55 | "scripts": []
56 | },
57 | "configurations": {
58 | "production": {
59 | "budgets": [
60 | {
61 | "type": "initial",
62 | "maximumWarning": "500kb",
63 | "maximumError": "1mb"
64 | },
65 | {
66 | "type": "anyComponentStyle",
67 | "maximumWarning": "2kb",
68 | "maximumError": "4kb"
69 | }
70 | ],
71 | "outputHashing": "all"
72 | },
73 | "development": {
74 | "optimization": false,
75 | "extractLicenses": false,
76 | "sourceMap": true
77 | }
78 | },
79 | "defaultConfiguration": "production"
80 | },
81 | "serve": {
82 | "builder": "@angular-devkit/build-angular:dev-server",
83 | "configurations": {
84 | "production": {
85 | "buildTarget": "angular-core-concepts:build:production"
86 | },
87 | "development": {
88 | "buildTarget": "angular-core-concepts:build:development"
89 | }
90 | },
91 | "defaultConfiguration": "development"
92 | },
93 | "extract-i18n": {
94 | "builder": "@angular-devkit/build-angular:extract-i18n",
95 | "options": {
96 | "buildTarget": "angular-core-concepts:build"
97 | }
98 | },
99 | "test": {
100 | "builder": "@angular-devkit/build-angular:karma",
101 | "options": {
102 | "polyfills": [
103 | "zone.js",
104 | "zone.js/testing"
105 | ],
106 | "tsConfig": "tsconfig.spec.json",
107 | "assets": [
108 | "src/favicon.ico",
109 | "src/assets"
110 | ],
111 | "styles": [
112 | "src/styles.css"
113 | ],
114 | "scripts": []
115 | }
116 | }
117 | }
118 | }
119 | },
120 | "cli": {
121 | "analytics": false
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-core-concepts-server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.5",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
11 | "requires": {
12 | "mime-types": "~2.1.18",
13 | "negotiator": "0.6.1"
14 | }
15 | },
16 | "array-flatten": {
17 | "version": "1.1.1",
18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
20 | },
21 | "body-parser": {
22 | "version": "1.18.2",
23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
24 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
25 | "requires": {
26 | "bytes": "3.0.0",
27 | "content-type": "~1.0.4",
28 | "debug": "2.6.9",
29 | "depd": "~1.1.1",
30 | "http-errors": "~1.6.2",
31 | "iconv-lite": "0.4.19",
32 | "on-finished": "~2.3.0",
33 | "qs": "6.5.1",
34 | "raw-body": "2.3.2",
35 | "type-is": "~1.6.15"
36 | }
37 | },
38 | "bytes": {
39 | "version": "3.0.0",
40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
41 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
42 | },
43 | "content-disposition": {
44 | "version": "0.5.2",
45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
46 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
47 | },
48 | "content-type": {
49 | "version": "1.0.4",
50 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
51 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
52 | },
53 | "cookie": {
54 | "version": "0.3.1",
55 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
56 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
57 | },
58 | "cookie-signature": {
59 | "version": "1.0.6",
60 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
61 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
62 | },
63 | "debug": {
64 | "version": "2.6.9",
65 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
66 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
67 | "requires": {
68 | "ms": "2.0.0"
69 | }
70 | },
71 | "depd": {
72 | "version": "1.1.2",
73 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
74 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
75 | },
76 | "destroy": {
77 | "version": "1.0.4",
78 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
79 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
80 | },
81 | "ee-first": {
82 | "version": "1.1.1",
83 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
84 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
85 | },
86 | "encodeurl": {
87 | "version": "1.0.2",
88 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
89 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
90 | },
91 | "escape-html": {
92 | "version": "1.0.3",
93 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
94 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
95 | },
96 | "etag": {
97 | "version": "1.8.1",
98 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
99 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
100 | },
101 | "express": {
102 | "version": "4.16.3",
103 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
104 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
105 | "requires": {
106 | "accepts": "~1.3.5",
107 | "array-flatten": "1.1.1",
108 | "body-parser": "1.18.2",
109 | "content-disposition": "0.5.2",
110 | "content-type": "~1.0.4",
111 | "cookie": "0.3.1",
112 | "cookie-signature": "1.0.6",
113 | "debug": "2.6.9",
114 | "depd": "~1.1.2",
115 | "encodeurl": "~1.0.2",
116 | "escape-html": "~1.0.3",
117 | "etag": "~1.8.1",
118 | "finalhandler": "1.1.1",
119 | "fresh": "0.5.2",
120 | "merge-descriptors": "1.0.1",
121 | "methods": "~1.1.2",
122 | "on-finished": "~2.3.0",
123 | "parseurl": "~1.3.2",
124 | "path-to-regexp": "0.1.7",
125 | "proxy-addr": "~2.0.3",
126 | "qs": "6.5.1",
127 | "range-parser": "~1.2.0",
128 | "safe-buffer": "5.1.1",
129 | "send": "0.16.2",
130 | "serve-static": "1.13.2",
131 | "setprototypeof": "1.1.0",
132 | "statuses": "~1.4.0",
133 | "type-is": "~1.6.16",
134 | "utils-merge": "1.0.1",
135 | "vary": "~1.1.2"
136 | },
137 | "dependencies": {
138 | "statuses": {
139 | "version": "1.4.0",
140 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
141 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
142 | }
143 | }
144 | },
145 | "finalhandler": {
146 | "version": "1.1.1",
147 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
148 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
149 | "requires": {
150 | "debug": "2.6.9",
151 | "encodeurl": "~1.0.2",
152 | "escape-html": "~1.0.3",
153 | "on-finished": "~2.3.0",
154 | "parseurl": "~1.3.2",
155 | "statuses": "~1.4.0",
156 | "unpipe": "~1.0.0"
157 | },
158 | "dependencies": {
159 | "statuses": {
160 | "version": "1.4.0",
161 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
162 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
163 | }
164 | }
165 | },
166 | "forwarded": {
167 | "version": "0.1.2",
168 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
169 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
170 | },
171 | "fresh": {
172 | "version": "0.5.2",
173 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
174 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
175 | },
176 | "http-errors": {
177 | "version": "1.6.3",
178 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
179 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
180 | "requires": {
181 | "depd": "~1.1.2",
182 | "inherits": "2.0.3",
183 | "setprototypeof": "1.1.0",
184 | "statuses": ">= 1.4.0 < 2"
185 | }
186 | },
187 | "iconv-lite": {
188 | "version": "0.4.19",
189 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
190 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
191 | },
192 | "inherits": {
193 | "version": "2.0.3",
194 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
195 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
196 | },
197 | "ipaddr.js": {
198 | "version": "1.6.0",
199 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
200 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
201 | },
202 | "media-typer": {
203 | "version": "0.3.0",
204 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
205 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
206 | },
207 | "merge-descriptors": {
208 | "version": "1.0.1",
209 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
210 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
211 | },
212 | "methods": {
213 | "version": "1.1.2",
214 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
215 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
216 | },
217 | "mime": {
218 | "version": "1.4.1",
219 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
220 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
221 | },
222 | "mime-db": {
223 | "version": "1.33.0",
224 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
225 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
226 | },
227 | "mime-types": {
228 | "version": "2.1.18",
229 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
230 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
231 | "requires": {
232 | "mime-db": "~1.33.0"
233 | }
234 | },
235 | "ms": {
236 | "version": "2.0.0",
237 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
238 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
239 | },
240 | "negotiator": {
241 | "version": "0.6.1",
242 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
243 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
244 | },
245 | "on-finished": {
246 | "version": "2.3.0",
247 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
248 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
249 | "requires": {
250 | "ee-first": "1.1.1"
251 | }
252 | },
253 | "parseurl": {
254 | "version": "1.3.2",
255 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
256 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
257 | },
258 | "path-to-regexp": {
259 | "version": "0.1.7",
260 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
261 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
262 | },
263 | "proxy-addr": {
264 | "version": "2.0.3",
265 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
266 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
267 | "requires": {
268 | "forwarded": "~0.1.2",
269 | "ipaddr.js": "1.6.0"
270 | }
271 | },
272 | "qs": {
273 | "version": "6.5.1",
274 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
275 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
276 | },
277 | "range-parser": {
278 | "version": "1.2.0",
279 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
280 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
281 | },
282 | "raw-body": {
283 | "version": "2.3.2",
284 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
285 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
286 | "requires": {
287 | "bytes": "3.0.0",
288 | "http-errors": "1.6.2",
289 | "iconv-lite": "0.4.19",
290 | "unpipe": "1.0.0"
291 | },
292 | "dependencies": {
293 | "depd": {
294 | "version": "1.1.1",
295 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
296 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
297 | },
298 | "http-errors": {
299 | "version": "1.6.2",
300 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
301 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
302 | "requires": {
303 | "depd": "1.1.1",
304 | "inherits": "2.0.3",
305 | "setprototypeof": "1.0.3",
306 | "statuses": ">= 1.3.1 < 2"
307 | }
308 | },
309 | "setprototypeof": {
310 | "version": "1.0.3",
311 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
312 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
313 | }
314 | }
315 | },
316 | "safe-buffer": {
317 | "version": "5.1.1",
318 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
319 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
320 | },
321 | "send": {
322 | "version": "0.16.2",
323 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
324 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
325 | "requires": {
326 | "debug": "2.6.9",
327 | "depd": "~1.1.2",
328 | "destroy": "~1.0.4",
329 | "encodeurl": "~1.0.2",
330 | "escape-html": "~1.0.3",
331 | "etag": "~1.8.1",
332 | "fresh": "0.5.2",
333 | "http-errors": "~1.6.2",
334 | "mime": "1.4.1",
335 | "ms": "2.0.0",
336 | "on-finished": "~2.3.0",
337 | "range-parser": "~1.2.0",
338 | "statuses": "~1.4.0"
339 | },
340 | "dependencies": {
341 | "statuses": {
342 | "version": "1.4.0",
343 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
344 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
345 | }
346 | }
347 | },
348 | "serve-static": {
349 | "version": "1.13.2",
350 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
351 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
352 | "requires": {
353 | "encodeurl": "~1.0.2",
354 | "escape-html": "~1.0.3",
355 | "parseurl": "~1.3.2",
356 | "send": "0.16.2"
357 | }
358 | },
359 | "setprototypeof": {
360 | "version": "1.1.0",
361 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
362 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
363 | },
364 | "statuses": {
365 | "version": "1.5.0",
366 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
367 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
368 | },
369 | "type-is": {
370 | "version": "1.6.16",
371 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
372 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
373 | "requires": {
374 | "media-typer": "0.3.0",
375 | "mime-types": "~2.1.18"
376 | }
377 | },
378 | "unpipe": {
379 | "version": "1.0.0",
380 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
381 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
382 | },
383 | "utils-merge": {
384 | "version": "1.0.1",
385 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
386 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
387 | },
388 | "vary": {
389 | "version": "1.1.2",
390 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
391 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
392 | }
393 | }
394 | }
395 |
--------------------------------------------------------------------------------