Compare commits

...

14 Commits

Author SHA1 Message Date
Patrick
45b5e04442
Merge branch 'develop' into BETTERZON-140 2021-06-17 22:55:30 +02:00
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
a6a5b58e25 wip: profile 2021-06-17 17:52:27 +02:00
012de346e8 Merge remote-tracking branch 'origin/develop' into BETTERZON-140 2021-06-17 17:19:07 +02:00
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
0cd1213c40 BETTERZON-146: Session handling rewrite HOTFIX 2 2021-06-15 12:09:04 +02:00
1fd115c2a2 BETTERZON-146: Session handling rewrite HOTFIX 2021-06-15 11:50:36 +02:00
41 changed files with 2449 additions and 2036 deletions

View File

@ -25,8 +25,8 @@ crawlingstatusRouter.get('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.body.session_id; const session_id = (req.query.session_id ?? '').toString();
const session_key = req.body.session_key; const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip); const user = await UserService.checkSession(session_id, session_key, user_ip);
if (!user.is_admin) { if (!user.is_admin) {

View File

@ -24,8 +24,8 @@ favoriteshopsRouter.get('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id; const session_id = (req.query.session_id ?? '').toString();
const session_key = req.params.session_key; const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip); const user = await UserService.checkSession(session_id, session_key, user_ip);
const priceAlarms = await FavoriteShopsService.getFavoriteShops(user.user_id); const priceAlarms = await FavoriteShopsService.getFavoriteShops(user.user_id);
@ -76,8 +76,8 @@ favoriteshopsRouter.delete('/:id', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id; const session_id = (req.query.session_id ?? '').toString();
const session_key = req.params.session_key; const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip); const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation // Get info for price alarm creation

View File

@ -24,8 +24,8 @@ pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id; const session_id = (req.query.session_id ?? '').toString();
const session_key = req.params.session_key; const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip); const user = await UserService.checkSession(session_id, session_key, user_ip);
const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id); const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id);
@ -106,3 +106,29 @@ pricealarmsRouter.put('/', async (req: Request, res: Response) => {
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
} }
}); });
// 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.'}));
}
});

View File

@ -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();
}
}
};

View File

@ -282,11 +282,11 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
'price_in_cents': lowestPrice.price_in_cents, 'price_in_cents': lowestPrice.price_in_cents,
'timestamp': lowestPrice.timestamp, 'timestamp': lowestPrice.timestamp,
'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents), 'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents),
'amazonDifferencePercent': ((1 - (lowestPrice.price_in_cents / amazonPrice.price_in_cents)) * 100), 'amazonDifferencePercent': ((amazonPrice.price_in_cents / lowestPrice.price_in_cents) * 100),
}; };
// Push only deals were the amazon price is actually higher // Push only deals were the amazon price is actually higher
if (deal.amazonDifferencePercent > 0) { if (deal.amazonDifferencePercent > 0 && deal.amazonDifference > 0) {
deals.push(deal as Deal); deals.push(deal as Deal);
} }
} }

View File

@ -37,8 +37,8 @@ vendorsRouter.get('/managed', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const session_id = req.params.session_id; const session_id = (req.query.session_id ?? '').toString();
const session_key = req.params.session_key; const session_key = (req.query.session_key ?? '').toString();
const user = await UserService.checkSession(session_id, session_key, user_ip); const user = await UserService.checkSession(session_id, session_key, user_ip);
const vendors = await VendorService.getManagedShops(user.user_id); const vendors = await VendorService.getManagedShops(user.user_id);

View File

@ -8,14 +8,16 @@ import stepdefs.Preconditions;
@RunWith(Cucumber.class) @RunWith(Cucumber.class)
@CucumberOptions( @CucumberOptions(
features = {"src/test/resource/searchProduct.feature", features = {"src/test/resource/searchProduct.feature",
"src/test/resource/priceAlarms.feature"} "src/test/resource/priceAlarms.feature",
"src/test/resource/favoriteShopList.feature",
"src/test/resource/manageVendor.feature"}
) )
public class RunTest { public class RunTest {
@BeforeClass @BeforeClass
public static void setup() { public static void setup() {
Preconditions.driver= new FirefoxDriver(); Preconditions.driver = new FirefoxDriver();
} }
@AfterClass @AfterClass

View File

@ -0,0 +1,27 @@
package stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
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 {
}
@When("^the user clicks on favorite shops$")
public void the_user_clicks_on_favorite_shops() throws Exception {
}
@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_enty() throws Exception {
}
@Then("^the favorite shop entry should be deleted$")
public void the_favorite_shop_entry_should_be_deleted() throws Exception {
}
}

View File

@ -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 {
}
}

View File

@ -16,7 +16,7 @@ public class SearchProduct {
//throw new PendingException(); //throw new PendingException();
Preconditions.driver.get("https://betterzon.xyz"); Preconditions.driver.get("https://betterzon.xyz");
WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".navbar-brand")));
} }
@When("^the user enters the search term \"([^\"]*)\" and clicks search$") @When("^the user enters the search term \"([^\"]*)\" and clicks search$")
@ -25,7 +25,7 @@ public class SearchProduct {
searchField.sendKeys(searchTerm); searchField.sendKeys(searchTerm);
searchField.sendKeys(Keys.ENTER); searchField.sendKeys(Keys.ENTER);
WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".navbar-brand")));
} }
@Then("^the user should see the error page \"([^\"]*)\"$") @Then("^the user should see the error page \"([^\"]*)\"$")

View File

@ -0,0 +1,20 @@
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 details popup should open
When the user clicks on favorite shops
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 details popup should open
When the user clicks on favorite shops
And he clicks on delete a favorite shop entry
Then the favorite shop entry should be deleted

View File

@ -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

View File

@ -10,8 +10,8 @@
</div> </div>
<!-- About Section Content--> <!-- About Section Content-->
<div class="row"> <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 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">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">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>
</div> </div>
</section> </section>

View File

@ -6,7 +6,7 @@
<div class="col-md-8 col-xs-12 col-sm-12 login_form "> <div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<h2>Konto erstellen</h2> <h2>Registration</h2>
</div> </div>
<div class="row"> <div class="row">
<form [formGroup]="form" class="form-group" (ngSubmit)="onSubmit()"> <form [formGroup]="form" class="form-group" (ngSubmit)="onSubmit()">
@ -22,17 +22,17 @@
</div> </div>
<div class="row"> <div class="row">
<!-- <span class="fa fa-lock"></span> --> <!-- <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>
<!-- <!--
<div class="row"> <div class="row">
<input type="password" name="password" id="password_repeated" class="form__input" placeholder="Kennwort bestätigen"> <input type="password" name="password" id="password_repeated" class="form__input" placeholder="Kennwort bestätigen">
</div> --> </div> -->
<div class="row"> <div class="row">
<input type="submit" value="Erstellen" class="btn_signin"> <input type="submit" value="Sign up" class="btn_signin">
</div> </div>
<div class="row"> <div class="row">
<a href="/signin">Sich anmelden</a> <p>Have an account?<a href="/signin">Log In</a></p>
</div> </div>
</form> </form>
</div> </div>

View File

@ -6,7 +6,7 @@
<div class="col-md-8 col-xs-12 col-sm-12 login_form "> <div class="col-md-8 col-xs-12 col-sm-12 login_form ">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<h2>Anmelden</h2> <h2>Sign In</h2>
</div> </div>
<div class="row"> <div class="row">
<form [formGroup]="loginForm" class="form-group" (ngSubmit)="onSubmit()"> <form [formGroup]="loginForm" class="form-group" (ngSubmit)="onSubmit()">
@ -18,12 +18,12 @@
<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>
<div class="row"> <div class="row">
<input type="submit" value="Anmelden" class="btn_signin"> <input type="submit" value="Log in" class="btn_signin">
</div> </div>
</form> </form>
</div> </div>
<div class="row"> <div class="row">
<a href="/registration">Konto erstellen</a> <p>No account yet?<a href="/registration">sign up</a></p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,24 +5,22 @@
<div class="col-lg-4 mb-5 mb-lg-0"> <div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">Location</h4> <h4 class="text-uppercase mb-4">Location</h4>
<p class="lead mb-0"> <p class="lead mb-0">
70376 Stuttgart 76133 Karlsruhe
<br /> <br />
</p> </p>
</div> </div>
<!-- Footer Social Icons--> <!-- Footer Social Icons-->
<div class="col-lg-4 mb-5 mb-lg-0"> <div class="col-lg-4 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">FOLGE UNS</h4> <h4 class="text-uppercase mb-4">FOLLOW 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="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="#!"><i class="fab fa-fw fa-twitter"></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>
<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>
</div> </div>
<!-- Footer About Text--> <!-- Footer About Text-->
<div class="col-lg-4"> <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"> <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> </p>
</div> </div>
</div> </div>

View File

@ -1,3 +1,3 @@
<div class="copyright py-4 text-center text-white"> <div class="copyright py-4 text-center text-white">
<div class="container"><small>Copyright &copy; Your Website 2021</small></div> <div class="container"><small>Copyright &copy; Betterzon 2021</small></div>
</div> </div>

View File

@ -1,7 +1,7 @@
<section class="page-section portfolio" id="top-gesuchte"> <section class="page-section portfolio" id="top-gesuchte">
<div class="container"> <div class="container">
<!-- Portfolio Section Heading--> <!-- 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-SEARCHES</h2>
<!-- Icon Divider--> <!-- Icon Divider-->
<div class="divider-custom"> <div class="divider-custom">
<div class="divider-custom-line"></div> <div class="divider-custom-line"></div>

View File

@ -81,14 +81,14 @@ export class HotDealsWidgetComponent implements OnInit {
} }
getVendors(): void { getVendors(): void {
this.productsPricesMap.keys().forEach( this.bestDealsProductIds.forEach(
key => { productId => {
const currentDeal = this.productsPricesMap[key].lowestPrice; const currentDeal = this.productsPricesMap[productId].lowestPrice;
this.apiService.getVendorById(currentDeal.vendor_id).subscribe( this.apiService.getVendorById(currentDeal.vendor_id).subscribe(
vendor => { vendor => {
this.productsPricesMap[key].vendor = vendor this.productsPricesMap[productId].vendor = vendor;
}) });
}) });
} }

View File

@ -1,7 +1,7 @@
<section class="page-section portfolio" id="unsere-kunden"> <section class="page-section portfolio" id="unsere-kunden">
<div class="container"> <div class="container">
<!-- Portfolio Section Heading--> <!-- 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--> <!-- Icon Divider-->
<div class="divider-custom"> <div class="divider-custom">
<div class="divider-custom-line"></div> <div class="divider-custom-line"></div>

View File

@ -1,3 +1,6 @@
<header class="masthead bg-transparent text-white text-center" id="w1">
</header>
<div class="productItem"> <div class="productItem">
<div class="productImageContainer"> <div class="productImageContainer">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/> <img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
@ -20,8 +23,12 @@
{{product?.short_description}} {{product?.short_description}}
</div> </div>
</div> </div>
<div class="priceAlarm" (click)="setPriceAlarm()"> <div class="priceAlarm" *ngIf="!isLoggedIn" routerLink="/signin">
Set Price Alarm Login to set a price alarm
</div>
<div class="priceAlarm" *ngIf="isLoggedIn">
<input type="search" id="s" name="price" [(ngModel)]="price">
<div (click)="setPriceAlarm()">Set Price Alarm</div>
</div> </div>
<div class="bestPriceContainer"> <div class="bestPriceContainer">
<div class="bestPrice"> <div class="bestPrice">

View File

@ -34,6 +34,8 @@ export class ProductDetailsComponent implements OnInit {
vendorMap = {}; vendorMap = {};
@ViewChild('chart') chart: ChartComponent; @ViewChild('chart') chart: ChartComponent;
public chartOptions: ChartOptions; public chartOptions: ChartOptions;
isLoggedIn: boolean;
price: any;
constructor( constructor(
private apiService: ApiService private apiService: ApiService
@ -44,6 +46,9 @@ export class ProductDetailsComponent implements OnInit {
this.getProduct(); this.getProduct();
this.getVendors(); this.getVendors();
this.getPrices(); this.getPrices();
if (this.apiService.getSessionInfoFromLocalStorage().session_id != "") {
this.isLoggedIn = true;
}
} }
getProduct(): void { getProduct(): void {
@ -119,8 +124,10 @@ export class ProductDetailsComponent implements OnInit {
} }
setPriceAlarm() { setPriceAlarm() {
this.apiService.createPriceAlarms(this.productId, 9).subscribe( this.apiService.createPriceAlarms(this.productId, this.price*100).subscribe(
alarms => console.log(alarms) alarms => console.log(alarms)
) )
} }
} }

View File

@ -1,6 +1,30 @@
<div *ngIf="products.length==0"> <div *ngIf="products.length==0">
No Products found! No Products found!
</div> </div>
<div class="container mt-5 mb-5">
<div class="d-flex justify-content-center row">
<div class="col-md-10">
<div class="row p-2 bg-white border rounded" *ngFor="let product of products">
<div class="col-md-3 mt-1"><img width="50%" class="img-fluid img-responsive rounded product-image" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"></div>
<div class="col-md-6 mt-1">
<h5>{{product.name}}</h5>
<div class="d-flex flex-row">
<p class="text-justify text-truncate para mb-0">{{product.short_description}}</p>
</div>
<div class="mt-1 mb-1 spec-1"><span></span><span class="dot"></span><span></span><span class="dot"></span><span><br></span></div>
<div class="mt-1 mb-1 spec-1"><span></span><span class="dot"></span><span></span><span class="dot"></span><span><br></span></div>
</div>
<div class="align-items-center align-content-center col-md-3 border-left mt-1">
<div class="d-flex flex-row align-items-center">
<h4 class="mr-1">${{pricesMap[product.product_id]?.price_in_cents/100}}</h4>
</div>
<div class="d-flex flex-column mt-4"><button class="btn btn-primary btn-sm" type="button" (click)="clickedProduct(product)">Details</button>
</div>
</div>
</div>
</div>
</div>
<!--
<div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)"> <div class="productItem" *ngFor="let product of products" (click)="clickedProduct(product)">
<div class="productImageContainer" *ngIf="showProductPicture===true"> <div class="productImageContainer" *ngIf="showProductPicture===true">
<img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/> <img class="productImage" src="https://www.mueller-patrick.tech/betterzon/images/{{product.image_guid}}.jpg"/>
@ -20,3 +44,4 @@
</div> </div>
</div> </div>
</div> </div>
-->

View File

@ -10,6 +10,7 @@ import {ActivatedRoute, Router} from '@angular/router';
}) })
export class ProductListComponent implements OnInit { export class ProductListComponent implements OnInit {
products: Product[] = []; products: Product[] = [];
pricesMap: any = {};
@Input() numberOfProducts: number; @Input() numberOfProducts: number;
@Input() showProductPicture: boolean; @Input() showProductPicture: boolean;
@Input() searchQuery: string; @Input() searchQuery: string;
@ -53,15 +54,35 @@ export class ProductListComponent implements OnInit {
} }
getProducts(): void { getProducts(): void {
this.apiService.getProducts().subscribe(products => this.products = products); this.apiService.getProducts().subscribe(products => {
this.products = products;
this.getPrices();
});
} }
getPrices(): void {
this.products.forEach(
product => {
this.apiService.getLowestPrices(product.product_id).subscribe(
prices => {
this.pricesMap[product.product_id] = prices[prices.length - 1];
}
);
}
);
}
getSearchedProducts(): void { getSearchedProducts(): void {
this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => this.products = products); this.apiService.getProductsByQuery(this.searchQuery).subscribe(products => {
this.products = products;
this.getPrices();
});
} }
clickedProduct(product: Product): void { clickedProduct(product: Product): void {
this.router.navigate([('/product/' + product.product_id)]); this.router.navigate([('/product/' + product.product_id)]);
} }
} }

View File

@ -0,0 +1,21 @@
.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;
}

View File

@ -1,3 +1,104 @@
<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/Delete_icon-icons.com_55931.png" (click)="delete(alarm.alarm_id)">
</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"> <div class="container" *ngIf="currentUser; else loggedOut">
<p> <p>
<strong>e-mail</strong> <strong>e-mail</strong>
@ -33,4 +134,4 @@
<ng-template #loggedOut> <ng-template #loggedOut>
Please login. Please login.
</ng-template> </ng-template>
-->

View File

@ -49,4 +49,10 @@ export class ProfileComponent implements OnInit {
} }
) )
} }
delete(id:number): void {
this.api.deletePriceAlarm(id).subscribe(
res => window.location.reload()
)
}
} }

View File

@ -10,13 +10,13 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarResponsive"> <div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto"> <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="#top-gesuchte">top-searches</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="#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">Unsere Kunden</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">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="/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">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="/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="" (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" *ngIf="isLoggedIn"><a class="nav-link py-3 px-0 px-lg-3 rounded" routerLink="/profile">profile</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -4,9 +4,9 @@ import {Router} from "@angular/router";
@Component({ @Component({
selector: 'app-top-bar', selector: 'app-top-bar',
templateUrl: './top-bar.component.html', templateUrl: './top-bar.component.html',
styleUrls: ['./top-bar.component.css'] styleUrls: ['./top-bar.component.css']
}) })
export class TopBarComponent implements OnInit { export class TopBarComponent implements OnInit {

View File

@ -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;
}

View File

@ -1,5 +1,43 @@
#mainComponents { body {
margin: 5em; background: #eee
margin-top: .5em;
margin-bottom: .5em;
} }
.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
}

View File

@ -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"> <div id="mainComponents">
<app-product-list numberOfProducts="20" [showProductPicture]="true" searchQuery="{{searchTerm}}" <app-product-list numberOfProducts="20" [showProductPicture]="true" searchQuery="{{searchTerm}}"
type="search"></app-product-list> type="search"></app-product-list>
</div> </div>
<header class="masthead bg-transparent text-white text-center">
</header>
<app-bottom-bar></app-bottom-bar> <app-bottom-bar></app-bottom-bar>

View File

@ -0,0 +1,4 @@
.header-in-page {
padding-top: calc(2rem + 20px);
padding-bottom: 6rem;
}

View File

@ -1,3 +1,10 @@
<app-top-bar></app-top-bar>
<header class="header-in-page">
</header>
<app-profile></app-profile> <app-profile></app-profile>
<header class="header-in-page">
</header>
<app-bottom-bar></app-bottom-bar>
<app-copyright></app-copyright>

View File

@ -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: 239 B

View File

@ -447,8 +447,8 @@ progress {
} }
.lead { .lead {
font-size: 1.25rem; font-size: 1.30rem;
font-weight: 300; font-weight: 400;
} }
.display-1 { .display-1 {

View File

@ -5,7 +5,7 @@ Wiki: https://github.com/Mueller-Patrick/Betterzon/wiki
# Code Quality # 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) [![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-71%25-green)](https://ci.betterzon.xyz)
# Project Status # 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) [![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)

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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because it is too large Load Diff