Compare commits

..

30 Commits

Author SHA1 Message Date
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
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
Paddy b1db97af87 Some fancy changes 2021-06-18 10:28:25 +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 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 45acbfd9a2 BETTERZON-151: Adding option to delete price alarm (#94) 2021-06-17 17:15:06 +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
Paddy 0cd1213c40 BETTERZON-146: Session handling rewrite HOTFIX 2 2021-06-15 12:09:04 +02:00
Paddy 1fd115c2a2 BETTERZON-146: Session handling rewrite HOTFIX 2021-06-15 11:50:36 +02:00
104 changed files with 4265 additions and 2809 deletions
@@ -25,8 +25,8 @@ crawlingstatusRouter.get('/', 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 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) {
@@ -24,8 +24,8 @@ favoriteshopsRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id;
const session_key = req.params.session_key;
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);
@@ -76,8 +76,8 @@ favoriteshopsRouter.delete('/:id', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id;
const session_key = req.params.session_key;
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
@@ -24,8 +24,8 @@ pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id;
const session_key = req.params.session_key;
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);
@@ -106,3 +106,29 @@ pricealarmsRouter.put('/', async (req: Request, res: Response) => {
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// DELETE pricealarms/:id
pricealarmsRouter.delete('/:id', async (req, res) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = (req.query.session_id ?? '').toString();
const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip);
const id: number = parseInt(req.params.id, 10);
const success = await PriceAlarmsService.deletePriceAlarm(id, user.user_id);
if (success) {
res.status(200).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@@ -92,3 +92,24 @@ export const updatePriceAlarm = async (alarm_id: number, user_id: number, define
}
}
};
/**
* Deletes the given price alarm
* @param alarm_id The id of the price alarm to update
* @param user_id The id of the user that wants to update the price alarm
*/
export const deletePriceAlarm = async (alarm_id: number, user_id: number): Promise<boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('DELETE FROM price_alarms WHERE alarm_id = ? AND user_id = ?', [alarm_id, user_id]);
return res.affectedRows === 1;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
+2 -2
View File
@@ -282,11 +282,11 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
'price_in_cents': lowestPrice.price_in_cents,
'timestamp': lowestPrice.timestamp,
'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents),
'amazonDifferencePercent': ((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
if (deal.amazonDifferencePercent > 0) {
if (deal.amazonDifferencePercent > 0 && deal.amazonDifference > 0) {
deals.push(deal as Deal);
}
}
+2 -2
View File
@@ -37,8 +37,8 @@ vendorsRouter.get('/managed', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id;
const session_key = req.params.session_key;
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);
+5 -3
View File
@@ -8,14 +8,16 @@ import stepdefs.Preconditions;
@RunWith(Cucumber.class)
@CucumberOptions(
features = {"src/test/resource/searchProduct.feature",
"src/test/resource/priceAlarms.feature"}
features = {"src/test/resource/searchProduct.feature",
"src/test/resource/priceAlarms.feature",
"src/test/resource/favoriteShopList.feature",
"src/test/resource/manageVendor.feature"}
)
public class RunTest {
@BeforeClass
public static void setup() {
Preconditions.driver= new FirefoxDriver();
Preconditions.driver = new FirefoxDriver();
}
@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 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.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.util.List;
public class PriceAlarm {
@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$")
public void the_user_clicks_on_the_profile_icon() throws Exception {
}
@Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception {
}
@When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception {
}
@Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception {
WebElement profileButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'profile')]")));
profileButton.click();
}
@Then("^the price alarm list should contain at least (\\d+) entry$")
public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception {
WebElement alarmEntry = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-hover tr:nth-child(2)")));
assert (alarmEntry != null);
}
@Then("^the price alarm list should contain a maximum of (\\d+) entries per page$")
public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception {
}
@Given("^the user is on the profile page$")
public void the_user_is_on_the_profile_page() throws Exception {
Preconditions.driver.get("https://www.betterzon.xyz/profile");
@Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception {
WebElement profile_info_text = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-user-information")));
assert (profile_info_text.isDisplayed());
}
@When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$")
public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception {
}
if (arg1.equals("remove")) {
WebElement entry = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("table.table.table-hover tr:nth-child(2)")));
@Then("^a popup should open asking the user to confirm the removal$")
public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception {
}
if (entry == null) {
throw new Exception("Too few price alarm entries found!");
}
@When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
WebElement btn = entry.findElement(By.cssSelector("img.delete[src='../assets/images/Delete_icon-icons.com_55931.png']"));
btn.click();
} else if (arg1.equals("edit")) {
}
}
@Then("^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.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
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 {
//throw new PendingException();
Preconditions.driver.get("https://betterzon.xyz");
WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo")));
WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("a.navbar-brand")));
}
@When("^the user enters the search term \"([^\"]*)\" and clicks search$")
@@ -24,49 +22,99 @@ public class SearchProduct {
WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid"));
searchField.sendKeys(searchTerm);
searchField.sendKeys(Keys.ENTER);
WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo")));
WebElement logo = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".navbar-brand")));
}
@Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception {
WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".ng-star-inserted")));
assert(noProdsFoundMsg.getText().contains("No Products found!"));
WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'No Products found!')]")));
assert (noProdsFoundMsg.isDisplayed());
}
@Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception {
try {
WebElement logoutButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'log out')]")));
logoutButton.click();
} catch (TimeoutException e) {
}
}
@Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception {
try {
WebElement loginButton = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.xpath("//*[contains(text(),'sign in')]")));
loginButton.click();
WebElement usernameField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.id("username")));
usernameField.sendKeys("Selenium");
WebElement passwordField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.id("password")));
passwordField.sendKeys("Selenium");
WebElement loginBtn = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.className("btn_signin")));
loginBtn.click();
} catch (TimeoutException e) {
}
}
@Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception {
WebElement product = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".productItem.ng-star-inserted")));
assert(product.isDisplayed());
WebElement product = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".row.p-2.bg-white.border.rounded")));
assert (product.isDisplayed());
}
@When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception {
WebElement productDetailsBtn = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".row.p-2.bg-white.border.rounded button.btn.btn-primary.btn-sm")));
productDetailsBtn.click();
}
@Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception {
WebElement productTitle = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.productTitle")));
assert (productTitle.isDisplayed());
}
@Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
WebElement alarmBox = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm")));
if (arg0.equals("Login to set a price alarm")) {
assert (alarmBox.getText().equals("Login to set a price alarm"));
} else {
assert (alarmBox.isDisplayed());
}
}
@When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception {
WebElement alarmBoxField = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm input")));
alarmBoxField.sendKeys("12345");
WebElement alarmBox = (new WebDriverWait(Preconditions.driver, Preconditions.delaySeconds))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("div.priceAlarm")));
alarmBox.click();
assert (alarmBox.isDisplayed() && alarmBoxField.isDisplayed());
}
@Then("^the user should receive an email confirming the price alarm$")
public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
assert (true);
}
}
@@ -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 has at least 1 price alarm set
When the user clicks on the profile icon
Then the profile details popup should open
When the user clicks on price alarms
Then the price alarm list should open
Then the profile page should open
And the price alarm list should contain at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page
Scenario: Remove a price alarm
Given the user is on the price alarm list page
Given the user is on the landing page
And the user is logged in
When the user clicks on the profile icon
Then the profile page should open
When the user clicks on the "remove" button next to a price alarm
Then a popup should open asking the user to confirm the removal
When the user confirms the removal of the price alarm
Then the price alarm should be removed from the database
Scenario: Edit a price alarm
Given the user is on the price alarm list page
Given the user is on the landing page
And the user is logged in
When the user clicks on the profile icon
Then the profile page should open
When the user clicks on the "edit" button next to a price alarm
Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button
@@ -12,7 +12,7 @@ Feature: Search a Product
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "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
Given the user is on the landing page
+31 -28
View File
@@ -2,32 +2,35 @@
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-firefox-launcher'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/Betterzon'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Firefox'],
singleRun: false,
restartOnFileChange: true
});
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-firefox-launcher'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
random: false
}
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/Betterzon'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Firefox'],
singleRun: false,
restartOnFileChange: true
});
};
+3 -2
View File
@@ -1,9 +1,10 @@
.wrapper_app {
padding-bottom: 2.5rem; /* Footer height */
padding-bottom: 2.5rem; /* Footer height */
}
.footer_app {
position: relative;
bottom: 0;
width: 100%;
height: 2.5rem; /* Footer height */
height: 2.5rem; /* Footer height */
}
+3 -3
View File
@@ -1,8 +1,8 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
import {RouterTestingModule} from "@angular/router/testing";
import {NgcCookieConsentConfig, NgcCookieConsentModule} from "ngx-cookieconsent";
import {FormsModule} from "@angular/forms";
import {RouterTestingModule} from '@angular/router/testing';
import {NgcCookieConsentConfig, NgcCookieConsentModule} from 'ngx-cookieconsent';
import {FormsModule} from '@angular/forms';
// For cookie consent module testing
const cookieConfig: NgcCookieConsentConfig = {
+2 -3
View File
@@ -1,7 +1,7 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {NgcCookieConsentService, NgcInitializeEvent, NgcNoCookieLawEvent, NgcStatusChangeEvent} from 'ngx-cookieconsent';
import {Subscription} from 'rxjs';
import {ApiService} from "./services/api.service";
import {ApiService} from './services/api.service';
@Component({
@@ -26,8 +26,7 @@ export class AppComponent implements OnInit, OnDestroy {
username?: string;
constructor(
private ccService: NgcCookieConsentService,
private api: ApiService
private ccService: NgcCookieConsentService
) {
}
+13 -13
View File
@@ -23,23 +23,23 @@ import {NgcCookieConsentModule, NgcCookieConsentConfig} from 'ngx-cookieconsent'
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {TopBarComponent} from './components/top-bar/top-bar.component';
import {RouterModule} from '@angular/router';
import {MatButtonModule} from "@angular/material/button";
import {MatButtonModule} from '@angular/material/button';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatListModule} from "@angular/material/list";
import {MatListModule} from '@angular/material/list';
import {BottomBarComponent} from './components/bottom-bar/bottom-bar.component';
import { HotDealsWidgetComponent } from './components/hot-deals-widget/hot-deals-widget.component';
import { SliderForProductsComponent } from './components/slider-for-products/slider-for-products.component';
import { RegistrationComponent } from './components/auth/registration/registration.component';
import { MatCardModule } from "@angular/material/card";
import {SigninComponent} from "./components/auth/signin/signin.component";
import { CopyrightComponent } from './components/copyright/copyright.component';
import { GreetingInfoSliderComponent } from './components/greeting-info-slider/greeting-info-slider.component';
import { KundenComponent } from './components/kunden/kunden.component';
import { AboutUsComponent } from './components/about-us/about-us.component';
import { ProfileComponent } from './components/profile/profile.component';
import { ProfilePageComponent } from './pages/profile-page/profile-page.component';
import {HotDealsWidgetComponent} from './components/hot-deals-widget/hot-deals-widget.component';
import {SliderForProductsComponent} from './components/slider-for-products/slider-for-products.component';
import {RegistrationComponent} from './components/auth/registration/registration.component';
import {MatCardModule} from '@angular/material/card';
import {SigninComponent} from './components/auth/signin/signin.component';
import {CopyrightComponent} from './components/copyright/copyright.component';
import {GreetingInfoSliderComponent} from './components/greeting-info-slider/greeting-info-slider.component';
import {KundenComponent} from './components/kunden/kunden.component';
import {AboutUsComponent} from './components/about-us/about-us.component';
import {ProfileComponent} from './pages/profile/profile.component';
import {ProfilePageComponent} from './pages/profile-page/profile-page.component';
// For cookie popup
const cookieConfig: NgcCookieConsentConfig = {
+6 -6
View File
@@ -9,10 +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 {ImprintComponent} from './pages/imprint/imprint.component';
import {PrivacyComponent} from './pages/privacy/privacy.component';
import {SigninComponent} from "./components/auth/signin/signin.component";
import {RegistrationComponent} from "./components/auth/registration/registration.component";
import {ProfileComponent} from "./components/profile/profile.component";
import {ProfilePageComponent} from "./pages/profile-page/profile-page.component";
import {SigninComponent} from './components/auth/signin/signin.component';
import {RegistrationComponent} from './components/auth/registration/registration.component';
import {ProfileComponent} from './pages/profile/profile.component';
import {ProfilePageComponent} from './pages/profile-page/profile-page.component';
const routes: Routes = [
{path: '', component: LandingpageComponent, pathMatch: 'full'},
@@ -22,8 +22,8 @@ const routes: Routes = [
{path: 'datenschutz', component: PrivacyComponent},
{path: 'signin', component: SigninComponent},
{path: 'registration', component: RegistrationComponent},
{path: "product-detail", component: ProductDetailPageComponent},
{path: "profile", component: ProfilePageComponent},
{path: 'product-detail', component: ProductDetailPageComponent},
{path: 'profile', component: ProfilePageComponent},
{path: '**', component: PageNotFoundPageComponent}
];
@@ -10,8 +10,10 @@
</div>
<!-- About Section Content-->
<div class="row">
<div class="col-lg-4 ms-auto"><p class="lead">text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries</p></div>
<div class="col-lg-4 me-auto"><p class="lead">text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries</p></div>
<div class="col-lg-4 ms-auto"><p class="lead">You follow the same passion as we do and you want to find
alternatives to the de-facto monopolist Amazon?</p></div>
<div class="col-lg-4 me-auto"><p class="lead">In this case, welcome aboard! Were happy that you share our
passion and hope that we can help you achieving this goal with the website.</p></div>
</div>
</div>
</section>
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { AboutUsComponent } from './about-us.component';
import {AboutUsComponent} from './about-us.component';
describe('AboutUsComponent', () => {
let component: AboutUsComponent;
let fixture: ComponentFixture<AboutUsComponent>;
let component: AboutUsComponent;
let fixture: ComponentFixture<AboutUsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AboutUsComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AboutUsComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AboutUsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(AboutUsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-about-us',
templateUrl: './about-us.component.html',
styleUrls: ['./about-us.component.css']
selector: 'app-about-us',
templateUrl: './about-us.component.html',
styleUrls: ['./about-us.component.css']
})
export class AboutUsComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -1,8 +1,8 @@
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";
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 = [
{
@@ -20,7 +20,8 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule { }
export class AuthRoutingModule {
}
+18 -17
View File
@@ -1,22 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
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';
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,
],
declarations: [SigninComponent, RegistrationComponent, ResetpasswortComponent],
imports: [
CommonModule,
AuthRoutingModule,
],
exports: [
SigninComponent,
RegistrationComponent,
ResetpasswortComponent,
],
})
export class AuthModule { }
export class AuthModule {
}
@@ -1,11 +1,12 @@
.main-content{
.main-content {
width: 50%;
border-radius: 20px;
box-shadow: 0 5px 5px rgba(0,0,0,.4);
box-shadow: 0 5px 5px rgba(0, 0, 0, .4);
margin: 5em auto;
display: flex;
}
.company__info{
.company__info {
background-color: #008080;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
@@ -14,63 +15,79 @@
justify-content: center;
color: #fff;
}
.fa-android{
font-size:3em;
.fa-android {
font-size: 3em;
}
@media screen and (max-width: 640px) {
.main-content{width: 90%;}
.company__info{
.main-content {
width: 90%;
}
.company__info {
display: none;
}
.login_form{
border-top-left-radius:20px;
border-bottom-left-radius:20px;
.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%;}
@media screen and (min-width: 642px) and (max-width: 800px) {
.main-content {
width: 70%;
}
}
.row > h2{
color:#008080;
.row > h2 {
color: #008080;
}
.login_form{
.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;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
}
form{
form {
padding: 0 2em;
}
.form__input{
.form__input {
width: 100%;
border:0px solid transparent;
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;
outline: none;
margin: 1.5em auto;
transition: all .5s ease;
}
.form__input:focus{
.form__input:focus {
border-bottom-color: #008080;
box-shadow: 0 0 5px rgba(0,80,80,.4);
box-shadow: 0 0 5px rgba(0, 80, 80, .4);
border-radius: 4px;
}
.btn_signin{
.btn_signin {
transition: all .5s ease;
width: 100%;
border-radius: 30px;
color:#008080;
color: #008080;
font-weight: 600;
background-color: #fff;
border: 1px solid #008080;
margin-top: 1.5em;
margin-bottom: 1em;
}
.btn_signin:hover, .btn:focus{
.btn_signin:hover, .btn:focus {
background-color: #008080;
color:#fff;
color: #fff;
}
@@ -6,33 +6,36 @@
<div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid">
<div class="row">
<h2>Konto erstellen</h2>
<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">
<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">
<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="Kennwort">
<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="Erstellen" class="btn_signin">
<input type="submit" value="Sign up" class="btn_signin">
</div>
<div class="row">
<a href="/signin">Sich anmelden</a>
<p>Have an account?<a href="/signin">Log In</a></p>
</div>
</form>
</div>
@@ -1,25 +1,54 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { RegistrationComponent } from './registration.component';
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 component: RegistrationComponent;
let fixture: ComponentFixture<RegistrationComponent>;
let mockService;
let formBuilder: FormBuilder;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RegistrationComponent ]
})
.compileComponents();
});
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;
fixture.detectChanges();
});
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();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,44 +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";
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']
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
) { }
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)]
],
});
}
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; }
get me() {
return this.form.controls;
}
onSubmit() {
this.api.registerUser(this.form.value.username, this.form.value.password, this.form.value.email).subscribe(
res=> {
this.api.saveSessionInfoToLocalStorage(res);
this.router.navigate(['/']);
}
);
}
onSubmit(): void {
this.api.registerUser(this.form.value.username, this.form.value.password, this.form.value.email).subscribe(
res => {
this.api.saveSessionInfoToLocalStorage(res);
this.router.navigate(['/']);
}
);
}
}
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { ResetpasswortComponent } from './resetpasswort.component';
import {ResetpasswortComponent} from './resetpasswort.component';
describe('ResetpasswortComponent', () => {
let component: ResetpasswortComponent;
let fixture: ComponentFixture<ResetpasswortComponent>;
let component: ResetpasswortComponent;
let fixture: ComponentFixture<ResetpasswortComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ResetpasswortComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ResetpasswortComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ResetpasswortComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ResetpasswortComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-resetpasswort',
templateUrl: './resetpasswort.component.html',
styleUrls: ['./resetpasswort.component.css']
selector: 'app-resetpasswort',
templateUrl: './resetpasswort.component.html',
styleUrls: ['./resetpasswort.component.css']
})
export class ResetpasswortComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -1,11 +1,12 @@
.main-content{
.main-content {
width: 50%;
border-radius: 20px;
box-shadow: 0 5px 5px rgba(0,0,0,.4);
box-shadow: 0 5px 5px rgba(0, 0, 0, .4);
margin: 5em auto;
display: flex;
}
.company__info{
.company__info {
background-color: #008080;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
@@ -14,63 +15,79 @@
justify-content: center;
color: #fff;
}
.fa-android{
font-size:3em;
.fa-android {
font-size: 3em;
}
@media screen and (max-width: 640px) {
.main-content{width: 90%;}
.company__info{
.main-content {
width: 90%;
}
.company__info {
display: none;
}
.login_form{
border-top-left-radius:20px;
border-bottom-left-radius:20px;
.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%;}
@media screen and (min-width: 642px) and (max-width: 800px) {
.main-content {
width: 70%;
}
}
.row > h2{
color:#008080;
.row > h2 {
color: #008080;
}
.login_form{
.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;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
}
form{
form {
padding: 0 2em;
}
.form__input{
.form__input {
width: 100%;
border:0px solid transparent;
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;
outline: none;
margin: 1.5em auto;
transition: all .5s ease;
}
.form__input:focus{
.form__input:focus {
border-bottom-color: #008080;
box-shadow: 0 0 5px rgba(0,80,80,.4);
box-shadow: 0 0 5px rgba(0, 80, 80, .4);
border-radius: 4px;
}
.btn_signin{
.btn_signin {
transition: all .5s ease;
width: 100%;
border-radius: 30px;
color:#008080;
color: #008080;
font-weight: 600;
background-color: #fff;
border: 1px solid #008080;
margin-top: 1.5em;
margin-bottom: 1em;
}
.btn_signin:hover, .btn:focus{
.btn_signin:hover, .btn:focus {
background-color: #008080;
color:#fff;
color: #fff;
}
@@ -6,24 +6,26 @@
<div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid">
<div class="row">
<h2>Anmelden</h2>
<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">
<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">
<input type="password" formControlName="password" name="password" id="password"
class="form__input" placeholder="Password">
</div>
<div class="row">
<input type="submit" value="Anmelden" class="btn_signin">
<input type="submit" value="Log in" class="btn_signin">
</div>
</form>
</div>
<div class="row">
<a href="/registration">Konto erstellen</a>
<p>No account yet?<a href="/registration">sign up</a></p>
</div>
</div>
</div>
@@ -1,25 +1,54 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { SigninComponent } from './signin.component';
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 component: SigninComponent;
let fixture: ComponentFixture<SigninComponent>;
let mockService;
let formBuilder: FormBuilder;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SigninComponent ]
})
.compileComponents();
});
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;
fixture.detectChanges();
});
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();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -7,12 +7,14 @@
}
.folge-uns-item {
grid-column: 2; grid-row: 1;
grid-column: 2;
grid-row: 1;
justify-self: center;
}
.link-items {
grid-column: 2; grid-row: 2;
grid-column: 2;
grid-row: 2;
justify-self: center;
}
@@ -29,11 +31,13 @@
}
.bottom-logo {
grid-column: 1; grid-row: 3;
grid-column: 1;
grid-row: 3;
}
.bottom-info {
grid-column: 3; grid-row: 3;
grid-column: 3;
grid-row: 3;
justify-self: right;
}
@@ -5,24 +5,24 @@
<div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">Location</h4>
<p class="lead mb-0">
70376 Stuttgart
<br />
76133 Karlsruhe
<br/>
</p>
</div>
<!-- Footer Social Icons-->
<div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">FOLGE UNS</h4>
<a class="btn btn-outline-light btn-social mx-1" href="#!"><i class="fab fa-fw fa-github"></i></a>
<a class="btn btn-outline-light btn-social mx-1" href="#!"><i class="fab fa-fw fa-twitter"></i></a>
<a class="btn btn-outline-light btn-social mx-1" href="#!"><i class="fab fa-fw fa-linkedin-in"></i></a>
<a class="btn btn-outline-light btn-social mx-1" href="#!"><i class="fab fa-fw fa-dribbble"></i></a>
<h4 class="text-uppercase mb-4">FOLLOW US</h4>
<a class="btn btn-outline-light btn-social mx-1" href="https://github.com/Mueller-Patrick/Betterzon"><i
class="fab fa-fw fa-github"></i></a>
<a class="btn btn-outline-light btn-social mx-1" href="https://blog.betterzon.xyz/"><i
class="fab fa-fw fa-dribbble"></i></a>
</div>
<!-- Footer About Text-->
<div class="col-lg-4">
<h4 class="text-uppercase mb-4">SOME INFO</h4>
<h4 class="text-uppercase mb-4">CONTACT US</h4>
<p class="lead mb-0">
text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
betterzon-contact@mueller-patrick.tech
</p>
</div>
</div>
@@ -1,25 +1,25 @@
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", () => {
let component: BottomBarComponent;
let fixture: ComponentFixture<BottomBarComponent>;
describe('BottomBarComponent', () => {
let component: BottomBarComponent;
let fixture: ComponentFixture<BottomBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BottomBarComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BottomBarComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BottomBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(BottomBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-bottom-bar',
templateUrl: "./bottom-bar.component.html",
styleUrls: ["./bottom-bar.component.css"]
selector: 'app-bottom-bar',
templateUrl: './bottom-bar.component.html',
styleUrls: ['./bottom-bar.component.css']
})
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;
}
@@ -1,3 +1,7 @@
<div class="copyright py-4 text-center text-white">
<div class="container"><small>Copyright &copy; Your Website 2021</small></div>
<div class="copyright py-4 text-center text-white" id="copyright">
<div class="container" id="copyright-text"><small>Copyright &copy; Betterzon 2021</small></div>
<div id="imprintSection">
<a href="/impressum">Imprint</a><br>
<a href="/datenschutz">Privacy Policy</a>
</div>
</div>
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { CopyrightComponent } from './copyright.component';
import {CopyrightComponent} from './copyright.component';
describe('CopyrightComponent', () => {
let component: CopyrightComponent;
let fixture: ComponentFixture<CopyrightComponent>;
let component: CopyrightComponent;
let fixture: ComponentFixture<CopyrightComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CopyrightComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CopyrightComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CopyrightComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(CopyrightComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-copyright',
templateUrl: './copyright.component.html',
styleUrls: ['./copyright.component.css']
selector: 'app-copyright',
templateUrl: './copyright.component.html',
styleUrls: ['./copyright.component.css']
})
export class CopyrightComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -9,10 +9,10 @@
<a href="https://blog.betterzon.xyz/" class="fa fa-info fa-4x icon-3d"></a>
<a href="https://github.com/Mueller-Patrick/Betterzon/wiki" class="fa fa-wikipedia-w fa-4x icon-3d"></a>
</div>
<div class = "blocks" id="copyright">© COPYRIGHT 2020</div>
<div class="blocks" id="copyright">© COPYRIGHT 2020</div>
</div>
<div id="imprintSection">
<a href="/impressum" >Imprint</a><br>
<a href="/impressum">Imprint</a><br>
<a href="/datenschutz">Privacy Policy</a>
</div>
</footer>
@@ -1,23 +1,24 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.css']
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.css']
})
export class FooterComponent implements OnInit {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
constructor(
private router: Router,
private route: ActivatedRoute
) {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
navigateImprint(): void {
this.router.navigate([('/impressum')]);
}
navigateImprint(): void {
this.router.navigate([('/impressum')]);
}
}
@@ -1,7 +1,7 @@
<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="..." />
<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-->
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { GreetingInfoSliderComponent } from './greeting-info-slider.component';
import {GreetingInfoSliderComponent} from './greeting-info-slider.component';
describe('GreetingInfoSliderComponent', () => {
let component: GreetingInfoSliderComponent;
let fixture: ComponentFixture<GreetingInfoSliderComponent>;
let component: GreetingInfoSliderComponent;
let fixture: ComponentFixture<GreetingInfoSliderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GreetingInfoSliderComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [GreetingInfoSliderComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(GreetingInfoSliderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(GreetingInfoSliderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-greeting-info-slider',
templateUrl: './greeting-info-slider.component.html',
styleUrls: ['./greeting-info-slider.component.css']
selector: 'app-greeting-info-slider',
templateUrl: './greeting-info-slider.component.html',
styleUrls: ['./greeting-info-slider.component.css']
})
export class GreetingInfoSliderComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -5,7 +5,8 @@
<img src="assets/images/Betterzon.svg" alt="Betterzon Logo" width="50px" (click)="clickedLogo()">
</div>
<div class="searchBox">
<input *ngIf="showSearch===true" type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()">
<input *ngIf="showSearch===true" type="text" [(ngModel)]="searchInput" placeholder="Search"
(keyup.enter)="startedSearch()">
</div>
<div class="slider">
<mat-slide-toggle color="primary">dark me</mat-slide-toggle>
@@ -1,7 +1,7 @@
<section class="page-section portfolio" id="top-gesuchte">
<div class="container">
<!-- Portfolio Section Heading-->
<h2 class="page-section-heading text-center text-uppercase text-secondary mb-0">TOP-ANGEBOTE</h2>
<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>
@@ -9,26 +9,33 @@
<!-- Portfolio Grid Items-->
<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="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_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 class="bbb_deals_info_line d-flex flex-row justify-content-start">
<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 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 class="bbb_deals_info_line d-flex flex-row justify-content-start">
<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 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 class="available_bar">
<span style="width:17%"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -1,25 +1,58 @@
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', () => {
let component: HotDealsWidgetComponent;
let fixture: ComponentFixture<HotDealsWidgetComponent>;
let component: HotDealsWidgetComponent;
let fixture: ComponentFixture<HotDealsWidgetComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HotDealsWidgetComponent ]
})
.compileComponents();
});
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [HotDealsWidgetComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}, {
provide: ActivatedRoute,
useValue: ActivatedRouteMock
}]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HotDealsWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(HotDealsWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -4,9 +4,9 @@ import {Product} from '../../models/product';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-hot-deals-widget',
templateUrl: './hot-deals-widget.component.html',
styleUrls: ['./hot-deals-widget.component.css']
selector: 'app-hot-deals-widget',
templateUrl: './hot-deals-widget.component.html',
styleUrls: ['./hot-deals-widget.component.css']
})
export class HotDealsWidgetComponent implements OnInit {
@@ -52,7 +52,7 @@ export class HotDealsWidgetComponent implements OnInit {
default: {
this.getProductsByIds();
this.getAmazonPricesForBestDeals();
this.getVendors()
this.getVendors();
break;
}
}
@@ -73,7 +73,7 @@ export class HotDealsWidgetComponent implements OnInit {
deals => {
deals.forEach(deal => {
this.bestDealsProductIds.push(deal.product_id);
this.productsPricesMap [deal.product_id] = {lowestPrice: deal}
this.productsPricesMap [deal.product_id] = {lowestPrice: deal};
});
this.loadParams();
}
@@ -81,18 +81,18 @@ export class HotDealsWidgetComponent implements OnInit {
}
getVendors(): void {
this.productsPricesMap.keys().forEach(
key => {
const currentDeal = this.productsPricesMap[key].lowestPrice;
this.bestDealsProductIds.forEach(
productId => {
const currentDeal = this.productsPricesMap[productId].lowestPrice;
this.apiService.getVendorById(currentDeal.vendor_id).subscribe(
vendor => {
this.productsPricesMap[key].vendor = vendor
})
})
this.productsPricesMap[productId].vendor = vendor;
});
});
}
getAmazonPricesForBestDeals(): void{
getAmazonPricesForBestDeals(): void {
this.bestDealsProductIds.forEach(id => {
this.apiService.getAmazonPrice(id).subscribe(
price => {
@@ -1,7 +1,7 @@
<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">SIE VERTRAUEN UNS</h2>
<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>
@@ -12,25 +12,28 @@
<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 class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img class="productImage" src="assets/images/Betterzon.svg"/>
<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 class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img class="productImage" src="assets/images/Betterzon.svg"/>
<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 class="portfolio-item-caption-content text-center text-white"><i
class="fas fa-plus fa-3x"></i></div>
</div>
<img class="productImage" src="assets/images/Betterzon.svg"/>
<img width="70%" class="productImage" src="assets/images/CeangalLogo.png"/>
</div>
</div>
</div>
@@ -1,25 +1,53 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { KundenComponent } from './kunden.component';
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 component: KundenComponent;
let fixture: ComponentFixture<KundenComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ KundenComponent ]
})
.compileComponents();
});
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();
});
beforeEach(() => {
fixture = TestBed.createComponent(KundenComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -4,9 +4,9 @@ import {Product} from '../../models/product';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-kunden',
templateUrl: './kunden.component.html',
styleUrls: ['./kunden.component.css']
selector: 'app-kunden',
templateUrl: './kunden.component.html',
styleUrls: ['./kunden.component.css']
})
export class KundenComponent implements OnInit {
products: Product[] = [];
@@ -1,3 +1,6 @@
<header class="masthead bg-transparent text-white text-center" id="w1">
</header>
<div class="productItem">
<div class="productImageContainer">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
@@ -20,8 +23,12 @@
{{product?.short_description}}
</div>
</div>
<div class="priceAlarm" (click)="setPriceAlarm()">
Set Price Alarm
<div class="priceAlarm" *ngIf="!isLoggedIn" routerLink="/signin">
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 class="bestPriceContainer">
<div class="bestPrice">
@@ -58,6 +58,11 @@ class MockApiService extends AbstractMockObservableService {
this.content = [vendor];
return this;
}
getSessionInfoFromLocalStorage(): any {
this.content = [];
return this;
}
}
describe('ProductDetailsComponent', () => {
@@ -34,6 +34,8 @@ export class ProductDetailsComponent implements OnInit {
vendorMap = {};
@ViewChild('chart') chart: ChartComponent;
public chartOptions: ChartOptions;
isLoggedIn: boolean;
price: any;
constructor(
private apiService: ApiService
@@ -44,6 +46,9 @@ export class ProductDetailsComponent implements OnInit {
this.getProduct();
this.getVendors();
this.getPrices();
if (this.apiService.getSessionInfoFromLocalStorage().session_id != '') {
this.isLoggedIn = true;
}
}
getProduct(): void {
@@ -118,9 +123,11 @@ export class ProductDetailsComponent implements OnInit {
return Math.round(percentage);
}
setPriceAlarm() {
this.apiService.createPriceAlarms(this.productId, 9).subscribe(
setPriceAlarm(): void {
this.apiService.createPriceAlarms(this.productId, this.price * 100).subscribe(
alarms => console.log(alarms)
)
);
}
}
@@ -1,22 +1,53 @@
<div *ngIf="products.length==0">
No Products found!
</div>
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
<div class="productImageContainer" *ngIf="showProductPicture===true">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
</div>
<div class="productTitle">
<b>{{product.name}}</b>
</div>
<div class="productPrice">
5€
</div>
<div class="productDescription">
<div *ngIf="product.short_description.length > 300">
{{product.short_description.substring(0, 300) + "..."}}
</div>
<div *ngIf="product.short_description.length <= 300">
{{product.short_description}}
<div 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>
<!--
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
<div class="productImageContainer" *ngIf="showProductPicture===true">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
</div>
<div class="productTitle">
<b>{{product.name}}</b>
</div>
<div class="productPrice">
5€
</div>
<div class="productDescription">
<div *ngIf="product.short_description.length > 300">
{{product.short_description.substring(0, 300) + "..."}}
</div>
<div *ngIf="product.short_description.length <= 300">
{{product.short_description}}
</div>
</div>
</div>
-->
@@ -70,7 +70,8 @@ describe('ProductListComponent', () => {
last_modified: new Date(),
manufacturer_id: 1,
selling_rank: '1',
category_id: 1
category_id: 1,
price: 0
};
component.clickedProduct(product);
@@ -10,6 +10,7 @@ import {ActivatedRoute, Router} from '@angular/router';
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
pricesMap: any = {};
@Input() numberOfProducts: number;
@Input() showProductPicture: boolean;
@Input() searchQuery: string;
@@ -53,15 +54,35 @@ export class ProductListComponent implements OnInit {
}
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 {
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 {
this.router.navigate([('/product/' + product.product_id)]);
}
}
@@ -1,36 +0,0 @@
<div class="container" *ngIf="currentUser; else loggedOut">
<p>
<strong>e-mail</strong>
{{ currentUser.email}}
</p>
<p>
<strong>username:</strong>
{{ currentUser.username}}
</p>
<p>
<strong>alarms</strong>
{{alarms}}
</p>
<table>
<tr>
<th>Produkt</th>
<th>Preis</th>
</tr>
<tr *ngFor="let alarm of alarms">
<td>
{{productsMap[alarm.product_id]?.name}}
</td>
<td>
{{alarm.defined_price/100}}
</td>
</tr>
</table>
<p>
<strong><a routerLink="/">zurück</a></strong>
</p>
</div>
<ng-template #loggedOut>
Please login.
</ng-template>
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileComponent } from './profile.component';
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ProfileComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,52 +0,0 @@
import { Component, OnInit } from '@angular/core';
import {ApiService} from "../../services/api.service";
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
currentUser: any;
obj:any;
alarms: any [];
productsMap: any = {};
constructor(private api: ApiService ) { }
ngOnInit(): void {
this.api.getUserInfo().subscribe(
user=> {
this.currentUser = user
console.log(this.currentUser);
},
);
this.getPriceAlarms();
}
getPriceAlarms(): void {
this.api.getPriceAlarms().subscribe(
alarms => {
this.alarms = alarms
this.getProductsByIds()
}
)
}
getProductsByIds(): void {
let productIds: number [] = [];
this.alarms.forEach(
alarm => {productIds.push(alarm.product_id)}
);
this.api.getProductsByIds(productIds).subscribe(
products => {
products.forEach(
product => {this.productsMap[product.product_id] = product}
)
}
)
}
}
@@ -1,25 +1,25 @@
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', () => {
let component: SliderForProductsComponent;
let fixture: ComponentFixture<SliderForProductsComponent>;
let component: SliderForProductsComponent;
let fixture: ComponentFixture<SliderForProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SliderForProductsComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SliderForProductsComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SliderForProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(SliderForProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-slider-for-products',
templateUrl: './slider-for-products.component.html',
styleUrls: ['./slider-for-products.component.css']
selector: 'app-slider-for-products',
templateUrl: './slider-for-products.component.html',
styleUrls: ['./slider-for-products.component.css']
})
export class SliderForProductsComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -2,21 +2,32 @@
<div class="container">
<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">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"
(keyup.enter)="getSearchedProducts()" [(ngModel)]="searchQuery">
</div>
<button class="navbar-toggler text-uppercase font-weight-bold bg-primary text-white rounded" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<button class="navbar-toggler text-uppercase font-weight-bold bg-primary text-white rounded" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</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-Gesuchte</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">über uns</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">Unsere Kunden</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">Anmelden</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">Konto Erstellen</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>
<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>
@@ -1,25 +1,48 @@
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', () => {
let component: TopBarComponent;
let fixture: ComponentFixture<TopBarComponent>;
let component: TopBarComponent;
let fixture: ComponentFixture<TopBarComponent>;
let mockService;
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TopBarComponent ]
})
.compileComponents();
});
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [TopBarComponent],
providers: [{provide: ApiService, useValue: mockService}, {provide: Router, useValue: router}]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TopBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(TopBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,12 +1,12 @@
import {Component, Input, OnInit} from '@angular/core';
import {ApiService} from "../../services/api.service";
import {Router} from "@angular/router";
import {ApiService} from '../../services/api.service';
import {NavigationEnd, Router} from '@angular/router';
@Component({
selector: 'app-top-bar',
templateUrl: './top-bar.component.html',
styleUrls: ['./top-bar.component.css']
selector: 'app-top-bar',
templateUrl: './top-bar.component.html',
styleUrls: ['./top-bar.component.css']
})
export class TopBarComponent implements OnInit {
@@ -20,13 +20,12 @@ export class TopBarComponent implements OnInit {
) {
}
ngOnInit() {
ngOnInit(): void {
this.api.getUserInfo().subscribe(data => {
console.log(data)
console.log(data);
});
if (this.api.getSessionInfoFromLocalStorage().session_id != "") {
if (this.api.getSessionInfoFromLocalStorage().session_id !== '') {
this.isLoggedIn = true;
}
}
@@ -34,7 +33,11 @@ export class TopBarComponent implements OnInit {
logout(): void {
localStorage.setItem('session_id', '');
localStorage.setItem('session_key', '');
window.location.reload()
if (this.router.url === '/profile') {
this.router.navigate(['/']);
} else {
window.location.reload();
}
}
getSearchedProducts(): void {
@@ -1,5 +1,5 @@
<app-header [showSearch]="true"></app-header>
<div id="imprint">
<app-top-bar></app-top-bar>
<div id="imprint" class="container masthead">
<h1>Impressum</h1>
<h2>Angaben gem&auml;&szlig; &sect; 5 TMG</h2>
@@ -17,14 +17,16 @@
<h3>Haftung f&uuml;r Inhalte</h3>
<p>
Als Diensteanbieter sind wir gem&auml;&szlig; &sect; 7 Abs.1 TMG f&uuml;r eigene
Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach &sect;&sect; 8 bis 10 TMG sind wir als
Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach &sect;&sect; 8 bis 10 TMG sind wir
als
Diensteanbieter jedoch nicht verpflichtet, &uuml;bermittelte oder gespeicherte fremde Informationen zu &uuml;berwachen
oder nach Umst&auml;nden zu forschen, die auf eine rechtswidrige T&auml;tigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben
hiervon unber&uuml;hrt.
Eine diesbez&uuml;gliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung m&ouml;glich.
Eine diesbez&uuml;gliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung
m&ouml;glich.
Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
</p>
@@ -44,16 +46,21 @@
<h3>Urheberrecht</h3>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht.
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
Urheberrecht.
Die Vervielf&auml;ltigung, Bearbeitung, Verbreitung und jede Art der Verwertung au&szlig;erhalb der Grenzen des
Urheberrechtes bed&uuml;rfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und
Kopien dieser Seite sind nur f&uuml;r den privaten, nicht kommerziellen Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet.
Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung
aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter
beachtet.
Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine
Urheberrechtsverletzung
aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden
wir
derartige Inhalte umgehend entfernen.
</p>
</div>
<app-footer></app-footer>
<app-bottom-bar></app-bottom-bar>
<app-copyright></app-copyright>
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-imprint',
templateUrl: './imprint.component.html',
styleUrls: ['./imprint.component.css']
selector: 'app-imprint',
templateUrl: './imprint.component.html',
styleUrls: ['./imprint.component.css']
})
export class ImprintComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -1,50 +0,0 @@
#mainComponents {
margin: 5em;
margin-top: .5em;
margin-bottom: .5em;
}
#productListsContainer {
display: grid;
grid-template-areas:
'search search'
'popularSearches bestDeals';
grid-template-columns: 50% 50%;
}
#searchContainer {
position: relative;
grid-area: search;
height: 10em;
}
#searchContainer input {
position: relative;
font-size: 1.5em;
padding: .25em;
display: block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin: auto;
-ms-transform: translateY(50%);
transform: translateY(2.5em);
}
#popularSearchesList {
grid-area: popularSearches;
padding: .5em;
}
#popularSearchesList h2 {
text-align: center;
}
#bestDealsList {
grid-area: bestDeals;
padding: .5em;
}
#bestDealsList h2 {
text-align: center;
}
@@ -1,6 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {ApiService} from "../../services/api.service";
import {ApiService} from '../../services/api.service';
@Component({
selector: 'app-landingpage',
@@ -12,8 +12,7 @@ export class LandingpageComponent implements OnInit {
isLoggedIn = false;
constructor(
private router: Router,
private api: ApiService
private router: Router
) {
}
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-page-not-found-page',
templateUrl: './page-not-found-page.component.html',
styleUrls: ['./page-not-found-page.component.css']
selector: 'app-page-not-found-page',
templateUrl: './page-not-found-page.component.html',
styleUrls: ['./page-not-found-page.component.css']
})
export class PageNotFoundPageComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -1,11 +1,12 @@
<app-header [showSearch]="true"></app-header>
<div id="privacy">
<app-top-bar></app-top-bar>
<div id="privacy" class="container masthead">
<h1>Datenschutz&shy;erkl&auml;rung</h1>
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3>
<p>
Die folgenden Hinweise geben einen einfachen &Uuml;berblick dar&uuml;ber, was mit Ihren
personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen
personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit
denen
Sie pers&ouml;nlich identifiziert werden k&ouml;nnen. Ausf&uuml;hrliche Informationen zum Thema Datenschutz
entnehmen Sie unserer unter diesem Text aufgef&uuml;hrten Datenschutzerkl&auml;rung.
</p>
@@ -40,8 +41,10 @@
Sie haben jederzeit das Recht, unentgeltlich Auskunft &uuml;ber
Herkunft, Empf&auml;nger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben au&szlig;erdem
ein Recht, die Berichtigung oder L&ouml;schung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur
Datenverarbeitung erteilt haben, k&ouml;nnen Sie diese Einwilligung jederzeit f&uuml;r die Zukunft widerrufen. Au&szlig;erdem
haben Sie das Recht, unter bestimmten Umst&auml;nden die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen
Datenverarbeitung erteilt haben, k&ouml;nnen Sie diese Einwilligung jederzeit f&uuml;r die Zukunft widerrufen.
Au&szlig;erdem
haben Sie das Recht, unter bestimmten Umst&auml;nden die Einschr&auml;nkung der Verarbeitung Ihrer
personenbezogenen
Daten zu verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zust&auml;ndigen Aufsichtsbeh&ouml;rde
zu.
</p>
@@ -85,13 +88,15 @@
<h3>Datenschutz</h3>
<p>
Die Betreiber dieser Seiten nehmen den Schutz Ihrer pers&ouml;nlichen Daten sehr ernst. Wir
behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie
behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften
sowie
dieser Datenschutzerkl&auml;rung.
</p>
<p>
Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten
erhoben. Personenbezogene Daten sind Daten, mit denen Sie pers&ouml;nlich identifiziert werden k&ouml;nnen. Die
vorliegende Datenschutzerkl&auml;rung erl&auml;utert, welche Daten wir erheben und wof&uuml;r wir sie nutzen. Sie
vorliegende Datenschutzerkl&auml;rung erl&auml;utert, welche Daten wir erheben und wof&uuml;r wir sie nutzen.
Sie
erl&auml;utert auch, wie und zu welchem Zweck das geschieht.
</p>
<p>
@@ -115,7 +120,8 @@
E-Mail: betterzon-privacy@mueller-patrick.tech
</p>
<p>
Verantwortliche Stelle ist die nat&uuml;rliche oder juristische Person, die allein oder gemeinsam mit anderen &uuml;ber
Verantwortliche Stelle ist die nat&uuml;rliche oder juristische Person, die allein oder gemeinsam mit anderen
&uuml;ber
die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z.&nbsp;B. Namen, E-Mail-Adressen o. &Auml;.)
entscheidet.
</p>
@@ -124,7 +130,8 @@
<p>
Soweit innerhalb dieser Datenschutzerkl&auml;rung keine speziellere Speicherdauer genannt
wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck f&uuml;r die Datenverarbeitung entf&auml;llt.
Wenn Sie ein berechtigtes L&ouml;schersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen,
Wenn Sie ein berechtigtes L&ouml;schersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung
widerrufen,
werden Ihre Daten gel&ouml;scht, sofern wir keine anderen rechtlich zul&auml;ssigen Gr&uuml;nde f&uuml;r die
Speicherung Ihrer personenbezogenen Daten haben (z.B. steuer- oder handelsrechtliche Aufbewahrungsfristen); im
letztgenannten Fall erfolgt die L&ouml;schung nach Fortfall dieser Gr&uuml;nde.
@@ -139,7 +146,8 @@
Datenschutzniveau garantiert werden kann. Beispielsweise sind US-Unternehmen dazu verpflichtet, personenbezogene
Daten an Sicherheitsbeh&ouml;rden herauszugeben, ohne dass Sie als Betroffener hiergegen gerichtlich vorgehen k&ouml;nnten.
Es kann daher nicht ausgeschlossen werden, dass US-Beh&ouml;rden (z.B. Geheimdienste) Ihre auf US-Servern
befindlichen Daten zu &Uuml;berwachungszwecken verarbeiten, auswerten und dauerhaft speichern. Wir haben auf diese
befindlichen Daten zu &Uuml;berwachungszwecken verarbeiten, auswerten und dauerhaft speichern. Wir haben auf
diese
Verarbeitungst&auml;tigkeiten keinen Einfluss.
</p>
@@ -150,15 +158,19 @@
erfolgten Datenverarbeitung bleibt vom Widerruf unber&uuml;hrt.
</p>
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen F&auml;llen sowie gegen Direktwerbung (Art. 21 DSGVO)</h3>
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen F&auml;llen sowie gegen Direktwerbung (Art. 21
DSGVO)</h3>
<p>
WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F DSGVO ERFOLGT, HABEN SIE JEDERZEIT DAS
RECHT, AUS GR&Uuml;NDEN, DIE SICH AUS IHRER BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER
PERSONENBEZOGENEN DATEN WIDERSPRUCH EINZULEGEN; DIES GILT AUCH F&Uuml;R EIN AUF DIESE BESTIMMUNGEN GEST&Uuml;TZTES
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN SIE DIESER DATENSCHUTZERKL&Auml;RUNG.
WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN SIE DIESER
DATENSCHUTZERKL&Auml;RUNG.
WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES
SEI
DENN, WIR K&Ouml;NNEN ZWINGENDE SCHUTZW&Uuml;RDIGE GR&Uuml;NDE F&Uuml;R DIE VERARBEITUNG NACHWEISEN, DIE IHRE
INTERESSEN, RECHTE UND FREIHEITEN &Uuml;BERWIEGEN ODER DIE VERARBEITUNG DIENT DER GELTENDMACHUNG, AUS&Uuml;BUNG ODER
INTERESSEN, RECHTE UND FREIHEITEN &Uuml;BERWIEGEN ODER DIE VERARBEITUNG DIENT DER GELTENDMACHUNG, AUS&Uuml;BUNG
ODER
VERTEIDIGUNG VON RECHTSANSPR&Uuml;CHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).
</p>
<p>
@@ -166,7 +178,8 @@
DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE
VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH F&Uuml;R
DAS PROFILING, SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN, WERDEN IHRE
PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21
PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART.
21
ABS. 2 DSGVO).
</p>
@@ -174,7 +187,8 @@
<p>
Im Falle von Verst&ouml;&szlig;en
gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer Aufsichtsbeh&ouml;rde, insbesondere in dem
Mitgliedstaat ihres gew&ouml;hnlichen Aufenthalts, ihres Arbeitsplatzes oder des Orts des mutma&szlig;lichen Versto&szlig;es
Mitgliedstaat ihres gew&ouml;hnlichen Aufenthalts, ihres Arbeitsplatzes oder des Orts des mutma&szlig;lichen
Versto&szlig;es
zu. Das Beschwerderecht besteht unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher
Rechtsbehelfe.
</p>
@@ -182,7 +196,8 @@
<h3>Recht auf Daten&shy;&uuml;bertrag&shy;barkeit</h3>
<p>
Sie haben das Recht, Daten, die wir auf Grundlage Ihrer
Einwilligung oder in Erf&uuml;llung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem
Einwilligung oder in Erf&uuml;llung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in
einem
g&auml;ngigen, maschinenlesbaren Format aush&auml;ndigen zu lassen. Sofern Sie die direkte &Uuml;bertragung der
Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.
</p>
@@ -190,8 +205,10 @@
<h3>SSL- bzw. TLS-Verschl&uuml;sselung</h3>
<p>
Diese Seite nutzt aus Sicherheitsgr&uuml;nden und zum Schutz der &Uuml;bertragung
vertraulicher Inhalte, wie zum Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden, eine
SSL- bzw. TLS-Verschl&uuml;sselung. Eine verschl&uuml;sselte Verbindung erkennen Sie daran, dass die Adresszeile des
vertraulicher Inhalte, wie zum Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden,
eine
SSL- bzw. TLS-Verschl&uuml;sselung. Eine verschl&uuml;sselte Verbindung erkennen Sie daran, dass die Adresszeile
des
Browsers von &bdquo;http://&ldquo; auf &bdquo;https://&ldquo; wechselt und an dem Schloss-Symbol in Ihrer
Browserzeile.
</p>
@@ -203,8 +220,10 @@
<h3>Auskunft, L&ouml;schung und Berichtigung</h3>
<p>
Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen
jederzeit das Recht auf unentgeltliche Auskunft &uuml;ber Ihre gespeicherten personenbezogenen Daten, deren Herkunft
und Empf&auml;nger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung oder L&ouml;schung dieser
jederzeit das Recht auf unentgeltliche Auskunft &uuml;ber Ihre gespeicherten personenbezogenen Daten, deren
Herkunft
und Empf&auml;nger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung oder L&ouml;schung
dieser
Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten k&ouml;nnen Sie sich jederzeit an uns
wenden.
</p>
@@ -216,22 +235,29 @@
</p>
<ul>
<li>
Wenn Sie die Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, ben&ouml;tigen wir in
der Regel Zeit, um dies zu &uuml;berpr&uuml;fen. F&uuml;r die Dauer der Pr&uuml;fung haben Sie das Recht, die
Wenn Sie die Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, ben&ouml;tigen wir
in
der Regel Zeit, um dies zu &uuml;berpr&uuml;fen. F&uuml;r die Dauer der Pr&uuml;fung haben Sie das Recht,
die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
</li>
<li>
Wenn die Verarbeitung Ihrer personenbezogenen Daten unrechtm&auml;&szlig;ig geschah/geschieht, k&ouml;nnen Sie
Wenn die Verarbeitung Ihrer personenbezogenen Daten unrechtm&auml;&szlig;ig geschah/geschieht, k&ouml;nnen
Sie
statt der L&ouml;schung die Einschr&auml;nkung der Datenverarbeitung verlangen.
</li>
<li>
Wenn wir Ihre personenbezogenen Daten nicht mehr ben&ouml;tigen, Sie sie jedoch zur Aus&uuml;bung, Verteidigung
oder Geltendmachung von Rechtsanspr&uuml;chen ben&ouml;tigen, haben Sie das Recht, statt der L&ouml;schung die
Wenn wir Ihre personenbezogenen Daten nicht mehr ben&ouml;tigen, Sie sie jedoch zur Aus&uuml;bung,
Verteidigung
oder Geltendmachung von Rechtsanspr&uuml;chen ben&ouml;tigen, haben Sie das Recht, statt der L&ouml;schung
die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
</li>
<li>
Wenn Sie einen Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abw&auml;gung zwischen Ihren und
unseren Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen &uuml;berwiegen, haben
Wenn Sie einen Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abw&auml;gung zwischen Ihren
und
unseren Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen &uuml;berwiegen,
haben
Sie das Recht, die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
</li>
</ul>
@@ -239,7 +265,8 @@
Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschr&auml;nkt haben, d&uuml;rfen diese Daten
&ndash; von ihrer Speicherung abgesehen &ndash; nur mit Ihrer Einwilligung oder zur Geltendmachung, Aus&uuml;bung
oder Verteidigung von Rechtsanspr&uuml;chen oder zum Schutz der Rechte einer anderen nat&uuml;rlichen oder
juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der Europ&auml;ischen Union
juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der Europ&auml;ischen
Union
oder eines Mitgliedstaats verarbeitet werden.
</p>
@@ -250,12 +277,14 @@
und richten auf Ihrem Endger&auml;t keinen Schaden an. Sie werden entweder vor&uuml;bergehend f&uuml;r die Dauer
einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endger&auml;t gespeichert.
Session-Cookies werden nach Ende Ihres Besuchs automatisch gel&ouml;scht. Permanente Cookies bleiben auf Ihrem
Endger&auml;t gespeichert, bis Sie diese selbst l&ouml;schen&nbsp;oder eine automatische L&ouml;schung durch Ihren
Endger&auml;t gespeichert, bis Sie diese selbst l&ouml;schen&nbsp;oder eine automatische L&ouml;schung durch
Ihren
Webbrowser erfolgt.
</p>
<p>
Teilweise k&ouml;nnen auch Cookies von Drittunternehmen auf Ihrem Endger&auml;t
gespeichert werden, wenn Sie unsere Seite betreten (Third-Party-Cookies). Diese erm&ouml;glichen uns oder Ihnen die
gespeichert werden, wenn Sie unsere Seite betreten (Third-Party-Cookies). Diese erm&ouml;glichen uns oder Ihnen
die
Nutzung bestimmter Dienstleistungen des Drittunternehmens (z.B. Cookies zur Abwicklung von
Zahlungsdienstleistungen).
</p>
@@ -267,24 +296,31 @@
</p>
<p>
Cookies, die zur Durchf&uuml;hrung des elektronischen Kommunikationsvorgangs (notwendige Cookies)
oder zur Bereitstellung bestimmter, von Ihnen erw&uuml;nschter Funktionen (funktionale Cookies, z. B. f&uuml;r die
Warenkorbfunktion) oder zur Optimierung der Website (z.B. Cookies zur Messung des Webpublikums) erforderlich sind,
werden auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird.
Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von Cookies zur technisch fehlerfreien und
oder zur Bereitstellung bestimmter, von Ihnen erw&uuml;nschter Funktionen (funktionale Cookies, z. B. f&uuml;r
die
Warenkorbfunktion) oder zur Optimierung der Website (z.B. Cookies zur Messung des Webpublikums) erforderlich
sind,
werden auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben
wird.
Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von Cookies zur technisch fehlerfreien
und
optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur Speicherung von Cookies abgefragt wurde,
erfolgt die Speicherung der betreffenden Cookies ausschlie&szlig;lich auf Grundlage dieser Einwilligung (Art. 6 Abs.
erfolgt die Speicherung der betreffenden Cookies ausschlie&szlig;lich auf Grundlage dieser Einwilligung (Art. 6
Abs.
1 lit. a DSGVO); die Einwilligung ist jederzeit widerrufbar.
</p>
<p>
Sie k&ouml;nnen Ihren Browser so einstellen,
dass Sie &uuml;ber das Setzen von Cookies informiert werden und Cookies nur im Einzelfall erlauben, die Annahme von
dass Sie &uuml;ber das Setzen von Cookies informiert werden und Cookies nur im Einzelfall erlauben, die Annahme
von
Cookies f&uuml;r bestimmte F&auml;lle oder generell ausschlie&szlig;en sowie das automatische L&ouml;schen der
Cookies beim Schlie&szlig;en des Browsers aktivieren. Bei der Deaktivierung von Cookies kann die Funktionalit&auml;t
dieser Website eingeschr&auml;nkt sein.
</p>
<p>
Soweit Cookies von Drittunternehmen oder zu Analysezwecken eingesetzt
werden, werden wir Sie hier&uuml;ber im Rahmen dieser Datenschutzerkl&auml;rung gesondert informieren und ggf. eine
werden, werden wir Sie hier&uuml;ber im Rahmen dieser Datenschutzerkl&auml;rung gesondert informieren und ggf.
eine
Einwilligung abfragen.
</p>
@@ -318,10 +354,13 @@
den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
</p>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre Anfrage mit der Erf&uuml;llung
eines Vertrags zusammenh&auml;ngt oder zur Durchf&uuml;hrung vorvertraglicher Ma&szlig;nahmen erforderlich ist. In
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre Anfrage mit der
Erf&uuml;llung
eines Vertrags zusammenh&auml;ngt oder zur Durchf&uuml;hrung vorvertraglicher Ma&szlig;nahmen erforderlich ist.
In
allen &uuml;brigen F&auml;llen beruht die Verarbeitung auf unserem berechtigten Interesse an der effektiven
Bearbeitung der an uns gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer Einwilligung (Art. 6 Abs. 1
Bearbeitung der an uns gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer Einwilligung (Art. 6
Abs. 1
lit. a DSGVO) sofern diese abgefragt wurde.
</p>
<p>
@@ -335,21 +374,25 @@
<p>
Wenn Sie uns per E-Mail, Telefon oder Telefax kontaktieren, wird
Ihre Anfrage inklusive aller daraus hervorgehenden personenbezogenen Daten (Name, Anfrage) zum Zwecke der
Bearbeitung Ihres Anliegens bei uns gespeichert und verarbeitet. Diese Daten geben wir nicht ohne Ihre Einwilligung
Bearbeitung Ihres Anliegens bei uns gespeichert und verarbeitet. Diese Daten geben wir nicht ohne Ihre
Einwilligung
weiter.
</p>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre
Anfrage mit der Erf&uuml;llung eines Vertrags zusammenh&auml;ngt oder zur Durchf&uuml;hrung vorvertraglicher Ma&szlig;nahmen
erforderlich ist. In allen &uuml;brigen F&auml;llen beruht die Verarbeitung auf unserem berechtigten Interesse an
der effektiven Bearbeitung der an uns gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer Einwilligung
erforderlich ist. In allen &uuml;brigen F&auml;llen beruht die Verarbeitung auf unserem berechtigten Interesse
an
der effektiven Bearbeitung der an uns gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer
Einwilligung
(Art. 6 Abs. 1 lit. a DSGVO) sofern diese abgefragt wurde.
</p>
<p>
Die von Ihnen an uns per Kontaktanfragen &uuml;bersandten
Daten verbleiben bei uns, bis Sie uns zur L&ouml;schung auffordern, Ihre Einwilligung zur Speicherung widerrufen
oder der Zweck f&uuml;r die Datenspeicherung entf&auml;llt (z.&nbsp;B. nach abgeschlossener Bearbeitung Ihres
Anliegens). Zwingende gesetzliche Bestimmungen &ndash; insbesondere gesetzliche Aufbewahrungsfristen &ndash; bleiben
Anliegens). Zwingende gesetzliche Bestimmungen &ndash; insbesondere gesetzliche Aufbewahrungsfristen &ndash;
bleiben
unber&uuml;hrt.
</p>
@@ -363,10 +406,13 @@
<p>
Zu diesem Zweck muss der von Ihnen
verwendete Browser Verbindung zu den Servern von Google aufnehmen. Hierdurch erlangt Google Kenntnis dar&uuml;ber,
dass &uuml;ber Ihre IP-Adresse diese Website aufgerufen wurde. Die Nutzung von Google WebFonts erfolgt auf Grundlage
von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an der einheitlichen Darstellung
dass &uuml;ber Ihre IP-Adresse diese Website aufgerufen wurde. Die Nutzung von Google WebFonts erfolgt auf
Grundlage
von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an der einheitlichen
Darstellung
des Schriftbildes auf seiner Website. Sofern eine entsprechende Einwilligung abgefragt wurde (z. B. eine
Einwilligung zur Speicherung von Cookies), erfolgt die Verarbeitung ausschlie&szlig;lich auf Grundlage von Art. 6
Einwilligung zur Speicherung von Cookies), erfolgt die Verarbeitung ausschlie&szlig;lich auf Grundlage von Art.
6
Abs. 1 lit. a DSGVO; die Einwilligung ist jederzeit widerrufbar.
</p>
<p>
@@ -376,7 +422,8 @@
<p>
Weitere Informationen zu Google Web Fonts finden Sie
unter <a href="https://developers.google.com/fonts/faq" target="_blank" rel="noopener noreferrer">https://developers.google.com/fonts/faq</a>
und in der Datenschutzerkl&auml;rung von Google: <a href="https://policies.google.com/privacy?hl=de" target="_blank"
und in der Datenschutzerkl&auml;rung von Google: <a href="https://policies.google.com/privacy?hl=de"
target="_blank"
rel="noopener noreferrer">https://policies.google.com/privacy?hl=de</a>.
</p>
@@ -388,10 +435,12 @@
<p>
Beim Aufruf
einer Seite l&auml;dt Ihr Browser die ben&ouml;tigten Fonts in ihren Browsercache, um Texte, Schriftarten und
Symbole korrekt anzuzeigen. Zu diesem Zweck muss der von Ihnen verwendete Browser Verbindung zu den Servern von Font
Symbole korrekt anzuzeigen. Zu diesem Zweck muss der von Ihnen verwendete Browser Verbindung zu den Servern von
Font
Awesome aufnehmen. Hierdurch erlangt Font Awesome Kenntnis dar&uuml;ber, dass &uuml;ber Ihre IP-Adresse diese
Website aufgerufen wurde. Die Nutzung von Font Awesome erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir
haben ein berechtigtes Interesse an der einheitlichen Darstellung des Schriftbildes auf unserer Website. Sofern eine
haben ein berechtigtes Interesse an der einheitlichen Darstellung des Schriftbildes auf unserer Website. Sofern
eine
entsprechende Einwilligung abgefragt wurde (z. B. eine Einwilligung zur Speicherung von Cookies), erfolgt die
Verarbeitung ausschlie&szlig;lich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO; die Einwilligung ist jederzeit
widerrufbar.
@@ -405,4 +454,5 @@
von Font Awesome unter: <a href="https://fontawesome.com/privacy" target="_blank" rel="noopener noreferrer">https://fontawesome.com/privacy</a>.
</p>
</div>
<app-footer></app-footer>
<app-bottom-bar></app-bottom-bar>
<app-copyright></app-copyright>
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-privacy',
templateUrl: './privacy.component.html',
styleUrls: ['./privacy.component.css']
selector: 'app-privacy',
templateUrl: './privacy.component.html',
styleUrls: ['./privacy.component.css']
})
export class PrivacyComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -1,5 +1,43 @@
#mainComponents {
margin: 5em;
margin-top: .5em;
margin-bottom: .5em;
body {
background: #eee
}
.ratings i {
font-size: 16px;
color: red
}
.strike-text {
color: red;
text-decoration: line-through
}
.product-image {
width: 20%;
height: 20%;
}
.dot {
height: 7px;
width: 7px;
margin-left: 6px;
margin-right: 6px;
margin-top: 3px;
background-color: blue;
border-radius: 50%;
display: inline-block
}
.spec-1 {
color: #938787;
font-size: 15px
}
h5 {
font-weight: 400
}
.para {
font-size: 16px
}
@@ -1,7 +1,14 @@
<app-top-bar></app-top-bar>
<header class="masthead bg-transparent text-white text-center" id="w1">
</header>
<div id="mainComponents">
<app-product-list numberOfProducts="20" [showProductPicture]="true" searchQuery="{{searchTerm}}"
type="search"></app-product-list>
</div>
<header class="masthead bg-transparent text-white text-center">
</header>
<app-bottom-bar></app-bottom-bar>
@@ -0,0 +1,4 @@
.header-in-page {
padding-top: calc(2rem + 20px);
padding-bottom: 6rem;
}
@@ -1,3 +1,10 @@
<app-top-bar></app-top-bar>
<header class="header-in-page">
</header>
<app-profile></app-profile>
<header class="header-in-page">
</header>
<app-bottom-bar></app-bottom-bar>
<app-copyright></app-copyright>
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { ProfilePageComponent } from './profile-page.component';
import {ProfilePageComponent} from './profile-page.component';
describe('ProfilePageComponent', () => {
let component: ProfilePageComponent;
let fixture: ComponentFixture<ProfilePageComponent>;
let component: ProfilePageComponent;
let fixture: ComponentFixture<ProfilePageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ProfilePageComponent ]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProfilePageComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProfilePageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProfilePageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-profile-page',
templateUrl: './profile-page.component.html',
styleUrls: ['./profile-page.component.css']
selector: 'app-profile-page',
templateUrl: './profile-page.component.html',
styleUrls: ['./profile-page.component.css']
})
export class ProfilePageComponent implements OnInit {
constructor() { }
constructor() {
}
ngOnInit(): void {
}
ngOnInit(): void {
}
}
@@ -0,0 +1,26 @@
.inf-content {
border: 1px solid #DDDDDD;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
box-shadow: 7px 7px 7px rgba(0, 0, 0, 0.3);
}
.header-in-page {
padding-top: calc(1rem + 20px);
padding-bottom: 1rem;
}
table, th, td {
border: 1px solid black;
}
.delete:hover {
cursor: pointer;
color: #0d5a4b;
}
.edit {
width: 5%;
height: 5%;
}
@@ -0,0 +1,139 @@
<div class="container bootstrap snippets bootdey">
<div class="panel-body inf-content">
<div class="row">
<div class="col-md-4">
<img alt="" style="width:600px;" title="" class="img-circle img-thumbnail isTooltip"
src="https://bootdey.com/img/Content/avatar/avatar7.png" data-original-title="Usuario">
<ul title="Ratings" class="list-inline ratings text-center">
<li><a href="#"><span class="glyphicon glyphicon-star"></span></a></li>
<li><a href="#"><span class="glyphicon glyphicon-star"></span></a></li>
<li><a href="#"><span class="glyphicon glyphicon-star"></span></a></li>
<li><a href="#"><span class="glyphicon glyphicon-star"></span></a></li>
<li><a href="#"><span class="glyphicon glyphicon-star"></span></a></li>
</ul>
</div>
<div class="col-md-6">
<strong>Information</strong><br>
<div class="table-responsive">
<table class="table table-user-information">
<tbody>
<tr>
<td>
<strong>
<span class="glyphicon glyphicon-bookmark text-primary"></span>
Username
</strong>
</td>
<td class="text-primary">
{{currentUser.username}}
</td>
</tr>
<tr>
<td>
<strong>
<span class="glyphicon glyphicon-eye-open text-primary"></span>
Role
</strong>
</td>
<td class="text-primary">
User
</td>
</tr>
<tr>
<td>
<strong>
<span class="glyphicon glyphicon-envelope text-primary"></span>
Email
</strong>
</td>
<td class="text-primary">
{{currentUser.email}}
</td>
</tr>
<tr>
<td>
<strong>
<span class="glyphicon glyphicon-calendar text-primary"></span>
created
</strong>
</td>
<td class="text-primary">
{{currentUser.registration_date}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<header class="header-in-page">
</header>
<div class="container bootstrap snippets bootdey">
<div class="col-auto">
<table class="table table-hover">
<tr>
<th>Product</th>
<th>Price</th>
<th>Change</th>
<th>Delete</th>
</tr>
<tr *ngFor="let alarm of alarms">
<td>
{{productsMap[alarm.product_id]?.name}}
</td>
<td>
{{alarm.defined_price / 100}}€
</td>
<td>
<img class="delete" src="../assets/images/pencil.png">
</td>
<td>
<img class="delete" src="../assets/images/Delete_icon-icons.com_55931.png"
(click)="delete(alarm.alarm_id)">
</td>
</tr>
</table>
</div>
</div>
<!--
<div class="container" *ngIf="currentUser; else loggedOut">
<p>
<strong>e-mail</strong>
{{ currentUser.email}}
</p>
<p>
<strong>username:</strong>
{{ currentUser.username}}
</p>
<p>
<strong>alarms</strong>
{{alarms}}
</p>
<table>
<tr>
<th>Produkt</th>
<th>Preis</th>
</tr>
<tr *ngFor="let alarm of alarms">
<td>
{{productsMap[alarm.product_id]?.name}}
</td>
<td>
{{alarm.defined_price/100}}
</td>
</tr>
</table>
<p>
<strong><a routerLink="/">zurück</a></strong>
</p>
</div>
<ng-template #loggedOut>
Please login.
</ng-template>
-->
@@ -0,0 +1,47 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProfileComponent} from './profile.component';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {ApiService} from '../../services/api.service';
class MockApiService extends AbstractMockObservableService {
getUserInfo(): any {
this.content = [];
return this;
}
getPriceAlarms(): any {
this.content = [];
return this;
}
getProductsByIds(): any {
this.content = [];
return this;
}
}
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
let mockService;
beforeEach(async () => {
mockService = new MockApiService();
await TestBed.configureTestingModule({
declarations: [ProfileComponent],
providers: [{provide: ApiService, useValue: mockService}]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,63 @@
import {Component, OnInit} from '@angular/core';
import {ApiService} from '../../services/api.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
currentUser: any;
obj: any;
alarms: any [];
productsMap: any = {};
constructor(private api: ApiService) {
}
ngOnInit(): void {
this.api.getUserInfo().subscribe(
user => {
this.currentUser = user;
console.log(this.currentUser);
},
);
this.getPriceAlarms();
}
getPriceAlarms(): void {
this.api.getPriceAlarms().subscribe(
alarms => {
this.alarms = alarms;
this.getProductsByIds();
}
);
}
getProductsByIds(): void {
const productIds: number [] = [];
this.alarms.forEach(
alarm => {
productIds.push(alarm.product_id);
}
);
this.api.getProductsByIds(productIds).subscribe(
products => {
products.forEach(
product => {
this.productsMap[product.product_id] = product;
}
);
}
);
}
delete(id: number): void {
this.api.deletePriceAlarm(id).subscribe(
res => window.location.reload()
);
}
}
@@ -1,7 +1,7 @@
import {TestBed} from '@angular/core/testing';
import {ApiService} from './api.service';
import {HttpClientModule} from "@angular/common/http";
import {HttpClientModule} from '@angular/common/http';
describe('ApiService', () => {
let service: ApiService;
+20 -1
View File
@@ -106,7 +106,7 @@ export class ApiService {
let asin = '';
// Check if the parameter is a link or an asin
const linkRegex: RegExp = /^http[s]{0,1}:\/\/.*\/dp\/(.[^\/]*)\/{0,1}.*$/;
const linkRegex: RegExp = /^http[s]?:\/\/.*\/dp\/(.[^\/]*)\/?.*$/;
const matches = linkRegex.exec(asinOrLink);
if (matches) {
// param is a link, extract asin
@@ -445,6 +445,25 @@ export class ApiService {
}
}
/**
* Deletes the given price alarm
* @param alarmId the price alarm to delete
* @return Observable<any> The observable response of the api
*/
deletePriceAlarm(alarmId: number): Observable<any> {
try {
const sessionInfo = this.getSessionInfoFromLocalStorage();
let params = new HttpParams();
params = params.append('session_id', sessionInfo.session_id);
params = params.append('session_key', sessionInfo.session_key);
return this.http.delete((this.apiUrl + '/pricealarms/' + alarmId), {params});
} catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
}
}
/* __ __
/ / / /_______ __________
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+6 -6
View File
@@ -1,12 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import {enableProdMode} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import {AppModule} from './app/app.module';
import {environment} from './environments/environment';
if (environment.production) {
enableProdMode();
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
.catch(err => console.error(err));
+622 -2
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,23 +1,23 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {getTestBed} from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
+5 -5
View File
@@ -1,12 +1,12 @@
# Betterzon
Website: https://www.betterzon.xyz<br>
Blog: https://blog.betterzon.xyz<br>
Website: https://betterzon.p4ddy.com<br>
Blog: https://blog.betterzon.p4ddy.com<br>
Wiki: https://github.com/Mueller-Patrick/Betterzon/wiki
# Code Quality
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/88e47ebf837b43af9d12147c22f77f7f)](https://www.codacy.com/gh/Mueller-Patrick/Betterzon/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Mueller-Patrick/Betterzon&amp;utm_campaign=Badge_Grade)
[![Code Coverage](https://img.shields.io/badge/coverage-82%25-green)](https://ci.betterzon.xyz)
[![Code Coverage](https://img.shields.io/badge/coverage-81%25-green)](https://ci.betterzon.xyz)
# Project Status
[![Website Status](https://img.shields.io/website?label=www.betterzon.xyz&style=for-the-badge&url=https%3A%2F%2Fwww.betterzon.xyz)](https://www.betterzon.xyz)
[![Blog Status](https://img.shields.io/website?label=blog.betterzon.xyz&style=for-the-badge&url=https%3A%2F%2Fblog.betterzon.xyz)](https://blog.betterzon.xyz)
[![Website Status](https://img.shields.io/website?label=betterzon.p4ddy.com&style=for-the-badge&url=https%3A%2F%2Fbetterzon.p4ddy.com)](https://betterzon.p4ddy.com)
[![Blog Status](https://img.shields.io/website?label=blog.betterzon.p4ddy.com&style=for-the-badge&url=https%3A%2F%2Fblog.betterzon.p4ddy.com)](https://blog.betterzon.p4ddy.com)
+1 -1
View File
@@ -1 +1 @@
<mxfile host="app.diagrams.net" modified="2020-12-03T09:39:52.243Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36" etag="RUzsjnuqX_zRTCHSTruo" version="13.10.6" type="github"><diagram id="QFWcWedTnleHV76omDGD" name="Page-1">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</diagram></mxfile>
<mxfile host="Electron" modified="2021-06-15T10:10:23.336Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.4.5 Chrome/83.0.4103.122 Electron/9.1.0 Safari/537.36" etag="xPXaWnl_FRrq1LtBykUU" version="13.4.5" type="device"><diagram id="QFWcWedTnleHV76omDGD" name="Page-1">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</diagram></mxfile>

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