Compare commits

..

106 Commits

Author SHA1 Message Date
Patrick 1b57ebfb53 Merge pull request #102 from Mueller-Patrick/develop
Synchronising develop and master
2021-11-11 12:05:52 +01:00
Patrick ddebe78f15 Updating README links because of the switch to a new domain 2021-11-11 12:03:06 +01:00
Paddy 086e0d41b3 Updating blog post exports 2021-06-28 17:24:35 +02:00
Paddy 2bc97503de Adding Blog PDF export and function points file 2021-06-27 12:37:44 +02:00
Paddy 8049fd9987 Adding some more cucumber tests 2021-06-26 22:31:08 +02:00
Patrick b839994de0 Merge pull request #101 from Mueller-Patrick/develop
Deployment
2021-06-26 14:43:54 +02:00
Paddy 91716b8147 Merge remote-tracking branch 'origin/develop' into develop 2021-06-26 14:38:18 +02:00
Patrick 5be6d9bd4f Update README.md 2021-06-26 14:37:06 +02:00
Paddy 6e0f1e7659 Reformatting a bunch of files 2021-06-26 14:36:26 +02:00
Paddy 4a7ef6d637 Adjusting some frontend tests 2021-06-26 14:33:58 +02:00
Patrick c3c0fa73b9 Merge pull request #100 from Mueller-Patrick/develop
Some fancy changes
2021-06-18 10:29:55 +02:00
Paddy b1db97af87 Some fancy changes 2021-06-18 10:28:25 +02:00
Patrick 730e1aada0 Merge pull request #99 from Mueller-Patrick/develop
Deployment
2021-06-18 09:29:52 +02:00
Patrick deec125242 Merge pull request #98 from Mueller-Patrick/BETTERZON-152
BETTERZON-152
2021-06-18 09:28:19 +02:00
illumizoldyck d8638cab4b wip: last changes 2 2021-06-18 09:24:16 +02:00
illumizoldyck cfda636f07 Merge remote-tracking branch 'origin/develop' into BETTERZON-152 2021-06-18 09:10:52 +02:00
illumizoldyck 61a0125229 wip: last changes 1 2021-06-18 09:10:47 +02:00
Paddy 0bc02ee9ea Fixing currency sign 2021-06-17 23:33:53 +02:00
Paddy 7ac73204df Fixing typo 2021-06-17 23:04:47 +02:00
Patrick 5c02314180 Merge pull request #97 from Mueller-Patrick/develop
Frontend changes deployment
2021-06-17 22:58:11 +02:00
Patrick 52e38461a4 Merge pull request #96 from Mueller-Patrick/BETTERZON-140
BETTERZON-140: Frontend improvements
2021-06-17 22:56:47 +02:00
Patrick 45b5e04442 Merge branch 'develop' into BETTERZON-140 2021-06-17 22:55:30 +02:00
illumizoldyck a792c43e24 wip: german -> english 2021-06-17 22:52:09 +02:00
Patrick 78e2de6545 Update README.md 2021-06-17 19:06:18 +02:00
illumizoldyck a6a5b58e25 wip: profile 2021-06-17 17:52:27 +02:00
illumizoldyck 012de346e8 Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-17 17:19:07 +02:00
illumizoldyck f28b301a28 wip: profile 2021-06-17 17:18:36 +02:00
Patrick af85f8ff7b Merge pull request #95 from Mueller-Patrick/develop
BETTERZON-151: Adding option to delete price alarm (#94)
2021-06-17 17:16:52 +02:00
Patrick 45acbfd9a2 BETTERZON-151: Adding option to delete price alarm (#94) 2021-06-17 17:15:06 +02:00
illumizoldyck fe2c064e30 wip: hot-deals vendors 2021-06-17 10:55:30 +02:00
illumizoldyck 9821c004ec wip: profile component 2021-06-17 10:30:40 +02:00
illumizoldyck b748b9492a wip: profile component 2021-06-17 09:58:49 +02:00
illumizoldyck 969ac6feaf wip: profile component 2021-06-17 09:55:40 +02:00
illumizoldyck be534551ba wip: hot deals widget loadig data 2021-06-16 15:25:12 +02:00
illumizoldyck c88efc5484 WIP: problems with best deals. 2021-06-16 09:59:57 +02:00
illumizoldyck 8eba80f7d4 WIP: proile component created. 2021-06-16 09:59:25 +02:00
Patrick 6f33186d7e Merge pull request #93 from Mueller-Patrick/develop
Master Deployment
2021-06-16 09:40:10 +02:00
Patrick 841502f9d1 BETTERZON-150: Fixing best deals API endpoint logic (#92) 2021-06-16 09:20:40 +02:00
Nico c90949de47 🚑 Fixed Copyright for you :) (#91) 2021-06-16 00:10:17 +02:00
henningxtro 7f43d27a79 Repaired API Tests (#90) 2021-06-15 23:34:31 +02:00
Patrick d83fcdf693 BETTERZON-147, BETTERZON-148: Adding feature files (#89) 2021-06-15 15:58:54 +02:00
henningxtro ce92abdb40 Doku (#88)
* Added AC-ManageVendorShop.drawio

* Update AC-ManageVendorShop.drawio

* Added AC-ManageVendorShop.png

* Added AC_VendorShop.drawio

* Added Deployment_View.drawio

* Update Deployment_View.drawio

* Added AC_FavoriteShopList.drawio

* Added Deployment_View.png

* Added AC_FavoriteShopList.png

* Added ERM.png

* Adding architectural view

* Updated Use-Case-Diagram

Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
2021-06-15 13:05:33 +02:00
Patrick ce083d8abb Merge pull request #87 from Mueller-Patrick/develop
BETTERZON-146: Session handling rewrite HOTFIX 2
2021-06-15 12:10:28 +02:00
Paddy 0cd1213c40 BETTERZON-146: Session handling rewrite HOTFIX 2 2021-06-15 12:09:04 +02:00
Patrick d9ad99cb72 Merge pull request #86 from Mueller-Patrick/develop
Hotfix Deploy
2021-06-15 11:51:53 +02:00
Paddy 1fd115c2a2 BETTERZON-146: Session handling rewrite HOTFIX 2021-06-15 11:50:36 +02:00
illumizoldyck 92de923fad Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-15 11:37:16 +02:00
Patrick bc437363e5 Merge pull request #85 from Mueller-Patrick/develop
Master deployment
2021-06-15 11:35:04 +02:00
Patrick 0118ac6861 Merge pull request #84 from Mueller-Patrick/BETTERZON-140
BETTERZON-140: Product Details Part 1
2021-06-15 11:34:00 +02:00
illumizoldyck 83f6018f7e Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-15 11:33:36 +02:00
Paddy bf56d2b509 BETTERZON-146: Changing session handling from cookies to localStorage 2021-06-15 11:32:48 +02:00
Paddy ad091954c1 Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-15 10:39:15 +02:00
illumizoldyck b062e14c9a auth with cookies. 2021-06-15 10:38:54 +02:00
Patrick f1dcaef351 Merge pull request #83 from Mueller-Patrick/BETTERZON-145
BETTERZON-145: Adding better error when trying to get session details…
2021-06-15 10:38:52 +02:00
Paddy 391a4b5e4b BETTERZON-145: Adding better error when trying to get session details without cookie 2021-06-15 10:37:14 +02:00
illumizoldyck cf300cb1b7 Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-15 10:24:51 +02:00
Patrick c35097a9a2 Merge pull request #82 from Mueller-Patrick/BETTERZON-144
BETTERZON-144: Adding service method to get session / user info
2021-06-15 10:24:20 +02:00
illumizoldyck 62795fd3f8 Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-15 10:24:02 +02:00
Patrick 4a0ba40da6 Merge branch 'develop' into BETTERZON-144 2021-06-15 10:20:17 +02:00
Paddy f4d1e93a7f BETTERZON-144: Adding service method to get session / user info 2021-06-15 10:19:48 +02:00
Patrick aae931099b Merge pull request #81 from Mueller-Patrick/develop
Master deployment
2021-06-14 20:57:18 +02:00
Patrick 3dbcc6707c Merge pull request #80 from Mueller-Patrick/BETTERZON-143
BETTERZON-143: Fixing API endpoints that didn't return a json
2021-06-14 20:56:08 +02:00
Paddy 1bb05a1baf BETTERZON-143: Fixing API endpoints that didn't return a json 2021-06-14 20:54:54 +02:00
Paddy 6eaf7aca82 Adding SQL script 2021-06-14 15:54:57 +02:00
Paddy 9512002081 Revert "Adjusting frontend and backend package names"
This reverts commit e0033065
2021-06-14 14:49:24 +02:00
Paddy e00330656e Adjusting frontend and backend package names 2021-06-14 14:40:41 +02:00
Paddy daef6ec208 BETTERZON-141: Fixing user updating query 2021-06-13 14:16:09 +02:00
Paddy f2adb1e375 BETTERZON-141: Fixing service class
- Because Patrick was stupid
2021-06-13 13:09:51 +02:00
illumizoldyck 68e9d75e2d wip: register api 2021-06-13 12:49:15 +02:00
Patrick 099f6f1c51 Merge pull request #79 from Mueller-Patrick/BETTERZON-141
BETTERZON-141: Adding more service methods for prices and products
2021-06-11 12:23:19 +02:00
Paddy a80e86b4ea BETTERZON-141: Adding more service methods for prices and products 2021-06-11 12:20:10 +02:00
Patrick dcb9edd562 Adding License 2021-06-10 14:31:10 +02:00
Patrick 5e450ea0c2 Merge pull request #78 from Mueller-Patrick/develop
Master Deployment
2021-06-07 16:51:02 +02:00
Patrick d2d3bcac8c Merge pull request #77 from Mueller-Patrick/BETTERZON-120
BETTERZON-120
2021-06-07 16:48:44 +02:00
Patrick 3eea4f7f72 Merge branch 'develop' into BETTERZON-120 2021-06-07 16:47:35 +02:00
illumizoldyck c8cbc53c91 kunden component is done. 2021-06-07 09:59:07 +02:00
illumizoldyck b291c0b828 greeting component is done. 2021-06-07 09:58:51 +02:00
illumizoldyck 6c7b28b4cb footer component adjusted. 2021-06-07 09:58:36 +02:00
illumizoldyck 14b943b64a hot-deals component is done. 2021-06-07 09:58:10 +02:00
illumizoldyck 7a547a6a72 basic components adjusted. 2021-06-07 09:57:28 +02:00
illumizoldyck 53fcb86295 basic components adjusted. 2021-06-07 09:55:21 +02:00
illumizoldyck 709a41bcdb about us component is done. 2021-06-07 09:54:48 +02:00
illumizoldyck f30942443f top-bar adjusted. 2021-06-07 09:53:39 +02:00
illumizoldyck 54e68479f6 auth. components done. 2021-06-07 09:53:03 +02:00
Paddy 1fa69c334b Merging develop 2021-06-06 22:53:24 +02:00
Patrick e760247866 Merge pull request #76 from Mueller-Patrick/BETTERZON-139
BETTERZON-139: Fixing Codacy Error Prone Issues
2021-06-06 22:41:31 +02:00
Paddy f3f1cba9ea BETTERZON-139: Fixing Codacy Error Prone Issues 2021-06-06 22:38:28 +02:00
henningxtro 3be39fad76 Api tests (#75)
* Added API Tests for Postman - BETTERZON-127 BETTERZON-128 BETTERZON-129 BETTERZON-130 BETTERZON-131 BETTERZON-132 BETTERZON-133 BETTERZON-134 BETTERZON-135

* Updated Postman Tests

Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
2021-05-30 16:32:15 +02:00
Patrick 5d3e48a3c8 Update README.md 2021-05-30 16:25:33 +02:00
Patrick 13f0242fc2 Merge pull request #73 from Mueller-Patrick/develop
Master deployment
2021-05-30 12:13:08 +02:00
Paddy 5a4ea9a134 Merge remote-tracking branch 'origin/master' into develop 2021-05-30 12:11:47 +02:00
Patrick d362c68fac BETTERZON-126: Correcting HTTP status codes sent by API (#72) 2021-05-30 12:07:08 +02:00
Patrick 25b6b43e9f BETTERZON-124: Adding service function for crawling status API (#71) 2021-05-29 13:57:19 +02:00
Patrick 63362fbe67 BETTERZON-125: Adding service functions for manufacturer API (#70) 2021-05-29 13:51:07 +02:00
Patrick 6bb1c8f66b BETTERZON-123: Adding service functions for categories API (#69) 2021-05-29 13:43:50 +02:00
Patrick 7dc76649fc BETTERZON-121: Adding service functions for contact persons API (#68) 2021-05-29 13:28:54 +02:00
illumizoldyck 9ef37cee03 Merge remote-tracking branch 'origin/develop' into BETTERZON-120 2021-05-29 11:03:06 +02:00
illumizoldyck 7f9e6e5197 BETTERZON-120: wip: auth components created. 2021-05-29 11:01:32 +02:00
Patrick 6e8c52857f Master deployment (#67)
* BETTERZON-58: Basic Functionality with scrapy (#33)

* BETTERZON-73: Adding API endpoint that returns the lowest non-amazon prices for a given list of product ids (#32)

* BETTERZON-75: User registration API endpoint (#34)

* BETTERZON-75: Adding backend functions to enable user registration

* BETTERZON-75: Adding regex to check email and username

* BETTERZON-83: FE unit testing (#35)

* BETTERZON-83: Making pre-generated unit tests work

* BETTERZON-83: Writing unit tests for angular to improve code coverage

* BETTERZON-79: Adding API endpoint for logging in (#36)

* BETTERZON-84: Adding service method to check if a session is valid (#37)

* BETTERZON-77: Changing error behavior as the previous behavior cloud have opened up security vulnerabilities (#38)

* BETTERZON-76: Adding method descriptions for backend service methods (#40)

* Adding Codacy code quality badge to README

* BETTERZON-89: Refactoring / Reformatting and adding unit tests (#41)

* BETTERZON-90: Adding API endpoint for creating price alarms (#42)

* BETTERZON-91: Adding API endpoint to GET all price alarms for the currently logged in user (#43)

* BETTERZON-92: Adding API endpoint to edit (update) price alarms (#44)

* BETTERZON-99: Adding some basic cucumber tests (#45)

* BETTERZON-100: Switching to cookies for session management (#46)

* BETTERZON-100: Switching session handling to cookies

* BETTERZON-100: Some code reformatting

* BETTERZON-100: Some more code reformatting

* BETTERZON-93: Adding API endpoint to get managed shops (#47)

* BETTERZON-94: Adding API endpoint to deactivate price listings as a vendor manager (#48)

* BETTERZON-97: Adding API endpoint to get all products listed by a specific vendor (#50)

* BETTERZON-98: Adding API endpoint for adding price entries as a registered vendor manager (#51)

* BETTERZON-95: Adding API endpoint for getting, inserting and updating contact persons (#52)

* BETTERZON-58 (#53)

* BETTERZON-58: Basic Functionality with scrapy

* Added independent crawler function, yielding price

* moved logic to amazon.py

* .

* moved scrapy files to unused folder

* Added basic amazon crawler using beautifulsoup4

* Connected Api to Crawler

* Fixed string concatenation for sql statement in getProductLinksForProduct

* BETTERZON-58: Fixing SQL insert

* BETTERZON-58: Adding access key verification

* BETTERZON-58: Fixing API endpoint of the crawler
- The list of products in the API request was treated like a string and henceforth, only the first product has been crawled

* Added another selector for price on amazon (does not work for books)

Co-authored-by: root <root@DESKTOP-ARBPL82.localdomain>
Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTERZON-96: Adding API endpoint for delisting a whole vendor (#54)

* BETTERZON-101: Adding service functions for pricealarms api (#55)

- Not properly tested though as login functionality is required to test but not yet implemented

* BETTERZON-110: Refactoring, reformatting and commenting api service (#56)

* BETTERZON-107: Refactoring code with Proxy as design pattern (#49)

* BETTERZON-78 (#39)

* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTERZON-109 (#57)

* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

* wip: new component created and added to the app.module.ts. Added a minimal grid layout.

* wip: all components were wrapped now. Grid structure has been applied to the main wrapper-class "container".

* wip: component created and added to the app.module.ts

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTERZON-108 (#58)

* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

* wip: new component created and added to the app.module.ts. Added a minimal grid layout.

* wip: all components were wrapped now. Grid structure has been applied to the main wrapper-class "container".

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTERZON-106 (#59)

* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

* wip: new component created and added to the app.module.ts. Added a minimal grid layout.

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTEZON-102 (#60)

* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>

* BETTERZON-113, BETTERZON-114, BETTERZON-115: Adding API endpoint for favorite shops (#61)

* BETTERZON-116: Adding API endpoint for searching a new product (#62)

* BETTERZON-117: Adding API endpoint for getting the latest crawling status (#63)

* BETTERZON-111: Adding service functions for login and registration (#64)

* BETTERZON-112: Adding service functions for managing vendor shops (#65)

* BETTERZON-118: Adding service functions for managing favorite shops (#66)

Co-authored-by: henningxtro <sextro.henning@student.dhbw-karlsruhe.de>
Co-authored-by: root <root@DESKTOP-ARBPL82.localdomain>
Co-authored-by: Reboooooorn <61185041+Reboooooorn@users.noreply.github.com>
2021-05-29 10:58:27 +02:00
Patrick 1a65783690 BETTERZON-118: Adding service functions for managing favorite shops (#66) 2021-05-26 21:30:53 +02:00
Patrick 6b88c48018 BETTERZON-112: Adding service functions for managing vendor shops (#65) 2021-05-26 21:13:17 +02:00
Patrick 36173c6117 BETTERZON-111: Adding service functions for login and registration (#64) 2021-05-26 20:51:19 +02:00
Patrick f28dae3272 BETTERZON-117: Adding API endpoint for getting the latest crawling status (#63) 2021-05-23 16:01:30 +02:00
Patrick 1e6d99a713 BETTERZON-116: Adding API endpoint for searching a new product (#62) 2021-05-20 20:59:50 +02:00
Patrick 13f1257740 BETTERZON-113, BETTERZON-114, BETTERZON-115: Adding API endpoint for favorite shops (#61) 2021-05-20 17:46:09 +02:00
Patrick 197c39a61d Merge pull request #31 from Mueller-Patrick/develop
Master Deployment for API changes
2021-04-17 12:34:10 +02:00
140 changed files with 23926 additions and 6027 deletions
+4
View File
@@ -16,6 +16,8 @@ import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router'; import {usersRouter} from './models/users/users.router';
import {pricealarmsRouter} from './models/pricealarms/pricealarms.router'; import {pricealarmsRouter} from './models/pricealarms/pricealarms.router';
import {contactpersonsRouter} from './models/contact_persons/contact_persons.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'); const cookieParser = require('cookie-parser');
@@ -51,6 +53,8 @@ app.use('/users', usersRouter);
app.use('/vendors', vendorsRouter); app.use('/vendors', vendorsRouter);
app.use('/pricealarms', pricealarmsRouter); app.use('/pricealarms', pricealarmsRouter);
app.use('/contactpersons', contactpersonsRouter); app.use('/contactpersons', contactpersonsRouter);
app.use('/favoriteshops', favoriteshopsRouter);
app.use('/crawlingstatus', crawlingstatusRouter);
app.use(errorHandler); app.use(errorHandler);
app.use(notFoundHandler); app.use(notFoundHandler);
@@ -76,7 +76,9 @@ contactpersonsRouter.post('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const vendor_id = req.body.vendor_id; const vendor_id = req.body.vendor_id;
@@ -89,9 +91,9 @@ contactpersonsRouter.post('/', async (req: Request, res: Response) => {
const success = await ContactPersonService.createContactEntry(user.user_id, vendor_id, first_name, last_name, gender, email, phone); const success = await ContactPersonService.createContactEntry(user.user_id, vendor_id, first_name, last_name, gender, email, phone);
if (success) { if (success) {
res.sendStatus(200); res.status(201).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
@@ -104,7 +106,9 @@ contactpersonsRouter.put('/:id', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const contact_person_id = parseInt(req.params.id, 10); const contact_person_id = parseInt(req.params.id, 10);
@@ -118,9 +122,9 @@ contactpersonsRouter.put('/:id', async (req: Request, res: Response) => {
const success = await ContactPersonService.updateContactEntry(user.user_id, contact_person_id, vendor_id, first_name, last_name, gender, email, phone); const success = await ContactPersonService.updateContactEntry(user.user_id, contact_person_id, vendor_id, first_name, last_name, gender, email, phone);
if (success) { if (success) {
res.sendStatus(200); res.status(200).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
@@ -0,0 +1,7 @@
export interface Crawling_Status {
process_id: number;
started_timestamp: Date;
combinations_to_crawl: number;
successful_crawls: number;
failed_crawls: number;
}
@@ -0,0 +1,44 @@
/**
* 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.'}));
}
});
@@ -0,0 +1,75 @@
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();
}
}
};
@@ -0,0 +1,5 @@
import {Crawling_Status} from './crawling_status.interface';
export interface Crawling_Statuses {
[key: number]: Crawling_Status;
}
@@ -0,0 +1,5 @@
export interface FavoriteShop {
favorite_id: number;
vendor_id: number;
user_id: number;
}
@@ -0,0 +1,5 @@
import {FavoriteShop} from './favoriteshop.interface';
export interface FavoriteShops {
[key: number]: FavoriteShop;
}
@@ -0,0 +1,106 @@
/**
* 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.'}));
}
});
@@ -0,0 +1,92 @@
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();
}
}
};
@@ -24,7 +24,9 @@ pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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); const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id);
@@ -35,12 +37,14 @@ pricealarmsRouter.get('/', async (req: Request, res: Response) => {
} }
}); });
// POST pricealarms/create // POST pricealarms/
pricealarmsRouter.post('/', async (req: Request, res: Response) => { pricealarmsRouter.post('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get info for price alarm creation
const product_id = req.body.product_id; const product_id = req.body.product_id;
@@ -68,12 +72,14 @@ pricealarmsRouter.post('/', async (req: Request, res: Response) => {
} }
}); });
// PUT pricealarms/update // PUT pricealarms/
pricealarmsRouter.put('/', async (req: Request, res: Response) => { pricealarmsRouter.put('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get info for price alarm creation
const alarm_id = req.body.alarm_id; const alarm_id = req.body.alarm_id;
@@ -85,11 +91,37 @@ pricealarmsRouter.put('/', async (req: Request, res: Response) => {
return; return;
} }
// Create price alarm // Update price alarm
const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_id, defined_price); const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_id, defined_price);
if (success) { if (success) {
res.status(201).send(JSON.stringify({success: true})); 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; return;
} else { } else {
res.status(500).send(JSON.stringify({success: false})); res.status(500).send(JSON.stringify({success: false}));
@@ -29,17 +29,13 @@ import {PriceAlarms} from './pricealarms.interface';
* @param product_id The id of the product 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 * @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> => { export const createPriceAlarm = async (user_id: number, product_id: number, defined_price: number): Promise<boolean> => {
let conn; let conn;
try { try {
conn = await pool.getConnection(); 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]); const res = await conn.query('INSERT INTO price_alarms (user_id, product_id, defined_price) VALUES (?, ?, ?)', [user_id, product_id, defined_price]);
if (res.affectedRows === 1) { return res.affectedRows === 1;
return true;
} else {
return false;
}
} catch (err) { } catch (err) {
throw err; throw err;
} finally { } finally {
@@ -47,8 +43,6 @@ export const createPriceAlarm = async (user_id: number, product_id: number, defi
conn.end(); conn.end();
} }
} }
return false;
}; };
/** /**
@@ -83,17 +77,34 @@ export const getPriceAlarms = async (user_id: number): Promise<PriceAlarms> => {
* @param user_id The id of the user that wants to update the price alarm * @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 * @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> => { export const updatePriceAlarm = async (alarm_id: number, user_id: number, defined_price: number): Promise<boolean> => {
let conn; let conn;
try { try {
conn = await pool.getConnection(); 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]); const res = await conn.query('UPDATE price_alarms SET defined_price = ? WHERE alarm_id = ? AND user_id = ?', [defined_price, alarm_id, user_id]);
if (res.affectedRows === 1) { return res.affectedRows === 1;
return true; } catch (err) {
} else { throw err;
return false; } 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) { } catch (err) {
throw err; throw err;
} finally { } finally {
@@ -101,6 +112,4 @@ export const updatePriceAlarm = async (alarm_id: number, user_id: number, define
conn.end(); conn.end();
} }
} }
return false;
}; };
+5 -3
View File
@@ -107,7 +107,9 @@ pricesRouter.post('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const vendor_id = req.body.vendor_id; const vendor_id = req.body.vendor_id;
@@ -117,9 +119,9 @@ pricesRouter.post('/', async (req: Request, res: Response) => {
const success = await PriceService.createPriceEntry(user.user_id, vendor_id, product_id, price_in_cents); const success = await PriceService.createPriceEntry(user.user_id, vendor_id, product_id, price_in_cents);
if (success) { if (success) {
res.sendStatus(200); res.status(201).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
+2 -2
View File
@@ -282,11 +282,11 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
'price_in_cents': lowestPrice.price_in_cents, 'price_in_cents': lowestPrice.price_in_cents,
'timestamp': lowestPrice.timestamp, 'timestamp': lowestPrice.timestamp,
'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents), 'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents),
'amazonDifferencePercent': ((1 - (lowestPrice.price_in_cents / amazonPrice.price_in_cents)) * 100), 'amazonDifferencePercent': ((amazonPrice.price_in_cents / lowestPrice.price_in_cents) * 100),
}; };
// Push only deals were the amazon price is actually higher // Push only deals were the amazon price is actually higher
if (deal.amazonDifferencePercent > 0) { if (deal.amazonDifferencePercent > 0 && deal.amazonDifference > 0) {
deals.push(deal as Deal); deals.push(deal as Deal);
} }
} }
@@ -106,3 +106,26 @@ productsRouter.get('/vendor/:id', async (req: Request, res: Response) => {
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 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.'}));
}
});
@@ -17,6 +17,7 @@ const pool = mariadb.createPool({
import {Product} from './product.interface'; import {Product} from './product.interface';
import {Products} from './products.interface'; import {Products} from './products.interface';
import * as http from 'http';
/** /**
@@ -197,3 +198,32 @@ export const findByVendor = async (id: number): Promise<Products> => {
return prodRows; 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,4 +5,5 @@ export interface User {
password_hash: string; password_hash: string;
registration_date: Date; registration_date: Date;
last_login_date: Date; last_login_date: Date;
is_admin: boolean;
} }
+18 -10
View File
@@ -47,10 +47,10 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
const session: Session = await UserService.createUser(username, password, email, ip); const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user // Send the session details back to the user
res.cookie('betterauth', JSON.stringify({ res.status(201).send({
id: session.session_id, session_id: session.session_id,
key: session.session_key session_key: session.session_key
}), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(201); });
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
@@ -80,10 +80,10 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
} }
// Send the session details back to the user // Send the session details back to the user
res.cookie('betterauth', JSON.stringify({ res.status(200).send({
id: session.session_id, session_id: session.session_id,
key: session.session_key session_key: session.session_key
}), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(200); });
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
@@ -94,9 +94,17 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
try { try {
const ip: string = req.connection.remoteAddress ?? ''; 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]}));
return;
}
// Update the user entry and create a session // Update the user entry and create a session
const user: User = await UserService.checkSessionWithCookie(req.cookies.betterauth, ip); const user: User = await UserService.checkSession(session_id, session_key, ip);
if (!user.user_id) { if (!user.user_id) {
// Error logging in, probably wrong username / password // Error logging in, probably wrong username / password
@@ -105,7 +113,7 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
} }
// Send the session details back to the user // Send the session details back to the user
res.status(201).send(user); res.status(200).send(user);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
+7 -8
View File
@@ -115,8 +115,8 @@ export const login = async (username: string, password: string, ip: string): Pro
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
// Update user entry in SQL // Update user entry in SQL
const userQuery = 'UPDATE users SET last_login_date = NOW()'; const userQuery = 'UPDATE users SET last_login_date = NOW() WHERE user_id = ?';
const userIdRes = await conn.query(userQuery); const userIdRes = await conn.query(userQuery, userId);
await conn.commit(); await conn.commit();
// Create session // Create session
@@ -193,18 +193,20 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
await conn.commit(); await conn.commit();
// Get the other required user information and update the user // Get the other required user information and update the user
const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?'; const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date, is_admin FROM users WHERE user_id = ?';
const userRows = await conn.query(userQuery, userId); const userRows = await conn.query(userQuery, userId);
let username = ''; let username = '';
let email = ''; let email = '';
let registrationDate = new Date(); let registrationDate = new Date();
let lastLoginDate = new Date(); let lastLoginDate = new Date();
let is_admin = false;
for (const row in userRows) { for (const row in userRows) {
if (row !== 'meta' && userRows[row].user_id != null) { if (row !== 'meta' && userRows[row].user_id != null) {
username = userRows[row].username; username = userRows[row].username;
email = userRows[row].email; email = userRows[row].email;
registrationDate = userRows[row].registration_date; registrationDate = userRows[row].registration_date;
lastLoginDate = userRows[row].last_login_date; lastLoginDate = userRows[row].last_login_date;
is_admin = userRows[row].is_admin;
} }
} }
@@ -215,7 +217,8 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
email: email, email: email,
password_hash: 'HIDDEN', password_hash: 'HIDDEN',
registration_date: registrationDate, registration_date: registrationDate,
last_login_date: lastLoginDate last_login_date: lastLoginDate,
is_admin: is_admin
}; };
} catch (err) { } catch (err) {
@@ -225,8 +228,6 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
conn.end(); conn.end();
} }
} }
return {} as User;
}; };
/** /**
@@ -312,6 +313,4 @@ export const checkUsernameAndEmail = async (username: string, email: string): Pr
conn.end(); conn.end();
} }
} }
return {hasProblems: true, messages: ['Internal server error'], codes: [3]};
}; };
+21 -13
View File
@@ -37,7 +37,9 @@ vendorsRouter.get('/managed', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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); const vendors = await VendorService.getManagedShops(user.user_id);
@@ -86,12 +88,14 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
} }
}); });
// PUT /manage/deactivatelisting // PUT vendors/manage/deactivatelisting
vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => { vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const vendor_id = req.body.vendor_id; const vendor_id = req.body.vendor_id;
@@ -100,9 +104,9 @@ vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Respons
const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id); const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id);
if (success) { if (success) {
res.sendStatus(200); res.status(200).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
@@ -110,12 +114,14 @@ vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Respons
} }
}); });
// PUT /manage/shop/deactivate/:id // PUT vendors/manage/shop/deactivate/:id
vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Response) => { vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const vendor_id = parseInt(req.params.id, 10); const vendor_id = parseInt(req.params.id, 10);
@@ -123,9 +129,9 @@ vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Respo
const success = await VendorService.setShopStatus(user.user_id, vendor_id, false); const success = await VendorService.setShopStatus(user.user_id, vendor_id, false);
if (success) { if (success) {
res.sendStatus(200); res.status(200).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
@@ -133,12 +139,14 @@ vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Respo
} }
}); });
// PUT /manage/shop/activate/:id // PUT vendors/manage/shop/activate/:id
vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Response) => { vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); 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 // Get required parameters
const vendor_id = parseInt(req.params.id, 10); const vendor_id = parseInt(req.params.id, 10);
@@ -146,9 +154,9 @@ vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Respons
const success = await VendorService.setShopStatus(user.user_id, vendor_id, true); const success = await VendorService.setShopStatus(user.user_id, vendor_id, true);
if (success) { if (success) {
res.sendStatus(200); res.status(200).send({});
} else { } else {
res.sendStatus(500); res.status(500).send({});
} }
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); console.log('Error handling a request: ' + e.message);
+2 -2
View File
@@ -1,10 +1,10 @@
# Base image # Base image
FROM python FROM python:3.9.5-buster
# Create directories and copy files # Create directories and copy files
RUN echo 'Creating directory and copying files' RUN echo 'Creating directory and copying files'
RUN mkdir /crawler RUN mkdir /crawler
ADD . /crawler COPY . /crawler
WORKDIR /crawler WORKDIR /crawler
# Install dependencies # Install dependencies
+4 -2
View File
@@ -9,13 +9,15 @@ import stepdefs.Preconditions;
@RunWith(Cucumber.class) @RunWith(Cucumber.class)
@CucumberOptions( @CucumberOptions(
features = {"src/test/resource/searchProduct.feature", features = {"src/test/resource/searchProduct.feature",
"src/test/resource/priceAlarms.feature"} "src/test/resource/priceAlarms.feature",
"src/test/resource/favoriteShopList.feature",
"src/test/resource/manageVendor.feature"}
) )
public class RunTest { public class RunTest {
@BeforeClass @BeforeClass
public static void setup() { public static void setup() {
Preconditions.driver= new FirefoxDriver(); Preconditions.driver = new FirefoxDriver();
} }
@AfterClass @AfterClass
@@ -0,0 +1,34 @@
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 {
}
}
@@ -0,0 +1,31 @@
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 {
}
}
@@ -4,4 +4,5 @@ import org.openqa.selenium.WebDriver;
public class Preconditions { public class Preconditions {
public static WebDriver driver; public static WebDriver driver;
public static final int delaySeconds = 7;
} }
@@ -4,6 +4,12 @@ import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given; import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then; import io.cucumber.java.en.Then;
import io.cucumber.java.en.When; 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;
public class PriceAlarm { public class PriceAlarm {
@Given("^the user has at least (\\d+) price alarm set$") @Given("^the user has at least (\\d+) price alarm set$")
@@ -12,42 +18,43 @@ public class PriceAlarm {
@When("^the user clicks on the profile icon$") @When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception { 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')]")));
@Then("^the profile details popup should open$") profileButton.click();
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$") @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 { 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);
} }
@Then("^the price alarm list should contain a maximum of (\\d+) entries per page$") @Given("^the user is on the profile page$")
public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception { public void the_user_is_on_the_profile_page() throws Exception {
} Preconditions.driver.get("https://www.betterzon.xyz/profile");
@Given("^the user is on the price alarm list page$") WebElement profile_info_text = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
public void the_user_is_on_the_price_alarm_list_page() throws Exception { .until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-user-information")));
assert (profile_info_text.isDisplayed());
} }
@When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$") @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 { 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!");
} }
@Then("^a popup should open asking the user to confirm the removal$") WebElement btn = entry.findElement(By.cssSelector("img.delete[src='../assets/images/Delete_icon-icons.com_55931.png']"));
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$") btn.click();
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception { } else if (arg1.equals("edit")) {
}
} }
@Then("^the price alarm should be removed from the database$") @Then("^the price alarm should be removed from the database$")
@@ -4,9 +4,7 @@ import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given; import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then; import io.cucumber.java.en.Then;
import io.cucumber.java.en.When; import io.cucumber.java.en.When;
import org.openqa.selenium.By; import org.openqa.selenium.*;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.WebDriverWait;
@@ -15,8 +13,8 @@ public class SearchProduct {
public void the_user_is_on_the_landing_page() throws Exception { public void the_user_is_on_the_landing_page() throws Exception {
//throw new PendingException(); //throw new PendingException();
Preconditions.driver.get("https://betterzon.xyz"); Preconditions.driver.get("https://betterzon.xyz");
WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); .until(ExpectedConditions.elementToBeClickable(By.cssSelector("a.navbar-brand")));
} }
@When("^the user enters the search term \"([^\"]*)\" and clicks search$") @When("^the user enters the search term \"([^\"]*)\" and clicks search$")
@@ -24,49 +22,99 @@ public class SearchProduct {
WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid")); WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid"));
searchField.sendKeys(searchTerm); searchField.sendKeys(searchTerm);
searchField.sendKeys(Keys.ENTER); searchField.sendKeys(Keys.ENTER);
WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".navbar-brand")));
} }
@Then("^the user should see the error page \"([^\"]*)\"$") @Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception { public void the_user_should_see_the_error_page(String arg0) throws Exception {
WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, 10)) WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".ng-star-inserted"))); .until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'No Products found!')]")));
assert(noProdsFoundMsg.getText().contains("No Products found!")); assert (noProdsFoundMsg.isDisplayed());
} }
@Given("^the user is not logged in$") @Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception { 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$") @Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception { 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$") @Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception { public void the_user_should_see_a_list_of_products() throws Exception {
WebElement product = (new WebDriverWait(Preconditions.driver, 10)) WebElement product = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".productItem.ng-star-inserted"))); .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".row.p-2.bg-white.border.rounded")));
assert(product.isDisplayed()); assert (product.isDisplayed());
} }
@When("^the user clicks on the first product$") @When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception { 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$") @Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception { 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 \"([^\"]*)\"$") @Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception { 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$") @When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception { 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$") @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 { public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
assert (true);
} }
} }
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,15 @@
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,23 +5,22 @@ Feature: Price Alarms
And the user is logged in And the user is logged in
And the user has at least 1 price alarm set And the user has at least 1 price alarm set
When the user clicks on the profile icon When the user clicks on the profile icon
Then the profile details popup should open Then the profile page 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 at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page
Scenario: Remove a price alarm Scenario: Remove a price alarm
Given the user is on the price alarm list page Given the user is on the landing page
And the user is logged in 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 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 Then the price alarm should be removed from the database
Scenario: Edit a price alarm Scenario: Edit a price alarm
Given the user is on the price alarm list page Given the user is on the landing page
And the user is logged in 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 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 Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button 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 Then the user should see a list of products
When the user clicks on the first product When the user clicks on the first product
Then the user should see the product detail page Then the user should see the product detail page
And the set price alarm box should show "Log in to continue" And the set price alarm box should show "Login to set a price alarm"
Scenario: User is logged in, searches for known product Scenario: User is logged in, searches for known product
Given the user is on the landing page Given the user is on the landing page
+2 -1
View File
@@ -92,7 +92,8 @@
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"codeCoverage": true, "codeCoverage": true,
"codeCoverageExclude": [ "codeCoverageExclude": [
"src/app/mocks/mock.service.ts" "src/app/mocks/mock.service.ts",
"src/app/services/api.service.ts"
], ],
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
+4 -1
View File
@@ -14,7 +14,10 @@ module.exports = function (config) {
require('@angular-devkit/build-angular/plugins/karma') require('@angular-devkit/build-angular/plugins/karma')
], ],
client: { client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
random: false
}
}, },
coverageIstanbulReporter: { coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/Betterzon'), dir: require('path').join(__dirname, './coverage/Betterzon'),
+6523 -5247
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -7,7 +7,8 @@
"build": "ng build", "build": "ng build",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e",
"postinstall": "ngcc"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
+9
View File
@@ -1 +1,10 @@
.wrapper_app {
padding-bottom: 2.5rem; /* Footer height */
}
.footer_app {
position: relative;
bottom: 0;
width: 100%;
height: 2.5rem; /* Footer height */
}
+6 -11
View File
@@ -1,13 +1,8 @@
<div class="container"> <router-outlet></router-outlet>
<div class="header">
<app-top-bar></app-top-bar>
</div>
<div class="page-content">
<router-outlet></router-outlet>
</div>
<div class="footer">
<app-bottom-bar></app-bottom-bar>
</div>
</div>
+3 -3
View File
@@ -1,8 +1,8 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {RouterTestingModule} from "@angular/router/testing"; import {RouterTestingModule} from '@angular/router/testing';
import {NgcCookieConsentConfig, NgcCookieConsentModule} from "ngx-cookieconsent"; import {NgcCookieConsentConfig, NgcCookieConsentModule} from 'ngx-cookieconsent';
import {FormsModule} from "@angular/forms"; import {FormsModule} from '@angular/forms';
// For cookie consent module testing // For cookie consent module testing
const cookieConfig: NgcCookieConsentConfig = { const cookieConfig: NgcCookieConsentConfig = {
+7
View File
@@ -1,6 +1,8 @@
import {Component, OnDestroy, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {NgcCookieConsentService, NgcInitializeEvent, NgcNoCookieLawEvent, NgcStatusChangeEvent} from 'ngx-cookieconsent'; import {NgcCookieConsentService, NgcInitializeEvent, NgcNoCookieLawEvent, NgcStatusChangeEvent} from 'ngx-cookieconsent';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {ApiService} from './services/api.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -19,12 +21,17 @@ export class AppComponent implements OnInit, OnDestroy {
private revokeChoiceSubscription: Subscription; private revokeChoiceSubscription: Subscription;
private noCookieLawSubscription: Subscription; private noCookieLawSubscription: Subscription;
isLoggedIn = false;
showUserBoard = false;
username?: string;
constructor( constructor(
private ccService: NgcCookieConsentService private ccService: NgcCookieConsentService
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
// subscribe to cookieconsent observables to react to main events // subscribe to cookieconsent observables to react to main events
this.popupOpenSubscription = this.ccService.popupOpen$.subscribe( this.popupOpenSubscription = this.ccService.popupOpen$.subscribe(
() => { () => {
+25 -7
View File
@@ -13,7 +13,7 @@ import {NgApexchartsModule} from 'ng-apexcharts';
import {ProductSearchPageComponent} from './pages/product-search-page/product-search-page.component'; import {ProductSearchPageComponent} from './pages/product-search-page/product-search-page.component';
import {HeaderComponent} from './components/header/header.component'; import {HeaderComponent} from './components/header/header.component';
import {NewestPricesListComponent} from './components/newest-prices-list/newest-prices-list.component'; import {NewestPricesListComponent} from './components/newest-prices-list/newest-prices-list.component';
import {FormsModule} from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component'; import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
import {MatMenuModule} from '@angular/material/menu'; import {MatMenuModule} from '@angular/material/menu';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@@ -23,15 +23,23 @@ import {NgcCookieConsentModule, NgcCookieConsentConfig} from 'ngx-cookieconsent'
import {MatSlideToggleModule} from '@angular/material/slide-toggle'; import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {TopBarComponent} from './components/top-bar/top-bar.component'; import {TopBarComponent} from './components/top-bar/top-bar.component';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {MatButtonModule} from "@angular/material/button"; import {MatButtonModule} from '@angular/material/button';
import {MatToolbarModule} from '@angular/material/toolbar'; import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon'; import {MatIconModule} from '@angular/material/icon';
import {MatSidenavModule} from '@angular/material/sidenav'; 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 {BottomBarComponent} from './components/bottom-bar/bottom-bar.component';
import { HotDealsWidgetComponent } from './components/hot-deals-widget/hot-deals-widget.component'; import {HotDealsWidgetComponent} from './components/hot-deals-widget/hot-deals-widget.component';
import { SliderForProductsComponent } from './components/slider-for-products/slider-for-products.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 // For cookie popup
const cookieConfig: NgcCookieConsentConfig = { const cookieConfig: NgcCookieConsentConfig = {
@@ -89,7 +97,15 @@ const cookieConfig: NgcCookieConsentConfig = {
TopBarComponent, TopBarComponent,
BottomBarComponent, BottomBarComponent,
HotDealsWidgetComponent, HotDealsWidgetComponent,
SliderForProductsComponent SliderForProductsComponent,
RegistrationComponent,
SigninComponent,
CopyrightComponent,
GreetingInfoSliderComponent,
KundenComponent,
AboutUsComponent,
ProfileComponent,
ProfilePageComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -110,6 +126,8 @@ const cookieConfig: NgcCookieConsentConfig = {
RouterModule.forRoot([ RouterModule.forRoot([
{path: '', component: LandingpageComponent}, {path: '', component: LandingpageComponent},
]), ]),
MatCardModule,
ReactiveFormsModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
+8
View File
@@ -9,6 +9,10 @@ import {ProductSearchPageComponent} from './pages/product-search-page/product-se
import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component'; import {PageNotFoundPageComponent} from './pages/page-not-found-page/page-not-found-page.component';
import {ImprintComponent} from './pages/imprint/imprint.component'; import {ImprintComponent} from './pages/imprint/imprint.component';
import {PrivacyComponent} from './pages/privacy/privacy.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 = [ const routes: Routes = [
{path: '', component: LandingpageComponent, pathMatch: 'full'}, {path: '', component: LandingpageComponent, pathMatch: 'full'},
@@ -16,6 +20,10 @@ const routes: Routes = [
{path: 'product/:id', component: ProductDetailPageComponent}, {path: 'product/:id', component: ProductDetailPageComponent},
{path: 'impressum', component: ImprintComponent}, {path: 'impressum', component: ImprintComponent},
{path: 'datenschutz', component: PrivacyComponent}, {path: 'datenschutz', component: PrivacyComponent},
{path: 'signin', component: SigninComponent},
{path: 'registration', component: RegistrationComponent},
{path: 'product-detail', component: ProductDetailPageComponent},
{path: 'profile', component: ProfilePageComponent},
{path: '**', component: PageNotFoundPageComponent} {path: '**', component: PageNotFoundPageComponent}
]; ];
@@ -0,0 +1,19 @@
<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>
@@ -0,0 +1,25 @@
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();
});
});
@@ -0,0 +1,16 @@
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 {
}
}
@@ -0,0 +1,27 @@
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 {
}
@@ -0,0 +1,23 @@
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 {
}
@@ -0,0 +1,93 @@
.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;
}
@@ -0,0 +1,46 @@
<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>
@@ -0,0 +1,54 @@
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();
});
});
@@ -0,0 +1,47 @@
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(['/']);
}
);
}
}
@@ -0,0 +1 @@
<p>resetpasswort works!</p>
@@ -0,0 +1,25 @@
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();
});
});
@@ -0,0 +1,16 @@
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 {
}
}
@@ -0,0 +1,93 @@
.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;
}
@@ -0,0 +1,36 @@
<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>
@@ -0,0 +1,54 @@
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();
});
});
@@ -0,0 +1,54 @@
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;
});
}
}
@@ -7,12 +7,14 @@
} }
.folge-uns-item { .folge-uns-item {
grid-column: 2; grid-row: 1; grid-column: 2;
grid-row: 1;
justify-self: center; justify-self: center;
} }
.link-items { .link-items {
grid-column: 2; grid-row: 2; grid-column: 2;
grid-row: 2;
justify-self: center; justify-self: center;
} }
@@ -29,11 +31,13 @@
} }
.bottom-logo { .bottom-logo {
grid-column: 1; grid-row: 3; grid-column: 1;
grid-row: 3;
} }
.bottom-info { .bottom-info {
grid-column: 3; grid-row: 3; grid-column: 3;
grid-row: 3;
justify-self: right; justify-self: right;
} }
@@ -1,26 +1,31 @@
<div class="bottom-bar-wrapper"> <footer class="footer text-center">
<div class="folge-uns-item"> <div class="container">
<p><span id="folge">FOLGE</span><span id="uns">UNS</span></p> <div class="row">
</div> <!-- Footer Location-->
<div class="link-items"> <div class="col-lg-4 mb-5 mb-lg-0">
<ul style="list-style-type:none" class="footer-links"> <h4 class="text-uppercase mb-4">Location</h4>
<li><a href="https://github.com/Mueller-Patrick/Betterzon">GiT</a></li> <p class="lead mb-0">
<li><a href="https://blog.betterzon.xyz/">BLOG</a></li> 76133 Karlsruhe
<li><a href="https://github.com/Mueller-Patrick/Betterzon/wiki">Wiki</a></li> <br/>
</ul>
</div>
<div id="footer-line">
</p>
</div> </div>
<div class="bottom-logo"> <!-- Footer Social Icons-->
<p><span id="better">BETTER</span><span id="zon">ZON</span></p> <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> </div>
<div class="bottom-info"> <!-- Footer About Text-->
<ul style="list-style-type:none" class="footer-links"> <div class="col-lg-4">
<li><a>DATENSCHUTZERKLÄRUNG</a></li> <h4 class="text-uppercase mb-4">CONTACT US</h4>
<li><a>IMPRESSUM</a></li> <p class="lead mb-0">
</ul> betterzon-contact@mueller-patrick.tech
</p>
</div> </div>
</div> </div>
</div>
</footer>
@@ -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 component: BottomBarComponent;
let fixture: ComponentFixture<BottomBarComponent>; let fixture: ComponentFixture<BottomBarComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ BottomBarComponent ] declarations: [BottomBarComponent]
}) })
.compileComponents(); .compileComponents();
}); });
@@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
@Component({ @Component({
selector: 'app-bottom-bar', selector: 'app-bottom-bar',
templateUrl: "./bottom-bar.component.html", templateUrl: './bottom-bar.component.html',
styleUrls: ["./bottom-bar.component.css"] styleUrls: ['./bottom-bar.component.css']
}) })
export class BottomBarComponent implements OnInit { export class BottomBarComponent implements OnInit {
constructor() { } constructor() {
}
ngOnInit(): void { ngOnInit(): void {
} }
@@ -0,0 +1,24 @@
#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;
}
@@ -0,0 +1,7 @@
<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>
@@ -0,0 +1,25 @@
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();
});
});
@@ -0,0 +1,16 @@
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://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> <a href="https://github.com/Mueller-Patrick/Betterzon/wiki" class="fa fa-wikipedia-w fa-4x icon-3d"></a>
</div> </div>
<div class = "blocks" id="copyright">© COPYRIGHT 2020</div> <div class="blocks" id="copyright">© COPYRIGHT 2020</div>
</div> </div>
<div id="imprintSection"> <div id="imprintSection">
<a href="/impressum" >Imprint</a><br> <a href="/impressum">Imprint</a><br>
<a href="/datenschutz">Privacy Policy</a> <a href="/datenschutz">Privacy Policy</a>
</div> </div>
</footer> </footer>
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
@Component({ @Component({
@@ -11,7 +11,8 @@ export class FooterComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private route: ActivatedRoute private route: ActivatedRoute
) {} ) {
}
ngOnInit(): void { ngOnInit(): void {
} }
@@ -0,0 +1,16 @@
<header class="masthead bg-primary text-white text-center">
<div class="container d-flex align-items-center flex-column">
<!-- Masthead Avatar Image-->
<img class="masthead-avatar mb-5" src="assets/images/Betterzon.svg" alt="..."/>
<!-- Masthead Heading-->
<h1 class="masthead-heading text-uppercase mb-0"></h1>
<!-- 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>
<!-- Masthead Subheading-->
<p class="masthead-subheading font-weight-light mb-0"></p>
</div>
</header>
@@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {GreetingInfoSliderComponent} from './greeting-info-slider.component';
describe('GreetingInfoSliderComponent', () => {
let component: GreetingInfoSliderComponent;
let fixture: ComponentFixture<GreetingInfoSliderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [GreetingInfoSliderComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(GreetingInfoSliderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,16 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-greeting-info-slider',
templateUrl: './greeting-info-slider.component.html',
styleUrls: ['./greeting-info-slider.component.css']
})
export class GreetingInfoSliderComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
@@ -5,7 +5,8 @@
<img src="assets/images/Betterzon.svg" alt="Betterzon Logo" width="50px" (click)="clickedLogo()"> <img src="assets/images/Betterzon.svg" alt="Betterzon Logo" width="50px" (click)="clickedLogo()">
</div> </div>
<div class="searchBox"> <div class="searchBox">
<input *ngIf="showSearch===true" type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()"> <input *ngIf="showSearch===true" type="text" [(ngModel)]="searchInput" placeholder="Search"
(keyup.enter)="startedSearch()">
</div> </div>
<div class="slider"> <div class="slider">
<mat-slide-toggle color="primary">dark me</mat-slide-toggle> <mat-slide-toggle color="primary">dark me</mat-slide-toggle>
@@ -1,72 +1,97 @@
.hot-deal-widget-wrapper{ .bbb_deals_wrapper {
width: 1640px; overflow: hidden;
height: 820px;
background-color: #f8f9fa;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 0px;
grid-row-gap: 0px;
align-items: center;
} }
.product-description { .bbb_deals_wrapper:hover {
/*background-color: #3480E3;*/ transform: scale(1.2);
height: 100%;
display: grid;
grid-template-columns: 15% 16px 15% 16px 15% 16px 15% 16px 15% 16px 15% 8px;
grid-template-rows: repeat(5, 1fr);
} }
.product-image { .bbb_deals_featured {
width: 100%;
} }
#hot-deals{ .bbb_deals {
/*background-color: #E53167;*/ width: 100%;
justify-self: center; margin-right: 7%;
align-self: center; padding-top: 80px;
grid-column: 3/10; padding-left: 25px;
grid-row: 1/2; padding-right: 25px;
padding-bottom: 34px;
box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.1);
border-radius: 5px;
margin-top: 0px
} }
#product-name { .bbb_deals_title {
justify-self: center; position: absolute;
align-self: center; top: 10px;
grid-column: 3/10; left: 22px;
grid-row: 2/3; font-size: 18px;
/*background-color: #E53167;*/ font-weight: 500;
color: #000000
} }
#product-name > p { .bbb_deals_slider_container {
font-size: 65px; width: 100%
} }
#sales { .bbb_deals_item {
justify-self: center; width: 100% !important
align-self: center;
grid-column: 3/10;
grid-row: 3/4;
/*background-color: #E53167;*/
} }
#futher-informations { .bbb_deals_image {
justify-self: center; width: 40%;
align-self: center; height: 40%;
grid-column: 3/10;
grid-row: 4/5;
/*background-color: #E53167;*/
} }
#points { .bbb_deals_image img {
justify-self: center; width: 100%
align-self: start;
grid-column: 3/10;
grid-row: 5/6;
/*background-color: #E53167;*/
} }
.product-image { .bbb_deals_content {
display: grid; margin-top: 33px
justify-content: center;
} }
.bbb_deals_item_category a {
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.5)
}
#bbb_deals_item_price_a {
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.6);
color: red;
}
#bbb_deals_item_price_b {
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.6);
color: green;
}
.bbb_deals_item_name {
max-width: 300px;
word-wrap: break-word;
font-size: 16px;
font-weight: 400;
color: #000000;
}
.bbb_deals_item_price {
font-size: 24px;
font-weight: 500;
color: #228B22;
}
.available {
margin-top: 19px
}
.available_title {
font-size: 16px;
color: rgba(0, 0, 0, 0.5);
font-weight: 400
}
@@ -1,23 +1,41 @@
<div class="hot-deal-widget-wrapper"> <section class="page-section portfolio" id="top-gesuchte">
<div class="product-description"> <div class="container">
<div id="hot-deals"> <!-- Portfolio Section Heading-->
<h1>HOT DEALS</h1> <h2 class="page-section-heading text-center text-uppercase text-secondary mb-0">TOP-OFFERS</h2>
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line"></div>
</div> </div>
<div id="product-name"> <!-- Portfolio Grid Items-->
<h1>Neues Apple iPhone 12 Pro <br> (512 GB) - Graphit</h1> <div class="row justify-content-center">
<!-- Portfolio Item 1-->
<div class="col-md-4 mx-auto my-5" *ngFor="let productId of bestDealsProductIds"
(click)="clickedProduct(productId)">
<div class="bbb_deals_wrapper">
<div class="bbb_deals_image"><img
src="https://www.mueller-patrick.tech/betterzon/images/{{productsPricesMap[productId]?.product?.image_guid}}.jpg"
alt=""></div>
<div class="bbb_deals_content">
<div class="bbb_deals_info_line d-flex flex-row justify-content-start">
<div class="bbb_deals_item_name">{{productsPricesMap[productId]?.product?.name}}</div>
</div> </div>
<div id="sales"> <div class="bbb_deals_info_line d-flex flex-row justify-content-start">
SPARE BIS ZU 7%! <div class="bbb_deals_item_category">Amazon: <span
id="bbb_deals_item_price_a"><strike>{{productsPricesMap[productId]?.amazonPrice?.price_in_cents / 100}}
</strike></span></div>
</div> </div>
<div id="futher-informations"> <div class="bbb_deals_info_line d-flex flex-row justify-content-start">
Weitere Informationen <div class="bbb_deals_item_category">{{productsPricesMap[productId]?.vendor?.name}}: <span
id="bbb_deals_item_price_b">{{productsPricesMap[productId]?.lowestPrice?.price_in_cents / 100}}
</span></div>
</div> </div>
<div id="points"> <div class="available_bar">
points <span style="width:17%"></span>
</div> </div>
</div> </div>
<div class="product-image">
<img src="assets/images/iphone-12-pro-silver-hero.png" height="771">
</div> </div>
</div> </div>
</div>
</div>
</section>
@@ -1,14 +1,47 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import { HotDealsWidgetComponent } from './hot-deals-widget.component'; import {HotDealsWidgetComponent} from './hot-deals-widget.component';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {ApiService} from '../../services/api.service';
import {ActivatedRoute, convertToParamMap, Router} from '@angular/router';
import {Observable, of} from 'rxjs';
class MockApiService extends AbstractMockObservableService {
getBestDeals(): any {
this.content = [];
return this;
}
getProductsByIds(): any {
this.content = [];
return this;
}
}
class ActivatedRouteMock {
public paramMap = of(convertToParamMap({
testId: 'abc123',
anotherId: 'd31e8b48-7309-4c83-9884-4142efdf7271',
}));
}
describe('HotDealsWidgetComponent', () => { describe('HotDealsWidgetComponent', () => {
let component: HotDealsWidgetComponent; let component: HotDealsWidgetComponent;
let fixture: ComponentFixture<HotDealsWidgetComponent>; let fixture: ComponentFixture<HotDealsWidgetComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => { beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ HotDealsWidgetComponent ] declarations: [HotDealsWidgetComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}, {
provide: ActivatedRoute,
useValue: ActivatedRouteMock
}]
}) })
.compileComponents(); .compileComponents();
}); });
@@ -1,4 +1,7 @@
import { Component, OnInit } from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {ApiService} from '../../services/api.service';
import {Product} from '../../models/product';
import {ActivatedRoute, Router} from '@angular/router';
@Component({ @Component({
selector: 'app-hot-deals-widget', selector: 'app-hot-deals-widget',
@@ -7,9 +10,107 @@ import { Component, OnInit } from '@angular/core';
}) })
export class HotDealsWidgetComponent implements OnInit { export class HotDealsWidgetComponent implements OnInit {
constructor() { } products: Product[] = [];
bestDealsProductIds = [];
amazonPrices = [];
productsPricesMap = new Map();
@Input() numberOfProducts: number;
@Input() showProductPicture: boolean;
@Input() searchQuery: string;
@Input() type: string;
ngOnInit(): void { constructor(
private apiService: ApiService,
private router: Router,
private route: ActivatedRoute
) {
} }
ngOnInit(): void {
this.getBestDeals();
}
loadParams(): void {
if (!this.numberOfProducts) {
this.numberOfProducts = 9;
}
if (!this.showProductPicture) {
this.showProductPicture = false;
}
if (!this.searchQuery) {
this.searchQuery = '';
}
if (!this.type) {
this.type = '';
}
switch (this.type) {
case 'search': {
break;
}
default: {
this.getProductsByIds();
this.getAmazonPricesForBestDeals();
this.getVendors();
break;
}
}
}
getProductsByIds(): void {
this.apiService.getProductsByIds(this.bestDealsProductIds).subscribe(
products => {
products.forEach(product => {
this.productsPricesMap [product.product_id].product = product;
});
}
);
}
getBestDeals(): void {
this.apiService.getBestDeals(9).subscribe(
deals => {
deals.forEach(deal => {
this.bestDealsProductIds.push(deal.product_id);
this.productsPricesMap [deal.product_id] = {lowestPrice: deal};
});
this.loadParams();
}
);
}
getVendors(): void {
this.bestDealsProductIds.forEach(
productId => {
const currentDeal = this.productsPricesMap[productId].lowestPrice;
this.apiService.getVendorById(currentDeal.vendor_id).subscribe(
vendor => {
this.productsPricesMap[productId].vendor = vendor;
});
});
}
getAmazonPricesForBestDeals(): void {
this.bestDealsProductIds.forEach(id => {
this.apiService.getAmazonPrice(id).subscribe(
price => {
this.amazonPrices.push(price);
this.productsPricesMap[price[0].product_id].amazonPrice = price[0];
}
);
}
);
}
getSearchedProducts(): void {
this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => this.products = products);
}
clickedProduct(productId: string): void {
this.router.navigate([('/product/' + productId)]);
}
} }
@@ -0,0 +1,41 @@
<section class="page-section portfolio" id="unsere-kunden">
<div class="container">
<!-- Portfolio Section Heading-->
<h2 class="page-section-heading text-center text-uppercase text-secondary mb-0">THEY TRUST US</h2>
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line"></div>
</div>
<!-- Portfolio Grid Items-->
<div class="row justify-content-center">
<!-- Portfolio Item 1-->
<div class="col-md-6 col-lg-4 mb-5">
<div class="portfolio-item mx-auto" data-bs-toggle="modal" data-bs-target="#portfolioModal1">
<div class="portfolio-item-caption d-flex align-items-center justify-content-center h-100 w-100">
<div class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img width="100%" class="productImage" src="assets/images/cropped-unknown-1-1.png"/>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-5">
<div class="portfolio-item mx-auto" data-bs-toggle="modal" data-bs-target="#portfolioModal1">
<div class="portfolio-item-caption d-flex align-items-center justify-content-center h-100 w-100">
<div class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img width="100%" class="productImage" src="assets/images/plantshub.jpg"/>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-5">
<div class="portfolio-item mx-auto" data-bs-toggle="modal" data-bs-target="#portfolioModal1">
<div class="portfolio-item-caption d-flex align-items-center justify-content-center h-100 w-100">
<div class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img width="70%" class="productImage" src="assets/images/CeangalLogo.png"/>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,53 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {KundenComponent} from './kunden.component';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {ApiService} from '../../services/api.service';
import {ActivatedRoute, convertToParamMap, Router} from '@angular/router';
import {of} from 'rxjs';
class MockApiService extends AbstractMockObservableService {
getProducts(): any {
this.content = [];
return this;
}
}
class ActivatedRouteMock {
public paramMap = of(convertToParamMap({
testId: 'abc123',
anotherId: 'd31e8b48-7309-4c83-9884-4142efdf7271',
}));
}
describe('KundenComponent', () => {
let component: KundenComponent;
let fixture: ComponentFixture<KundenComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [KundenComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}, {
provide: ActivatedRoute,
useValue: ActivatedRouteMock
}]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KundenComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,67 @@
import {Component, Input, OnInit} from '@angular/core';
import {ApiService} from '../../services/api.service';
import {Product} from '../../models/product';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-kunden',
templateUrl: './kunden.component.html',
styleUrls: ['./kunden.component.css']
})
export class KundenComponent implements OnInit {
products: Product[] = [];
@Input() numberOfProducts: number;
@Input() showProductPicture: boolean;
@Input() searchQuery: string;
@Input() type: string;
constructor(
private apiService: ApiService,
private router: Router,
private route: ActivatedRoute
) {
}
ngOnInit(): void {
this.loadParams();
}
loadParams(): void {
if (!this.numberOfProducts) {
this.numberOfProducts = 10;
}
if (!this.showProductPicture) {
this.showProductPicture = false;
}
if (!this.searchQuery) {
this.searchQuery = '';
}
if (!this.type) {
this.type = '';
}
switch (this.type) {
case 'search': {
this.getSearchedProducts();
break;
}
default: {
this.getProducts();
break;
}
}
}
getProducts(): void {
this.apiService.getProducts().subscribe(products => this.products = products);
}
getSearchedProducts(): void {
this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => this.products = products);
}
clickedProduct(product: Product): void {
this.router.navigate([('/product/' + product.product_id)]);
}
}
@@ -1,3 +1,6 @@
<header class="masthead bg-transparent text-white text-center" id="w1">
</header>
<div class="productItem"> <div class="productItem">
<div class="productImageContainer"> <div class="productImageContainer">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/> <img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
@@ -20,8 +23,12 @@
{{product?.short_description}} {{product?.short_description}}
</div> </div>
</div> </div>
<div class="priceAlarm"> <div class="priceAlarm" *ngIf="!isLoggedIn" routerLink="/signin">
Set Price Alarm Login to set a price alarm
</div>
<div class="priceAlarm" *ngIf="isLoggedIn">
<input type="search" id="s" name="price" [(ngModel)]="price">
<div (click)="setPriceAlarm()">Set Price Alarm</div>
</div> </div>
<div class="bestPriceContainer"> <div class="bestPriceContainer">
<div class="bestPrice"> <div class="bestPrice">
@@ -58,6 +58,11 @@ class MockApiService extends AbstractMockObservableService {
this.content = [vendor]; this.content = [vendor];
return this; return this;
} }
getSessionInfoFromLocalStorage(): any {
this.content = [];
return this;
}
} }
describe('ProductDetailsComponent', () => { describe('ProductDetailsComponent', () => {
@@ -34,6 +34,8 @@ export class ProductDetailsComponent implements OnInit {
vendorMap = {}; vendorMap = {};
@ViewChild('chart') chart: ChartComponent; @ViewChild('chart') chart: ChartComponent;
public chartOptions: ChartOptions; public chartOptions: ChartOptions;
isLoggedIn: boolean;
price: any;
constructor( constructor(
private apiService: ApiService private apiService: ApiService
@@ -44,10 +46,15 @@ export class ProductDetailsComponent implements OnInit {
this.getProduct(); this.getProduct();
this.getVendors(); this.getVendors();
this.getPrices(); this.getPrices();
if (this.apiService.getSessionInfoFromLocalStorage().session_id != '') {
this.isLoggedIn = true;
}
} }
getProduct(): void { getProduct(): void {
this.apiService.getProduct(this.productId).subscribe(product => {this.product = product}); this.apiService.getProduct(this.productId).subscribe(product => {
this.product = product;
});
} }
getPrices(): void { getPrices(): void {
@@ -115,4 +122,12 @@ export class ProductDetailsComponent implements OnInit {
return Math.round(percentage); return Math.round(percentage);
} }
setPriceAlarm(): void {
this.apiService.createPriceAlarms(this.productId, this.price * 100).subscribe(
alarms => console.log(alarms)
);
}
} }
@@ -1,7 +1,37 @@
<div *ngIf="products.length==0"> <div *ngIf="products.length==0">
No Products found! No Products found!
</div> </div>
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)"> <div class="container mt-5 mb-5">
<div class="d-flex justify-content-center row">
<div class="col-md-10">
<div class="row p-2 bg-white border rounded" *ngFor="let product of products">
<div class="col-md-3 mt-1"><img width="50%" class="img-fluid img-responsive rounded product-image"
src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg">
</div>
<div class="col-md-6 mt-1">
<h5>{{product.name}}</h5>
<div class="d-flex flex-row">
<p class="text-justify text-truncate para mb-0">{{product.short_description}}</p>
</div>
<div class="mt-1 mb-1 spec-1"><span></span><span class="dot"></span><span></span><span
class="dot"></span><span><br></span></div>
<div class="mt-1 mb-1 spec-1"><span></span><span class="dot"></span><span></span><span
class="dot"></span><span><br></span></div>
</div>
<div class="align-items-center align-content-center col-md-3 border-left mt-1">
<div class="d-flex flex-row align-items-center">
<h4 class="mr-1">${{pricesMap[product.product_id]?.price_in_cents / 100}}</h4>
</div>
<div class="d-flex flex-column mt-4">
<button class="btn btn-primary btn-sm" type="button" (click)="clickedProduct(product)">Details
</button>
</div>
</div>
</div>
</div>
</div>
<!--
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
<div class="productImageContainer" *ngIf="showProductPicture===true"> <div class="productImageContainer" *ngIf="showProductPicture===true">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/> <img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
</div> </div>
@@ -19,4 +49,5 @@
{{product.short_description}} {{product.short_description}}
</div> </div>
</div> </div>
</div> </div>
-->
@@ -70,7 +70,8 @@ describe('ProductListComponent', () => {
last_modified: new Date(), last_modified: new Date(),
manufacturer_id: 1, manufacturer_id: 1,
selling_rank: '1', selling_rank: '1',
category_id: 1 category_id: 1,
price: 0
}; };
component.clickedProduct(product); component.clickedProduct(product);
@@ -10,6 +10,7 @@ import {ActivatedRoute, Router} from '@angular/router';
}) })
export class ProductListComponent implements OnInit { export class ProductListComponent implements OnInit {
products: Product[] = []; products: Product[] = [];
pricesMap: any = {};
@Input() numberOfProducts: number; @Input() numberOfProducts: number;
@Input() showProductPicture: boolean; @Input() showProductPicture: boolean;
@Input() searchQuery: string; @Input() searchQuery: string;
@@ -53,15 +54,35 @@ export class ProductListComponent implements OnInit {
} }
getProducts(): void { getProducts(): void {
this.apiService.getProducts().subscribe(products => this.products = products); this.apiService.getProducts().subscribe(products => {
this.products = products;
this.getPrices();
});
} }
getPrices(): void {
this.products.forEach(
product => {
this.apiService.getLowestPrices(product.product_id).subscribe(
prices => {
this.pricesMap[product.product_id] = prices[prices.length - 1];
}
);
}
);
}
getSearchedProducts(): void { getSearchedProducts(): void {
this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => this.products = products); this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => {
this.products = products;
this.getPrices();
});
} }
clickedProduct(product: Product): void { clickedProduct(product: Product): void {
this.router.navigate([('/product/' + product.product_id)]); this.router.navigate([('/product/' + product.product_id)]);
} }
} }
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import { SliderForProductsComponent } from './slider-for-products.component'; import {SliderForProductsComponent} from './slider-for-products.component';
describe('SliderForProductsComponent', () => { describe('SliderForProductsComponent', () => {
let component: SliderForProductsComponent; let component: SliderForProductsComponent;
@@ -8,7 +8,7 @@ describe('SliderForProductsComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ SliderForProductsComponent ] declarations: [SliderForProductsComponent]
}) })
.compileComponents(); .compileComponents();
}); });
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
@Component({ @Component({
selector: 'app-slider-for-products', selector: 'app-slider-for-products',
@@ -7,7 +7,8 @@ import { Component, OnInit } from '@angular/core';
}) })
export class SliderForProductsComponent implements OnInit { export class SliderForProductsComponent implements OnInit {
constructor() { } constructor() {
}
ngOnInit(): void { ngOnInit(): void {
} }
@@ -1,54 +0,0 @@
.top-bar-wrapper {
display: grid;
grid-template-columns: 200px 360px 820px 20px 250px;
grid-template-rows: 40px;
grid-column-gap: 0px;
grid-row-gap: 0px;
align-items: center;
}
.top-logo {
grid-area: 1/1;
}
#better {
font-size: 28px;
font-weight: bold;
color: #3480E3;
}
#zon {
font-size: 28px;
font-weight: bold;
color: #E53167;
}
.search-button {
/*background-color: #E53167;*/
}
.sign-up {
/*background-color: #E53167;*/
margin-left: 50px;
margin-right: 25px;
}
.login {
margin-right: 25px;
}
#signin {
border-radius: 25px;
background-color: #E53167;
}
._links > a {
/*background-color: #E53167;*/
margin-left: 10px;
margin-right: 10px;
}
._signing_links > a {
/*background-color: #E53167;*/
margin-left: 50px;
}
@@ -1,26 +1,35 @@
<div class="top-bar-wrapper"> <nav class="navbar navbar-expand-lg bg-secondary text-uppercase fixed-top" id="mainNav">
<div class="<top-logo>"> <div class="container">
<a><span id="better">BETTER</span><span id="zon">ZON</span></a> <a class="navbar-brand" routerLink=""> Betterzon</a>
<div class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"
(keyup.enter)="getSearchedProducts()" [(ngModel)]="searchQuery">
</div> </div>
<div class="links"> <button class="navbar-toggler text-uppercase font-weight-bold bg-primary text-white rounded" type="button"
<nav class="_links"> data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive"
<a>KONTAKTIERE UNS</a> aria-expanded="false" aria-label="Toggle navigation">
<a>KUNDEN</a> Menu
<a>FAQ</a> <i class="fas fa-bars"></i>
</nav> </button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded" href="#top-gesuchte">top-offers</a>
</li>
<li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded" href="#about">about</a>
</li>
<li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded" href="#unsere-kunden">our
clients</a></li>
<li class="nav-item mx-0 mx-lg-1" *ngIf="!isLoggedIn"><a class="nav-link py-3 px-0 px-lg-3 rounded"
routerLink="/signin">sign in</a></li>
<li class="nav-item mx-0 mx-lg-1" *ngIf="!isLoggedIn"><a class="nav-link py-3 px-0 px-lg-3 rounded"
routerLink="/registration">sign up</a></li>
<li class="nav-item mx-0 mx-lg-1" *ngIf="isLoggedIn"><a class="nav-link py-3 px-0 px-lg-3 rounded"
routerLink="" (click)="logout()">log out</a>
</li>
<li class="nav-item mx-0 mx-lg-1" *ngIf="isLoggedIn"><a class="nav-link py-3 px-0 px-lg-3 rounded"
routerLink="/profile">profile</a></li>
</ul>
</div> </div>
<div class="footer_space"> </div>
</nav>
</div>
<div class="search-button">
<a>
<img src="assets/images/search_black_24dp.svg" alt="Sarch button">
</a>
</div>
<div class="links">
<nav class="_signing_links">
<a>SIGN UP</a>
<a><span id="signin">SIGN IN</span></a>
</nav>
</div>
</div>
@@ -1,14 +1,37 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import { TopBarComponent } from './top-bar.component'; import {TopBarComponent} from './top-bar.component';
import {FormBuilder} from '@angular/forms';
import {ApiService} from '../../services/api.service';
import {Router} from '@angular/router';
import {AbstractMockObservableService} from '../../mocks/mock.service';
class MockApiService extends AbstractMockObservableService {
getUserInfo(): any {
this.content = [];
return this;
}
getSessionInfoFromLocalStorage(): any {
this.content = [];
return this;
}
}
describe('TopBarComponent', () => { describe('TopBarComponent', () => {
let component: TopBarComponent; let component: TopBarComponent;
let fixture: ComponentFixture<TopBarComponent>; let fixture: ComponentFixture<TopBarComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => { beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ TopBarComponent ] declarations: [TopBarComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}]
}) })
.compileComponents(); .compileComponents();
}); });
@@ -1,4 +1,7 @@
import { Component, OnInit } from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {ApiService} from '../../services/api.service';
import {NavigationEnd, Router} from '@angular/router';
@Component({ @Component({
selector: 'app-top-bar', selector: 'app-top-bar',
@@ -8,10 +11,42 @@ import { Component, OnInit } from '@angular/core';
export class TopBarComponent implements OnInit { export class TopBarComponent implements OnInit {
sidenav: any; sidenav: any;
isLoggedIn: boolean;
searchQuery: string;
constructor() { } constructor(
private api: ApiService,
ngOnInit(): void { private router: Router
) {
} }
ngOnInit(): void {
this.api.getUserInfo().subscribe(data => {
console.log(data);
});
if (this.api.getSessionInfoFromLocalStorage().session_id !== '') {
this.isLoggedIn = true;
}
}
logout(): void {
localStorage.setItem('session_id', '');
localStorage.setItem('session_key', '');
if (this.router.url === '/profile') {
this.router.navigate(['/']);
} else {
window.location.reload();
}
}
getSearchedProducts(): void {
console.log(this.searchQuery);
this.redirectTo('/search', {queryParams: {q: this.searchQuery}});
}
redirectTo(uri: string, queryParams: object): void {
this.router.navigateByUrl('/', {skipLocationChange: true}).then(() =>
this.router.navigate([uri], queryParams));
}
} }
+4
View File
@@ -0,0 +1,4 @@
export interface Category {
category_id: number;
name: string;
}
+9
View File
@@ -0,0 +1,9 @@
export interface ContactPerson {
contact_person_id: number;
first_name: string;
last_name: string;
gender: string;
email: string;
phone: string;
vendor_id: number;
}
@@ -0,0 +1,7 @@
export interface CrawlingStatus {
process_id: number;
started_timestamp: Date;
combinations_to_crawl: number;
successful_crawls: number;
failed_crawls: number;
}
+5
View File
@@ -0,0 +1,5 @@
export interface FavoriteShop {
favorite_id: number;
vendor_id: number;
user_id: number;
}
+4
View File
@@ -0,0 +1,4 @@
export interface Manufacturer {
manufacturer_id: number;
name: string;
}
+21
View File
@@ -5,3 +5,24 @@ export interface Price {
price_in_cents: number; price_in_cents: number;
timestamp: Date; 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;
}
}

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