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 @@
+
+
+
+
+ You have no items in your cart
+
+
0">
+ -
+
+
+
+
+
{{ product.name }}
+
{{ product.description }}
+
Part Type: {{ product.category }}
+
+
+
+
0 }">
+ {{ product.price | currency : "USD" }}
+
+
0" class="discount">
+ {{ product.price * (1 - product.discount) | currency : "USD" }}
+
+
+
+
+
+
+
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 @@
+
\ 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 @@
+
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 @@
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 }}
+
+
+
+
0 }">
+ {{ product.price | currency : "USD" }}
+
+
0" class="discount">
+ {{ 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 @@
+
\ 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;
+}