mirror of
https://github.com/Mueller-Patrick/Betterzon.git
synced 2026-11-04 22:01:31 +00:00
Merge pull request #16 from Mueller-Patrick/develop
Deploying prototype
This commit is contained in:
57 files changed
+1198
-145
No files matched your search
@@ -23,7 +23,20 @@ export const pricesRouter = express.Router();
|
|||||||
|
|
||||||
pricesRouter.get('/', async (req: Request, res: Response) => {
|
pricesRouter.get('/', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const prices: Prices = await PriceService.findAll();
|
let prices: Prices = [];
|
||||||
|
const product = req.query.product;
|
||||||
|
const vendor = req.query.vendor;
|
||||||
|
const type = req.query.type;
|
||||||
|
|
||||||
|
if (product) {
|
||||||
|
if (vendor) {
|
||||||
|
prices = await PriceService.findByVendor(<string> product, <string> vendor, <string> type);
|
||||||
|
} else {
|
||||||
|
prices = await PriceService.findByType(<string> product, <string> type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prices = await PriceService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).send(prices);
|
res.status(200).send(prices);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -50,26 +63,6 @@ pricesRouter.get('/:id', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET items/:name
|
|
||||||
|
|
||||||
pricesRouter.get('/products/:id', async (req: Request, res: Response) => {
|
|
||||||
const id: number = parseInt(req.params.id, 10);
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
res.status(400).send('Missing parameters.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const prices: Prices = await PriceService.findByProduct(id);
|
|
||||||
|
|
||||||
res.status(200).send(prices);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(404).send(e.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// POST items/
|
// POST items/
|
||||||
|
|
||||||
// pricesRouter.post('/', async (req: Request, res: Response) => {
|
// pricesRouter.post('/', async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -106,6 +106,86 @@ export const findByProduct = async (product: number): Promise<Prices> => {
|
|||||||
return priceRows;
|
return priceRows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findByType = async (product: string, type: string): Promise<Prices> => {
|
||||||
|
let conn;
|
||||||
|
let priceRows = [];
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
let rows = [];
|
||||||
|
if (type === 'newest') {
|
||||||
|
// Used to get the newest price for this product per vendor
|
||||||
|
rows = await conn.query(('WITH summary AS ( ' +
|
||||||
|
'SELECT p.product_id, ' +
|
||||||
|
'p.vendor_id, ' +
|
||||||
|
'p.price_in_cents, ' +
|
||||||
|
'p.timestamp, ' +
|
||||||
|
'ROW_NUMBER() OVER( ' +
|
||||||
|
'PARTITION BY p.vendor_id ' +
|
||||||
|
'ORDER BY p.timestamp DESC) AS rk ' +
|
||||||
|
'FROM prices p ' +
|
||||||
|
'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 = ? 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 = ? AND vendor_id != 1', product);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row in rows) {
|
||||||
|
if (row !== 'meta') {
|
||||||
|
priceRows.push(rows[row]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceRows;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findByVendor = async (product: string, vendor: string, type: string): Promise<Prices> => {
|
||||||
|
let conn;
|
||||||
|
let priceRows = [];
|
||||||
|
try {
|
||||||
|
conn = await pool.getConnection();
|
||||||
|
let rows = [];
|
||||||
|
if (type === 'newest') {
|
||||||
|
// Used to get the newest price for this product and vendor
|
||||||
|
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? ORDER BY timestamp DESC LIMIT 1', [product, vendor]);
|
||||||
|
} else if (type === 'lowest') {
|
||||||
|
// Used to get the lowest prices for this product and vendor in all 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 = ? AND vendor_id = ? LIMIT 1', [product, vendor]);
|
||||||
|
} else {
|
||||||
|
// If no type is given, return all prices for this product and vendor
|
||||||
|
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ?', [product, vendor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row in rows) {
|
||||||
|
if (row !== 'meta') {
|
||||||
|
priceRows.push(rows[row]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (conn) {
|
||||||
|
conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceRows;
|
||||||
|
};
|
||||||
|
|
||||||
// export const create = async (newItem: Product): Promise<void> => {
|
// export const create = async (newItem: Product): Promise<void> => {
|
||||||
// let conn;
|
// let conn;
|
||||||
// try {
|
// try {
|
||||||
|
|||||||
Generated
+92
@@ -2260,6 +2260,19 @@
|
|||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"apexcharts": {
|
||||||
|
"version": "3.22.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.22.3.tgz",
|
||||||
|
"integrity": "sha512-ZRZWmAmSdyc+tFhHMZ10ZxbvSbomWe46izpi8yQj5cKLxuujw2XeXVQ0jxnPl9yE5Q7W2hAbDWStaouBN4mSuw==",
|
||||||
|
"requires": {
|
||||||
|
"svg.draggable.js": "^2.2.2",
|
||||||
|
"svg.easing.js": "^2.0.0",
|
||||||
|
"svg.filter.js": "^2.0.2",
|
||||||
|
"svg.pathmorphing.js": "^0.1.3",
|
||||||
|
"svg.resize.js": "^1.4.3",
|
||||||
|
"svg.select.js": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"app-root-path": {
|
"app-root-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
|
||||||
@@ -7844,6 +7857,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ng/-/ng-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ng/-/ng-0.0.0.tgz",
|
||||||
"integrity": "sha512-HwR40IBJa1ZU+CIGyuy7vSCN3xFYlSRfw5eIwwKOdOMNNNIl8KhT6PXKmHuFEFYpfrbOMaCYtr4QOJ3gkkubcg=="
|
"integrity": "sha512-HwR40IBJa1ZU+CIGyuy7vSCN3xFYlSRfw5eIwwKOdOMNNNIl8KhT6PXKmHuFEFYpfrbOMaCYtr4QOJ3gkkubcg=="
|
||||||
},
|
},
|
||||||
|
"ng-apexcharts": {
|
||||||
|
"version": "1.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.5.6.tgz",
|
||||||
|
"integrity": "sha512-78vmZvrT9iqfZXE00+T8NTvR+EHV0wo4qqf0Zfu1/2KiwazCU9S5EROcmgqMQ1eCO7Sz4GiR19rLTMdtWL/WmQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nice-try": {
|
"nice-try": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
@@ -11950,6 +11978,70 @@
|
|||||||
"has-flag": "^3.0.0"
|
"has-flag": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"svg.draggable.js": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"svg.easing.js": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": ">=2.3.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"svg.filter.js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||||
|
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"svg.js": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||||
|
},
|
||||||
|
"svg.pathmorphing.js": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"svg.resize.js": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.6.5",
|
||||||
|
"svg.select.js": "^2.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"svg.select.js": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"svg.select.js": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||||
|
"requires": {
|
||||||
|
"svg.js": "^2.6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"svgo": {
|
"svgo": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
"@angular/platform-browser": "^10.2.3",
|
"@angular/platform-browser": "^10.2.3",
|
||||||
"@angular/platform-browser-dynamic": "^10.2.3",
|
"@angular/platform-browser-dynamic": "^10.2.3",
|
||||||
"@angular/router": "^10.2.3",
|
"@angular/router": "^10.2.3",
|
||||||
|
"apexcharts": "^3.22.3",
|
||||||
"ng": "0.0.0",
|
"ng": "0.0.0",
|
||||||
|
"ng-apexcharts": "^1.5.6",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
"tslib": "^2.0.3",
|
"tslib": "^2.0.3",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import {Injectable} from '@angular/core';
|
|
||||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
|
||||||
import process from 'process';
|
|
||||||
import {Product} from './models/product';
|
|
||||||
import {Observable, of} from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class ApiService {
|
|
||||||
apiUrl = 'https://backend.betterzon.xyz';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private http: HttpClient
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
getProducts(): Observable<Product[]> {
|
|
||||||
try {
|
|
||||||
const prods = this.http.get<Product[]>((this.apiUrl + '/products'));
|
|
||||||
console.log(prods);
|
|
||||||
return prods;
|
|
||||||
} catch (exception) {
|
|
||||||
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,17 @@ import {NgModule} from '@angular/core';
|
|||||||
|
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {AppRouting} from './app.routing';
|
import {AppRouting} from './app.routing';
|
||||||
import {ProductListComponent} from './product-list/product-list.component';
|
import {ProductListComponent} from './components/product-list/product-list.component';
|
||||||
import { LandingpageComponent } from './landingpage/landingpage.component';
|
import {LandingpageComponent} from './pages/landingpage/landingpage.component';
|
||||||
import { ProductDetailPageComponent } from './product-detail-page/product-detail-page.component';
|
import {ProductDetailPageComponent} from './pages/product-detail-page/product-detail-page.component';
|
||||||
import { FooterComponent } from './footer/footer.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 {FormsModule} from '@angular/forms';
|
||||||
|
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -15,12 +22,19 @@ import { FooterComponent } from './footer/footer.component';
|
|||||||
ProductListComponent,
|
ProductListComponent,
|
||||||
LandingpageComponent,
|
LandingpageComponent,
|
||||||
ProductDetailPageComponent,
|
ProductDetailPageComponent,
|
||||||
FooterComponent
|
FooterComponent,
|
||||||
|
ProductDetailsComponent,
|
||||||
|
ProductSearchPageComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
NewestPricesListComponent,
|
||||||
|
PageNotFoundPageComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRouting,
|
AppRouting,
|
||||||
HttpClientModule
|
HttpClientModule,
|
||||||
|
NgApexchartsModule,
|
||||||
|
FormsModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import {NgModule} from '@angular/core';
|
|||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {ProductListComponent} from './product-list/product-list.component';
|
import {ProductListComponent} from './components/product-list/product-list.component';
|
||||||
import {LandingpageComponent} from './landingpage/landingpage.component';
|
import {LandingpageComponent} from './pages/landingpage/landingpage.component';
|
||||||
import {ProductDetailPageComponent} from './product-detail-page/product-detail-page.component';
|
import {ProductDetailPageComponent} from './pages/product-detail-page/product-detail-page.component';
|
||||||
|
import {ProductSearchPageComponent} from './pages/product-search-page/product-search-page.component';
|
||||||
|
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: LandingpageComponent},
|
{path: '', component: LandingpageComponent},
|
||||||
{path: 'product', component: ProductDetailPageComponent}
|
{path: 'search', component: ProductSearchPageComponent},
|
||||||
|
{path: 'product/:id', component: ProductDetailPageComponent},
|
||||||
|
{path: '**', component: PageNotFoundPageComponent}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,42 @@
|
|||||||
|
.header {
|
||||||
|
width: auto;
|
||||||
|
background-color: dimgrey;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerContent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox input {
|
||||||
|
width: 100%;
|
||||||
|
padding: .25em;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileIcon {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-3d {
|
||||||
|
padding: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<header class="header">
|
||||||
|
<div id="headerContent" class='wrap'>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="assets/images/Betterzon.svg" alt="Betterzon Logo" width="50px" (click)="clickedLogo()">
|
||||||
|
</div>
|
||||||
|
<div class="searchBox">
|
||||||
|
<input *ngIf="showSearch===true" type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()">
|
||||||
|
</div>
|
||||||
|
<div class="profileIcon">
|
||||||
|
Profile
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
describe('HeaderComponent', () => {
|
||||||
|
let component: HeaderComponent;
|
||||||
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ HeaderComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
styleUrls: ['./header.component.css']
|
||||||
|
})
|
||||||
|
export class HeaderComponent implements OnInit {
|
||||||
|
searchInput: string;
|
||||||
|
@Input() showSearch: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.showSearch) {
|
||||||
|
this.showSearch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedLogo(): void {
|
||||||
|
this.router.navigate([('/')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.priceList {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="priceList">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Vendor</th>
|
||||||
|
<th>Current price</th>
|
||||||
|
<th>Visit</th>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let price of prices">
|
||||||
|
<td>{{vendorMap[price.vendor_id]?.name}}</td>
|
||||||
|
<td>{{price.price_in_cents / 100}}€</td>
|
||||||
|
<td><a href="https://www.amazon.com">Visit Shop</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NewestPricesListComponent } from './newest-prices-list.component';
|
||||||
|
|
||||||
|
describe('NewestPricesListComponent', () => {
|
||||||
|
let component: NewestPricesListComponent;
|
||||||
|
let fixture: ComponentFixture<NewestPricesListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ NewestPricesListComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NewestPricesListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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']
|
||||||
|
})
|
||||||
|
export class NewestPricesListComponent implements OnInit {
|
||||||
|
@Input() productId: number;
|
||||||
|
prices: Price[] = [];
|
||||||
|
vendors: Vendor[] = [];
|
||||||
|
vendorMap = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/* Div that contains each product */
|
||||||
|
.productItem {
|
||||||
|
padding: .25em;
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 25% 25% 30%;
|
||||||
|
grid-template-areas:
|
||||||
|
'image title title priceChart'
|
||||||
|
'image description description priceChart'
|
||||||
|
'image priceAlarm bestPrice blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image div */
|
||||||
|
.productImageContainer {
|
||||||
|
grid-area: image;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product Image */
|
||||||
|
.productImage {
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 300px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title div */
|
||||||
|
.productTitle {
|
||||||
|
grid-area: title;
|
||||||
|
font-size: 2em;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price div */
|
||||||
|
.priceChart {
|
||||||
|
grid-area: priceChart;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description div */
|
||||||
|
.productDescription {
|
||||||
|
grid-area: description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price alarm div */
|
||||||
|
.priceAlarm {
|
||||||
|
grid-area: priceAlarm;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: dimgrey;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: .25em;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<div class="productItem">
|
||||||
|
<div class="productImageContainer">
|
||||||
|
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
|
||||||
|
</div>
|
||||||
|
<div class="productTitle">
|
||||||
|
<b>{{product?.name}}</b>
|
||||||
|
</div>
|
||||||
|
<div class="priceChart">
|
||||||
|
<div style="text-align:center">
|
||||||
|
<apx-chart
|
||||||
|
[series]="chartOptions.series"
|
||||||
|
[chart]="chartOptions.chart"
|
||||||
|
[xaxis]="chartOptions.xaxis"
|
||||||
|
[title]="chartOptions.title"
|
||||||
|
></apx-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="productDescription">
|
||||||
|
<div>
|
||||||
|
{{product?.short_description}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="priceAlarm">
|
||||||
|
Set Price Alarm
|
||||||
|
</div>
|
||||||
|
<div class="bestPriceContainer">
|
||||||
|
<div class="bestPrice">
|
||||||
|
Best price: {{currentlyLowestPrice?.price_in_cents / 100}}€ at
|
||||||
|
vendor {{vendorMap[currentlyLowestPrice.vendor_id]?.name}}!
|
||||||
|
</div>
|
||||||
|
<div class="amazonPrice">
|
||||||
|
Amazon-price: {{currentAmazonPrice?.price_in_cents / 100}}€ (<span
|
||||||
|
*ngIf="getAmazonPriceDifference()>0">+</span>{{getAmazonPriceDifference()}}%)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProductDetailsComponent } from './product-details.component';
|
||||||
|
|
||||||
|
describe('ProductDetailsComponent', () => {
|
||||||
|
let component: ProductDetailsComponent;
|
||||||
|
let fixture: ComponentFixture<ProductDetailsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ProductDetailsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProductDetailsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import {Component, Input, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {Product} from '../../models/product';
|
||||||
|
import {ApiService} from '../../services/api.service';
|
||||||
|
import {
|
||||||
|
ChartComponent,
|
||||||
|
ApexAxisChartSeries,
|
||||||
|
ApexChart,
|
||||||
|
ApexXAxis,
|
||||||
|
ApexTitleSubtitle, ApexStroke
|
||||||
|
} from 'ng-apexcharts';
|
||||||
|
import {Price} from '../../models/price';
|
||||||
|
import {Vendor} from '../../models/vendor';
|
||||||
|
|
||||||
|
export type ChartOptions = {
|
||||||
|
series: ApexAxisChartSeries;
|
||||||
|
chart: ApexChart;
|
||||||
|
xaxis: ApexXAxis;
|
||||||
|
title: ApexTitleSubtitle;
|
||||||
|
stroke: ApexStroke;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-details',
|
||||||
|
templateUrl: './product-details.component.html',
|
||||||
|
styleUrls: ['./product-details.component.css']
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getProduct();
|
||||||
|
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: prices
|
||||||
|
}
|
||||||
|
],
|
||||||
|
chart: {
|
||||||
|
height: 350,
|
||||||
|
type: 'area'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: 'Lowest price'
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: categs
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: 'stepline'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAmazonPriceDifference(): number {
|
||||||
|
const amazonPrice = this.currentAmazonPrice?.price_in_cents;
|
||||||
|
const lowestPrice = this.currentlyLowestPrice?.price_in_cents;
|
||||||
|
|
||||||
|
const percentage = ((amazonPrice / lowestPrice) - 1) * 100;
|
||||||
|
|
||||||
|
return Math.round(percentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* Div that contains each product */
|
||||||
|
.productItem {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: dimgrey;
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: .25em;
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
max-width: 50em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 10% 80% 10%;
|
||||||
|
grid-template-areas:
|
||||||
|
'image title price'
|
||||||
|
'image description price';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image div */
|
||||||
|
.productImageContainer {
|
||||||
|
grid-area: image;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product Image */
|
||||||
|
.productImage {
|
||||||
|
max-width: 50px;
|
||||||
|
max-height: 50px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title div */
|
||||||
|
.productTitle {
|
||||||
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price div */
|
||||||
|
.productPrice {
|
||||||
|
grid-area: price;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price text */
|
||||||
|
.productPrice * {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description div */
|
||||||
|
.productDescription {
|
||||||
|
grid-area: description;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div *ngIf="products.length==0">
|
||||||
|
No Products found!
|
||||||
|
</div>
|
||||||
|
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
|
||||||
|
<div class="productImageContainer" *ngIf="showProductPicture===true">
|
||||||
|
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
|
||||||
|
</div>
|
||||||
|
<div class="productTitle">
|
||||||
|
<b>{{product.name}}</b>
|
||||||
|
</div>
|
||||||
|
<div class="productPrice">
|
||||||
|
5€
|
||||||
|
</div>
|
||||||
|
<div class="productDescription">
|
||||||
|
<div *ngIf="product.short_description.length > 300">
|
||||||
|
{{product.short_description.substring(0, 300) + "..."}}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="product.short_description.length <= 300">
|
||||||
|
{{product.short_description}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
File renamed without changes.
@@ -0,0 +1,67 @@
|
|||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {ApiService} from '../../services/api.service';
|
||||||
|
import {Product} from '../../models/product';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-list',
|
||||||
|
templateUrl: './product-list.component.html',
|
||||||
|
styleUrls: ['./product-list.component.css']
|
||||||
|
})
|
||||||
|
export class ProductListComponent implements OnInit {
|
||||||
|
products: Product[] = [];
|
||||||
|
@Input() numberOfProducts: number;
|
||||||
|
@Input() showProductPicture: boolean;
|
||||||
|
@Input() searchQuery: string;
|
||||||
|
@Input() type: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadParams(): void {
|
||||||
|
if (!this.numberOfProducts) {
|
||||||
|
this.numberOfProducts = 10;
|
||||||
|
}
|
||||||
|
if (!this.showProductPicture) {
|
||||||
|
this.showProductPicture = false;
|
||||||
|
}
|
||||||
|
if (!this.searchQuery) {
|
||||||
|
this.searchQuery = '';
|
||||||
|
}
|
||||||
|
if (!this.type) {
|
||||||
|
this.type = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case 'search': {
|
||||||
|
this.getSearchedProducts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this.getProducts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProducts(): void {
|
||||||
|
this.apiService.getProducts().subscribe(products => this.products = products);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchedProducts(): void {
|
||||||
|
this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => this.products = products);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedProduct(product: Product): void {
|
||||||
|
this.router.navigate([('/product/' + product.product_id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<app-product-list numberOfProducts="20" showProductPicture="true"></app-product-list>
|
|
||||||
<app-footer></app-footer>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-landingpage',
|
|
||||||
templateUrl: './landingpage.component.html',
|
|
||||||
styleUrls: ['./landingpage.component.css']
|
|
||||||
})
|
|
||||||
export class LandingpageComponent implements OnInit {
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface Price {
|
||||||
|
price_id: number;
|
||||||
|
product_id: number;
|
||||||
|
vendor_id: number;
|
||||||
|
price_in_cents: number;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#mainComponents {
|
||||||
|
margin: 5em;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<app-header [showSearch]="false"></app-header>
|
||||||
|
<div id="mainComponents">
|
||||||
|
<div id="searchContainer">
|
||||||
|
<input type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()">
|
||||||
|
</div>
|
||||||
|
<div id="productListsContainer">
|
||||||
|
<div id="popularSearchesList">
|
||||||
|
<h2>Popular Searches</h2>
|
||||||
|
<app-product-list numberOfProducts="3" [showProductPicture]="true"
|
||||||
|
type="popularSearches"></app-product-list>
|
||||||
|
</div>
|
||||||
|
<div id="bestDealsList">
|
||||||
|
<h2>Best Deals</h2>
|
||||||
|
<app-product-list numberOfProducts="3" [showProductPicture]="true" type="bestDeals"></app-product-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
File renamed without changes.
@@ -0,0 +1,29 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-landingpage',
|
||||||
|
templateUrl: './landingpage.component.html',
|
||||||
|
styleUrls: ['./landingpage.component.css']
|
||||||
|
})
|
||||||
|
export class LandingpageComponent implements OnInit {
|
||||||
|
searchInput: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>404</h1>
|
||||||
|
<p>Page not found!</p>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PageNotFoundPageComponent } from './page-not-found-page.component';
|
||||||
|
|
||||||
|
describe('PageNotFoundPageComponent', () => {
|
||||||
|
let component: PageNotFoundPageComponent;
|
||||||
|
let fixture: ComponentFixture<PageNotFoundPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ PageNotFoundPageComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PageNotFoundPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-page-not-found-page',
|
||||||
|
templateUrl: './page-not-found-page.component.html',
|
||||||
|
styleUrls: ['./page-not-found-page.component.css']
|
||||||
|
})
|
||||||
|
export class PageNotFoundPageComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#mainComponents {
|
||||||
|
padding: 5em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<app-header [showSearch]="true"></app-header>
|
||||||
|
<div id="mainComponents">
|
||||||
|
<app-product-details [productId]="productId"></app-product-details>
|
||||||
|
<app-newest-prices-list [productId]="productId"></app-newest-prices-list>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
File renamed without changes.
@@ -0,0 +1,21 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-detail-page',
|
||||||
|
templateUrl: './product-detail-page.component.html',
|
||||||
|
styleUrls: ['./product-detail-page.component.css']
|
||||||
|
})
|
||||||
|
export class ProductDetailPageComponent implements OnInit {
|
||||||
|
productId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.productId = router.url.substr(9, router.url.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#mainComponents {
|
||||||
|
margin: 5em;
|
||||||
|
margin-top: .5em;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<app-header [showSearch]="true"></app-header>
|
||||||
|
<div id="mainComponents">
|
||||||
|
<app-product-list numberOfProducts="20" [showProductPicture]="true" searchQuery="{{searchTerm}}"
|
||||||
|
type="search"></app-product-list>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProductSearchPageComponent } from './product-search-page.component';
|
||||||
|
|
||||||
|
describe('ProductSearchPageComponent', () => {
|
||||||
|
let component: ProductSearchPageComponent;
|
||||||
|
let fixture: ComponentFixture<ProductSearchPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ProductSearchPageComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProductSearchPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-search-page',
|
||||||
|
templateUrl: './product-search-page.component.html',
|
||||||
|
styleUrls: ['./product-search-page.component.css']
|
||||||
|
})
|
||||||
|
export class ProductSearchPageComponent implements OnInit {
|
||||||
|
searchTerm: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchTerm = this.route.snapshot.queryParamMap.get('q');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -1 +0,0 @@
|
|||||||
<p>product-detail-page works!</p>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-product-detail-page',
|
|
||||||
templateUrl: './product-detail-page.component.html',
|
|
||||||
styleUrls: ['./product-detail-page.component.css']
|
|
||||||
})
|
|
||||||
export class ProductDetailPageComponent implements OnInit {
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.productItem {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: dimgrey;
|
|
||||||
border-radius: .5em;
|
|
||||||
padding: .25em;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<meta charset="UTF-8">
|
|
||||||
<p class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
|
|
||||||
{{product.name}}
|
|
||||||
</p>
|
|
||||||
<div *ngIf="showProductPicture">
|
|
||||||
Test
|
|
||||||
</div>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
|
||||||
import {ApiService} from '../api.service';
|
|
||||||
import {Product} from '../models/product';
|
|
||||||
import {Router} from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-product-list',
|
|
||||||
templateUrl: './product-list.component.html',
|
|
||||||
styleUrls: ['./product-list.component.css']
|
|
||||||
})
|
|
||||||
export class ProductListComponent implements OnInit {
|
|
||||||
products: Product[];
|
|
||||||
@Input() numberOfProducts: number;
|
|
||||||
@Input() showProductPicture: boolean;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private apiService: ApiService,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.getProducts();
|
|
||||||
|
|
||||||
if (!this.numberOfProducts) {
|
|
||||||
this.numberOfProducts = 10;
|
|
||||||
}
|
|
||||||
if (!this.showProductPicture) {
|
|
||||||
this.showProductPicture = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getProducts(): void {
|
|
||||||
this.apiService.getProducts().subscribe(products => this.products = products);
|
|
||||||
}
|
|
||||||
|
|
||||||
clickedProduct(product: Product): void {
|
|
||||||
this.router.navigate([('/helloworld/' + product.product_id)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File renamed without changes.
@@ -0,0 +1,101 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
apiUrl = 'https://backend.betterzon.xyz';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getProduct(id): Observable<Product> {
|
||||||
|
try {
|
||||||
|
const prod = this.http.get<Product>((this.apiUrl + '/products/' + id));
|
||||||
|
return prod;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductsByQuery(query): Observable<Product[]> {
|
||||||
|
try {
|
||||||
|
const prods = this.http.get<Product[]>((this.apiUrl + '/products/search/' + query));
|
||||||
|
return prods;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProducts(): Observable<Product[]> {
|
||||||
|
try {
|
||||||
|
const prods = this.http.get<Product[]>((this.apiUrl + '/products'));
|
||||||
|
return prods;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrices(): Observable<Price[]> {
|
||||||
|
try {
|
||||||
|
const prices = this.http.get<Price[]>((this.apiUrl + '/prices'));
|
||||||
|
return prices;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLowestPrices(productId): Observable<Price[]> {
|
||||||
|
try {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = params.append('product', productId);
|
||||||
|
params = params.append('type', 'lowest');
|
||||||
|
const prices = this.http.get<Price[]>((this.apiUrl + '/prices'), {params});
|
||||||
|
return prices;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAmazonPrice(productId): Observable<Price> {
|
||||||
|
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<Price>((this.apiUrl + '/prices'), {params});
|
||||||
|
return price;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPricePerVendor(productId): Observable<Price[]> {
|
||||||
|
try {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = params.append('product', productId);
|
||||||
|
params = params.append('type', 'newest');
|
||||||
|
const prices = this.http.get<Price[]>((this.apiUrl + '/prices'), {params});
|
||||||
|
return prices;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVendors(): Observable<Vendor[]> {
|
||||||
|
try {
|
||||||
|
const vendors = this.http.get<Vendor[]>((this.apiUrl + '/vendors'));
|
||||||
|
return vendors;
|
||||||
|
} catch (exception) {
|
||||||
|
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 454.14 358"><defs><style>.cls-1{fill:#d63031;}.cls-2,.cls-8,.cls-9{fill:#fff;}.cls-3,.cls-7,.cls-8{opacity:0.2;}.cls-4{opacity:0.4;}.cls-5{fill:#e6e6e6;}.cls-6{fill:#f2f2f2;}.cls-9{opacity:0.3;}.cls-10{fill:#092ce3;}.cls-11{fill:#81ecec;}</style></defs><g id="Ebene_1" data-name="Ebene 1"><path class="cls-1" d="M642.6,590.49a178,178,0,0,1-140-69.45,178.5,178.5,0,0,1,140-287.53h2.31c.59,0,1.18,0,1.77,0h.37c1,0,2.07,0,3.1.08l1.22,0,1.91.09c1,0,2,.12,3.07.19a178.52,178.52,0,0,1,17,354.15l-.1,0,.13,0c-1.26.21-2.44.39-3.64.55s-2.42.34-3.62.49-2.43.3-3.65.42-2.45.24-3.68.34c-2.32.2-4.62.34-6.83.44l-.63,0-.49,0c-1,0-2,.07-3,.08h-.27l-.9,0c-.59,0-1.16,0-1.75,0Z" transform="translate(-403.93 -233)"/><path d="M644.94,234c.58,0,1.15,0,1.76,0H647c.92,0,1.94,0,3.1.08l.6,0,.59,0,.88,0,.9,0h.12c1,0,2.06.12,3.08.19a178,178,0,0,1,17,353.15l-.11,0c-1.06.18-2.21.35-3.53.54-1,.16-2.18.31-3.61.49-1.21.16-2.43.3-3.64.42s-2.29.22-3.66.33c-2.33.2-4.63.35-6.82.44h-.27l-.32,0-.56,0c-1.12,0-2.06.07-2.95.08h-1.17c-.59,0-1.17,0-1.76,0h-2.3a178,178,0,0,1,0-356h2.34m0-1H642.6a179,179,0,0,0,0,358h2.31c.6,0,1.19,0,1.79,0l.84,0h.31c1,0,2,0,3-.08l.55,0,.61,0q3.45-.15,6.85-.44c1.23-.1,2.46-.21,3.69-.34s2.44-.26,3.66-.42,2.43-.31,3.63-.49,2.39-.35,3.57-.55h.05a179,179,0,0,0-17.06-355.14c-1-.07-2.06-.14-3.1-.19h-.16l-1.74-.08-1.21,0c-1,0-2.08-.07-3.12-.08h-.35c-.6,0-1.19,0-1.79,0Z" transform="translate(-403.93 -233)"/><ellipse class="cls-2" cx="237.97" cy="181" rx="107.13" ry="106.5"/><path d="M641.91,308c58.79,0,106.62,47.55,106.62,106S700.7,520,641.91,520,535.28,472.45,535.28,414s47.83-106,106.63-106m0-1c-59.44,0-107.63,47.91-107.63,107s48.19,107,107.63,107,107.62-47.91,107.62-107S701.35,307,641.91,307Z" transform="translate(-403.93 -233)"/><g class="cls-3"><path class="cls-2" d="M673.44,588.57h-.05c-1.17.2-2.36.38-3.55.55s-2.41.34-3.61.49-2.43.3-3.64.42-2.45.24-3.67.34q-3.39.29-6.82.44l-.54,0h-.06l-.55,0c-1,0-2,.07-3,.08h-.3c-1.11,0-2.21,0-3.33,0h-1.59A178,178,0,0,1,466,412c0-98.42,79-178.29,176.76-179h1.8c.88,0,1.75,0,2.63,0s2.07,0,3.1.08c.38,0,.76,0,1.13,0,.6,0,1.21,0,1.81.07h.16c1,0,2.05.12,3.08.19a176.84,176.84,0,0,0-82.57,37.36,13.8,13.8,0,0,0-8.33-2.79A14,14,0,0,0,554,289.73q-3.35,3.7-6.47,7.58-5.41,6.7-10.17,13.93a179.6,179.6,0,0,0,71,260A176.13,176.13,0,0,0,673.44,588.57Z" transform="translate(-403.93 -233)"/></g><g class="cls-4"><path class="cls-2" d="M651.44,590.85l.47,0c-1.74.06-3.49.1-5.24.11-.6,0-1.19,0-1.79,0h-2.33a181.36,181.36,0,0,1-46.45-6.63C520.45,563.26,465,494.31,465,412.5c0-97.82,79.28-177.25,177.55-178.49h2.33c.6,0,1.19,0,1.79,0,1.75,0,3.5,0,5.24.11l-.53,0h-.08c-95.8,4-172.23,82.3-172.23,178.34,0,77.58,49.86,143.6,119.53,168.21a180.7,180.7,0,0,0,52.78,10.14Z" transform="translate(-403.93 -233)"/></g><g id="Ebene_3" data-name="Ebene 3"><path class="cls-5" d="M736,466c16.42,18.66,36.86,29.82,62,34-12.09,25-32.82,45.66-58,62-25.76-4.22-40.72-24.1-52-51C708,501.51,725.42,486.64,736,466Z" transform="translate(-403.93 -233)"/><path class="cls-5" d="M599.51,511.26c-16.6,17.16-25.27,38.66-27,62.62-25.39-9-46.8-25-65-47,1.74-24.55,18.62-41.85,43.69-55C562.24,489.72,578.52,503.29,599.51,511.26Z" transform="translate(-403.93 -233)"/><path class="cls-6" d="M563.76,340.32c-11.73-20.93-29.42-37.78-52.45-47,16.71-20.85,40-37.43,67.36-47.38,23.61,9.39,37.44,35.81,42.57,63.36C600.24,313.93,578.12,323.25,563.76,340.32Z" transform="translate(-403.93 -233)"/><path d="M450,399" transform="translate(-403.93 -233)"/><path class="cls-5" d="M690.5,318.5c19.56-16.7,30.09-38.91,34.5-64.5,26.2,12.34,48.41,31.85,65.5,57.5-4.45,26.22-24.32,43-52.5,54.5C728.09,345.62,712.11,329.29,690.5,318.5Z" transform="translate(-403.93 -233)"/></g><path class="cls-7" d="M822.91,412A179,179,0,0,1,673.44,588.57h-.05c-1.18.2-2.37.38-3.57.55s-2.42.34-3.63.49-2.44.3-3.66.42-2.46.24-3.69.34q-3.4.29-6.85.44l-.55,0,.47,0c-1.73.06-3.47.1-5.21.11-.6,0-1.19,0-1.79,0H642.6a179,179,0,0,1-46.37-6.41l.14-.24a41.43,41.43,0,0,1,2.49-3.66,60.28,60.28,0,0,1,9.17-9.47,80.29,80.29,0,0,1,14.47-9.71c14.63-7.56,19.55-3.08,42-10,15.22-4.69,25.09-10.49,43-21,23.09-13.56,34.64-20.34,47-33,11.14-11.41,28.77-29.47,33-56,2.87-18-3.24-22.35,1-45,3.84-20.5,12-33.85,14-37,.87-1.38,4-6.31,8.46-11.92A178.43,178.43,0,0,1,822.91,412Z" transform="translate(-403.93 -233)"/><ellipse class="cls-8" cx="138.12" cy="71.5" rx="8.94" ry="8.5"/><ellipse class="cls-8" cx="133.07" cy="51.5" rx="5" ry="5.5"/><path class="cls-9" d="M579,282a14,14,0,1,1-5.62-11.21A14,14,0,0,1,579,282Z" transform="translate(-403.93 -233)"/></g><g id="Ebene_2" data-name="Ebene 2"><path class="cls-10" d="M708.37,477.38c-9.8,0-17.19-6.53-17.19-15.19,0-9.56,6.09-15,15.88-19.39a8.54,8.54,0,0,1-1.93-3.64c-5.39,5.86-11.62,10.1-19,10.1-9.44,0-16-7.07-16-17.19,0-.34,0-.67,0-1-9,11.33-21.45,18.19-33.5,18.19-7.89,0-14.06-2.53-17.94-7.09-4.33,3.95-9.66,7.09-16,7.09-5.71,0-10.31-2.17-13.31-6-4,3.38-8.95,6-14.71,6-6.39,0-11.4-2.72-14.33-7.4-6.93,4.78-14.58,7.4-22,7.4-9.21,0-16.14-3.56-19.75-9Line truncated
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1 +1,5 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user