├── 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 | 9 | 10 | 11 | 12 |
{{ orderItem.productName }}{{ orderItem.itemCost | currency:'USD':'symbol' }}
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 | 10 | 11 | 12 | } 13 | } 14 |
{{ orderItem.productName }}{{ orderItem.itemCost | currency:'USD':'symbol' }}
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 | 8 | 9 | 10 | 11 | 12 | 13 | @for (cust of filteredCustomers; track cust.id) { 14 | 15 | 20 | 21 | 23 | } 24 | @empty { 25 | 26 | 27 | 28 | } 29 | 30 | @if (this.filteredCustomers.length) { 31 | 32 | 33 | 36 | 37 | } 38 | @else { 39 | 40 | 41 | 42 | } 43 | 44 |
NameCityOrder Total
16 | 17 | {{ cust.name | capitalize }} 18 | 19 | {{ cust.city }}{{ cust.orderTotal | currency:currencyCode:'symbol' }} 22 |
There are no orders.
  34 | {{ customersOrderTotal | currency:currencyCode:'symbol' }} 35 |
No customers found
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 | --------------------------------------------------------------------------------