├── .gitignore ├── LICENSE ├── README.md └── swVehicles ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── angular.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── cart │ │ ├── cart-item │ │ │ ├── cart-item.component.html │ │ │ └── cart-item.component.ts │ │ ├── cart-list │ │ │ └── cart-list.component.ts │ │ ├── cart-shell │ │ │ └── cart-shell.component.ts │ │ ├── cart-total │ │ │ ├── cart-total.component.html │ │ │ └── cart-total.component.ts │ │ ├── cart.service.ts │ │ └── cart.ts │ ├── page-not-found.component.ts │ ├── vehicles │ │ ├── vehicle-classes │ │ │ ├── vehicle-class-data.ts │ │ │ ├── vehicle-class.service.ts │ │ │ └── vehicle-class.ts │ │ ├── vehicle-detail │ │ │ ├── vehicle-detail.component.html │ │ │ └── vehicle-detail.component.ts │ │ ├── vehicle-list │ │ │ ├── vehicle-list.component.html │ │ │ └── vehicle-list.component.ts │ │ ├── vehicle-shell │ │ │ └── vehicle-shell.component.ts │ │ ├── vehicle.service.ts │ │ └── vehicle.ts │ └── welcome │ │ ├── welcome.component.html │ │ └── welcome.component.ts ├── assets │ ├── .gitkeep │ └── Speeder_Bike.png ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deborah Kurata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swVehicles 2 | Sample application depicting reactive programming in a CRUD application. 3 | -------------------------------------------------------------------------------- /swVehicles/.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 | -------------------------------------------------------------------------------- /swVehicles/.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 | -------------------------------------------------------------------------------- /swVehicles/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /swVehicles/.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": "pwa-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 | -------------------------------------------------------------------------------- /swVehicles/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.autoSave": "afterDelay", 3 | "html.format.wrapAttributes": "force-aligned", 4 | // "editor.quickSuggestionsDelay": 1000, 5 | //"editor.hover.enabled": false, 6 | // "editor.parameterHints": false, 7 | } -------------------------------------------------------------------------------- /swVehicles/.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 | -------------------------------------------------------------------------------- /swVehicles/README.md: -------------------------------------------------------------------------------- 1 | # swVehicles 2 | Sample application depicting reactive programming in a CRUD application. 3 | -------------------------------------------------------------------------------- /swVehicles/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "swVehicles": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "swv", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/sw-vehicles", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "src/styles.css" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "initial", 37 | "maximumWarning": "500kb", 38 | "maximumError": "1mb" 39 | }, 40 | { 41 | "type": "anyComponentStyle", 42 | "maximumWarning": "2kb", 43 | "maximumError": "4kb" 44 | } 45 | ], 46 | "outputHashing": "all" 47 | }, 48 | "development": { 49 | "buildOptimizer": false, 50 | "optimization": false, 51 | "vendorChunk": true, 52 | "extractLicenses": false, 53 | "sourceMap": true, 54 | "namedChunks": true 55 | } 56 | }, 57 | "defaultConfiguration": "production" 58 | }, 59 | "serve": { 60 | "builder": "@angular-devkit/build-angular:dev-server", 61 | "configurations": { 62 | "production": { 63 | "browserTarget": "swVehicles:build:production" 64 | }, 65 | "development": { 66 | "browserTarget": "swVehicles:build:development" 67 | } 68 | }, 69 | "defaultConfiguration": "development" 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n", 73 | "options": { 74 | "browserTarget": "swVehicles:build" 75 | } 76 | }, 77 | "test": { 78 | "builder": "@angular-devkit/build-angular:karma", 79 | "options": { 80 | "polyfills": [ 81 | "zone.js", 82 | "zone.js/testing" 83 | ], 84 | "tsConfig": "tsconfig.spec.json", 85 | "assets": [ 86 | "src/favicon.ico", 87 | "src/assets" 88 | ], 89 | "styles": [ 90 | "src/styles.css" 91 | ], 92 | "scripts": [] 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "cli": { 99 | "analytics": "3068aa38-1af3-41e3-b79c-a4984b7e52bf" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /swVehicles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sw-vehicles", 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": "^15.2.0", 14 | "@angular/common": "^15.2.0", 15 | "@angular/compiler": "^15.2.0", 16 | "@angular/core": "^15.2.0", 17 | "@angular/forms": "^15.2.0", 18 | "@angular/platform-browser": "^15.2.0", 19 | "@angular/platform-browser-dynamic": "^15.2.0", 20 | "@angular/router": "^15.2.0", 21 | "bootstrap": "^5.2.3", 22 | "rxjs": "~7.8.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.12.0" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^15.2.2", 28 | "@angular/cli": "~15.2.2", 29 | "@angular/compiler-cli": "^15.2.0", 30 | "@types/jasmine": "~4.3.0", 31 | "jasmine-core": "~4.5.0", 32 | "karma": "~6.4.0", 33 | "karma-chrome-launcher": "~3.1.0", 34 | "karma-coverage": "~2.2.0", 35 | "karma-jasmine": "~5.1.0", 36 | "karma-jasmine-html-reporter": "~2.0.0", 37 | "typescript": "~4.9.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /swVehicles/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | font-size: large; 3 | cursor: pointer; 4 | } 5 | 6 | .navbar-light .navbar-nav .nav-link.active { 7 | color: #007ACC 8 | } 9 | 10 | .navbar-brand { 11 | margin-left: 10px; 12 | } 13 | -------------------------------------------------------------------------------- /swVehicles/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /swVehicles/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { map } from 'rxjs'; 3 | 4 | import { CartService } from './cart/cart.service'; 5 | 6 | @Component({ 7 | selector: 'sw-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.css'] 10 | }) 11 | export class AppComponent { 12 | pageTitle = 'Star Wars Vehicle Sales'; 13 | 14 | cartCount$ = this.cartService.cartItems$.pipe( 15 | map(items => items.length) 16 | ); 17 | 18 | constructor(private cartService: CartService) {} 19 | } 20 | -------------------------------------------------------------------------------- /swVehicles/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { CartShellComponent } from './cart/cart-shell/cart-shell.component'; 8 | import { WelcomeComponent } from './welcome/welcome.component'; 9 | import { PageNotFoundComponent } from './page-not-found.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | HttpClientModule, 18 | RouterModule.forRoot([ 19 | { path: 'welcome', component: WelcomeComponent }, 20 | { 21 | path: 'vehicles', 22 | loadComponent: () => 23 | import('./vehicles/vehicle-shell/vehicle-shell.component').then(m => m.VehicleShellComponent) 24 | }, 25 | { 26 | path: 'cart', 27 | component: CartShellComponent 28 | }, 29 | { path: '', redirectTo: 'welcome', pathMatch: 'full' }, 30 | { path: '**', component: PageNotFoundComponent } 31 | ]) 32 | ], 33 | providers: [], 34 | bootstrap: [AppComponent] 35 | }) 36 | export class AppModule { } 37 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-item/cart-item.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 |
6 |
Name:
7 |
{{item.vehicle.name}}
8 |
9 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
Price (credits):
20 |
{{item.vehicle.cost_in_credits | number:'1.2-2'}}
21 |
22 |
23 |
Class:
24 |
{{item.vehicle.vehicle_class}}
25 |
26 | 27 |
28 |
Quantity:
29 |
30 | 39 |
40 |
41 | 42 |
43 |
Cost:
44 |
{{exPrice$ | async | number:'1.2-2'}}
45 |
46 |
47 |
-------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-item/cart-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { AsyncPipe, DecimalPipe, NgFor, NgIf } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { BehaviorSubject, map, tap } from 'rxjs'; 6 | import { CartItem } from '../cart'; 7 | import { CartService } from '../cart.service'; 8 | 9 | @Component({ 10 | selector: 'sw-cart-item', 11 | standalone: true, 12 | imports: [AsyncPipe, DecimalPipe, FormsModule, NgFor, NgIf], 13 | templateUrl: './cart-item.component.html' 14 | }) 15 | export class CartItemComponent { 16 | // Use a setter to emit whenever a new item is set 17 | _item!: CartItem; 18 | get item(): CartItem { 19 | return this._item; 20 | } 21 | @Input() set item(item: CartItem) { 22 | this._item = item; 23 | this.itemChangedSubject.next(item); 24 | } 25 | 26 | // Hard-coded quantity 27 | // These could instead come from an inventory system 28 | qtyArr = [1, 2, 3, 4, 5, 6, 7, 8]; 29 | 30 | // Item was changed action 31 | private itemChangedSubject = new BehaviorSubject(this.item); 32 | item$ = this.itemChangedSubject.asObservable(); 33 | 34 | // When the item changes, recalculate the extended price 35 | exPrice$ = this.item$.pipe( 36 | map(it => it.quantity * Number(it.vehicle.cost_in_credits)) 37 | ); 38 | 39 | constructor(private cartService: CartService) { } 40 | 41 | onQuantitySelected(quantity: number): void { 42 | // Update the quantity in the item 43 | this.cartService.updateInCart(this.item, quantity); 44 | } 45 | 46 | onRemove(): void { 47 | this.cartService.removeFromCart(this.item); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-list/cart-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AsyncPipe, NgFor } from '@angular/common'; 3 | 4 | import { CartService } from '../cart.service'; 5 | import { CartItemComponent } from "../cart-item/cart-item.component"; 6 | 7 | @Component({ 8 | selector: 'sw-cart-list', 9 | standalone: true, 10 | template: ` 11 |
12 | 13 |
14 | `, 15 | imports: [AsyncPipe, NgFor, CartItemComponent] 16 | }) 17 | export class CartListComponent { 18 | cartItems$ = this.cartService.cartItems$; 19 | 20 | constructor(private cartService: CartService) { } 21 | } 22 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-shell/cart-shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { CartListComponent } from "../cart-list/cart-list.component"; 4 | import { CartTotalComponent } from "../cart-total/cart-total.component"; 5 | 6 | @Component({ 7 | selector: 'sw-cart-shell', 8 | standalone: true, 9 | template: ` 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | `, 19 | imports: [ CartListComponent, CartTotalComponent] 20 | }) 21 | export class CartShellComponent { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-total/cart-total.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 |
6 |
Cart Total
7 |
8 |
9 | 10 |
11 |
12 |
Subtotal:
13 |
{{subTotal$ | async | number:'1.2-2'}}
15 |
16 |
17 |
Delivery:
18 |
{{deliveryFee | number:'1.2-2'}}
21 |
Free
24 |
25 | 26 |
27 |
Estimated Tax:
28 |
{{tax$ | async | number:'1.2-2'}}
30 |
31 | 32 |
33 |
Total:
34 |
{{totalPrice$ | async | number:'1.2-2'}}
36 |
37 |
38 |
39 | 40 | 41 | No items in cart 42 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart-total/cart-total.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AsyncPipe, DecimalPipe, NgIf } from '@angular/common'; 3 | 4 | import { CartService } from '../cart.service'; 5 | 6 | @Component({ 7 | selector: 'sw-cart-total', 8 | standalone: true, 9 | imports: [AsyncPipe, DecimalPipe, NgIf], 10 | templateUrl: './cart-total.component.html' 11 | }) 12 | export class CartTotalComponent { 13 | 14 | cartItems$ = this.cartService.cartItems$; 15 | 16 | subTotal$ = this.cartService.subTotal$; 17 | 18 | deliveryFee$ = this.cartService.deliveryFee$; 19 | 20 | tax$ = this.cartService.tax$; 21 | 22 | totalPrice$ = this.cartService.totalPrice$; 23 | 24 | constructor(private cartService: CartService) { } 25 | } 26 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { combineLatest, map, scan, shareReplay, Subject } from "rxjs"; 3 | import { Vehicle } from "../vehicles/vehicle"; 4 | import { Action, CartItem } from "./cart"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class CartService { 10 | 11 | // Add item action 12 | private itemSubject = new Subject>(); 13 | itemAction$ = this.itemSubject.asObservable(); 14 | 15 | cartItems$ = this.itemAction$ 16 | .pipe( 17 | scan((items, itemAction) => 18 | this.modifyCart(items, itemAction), [] as CartItem[]), 19 | shareReplay(1) 20 | ); 21 | 22 | // Total up the extended price for each item 23 | subTotal$ = this.cartItems$.pipe( 24 | map(items => items.reduce((a, b) => a + (b.quantity * Number(b.vehicle.cost_in_credits)), 0)), 25 | ); 26 | 27 | // Delivery is free if spending more than 100,000 credits 28 | deliveryFee$ = this.subTotal$.pipe( 29 | map((t) => (t < 100000 ? 999 : 0)) 30 | ); 31 | 32 | // Tax could be based on shipping address zip code 33 | tax$ = this.subTotal$.pipe( 34 | map((t) => Math.round(t * 10.75) / 100) 35 | ); 36 | 37 | // Total price 38 | totalPrice$ = combineLatest([ 39 | this.subTotal$, 40 | this.deliveryFee$, 41 | this.tax$, 42 | ]).pipe(map(([st, d, t]) => st + d + t)); 43 | 44 | // Add the vehicle to the cart as an Action 45 | addToCart(vehicle: Vehicle): void { 46 | this.itemSubject.next({ 47 | item: { vehicle, quantity: 1 }, 48 | action: 'add' 49 | }); 50 | } 51 | 52 | // Remove the item from the cart 53 | removeFromCart(cartItem: CartItem): void { 54 | this.itemSubject.next({ 55 | item: { vehicle: cartItem.vehicle, quantity: 0 }, 56 | action: 'delete' 57 | }); 58 | } 59 | 60 | updateInCart(cartItem: CartItem, quantity: number) { 61 | this.itemSubject.next({ 62 | item: { vehicle: cartItem.vehicle, quantity }, 63 | action: 'update' 64 | }); 65 | } 66 | 67 | // Return the updated array of cart items 68 | private modifyCart(items: CartItem[], operation: Action): CartItem[] { 69 | if (operation.action === 'add') { 70 | return [...items, operation.item]; 71 | } else if (operation.action === 'update') { 72 | return items.map(item => item.vehicle.name === operation.item.vehicle.name ? operation.item : item) 73 | } else if (operation.action === 'delete') { 74 | return items.filter(item => item.vehicle.name !== operation.item.vehicle.name); 75 | } 76 | return [...items]; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /swVehicles/src/app/cart/cart.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { Vehicle } from "../vehicles/vehicle"; 3 | 4 | export interface Cart { 5 | cartItems: CartItem[] 6 | } 7 | 8 | export interface CartItem { 9 | vehicle: Vehicle; 10 | quantity: number; 11 | } 12 | 13 | // You could move this to a shared file 14 | // and reuse it for every entity in the application 15 | type ActionType = 'add' | 'update' | 'delete'; 16 | 17 | export interface Action { 18 | item: T; 19 | action: ActionType; 20 | } 21 | 22 | // Test data for easier debugging 23 | export const testCartItems$: Observable = of([ 24 | { 25 | vehicle: { 26 | "name": "Vulture Droid", 27 | "model": "Vulture-class droid starfighter", 28 | "manufacturer": "Haor Chall Engineering, Baktoid Armor Workshop", 29 | "cost_in_credits": "100550", 30 | "crew": 0, 31 | "passengers": 0, 32 | "cargo_capacity": 0, 33 | "vehicle_class": "starfighter", 34 | "films": [ 35 | "https://swapi.py4e.com/api/films/4/", 36 | "https://swapi.py4e.com/api/films/6/" 37 | ] 38 | }, 39 | quantity: 2 40 | }, 41 | { 42 | vehicle: { 43 | "name": "AT-AT", 44 | "model": "All Terrain Armored Transport", 45 | "manufacturer": "Kuat Drive Yards, Imperial Department of Military Research", 46 | "cost_in_credits": "200988", 47 | "crew": 5, 48 | "passengers": 40, 49 | "cargo_capacity": 1000, 50 | "vehicle_class": "assault walker", 51 | "films": [ 52 | "https://swapi.py4e.com/api/films/2/", 53 | "https://swapi.py4e.com/api/films/3/" 54 | ] 55 | }, 56 | quantity: 1, 57 | } 58 | ]); 59 | -------------------------------------------------------------------------------- /swVehicles/src/app/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | standalone: true, 5 | template: ` 6 |

This is not the page you were looking for!

7 | ` 8 | }) 9 | export class PageNotFoundComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-classes/vehicle-class-data.ts: -------------------------------------------------------------------------------- 1 | import { VehicleClass } from "./vehicle-class"; 2 | 3 | export class VehicleClassData { 4 | 5 | // The API used didn't have an easy way to retrieve vehicle classification data 6 | // so they are hard-coded here 7 | static classes: VehicleClass[] = [ 8 | { 9 | name: 'wheeled' 10 | }, 11 | { 12 | name: 'repulsorcraft' 13 | }, 14 | { 15 | name: 'starfighter' 16 | }, 17 | { 18 | name: 'airspeeder' 19 | }, 20 | { 21 | name: 'speeder' 22 | }, 23 | { 24 | name: 'bomber' 25 | }, 26 | { 27 | name: 'walker' 28 | }, 29 | { 30 | name: 'barge' 31 | }, 32 | { 33 | name: 'transport' 34 | }, 35 | { 36 | name: 'tank' 37 | } 38 | ]; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-classes/vehicle-class.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { shareReplay, of, map } from 'rxjs'; 4 | import { VehicleClassData } from './vehicle-class-data'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class VehicleClassService { 10 | 11 | // All vehicle classifications 12 | // Sort on the name 13 | vehicleClassifications$ = of(VehicleClassData.classes).pipe( 14 | map(classes => classes.sort((a, b) => a.name.localeCompare(b.name))), 15 | shareReplay(1) 16 | ); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-classes/vehicle-class.ts: -------------------------------------------------------------------------------- 1 | export interface VehicleClass { 2 | name: string; 3 | description?: string; 4 | } -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-detail/vehicle-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 |
4 |
5 | {{vm.pageTitle}} 6 |
7 | 8 |
10 |
11 |
Name:
12 |
{{vm.vehicle.name}}
13 |
14 | 17 |
18 |
19 |
20 |
Classification:
21 |
{{vm.vehicle.vehicle_class}}
22 |
23 |
24 |
Price (in credits):
25 |
{{vm.vehicle.cost_in_credits | number:'1.2-2'}}
26 |
27 |
28 |
Crew:
29 |
{{vm.vehicle.crew}}
30 |
31 |
32 |
Passengers:
33 |
{{vm.vehicle.passengers}}
34 |
35 |
36 |
Cargo Capacity:
37 |
{{vm.vehicle.cargo_capacity}}
38 |
39 | 40 |
41 |
Films:
42 |
43 |
    44 |
  • {{film.title}}
  • 46 |
47 |
48 |
49 | 50 |
51 |
52 |
-------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-detail/vehicle-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AsyncPipe, DecimalPipe, NgFor, NgIf } from '@angular/common'; 3 | import { catchError, combineLatest, EMPTY, map, Subject, tap } from 'rxjs'; 4 | import { CartService } from 'src/app/cart/cart.service'; 5 | import { Vehicle } from '../vehicle'; 6 | import { VehicleService } from '../vehicle.service'; 7 | 8 | @Component({ 9 | selector: 'sw-vehicle-detail', 10 | standalone: true, 11 | imports: [AsyncPipe, NgFor, NgIf, DecimalPipe], 12 | templateUrl: './vehicle-detail.component.html' 13 | }) 14 | export class VehicleDetailComponent { 15 | private errorMessageSubject = new Subject(); 16 | errorMessage$ = this.errorMessageSubject.asObservable(); 17 | 18 | selectedVehicle$ = this.vehicleService.selectedVehicle$.pipe( 19 | catchError(err => { 20 | this.errorMessageSubject.next(err); 21 | return EMPTY; 22 | }) 23 | ); 24 | 25 | pageTitle$ = this.selectedVehicle$.pipe( 26 | map(vehicle => vehicle ? `Detail for: ${vehicle.name}` : null) 27 | ) 28 | 29 | vehicleFilms$ = this.vehicleService.vehicleFilms$.pipe( 30 | catchError(err => { 31 | this.errorMessageSubject.next(err); 32 | return EMPTY; 33 | }) 34 | ); 35 | 36 | vm$ = combineLatest([ 37 | this.selectedVehicle$, 38 | this.vehicleFilms$, 39 | this.pageTitle$ 40 | ]) 41 | .pipe( 42 | map(([vehicle, films, pageTitle]) => 43 | ({ vehicle, films, pageTitle })) 44 | ); 45 | 46 | constructor(private vehicleService: VehicleService, 47 | private cartService: CartService) { } 48 | 49 | addToCart(vehicle: Vehicle) { 50 | this.cartService.addToCart(vehicle); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-list/vehicle-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{pageTitle}} 4 |
5 | 6 |
8 | 9 | 17 | 18 |
19 | 26 |
27 |
28 | 29 |
31 | {{errorMessage }} 32 |
33 |
-------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-list/vehicle-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgFor, NgClass, NgIf, AsyncPipe } from '@angular/common'; 3 | import { combineLatest, EMPTY, Subject } from 'rxjs'; 4 | import { catchError, map } from 'rxjs/operators'; 5 | import { VehicleClassService } from '../vehicle-classes/vehicle-class.service'; 6 | import { VehicleService } from '../vehicle.service'; 7 | 8 | @Component({ 9 | selector: 'sw-vehicle-list', 10 | standalone: true, 11 | imports: [AsyncPipe, NgClass, NgFor, NgIf], 12 | templateUrl: './vehicle-list.component.html' 13 | }) 14 | export class VehicleListComponent { 15 | pageTitle = 'Vehicles'; 16 | private errorMessageSubject = new Subject(); 17 | errorMessage$ = this.errorMessageSubject.asObservable(); 18 | 19 | // Categories for drop down list 20 | vehicleClasses$ = this.vehicleClassService.vehicleClassifications$ 21 | .pipe( 22 | catchError(err => { 23 | this.errorMessageSubject.next(err); 24 | return EMPTY; 25 | })); 26 | 27 | // Vehicles filtered by the selected classification 28 | vehicles$ = this.vehicleService.vehicles$ 29 | .pipe( 30 | catchError(err => { 31 | this.errorMessageSubject.next(err); 32 | return EMPTY; 33 | })); 34 | 35 | selectedVehicle$ = this.vehicleService.selectedVehicle$.pipe( 36 | catchError(err => { 37 | this.errorMessageSubject.next(err); 38 | return EMPTY; 39 | }) 40 | ); 41 | 42 | vehicleFilms$ = this.vehicleService.vehicleFilms$.pipe( 43 | catchError(err => { 44 | this.errorMessageSubject.next(err); 45 | return EMPTY; 46 | }) 47 | ); 48 | 49 | // Make it easier to work with the result in the UI 50 | vm$ = combineLatest([this.vehicles$, this.selectedVehicle$, this.vehicleClasses$]).pipe( 51 | map(([vehicles, selectedVehicle, vehicleClasses]) => ({ 52 | vehicles, 53 | selectedVehicle, 54 | vehicleClasses 55 | })) 56 | ); 57 | 58 | constructor(private vehicleService: VehicleService, private vehicleClassService: VehicleClassService) { } 59 | 60 | // When a vehicle is selected, emit the selected vehicle name 61 | onSelected(vehicleName: string): void { 62 | this.vehicleService.vehicleSelected(vehicleName); 63 | } 64 | 65 | // When a vehicle classification is selected, 66 | // emit the selected vehicle class 67 | onVehicleClassSelected(vehicleClass: string): void { 68 | this.vehicleService.vehicleClassSelected(vehicleClass); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle-shell/vehicle-shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { VehicleListComponent } from "../vehicle-list/vehicle-list.component"; 3 | import { VehicleDetailComponent } from "../vehicle-detail/vehicle-detail.component"; 4 | 5 | @Component({ 6 | selector: 'sw-vehicle-shell', 7 | standalone: true, 8 | template: ` 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | `, 18 | imports: [VehicleListComponent, VehicleDetailComponent] 19 | }) 20 | export class VehicleShellComponent { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { 4 | BehaviorSubject, 5 | catchError, 6 | combineLatest, 7 | EMPTY, 8 | expand, 9 | filter, 10 | forkJoin, 11 | map, 12 | mergeMap, 13 | Observable, 14 | of, 15 | reduce, 16 | shareReplay, 17 | switchMap, 18 | tap, 19 | throwError 20 | } from 'rxjs'; 21 | import { Film, Vehicle, VehicleResponse } from './vehicle'; 22 | 23 | @Injectable({ 24 | providedIn: 'root' 25 | }) 26 | export class VehicleService { 27 | private url = 'https://swapi.py4e.com/api/vehicles'; 28 | 29 | // Action stream 30 | private vehicleSelectedSubject = new BehaviorSubject(''); 31 | vehicleSelected$ = this.vehicleSelectedSubject.asObservable(); 32 | 33 | // Action stream 34 | private vehicleClassSubject = new BehaviorSubject(''); 35 | vehicleClassSelected$ = this.vehicleClassSubject.asObservable(); 36 | 37 | // All pages of vehicles 38 | allVehicles$ = this.http.get(this.url).pipe( 39 | expand(data => data.next ? this.http.get(data.next) : EMPTY), 40 | reduce((acc, data) => acc.concat(data.results), [] as Vehicle[]) 41 | ); 42 | 43 | // One page of vehicles with their films 44 | // Not currently used in the app 45 | // To try it out, subscribe in the constructor below. 46 | vehiclesWithFilms$ = this.http.get(this.url).pipe( 47 | map(data => data.results as Vehicle[]), 48 | mergeMap(vehicles => forkJoin(vehicles.map(vehicle => 49 | forkJoin(vehicle.films.map(film => this.http.get(film).pipe( 50 | map(f => f.title), 51 | ))).pipe( 52 | map(films => ({ ...vehicle, films } as Vehicle)) 53 | ), 54 | ))) 55 | ); 56 | 57 | // Vehicles filtered by the selected classification 58 | // NOTE: This particular API does not provide a feature to search by vehicle class 59 | // Rather, the code gets all vehicles, and filters by the class. 60 | vehicles$ = combineLatest([ 61 | this.allVehicles$, 62 | this.vehicleClassSelected$.pipe( 63 | // When the classification changes, clear the selected vehicle 64 | tap(() => this.vehicleSelected('')) 65 | ) 66 | ]) 67 | .pipe( 68 | map(([vehicles, selectedVehicleClass]) => 69 | vehicles.filter(v => 70 | selectedVehicleClass ? v.vehicle_class.toLocaleLowerCase().includes(selectedVehicleClass.toLocaleLowerCase()) : true 71 | )), 72 | catchError(this.handleError) 73 | ); 74 | 75 | // First page of vehicles 76 | firstPageVehicles$ = this.http.get(this.url).pipe( 77 | map(data => data.results), 78 | catchError(this.handleError) 79 | ); 80 | 81 | selectedVehicle$ = this.vehicleSelected$.pipe( 82 | switchMap(vehicleName => 83 | vehicleName.length ? 84 | this.http.get(`${this.url}?search=${vehicleName}`).pipe( 85 | map(data => data.results[0]), 86 | // Fill in a random price for any missing the price 87 | // (We can't modify the backend in this demo) 88 | map(v => ({ 89 | ...v, 90 | cost_in_credits: isNaN(Number(v.cost_in_credits)) ? String(Math.random() * 100000) : v.cost_in_credits 91 | }) as Vehicle), 92 | catchError(this.handleError) 93 | ) : of(null) 94 | ), 95 | shareReplay(1), 96 | ); 97 | 98 | vehicleFilms$ = this.selectedVehicle$.pipe( 99 | filter(Boolean), 100 | switchMap(vehicle => 101 | forkJoin(vehicle.films.map(link => 102 | this.http.get(link))) 103 | ) 104 | ); 105 | 106 | constructor(private http: HttpClient) { 107 | } 108 | 109 | vehicleSelected(vehicleName: string) { 110 | this.vehicleSelectedSubject.next(vehicleName); 111 | } 112 | 113 | // When a vehicle classification is selected, 114 | // emit the selected vehicle class 115 | vehicleClassSelected(vehicleClass: string): void { 116 | this.vehicleClassSubject.next(vehicleClass); 117 | } 118 | 119 | private handleError(err: HttpErrorResponse): Observable { 120 | // in a real world app, we may send the server to some remote logging infrastructure 121 | // instead of just logging it to the console 122 | let errorMessage = ''; 123 | if (err.error instanceof ErrorEvent) { 124 | // A client-side or network error occurred. Handle it accordingly. 125 | errorMessage = `An error occurred: ${err.error.message}`; 126 | } else { 127 | // The backend returned an unsuccessful response code. 128 | // The response body may contain clues as to what went wrong, 129 | errorMessage = `Server returned code: ${err.status}, error message is: ${err.message 130 | }`; 131 | } 132 | console.error(errorMessage); 133 | return throwError(() => errorMessage); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /swVehicles/src/app/vehicles/vehicle.ts: -------------------------------------------------------------------------------- 1 | export interface VehicleResponse { 2 | count: number; 3 | next: string; 4 | previous: string; 5 | results: Vehicle[] 6 | } 7 | 8 | export interface Vehicle { 9 | cargo_capacity: number; 10 | crew: number; 11 | name: string; 12 | model: string; 13 | manufacturer: string, 14 | cost_in_credits: string 15 | passengers: number; 16 | vehicle_class: string; 17 | films: string[]; 18 | price?: number; 19 | } 20 | 21 | export interface Film { 22 | title: string; 23 | } 24 | -------------------------------------------------------------------------------- /swVehicles/src/app/welcome/welcome.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{pageTitle}} 4 |
5 |
6 |
7 |
8 | 11 |
12 | 13 |
Developed by:
14 |
15 |

Deborah Kurata

16 |
17 | 18 |
@deborahkurata
19 |
20 | YouTube Channel 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /swVehicles/src/app/welcome/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'sw-welcome', 5 | standalone: true, 6 | imports: [], 7 | templateUrl: './welcome.component.html' 8 | }) 9 | export class WelcomeComponent { 10 | public pageTitle = 'Welcome to Star Wars Vehicle Sales'; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /swVehicles/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/Angular-ReactiveDevelopment/baba63b82ada1bce5781d6209707d4961b0b3301/swVehicles/src/assets/.gitkeep -------------------------------------------------------------------------------- /swVehicles/src/assets/Speeder_Bike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/Angular-ReactiveDevelopment/baba63b82ada1bce5781d6209707d4961b0b3301/swVehicles/src/assets/Speeder_Bike.png -------------------------------------------------------------------------------- /swVehicles/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/Angular-ReactiveDevelopment/baba63b82ada1bce5781d6209707d4961b0b3301/swVehicles/src/favicon.ico -------------------------------------------------------------------------------- /swVehicles/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SwVehicles 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /swVehicles/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /swVehicles/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~bootstrap/dist/css/bootstrap.min.css"; 3 | 4 | div.card-header { 5 | font-size: large; 6 | } 7 | 8 | div.card { 9 | margin-top: 10px; 10 | margin-left: 10px; 11 | } 12 | 13 | .table { 14 | margin-top: 10px; 15 | } 16 | -------------------------------------------------------------------------------- /swVehicles/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 | -------------------------------------------------------------------------------- /swVehicles/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 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 | -------------------------------------------------------------------------------- /swVehicles/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 | --------------------------------------------------------------------------------