From 31423c630a620e09221fc32dd0efb11b1ea2ad29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCller?= Date: Wed, 9 Dec 2020 20:35:08 +0100 Subject: [PATCH] BETTERZON-25: Finishing prototype of UC --- Backend/src/models/prices/prices.service.ts | 6 +- Frontend/src/app/app.module.ts | 16 ++--- .../components/header/header.component.html | 2 +- .../app/components/header/header.component.ts | 6 +- .../newest-prices-list.component.css | 28 +++++++-- .../newest-prices-list.component.html | 13 +++- .../newest-prices-list.component.ts | 44 +++++++++++--- .../product-details.component.css | 19 +++++- .../product-details.component.html | 15 +++-- .../product-details.component.ts | 59 ++++++++++++++++++- .../product-list/product-list.component.css | 2 +- Frontend/src/app/models/vendor.ts | 10 ++++ .../landingpage/landingpage.component.css | 45 ++++++++++++++ .../landingpage/landingpage.component.html | 17 +++++- .../landingpage/landingpage.component.ts | 28 ++++++--- .../product-detail-page.component.html | 4 +- .../product-search-page.component.html | 2 +- Frontend/src/app/services/api.service.ts | 50 +++++++++++++++- 18 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 Frontend/src/app/models/vendor.ts diff --git a/Backend/src/models/prices/prices.service.ts b/Backend/src/models/prices/prices.service.ts index 14846fe..b0a8038 100644 --- a/Backend/src/models/prices/prices.service.ts +++ b/Backend/src/models/prices/prices.service.ts @@ -123,16 +123,16 @@ export const findByType = async (product: string, type: string): Promise 'PARTITION BY p.vendor_id ' + 'ORDER BY p.timestamp DESC) AS rk ' + 'FROM prices p ' + - 'WHERE product_id = ?) ' + + 'WHERE product_id = ? AND vendor_id != 1) ' + 'SELECT s.* ' + 'FROM summary s ' + 'WHERE s.rk = 1 '), product); } else if (type === 'lowest') { // Used to get the lowest prices for this product over a period of time - rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? GROUP BY DAY(timestamp) ORDER BY timestamp', product); + rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1 GROUP BY DAY(timestamp) ORDER BY timestamp', product); } else { // If no type is given, return all prices for this product - rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ?', product); + rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1', product); } for (let row in rows) { diff --git a/Frontend/src/app/app.module.ts b/Frontend/src/app/app.module.ts index 01415bc..eef8e23 100644 --- a/Frontend/src/app/app.module.ts +++ b/Frontend/src/app/app.module.ts @@ -5,16 +5,16 @@ import {NgModule} from '@angular/core'; import {AppComponent} from './app.component'; import {AppRouting} from './app.routing'; import {ProductListComponent} from './components/product-list/product-list.component'; -import { LandingpageComponent } from './pages/landingpage/landingpage.component'; -import { ProductDetailPageComponent } from './pages/product-detail-page/product-detail-page.component'; -import { FooterComponent } from './components/footer/footer.component'; -import { ProductDetailsComponent } from './components/product-details/product-details.component'; +import {LandingpageComponent} from './pages/landingpage/landingpage.component'; +import {ProductDetailPageComponent} from './pages/product-detail-page/product-detail-page.component'; +import {FooterComponent} from './components/footer/footer.component'; +import {ProductDetailsComponent} from './components/product-details/product-details.component'; import {NgApexchartsModule} from 'ng-apexcharts'; -import { ProductSearchPageComponent } from './pages/product-search-page/product-search-page.component'; -import { HeaderComponent } from './components/header/header.component'; -import { NewestPricesListComponent } from './components/newest-prices-list/newest-prices-list.component'; +import {ProductSearchPageComponent} from './pages/product-search-page/product-search-page.component'; +import {HeaderComponent} from './components/header/header.component'; +import {NewestPricesListComponent} from './components/newest-prices-list/newest-prices-list.component'; import {FormsModule} from '@angular/forms'; -import { PageNotFoundPageComponent } from './pages/page-not-found-page/page-not-found-page.component'; +import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component'; @NgModule({ declarations: [ diff --git a/Frontend/src/app/components/header/header.component.html b/Frontend/src/app/components/header/header.component.html index 38bc6fa..f99e61f 100644 --- a/Frontend/src/app/components/header/header.component.html +++ b/Frontend/src/app/components/header/header.component.html @@ -5,7 +5,7 @@ Betterzon Logo
Profile diff --git a/Frontend/src/app/components/header/header.component.ts b/Frontend/src/app/components/header/header.component.ts index 3a1d6a4..ee76e9f 100644 --- a/Frontend/src/app/components/header/header.component.ts +++ b/Frontend/src/app/components/header/header.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {Router} from '@angular/router'; @Component({ @@ -8,6 +8,7 @@ import {Router} from '@angular/router'; }) export class HeaderComponent implements OnInit { searchInput: string; + @Input() showSearch: boolean; constructor( private router: Router @@ -15,6 +16,9 @@ export class HeaderComponent implements OnInit { } ngOnInit(): void { + if (!this.showSearch) { + this.showSearch = false; + } } clickedLogo(): void { diff --git a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.css b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.css index 0eacd0e..761167b 100644 --- a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.css +++ b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.css @@ -1,7 +1,25 @@ .priceList { - max-width: 50%; - margin: auto; - margin: auto; - align-content: center; - text-align: center; + margin: 2em; + position: relative; +} + +.priceList table { + position: relative; + margin: auto; + font-family: Arial, Helvetica, sans-serif; + border-collapse: collapse; + width: 80%; +} + +.priceList table td, .priceList table th { + border: 1px solid #ddd; + padding: 8px; +} + +.priceList table tr:nth-child(even) { + background-color: #f2f2f2; +} + +.priceList table tr:hover { + background-color: #ddd; } diff --git a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.html b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.html index c62745c..0c8de11 100644 --- a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.html +++ b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.html @@ -1,3 +1,14 @@
- PriceList + + + + + + + + + + + +
VendorCurrent priceVisit
{{vendorMap[price.vendor_id]?.name}}{{price.price_in_cents / 100}}€Visit Shop
diff --git a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.ts b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.ts index 42b67a6..6c490b8 100644 --- a/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.ts +++ b/Frontend/src/app/components/newest-prices-list/newest-prices-list.component.ts @@ -1,15 +1,45 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; +import {Price} from '../../models/price'; +import {Vendor} from '../../models/vendor'; +import {ApiService} from '../../services/api.service'; @Component({ - selector: 'app-newest-prices-list', - templateUrl: './newest-prices-list.component.html', - styleUrls: ['./newest-prices-list.component.css'] + selector: 'app-newest-prices-list', + templateUrl: './newest-prices-list.component.html', + styleUrls: ['./newest-prices-list.component.css'] }) export class NewestPricesListComponent implements OnInit { + @Input() productId: number; + prices: Price[] = []; + vendors: Vendor[] = []; + vendorMap = {}; - constructor() { } + constructor( + private apiService: ApiService + ) { + } - ngOnInit(): void { - } + ngOnInit(): void { + this.getVendors(); + this.getPrices(); + } + + getPrices(): void { + // Lowest prices + this.apiService.getCurrentPricePerVendor(this.productId).subscribe( + prices => { + this.prices = prices; + }); + } + + getVendors(): void { + this.apiService.getVendors().subscribe(vendors => { + this.vendors = vendors; + + this.vendors.forEach(vendor => { + this.vendorMap[vendor.vendor_id] = vendor; + }); + }); + } } diff --git a/Frontend/src/app/components/product-details/product-details.component.css b/Frontend/src/app/components/product-details/product-details.component.css index 55239cd..42dfa5b 100644 --- a/Frontend/src/app/components/product-details/product-details.component.css +++ b/Frontend/src/app/components/product-details/product-details.component.css @@ -21,7 +21,7 @@ .productImage { max-width: 300px; max-height: 300px; - display:block; + display: block; margin: auto; position: absolute; top: 0; @@ -57,14 +57,27 @@ border-radius: 5px; padding: .25em; margin: auto; + font-size: 1.5em; } -/* Best price div */ -.bestPrice { +/* Best price container div */ +.bestPriceContainer { grid-area: bestPrice; +} + +/* best price div */ +.bestPrice { border-style: solid; border-color: dimgrey; border-radius: 5px; padding: .25em; margin: auto; + text-align: center; + font-size: 1.5em; +} + +/* amazon price div */ +.amazonPrice { + margin-top: .5em; + text-align: center; } diff --git a/Frontend/src/app/components/product-details/product-details.component.html b/Frontend/src/app/components/product-details/product-details.component.html index f4c7667..f392f00 100644 --- a/Frontend/src/app/components/product-details/product-details.component.html +++ b/Frontend/src/app/components/product-details/product-details.component.html @@ -3,7 +3,7 @@
- {{product.name}} + {{product?.name}}
@@ -17,13 +17,20 @@
- {{product.short_description}} + {{product?.short_description}}
Set Price Alarm
-
- 5€ +
+
+ Best price: {{currentlyLowestPrice?.price_in_cents / 100}}€ at + vendor {{vendorMap[currentlyLowestPrice.vendor_id]?.name}}! +
+
+ Amazon-price: {{currentAmazonPrice?.price_in_cents / 100}}€ (+{{getAmazonPriceDifference()}}%) +
diff --git a/Frontend/src/app/components/product-details/product-details.component.ts b/Frontend/src/app/components/product-details/product-details.component.ts index 6537665..fa9e87d 100644 --- a/Frontend/src/app/components/product-details/product-details.component.ts +++ b/Frontend/src/app/components/product-details/product-details.component.ts @@ -8,6 +8,8 @@ import { ApexXAxis, ApexTitleSubtitle, ApexStroke } from 'ng-apexcharts'; +import {Price} from '../../models/price'; +import {Vendor} from '../../models/vendor'; export type ChartOptions = { series: ApexAxisChartSeries; @@ -25,6 +27,11 @@ export type ChartOptions = { export class ProductDetailsComponent implements OnInit { @Input() productId: number; product: Product; + lowestPrices: Price[]; + currentlyLowestPrice: Price; + currentAmazonPrice: Price; + vendors: Vendor[] = []; + vendorMap = {}; @ViewChild('chart') chart: ChartComponent; public chartOptions: ChartOptions; @@ -35,19 +42,53 @@ export class ProductDetailsComponent implements OnInit { ngOnInit(): void { this.getProduct(); - this.getChartData(); + this.getVendors(); + this.getPrices(); } getProduct(): void { this.apiService.getProduct(this.productId).subscribe(product => this.product = product); } + getPrices(): void { + // Lowest prices + this.apiService.getLowestPrices(this.productId).subscribe( + prices => { + this.lowestPrices = prices; + this.currentlyLowestPrice = prices[prices.length - 1]; + + // Update charts + this.getChartData(); + }); + + // Amazon price + this.apiService.getAmazonPrice(this.productId).subscribe(price => { + this.currentAmazonPrice = price[0]; + }); + } + + getVendors(): void { + this.apiService.getVendors().subscribe(vendors => { + this.vendors = vendors; + this.vendors.forEach(vendor => { + this.vendorMap[vendor.vendor_id] = vendor; + }); + }); + } + getChartData(): void { + const prices = []; + const categs = []; + this.lowestPrices?.forEach(price => { + prices.push(price.price_in_cents / 100); + categs.push(new Date(price.timestamp).toDateString()); + }); + this.chartOptions = { series: [ { name: 'Lowest Price', - data: [1061.20, 1060, 1070, 1040, 1061.20, 1061, 1100, 1070, 1061.20] + data: prices } ], chart: { @@ -58,7 +99,7 @@ export class ProductDetailsComponent implements OnInit { text: 'Lowest price' }, xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'] + categories: categs }, stroke: { curve: 'stepline' @@ -66,4 +107,16 @@ export class ProductDetailsComponent implements OnInit { }; } + getAmazonPriceDifference(): number { + const amazonPrice = this.currentAmazonPrice?.price_in_cents; + const lowestPrice = this.currentlyLowestPrice?.price_in_cents; + + const percentage = amazonPrice / lowestPrice; + + if (percentage < 1) { + return -Math.round(percentage); + } else { + return +Math.round(percentage); + } + } } diff --git a/Frontend/src/app/components/product-list/product-list.component.css b/Frontend/src/app/components/product-list/product-list.component.css index 814df74..d80c1f5 100644 --- a/Frontend/src/app/components/product-list/product-list.component.css +++ b/Frontend/src/app/components/product-list/product-list.component.css @@ -24,7 +24,7 @@ .productImage { max-width: 50px; max-height: 50px; - display:block; + display: block; margin: auto; position: absolute; top: 0; diff --git a/Frontend/src/app/models/vendor.ts b/Frontend/src/app/models/vendor.ts new file mode 100644 index 0000000..51afc49 --- /dev/null +++ b/Frontend/src/app/models/vendor.ts @@ -0,0 +1,10 @@ +export interface Vendor { + vendor_id: number; + name: string; + streetname: string; + zip_code: string; + city: string; + country_code: string; + phone: string; + website: string; +} diff --git a/Frontend/src/app/pages/landingpage/landingpage.component.css b/Frontend/src/app/pages/landingpage/landingpage.component.css index 653b875..cac058e 100644 --- a/Frontend/src/app/pages/landingpage/landingpage.component.css +++ b/Frontend/src/app/pages/landingpage/landingpage.component.css @@ -3,3 +3,48 @@ margin-top: .5em; margin-bottom: .5em; } + +#productListsContainer { + display: grid; + grid-template-areas: + 'search search' + 'popularSearches bestDeals'; + grid-template-columns: 50% 50%; +} + +#searchContainer { + position: relative; + grid-area: search; + height: 10em; +} + +#searchContainer input { + position: relative; + font-size: 1.5em; + padding: .25em; + display: block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + margin: auto; + -ms-transform: translateY(50%); + transform: translateY(2.5em); +} + +#popularSearchesList { + grid-area: popularSearches; + padding: .5em; +} + +#popularSearchesList h2 { + text-align: center; +} + +#bestDealsList { + grid-area: bestDeals; + padding: .5em; +} + +#bestDealsList h2 { + text-align: center; +} diff --git a/Frontend/src/app/pages/landingpage/landingpage.component.html b/Frontend/src/app/pages/landingpage/landingpage.component.html index e724bd0..9cc4252 100644 --- a/Frontend/src/app/pages/landingpage/landingpage.component.html +++ b/Frontend/src/app/pages/landingpage/landingpage.component.html @@ -1,5 +1,18 @@ - +
- +
+ +
+
+
+

Popular Searches

+ +
+
+

Best Deals

+ +
+
diff --git a/Frontend/src/app/pages/landingpage/landingpage.component.ts b/Frontend/src/app/pages/landingpage/landingpage.component.ts index ad70dcb..d62ca82 100644 --- a/Frontend/src/app/pages/landingpage/landingpage.component.ts +++ b/Frontend/src/app/pages/landingpage/landingpage.component.ts @@ -1,15 +1,29 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; @Component({ - selector: 'app-landingpage', - templateUrl: './landingpage.component.html', - styleUrls: ['./landingpage.component.css'] + selector: 'app-landingpage', + templateUrl: './landingpage.component.html', + styleUrls: ['./landingpage.component.css'] }) export class LandingpageComponent implements OnInit { + searchInput: string; - constructor() { } + constructor( + private router: Router + ) { + } - ngOnInit(): void { - } + ngOnInit(): void { + } + + startedSearch(): void { + this.redirectTo('/search', {queryParams: {q: this.searchInput}}); + } + + redirectTo(uri: string, queryParams: object): void { + this.router.navigateByUrl('/', {skipLocationChange: true}).then(() => + this.router.navigate([uri], queryParams)); + } } diff --git a/Frontend/src/app/pages/product-detail-page/product-detail-page.component.html b/Frontend/src/app/pages/product-detail-page/product-detail-page.component.html index e919a51..a0b0822 100644 --- a/Frontend/src/app/pages/product-detail-page/product-detail-page.component.html +++ b/Frontend/src/app/pages/product-detail-page/product-detail-page.component.html @@ -1,6 +1,6 @@ - +
- +
diff --git a/Frontend/src/app/pages/product-search-page/product-search-page.component.html b/Frontend/src/app/pages/product-search-page/product-search-page.component.html index 6bd08c7..793cf38 100644 --- a/Frontend/src/app/pages/product-search-page/product-search-page.component.html +++ b/Frontend/src/app/pages/product-search-page/product-search-page.component.html @@ -1,4 +1,4 @@ - +
diff --git a/Frontend/src/app/services/api.service.ts b/Frontend/src/app/services/api.service.ts index c53ff9e..ec91051 100644 --- a/Frontend/src/app/services/api.service.ts +++ b/Frontend/src/app/services/api.service.ts @@ -1,9 +1,10 @@ import {Injectable} from '@angular/core'; -import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import process from 'process'; import {Product} from '../models/product'; import {Price} from '../models/price'; import {Observable, of} from 'rxjs'; +import {Vendor} from '../models/vendor'; @Injectable({ providedIn: 'root' @@ -46,10 +47,55 @@ export class ApiService { getPrices(): Observable { try { const prices = this.http.get((this.apiUrl + '/prices')); - console.log(prices); return prices; } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + + getLowestPrices(productId): Observable { + try { + let params = new HttpParams(); + params = params.append('product', productId); + params = params.append('type', 'lowest'); + const prices = this.http.get((this.apiUrl + '/prices'), {params}); + return prices; + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + getAmazonPrice(productId): Observable { + try { + let params = new HttpParams(); + params = params.append('product', productId); + params = params.append('vendor', '1'); + params = params.append('type', 'newest'); + const price = this.http.get((this.apiUrl + '/prices'), {params}); + return price; + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + getCurrentPricePerVendor(productId): Observable { + try { + let params = new HttpParams(); + params = params.append('product', productId); + params = params.append('type', 'newest'); + const prices = this.http.get((this.apiUrl + '/prices'), {params}); + return prices; + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + getVendors(): Observable { + try { + const vendors = this.http.get((this.apiUrl + '/vendors')); + return vendors; + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } }