Compare commits

..

16 Commits

Author SHA1 Message Date
Patrick 0f19bd77cd Merge branch 'develop' into BETTERZON-78 2021-05-11 21:59:02 +02:00
Patrick 536acc06ee Merge branch 'develop' into BETTERZON-78 2021-05-08 20:01:12 +02:00
Patrick ac237a6ba1 Apply suggestions from code review
Switching from single to double quotes
2021-05-08 20:00:30 +02:00
Patrick 058230e9a9 Merge branch 'develop' into BETTERZON-78 2021-05-06 22:47:22 +02:00
Paddy f71f53c869 Adding cookieconsent as dependency again since it was removed by a merge 2021-05-05 19:59:02 +02:00
Paddy 6982a139af Adding cookieconsent as dependency again since it was removed by a merge 2021-05-05 18:48:53 +02:00
illumizoldyck b0158198d4 Merge branch 'BETTERZON-78' of https://github.com/Mueller-Patrick/Betterzon into BETTERZON-78 2021-05-05 18:42:52 +02:00
illumizoldyck 7e5ef71924 BETTERZON-78 adding bottom bar and top bar 2021-05-05 18:40:22 +02:00
Paddy b17b92fcc3 Merge remote-tracking branch 'origin/develop' into BETTERZON-78
# Conflicts:
#	Frontend/angular.json
#	Frontend/package-lock.json
#	Frontend/src/app/app.module.ts
#	Frontend/src/styles.css
2021-05-05 18:39:04 +02:00
illumizoldyck 0c50162fdf WIP: creating footer using grid. 2021-05-04 22:17:15 +02:00
illumizoldyck 825f744af9 BETTERZON-74
simple top-bar has been created.
2021-04-28 17:53:56 +02:00
Paddy e0f1724d95 Merge remote-tracking branch 'origin/develop' into BETTERZON-52
# Conflicts:
#	Frontend/angular.json
#	Frontend/package-lock.json
#	Frontend/package.json
#	Frontend/src/app/app.module.ts
#	Frontend/src/index.html
2021-04-08 10:52:52 +02:00
illumizoldyck 93b17bc65f BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.
2021-04-08 10:42:45 +02:00
Paddy 64074b48e8 Merge remote-tracking branch 'origin/develop' into BETTERZON-31
# Conflicts:
#	Frontend/src/app/app.module.ts
2021-04-06 20:19:40 +02:00
Paddy ee50296dc9 BETTERZON-31: Fixing dependencies 2021-04-06 20:18:17 +02:00
illumizoldyck 57c3f521d1 BETTERZON-31, dependencies. 2021-04-06 20:14:10 +02:00
176 changed files with 6106 additions and 30401 deletions
+20 -4964
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -11,9 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@types/cookie-parser": "^1.4.2",
"bcrypt": "^5.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
-11
View File
@@ -14,12 +14,6 @@ import {vendorsRouter} from './models/vendors/vendors.router';
import {errorHandler} from './middleware/error.middleware';
import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router';
import {pricealarmsRouter} from './models/pricealarms/pricealarms.router';
import {contactpersonsRouter} from './models/contact_persons/contact_persons.router';
import {favoriteshopsRouter} from './models/favorite_shops/favoriteshops.router';
import {crawlingstatusRouter} from './models/crawling_status/crawling_status.router';
const cookieParser = require('cookie-parser');
dotenv.config();
@@ -44,17 +38,12 @@ const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use('/products', productsRouter);
app.use('/categories', categoriesRouter);
app.use('/manufacturers', manufacturersRouter);
app.use('/prices', pricesRouter);
app.use('/users', usersRouter);
app.use('/vendors', vendorsRouter);
app.use('/pricealarms', pricealarmsRouter);
app.use('/contactpersons', contactpersonsRouter);
app.use('/favoriteshops', favoriteshopsRouter);
app.use('/crawlingstatus', crawlingstatusRouter);
app.use(errorHandler);
app.use(notFoundHandler);
+3 -3
View File
@@ -1,5 +1,5 @@
import HttpException from '../common/http-exception';
import {Request, Response, NextFunction} from 'express';
import HttpException from "../common/http-exception";
import { Request, Response, NextFunction } from "express";
export const errorHandler = (
error: HttpException,
@@ -9,7 +9,7 @@ export const errorHandler = (
) => {
const status = error.statusCode || 500;
const message =
error.message || 'It\'s not you. It\'s us. We are having some problems.';
error.message || "It's not you. It's us. We are having some problems.";
response.status(status).send(message);
};
@@ -1,4 +1,4 @@
import {Request, Response, NextFunction} from 'express';
import { Request, Response, NextFunction } from "express";
export const notFoundHandler = (
request: Request,
@@ -6,7 +6,7 @@ export const notFoundHandler = (
next: NextFunction
) => {
const message = 'Resource not found';
const message = "Resource not found";
response.status(404).send(message);
};
@@ -27,7 +27,7 @@ categoriesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(categories);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -46,7 +46,7 @@ categoriesRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(category);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -65,6 +65,6 @@ categoriesRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(categories);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -1,9 +0,0 @@
export interface Contact_Person {
contact_person_id: number;
first_name: string;
last_name: string;
gender: string;
email: string;
phone: string;
vendor_id: number;
}
@@ -1,5 +0,0 @@
import {Contact_Person} from './contact_person.interface';
export interface Contact_Persons {
[key: number]: Contact_Person;
}
@@ -1,133 +0,0 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as ContactPersonService from './contact_persons.service';
import {Contact_Person} from './contact_person.interface';
import {Contact_Persons} from './contact_persons.interface';
import * as UserService from '../users/users.service';
import * as PriceService from '../prices/prices.service';
/**
* Router Definition
*/
export const contactpersonsRouter = express.Router();
/**
* Controller Definitions
*/
// GET contactpersons/
contactpersonsRouter.get('/', async (req: Request, res: Response) => {
try {
const contacts: Contact_Persons = await ContactPersonService.findAll();
res.status(200).send(contacts);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET contactpersons/:id
contactpersonsRouter.get('/: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 contact: Contact_Person = await ContactPersonService.find(id);
res.status(200).send(contact);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET contactpersons/byvendor/:id
contactpersonsRouter.get('/byvendor/: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 contacts: Contact_Persons = await ContactPersonService.findByVendor(id);
res.status(200).send(contacts);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST contactpersons/
contactpersonsRouter.post('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const vendor_id = req.body.vendor_id;
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const gender = req.body.gender;
const email = req.body.email;
const phone = req.body.phone;
const success = await ContactPersonService.createContactEntry(user.user_id, vendor_id, first_name, last_name, gender, email, phone);
if (success) {
res.status(201).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT contactpersons/:id
contactpersonsRouter.put('/:id', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const contact_person_id = parseInt(req.params.id, 10);
const vendor_id = req.body.vendor_id;
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const gender = req.body.gender;
const email = req.body.email;
const phone = req.body.phone;
const success = await ContactPersonService.updateContactEntry(user.user_id, contact_person_id, vendor_id, first_name, last_name, gender, email, phone);
if (success) {
res.status(200).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@@ -1,175 +0,0 @@
import * as dotenv from 'dotenv';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {Contact_Person} from './contact_person.interface';
import {Contact_Persons} from './contact_persons.interface';
/**
* Service Methods
*/
/**
* Fetches and returns all known contact persons
*/
export const findAll = async (): Promise<Contact_Persons> => {
let conn;
let contRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons');
for (let row in rows) {
if (row !== 'meta') {
contRows.push(rows[row]);
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return contRows;
};
/**
* Fetches and returns the contact person with the specified id
* @param id The id of the contact person to fetch
*/
export const find = async (id: number): Promise<Contact_Person> => {
let conn;
let cont: any;
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons WHERE contact_person_id = ?', id);
for (let row in rows) {
if (row !== 'meta') {
cont = rows[row];
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return cont;
};
/**
* Fetches and returns the contact persons for the specified vendor
* @param id The id of the vendor to fetch contact persons for
*/
export const findByVendor = async (id: number): Promise<Contact_Persons> => {
let conn;
let contRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons WHERE vendor_id = ?', id);
for (let row in rows) {
if (row !== 'meta') {
contRows.push(rows[row]);
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return contRows;
};
/**
* Creates a contact entry record
* @param user_id The user id of the issuing user
* @param vendor_id The vendor id of the vendor to create the record for
* @param first_name The first name of the contact person
* @param last_name The last name of the contact person
* @param gender The gender of the contact person
* @param email The email of the contact person
* @param phone The phone number of the contact person
*/
export const createContactEntry = async (user_id: number, vendor_id: number, first_name: string, last_name: string, gender: string, email: string, phone: string): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
// Create contact person entry
const res = await conn.query('INSERT INTO contact_persons (first_name, last_name, gender, email, phone, vendor_id) VALUES (?, ?, ?, ?, ?, ?)', [first_name, last_name, gender, email, phone, vendor_id]);
// If there are more / less than 1 affected rows, return false
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Updates a contact entry record
* @param user_id The user id of the issuing user
* @param contact_person_id The id of the record to update
* @param vendor_id The vendor id of the vendor to create the record for
* @param first_name The first name of the contact person
* @param last_name The last name of the contact person
* @param gender The gender of the contact person
* @param email The email of the contact person
* @param phone The phone number of the contact person
*/
export const updateContactEntry = async (user_id: number, contact_person_id: number, vendor_id: number, first_name: string, last_name: string, gender: string, email: string, phone: string): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
// Create contact person entry
const res = await conn.query('UPDATE contact_persons SET first_name = ?, last_name = ?, gender = ?, email = ?, phone = ? WHERE contact_person_id = ? AND vendor_id = ?', [first_name, last_name, gender, email, phone, contact_person_id, vendor_id]);
// If there are more / less than 1 affected rows, return false
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
@@ -1,7 +0,0 @@
export interface Crawling_Status {
process_id: number;
started_timestamp: Date;
combinations_to_crawl: number;
successful_crawls: number;
failed_crawls: number;
}
@@ -1,44 +0,0 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as CrawlingStatusService from './crawling_status.service';
import {Crawling_Status} from './crawling_status.interface';
import {Crawling_Statuses} from './crawling_statuses.interface';
import * as UserService from '../users/users.service';
/**
* Router Definition
*/
export const crawlingstatusRouter = express.Router();
/**
* Controller Definitions
*/
// GET crawlingstatus/
crawlingstatusRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
if (!user.is_admin) {
res.status(403).send({});
return;
}
const status: Crawling_Status = await CrawlingStatusService.getCurrent();
res.status(200).send(status);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@@ -1,75 +0,0 @@
import * as dotenv from 'dotenv';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {Crawling_Status} from './crawling_status.interface';
import {Crawling_Statuses} from './crawling_statuses.interface';
/**
* Service Methods
*/
/**
* Fetches and returns the current crawling status if the issuing user is an admin
*/
export const getCurrent = async (): Promise<Crawling_Status> => {
let conn;
try {
conn = await pool.getConnection();
// Get the current crawling process
let process_info = {
process_id: -1,
started_timestamp: new Date(),
combinations_to_crawl: -1
};
const process = await conn.query('SELECT process_id, started_timestamp, combinations_to_crawl FROM crawling_processes ORDER BY started_timestamp DESC LIMIT 1');
for (let row in process) {
if (row !== 'meta') {
process_info = process[row];
}
}
// Get the current status
let total_crawls = 0;
let successful_crawls = 0;
const rows = await conn.query('SELECT COUNT(status_id) as total, SUM(success) as successful FROM crawling_status WHERE process_id = ?', process_info.process_id);
for (let row in rows) {
if (row !== 'meta') {
total_crawls = rows[row].total;
successful_crawls = rows[row].successful;
}
}
const failed_crawls = total_crawls - successful_crawls;
return {
process_id: process_info.process_id,
started_timestamp: process_info.started_timestamp,
combinations_to_crawl: process_info.combinations_to_crawl,
successful_crawls: successful_crawls,
failed_crawls: failed_crawls,
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
@@ -1,5 +0,0 @@
import {Crawling_Status} from './crawling_status.interface';
export interface Crawling_Statuses {
[key: number]: Crawling_Status;
}
@@ -1,5 +0,0 @@
export interface FavoriteShop {
favorite_id: number;
vendor_id: number;
user_id: number;
}
@@ -1,5 +0,0 @@
import {FavoriteShop} from './favoriteshop.interface';
export interface FavoriteShops {
[key: number]: FavoriteShop;
}
@@ -1,106 +0,0 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as FavoriteShopsService from './favoriteshops.service';
import {FavoriteShop} from './favoriteshop.interface';
import {FavoriteShops} from './favoriteshops.interface';
import * as UserService from '../users/users.service';
/**
* Router Definition
*/
export const favoriteshopsRouter = express.Router();
/**
* Controller Definitions
*/
//GET favoriteshops/
favoriteshopsRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
const priceAlarms = await FavoriteShopsService.getFavoriteShops(user.user_id);
res.status(200).send(priceAlarms);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST favoriteshops/
favoriteshopsRouter.post('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation
const vendor_id = req.body.vendor_id;
if (!vendor_id) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Create price alarm
const success = await FavoriteShopsService.createFavoriteShop(user.user_id, vendor_id);
if (success) {
res.status(201).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// DELETE favoriteshops/
favoriteshopsRouter.delete('/:id', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation
const favorite_id = parseInt(req.params.id, 10);
if (!favorite_id) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Create price alarm
const success = await FavoriteShopsService.deleteFavoriteShop(user.user_id, favorite_id);
if (success) {
res.status(201).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@@ -1,92 +0,0 @@
import * as dotenv from 'dotenv';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {FavoriteShop} from './favoriteshop.interface';
import {FavoriteShops} from './favoriteshops.interface';
/**
* Service Methods
*/
/**
* Creates a favorite shop entry for the given user for the given shop
* @param user_id The id of the user to create the favorite shop entry for
* @param vendor_id The id of the vendor to set as favorite
*/
export const createFavoriteShop = async (user_id: number, vendor_id: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('INSERT INTO favorite_shops (vendor_id, user_id) VALUES (?, ?)', [vendor_id, user_id]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Fetches and returns all favorite shops for the given user
* @param user_id
*/
export const getFavoriteShops = async (user_id: number): Promise<FavoriteShops> => {
let conn;
let shops = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT favorite_id, vendor_id, user_id FROM favorite_shops WHERE user_id = ?', user_id);
for (let row in rows) {
if (row !== 'meta') {
shops.push(rows[row]);
}
}
return shops;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Deletes the given favorite shop entry
* @param user_id The id of the user that wants to delete the favorite shop entry
* @param favorite_id The favorite shop to delete
*/
export const deleteFavoriteShop = async (user_id: number, favorite_id: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('DELETE FROM favorite_shops WHERE favorite_id = ? AND user_id = ?', [favorite_id, user_id]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
@@ -19,7 +19,7 @@ export const manufacturersRouter = express.Router();
* Controller Definitions
*/
// GET manufacturers/
// GET items/
manufacturersRouter.get('/', async (req: Request, res: Response) => {
try {
const manufacturers: Manufacturers = await ManufacturerService.findAll();
@@ -27,11 +27,11 @@ manufacturersRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(manufacturers);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
// GET manufacturers/:id
// GET items/:id
manufacturersRouter.get('/:id', async (req: Request, res: Response) => {
const id: number = parseInt(req.params.id, 10);
@@ -46,11 +46,11 @@ manufacturersRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(manufacturer);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
// GET manufacturers/:term
// GET items/:term
manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term;
@@ -65,6 +65,6 @@ manufacturersRouter.get('/search/:term', async (req: Request, res: Response) =>
res.status(200).send(manufacturer);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -1,6 +0,0 @@
export interface PriceAlarm {
alarm_id: number;
user_id: number;
product_id: number;
defined_price: number;
}
@@ -1,5 +0,0 @@
import {PriceAlarm} from './pricealarm.interface';
export interface PriceAlarms {
[key: number]: PriceAlarm;
}
@@ -1,134 +0,0 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as PriceAlarmsService from './pricealarms.service';
import {PriceAlarm} from './pricealarm.interface';
import {PriceAlarms} from './pricealarms.interface';
import * as UserService from '../users/users.service';
/**
* Router Definition
*/
export const pricealarmsRouter = express.Router();
/**
* Controller Definitions
*/
//GET pricealarms/
pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id);
res.status(200).send(priceAlarms);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST pricealarms/
pricealarmsRouter.post('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation
const product_id = req.body.product_id;
const defined_price = req.body.defined_price;
if (!product_id || !defined_price) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Create price alarm
const success = await PriceAlarmsService.createPriceAlarm(user.user_id, product_id, defined_price);
if (success) {
res.status(201).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT pricealarms/
pricealarmsRouter.put('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation
const alarm_id = req.body.alarm_id;
const defined_price = req.body.defined_price;
if (!alarm_id || !defined_price) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Update price alarm
const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_id, defined_price);
if (success) {
res.status(200).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// DELETE pricealarms/:id
pricealarmsRouter.delete('/:id', async (req, res) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
const id: number = parseInt(req.params.id, 10);
const success = await PriceAlarmsService.deletePriceAlarm(id, user.user_id);
if (success) {
res.status(200).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@@ -1,115 +0,0 @@
import * as dotenv from 'dotenv';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {PriceAlarm} from './pricealarm.interface';
import {PriceAlarms} from './pricealarms.interface';
/**
* Service Methods
*/
/**
* Creates a price alarm for the given user for the product with the defined price
* @param user_id The id of the user to create the price alarm for
* @param product_id The id of the product to create the price alarm for
* @param defined_price The defined price for the price alarm
*/
export const createPriceAlarm = async (user_id: number, product_id: number, defined_price: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('INSERT INTO price_alarms (user_id, product_id, defined_price) VALUES (?, ?, ?)', [user_id, product_id, defined_price]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Fetches and returns all price alarms for the given user
* @param user_id
*/
export const getPriceAlarms = async (user_id: number): Promise<PriceAlarms> => {
let conn;
let priceAlarms = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT alarm_id, user_id, product_id, defined_price FROM price_alarms WHERE user_id = ?', user_id);
for (let row in rows) {
if (row !== 'meta') {
priceAlarms.push(rows[row]);
}
}
return priceAlarms;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Updates the given price alarm with the given fields
* @param alarm_id The id of the price alarm to update
* @param user_id The id of the user that wants to update the price alarm
* @param defined_price The defined price for the price alarm
*/
export const updatePriceAlarm = async (alarm_id: number, user_id: number, defined_price: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('UPDATE price_alarms SET defined_price = ? WHERE alarm_id = ? AND user_id = ?', [defined_price, alarm_id, user_id]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Deletes the given price alarm
* @param alarm_id The id of the price alarm to update
* @param user_id The id of the user that wants to update the price alarm
*/
export const deletePriceAlarm = async (alarm_id: number, user_id: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('DELETE FROM price_alarms WHERE alarm_id = ? AND user_id = ?', [alarm_id, user_id]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
+3 -20
View File
@@ -4,24 +4,7 @@ export interface Price {
vendor_id: number;
price_in_cents: number;
timestamp: Date;
}
export class Deal implements Price {
price_id: number;
product_id: number;
vendor_id: number;
price_in_cents: number;
timestamp: Date;
amazonDifference: number;
amazonDifferencePercent: number;
constructor(price_id: number, product_id: number, vendor_id: number, price_in_cents: number, timestamp: Date, amazonDifference: number, amazonDifferencePercent: number) {
this.price_id = price_id;
this.product_id = product_id;
this.vendor_id = vendor_id;
this.price_in_cents = price_in_cents;
this.timestamp = timestamp;
this.amazonDifference = amazonDifference;
this.amazonDifferencePercent = amazonDifferencePercent;
}
// Only for deals
amazonDifference?: number;
amazonDifferencePercent?: number;
}
+4 -32
View File
@@ -6,7 +6,6 @@ import express, {Request, Response} from 'express';
import * as PriceService from './prices.service';
import {Price} from './price.interface';
import {Prices} from './prices.interface';
import * as UserService from '../users/users.service';
/**
@@ -41,7 +40,7 @@ pricesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -60,7 +59,7 @@ pricesRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(price);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -79,7 +78,7 @@ pricesRouter.get('/bestDeals/:amount', async (req: Request, res: Response) => {
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -98,33 +97,6 @@ pricesRouter.get('/byProduct/list/:ids', async (req: Request, res: Response) =>
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST prices/
pricesRouter.post('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const vendor_id = req.body.vendor_id;
const product_id = req.body.product_id;
const price_in_cents = req.body.price_in_cents;
const success = await PriceService.createPriceEntry(user.user_id, vendor_id, product_id, price_in_cents);
if (success) {
res.status(201).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
+22 -45
View File
@@ -15,7 +15,7 @@ const pool = mariadb.createPool({
* Data Model Interfaces
*/
import {Deal, Price} from './price.interface';
import {Price} from './price.interface';
import {Prices} from './prices.interface';
@@ -31,7 +31,7 @@ export const findAll = async (): Promise<Prices> => {
let priceRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, v.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE active_listing = true AND v.isActive = true');
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices');
for (let row in rows) {
if (row !== 'meta') {
let price: Price = {
@@ -72,7 +72,7 @@ export const find = async (id: number): Promise<Price> => {
let price: any;
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE price_id = ? AND active_listing = true AND v.isActive = true', id);
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE price_id = ?', id);
for (let row in rows) {
if (row !== 'meta') {
price = rows[row];
@@ -99,7 +99,7 @@ export const findByProduct = async (product: number): Promise<Prices> => {
let priceRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND active_listing = true AND v.isActive = true', product);
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ?', product);
for (let row in rows) {
if (row !== 'meta') {
priceRows.push(rows[row]);
@@ -142,17 +142,16 @@ export const findByType = async (product: string, type: string): Promise<Prices>
'PARTITION BY p.vendor_id ' +
'ORDER BY p.timestamp DESC) AS rk ' +
'FROM prices p ' +
'LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id ' +
'WHERE product_id = ? AND p.vendor_id != 1 AND active_listing = true AND v.isActive = true) ' +
'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, p.vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND v.vendor_id != 1 AND active_listing = true AND v.isActive = true 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, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id != 1 AND active_listing = true AND v.isActive = true', 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) {
@@ -189,13 +188,13 @@ export const findByVendor = async (product: string, vendor: string, type: string
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, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true ORDER BY timestamp DESC LIMIT 1', [product, 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, p.vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true LIMIT 1', [product, vendor]);
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, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true', [product, 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) {
@@ -238,7 +237,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE active_listing = true AND v.isActive = true)\n' +
' FROM prices p)\n' +
'SELECT s.*\n' +
'FROM summary s\n' +
'WHERE s.rk = 1');
@@ -255,7 +254,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
}
// Iterate over all prices to find the products with the biggest difference between amazon and other vendor
let deals: Deal[] = [];
let deals: Price[] = [];
Object.keys(allPrices).forEach(productId => {
if (allPrices[parseInt(productId)]) {
@@ -282,12 +281,12 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
'price_in_cents': lowestPrice.price_in_cents,
'timestamp': lowestPrice.timestamp,
'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents),
'amazonDifferencePercent': ((amazonPrice.price_in_cents / lowestPrice.price_in_cents) * 100),
'amazonDifferencePercent': ((1 - (lowestPrice.price_in_cents / amazonPrice.price_in_cents)) * 100),
};
// Push only deals were the amazon price is actually higher
if (deal.amazonDifferencePercent > 0 && deal.amazonDifference > 0) {
deals.push(deal as Deal);
if (deal.amazonDifferencePercent > 0) {
deals.push(deal as Price);
}
}
});
@@ -299,8 +298,10 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
let maxAmt = Math.min(amount, deals.length);
for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) {
//console.log(deals[dealIndex]);
priceRows.push(deals[dealIndex] as Price);
}
} catch (err) {
console.log(err);
throw err;
@@ -315,7 +316,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
/**
* Fetches and returns the lowest, latest, non-amazon price for each given product
* @param productIds the ids of the products
* @param ids the ids of the products
*/
export const findListByProducts = async (productIds: [number]): Promise<Prices> => {
let conn;
@@ -335,9 +336,9 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id ' +
' WHERE p.product_id IN (?) AND v.isActive = true' +
' AND p.vendor_id != 1 AND active_listing = true)\n' +
' FROM prices p' +
' WHERE p.product_id IN (?)' +
' AND p.vendor_id != 1)\n' +
'SELECT s.*\n' +
'FROM summary s\n' +
'WHERE s.rk = 1', [productIds]);
@@ -365,6 +366,7 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
priceRows.push(pricesForProd[0]);
}
});
} catch (err) {
throw err;
} finally {
@@ -375,28 +377,3 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
return priceRows;
};
export const createPriceEntry = async (user_id: number, vendor_id: number, product_id: number, price_in_cents: number): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
// Create price entry
const res = await conn.query('INSERT INTO prices (product_id, vendor_id, price_in_cents) VALUES (?,?,?)', [product_id, vendor_id, price_in_cents]);
// If there are more / less than 1 affected rows, return false
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
+4 -46
View File
@@ -27,7 +27,7 @@ productsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -46,7 +46,7 @@ productsRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(product);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -65,7 +65,7 @@ productsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -84,48 +84,6 @@ productsRouter.get('/list/:ids', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET products/vendor/:id
productsRouter.get('/vendor/: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 products: Products = await ProductService.findByVendor(id);
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST products/
productsRouter.post('/', async (req: Request, res: Response) => {
const asin: string = req.body.asin;
if (!asin) {
res.status(400).send('Missing parameters.');
return;
}
try {
const result: boolean = await ProductService.addNewProduct(asin);
if (result) {
res.status(201).send({});
} else {
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -17,7 +17,6 @@ const pool = mariadb.createPool({
import {Product} from './product.interface';
import {Products} from './products.interface';
import * as http from 'http';
/**
@@ -160,70 +159,3 @@ export const findList = async (ids: [number]): Promise<Products> => {
return prodRows;
};
/**
* Fetches and returns the products that the given vendor has price entries for
* @param id The id of the vendor to fetch the products for
*/
export const findByVendor = async (id: number): Promise<Products> => {
let conn;
let prodRows = [];
try {
conn = await pool.getConnection();
// Get the relevant product ids
let relevant_prod_ids = [];
const relevantProds = await conn.query('SELECT product_id FROM prices WHERE vendor_id = ? GROUP BY product_id', id);
for (let row in relevantProds) {
if (row !== 'meta') {
relevant_prod_ids.push(relevantProds[row].product_id);
}
}
// Fetch products
const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products WHERE product_id IN (?)', [relevant_prod_ids]);
for (let row in rows) {
if (row !== 'meta') {
prodRows.push(rows[row]);
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return prodRows;
};
/**
* Makes a callout to a crawler instance to search for the requested product
* @param asin The amazon asin of the product to look for
*/
export const addNewProduct = async (asin: string): Promise<boolean> => {
try {
let options = {
host: 'crawl.p4ddy.com',
path: '/searchNew',
port: '443',
method: 'POST'
};
let req = http.request(options, res => {
return res.statusCode === 202;
});
req.write(JSON.stringify({
asin: asin,
key: process.env.CRAWLER_ACCESS_KEY
}));
req.end();
} catch (err) {
console.log(err);
throw(err);
}
return false;
};
@@ -5,5 +5,4 @@ export interface User {
password_hash: string;
registration_date: Date;
last_login_date: Date;
is_admin: boolean;
}
+16 -22
View File
@@ -47,13 +47,10 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user
res.status(201).send({
session_id: session.session_id,
session_key: session.session_key
});
res.status(201).send(session);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
@@ -73,49 +70,46 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
// Update the user entry and create a session
const session: Session = await UserService.login(username, password, ip);
if (!session.session_id) {
if(!session.session_id) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ['Wrong username and / or password'], codes: [1, 4]}));
res.status(401).send(JSON.stringify({messages: ["Wrong username and / or password"], codes: [1, 4]}));
return;
}
// Send the session details back to the user
res.status(200).send({
session_id: session.session_id,
session_key: session.session_key
});
res.status(201).send(session);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
// POST users/checkSessionValid
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
try {
const sessionId: string = req.body.sessionId;
const sessionKey: string = req.body.sessionKey;
const ip: string = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
if(!session_id || !session_key) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ['No session detected'], codes: [5]}));
if (!sessionId || !sessionKey) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Update the user entry and create a session
const user: User = await UserService.checkSession(session_id, session_key, ip);
const user: User = await UserService.checkSession(sessionId, sessionKey, ip);
if (!user.user_id) {
if(!user.user_id) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ['Invalid session'], codes: [5]}));
res.status(401).send(JSON.stringify({messages: ["Invalid session"], codes: [5]}));
return;
}
// Send the session details back to the user
res.status(200).send(user);
res.status(201).send(user);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
+11 -24
View File
@@ -68,7 +68,7 @@ export const createUser = async (username: string, password: string, email: stri
return {
session_id: sessionId,
session_key: sessionKey,
session_key_hash: 'HIDDEN',
session_key_hash: '',
last_IP: ip
};
@@ -115,8 +115,8 @@ export const login = async (username: string, password: string, ip: string): Pro
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
// Update user entry in SQL
const userQuery = 'UPDATE users SET last_login_date = NOW() WHERE user_id = ?';
const userIdRes = await conn.query(userQuery, userId);
const userQuery = 'UPDATE users SET last_login_date = NOW()';
const userIdRes = await conn.query(userQuery);
await conn.commit();
// Create session
@@ -135,7 +135,7 @@ export const login = async (username: string, password: string, ip: string): Pro
return {
session_id: sessionId,
session_key: sessionKey,
session_key_hash: 'HIDDEN',
session_key_hash: '',
last_IP: ip
};
@@ -179,7 +179,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
// Key is valid, continue
// Check if the session is still valid
if (validUntil <= new Date()) {
if(validUntil <= new Date()) {
// Session expired, return invalid
return {} as User;
}
@@ -193,20 +193,18 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
await conn.commit();
// Get the other required user information and update the user
const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date, is_admin FROM users WHERE user_id = ?';
const userQuery = "SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?";
const userRows = await conn.query(userQuery, userId);
let username = '';
let email = '';
let registrationDate = new Date();
let lastLoginDate = new Date();
let is_admin = false;
for (const row in userRows) {
if (row !== 'meta' && userRows[row].user_id != null) {
username = userRows[row].username;
email = userRows[row].email;
registrationDate = userRows[row].registration_date;
lastLoginDate = userRows[row].last_login_date;
is_admin = userRows[row].is_admin;
}
}
@@ -215,10 +213,9 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
user_id: userId,
username: username,
email: email,
password_hash: 'HIDDEN',
password_hash: '',
registration_date: registrationDate,
last_login_date: lastLoginDate,
is_admin: is_admin
last_login_date: lastLoginDate
};
} catch (err) {
@@ -228,20 +225,8 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
conn.end();
}
}
};
/**
* Calls the checkSession method after extracting the required information from the authentication cookie
* @param cookie The betterauth cookie
* @param ip The users IP address
*/
export const checkSessionWithCookie = async (cookie: any, ip: string): Promise<User> => {
const parsedCookie = JSON.parse(cookie);
const session_id = parsedCookie.id;
const session_key = parsedCookie.key;
return checkSession(session_id, session_key, '');
return {} as User;
};
/**
@@ -313,4 +298,6 @@ export const checkUsernameAndEmail = async (username: string, email: string): Pr
conn.end();
}
}
return {hasProblems: true, messages: ['Internal server error'], codes: [3]};
};
+6 -101
View File
@@ -6,7 +6,6 @@ import express, {Request, Response} from 'express';
import * as VendorService from './vendors.service';
import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface';
import * as UserService from '../users/users.service';
/**
@@ -20,7 +19,7 @@ export const vendorsRouter = express.Router();
* Controller Definitions
*/
// GET vendors/
// GET items/
vendorsRouter.get('/', async (req: Request, res: Response) => {
try {
const vendors: Vendors = await VendorService.findAll();
@@ -28,29 +27,11 @@ vendorsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
// GET vendors/managed
vendorsRouter.get('/managed', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
const vendors = await VendorService.getManagedShops(user.user_id);
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET vendors/:id
// GET items/:id
vendorsRouter.get('/:id', async (req: Request, res: Response) => {
const id: number = parseInt(req.params.id, 10);
@@ -65,11 +46,11 @@ vendorsRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(vendor);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
// GET vendors/search/:term
// GET items/:name
vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term;
@@ -84,82 +65,6 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT vendors/manage/deactivatelisting
vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const vendor_id = req.body.vendor_id;
const product_id = req.body.product_id;
const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id);
if (success) {
res.status(200).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT vendors/manage/shop/deactivate/:id
vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const vendor_id = parseInt(req.params.id, 10);
const success = await VendorService.setShopStatus(user.user_id, vendor_id, false);
if (success) {
res.status(200).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT vendors/manage/shop/activate/:id
vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get required parameters
const vendor_id = parseInt(req.params.id, 10);
const success = await VendorService.setShopStatus(user.user_id, vendor_id, true);
if (success) {
res.status(200).send({});
} else {
res.status(500).send({});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
}
});
+3 -94
View File
@@ -17,7 +17,6 @@ const pool = mariadb.createPool({
import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface';
import {User} from '../users/user.interface';
/**
@@ -32,7 +31,7 @@ export const findAll = async (): Promise<Vendors> => {
let vendorRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE isActive = true');
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors');
for (let row in rows) {
if (row !== 'meta') {
let vendor: Vendor = {
@@ -79,7 +78,7 @@ export const find = async (id: number): Promise<Vendor> => {
let vendor: any;
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE vendor_id = ? AND isActive = true', id);
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE vendor_id = ?', id);
for (let row in rows) {
if (row !== 'meta') {
vendor = rows[row];
@@ -107,7 +106,7 @@ export const findBySearchTerm = async (term: string): Promise<Vendors> => {
try {
conn = await pool.getConnection();
term = '%' + term + '%';
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE name LIKE ? AND isActive = true', term);
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE name LIKE ?', term);
for (let row in rows) {
if (row !== 'meta') {
vendorRows.push(rows[row]);
@@ -124,93 +123,3 @@ export const findBySearchTerm = async (term: string): Promise<Vendors> => {
return vendorRows;
};
/**
* Get all vendors that have the given user as admin
* @param user The user to return the managed shops for
*/
export const getManagedShops = async (user_id: number): Promise<Vendors> => {
let conn;
let vendorRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE admin_id LIKE ?', user_id);
for (let row in rows) {
if (row !== 'meta') {
vendorRows.push(rows[row]);
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return vendorRows;
};
/**
* Deactivates a product listing for a specific vendor
* @param user_id The user id of the issuing user
* @param vendor_id The vendor id of the vendor to deactivate the listing for
* @param product_id The product id of the product to deactivate the listing for
*/
export const deactivateListing = async (user_id: number, vendor_id: number, product_id: number): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
const status = await conn.query('UPDATE prices SET active_listing = false WHERE vendor_id = ? and product_id = ?', [vendor_id, product_id]);
return status.affectedRows > 0;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return false;
};
/**
* Set the specified shop to either active or not active
* @param user_id The user id of the issuing user
* @param vendor_id The vendor id of the shop to update
* @param isActive The new active state
*/
export const setShopStatus = async (user_id: number, vendor_id: number, isActive: boolean): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
// Update the vendor state
const status = await conn.query('UPDATE vendors SET isActive = ? WHERE vendor_id = ?', [isActive, vendor_id]);
return status.affectedRows > 0;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return false;
};
+11 -11
View File
@@ -1,32 +1,32 @@
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const webpack = require("webpack");
const path = require("path");
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: ['webpack/hot/poll?100', './src/index.ts'],
entry: ["webpack/hot/poll?100", "./src/index.ts"],
watch: false,
target: 'node',
target: "node",
externals: [
nodeExternals({
whitelist: ['webpack/hot/poll?100']
whitelist: ["webpack/hot/poll?100"]
})
],
module: {
rules: [
{
test: /.tsx?$/,
use: 'ts-loader',
use: "ts-loader",
exclude: /node_modules/
}
]
},
mode: 'development',
mode: "development",
resolve: {
extensions: ['.tsx', '.ts', '.js']
extensions: [".tsx", ".ts", ".js"]
},
plugins: [new webpack.HotModuleReplacementPlugin()],
output: {
path: path.join(__dirname, 'dist'),
filename: 'index.js'
path: path.join(__dirname, "dist"),
filename: "index.js"
}
};
+2 -1
View File
@@ -2,12 +2,13 @@
<module type="WEB_MODULE" version="4">
<component name="FacetManager">
<facet type="Python" name="Python">
<configuration sdkName="Python 3.9 (venv)" />
<configuration sdkName="Python 3.9" />
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
</component>
</module>
+2 -2
View File
@@ -1,10 +1,10 @@
# Base image
FROM python:3.9.5-buster
FROM python
# Create directories and copy files
RUN echo 'Creating directory and copying files'
RUN mkdir /crawler
COPY . /crawler
ADD . /crawler
WORKDIR /crawler
# Install dependencies
+3 -12
View File
@@ -1,17 +1,13 @@
import os
from flask import Flask
from flask_restful import Resource, Api, reqparse
import crawler
app = Flask(__name__)
api = Api(app)
# To parse request data
parser = reqparse.RequestParser()
parser.add_argument('key', type=str)
parser.add_argument('products', type=int, action='append')
parser.add_argument('key')
parser.add_argument('products')
class CrawlerApi(Resource):
@@ -21,12 +17,7 @@ class CrawlerApi(Resource):
def post(self):
# Accept crawler request here
args = parser.parse_args()
access_key = os.getenv('CRAWLER_ACCESS_KEY')
if(args['key'] == access_key):
crawler.crawl(args['products'])
return {'message': 'success'}
else:
return {'message': 'Wrong access key'}
return args
api.add_resource(CrawlerApi, '/')
+5 -34
View File
@@ -1,10 +1,4 @@
import sql
import requests
from bs4 import BeautifulSoup
HEADERS = ({'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 '
'Safari/537.36'})
def crawl(product_ids: [int]) -> dict:
@@ -34,19 +28,13 @@ def crawl(product_ids: [int]) -> dict:
# Call the appropriate vendor crawling function and append the result to the list of crawled data
if product_vendor_info['vendor_id'] == 1:
# Amazon
data = __crawl_amazon__(product_vendor_info)
if data:
crawled_data.append(data)
crawled_data.append(__crawl_amazon__(product_vendor_info))
elif product_vendor_info['vendor_id'] == 2:
# Apple
data = __crawl_apple__(product_vendor_info)
if data:
crawled_data.append(data)
crawled_data.append(__crawl_apple__(product_vendor_info))
elif product_vendor_info['vendor_id'] == 3:
# Media Markt
data = __crawl_mediamarkt__(product_vendor_info)
if data:
crawled_data.append(data)
crawled_data.append(__crawl_mediamarkt__(product_vendor_info))
else:
products_with_problems.append(product_vendor_info)
continue
@@ -69,23 +57,7 @@ def __crawl_amazon__(product_info: dict) -> tuple:
:param product_info: A dict with product info containing product_id, vendor_id, url
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents)
"""
page = requests.get(product_info['url'], headers=HEADERS)
soup = BeautifulSoup(page.content, features="lxml")
try:
price = int(
soup.find(id='priceblock_ourprice').get_text().replace(".", "").replace(",", "").replace("", "").strip())
if not price:
price = int(soup.find(id='price_inside_buybox').get_text().replace(".", "").replace(",", "").replace("", "").strip())
except RuntimeError:
price = -1
except AttributeError:
price = -1
if price != -1:
return (product_info['product_id'], product_info['vendor_id'], price)
else:
return None
return (product_info['product_id'], product_info['vendor_id'], 123)
def __crawl_apple__(product_info: dict) -> tuple:
@@ -94,8 +66,7 @@ def __crawl_apple__(product_info: dict) -> tuple:
:param product_info: A dict with product info containing product_id, vendor_id, url
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents)
"""
# return (product_info['product_id'], product_info['vendor_id'], 123)
pass
return (product_info['product_id'], product_info['vendor_id'], 123)
def __crawl_mediamarkt__(product_info: dict) -> tuple:
+66
View File
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
import scrapy
from urllib.parse import urlencode
from urllib.parse import urljoin
import re
import json
queries = ['iphone']
API = ''
def get_url(url):
payload = {'api_key': API, 'url': url, 'country_code': 'us'}
proxy_url = 'http://api.scraperapi.com/?' + urlencode(payload)
return proxy_url
class AmazonSpider(scrapy.Spider):
name = 'amazon'
def start_requests(self):
for query in queries:
url = 'https://www.amazon.de/s?' + urlencode({'k': query})
yield scrapy.Request(url=url, callback=self.parse_keyword_response)
def parse_keyword_response(self, response):
products = response.xpath('//*[@data-asin]')
for product in products:
asin = product.xpath('@data-asin').extract_first()
product_url = f"https://www.amazon.de/dp/{asin}"
yield scrapy.Request(url=product_url, callback=self.parse_product_page, meta={'asin': asin})
next_page = response.xpath('//li[@class="a-last"]/a/@href').extract_first()
if next_page:
url = urljoin("https://www.amazon.de", next_page)
yield scrapy.Request(url=url, callback=self.parse_keyword_response)
def parse_product_page(self, response):
asin = response.meta['asin']
title = response.xpath('//*[@id="productTitle"]/text()').extract_first()
image = re.search('"large":"(.*?)"', response.text).groups()[0]
rating = response.xpath('//*[@id="acrPopover"]/@title').extract_first()
number_of_reviews = response.xpath('//*[@id="acrCustomerReviewText"]/text()').extract_first()
price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
if not price:
price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \
response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first()
temp = response.xpath('//*[@id="twister"]')
sizes = []
colors = []
if temp:
s = re.search('"variationValues" : ({.*})', response.text).groups()[0]
json_acceptable = s.replace("'", "\"")
di = json.loads(json_acceptable)
sizes = di.get('size_name', [])
colors = di.get('color_name', [])
bullet_points = response.xpath('//*[@id="feature-bullets"]//li/span/text()').extract()
seller_rank = response.xpath(
'//*[text()="Amazon Best Sellers Rank:"]/parent::*//text()[not(parent::style)]').extract()
yield {'asin': asin, 'Title': title, 'MainImage': image, 'Rating': rating, 'NumberOfReviews': number_of_reviews,
'Price': price, 'AvailableSizes': sizes, 'AvailableColors': colors, 'BulletPoints': bullet_points,
'SellerRank': seller_rank}
+2 -4
View File
@@ -1,7 +1,5 @@
pymysql
flask==1.1.2
flask
flask-sqlalchemy
flask_restful
beautifulsoup4
requests
lxml
scrapy
+1
View File
@@ -54,6 +54,7 @@ def getProductLinksForProduct(product_id: int) -> [dict]:
cur = conn.cursor()
query = 'SELECT vendor_id, url FROM product_links WHERE product_id = %s'
cur.execute(query, (product_id,))
products = list(map(lambda x: {'product_id': product_id, 'vendor_id': x[0], 'url': x[1]}, cur.fetchall()))
-33
View File
@@ -1,33 +0,0 @@
import scrapy
from scrapy.crawler import CrawlerProcess
import re
class AmazonSpider(scrapy.Spider):
name = 'amazon'
allowed_domains = ['amazon.de']
start_urls = ['https://amazon.de/dp/B083DRCPJG']
# def __init__(self, start_urls):
# self.start_urls = start_urls
def parse(self, response):
price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
if not price:
price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \
response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first()
euros = re.match('(\d*),\d\d', price).group(1)
cents = re.match('\d*,(\d\d)', price).group(1)
priceincents = euros + cents
yield {'price': priceincents}
def start_crawling():
process = CrawlerProcess(
settings={'COOKIES_ENABLED': 'False', 'CONCURRENT_REQUESTS_PER_IP': 1, 'ROBOTSTXT_OBEY': False,
'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'DOWNLOAD_DELAY': 3}
, install_root_handler=False)
process.crawl()
process.start()
-25
View File
@@ -1,25 +0,0 @@
import scrapy
import re
class AmazonSpider(scrapy.Spider):
name = 'amazon'
allowed_domains = ['amazon.de']
start_urls = ['https://amazon.de/dp/B083DRCPJG']
def parse(self, response):
price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
if not price:
price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \
response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first()
euros = re.match('(\d*),\d\d', price).group(1)
cents = re.match('\d*,(\d\d)', price).group(1)
priceincents = euros + cents
yield {'price': priceincents}
+8 -35
View File
@@ -10,24 +10,17 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="jdk" jdkName="openjdk-16" jdkType="JavaSDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-java:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-core:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: info.cukes:cucumber-html:0.2.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-jvm-deps:1.0.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:gherkin:5.0.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:tag-expressions:1.1.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-junit:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-java:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-core:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin-messages:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:messages:15.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:tag-expressions:3.0.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-expressions:10.3.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:datatable:3.5.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-plugin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:docstring:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:html-formatter:13.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:create-meta:4.0.0" level="project" />
<orderEntry type="library" name="Maven: org.apiguardian:apiguardian-api:1.1.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-junit:6.10.3" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven.plugins:maven-compiler-plugin:3.8.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-plugin-api:3.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-model:3.0" level="project" />
@@ -60,25 +53,5 @@
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-api:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-manager:2.8.4" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.codehaus.plexus:plexus-compiler-javac:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-java:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-api:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-chrome-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-edge-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-firefox-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-ie-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-opera-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-remote-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-safari-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-support:3.141.59" level="project" />
<orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.8.15" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-exec:1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:25.0-jre" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: org.checkerframework:checker-compat-qual:2.0.0" level="project" />
<orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.1" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.mojo:animal-sniffer-annotations:1.14" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okhttp3:okhttp:3.11.0" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okio:okio:1.14.0" level="project" />
</component>
</module>
+6 -15
View File
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>xyz.betterzon</groupId>
<groupId>de.taskhub</groupId>
<artifactId>CucumberTests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
@@ -13,30 +13,21 @@
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.10.3</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>6.10.3</version>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
</dependencies>
</project>
+3 -18
View File
@@ -1,27 +1,12 @@
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;
import org.openqa.selenium.firefox.FirefoxDriver;
import stepdefs.Preconditions;
@RunWith(Cucumber.class)
@CucumberOptions(
features = {"src/test/resource/searchProduct.feature",
"src/test/resource/priceAlarms.feature",
"src/test/resource/favoriteShopList.feature",
"src/test/resource/manageVendor.feature"}
"src/test/resource/priceAlarms.feature"}
)
public class RunTest {
@BeforeClass
public static void setup() {
Preconditions.driver = new FirefoxDriver();
}
@AfterClass
public static void teardown() {
Preconditions.driver.close();
}
}
@@ -1,34 +0,0 @@
package stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class FavoriteShopList {
@Given("^the user has at least (\\d+) favorite shop$")
public void the_user_has_at_least_favorite_shop(int arg1) throws Exception {
}
@Then("^the profile page should open$")
public void the_profile_page_should_open() throws Exception {
WebElement profile_info_text = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-hover")));
assert(profile_info_text.isDisplayed());
}
@Then("^he should see his favorite shops list$")
public void he_should_see_his_favorite_shops_list() throws Exception {
}
@When("^he clicks on delete a favorite shop entry$")
public void he_clicks_on_delete_a_favorite_shop_entry() throws Exception {
}
@Then("^the favorite shop entry should be deleted$")
public void the_favorite_shop_entry_should_be_deleted() throws Exception {
}
}
@@ -1,31 +0,0 @@
package stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
public class ManageVendor {
@Given("^the user is logged in as vendor manager$")
public void the_user_is_logged_in_as_vendor_manager() throws Exception {
}
@When("^the user opens the shop managing page$")
public void the_user_opens_the_shop_managing_page() throws Exception {
}
@When("^the user clicks on deactivate a listing$")
public void the_user_clicks_on_deactivate_a_listing() throws Exception {
}
@Then("^the listing should be deactivated$")
public void the_listing_should_be_deactivated() throws Exception {
}
@When("^the user clicks on deactivate the shop$")
public void the_user_clicks_on_deactivate_the_shop() throws Exception {
}
@Then("^the shop and all related listings should be deactivated$")
public void the_shop_and_all_related_listings_should_be_deactivated() throws Exception {
}
}
@@ -1,8 +0,0 @@
package stepdefs;
import org.openqa.selenium.WebDriver;
public class Preconditions {
public static WebDriver driver;
public static final int delaySeconds = 7;
}
@@ -1,15 +1,8 @@
package stepdefs;
import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.util.List;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
public class PriceAlarm {
@Given("^the user has at least (\\d+) price alarm set$")
@@ -18,43 +11,42 @@ public class PriceAlarm {
@When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception {
WebElement profileButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'profile')]")));
profileButton.click();
}
@Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception {
}
@When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception {
}
@Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception {
}
@Then("^the price alarm list should contain at least (\\d+) entry$")
public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception {
WebElement alarmEntry = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-hover tr:nth-child(2)")));
assert (alarmEntry != null);
}
@Given("^the user is on the profile page$")
public void the_user_is_on_the_profile_page() throws Exception {
Preconditions.driver.get("https://www.betterzon.xyz/profile");
@Then("^the price alarm list should contain a maximum of (\\d+) entries per page$")
public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception {
}
WebElement profile_info_text = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-user-information")));
assert (profile_info_text.isDisplayed());
@Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception {
}
@When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$")
public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception {
if (arg1.equals("remove")) {
WebElement entry = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-hover tr:nth-child(2)")));
if (entry == null) {
throw new Exception("Too few price alarm entries found!");
}
WebElement btn = entry.findElement(By.cssSelector("img.delete[src='../assets/images/Delete_icon-icons.com_55931.png']"));
btn.click();
} else if (arg1.equals("edit")) {
@Then("^a popup should open asking the user to confirm the removal$")
public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception {
}
@When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
}
@Then("^the price alarm should be removed from the database$")
@@ -1,120 +1,52 @@
package stepdefs;
import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import cucumber.api.PendingException;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
public class SearchProduct {
@Given("^the user is on the landing page$")
public void the_user_is_on_the_landing_page() throws Exception {
//throw new PendingException();
Preconditions.driver.get("https://betterzon.xyz");
WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("a.navbar-brand")));
}
@When("^the user enters the search term \"([^\"]*)\" and clicks search$")
public void the_user_enters_the_search_term_and_clicks_search(String searchTerm) throws Exception {
WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid"));
searchField.sendKeys(searchTerm);
searchField.sendKeys(Keys.ENTER);
WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".navbar-brand")));
public void the_user_enters_the_search_term_and_clicks_search(String arg0) throws Exception {
}
@Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception {
WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'No Products found!')]")));
assert (noProdsFoundMsg.isDisplayed());
}
@Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception {
try {
WebElement logoutButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'log out')]")));
logoutButton.click();
} catch (TimeoutException e) {
}
}
@Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception {
try {
WebElement loginButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'sign in')]")));
loginButton.click();
WebElement usernameField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.id("username")));
usernameField.sendKeys("Selenium");
WebElement passwordField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.id("password")));
passwordField.sendKeys("Selenium");
WebElement loginBtn = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.className("btn_signin")));
loginBtn.click();
} catch (TimeoutException e) {
}
}
@Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception {
WebElement product = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".row.p-2.bg-white.border.rounded")));
assert (product.isDisplayed());
}
@When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception {
WebElement productDetailsBtn = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".row.p-2.bg-white.border.rounded button.btn.btn-primary.btn-sm")));
productDetailsBtn.click();
}
@Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception {
WebElement productTitle = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.productTitle")));
assert (productTitle.isDisplayed());
}
@Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
WebElement alarmBox = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm")));
if (arg0.equals("Login to set a price alarm")) {
assert (alarmBox.getText().equals("Login to set a price alarm"));
} else {
assert (alarmBox.isDisplayed());
}
}
@When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception {
WebElement alarmBoxField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm input")));
alarmBoxField.sendKeys("12345");
WebElement alarmBox = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm")));
alarmBox.click();
assert (alarmBox.isDisplayed() && alarmBoxField.isDisplayed());
}
@Then("^the user should receive an email confirming the price alarm$")
public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
assert (true);
}
}
@@ -1,18 +0,0 @@
Feature: Favorite Shop List
Scenario: Access Favorite Shop List
Given the user is on the landing page
And the user is logged in
And the user has at least 1 favorite shop
When the user clicks on the profile icon
Then the profile page should open
Then he should see his favorite shops list
Scenario: Remove Favorite Shop Entry
Given the user is on the landing page
And the user is logged in
And the user has at least 1 favorite shop
When the user clicks on the profile icon
Then the profile page should open
When he clicks on delete a favorite shop entry
Then the favorite shop entry should be deleted
@@ -1,15 +0,0 @@
Feature: Manage Vendor Shop
Scenario: Deactivate Product Listing
Given the user is on the landing page
And the user is logged in as vendor manager
When the user opens the shop managing page
And the user clicks on deactivate a listing
Then the listing should be deactivated
Scenario: Deactivate Shop Completely
Given the user is on the landing page
And the user is logged in as vendor manager
When the user opens the shop managing page
And the user clicks on deactivate the shop
Then the shop and all related listings should be deactivated
@@ -5,22 +5,23 @@ Feature: Price Alarms
And the user is logged in
And the user has at least 1 price alarm set
When the user clicks on the profile icon
Then the profile page should open
Then the profile details popup should open
When the user clicks on price alarms
Then the price alarm list should open
And the price alarm list should contain at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page
Scenario: Remove a price alarm
Given the user is on the landing page
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the profile icon
Then the profile page should open
When the user clicks on the "remove" button next to a price alarm
Then a popup should open asking the user to confirm the removal
When the user confirms the removal of the price alarm
Then the price alarm should be removed from the database
Scenario: Edit a price alarm
Given the user is on the landing page
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the profile icon
Then the profile page should open
When the user clicks on the "edit" button next to a price alarm
Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button
@@ -12,7 +12,7 @@ Feature: Search a Product
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "Login to set a price alarm"
And the set price alarm box should show "Log in to continue"
Scenario: User is logged in, searches for known product
Given the user is on the landing page
+1 -2
View File
@@ -92,8 +92,7 @@
"karmaConfig": "karma.conf.js",
"codeCoverage": true,
"codeCoverageExclude": [
"src/app/mocks/mock.service.ts",
"src/app/services/api.service.ts"
"src/app/mocks/mock.service.ts"
],
"assets": [
"src/favicon.ico",
+1 -4
View File
@@ -14,10 +14,7 @@ module.exports = function (config) {
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
random: false
}
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/Betterzon'),
+5239 -6533
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -7,8 +7,7 @@
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"postinstall": "ngcc"
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
-9
View File
@@ -1,10 +1 @@
.wrapper_app {
padding-bottom: 2.5rem; /* Footer height */
}
.footer_app {
position: relative;
bottom: 0;
width: 100%;
height: 2.5rem; /* Footer height */
}
+3 -4
View File
@@ -1,8 +1,7 @@
<app-top-bar></app-top-bar>
<router-outlet></router-outlet>
<app-bottom-bar></app-bottom-bar>
+3 -3
View File
@@ -1,8 +1,8 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
import {RouterTestingModule} from '@angular/router/testing';
import {NgcCookieConsentConfig, NgcCookieConsentModule} from 'ngx-cookieconsent';
import {FormsModule} from '@angular/forms';
import {RouterTestingModule} from "@angular/router/testing";
import {NgcCookieConsentConfig, NgcCookieConsentModule} from "ngx-cookieconsent";
import {FormsModule} from "@angular/forms";
// For cookie consent module testing
const cookieConfig: NgcCookieConsentConfig = {
-7
View File
@@ -1,8 +1,6 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {NgcCookieConsentService, NgcInitializeEvent, NgcNoCookieLawEvent, NgcStatusChangeEvent} from 'ngx-cookieconsent';
import {Subscription} from 'rxjs';
import {ApiService} from './services/api.service';
@Component({
selector: 'app-root',
@@ -21,17 +19,12 @@ export class AppComponent implements OnInit, OnDestroy {
private revokeChoiceSubscription: Subscription;
private noCookieLawSubscription: Subscription;
isLoggedIn = false;
showUserBoard = false;
username?: string;
constructor(
private ccService: NgcCookieConsentService
) {
}
ngOnInit(): void {
// subscribe to cookieconsent observables to react to main events
this.popupOpenSubscription = this.ccService.popupOpen$.subscribe(
() => {
+5 -27
View File
@@ -13,7 +13,7 @@ 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, ReactiveFormsModule} from '@angular/forms';
import {FormsModule} from '@angular/forms';
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
import {MatMenuModule} from '@angular/material/menu';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@@ -23,23 +23,13 @@ import {NgcCookieConsentModule, NgcCookieConsentConfig} from 'ngx-cookieconsent'
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {TopBarComponent} from './components/top-bar/top-bar.component';
import {RouterModule} from '@angular/router';
import {MatButtonModule} from '@angular/material/button';
import {MatButtonModule} from "@angular/material/button";
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatListModule} from '@angular/material/list';
import {MatListModule} from "@angular/material/list";
import {BottomBarComponent} from './components/bottom-bar/bottom-bar.component';
import {HotDealsWidgetComponent} from './components/hot-deals-widget/hot-deals-widget.component';
import {SliderForProductsComponent} from './components/slider-for-products/slider-for-products.component';
import {RegistrationComponent} from './components/auth/registration/registration.component';
import {MatCardModule} from '@angular/material/card';
import {SigninComponent} from './components/auth/signin/signin.component';
import {CopyrightComponent} from './components/copyright/copyright.component';
import {GreetingInfoSliderComponent} from './components/greeting-info-slider/greeting-info-slider.component';
import {KundenComponent} from './components/kunden/kunden.component';
import {AboutUsComponent} from './components/about-us/about-us.component';
import {ProfileComponent} from './pages/profile/profile.component';
import {ProfilePageComponent} from './pages/profile-page/profile-page.component';
// For cookie popup
const cookieConfig: NgcCookieConsentConfig = {
@@ -95,17 +85,7 @@ const cookieConfig: NgcCookieConsentConfig = {
ImprintComponent,
PrivacyComponent,
TopBarComponent,
BottomBarComponent,
HotDealsWidgetComponent,
SliderForProductsComponent,
RegistrationComponent,
SigninComponent,
CopyrightComponent,
GreetingInfoSliderComponent,
KundenComponent,
AboutUsComponent,
ProfileComponent,
ProfilePageComponent,
BottomBarComponent
],
imports: [
BrowserModule,
@@ -126,8 +106,6 @@ const cookieConfig: NgcCookieConsentConfig = {
RouterModule.forRoot([
{path: '', component: LandingpageComponent},
]),
MatCardModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent]
-8
View File
@@ -9,10 +9,6 @@ import {ProductSearchPageComponent} from './pages/product-search-page/product-se
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
import {ImprintComponent} from './pages/imprint/imprint.component';
import {PrivacyComponent} from './pages/privacy/privacy.component';
import {SigninComponent} from './components/auth/signin/signin.component';
import {RegistrationComponent} from './components/auth/registration/registration.component';
import {ProfileComponent} from './pages/profile/profile.component';
import {ProfilePageComponent} from './pages/profile-page/profile-page.component';
const routes: Routes = [
{path: '', component: LandingpageComponent, pathMatch: 'full'},
@@ -20,10 +16,6 @@ const routes: Routes = [
{path: 'product/:id', component: ProductDetailPageComponent},
{path: 'impressum', component: ImprintComponent},
{path: 'datenschutz', component: PrivacyComponent},
{path: 'signin', component: SigninComponent},
{path: 'registration', component: RegistrationComponent},
{path: 'product-detail', component: ProductDetailPageComponent},
{path: 'profile', component: ProfilePageComponent},
{path: '**', component: PageNotFoundPageComponent}
];
@@ -1,19 +0,0 @@
<section class="page-section bg-primary text-white mb-0" id="about">
<div class="container">
<!-- About Section Heading-->
<h2 class="page-section-heading text-center text-uppercase text-white">About</h2>
<!-- Icon Divider-->
<div class="divider-custom divider-light">
<div class="divider-custom-line"></div>
<div class="divider-custom-icon"><i class="fas fa-star"></i></div>
<div class="divider-custom-line"></div>
</div>
<!-- About Section Content-->
<div class="row">
<div class="col-lg-4 ms-auto"><p class="lead">You follow the same passion as we do and you want to find
alternatives to the de-facto monopolist Amazon?</p></div>
<div class="col-lg-4 me-auto"><p class="lead">In this case, welcome aboard! Were happy that you share our
passion and hope that we can help you achieving this goal with the website.</p></div>
</div>
</div>
</section>
@@ -1,25 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AboutUsComponent} from './about-us.component';
describe('AboutUsComponent', () => {
let component: AboutUsComponent;
let fixture: ComponentFixture<AboutUsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AboutUsComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AboutUsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,16 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-about-us',
templateUrl: './about-us.component.html',
styleUrls: ['./about-us.component.css']
})
export class AboutUsComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
@@ -1,27 +0,0 @@
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {RegistrationComponent} from './registration/registration.component';
import {SigninComponent} from './signin/signin.component';
import {ResetpasswortComponent} from './resetpasswort/resetpasswort.component';
const routes: Routes = [
{
path: 'registration',
component: RegistrationComponent
},
{
path: 'signin',
component: SigninComponent
},
{
path: 'resetpasswort',
component: ResetpasswortComponent
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule {
}
@@ -1,23 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AuthRoutingModule} from './auth-routing.module';
import {SigninComponent} from './signin/signin.component';
import {RegistrationComponent} from './registration/registration.component';
import {ResetpasswortComponent} from './resetpasswort/resetpasswort.component';
@NgModule({
declarations: [SigninComponent, RegistrationComponent, ResetpasswortComponent],
imports: [
CommonModule,
AuthRoutingModule,
],
exports: [
SigninComponent,
RegistrationComponent,
ResetpasswortComponent,
],
})
export class AuthModule {
}
@@ -1,93 +0,0 @@
.main-content {
width: 50%;
border-radius: 20px;
box-shadow: 0 5px 5px rgba(0, 0, 0, .4);
margin: 5em auto;
display: flex;
}
.company__info {
background-color: #008080;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
display: flex;
flex-direction: column;
justify-content: center;
color: #fff;
}
.fa-android {
font-size: 3em;
}
@media screen and (max-width: 640px) {
.main-content {
width: 90%;
}
.company__info {
display: none;
}
.login_form {
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
}
}
@media screen and (min-width: 642px) and (max-width: 800px) {
.main-content {
width: 70%;
}
}
.row > h2 {
color: #008080;
}
.login_form {
background-color: #fff;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
}
form {
padding: 0 2em;
}
.form__input {
width: 100%;
border: 0px solid transparent;
border-radius: 0;
border-bottom: 1px solid #aaa;
padding: 1em .5em .5em;
padding-left: 2em;
outline: none;
margin: 1.5em auto;
transition: all .5s ease;
}
.form__input:focus {
border-bottom-color: #008080;
box-shadow: 0 0 5px rgba(0, 80, 80, .4);
border-radius: 4px;
}
.btn_signin {
transition: all .5s ease;
width: 100%;
border-radius: 30px;
color: #008080;
font-weight: 600;
background-color: #fff;
border: 1px solid #008080;
margin-top: 1.5em;
margin-bottom: 1em;
}
.btn_signin:hover, .btn:focus {
background-color: #008080;
color: #fff;
}
@@ -1,46 +0,0 @@
<div class="container">
<div class="row main-content bg-success text-center">
<div class="col-md-4 text-center company__info">
<span class="company__logo" routerLink=""><h2><img src="assets/images/Betterzon.svg"></h2></span>
</div>
<div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid">
<div class="row">
<h2>Registration</h2>
</div>
<div class="row">
<form [formGroup]="form" class="form-group" (ngSubmit)="onSubmit()">
<div class="row">
<input type="text" formControlName="username" id="username" name="username"
class="form__input" placeholder="Username">
<div *ngIf="submitted && me.username.errors" class="invalid-feedback">
<div *ngIf="me.username.errors.required">Username is required</div>
</div>
</div>
<div class="row">
<!-- <span class="fa fa-lock"></span> -->
<input type="email" formControlName="email" name="email" id="email" class="form__input"
placeholder="E-Mail">
</div>
<div class="row">
<!-- <span class="fa fa-lock"></span> -->
<input type="password" formControlName="password" name="password" id="password"
class="form__input" placeholder="Password">
</div>
<!--
<div class="row">
<input type="password" name="password" id="password_repeated" class="form__input" placeholder="Kennwort bestätigen">
</div> -->
<div class="row">
<input type="submit" value="Sign up" class="btn_signin">
</div>
<div class="row">
<p>Have an account?<a href="/signin">Log In</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@@ -1,54 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {RegistrationComponent} from './registration.component';
import {AbstractMockObservableService} from '../../../mocks/mock.service';
import {ApiService} from '../../../services/api.service';
import {FormBuilder, FormControl, Validators} from '@angular/forms';
import {Router} from '@angular/router';
class MockApiService extends AbstractMockObservableService {
registerUser(username: string, password: string, email: string): any {
this.content = [];
return this;
}
}
describe('RegistrationComponent', () => {
let component: RegistrationComponent;
let fixture: ComponentFixture<RegistrationComponent>;
let mockService;
let formBuilder: FormBuilder;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [RegistrationComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}, FormBuilder]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RegistrationComponent);
component = fixture.componentInstance;
formBuilder = TestBed.get(FormBuilder);
component.form = formBuilder.group({
recipientTypes: new FormControl(
{
value: ['mock'],
disabled: true
},
Validators.required
)
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,47 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ApiService} from '../../../services/api.service';
import {Router} from '@angular/router';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.css']
})
export class RegistrationComponent implements OnInit {
form: any;
loading = false;
submitted = false;
constructor(
private formBuilder: FormBuilder,
private api: ApiService,
private router: Router
) {
}
ngOnInit(): void {
this.form = this.formBuilder.group({
username: ['', Validators.required],
email: ['', Validators.required],
password: ['', [
Validators.required,
Validators.minLength(8)]
],
});
}
get me() {
return this.form.controls;
}
onSubmit(): void {
this.api.registerUser(this.form.value.username, this.form.value.password, this.form.value.email).subscribe(
res => {
this.api.saveSessionInfoToLocalStorage(res);
this.router.navigate(['/']);
}
);
}
}
@@ -1 +0,0 @@
<p>resetpasswort works!</p>
@@ -1,25 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ResetpasswortComponent} from './resetpasswort.component';
describe('ResetpasswortComponent', () => {
let component: ResetpasswortComponent;
let fixture: ComponentFixture<ResetpasswortComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ResetpasswortComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ResetpasswortComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,16 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-resetpasswort',
templateUrl: './resetpasswort.component.html',
styleUrls: ['./resetpasswort.component.css']
})
export class ResetpasswortComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
@@ -1,93 +0,0 @@
.main-content {
width: 50%;
border-radius: 20px;
box-shadow: 0 5px 5px rgba(0, 0, 0, .4);
margin: 5em auto;
display: flex;
}
.company__info {
background-color: #008080;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
display: flex;
flex-direction: column;
justify-content: center;
color: #fff;
}
.fa-android {
font-size: 3em;
}
@media screen and (max-width: 640px) {
.main-content {
width: 90%;
}
.company__info {
display: none;
}
.login_form {
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
}
}
@media screen and (min-width: 642px) and (max-width: 800px) {
.main-content {
width: 70%;
}
}
.row > h2 {
color: #008080;
}
.login_form {
background-color: #fff;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
}
form {
padding: 0 2em;
}
.form__input {
width: 100%;
border: 0px solid transparent;
border-radius: 0;
border-bottom: 1px solid #aaa;
padding: 1em .5em .5em;
padding-left: 2em;
outline: none;
margin: 1.5em auto;
transition: all .5s ease;
}
.form__input:focus {
border-bottom-color: #008080;
box-shadow: 0 0 5px rgba(0, 80, 80, .4);
border-radius: 4px;
}
.btn_signin {
transition: all .5s ease;
width: 100%;
border-radius: 30px;
color: #008080;
font-weight: 600;
background-color: #fff;
border: 1px solid #008080;
margin-top: 1.5em;
margin-bottom: 1em;
}
.btn_signin:hover, .btn:focus {
background-color: #008080;
color: #fff;
}
@@ -1,36 +0,0 @@
<div class="container">
<div class="row main-content bg-success text-center">
<div class="col-md-4 text-center company__info">
<span class="company__logo" routerLink=""><h2><img src="assets/images/Betterzon.svg"></h2></span>
</div>
<div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid">
<div class="row">
<h2>Sign In</h2>
</div>
<div class="row">
<form [formGroup]="loginForm" class="form-group" (ngSubmit)="onSubmit()">
<div class="row">
<input type="text" formControlName="username" name="username" id="username"
class="form__input" placeholder="Username">
</div>
<div class="row">
<!-- <span class="fa fa-lock"></span> -->
<input type="password" formControlName="password" name="password" id="password"
class="form__input" placeholder="Password">
</div>
<div class="row">
<input type="submit" value="Log in" class="btn_signin">
</div>
</form>
</div>
<div class="row">
<p>No account yet?<a href="/registration">sign up</a></p>
</div>
</div>
</div>
</div>
</div>
@@ -1,54 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SigninComponent} from './signin.component';
import {AbstractMockObservableService} from '../../../mocks/mock.service';
import {ApiService} from '../../../services/api.service';
import {FormBuilder, FormControl, Validators} from '@angular/forms';
import {Router} from '@angular/router';
class MockApiService extends AbstractMockObservableService {
loginUser(username: string, password: string): any {
this.content = [];
return this;
}
}
describe('SigninComponent', () => {
let component: SigninComponent;
let fixture: ComponentFixture<SigninComponent>;
let mockService;
let formBuilder: FormBuilder;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [SigninComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}, FormBuilder]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SigninComponent);
component = fixture.componentInstance;
formBuilder = TestBed.get(FormBuilder);
component.loginForm = formBuilder.group({
recipientTypes: new FormControl(
{
value: ['mock'],
disabled: true
},
Validators.required
)
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,54 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ApiService} from '../../../services/api.service';
import {Router} from '@angular/router';
@Component({
selector: 'app-signin',
templateUrl: './signin.component.html',
styleUrls: ['./signin.component.css']
})
export class SigninComponent implements OnInit {
loginForm: FormGroup;
loading = false;
submitted = false;
private isSuccessful: boolean;
private isSignUpFailed: boolean;
private errorMessage: '';
constructor(
private formBuilder: FormBuilder,
private api: ApiService,
private router: Router
) {
}
ngOnInit(): void {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}
onSubmit(): void {
this.submitted = true;
if (this.loginForm.invalid) {
return;
}
this.api.loginUser(this.loginForm.value.username, this.loginForm.value.password)
.subscribe(
data => {
this.isSuccessful = true;
this.router.navigate(['']);
this.api.saveSessionInfoToLocalStorage(data);
},
err => {
this.errorMessage = err.error.message;
this.isSignUpFailed = true;
});
}
}
@@ -1,67 +1,28 @@
.bottom-bar-wrapper {
display: grid;
grid-template-columns: 546px 546px 546px;
grid-template-rows: 70px 70px 70px;
grid-column-gap: 0px;
grid-row-gap: 0px;
}
.folge-uns-item {
grid-column: 2;
grid-row: 1;
justify-self: center;
}
.link-items {
grid-column: 2;
grid-row: 2;
justify-self: center;
}
.footer-links li {
display: inline;
margin-right: 60px;
}
#footer-line {
grid-area: 3/1/3/4;
.bottom-bar-container {
background-color: black
;
width: 100%;
background-color: #000000;
height: 2px;
height: 68px;
position: fixed;
padding: 16px;
align-items: center;
bottom: 0;
display: grid;
grid-template-columns: 100px auto 100px;
}
.bottom-logo {
grid-column: 1;
grid-row: 3;
.betterzonlogo {
grid-column-start: 1;
grid-column-end: 2;
}
.bottom-info {
grid-column: 3;
grid-row: 3;
justify-self: right;
.references {
grid-column-start: 2;
grid-column-end: 3;
align-items: center;
}
#folge {
font-size: 46px;
font-weight: bold;
color: #E53167;
margin-right: 10px;
}
#uns {
font-size: 32px;
font-weight: bold;
color: #000000;
}
#better {
font-size: 28px;
font-weight: bold;
color: #3480E3;
}
#zon {
font-size: 28px;
font-weight: bold;
color: #E53167;
.copyright {
grid-column-start: 3;
grid-column-end: 4;
}
@@ -1,31 +1,14 @@
<footer class="footer text-center">
<div class="container">
<div class="row">
<!-- Footer Location-->
<div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">Location</h4>
<p class="lead mb-0">
76133 Karlsruhe
<br/>
</p>
</div>
<!-- Footer Social Icons-->
<div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">FOLLOW US</h4>
<a class="btn btn-outline-light btn-social mx-1" href="https://github.com/Mueller-Patrick/Betterzon"><i
class="fab fa-fw fa-github"></i></a>
<a class="btn btn-outline-light btn-social mx-1" href="https://blog.betterzon.xyz/"><i
class="fab fa-fw fa-dribbble"></i></a>
</div>
<!-- Footer About Text-->
<div class="col-lg-4">
<h4 class="text-uppercase mb-4">CONTACT US</h4>
<p class="lead mb-0">
betterzon-contact@mueller-patrick.tech
</p>
</div>
</div>
</div>
</footer>
<div class="bottom-bar-container">
<div class="betterzonlogo">
<a>
<img src="assets/images/Betterzon.svg" [routerLink]="''" alt="Betterzon Logo" width="50px">
</a>
</div>
<div class="references">
<mat-icon aria-hidden="false" aria-label="Example home icon" [routerLink]="''">home</mat-icon>
</div>
<div class="copyright">
Betterzon ©2020
</div>
</div>
@@ -1,14 +1,14 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {BottomBarComponent} from './bottom-bar.component';
import { BottomBarComponent } from "./bottom-bar.component";
describe('BottomBarComponent', () => {
describe("BottomBarComponent", () => {
let component: BottomBarComponent;
let fixture: ComponentFixture<BottomBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BottomBarComponent]
declarations: [ BottomBarComponent ]
})
.compileComponents();
});
@@ -1,14 +1,13 @@
import {Component, OnInit} from '@angular/core';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-bottom-bar',
templateUrl: './bottom-bar.component.html',
styleUrls: ['./bottom-bar.component.css']
templateUrl: "./bottom-bar.component.html",
styleUrls: ["./bottom-bar.component.css"]
})
export class BottomBarComponent implements OnInit {
constructor() {
}
constructor() { }
ngOnInit(): void {
}
@@ -1,24 +0,0 @@
#imprintSection {
right: 1em;
bottom: 1em;
width: 100%;
text-align: right;
padding-right: 1em;
grid-area: right;
}
#imprintSection a {
color: white;
text-decoration: none;
}
#copyright {
display: grid;
grid-template-areas:
'left center right';
grid-template-columns: 30% 40% 30%;
}
#copyright-text {
grid-area: center;
}
@@ -1,7 +0,0 @@
<div class="copyright py-4 text-center text-white" id="copyright">
<div class="container" id="copyright-text"><small>Copyright &copy; Betterzon 2021</small></div>
<div id="imprintSection">
<a href="/impressum">Imprint</a><br>
<a href="/datenschutz">Privacy Policy</a>
</div>
</div>
@@ -1,25 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CopyrightComponent} from './copyright.component';
describe('CopyrightComponent', () => {
let component: CopyrightComponent;
let fixture: ComponentFixture<CopyrightComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CopyrightComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CopyrightComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,16 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-copyright',
templateUrl: './copyright.component.html',
styleUrls: ['./copyright.component.css']
})
export class CopyrightComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
@@ -9,10 +9,10 @@
<a href="https://blog.betterzon.xyz/" class="fa fa-info fa-4x icon-3d"></a>
<a href="https://github.com/Mueller-Patrick/Betterzon/wiki" class="fa fa-wikipedia-w fa-4x icon-3d"></a>
</div>
<div class="blocks" id="copyright">© COPYRIGHT 2020</div>
<div class = "blocks" id="copyright">© COPYRIGHT 2020</div>
</div>
<div id="imprintSection">
<a href="/impressum">Imprint</a><br>
<a href="/impressum" >Imprint</a><br>
<a href="/datenschutz">Privacy Policy</a>
</div>
</footer>
@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
@@ -11,8 +11,7 @@ export class FooterComponent implements OnInit {
constructor(
private router: Router,
private route: ActivatedRoute
) {
}
) {}
ngOnInit(): void {
}

Some files were not shown because too many files have changed in this diff Show More