diff --git a/_course-resources/cart/cart.component.css b/_course-resources/cart/cart.component.css new file mode 100644 index 0000000..b9aa7a1 --- /dev/null +++ b/_course-resources/cart/cart.component.css @@ -0,0 +1,94 @@ +.container { + display: flex; + flex-direction: column; +} + +.header { + align-self: center; +} + +.empty-cart { + margin: 0 100px; + align-self: center; + font-size: 20px; + margin-top: 20px; +} + +.cart { + margin: 0 100px; + border-top: 2px solid #999; +} + +.cart-item { + border-bottom: 2px solid #999; +} + +.total { + margin: 25px 175px 0 0; + align-self: right; + font-size: 25px; + text-align: right; +} + +/* Product Details */ +.product { + display: flex; + justify-content: space-between; + padding: 20px 25px; +} + +.product .product-details { + display: flex; + align-items: center; +} + + +.product img { + width: 125px; +} + +.product .product-info { + margin-left: 25px; +} + +.product .name { + font-size: 22px; + font-weight: bold; +} + +.product .description { + margin-top: 3px; + font-size: 18px; +} + +.product .category { + margin-top: 20px; + color: #777; +} + +.product .price { + display: flex; + flex-direction: column; + font-size: 25px; + justify-content: space-around; + align-items: center; + min-width: 190px; + color: #555; + border-left: 2px solid #aaa; + margin-left: 50px; +} + +.product .price button { + padding: 10px; + width: 100px; +} + +.discount { + margin-top: -15px; + color: #d25ca1; +} + +.strikethrough { + text-decoration: line-through; + font-size: 18px; +} \ No newline at end of file diff --git a/_course-resources/cart/cart.component.html b/_course-resources/cart/cart.component.html new file mode 100644 index 0000000..2bea18d --- /dev/null +++ b/_course-resources/cart/cart.component.html @@ -0,0 +1,31 @@ +
+

Your Cart

+ +
+ You have no items in your cart +
+ +
Total: {{ cartTotal | currency }}
+
\ No newline at end of file diff --git a/_course-resources/cart/cart.component.spec.ts b/_course-resources/cart/cart.component.spec.ts new file mode 100644 index 0000000..0dd9328 --- /dev/null +++ b/_course-resources/cart/cart.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CartComponent } from './cart.component'; + +describe('CartComponent', () => { + let component: CartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CartComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/_course-resources/cart/cart.component.ts b/_course-resources/cart/cart.component.ts new file mode 100644 index 0000000..e7a6f00 --- /dev/null +++ b/_course-resources/cart/cart.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; +import { IProduct } from '../catalog/product.model'; +import { CartService } from './cart.service'; + +@Component({ + selector: 'bot-cart', + templateUrl: './cart.component.html', + styleUrls: ['./cart.component.css'], +}) +export class CartComponent implements OnInit { + private cart: IProduct[] = []; + constructor(private cartService: CartService) { } + + ngOnInit() { + this.cartService.getCart().subscribe({ + next: (cart) => (this.cart = cart), + }); + } + + get cartItems() { + return this.cart; + } + + get cartTotal() { + return this.cart.reduce((prev, next) => { + let discount = next.discount && next.discount > 0 ? 1 - next.discount : 1; + return prev + next.price * discount; + }, 0); + } + + removeFromCart(product: IProduct) { + this.cartService.remove(product); + } + + getImageUrl(product: IProduct) { + if (!product) return ''; + return '/assets/images/robot-parts/' + product.imageName; + } +} diff --git a/_course-resources/cart/cart.service.spec.ts b/_course-resources/cart/cart.service.spec.ts new file mode 100644 index 0000000..cb4a750 --- /dev/null +++ b/_course-resources/cart/cart.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CartService } from './cart.service'; + +describe('CartService', () => { + let service: CartService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CartService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/_course-resources/cart/cart.service.ts b/_course-resources/cart/cart.service.ts new file mode 100644 index 0000000..0a481c4 --- /dev/null +++ b/_course-resources/cart/cart.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; + +import { IProduct } from '../catalog/product.model'; + +@Injectable({ + providedIn: 'root', +}) +export class CartService { + private cart: BehaviorSubject = new BehaviorSubject([]); + + constructor(private http: HttpClient) { + this.http.get('/api/cart').subscribe({ + next: (cart) => this.cart.next(cart), + }); + } + + getCart(): Observable { + return this.cart.asObservable(); + } + + add(product: IProduct) { + const newCart = [...this.cart.getValue(), product]; + this.cart.next(newCart); + this.http.post('/api/cart', newCart).subscribe(() => { + console.log('added ' + product.name + ' to cart!'); + }); + } + + remove(product: IProduct) { + let newCart = this.cart.getValue().filter((i) => i !== product); + this.cart.next(newCart); + this.http.post('/api/cart', newCart).subscribe(() => { + console.log('removed ' + product.name + ' from cart!'); + }); + } +} diff --git a/_course-resources/styles.css b/_course-resources/styles.css index 5199427..52e169a 100644 --- a/_course-resources/styles.css +++ b/_course-resources/styles.css @@ -15,6 +15,11 @@ a.active { border-bottom: 2px solid #d25ca1; } +a.button.active { + padding-bottom: 5px; + border-bottom: 2px solid #d25ca1; +} + a:visited { color: #444; } diff --git a/_course-resources/template-form-controls/template-form-controls.component.css b/_course-resources/template-form-controls/template-form-controls.component.css new file mode 100644 index 0000000..03f336a --- /dev/null +++ b/_course-resources/template-form-controls/template-form-controls.component.css @@ -0,0 +1,59 @@ +.container { + display: flex; + justify-content: space-around; +} + +.form { + display: flex; + padding: 30px 50px 50px 50px; + flex-direction: column; + margin-top: 25px; + border: 1px solid #ddd; + border-radius: 15px; +} + +.control { + display: flex; + flex-direction: column; +} + +.control { + margin-top: 25px; +} + +.control div { + font-size: 18px; +} + +.control select { + padding: 15px; + font-size: 18px; +} + +.control input { + display: inline-block; +} + +.header { + font-size: 30px; + align-self: center; + color: #555; +} + +.sub-text { + font-size: 18px; + align-self: center; + margin-bottom: 15px; + color: #444; +} + +.form .buttons { + margin-top: 10px; + display: inline-flex; + justify-content: flex-end; + font-size: 16px; +} + +input.error { + border: 1px solid #d25ca1; +} \ No newline at end of file diff --git a/_course-resources/template-form-controls/template-form-controls.component.html b/_course-resources/template-form-controls/template-form-controls.component.html new file mode 100644 index 0000000..f55ba62 --- /dev/null +++ b/_course-resources/template-form-controls/template-form-controls.component.html @@ -0,0 +1,45 @@ +
+
+
Form Control Examples
+ +
+ +
Value: {{textInput}} Type: {{getType(textInput)}}
+
+ +
+ +
Value: {{numericInput}} Type: {{getType(numericInput)}}
+
+ +
+ +
Value: {{stringInput}} Type: {{getType(stringInput)}}
+
+ +
+ +
Value: {{numericSelect}} Type: {{getType(numericSelect)}}
+
+ +
+
+ Radio One +
+
+ Radio Two +
+
Value: {{radioInput}} Type: {{getType(radioInput)}}
+
+ +
+
Checkbox Input
+
Value: {{checkboxInput}} Type: {{getType(checkboxInput)}}
+
+
+
\ No newline at end of file diff --git a/_course-resources/template-form-controls/template-form-controls.component.spec.ts b/_course-resources/template-form-controls/template-form-controls.component.spec.ts new file mode 100644 index 0000000..3ffad6e --- /dev/null +++ b/_course-resources/template-form-controls/template-form-controls.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TemplateFormControlsComponent } from './template-form-controls.component'; + +describe('TemplateFormControlsComponent', () => { + let component: TemplateFormControlsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TemplateFormControlsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TemplateFormControlsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/_course-resources/template-form-controls/template-form-controls.component.ts b/_course-resources/template-form-controls/template-form-controls.component.ts new file mode 100644 index 0000000..11f0293 --- /dev/null +++ b/_course-resources/template-form-controls/template-form-controls.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'bot-template-form-controls', + templateUrl: './template-form-controls.component.html', + styleUrls: ['./template-form-controls.component.css'], +}) +export class TemplateFormControlsComponent implements OnInit { + textInput: string = ''; + numericInput: number = 0; + stringInput: string = ''; + numericSelect: number = 0; + checkboxInput: boolean = false; + radioInput: number | null = null; + + selectOptions: any[] = [ + { text: 'Option One', value: 1 }, + { text: 'Option Two', value: 2 }, + ]; + constructor() { } + + ngOnInit(): void { } + + getType(value: any) { + if (value === null || value === undefined) return ''; + + console.log('ns', this.numericSelect); + return typeof value; + } +} diff --git a/_course-resources/user/sign-in/sign-in.component.css b/_course-resources/user/sign-in/sign-in.component.css new file mode 100644 index 0000000..e16a118 --- /dev/null +++ b/_course-resources/user/sign-in/sign-in.component.css @@ -0,0 +1,61 @@ +.container { + display: flex; + justify-content: space-around; +} + +.form { + display: flex; + padding: 30px 50px 50px 50px; + flex-direction: column; + margin-top: 25px; + border: 1px solid #ddd; + border-radius: 15px; +} + +.form input { + margin-top: 25px; +} + +.logo { + width: 150px; + align-self: center; + margin-bottom: 15px; +} + +.sign-in { + font-size: 30px; + align-self: center; + color: #555; +} + +.sub-text { + font-size: 18px; + align-self: center; + margin-bottom: 15px; + color: #444; +} + +.form .buttons { + margin-top: 10px; + display: inline-flex; + justify-content: flex-end; + font-size: 16px; +} + +input.error { + border: 1px solid #d25ca1; +} + +em.error { + padding: 2px 5px; + background-color: #d25ca1; + color: white; + font-style: normal; +} + +.signInError { + margin: 25px 0; + color: red; + font-size: 18px; + text-align: center; +} diff --git a/_course-resources/user/sign-in/sign-in.component.html b/_course-resources/user/sign-in/sign-in.component.html new file mode 100644 index 0000000..e8ee1d6 --- /dev/null +++ b/_course-resources/user/sign-in/sign-in.component.html @@ -0,0 +1,22 @@ +
+
+ + +
to acquire awesome bots
+ + +
+ +
+
+
diff --git a/_course-resources/user/sign-in/sign-in.component.spec.ts b/_course-resources/user/sign-in/sign-in.component.spec.ts new file mode 100644 index 0000000..c799467 --- /dev/null +++ b/_course-resources/user/sign-in/sign-in.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SignInComponent } from './sign-in.component'; + +describe('SignInComponent', () => { + let component: SignInComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SignInComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SignInComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/_course-resources/user/sign-in/sign-in.component.ts b/_course-resources/user/sign-in/sign-in.component.ts new file mode 100644 index 0000000..63dbeda --- /dev/null +++ b/_course-resources/user/sign-in/sign-in.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'bot-sign-in', + templateUrl: './sign-in.component.html', + styleUrls: ['./sign-in.component.css'], +}) +export class SignInComponent { + + constructor() { } + +} diff --git a/_course-resources/user/user.model.ts b/_course-resources/user/user.model.ts new file mode 100644 index 0000000..ef036ec --- /dev/null +++ b/_course-resources/user/user.model.ts @@ -0,0 +1,11 @@ +export interface IUser { + firstName: string; + lastName: string; + email: string; + password?: string; +} + +export interface IUserCredentials { + email: string; + password: string; +} diff --git a/_course-resources/user/user.service.ts b/_course-resources/user/user.service.ts new file mode 100644 index 0000000..0476534 --- /dev/null +++ b/_course-resources/user/user.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, map, Observable } from 'rxjs'; + +import { IUser, IUserCredentials } from './user.model'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + private user: BehaviorSubject; + + constructor(private http: HttpClient) { + this.user = new BehaviorSubject(null); + } + + getUser(): Observable { + return this.user; + } + + signIn(credentials: IUserCredentials): Observable { + return this.http + .post('/api/sign-in', credentials) + .pipe(map((user: IUser) => { + this.user.next(user); + return user; + })); + } + + signOut() { + this.user.next(null); + } +} diff --git a/angular.json b/angular.json index 1f4922c..a58c1a1 100644 --- a/angular.json +++ b/angular.json @@ -8,7 +8,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "app", + "prefix": "bot", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -22,7 +22,8 @@ "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + "src/app/home/images" ], "styles": [ "src/styles.css" @@ -63,7 +64,8 @@ "browserTarget": "joes-robot-shop:build:production" }, "development": { - "browserTarget": "joes-robot-shop:build:development" + "browserTarget": "joes-robot-shop:build:development", + "proxyConfig": "src/proxy.conf.json" } }, "defaultConfiguration": "development" @@ -95,4 +97,4 @@ } } } -} +} \ No newline at end of file diff --git a/api-server/.gitignore b/api-server/.gitignore new file mode 100644 index 0000000..763301f --- /dev/null +++ b/api-server/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/api-server/index.js b/api-server/index.js new file mode 100644 index 0000000..d2a7b38 --- /dev/null +++ b/api-server/index.js @@ -0,0 +1,253 @@ +const express = require("express"); +const bodyParser = require("body-parser"); + +const app = express(); +app.use(bodyParser.json()); +/* + IMPORTANT: + ***NEVER*** store credentials unencrypted like this. + This is for demo purposes only in order to simulate a functioning API serverr. +*/ +const users = { + "jim@joesrobotshop.com": { + firstName: "Jim", + lastName: "Cooper", + email: "jim@joesrobotshop.com", + password: "very-secret", + }, + "joe@joesrobotshop.com": { + firstName: "Joe", + lastName: "Eames", + email: "joe@joesrobotshop.com", + password: "super-secret", + }, +}; +let cart = []; + +// use this to add a 1 second delay to all requests +// app.use(function (req, res, next) { +// setTimeout(next, 1000); +// }); + +app.get("/api/products", (req, res) => { + let products = [ + { + id: 1, + description: + "A robot head with an unusually large eye and teloscpic neck -- excellent for exploring high spaces.", + name: "Large Cyclops", + imageName: "head-big-eye.png", + category: "Heads", + price: 1220.5, + discount: 0.2, + }, + { + id: 17, + description: "A spring base - great for reaching high places.", + name: "Spring Base", + imageName: "base-spring.png", + category: "Bases", + price: 1190.5, + discount: 0, + }, + { + id: 6, + description: + "An articulated arm with a claw -- great for reaching around corners or working in tight spaces.", + name: "Articulated Arm", + imageName: "arm-articulated-claw.png", + category: "Arms", + price: 275, + discount: 0, + }, + { + id: 2, + description: + "A friendly robot head with two eyes and a smile -- great for domestic use.", + name: "Friendly Bot", + imageName: "head-friendly.png", + category: "Heads", + price: 945.0, + discount: 0.2, + }, + { + id: 3, + description: + "A large three-eyed head with a shredder for a mouth -- great for crushing light medals or shredding documents.", + name: "Shredder", + imageName: "head-shredder.png", + category: "Heads", + price: 1275.5, + discount: 0, + }, + { + id: 16, + description: + "A single-wheeled base with an accelerometer capable of higher speeds and navigating rougher terrain than the two-wheeled variety.", + name: "Single Wheeled Base", + imageName: "base-single-wheel.png", + category: "Bases", + price: 1190.5, + discount: 0.1, + }, + { + id: 13, + description: "A simple torso with a pouch for carrying items.", + name: "Pouch Torso", + imageName: "torso-pouch.png", + category: "Torsos", + price: 785, + discount: 0, + }, + { + id: 7, + description: + "An arm with two independent claws -- great when you need an extra hand. Need four hands? Equip your bot with two of these arms.", + name: "Two Clawed Arm", + imageName: "arm-dual-claw.png", + category: "Arms", + price: 285, + discount: 0, + }, + + { + id: 4, + description: "A simple single-eyed head -- simple and inexpensive.", + name: "Small Cyclops", + imageName: "head-single-eye.png", + category: "Heads", + price: 750.0, + discount: 0, + }, + { + id: 9, + description: + "An arm with a propeller -- good for propulsion or as a cooling fan.", + name: "Propeller Arm", + imageName: "arm-propeller.png", + category: "Arms", + price: 230, + discount: 0.1, + }, + { + id: 15, + description: "A rocket base capable of high speed, controlled flight.", + name: "Rocket Base", + imageName: "base-rocket.png", + category: "Bases", + price: 1520.5, + discount: 0, + }, + { + id: 10, + description: "A short and stubby arm with a claw -- simple, but cheap.", + name: "Stubby Claw Arm", + imageName: "arm-stubby-claw.png", + category: "Arms", + price: 125, + discount: 0, + }, + { + id: 11, + description: + "A torso that can bend slightly at the waist and equiped with a heat guage.", + name: "Flexible Gauged Torso", + imageName: "torso-flexible-gauged.png", + category: "Torsos", + price: 1575, + discount: 0, + }, + { + id: 14, + description: "A two wheeled base with an accelerometer for stability.", + name: "Double Wheeled Base", + imageName: "base-double-wheel.png", + category: "Bases", + price: 895, + discount: 0, + }, + { + id: 5, + description: + "A robot head with three oscillating eyes -- excellent for surveillance.", + name: "Surveillance", + imageName: "head-surveillance.png", + category: "Heads", + price: 1255.5, + discount: 0, + }, + { + id: 8, + description: "A telescoping arm with a grabber.", + name: "Grabber Arm", + imageName: "arm-grabber.png", + category: "Arms", + price: 205.5, + discount: 0, + }, + { + id: 12, + description: "A less flexible torso with a battery gauge.", + name: "Gauged Torso", + imageName: "torso-gauged.png", + category: "Torsos", + price: 1385, + discount: 0, + }, + { + id: 18, + description: + "An inexpensive three-wheeled base. only capable of slow speeds and can only function on smooth surfaces.", + name: "Triple Wheeled Base", + imageName: "base-triple-wheel.png", + category: "Bases", + price: 700.5, + discount: 0, + }, + ]; + res.send(products); +}); + +app.post("/api/cart", (req, res) => { + cart = req.body; + setTimeout(() => res.status(201).send(), 20); +}); + +app.get("/api/cart", (req, res) => res.send(cart)); + +app.post("/api/register", (req, res) => + setTimeout(() => { + const user = req.body; + if (user.firstName && user.lastName && user.email && user.password) { + users[user.email] = user; + res.status(201).send({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + }); + } else { + res.status(500).send("Invalid user info"); + } + }, 800) +); + +/* IMPORTANT: + The code below is for demo purposes only and does not represent good security + practices. In a production application user credentials would be cryptographically + stored in a database server and the password should NEVER be stored as plain text. +*/ +app.post("/api/sign-in", (req, res) => { + const user = users[req.body.email]; + if (user && user.password === req.body.password) { + res.status(200).send({ + userId: user.userId, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + }); + } else { + res.status(401).send("Invalid user credentials."); + } +}); + +app.listen(8081, () => console.log("API Server listening on port 8081!")); diff --git a/api-server/package-lock.json b/api-server/package-lock.json new file mode 100644 index 0000000..ebe9dd0 --- /dev/null +++ b/api-server/package-lock.json @@ -0,0 +1,1236 @@ +{ + "name": "angular-fundamentals-api-server", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "angular-fundamentals-api-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.0", + "express": "^4.17.3" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + } + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/api-server/package.json b/api-server/package.json new file mode 100644 index 0000000..76ee083 --- /dev/null +++ b/api-server/package.json @@ -0,0 +1,15 @@ +{ + "name": "angular-fundamentals-api-server", + "version": "1.0.0", + "description": "API Server for use with the Pluralsight Angular Fundamentals Demo App", + "main": "index.js", + "scripts": { + "start": "node ." + }, + "author": "Joe Eames & Jim Cooper", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.0", + "express": "^4.17.3" + } +} diff --git a/package.json b/package.json index 3e256d1..7b74a54 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "server": "node api-server/index.js" + }, "private": true, "dependencies": { diff --git a/src/app/app.component.html b/src/app/app.component.html index ba7c290..39e16b0 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,3 @@ -

Hello World!

\ No newline at end of file + + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 68478a8..65dd519 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,7 +3,6 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + styleUrls: ['./app.component.css'], }) -export class AppComponent { -} +export class AppComponent {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8dfc1d6..b382ad3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,10 +2,18 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; +import { HomeComponent } from './home/home.component'; +import { CatalogComponent } from './catalog/catalog.component'; +import { SiteHeaderComponent } from './site-header/site-header.component'; +import { ProductDetailsComponent } from './product-details/product-details.component'; @NgModule({ declarations: [ - AppComponent + AppComponent, + HomeComponent, + CatalogComponent, + SiteHeaderComponent, + ProductDetailsComponent ], imports: [ BrowserModule diff --git a/src/app/cart.service.spec.ts b/src/app/cart.service.spec.ts new file mode 100644 index 0000000..cb4a750 --- /dev/null +++ b/src/app/cart.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CartService } from './cart.service'; + +describe('CartService', () => { + let service: CartService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CartService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/cart.service.ts b/src/app/cart.service.ts new file mode 100644 index 0000000..06ebf1e --- /dev/null +++ b/src/app/cart.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { IProduct } from './catalog/product.model'; + +@Injectable({ + providedIn: 'root', +}) +export class CartService { + private cart: IProduct[] = []; + + constructor() {} + + add(product: IProduct) { + this.cart.push(product); + + console.log('added ' + product.name + ' to cart!'); + } +} diff --git a/src/app/catalog/catalog.component.css b/src/app/catalog/catalog.component.css new file mode 100644 index 0000000..c61fe50 --- /dev/null +++ b/src/app/catalog/catalog.component.css @@ -0,0 +1,27 @@ +.bold { + font-weight: bold; +} + +.container { + display: flex; + flex-direction: column; +} + +.filters { + display: flex; + justify-content: space-between; + padding: 25px 200px; +} + +.filters button { + width: 100px; +} + +.products { + margin: 0 100px; + border-top: 2px solid #999; +} + +.product-item { + border-bottom: 2px solid #999; +} diff --git a/src/app/catalog/catalog.component.html b/src/app/catalog/catalog.component.html new file mode 100644 index 0000000..f68c720 --- /dev/null +++ b/src/app/catalog/catalog.component.html @@ -0,0 +1,18 @@ +
+
+ Heads + Arms + Torsos + Bases + All +
+ +
    +
  • + +
  • +
+
diff --git a/src/app/catalog/catalog.component.spec.ts b/src/app/catalog/catalog.component.spec.ts new file mode 100644 index 0000000..13d0be1 --- /dev/null +++ b/src/app/catalog/catalog.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CatalogComponent } from './catalog.component'; + +describe('CatalogComponent', () => { + let component: CatalogComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [CatalogComponent] + }); + fixture = TestBed.createComponent(CatalogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/catalog/catalog.component.ts b/src/app/catalog/catalog.component.ts new file mode 100644 index 0000000..0ea886b --- /dev/null +++ b/src/app/catalog/catalog.component.ts @@ -0,0 +1,203 @@ +import { Component, inject } from '@angular/core'; +import { IProduct } from './product.model'; +import { CartService } from '../cart.service'; + +@Component({ + selector: 'bot-catalog', + templateUrl: './catalog.component.html', + styleUrls: ['./catalog.component.css'], +}) +export class CatalogComponent { + products: any; + filter: string = ''; + + constructor(private cartSvc: CartService) { + this.products = [ + { + id: 1, + description: + 'A robot head with an unusually large eye and teloscpic neck -- excellent for exploring high spaces.', + name: 'Large Cyclops', + imageName: 'head-big-eye.png', + category: 'Heads', + price: 1220.5, + discount: 0.2, + }, + { + id: 17, + description: 'A spring base - great for reaching high places.', + name: 'Spring Base', + imageName: 'base-spring.png', + category: 'Bases', + price: 1190.5, + discount: 0, + }, + { + id: 6, + description: + 'An articulated arm with a claw -- great for reaching around corners or working in tight spaces.', + name: 'Articulated Arm', + imageName: 'arm-articulated-claw.png', + category: 'Arms', + price: 275, + discount: 0, + }, + { + id: 2, + description: + 'A friendly robot head with two eyes and a smile -- great for domestic use.', + name: 'Friendly Bot', + imageName: 'head-friendly.png', + category: 'Heads', + price: 945.0, + discount: 0.2, + }, + { + id: 3, + description: + 'A large three-eyed head with a shredder for a mouth -- great for crushing light medals or shredding documents.', + name: 'Shredder', + imageName: 'head-shredder.png', + category: 'Heads', + price: 1275.5, + discount: 0, + }, + { + id: 16, + description: + 'A single-wheeled base with an accelerometer capable of higher speeds and navigating rougher terrain than the two-wheeled variety.', + name: 'Single Wheeled Base', + imageName: 'base-single-wheel.png', + category: 'Bases', + price: 1190.5, + discount: 0.1, + }, + { + id: 13, + description: 'A simple torso with a pouch for carrying items.', + name: 'Pouch Torso', + imageName: 'torso-pouch.png', + category: 'Torsos', + price: 785, + discount: 0, + }, + { + id: 7, + description: + 'An arm with two independent claws -- great when you need an extra hand. Need four hands? Equip your bot with two of these arms.', + name: 'Two Clawed Arm', + imageName: 'arm-dual-claw.png', + category: 'Arms', + price: 285, + discount: 0, + }, + + { + id: 4, + description: 'A simple single-eyed head -- simple and inexpensive.', + name: 'Small Cyclops', + imageName: 'head-single-eye.png', + category: 'Heads', + price: 750.0, + discount: 0, + }, + { + id: 9, + description: + 'An arm with a propeller -- good for propulsion or as a cooling fan.', + name: 'Propeller Arm', + imageName: 'arm-propeller.png', + category: 'Arms', + price: 230, + discount: 0.1, + }, + { + id: 15, + description: 'A rocket base capable of high speed, controlled flight.', + name: 'Rocket Base', + imageName: 'base-rocket.png', + category: 'Bases', + price: 1520.5, + discount: 0, + }, + { + id: 10, + description: 'A short and stubby arm with a claw -- simple, but cheap.', + name: 'Stubby Claw Arm', + imageName: 'arm-stubby-claw.png', + category: 'Arms', + price: 125, + discount: 0, + }, + { + id: 11, + description: + 'A torso that can bend slightly at the waist and equiped with a heat guage.', + name: 'Flexible Gauged Torso', + imageName: 'torso-flexible-gauged.png', + category: 'Torsos', + price: 1575, + discount: 0, + }, + { + id: 14, + description: 'A two wheeled base with an accelerometer for stability.', + name: 'Double Wheeled Base', + imageName: 'base-double-wheel.png', + category: 'Bases', + price: 895, + discount: 0, + }, + { + id: 5, + description: + 'A robot head with three oscillating eyes -- excellent for surveillance.', + name: 'Surveillance', + imageName: 'head-surveillance.png', + category: 'Heads', + price: 1255.5, + discount: 0, + }, + { + id: 8, + description: 'A telescoping arm with a grabber.', + name: 'Grabber Arm', + imageName: 'arm-grabber.png', + category: 'Arms', + price: 205.5, + discount: 0, + }, + { + id: 12, + description: 'A less flexible torso with a battery gauge.', + name: 'Gauged Torso', + imageName: 'torso-gauged.png', + category: 'Torsos', + price: 1385, + discount: 0, + }, + { + id: 18, + description: + 'An inexpensive three-wheeled base. only capable of slow speeds and can only function on smooth surfaces.', + name: 'Triple Wheeled Base', + imageName: 'base-triple-wheel.png', + category: 'Bases', + price: 700.5, + discount: 0, + }, + ]; + } + + addToCart(product: IProduct) { + this.cartSvc.add(product); + } + + getFilteredProducts() { + return this.filter === '' + ? this.products + : this.products.filter( + (product: any) => product.category === this.filter + ); + } +} diff --git a/src/app/catalog/product.model.ts b/src/app/catalog/product.model.ts new file mode 100644 index 0000000..de3849b --- /dev/null +++ b/src/app/catalog/product.model.ts @@ -0,0 +1,9 @@ +export interface IProduct { + id: number; + description: string; + name: string; + imageName: string; + category: string; + price: number; + discount: number; +} diff --git a/src/app/home/home.component.css b/src/app/home/home.component.css new file mode 100644 index 0000000..2bf4f66 --- /dev/null +++ b/src/app/home/home.component.css @@ -0,0 +1,119 @@ +.container { + display: flex; + flex-direction: column; +} + +.hero { + background-image: url("/assets/images/hero-banner.png"); + background-repeat: no-repeat; + height: 300px; + background-size: cover; + background-position: center center; + text-align: center; + margin-left: -8px; + margin-right: -8px; +} + +.promoted { + display: flex; + justify-content: space-between; + margin: 25px; + border-top: 2px solid #888; + border-bottom: 2px solid #888; + padding: 10px 150px 10px 150px; +} + +.promoted img { + width: 150px; + height: 150px; +} + +.promo-text { + display: flex; + flex-direction: column; + justify-content: space-around; + margin: 40px 0 40px 0; +} + +.promo-main-text { + font-size: 24px; + text-align: center; +} + +.promo-sub-text { + font-size: 20px; + text-align: center; +} + +.robot-parts-cta { + display: flex; + justify-content: space-between; + margin: 0px 100px 25px 100px; + padding: 10px 0; +} + +.part { + display: flex; + flex-direction: column; + font-size: 18px; + text-align: center; + cursor: pointer; +} + +.part img { + width: 180px; + margin-bottom: 10px; + padding: 10px; + background-color: #888; +} + +.white-paper { + display: flex; + height: 100%; + margin: 0 25px; +} + +.white-paper img { + width: 75%; +} + +.white-paper .text { + display: flex; + width: 25%; + flex-direction: column; + background-color: #333; + text-align: center; + justify-content: space-between; +} + +.white-paper .header-text { + margin-top: 50px; + font-size: 30px; +} + +.white-paper .sub-text { + font-size: 20px; + color: #5cadd2; + margin-top: 10px; + padding: 0 15%; +} + +.white-paper .sub-text p { + margin: 0; +} + +.white-paper .large-text { + border-top: 2px solid #666; + border-bottom: 2px solid #666; + margin-top: -50px; + padding: 20px 0; + font-size: 35px; + color: #fff; +} + +.white-paper .learn-more { + background-color: #d25ca1; + color: white; + padding: 15px 25px; + text-decoration: none; +} \ No newline at end of file diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html new file mode 100644 index 0000000..8bebe71 --- /dev/null +++ b/src/app/home/home.component.html @@ -0,0 +1,57 @@ +
+
+ + + + + +
+ Robot Apocalyse +
+
+
Will they kill us all?
+
+

10 Myths About the

+

Robot Apocalyse

+
+
+
WHITE PAPER
+ Learn More +
+
+
\ No newline at end of file diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts new file mode 100644 index 0000000..ba1b4a3 --- /dev/null +++ b/src/app/home/home.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [HomeComponent] + }); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts new file mode 100644 index 0000000..671ad96 --- /dev/null +++ b/src/app/home/home.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'bot-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'], +}) +export class HomeComponent {} diff --git a/src/app/product-details/product-details.component.css b/src/app/product-details/product-details.component.css new file mode 100644 index 0000000..af595a5 --- /dev/null +++ b/src/app/product-details/product-details.component.css @@ -0,0 +1,60 @@ +/* Product Details */ +.product { + display: flex; + justify-content: space-between; + padding: 20px 25px; + .product-details { + display: flex; + align-items: center; + } +} + +.product img { + width: 125px; +} + +.product .product-info { + margin-left: 25px; +} + +.product .name { + font-size: 22px; + font-weight: bold; +} + +.product .description { + margin-top: 3px; + font-size: 18px; +} + +.product .category { + margin-top: 20px; + color: #777; +} + +.product .price { + display: flex; + flex-direction: column; + font-size: 25px; + justify-content: space-around; + align-items: center; + min-width: 190px; + color: #555; + border-left: 2px solid #aaa; + margin-left: 50px; +} + +.product .price button { + padding: 10px; + width: 100px; +} + +.discount { + margin-top: -15px; + color: #d25ca1; +} + +.strikethrough { + text-decoration: line-through; + font-size: 18px; +} diff --git a/src/app/product-details/product-details.component.html b/src/app/product-details/product-details.component.html new file mode 100644 index 0000000..0dacf6f --- /dev/null +++ b/src/app/product-details/product-details.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
{{ product.name }}
+
{{ product.description }}
+
Part Type: {{ product.category }}
+
+
+
+
+ {{ product.price | currency : "USD" }} +
+
+ {{ product.price * (1 - product.discount) | currency : "USD" }} +
+ +
+
diff --git a/src/app/product-details/product-details.component.spec.ts b/src/app/product-details/product-details.component.spec.ts new file mode 100644 index 0000000..b04cd8d --- /dev/null +++ b/src/app/product-details/product-details.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductDetailsComponent } from './product-details.component'; + +describe('ProductDetailsComponent', () => { + let component: ProductDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProductDetailsComponent] + }); + fixture = TestBed.createComponent(ProductDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/product-details/product-details.component.ts b/src/app/product-details/product-details.component.ts new file mode 100644 index 0000000..29e06e4 --- /dev/null +++ b/src/app/product-details/product-details.component.ts @@ -0,0 +1,21 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { IProduct } from '../catalog/product.model'; + +@Component({ + selector: 'bot-product-details', + templateUrl: './product-details.component.html', + styleUrls: ['./product-details.component.css'], +}) +export class ProductDetailsComponent { + @Input() product!: IProduct; + @Output() buy = new EventEmitter(); + + getImageUrl(product: IProduct) { + if (!product) return ''; + return '/assets/images/robot-parts/' + product.imageName; + } + + buyButtonClicked(product: IProduct) { + this.buy.emit(); + } +} diff --git a/src/app/site-header/site-header.component.css b/src/app/site-header/site-header.component.css new file mode 100644 index 0000000..de1fba2 --- /dev/null +++ b/src/app/site-header/site-header.component.css @@ -0,0 +1,67 @@ +.container { + display: flex; + justify-content: space-between; + font-size: 24px; + margin-bottom: 10px; + margin: -3px -8px 0 -8px; + padding: 0 8px 5px 8px; + border-bottom: 2px solid #818285; +} + +.left { + display: flex; + align-items: center; +} + +.left * { + margin-right: 25px; +} + +.logo { + height: 65px; +} + +.cart { + display: flex; + position: relative; +} + +.cartCount { + position: absolute; + display: flex; + justify-content: space-around; + align-items: center; + width: 20px; + height: 20px; + top: -7px; + right: -15px; + border-radius: 25px; + background-color: #f590c4; + color: white; + font-size: 14px; +} + +.cartCount div { + margin: auto; +} + +.right { + display: flex; + align-items: center; + margin-right: 25px; +} + +.right * { + margin-left: 25px; +} + +.right img { + height: 40px; + cursor: pointer; +} + +.sign-out { + position: absolute; + top: 60px; + right: 30px; +} \ No newline at end of file diff --git a/src/app/site-header/site-header.component.html b/src/app/site-header/site-header.component.html new file mode 100644 index 0000000..86ec5a3 --- /dev/null +++ b/src/app/site-header/site-header.component.html @@ -0,0 +1,14 @@ +
+
+ + Home + Catalog +
+ Cart +
+
+ +
\ No newline at end of file diff --git a/src/app/site-header/site-header.component.spec.ts b/src/app/site-header/site-header.component.spec.ts new file mode 100644 index 0000000..3ad3288 --- /dev/null +++ b/src/app/site-header/site-header.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SiteHeaderComponent } from './site-header.component'; + +describe('SiteHeaderComponent', () => { + let component: SiteHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SiteHeaderComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SiteHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/site-header/site-header.component.ts b/src/app/site-header/site-header.component.ts new file mode 100644 index 0000000..a9ab617 --- /dev/null +++ b/src/app/site-header/site-header.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'bot-site-header', + templateUrl: './site-header.component.html', + styleUrls: ['./site-header.component.css'], +}) +export class SiteHeaderComponent { + constructor() {} +} diff --git a/src/assets/images/hero-banner.png b/src/assets/images/hero-banner.png new file mode 100644 index 0000000..ced2818 Binary files /dev/null and b/src/assets/images/hero-banner.png differ diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..d0ebfd8 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/assets/images/profile.png b/src/assets/images/profile.png new file mode 100644 index 0000000..07b82cc Binary files /dev/null and b/src/assets/images/profile.png differ diff --git a/src/assets/images/robot-apocalypse.png b/src/assets/images/robot-apocalypse.png new file mode 100644 index 0000000..0d2c27d Binary files /dev/null and b/src/assets/images/robot-apocalypse.png differ diff --git a/src/assets/images/robot-parts/arm-articulated-claw.png b/src/assets/images/robot-parts/arm-articulated-claw.png new file mode 100644 index 0000000..f18c477 Binary files /dev/null and b/src/assets/images/robot-parts/arm-articulated-claw.png differ diff --git a/src/assets/images/robot-parts/arm-dual-claw.png b/src/assets/images/robot-parts/arm-dual-claw.png new file mode 100644 index 0000000..ca1d9e2 Binary files /dev/null and b/src/assets/images/robot-parts/arm-dual-claw.png differ diff --git a/src/assets/images/robot-parts/arm-grabber.png b/src/assets/images/robot-parts/arm-grabber.png new file mode 100644 index 0000000..cd66947 Binary files /dev/null and b/src/assets/images/robot-parts/arm-grabber.png differ diff --git a/src/assets/images/robot-parts/arm-propeller.png b/src/assets/images/robot-parts/arm-propeller.png new file mode 100644 index 0000000..41a123a Binary files /dev/null and b/src/assets/images/robot-parts/arm-propeller.png differ diff --git a/src/assets/images/robot-parts/arm-stubby-claw.png b/src/assets/images/robot-parts/arm-stubby-claw.png new file mode 100644 index 0000000..6d8fab9 Binary files /dev/null and b/src/assets/images/robot-parts/arm-stubby-claw.png differ diff --git a/src/assets/images/robot-parts/base-double-wheel.png b/src/assets/images/robot-parts/base-double-wheel.png new file mode 100644 index 0000000..b5048b0 Binary files /dev/null and b/src/assets/images/robot-parts/base-double-wheel.png differ diff --git a/src/assets/images/robot-parts/base-rocket.png b/src/assets/images/robot-parts/base-rocket.png new file mode 100644 index 0000000..8d83504 Binary files /dev/null and b/src/assets/images/robot-parts/base-rocket.png differ diff --git a/src/assets/images/robot-parts/base-single-wheel.png b/src/assets/images/robot-parts/base-single-wheel.png new file mode 100644 index 0000000..d190af8 Binary files /dev/null and b/src/assets/images/robot-parts/base-single-wheel.png differ diff --git a/src/assets/images/robot-parts/base-spring.png b/src/assets/images/robot-parts/base-spring.png new file mode 100644 index 0000000..6f21a0d Binary files /dev/null and b/src/assets/images/robot-parts/base-spring.png differ diff --git a/src/assets/images/robot-parts/base-triple-wheel.png b/src/assets/images/robot-parts/base-triple-wheel.png new file mode 100644 index 0000000..6ee2786 Binary files /dev/null and b/src/assets/images/robot-parts/base-triple-wheel.png differ diff --git a/src/assets/images/robot-parts/example_robot.png b/src/assets/images/robot-parts/example_robot.png new file mode 100644 index 0000000..79ddb58 Binary files /dev/null and b/src/assets/images/robot-parts/example_robot.png differ diff --git a/src/assets/images/robot-parts/head-big-eye.png b/src/assets/images/robot-parts/head-big-eye.png new file mode 100644 index 0000000..7949388 Binary files /dev/null and b/src/assets/images/robot-parts/head-big-eye.png differ diff --git a/src/assets/images/robot-parts/head-friendly.png b/src/assets/images/robot-parts/head-friendly.png new file mode 100644 index 0000000..f168378 Binary files /dev/null and b/src/assets/images/robot-parts/head-friendly.png differ diff --git a/src/assets/images/robot-parts/head-shredder.png b/src/assets/images/robot-parts/head-shredder.png new file mode 100644 index 0000000..b4bf0e0 Binary files /dev/null and b/src/assets/images/robot-parts/head-shredder.png differ diff --git a/src/assets/images/robot-parts/head-single-eye.png b/src/assets/images/robot-parts/head-single-eye.png new file mode 100644 index 0000000..9bc4d0b Binary files /dev/null and b/src/assets/images/robot-parts/head-single-eye.png differ diff --git a/src/assets/images/robot-parts/head-surveillance.png b/src/assets/images/robot-parts/head-surveillance.png new file mode 100644 index 0000000..c359605 Binary files /dev/null and b/src/assets/images/robot-parts/head-surveillance.png differ diff --git a/src/assets/images/robot-parts/robot.png b/src/assets/images/robot-parts/robot.png new file mode 100644 index 0000000..05a5db2 Binary files /dev/null and b/src/assets/images/robot-parts/robot.png differ diff --git a/src/assets/images/robot-parts/torso-flexible-gauged.png b/src/assets/images/robot-parts/torso-flexible-gauged.png new file mode 100644 index 0000000..ba6e065 Binary files /dev/null and b/src/assets/images/robot-parts/torso-flexible-gauged.png differ diff --git a/src/assets/images/robot-parts/torso-gauged.png b/src/assets/images/robot-parts/torso-gauged.png new file mode 100644 index 0000000..bd048df Binary files /dev/null and b/src/assets/images/robot-parts/torso-gauged.png differ diff --git a/src/assets/images/robot-parts/torso-pouch.png b/src/assets/images/robot-parts/torso-pouch.png new file mode 100644 index 0000000..678c30d Binary files /dev/null and b/src/assets/images/robot-parts/torso-pouch.png differ diff --git a/src/proxy.conf.json b/src/proxy.conf.json new file mode 100644 index 0000000..d964979 --- /dev/null +++ b/src/proxy.conf.json @@ -0,0 +1,6 @@ +{ + "/api":{ + "target": "http://localhost:8081", + "secure": false + } +} \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 90d4ee0..16d683c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,119 @@ -/* You can add global styles to this file, and also import other style files */ +.red { + color: red; +} + +body { + background-color: #fff; + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + color: #444; +} + +a { + color: #444; + text-decoration: none; +} + +a.active { + padding-bottom: 5px; + border-bottom: 2px solid #d25ca1; +} + +a:visited { + color: #444; +} + +a:hover { + color: #5cadd2; +} + +a.cta { + color: #d25ca1; +} + +a.cta:visited { + color: #d25ca1; +} + +a.cta:hover { + color: #f27cb1; +} + +.cta { + color: #d25ca1; +} + +a.cta { + color: #d25ca1; +} + +a.cta:visited { + color: #d25ca1; +} + +a.cta:hover { + color: #f27cb1; +} + +.cta { + color: #d25ca1; +} + +ul { + list-style-type: none; + padding: 0; +} + +button { + font-size: 15px; + padding: 15px 25px; + background-color: #5cadd2; + color: #fff; + border: 0; + box-shadow: none; + border-radius: 5px; + cursor: pointer; +} + +a.button { + font-size: 15px; + padding: 15px 25px; + background-color: #5cadd2; + color: #fff; + border: 0; + box-shadow: none; + border-radius: 5px; + cursor: pointer; +} + +a.button:hover { + color: #1c6d92; +} + +button:disabled { + background-color: #777; +} + +button:hover { + color: #1c6d92; +} + +button.cta { + font-family: Arial, Helvetica, sans-serif; + background-color: #d25ca1; + color: #ddd; +} + +button.cta:hover { + color: #fff; +} + +input { + padding: 15px; + font-size: 20px; +} + +.button.cta:disabled { + background-color: #f590c4; + color: #aaa; +}