diff --git a/client/src/app/models/post.model.ts b/client/src/app/models/post.model.ts index 7678903ebbae78fe5386c16add316bf655191fc5..3b5e585148fb55ab97ccdfc8cca3f2b579b14527 100644 --- a/client/src/app/models/post.model.ts +++ b/client/src/app/models/post.model.ts @@ -1,3 +1,4 @@ +import { defaultThrottleConfig } from "rxjs/internal/operators/throttle"; import { Deserializable } from "./deserializable.model"; import { Serializable } from "./serializable.model"; @@ -10,6 +11,7 @@ export class Post implements Deserializable, Serializable { private imageUrl: string; private price: number; private categoryid: number; + private status: number; constructor(input: any = null) { if (input) { @@ -23,6 +25,7 @@ export class Post implements Deserializable, Serializable { this.imageUrl = null; this.price = null; this.categoryid = null; + this.status = 0; } } @@ -43,7 +46,8 @@ export class Post implements Deserializable, Serializable { owner: this.owner, imageUrl: this.imageUrl, price: this.price, - categoryid: this.categoryid + categoryid: this.categoryid, + status: this.status }; } @@ -110,4 +114,22 @@ export class Post implements Deserializable, Serializable { set setCategory(categoryid: number) { this.categoryid = categoryid; } + + get getStatus() { + return this.status; + } + + get getStatusInfo() { + if (this.status == 0) { + return "Open"; + } else if (this.status == 2) { + return "Draft"; + } else { + return "Closed"; + } + } + + set setStatus(status: number) { + this.status = status; + } } \ No newline at end of file diff --git a/client/src/app/models/review.model.ts b/client/src/app/models/review.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c8a748a28989835a59ef203c62c082a8c192e7a --- /dev/null +++ b/client/src/app/models/review.model.ts @@ -0,0 +1,67 @@ +import { Deserializable } from "./deserializable.model"; +import { Serializable } from "./serializable.model"; + +export class Review implements Deserializable, Serializable { + private id: number; + private userId: number; + private stars: number; + private comment: string; + + constructor(input: any = null) { + if (input) { + this.deserialize(input); + } else { + this.id = 0; + this.userId = 0; + this.stars = 0; + this.comment = null; + } + } + + deserialize(input: Object): this { + Object.assign(this, input); + return this; + } + + serialize(): Object { + return { + id: this.id, + userId: this.userId, + stars: this.stars, + comment: this.comment, + }; + } + + get getId() { + return this.id; + } + + set setId(id: number) { + this.id = id; + } + + get getUserId() { + return this.userId; + } + + set setUserId(userId: number) { + this.userId = userId; + } + + get getStars() { + return this.stars; + } + + set setStars(stars: number) { + this.stars = stars; + } + + get getComment() { + return this.comment; + } + + set setComment(comment: string) { + this.comment = comment; + } + +} \ No newline at end of file diff --git a/client/src/app/models/user.model.ts b/client/src/app/models/user.model.ts index dfff1abd10ff054840d0c091362539370328f9d9..773c10040aace297acab374762af69d22329b4b9 100644 --- a/client/src/app/models/user.model.ts +++ b/client/src/app/models/user.model.ts @@ -8,6 +8,7 @@ export class User implements Deserializable, Serializable { private password: string; private create_time: Date; private isAdmin: number; + private location: string; constructor(input: any = null) { if (input) { @@ -19,15 +20,15 @@ export class User implements Deserializable, Serializable { this.password = null; this.create_time = new Date(); this.isAdmin = 0; + this.location = null; } } deserialize(input: Object): this { Object.assign(this, input); - console.log(this); return this; } - + serialize(): Object { return { userId: this.userId, @@ -36,6 +37,7 @@ export class User implements Deserializable, Serializable { password: this.password, create_time: this.create_time, isAdmin: this.isAdmin, + location: this.location }; } @@ -84,6 +86,14 @@ export class User implements Deserializable, Serializable { } set setIsAdmin(isAdmin: number) { - isAdmin = this.isAdmin; + this.isAdmin = isAdmin; + } + + get getLocation() { + return this.location; + } + + set setLocation(location: string){ + this.location = location; } } \ No newline at end of file diff --git a/client/src/app/posts/post-details/post-details.component.html b/client/src/app/posts/post-details/post-details.component.html index bf5baa3fb03649bbb82a21fe3bf3b55dde1046bd..36b87b33831c109a91536434921ccafc44d25061 100644 --- a/client/src/app/posts/post-details/post-details.component.html +++ b/client/src/app/posts/post-details/post-details.component.html @@ -1,3 +1,16 @@ +<div *ngIf="contactPopup" class="popupWrapper"> + <div class="popupContainer"> + <h2 >{{contactUsers.length > 0 ? "Choose user sold to:" : "No user found, continue?"}}</h2> + <div class="popup"> + <div *ngFor="let user of contactUsers"> + <input type="radio" [id]="user.getUserId" name="user" [(ngModel)]="soldToUser" [value]="user.getUserId"> + <label [for]="user.getUserId">{{user.getUsername}} - {{user.getEmail}}</label> + </div> + <app-button class="ownerButton" [text]="contactUsers.length > 0 ? 'Velg bruker solgt til' : 'Marker posten solgt!'" (click)="markClosedPost()"></app-button> + <app-button class="ownerButton" text="Lukk" (click)="closePopup()"></app-button> + </div> + </div> +</div> <div class="container"> <div class="greenBox"> <h2>{{post.getTitle}}</h2> @@ -7,7 +20,7 @@ </div> <div class="ownerInfo"> - <p>Owner: {{owner.getUsername}}</p> + <u id="owner" (click)="navigateOwner()">{{owner.getUsername}}</u> <p>E-mail: {{owner.getEmail}}</p> </div> @@ -21,12 +34,18 @@ <p id="timestamp">Publisert: {{post.getTimestamp}}</p> - <div *ngIf="userId == post.getOwner" class="buttonContainer"> + <div *ngIf="userId != post.getOwner" class="buttonContainer"> + <a [href]="'mailto:'+owner.getEmail"><app-button class="ownerButton" text="Kontakt selger" (click)="contactPost()"></app-button></a> + <app-button *ngIf="userId != post.getOwner && !isFavourited" text="Legg til som favoritt ♡" (click)="addFavourite()"></app-button> + <app-button *ngIf="userId != post.getOwner && isFavourited" text="Slett fra dine favoritter" (click)="removeFavourite()"></app-button> + </div> + <div *ngIf="userId == post.getOwner || user.getIsAdmin" class="buttonContainer"> <app-button class="ownerButton" text="Rediger annonse" (click)="editPost()"></app-button> <app-button class="ownerButton" text="Slett annonse" (click)="deletePost()"></app-button> </div> - <app-button *ngIf="userId != post.getOwner && !isFavourited" text="Legg til som favoritt ♡" (click)="addFavourite()"></app-button> - <app-button *ngIf="userId != post.getOwner && isFavourited" text="Slett fra dine favoritter" (click)="removeFavourite()"></app-button> + <div *ngIf="userId == post.getOwner && post.getStatus != 1"> + <app-button class="ownerButton" text="Marker solgt" (click)="markClosePost()"></app-button> + </div> </div> </div> </div> diff --git a/client/src/app/posts/post-details/post-details.component.scss b/client/src/app/posts/post-details/post-details.component.scss index 6c4e211d72acfeb2a30e9129b279f7d0dec5e44a..a4515594cf79755734821072e177aecd38d07caf 100644 --- a/client/src/app/posts/post-details/post-details.component.scss +++ b/client/src/app/posts/post-details/post-details.component.scss @@ -1,6 +1,33 @@ +div.popupWrapper { + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + font-size: 2rem; + z-index: 100; + div.popupContainer { + display: flex; + position: relative; + flex-direction: column; + justify-content: center; + background-color: #fff; + border-radius: 10px; + padding: 30px; + padding-top: 40px; + div.popup { + padding: 20px; + } + } +} + div.container{ margin-top: 250px; - margin-bottom: 50px; + margin-bottom: 100px; font-family: 'Josefin Sans', sans-serif; div.greenBox{ @@ -20,6 +47,12 @@ div.container{ box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.5); padding: 20px; + u#owner{ + color: #222222; + cursor: pointer; + font-size: 20pt; + } + div.pinkBox{ background-color: #FFA1A1; padding: 15px; diff --git a/client/src/app/posts/post-details/post-details.component.ts b/client/src/app/posts/post-details/post-details.component.ts index f654a5a38693feaa507ffbe9c702b37ebaa5a466..7070aaee4933d850878475823a7715c91005e520 100644 --- a/client/src/app/posts/post-details/post-details.component.ts +++ b/client/src/app/posts/post-details/post-details.component.ts @@ -13,22 +13,26 @@ import { UserService } from 'src/app/users/user.service'; }) export class PostDetailsComponent implements OnInit { + contactUsers: Array<User> = []; + contactPopup: boolean = false; post: Post = new Post(); owner: User = new User(); user: User = new User(); isAdmin: number = 0; userId: number = 0; isFavourited: boolean = false; + soldToUser: number = 0; + constructor(private postService: PostService, private activatedRoute: ActivatedRoute, private router: Router, - private authService: AuthService, private userService: UserService) { } + private authService: AuthService, private userService: UserService) { } ngOnInit() { // Gets current user information this.user = this.authService.getCurrentUser(false); - + // If user is logged in, assign userId and isAdmin - this.userId = this.user.getUserId; // 0 - this.isAdmin = this.user.getIsAdmin; // 0 + this.userId = this.user.getUserId; + this.isAdmin = this.user.getIsAdmin; // Gets id parameter from URL const id = this.activatedRoute.snapshot.params["id"]; @@ -36,10 +40,8 @@ export class PostDetailsComponent implements OnInit { // Gets Post with id from database this.postService.getPost(id).then(post => { this.post = post; - - // Checks for user favourite + // Check if post is favourited this.checkFavourite(); - // Gets Post owner from database this.userService.getUser(this.post.getOwner).then(user => { this.owner = user; @@ -50,7 +52,14 @@ export class PostDetailsComponent implements OnInit { console.log(error); }); } - + + /** + * Navigates to owner's profile + */ + navigateOwner() { + this.router.navigateByUrl("/user/" + this.owner.getUserId); + } + /** * Moves to edit page */ @@ -82,11 +91,22 @@ export class PostDetailsComponent implements OnInit { this.postService.addFavourite(this.post.getId, this.userId).then(data => { console.log("Successfully added post to favourites: " + this.post.getId); this.isFavourited = true; + }); + } + } + /** + * Get users in relation postContacted in database and opens popup + */ + markClosePost() { + if (this.contactUsers.length <= 0) { + this.userService.getContactPostUsers(this.post.getId).then(userList => { + this.contactUsers = userList; }).catch(error => { console.log(error); }); } } + /** * Check if post is favourited in database */ @@ -96,6 +116,32 @@ export class PostDetailsComponent implements OnInit { this.postService.getFavourite(this.post.getId, this.userId).then(data => { console.log("Successfully received post from favourites: " + this.post.getId); this.isFavourited = data; + }); + } + } + closePopup() { + this.contactPopup = false; + } + + /** + * Add chosen user if any to reviewPost, closes post in database and navigates to profile + */ + markClosedPost() { + this.reviewPost(); + this.closePost(); + } + + /** + * Closes post in database and navigates to profile + */ + closePost() { + this.contactPopup = false; + // Check if we are the owner of the post + if (this.userId == this.post.getOwner) { + this.post.setStatus = 1; + this.postService.updatePost(this.post.getId,this.post).then(data => { + console.log("Successfully closed post: " + this.post.getId); + this.router.navigateByUrl("/profil"); }).catch(error => { console.log(error); }); @@ -110,6 +156,32 @@ export class PostDetailsComponent implements OnInit { this.postService.deleteFavourite(this.post.getId, this.userId).then(data => { console.log("Successfully removed post from favourites: " + this.post.getId); this.isFavourited = false; + }); + } + } + + /** + * Add user to postContact relation in database + */ + contactPost() { + // Check if that we are NOT the owner of the post + if (this.userId != this.post.getOwner) { + this.postService.contactPost(this.post.getId,this.user.getUserId).then(data => { + console.log("Successfully contacted post: " + this.post.getId); + }).catch(error => { + console.log(error); + }); + } + } + + /** + * Add user to postReview relation in database + */ + reviewPost() { + // Checks that user sold to is NOT the owner of the post + if (this.userId == this.post.getOwner && this.soldToUser > 0 && this.soldToUser != this.post.getOwner) { + this.postService.reviewPost(this.post.getId,this.soldToUser, -1, "").then(data => { + console.log("Successfully added user the post sold to: " + this.post.getId); }).catch(error => { console.log(error); }); diff --git a/client/src/app/posts/post-list/post-list.component.html b/client/src/app/posts/post-list/post-list.component.html index a8f794955a7e85368b19bba002dc4ba36d774bcd..598e974d7704dd249b897d677dc3e97ef438e247 100644 --- a/client/src/app/posts/post-list/post-list.component.html +++ b/client/src/app/posts/post-list/post-list.component.html @@ -2,16 +2,44 @@ <h2>ANNONSER</h2> </div> +<div class="sortContainer"> + <app-select [(inputModel)]="selectedSort" (change)="filterCategory()"> + <option value="0" selected>Sorter etter . . .</option> + <option value="1" >Pris: Lav til høy</option> + <option value="2" >Pris: Høy til lav</option> + <option value="3" >Alfabetisk: A til Å</option> + <option value="4" >Alfabetisk: Å til A</option> + <option value="5" >Nyeste først</option> + <option value="6" >Eldste først</option> + </app-select> +</div> + <div class="container"> <div class="filterContainer"> <div class="whiteBox"> - <app-select id="categorySelect" [(inputModel)]="selectedCategory" (change)="filterCategory()"> + <app-select [(inputModel)]="selectedCategory" (change)="filterCategory(true)"> <option value="0" selected>Velg kategori . . .</option> <option *ngFor="let category of categories" [value]="category.getCategoryId">{{category.getName}}</option> </app-select> + + <!-- Doesn't do anything yet --> + <div class="areaContainer"> + <u>Område</u> + <label *ngFor="let area of areas"> + <input type="checkbox" [(ngModel)]="area.checked" class="checkbox"> + {{area.name}} + </label> + </div> + + <div class="priceRangeContainer"> + <u>Pris</u> + <p>Fra kr {{priceMin}}</p> + <p id="rightText">Til kr {{priceMax}}</p> + <input type="range" min="0" [max]="postMaxPrice" [(ngModel)]="priceMin" class="priceRange" id="priceRangeMin" (change)="priceMinChange()"> + <input type="range" min="0" [max]="postMaxPrice" [(ngModel)]="priceMax" class="priceRange" id="priceRangeMax" (change)="priceMaxChange()"> + </div> </div> </div> - <div class="postContainer"> <app-post-thumbnail *ngFor="let post of allPosts" [post]="post"></app-post-thumbnail> </div> diff --git a/client/src/app/posts/post-list/post-list.component.scss b/client/src/app/posts/post-list/post-list.component.scss index c07f6a9501bb19bbb934b7de778e2c3b7968548c..f02c27b52d1108d7c215b9a0e1e98cc8b4c4e33d 100644 --- a/client/src/app/posts/post-list/post-list.component.scss +++ b/client/src/app/posts/post-list/post-list.component.scss @@ -21,7 +21,7 @@ div.container{ div.filterContainer{ background: linear-gradient(#24e072 0%, #14A35A 100%); - height: 250px; + height: 600px; padding: 15px; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); @@ -29,7 +29,67 @@ div.container{ background-color: #fff; padding: 20px; height: 100%; - box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.5); + box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.5); + display: grid; + font-family: 'Josefin Sans', sans-serif; + font-size: 16pt; + gap: 20px; + + div.areaContainer{ + display: grid; + gap: 5px; + + input.checkbox{ + width: 16px; + height: 16px; + } + } + + div.priceRangeContainer{ + u{ + display: block; + } + + p{ + display: inline; + } + + p#rightText{ + float: right; + } + + input.priceRange{ + -webkit-appearance: none; + width: 100%; + display: inline; + outline: none; + background: #aaaaaa; + height: 6px; + } + + input.priceRange::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 5px; + height: 18px; + border-radius: 40%; + background: #FFA1A1; + cursor: pointer; + } + + input#priceRangeMin::-webkit-slider-thumb { + background: #e98686; + } + + input#priceRangeMax::-webkit-slider-thumb{ + position: relative; + top: -20px; + } + + input#priceRangeMax::-webkit-slider-runnable-track { + background: white; + } + } } } @@ -39,4 +99,11 @@ div.container{ grid-template-columns: repeat(auto-fill, 384px); gap: 30px; } +} + +.sortContainer{ + width: 250px; + float: right; + margin-right: 10%; + margin-top: 5%; } \ No newline at end of file diff --git a/client/src/app/posts/post-list/post-list.component.spec.ts b/client/src/app/posts/post-list/post-list.component.spec.ts index c7ac545160f9ddba6ef7dc13b6099cb94801f788..a140249633712c816d2db6a4c7ec01b1fbd12353 100644 --- a/client/src/app/posts/post-list/post-list.component.spec.ts +++ b/client/src/app/posts/post-list/post-list.component.spec.ts @@ -1,17 +1,78 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Category } from 'src/app/models/category.model'; +import { Post } from 'src/app/models/post.model'; import { SharedModule } from 'src/app/shared/shared.module'; +import { PostService } from '../post.service'; import { PostListComponent } from './post-list.component'; describe('PostListComponent', () => { let component: PostListComponent; let fixture: ComponentFixture<PostListComponent>; + let mockPostService; beforeEach(async () => { + // PostService mock setup + mockPostService = jasmine.createSpyObj(['getAllCategories', 'getAllPosts', 'getMaxPrice', 'getPostsByCategory']); + mockPostService.getAllCategories.and.returnValue( + new Promise<Array<Category>>( + (resolve) => { + resolve([new Category({categoryid: 1, name: "Elektronikk"}), new Category({categoryid: 2, name: "Bil"})]) + }) + ); + mockPostService.getAllPosts.and.returnValue( + new Promise<Array<Post>>((resolve) => { + resolve([ + new Post({ + id: 1, + title: "Test1", + description: "TestDescription", + timestamp: 23947298234, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 1 + }), + new Post({ + id: 2, + title: "Test2", + description: "TestDescription", + timestamp: 23453246527, + owner: "user", + imageUrl: null, + price: 159, + categoryid: 2 + }) + ]); + }) + ); + mockPostService.getPostsByCategory.and.returnValue( + new Promise<Array<Post>>((resolve) => { + resolve([ + new Post({ + id: 1, + title: "Test1", + description: "TestDescription", + timestamp: 23947298234, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 1 + }) + ]); + }) + ); + mockPostService.getMaxPrice.and.returnValue( + new Promise<number>((resolve) => { + resolve(159); + }) + ); + await TestBed.configureTestingModule({ declarations: [ PostListComponent ], - imports: [ HttpClientTestingModule, SharedModule ] + imports: [ HttpClientTestingModule, SharedModule ], + providers: [ { provide: PostService, useValue: mockPostService } ] }) .compileComponents(); }); @@ -25,4 +86,33 @@ describe('PostListComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should get all posts', async () => { + // Waits for ngOnInit and checks that we get all posts + await fixture.whenStable(); + expect(mockPostService.getAllPosts).toHaveBeenCalled(); + expect(component.allPosts.length).toBe(2); + expect(component.allPosts[0].getId).toBe(1); + expect(mockPostService.getMaxPrice).toHaveBeenCalled(); + expect(component.postMaxPrice).toBe(159); + expect(component.priceMax).toBe(159); + }); + + it('should get filtered posts', async () => { + // Waits for ngOnInit and checks that we get all posts + await fixture.whenStable(); + + component.selectedCategory = 1; + component.priceMax = 50; + component.selectedSort = 2; + await component.filterCategory(true); + + expect(mockPostService.getPostsByCategory).toHaveBeenCalledWith(1, 2, 0, 159); + expect(component.allPosts.length).toBe(1); + expect(component.allPosts[0].getId).toBe(1); + + expect(mockPostService.getMaxPrice).toHaveBeenCalled(); + expect(component.postMaxPrice).toBe(159); + expect(component.priceMax).toBe(159); + }); }); diff --git a/client/src/app/posts/post-list/post-list.component.ts b/client/src/app/posts/post-list/post-list.component.ts index 3910d07fc4a50c899528a3c4377722b2de10dd86..db23443e4c205cd1b30d47444c8ab6cc72c4078a 100644 --- a/client/src/app/posts/post-list/post-list.component.ts +++ b/client/src/app/posts/post-list/post-list.component.ts @@ -14,10 +14,29 @@ export class PostListComponent implements OnInit { categories: Array<Category> = []; selectedCategory: number; + selectedSort: number = 0; + + postMaxPrice: number; + priceMin: number = 0; + priceMax: number = 0; + + checked: boolean = false; + areas: Array<any> = [ + {name: "Agder", checked: false}, + {name: "Innlandet", checked: false}, + {name: "Møre og Romsdal", checked: false}, + {name: "Nordland", checked: false}, + {name: "Oslo", checked: false}, + {name: "Rogaland", checked: false}, + {name: "Troms og Finnmark", checked: false}, + {name: "Trøndelag", checked: false}, + {name: "Vestfold og Telemark", checked: false}, + {name: "Viken", checked: false} + ]; constructor(private postService: PostService) { } - ngOnInit(): void { + ngOnInit() { // Gets all categories from database and displays them in dropdown this.postService.getAllCategories().then(categories => { this.categories = categories; @@ -31,23 +50,56 @@ export class PostListComponent implements OnInit { getPosts() { // Gets all posts from database, and displays them this.postService.getAllPosts().then(posts => { - this.allPosts = posts; + this.allPosts = posts.filter((post: Post) => post.getStatus == 0); // Filter out closed post + }).catch(error => { + console.log(error); + }); + + // Gets the maximum price for a post + this.postService.getMaxPrice(this.selectedCategory).then(maxPrice => { + this.postMaxPrice = maxPrice; + if (this.priceMax >= this.postMaxPrice || this.priceMax == 0) { + this.priceMax = this.postMaxPrice; + } }).catch(error => { console.log(error); }); } - filterCategory() { - if (this.selectedCategory > 0) { - // Gets all posts by selected category - this.postService.getPostsByCategory(this.selectedCategory).then(posts => { - this.allPosts = posts; + async filterCategory(setMaxPriceRange: boolean = false) { + // Gets the maximum price for a post + if (setMaxPriceRange) { + await this.postService.getMaxPrice(this.selectedCategory).then(maxPrice => { + this.postMaxPrice = maxPrice; + this.priceMax = this.postMaxPrice; + this.priceMin = 0; }).catch(error => { console.log(error); }); - } else { - this.getPosts(); } + + // Gets all posts by selected category + await this.postService.getPostsByCategory(this.selectedCategory, this.selectedSort, this.priceMin, this.priceMax).then(posts => { + this.allPosts = posts; + }).catch(error => { + console.log(error); + }); + } + + priceMinChange(){ + if (this.priceMin > this.priceMax) { + this.priceMin = this.priceMax; + } + + this.filterCategory(); + } + + priceMaxChange(){ + if (this.priceMax < this.priceMin) { + this.priceMax = this.priceMin; + } + + this.filterCategory(); } } diff --git a/client/src/app/posts/post.service.spec.ts b/client/src/app/posts/post.service.spec.ts index e29b014e006436d6a2b2d05a374c8ab6ad72579f..8892d17f4a09c1fce8d9bb0e5cffdc54f6d07d28 100644 --- a/client/src/app/posts/post.service.spec.ts +++ b/client/src/app/posts/post.service.spec.ts @@ -2,6 +2,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { TestBed } from '@angular/core/testing'; import { Category } from '../models/category.model'; import { Post } from '../models/post.model'; +import { Review } from '../models/review.model'; import { PostService } from './post.service'; @@ -264,7 +265,7 @@ describe('PostService', () => { describe('getPostsByCategory', () => { it('should get posts by category', () => { // Gets posts by category and checks values - service.getPostsByCategory(2).then(posts => { + service.getPostsByCategory(2, 0, 0, 100).then(posts => { for (let i = 0; i < posts.length; i++) { expect(posts[i].getId).toBe(i + 1); expect(posts[i].getTitle).toBe("Test" + (i + 1)); @@ -275,7 +276,7 @@ describe('PostService', () => { }); // Mocks and checks HTTP request - const req = httpMock.expectOne("api/post/?categoryid=2"); + const req = httpMock.expectOne("api/post/?categoryid=2&sort=0&min_price=0&max_price=100"); expect(req.request.method).toBe("GET"); req.flush({ data: [{ @@ -302,12 +303,12 @@ describe('PostService', () => { it('should reject on invalid post', () => { // Gets invalid post, should go to catch - service.getPostsByCategory(52).then(posts => { + service.getPostsByCategory(52, 0, 0, 100).then(posts => { fail(); }).catch(error => {}); // Mocks and checks HTTP request - const req = httpMock.expectOne("api/post/?categoryid=52"); + const req = httpMock.expectOne("api/post/?categoryid=52&sort=0&min_price=0&max_price=100"); expect(req.request.method).toBe("GET"); req.flush({ data: [{ @@ -321,12 +322,44 @@ describe('PostService', () => { it('should reject on http error', () => { // Gets HTTP error instead of post, should catch - service.getPostsByCategory(35).then(post => { + service.getPostsByCategory(35, 0, 0, 100).then(post => { fail(); }).catch(error => {}); // Mocks and checks HTTP request - const req = httpMock.expectOne("api/post/?categoryid=35"); + const req = httpMock.expectOne("api/post/?categoryid=35&sort=0&min_price=0&max_price=100"); + expect(req.request.method).toBe("GET"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('getMaxPrice', () => { + it('should get posts by category', () => { + // Gets posts by category and checks values + service.getMaxPrice(2).then(maxPrice => { + expect(maxPrice).toBe(99); + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/max?categoryid=2"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + maxPrice: 99 + }] + }); + }); + + it('should reject on http error', () => { + // Gets HTTP error instead of maxPrice, should catch + service.getMaxPrice(2).then(maxPrice => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/max?categoryid=2"); expect(req.request.method).toBe("GET"); req.error(new ErrorEvent("400")); }); @@ -402,5 +435,108 @@ describe('PostService', () => { req.error(new ErrorEvent("400")); }); }); -}); + describe('contactPost', () => { + it('should add post and user postContacted relation', () => { + // Add relation of post with id = 2 and user with id = 1 + service.contactPost(2, 1) + .then(data => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/contact/"); + expect(req.request.method).toBe("POST"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + // Add relation of post with id = 2 and user with id = 1, but should catch HTTP error + service.contactPost(2, 1).then(data => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/contact/"); + expect(req.request.method).toBe("POST"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('reviewPost', () => { + it('should add review', () => { + // Add review on post with id = 2, user with id = 1 and 5 stars given + service.reviewPost(2, 1, 5, "Test comment") + .then(data => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/"); + expect(req.request.method).toBe("POST"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + // Add review on post with id = 2, user with id = 1 and 5 stars given, but should catch HTTP error + service.reviewPost(2, 1, 5, "Test comment").then(data => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/"); + expect(req.request.method).toBe("POST"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('updateReview', () => { + it('should update review', () => { + let review = new Review({ + id: 2, + userId: 1, + stars: 5, + comment: "Test comment", + }); + + // Updates review with id = 2 and userId = 1 + service.updateReview(review) + .then(data => {}) + .catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/"); + expect(req.request.method).toBe("PUT"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + let review = new Review({ + id: 2, + userId: 1, + stars: 5, + comment: "Test comment", + }); + + // Updates review with id = 2 and userId = 1, but should catch HTTP error + service.updateReview(review).then(data => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/"); + expect(req.request.method).toBe("PUT"); + req.error(new ErrorEvent("400")); + }); + }); +}); \ No newline at end of file diff --git a/client/src/app/posts/post.service.ts b/client/src/app/posts/post.service.ts index 33e9357d9f1e69649bd3fed95d62afd733631dde..0230ace215fd81300b20197a2b7bf7044b471ef4 100644 --- a/client/src/app/posts/post.service.ts +++ b/client/src/app/posts/post.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Category } from '../models/category.model'; import { Post } from '../models/post.model'; +import { Review } from '../models/review.model'; @Injectable({ providedIn: 'root' @@ -11,6 +12,8 @@ export class PostService { postUrl = "api/post/"; categoryUrl = "api/category/"; favouriteUrl = "api/post/favourite/"; + contactUrl = "api/post/contact/"; + reviewUrl = "api/post/review/"; categories: Array<Category>; @@ -197,15 +200,92 @@ export class PostService { private update_post(id: number, post: Post) { return this.http.put(this.postUrl + id, post.serialize()); + } + + /** + * Contact post in database by id. + */ + contactPost(id: number, userId: number): Promise<any> { + return new Promise<any>( + (resolve, reject) => { + this.contact_post(id, userId).subscribe((data: any) => { + try { + resolve(data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private contact_post(id: number, userId: number) { + return this.http.post(this.contactUrl, {id: id, userId: userId}); } + /** + * Contact post in database by id. + */ + reviewPost(id: number, userId: number, stars: number, comment: string): Promise<any> { + return new Promise<any>( + (resolve, reject) => { + this.review_post(id, userId, stars, comment).subscribe((data: any) => { + try { + resolve(data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private review_post(id: number, userId: number, stars: number, comment: string) { + return this.http.post(this.reviewUrl, {id: id, userId: userId, stars: stars, comment: comment}); + } + + /** + * Update post in database by id. + */ + updateReview(review: Review): Promise<any> { + console.log(review); + return new Promise<any>( + (resolve, reject) => { + this.update_review(review).subscribe((data: any) => { + try { + resolve(data); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private update_review(review: Review) { + return this.http.put(this.reviewUrl, review.serialize()); + } + + /** * Get all posts in database by specified category. */ - getPostsByCategory(categoryId: number): Promise<Array<Post>> { + getPostsByCategory(categoryId: number, sort: number, minPrice: number, maxPrice: number): Promise<Array<Post>> { return new Promise<Array<Post>>( (resolve, reject) => { - this.get_posts_by_category(categoryId).subscribe((data: any) => { + this.get_posts_by_category(categoryId, sort, minPrice, maxPrice).subscribe((data: any) => { try { let outputPosts = []; for (let post of data.data) { @@ -230,8 +310,33 @@ export class PostService { ); } - private get_posts_by_category(categoryId: number) { - return this.http.get(this.postUrl, {params: {categoryid: String(categoryId)}}); + private get_posts_by_category(categoryId: number, sort: number, minPrice: number, maxPrice: number) { + return this.http.get(this.postUrl, {params: {categoryid: String(categoryId), sort: String(sort), min_price: String(minPrice), max_price: String(maxPrice)}}); + } + + /** + * Get all posts in database by specified category. + */ + getMaxPrice(categoryId: number = undefined): Promise<number> { + return new Promise<number>( + (resolve, reject) => { + this.get_max_price(categoryId).subscribe((data: any) => { + try { + resolve(data.data[0].maxPrice); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private get_max_price(categoryId: number) { + return this.http.get(this.postUrl + "max", {params: {categoryid: String(categoryId)}}); } /** diff --git a/client/src/app/users/user-guest-profile/user-guest-profile.component.html b/client/src/app/users/user-guest-profile/user-guest-profile.component.html index 162f714a4181182ef41b7ebe303b8d95d4cc20c4..82f48b038e5e275fe5fafda741a4aed188610f01 100644 --- a/client/src/app/users/user-guest-profile/user-guest-profile.component.html +++ b/client/src/app/users/user-guest-profile/user-guest-profile.component.html @@ -1,27 +1,38 @@ -<div class="profile"> - <div class="profile_info"> - <div class="titleWrapper"> - <div class="img"></div> - <div class="info"> - <p class="name">{{user.getUsername}} Doe</p> - <p class="email">{{user.getEmail}}</p> - <p class="phone_number">+47 123 45 678</p> - </div> +<div *ngIf="receivedReviewPopup" class="popupWrapper"> + <div class="popupContainer"> + <h3>Omtaler fått:</h3> + <div class="popup"> + <app-user-review-detail *ngFor="let review of receivedReviews" [review]="review"></app-user-review-detail> + <app-button class="ownerButton" text="Lukk" (click)="closePopup()"></app-button> </div> - <div class="cardWrapper"> - <div class="rating"> - Mine omtaler: - <i class="material-icons">star</i> - <i class="material-icons">star</i> - <i class="material-icons">star</i> - <i class="material-icons">star_half</i> - <i class="material-icons">star_border</i> - </div> - <div class="location"> - Geografi: Trondheim, Oslo + </div> +</div> + +<div class="profile"> + <div class="infoWrapper"> + <div class="profile_info"> + <div class="titleWrapper"> + <div class="img"></div> + <div class="info"> + <p class="name">{{user.getUsername}}</p> + <p class="email">{{user.getEmail}}</p> + <p class="phone_number">+47 123 45 678</p> + </div> </div> - <div class="description"> - <pre> + <div class="cardWrapper"> + <div class="rating"> + Mine omtaler: + <i class="material-icons">star</i> + <i class="material-icons">star</i> + <i class="material-icons">star</i> + <i class="material-icons">star_half</i> + <i class="material-icons">star_border</i> + </div> + <div class="location"> + Område: {{user.getLocation}} + </div> + <div class="description"> + <pre> Om profilen min: Fiskeboller er digg Fotball er livet @@ -30,15 +41,19 @@ Fotball er livet (=’.’) + (=’.’) = (=’.’)=’,’) (,(‘’)(‘’) (,(‘’)(‘’) (,(“)(“)(“)(“) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - </pre> + </pre> + </div> </div> + <app-button class="btn pink reviewBtn" text="Se brukerens omtaler" (click)="showReceivedUserReviews()"></app-button> </div> </div> + <div class="post_list"> - <h2>Brukerens annonser</h2> + <h2>Aktive annonser</h2> <div class="posts"> + <!-- All users posts for now :) --> <app-post-thumbnail *ngFor="let post of userPosts" [post]="post"></app-post-thumbnail> </div> <a href="#">Se flere annonser</a> </div> -</div> \ No newline at end of file +</div> diff --git a/client/src/app/users/user-guest-profile/user-guest-profile.component.scss b/client/src/app/users/user-guest-profile/user-guest-profile.component.scss index 31c2f1ec152da55bb43bf2308d2cc5285d399b9f..086d96f5f5c932e0b96e1d69c1f374165512ca1e 100644 --- a/client/src/app/users/user-guest-profile/user-guest-profile.component.scss +++ b/client/src/app/users/user-guest-profile/user-guest-profile.component.scss @@ -1,12 +1,43 @@ +div.popupWrapper { + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + font-size: 2rem; + z-index: 100; + div.popupContainer { + display: flex; + position: relative; + flex-direction: column; + justify-content: center; + background-color: #fff; + border-radius: 10px; + padding: 30px; + padding-top: 40px; + max-width: 70%; + min-width: 300px; + width: 70%; + div.popup { + padding: 20px; + } + } +} + :host > .profile { - margin-top: 300px; + margin-top: 200px; display:flex; justify-content: center; padding: 0 5% 5% 5%; font-family: 'Josefin Sans', sans-serif; transform: translateY(-100px); - .profile_info { + .infoWrapper { order: 2; + margin-top: 40px; } } div.profile_info { @@ -49,6 +80,7 @@ div.profile_info { padding: 20px 10px; pre { font-size: 0.75rem; + padding: 5px; } } } @@ -69,5 +101,18 @@ div.post_list { } & > a { align-self: flex-end; + margin-top: 20px; + } +} +div.posts { + display: grid; + grid-template-columns: repeat(auto-fill,300px); + grid-gap: 10px; +} +:host ::ng-deep app-post-thumbnail div.postthumb { + width: 300px; + & > img { + width: 280px !important; + height: 280px !important; } } \ No newline at end of file diff --git a/client/src/app/users/user-guest-profile/user-guest-profile.component.spec.ts b/client/src/app/users/user-guest-profile/user-guest-profile.component.spec.ts index 69373d90f1ccef3f25e4f69bf8f1574cc8a66680..789c4505e223114b46e74e052b9816c12f238b92 100644 --- a/client/src/app/users/user-guest-profile/user-guest-profile.component.spec.ts +++ b/client/src/app/users/user-guest-profile/user-guest-profile.component.spec.ts @@ -2,6 +2,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { Review } from 'src/app/models/review.model'; import { User } from 'src/app/models/user.model'; import { UserService } from '../user.service'; import { UserGuestProfileComponent } from './user-guest-profile.component'; @@ -9,13 +10,13 @@ import { UserGuestProfileComponent } from './user-guest-profile.component'; describe('UserGuestProfileComponent', () => { let component: UserGuestProfileComponent; let fixture: ComponentFixture<UserGuestProfileComponent>; - let mockPostService; + let mockUserService; const dateNow = new Date(); beforeEach(async () => { // UserService mock setup - mockPostService = jasmine.createSpyObj(['getUser']); - mockPostService.getUser.and.returnValue( + mockUserService = jasmine.createSpyObj(['getUser', 'getAllReceivedUserReviews']); + mockUserService.getUser.and.returnValue( new Promise<User>( (resolve) => { resolve(new User({ @@ -27,13 +28,24 @@ describe('UserGuestProfileComponent', () => { })); }) ); + mockUserService.getAllReceivedUserReviews.and.returnValue( + new Promise<Review[]>( + (resolve) => { + resolve([new Review({ + id: 2, + userId: 1, + stars: 5, + comment: "Test comment", + })]); + }) + ); await TestBed.configureTestingModule({ declarations: [ UserGuestProfileComponent ], imports: [ HttpClientTestingModule, RouterTestingModule ], providers: [ { provide: ActivatedRoute, useValue: { snapshot: {params: {id: 1}}}}, - { provide: UserService, useValue: mockPostService } + { provide: UserService, useValue: mockUserService } ] }) .compileComponents(); @@ -54,7 +66,7 @@ describe('UserGuestProfileComponent', () => { expect(component.user).not.toBeNull(); fixture.whenStable().then(() => { - expect(mockPostService.getUser).toHaveBeenCalledWith(1); + expect(mockUserService.getUser).toHaveBeenCalledWith(1); expect(component.user).toEqual(new User({ userId: 1, username: "Test", @@ -64,4 +76,11 @@ describe('UserGuestProfileComponent', () => { })); }); }); + + it('should get user received reviews', async () => { + // Waits for ngOnInit and checks that we get reviews + await fixture.whenStable(); + component.getUserReceivedReviewsByUserId(); + expect(mockUserService.getAllReceivedUserReviews).toHaveBeenCalledWith(1); + }); }); \ No newline at end of file diff --git a/client/src/app/users/user-guest-profile/user-guest-profile.component.ts b/client/src/app/users/user-guest-profile/user-guest-profile.component.ts index 2db944d2e9f6a0346c14482380edb72b501c40c4..0d2d86bca89aa0327ab099dd822ee7ac35de3973 100644 --- a/client/src/app/users/user-guest-profile/user-guest-profile.component.ts +++ b/client/src/app/users/user-guest-profile/user-guest-profile.component.ts @@ -5,6 +5,7 @@ import { Post } from 'src/app/models/post.model'; import { User } from 'src/app/models/user.model'; import { UserService } from '../user.service'; import { PostService } from 'src/app/posts/post.service'; +import { Review } from 'src/app/models/review.model'; @Component({ selector: 'app-user-guest-profile', @@ -15,6 +16,8 @@ export class UserGuestProfileComponent implements OnInit { user: User = new User(); userPosts: Array<Post> = []; + receivedReviews: Array<Review> = []; + receivedReviewPopup: boolean = false; constructor(private userService: UserService, private authService: AuthService, private postService: PostService, private activatedRoute: ActivatedRoute, private router: Router) { } @@ -28,7 +31,7 @@ export class UserGuestProfileComponent implements OnInit { // Redirects to /profile if the user is accessing their own profile const currentUser: User = this.authService.getCurrentUser(); if (currentUser.getUserId == this.user.getUserId) { - this.router.navigateByUrl("/profile"); + this.router.navigateByUrl("/profil"); } else { this.getPosts(); } @@ -48,4 +51,22 @@ export class UserGuestProfileComponent implements OnInit { console.log(error); }); } + + showReceivedUserReviews() { + this.getUserReceivedReviewsByUserId(); + this.receivedReviewPopup = true; + } + closePopup() { + this.receivedReviewPopup = false; + } + /** + * Gets all received reviews from database + */ + getUserReceivedReviewsByUserId() { + this.userService.getAllReceivedUserReviews(this.user.getUserId).then(reviews => { + this.receivedReviews = reviews; + }).catch(error => { + console.log(error); + }); + } } diff --git a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.html b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.html index f56de5d83dc93ad2b3d0a1936b8f7011f92793ec..91aaccd0f010825ccd66e60ba09bc4c2c5b7d496 100644 --- a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.html +++ b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.html @@ -1,15 +1,15 @@ <div class="registrationForm"> - <h3>Rediger bruker</h3> - - <app-input [(inputModel)]="username" label="Brukernavn" (blur)="checkForm()"></app-input> - - <app-input [(inputModel)]="email" label="Epost" (blur)="checkForm()"></app-input> - - <app-input type="password" [(inputModel)]="password" label="Passord" (blur)="checkForm()"></app-input> - - <app-input type="password" [(inputModel)]="confirm_password" label="Bekreft passord" (blur)="checkForm()"></app-input> - - <p>{{statusMessage}}</p> - - <app-button (click)="updateUser()" text="Lagre endringer"></app-button> + <div class="cardWrapper"> + <h3>Rediger bruker</h3> + <div> + <app-input [(inputModel)]="username" label="Brukernavn" (blur)="checkForm()"></app-input> + <app-input [(inputModel)]="email" label="Epost" (blur)="checkForm()"></app-input> + <app-input type="password" [(inputModel)]="password" label="Passord" (blur)="checkForm()"></app-input> + <app-input type="password" [(inputModel)]="confirm_password" label="Bekreft passord" (blur)="checkForm()"></app-input> + <p class="status">{{statusMessage}}</p> + <app-button (click)="updateUser()" text="Lagre endringer"></app-button> + <app-button (click)="deleteUser()" text="Slett bruker"></app-button> + <a href="/profil"><app-button text="Tilbake til profil"></app-button></a> + </div> + </div> </div> \ No newline at end of file diff --git a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.scss b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2050a904fea4bbce0c70a3df1c7fba5b7c52828e 100644 --- a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.scss +++ b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.scss @@ -0,0 +1,35 @@ +:host > .registrationForm { + padding: 5%; + display:flex; + justify-content: center; + margin-bottom: 50px; + h3 { + font-family: 'Josefin Sans', sans-serif; + font-size: 1.5rem; + font-weight: 500; + padding: 10px; + } + & > div.cardWrapper { + color: #fff; + display: flex; + flex-direction: column; + background: linear-gradient(90deg, #14A35A 0%, #24e072 100%); + padding: 10px; + width: 70%; + min-width: 300px; + max-width: 900px; + } +} +div.cardWrapper > div { + color: #000; + display: flex; + flex-direction: column; + background-color: #fff; + box-shadow: inset 0px 4px 4px rgb(0 0 0 / 50%); + gap: 10px; + padding: 20px 10px; +} +p.status { + margin: 10px 0 5px 5px; + font-style: italic; +} \ No newline at end of file diff --git a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.spec.ts b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.spec.ts index 0b62b113655150afda3206e24cb7b2f3c9fac85f..acaeb831344097d1a4cf13fcdd9de53b7724fab3 100644 --- a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.spec.ts +++ b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.spec.ts @@ -8,22 +8,42 @@ import { FormsModule } from '@angular/forms'; import { UserProfileEditFormComponent } from './user-profile-edit-form.component'; import { UserProfileComponent } from '../user-profile/user-profile.component'; +import { User } from 'src/app/models/user.model'; +import { AuthService } from 'src/app/authentication/auth.service'; describe('UserProfileEditFormComponent', () => { let component: UserProfileEditFormComponent; let fixture: ComponentFixture<UserProfileEditFormComponent>; let router: Router; let mockUserService; + let mockAuthService; beforeEach(async () => { + // AuthService mock setup + mockAuthService = jasmine.createSpyObj(['getCurrentUser']); + mockAuthService.getCurrentUser.and.returnValue(new User({ + userId: 4, + username: "tester", + email: "test@test.com", + password: "1234", + create_time: 513498, + isAdmin: 0 + })); + // UserService mock setup - mockUserService = jasmine.createSpyObj(['updateUser']); + mockUserService = jasmine.createSpyObj(['updateUser', 'deleteUser']); mockUserService.updateUser.and.returnValue( new Promise<string>( (resolve) => { resolve("success") }) ); + mockUserService.deleteUser.and.returnValue( + new Promise<any>( + (resolve) => { + resolve({data: []}) + }) + ); }); @@ -38,7 +58,10 @@ describe('UserProfileEditFormComponent', () => { { path: 'profil', component: UserProfileComponent} ]) ], - providers: [ { provide: UserService, useValue: mockUserService } ] + providers: [ + { provide: UserService, useValue: mockUserService }, + { provide: AuthService, useValue: mockAuthService }, + ] }) .compileComponents(); }); @@ -50,7 +73,14 @@ describe('UserProfileEditFormComponent', () => { router = TestBed.inject(Router); }); - it('should validate form', () => { + it('should validate form', async () => { + await fixture.whenStable(); + // Reset form + component.username = ""; + component.email = ""; + component.password = ""; + component.confirm_password = ""; + // Tests all if-sentences in checkForm expect(component.checkForm()).toBeFalse(); expect(component.statusMessage).toBe("Brukernavn kan ikke være tom"); @@ -76,14 +106,32 @@ describe('UserProfileEditFormComponent', () => { expect(component.statusMessage).toBe(""); }); + it('should get current user', async () => { + expect(mockAuthService.getCurrentUser).toHaveBeenCalled(); + expect(component.user).toEqual(new User({ + userId: 4, + username: "tester", + email: "test@test.com", + password: "1234", + create_time: 513498, + isAdmin: 0 + })); + }); + it('should not update invalid user', fakeAsync(() => { + // Reset form + component.username = ""; + component.email = ""; + component.password = ""; + component.confirm_password = ""; // Tests that updating should not happen when user is invalid component.updateUser(); expect(component.statusMessage).toBe("Brukernavn kan ikke være tom"); })); - it('should route after updating user', () => { - // Tests that url is changed after user is updated + it('should route after updating user', async () => { + // Waits for ngOnInit and tests that url is changed after user is updated + await fixture.whenStable(); component.username = "Username"; component.email = "Email"; component.password = "Password"; @@ -93,4 +141,11 @@ describe('UserProfileEditFormComponent', () => { expect(mockUserService.updateUser).toHaveBeenCalled(); expect(router.url).toBe('/'); }); + + it('should delete current user', async () => { + // Waits for ngOnInit and checks that we can delete the current user + await fixture.whenStable(); + component.deleteUser(); + expect(mockUserService.deleteUser).toHaveBeenCalledWith(4); + }); }); diff --git a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.ts b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.ts index 6cf2107279cb1a12efec317a20a202e0b5eecd47..6894c17223e7051e89e5fc6179351a5ddde1a51b 100644 --- a/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.ts +++ b/client/src/app/users/user-profile-edit-form/user-profile-edit-form.component.ts @@ -103,6 +103,19 @@ export class UserProfileEditFormComponent implements OnInit { setStatusMessage(message: string) { this.statusMessage = message; } + + /** + * Deletes user in database and navigates to login + */ + deleteUser() { + this.userService.deleteUser(this.user.getUserId).then(data => { + console.log("Successfully deleted user: " + this.user.getUserId); + this.authService.logout(); + this.router.navigateByUrl("/login"); + }).catch(error => { + console.log(error); + }); + } } diff --git a/client/src/app/users/user-profile/user-profile.component.html b/client/src/app/users/user-profile/user-profile.component.html index f2e4ef40d355554388e5c37d3d626fbaf2b06d09..c994e67a3a61bc73e50f9e18ec6fa6e93659fad6 100644 --- a/client/src/app/users/user-profile/user-profile.component.html +++ b/client/src/app/users/user-profile/user-profile.component.html @@ -1,10 +1,36 @@ +<div *ngIf="givenReviewPopup" class="popupWrapper"> + <div class="popupContainer"> + <div *ngIf="notGivenReviews.length > 0"> + <h3>Annonser ikke gitt omtale til:</h3> + <div class="popup"> + <app-user-review-detail *ngFor="let review of notGivenReviews" [review]="review" [editable]="givenReviewPopup"></app-user-review-detail> + </div> + </div> + <h3>Dine omtaler:</h3> + <div class="popup"> + <app-user-review-detail *ngFor="let review of givenReviews" [review]="review"></app-user-review-detail> + <app-button class="ownerButton" text="Lukk" (click)="closePopup()"></app-button> + </div> + </div> +</div> + +<div *ngIf="receivedReviewPopup" class="popupWrapper"> + <div class="popupContainer"> + <h3>Omtaler fått:</h3> + <div class="popup"> + <app-user-review-detail *ngFor="let review of receivedReviews" [review]="review"></app-user-review-detail> + <app-button class="ownerButton" text="Lukk" (click)="closePopup()"></app-button> + </div> + </div> +</div> + <div class="profile"> <div class="infoWrapper"> <div class="profile_info"> <div class="titleWrapper"> <div class="img"></div> <div class="info"> - <p class="name">{{user.getUsername}} Doe</p> + <p class="name">{{user.getUsername}}</p> <p class="email">{{user.getEmail}}</p> <p class="phone_number">+47 123 45 678</p> </div> @@ -19,7 +45,7 @@ <i class="material-icons">star_border</i> </div> <div class="location"> - Geografi: Trondheim, Oslo + Område: {{user.getLocation}} </div> <div class="description"> <pre> @@ -34,19 +60,19 @@ Fotball er livet </pre> </div> <a href="/profil/rediger"><app-button text="Rediger profil" class="btn pink"></app-button></a> + <app-button class="btn pink reviewBtn" text="Omtaler gitt" (click)="showGivenUserReviews()"></app-button> + <app-button class="btn pink reviewBtn" text="Omtaler fått" (click)="showReceivedUserReviews()"></app-button> </div> </div> </div> <div class="post_list"> <h2>Mine aktive annonser</h2> <div class="posts"> - <!-- All users posts for now :) --> <app-post-thumbnail *ngFor="let post of allPosts" [post]="post"></app-post-thumbnail> </div> <br><br> <h2>Mine favoritter</h2> <div class="posts"> - <!-- All users posts for now :) --> <app-post-thumbnail *ngFor="let post of favouritedPosts" [post]="post"></app-post-thumbnail> </div> </div> diff --git a/client/src/app/users/user-profile/user-profile.component.scss b/client/src/app/users/user-profile/user-profile.component.scss index 935f075800346ecb464477a8f06cba0152e02516..3da546fc91380c181c59e5091d596f88d8d7e878 100644 --- a/client/src/app/users/user-profile/user-profile.component.scss +++ b/client/src/app/users/user-profile/user-profile.component.scss @@ -1,3 +1,33 @@ +div.popupWrapper { + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + font-size: 2rem; + z-index: 100; + div.popupContainer { + display: flex; + position: relative; + flex-direction: column; + justify-content: center; + background-color: #fff; + border-radius: 10px; + padding: 30px; + padding-top: 40px; + max-width: 70%; + min-width: 300px; + width: 70%; + div.popup { + padding: 20px; + } + } +} + :host > .profile { margin-top: 200px; display:flex; @@ -9,6 +39,9 @@ order: 2; margin-top: 40px; } + .reviewBtn { + margin-top: 15px; + } } div.profile_info { color: #fff; @@ -37,7 +70,7 @@ div.profile_info { min-width: 80px; width: 80px; height: 80px; - background-color: coral; + background-color: #FFA1A1; border-radius: 50%; margin: 10px; } @@ -71,6 +104,7 @@ div.post_list { } & > a { align-self: flex-end; + margin-top: 20px; } } div.posts { diff --git a/client/src/app/users/user-profile/user-profile.component.spec.ts b/client/src/app/users/user-profile/user-profile.component.spec.ts index 94f4550c98276ab5c05903f6f25bb013f4e8bdb2..b1dc746db82c5b451834b7a00afaa416e32338c1 100644 --- a/client/src/app/users/user-profile/user-profile.component.spec.ts +++ b/client/src/app/users/user-profile/user-profile.component.spec.ts @@ -2,6 +2,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AuthService } from 'src/app/authentication/auth.service'; +import { Review } from 'src/app/models/review.model'; import { User } from 'src/app/models/user.model'; import { SharedModule } from 'src/app/shared/shared.module'; import { UserLoginFormComponent } from '../user-login-form/user-login-form.component'; @@ -27,13 +28,35 @@ describe('UserProfileComponent', () => { })); // UserService mock setup - mockUserService = jasmine.createSpyObj(['deleteUser']); + mockUserService = jasmine.createSpyObj(['deleteUser', 'getAllGivenReviews', 'getAllReceivedUserReviews']); mockUserService.deleteUser.and.returnValue( new Promise<any>( (resolve) => { resolve({data: []}) }) ); + mockUserService.getAllGivenReviews.and.returnValue( + new Promise<Review[]>( + (resolve) => { + resolve([new Review({ + id: 2, + userId: 1, + stars: 5, + comment: "Test comment", + })]); + }) + ); + mockUserService.getAllReceivedUserReviews.and.returnValue( + new Promise<Review[]>( + (resolve) => { + resolve([new Review({ + id: 2, + userId: 1, + stars: 5, + comment: "Test comment", + })]); + }) + ); await TestBed.configureTestingModule({ @@ -75,10 +98,17 @@ describe('UserProfileComponent', () => { })); }); - it('should delete current user', async () => { - // Waits for ngOnInit and checks that we can delete the current user + it('should get user given reviews', async () => { + // Waits for ngOnInit and checks that we get reviews + await fixture.whenStable(); + component.getUserGivenReviewsByUserId(); + expect(mockUserService.getAllGivenReviews).toHaveBeenCalledWith(4); + }); + + it('should get user received reviews', async () => { + // Waits for ngOnInit and checks that we get reviews await fixture.whenStable(); - component.deleteUser(); - expect(mockUserService.deleteUser).toHaveBeenCalledWith(4); + component.getUserReceivedReviewsByUserId(); + expect(mockUserService.getAllReceivedUserReviews).toHaveBeenCalledWith(4); }); }); diff --git a/client/src/app/users/user-profile/user-profile.component.ts b/client/src/app/users/user-profile/user-profile.component.ts index 08126d0203a2340f11584e9b74f67a7835457cce..f1cac1ef25935ea44d57ca37537ff386d2d2b99f 100644 --- a/client/src/app/users/user-profile/user-profile.component.ts +++ b/client/src/app/users/user-profile/user-profile.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from 'src/app/authentication/auth.service'; import { Post } from 'src/app/models/post.model'; +import { Review } from 'src/app/models/review.model'; import { User } from 'src/app/models/user.model'; import { PostService } from 'src/app/posts/post.service'; import { UserService } from '../user.service'; @@ -20,6 +21,12 @@ export class UserProfileComponent implements OnInit { allPosts: Array<Post> = []; favouritedPosts: Array<Post> = []; user: User = new User(); + + givenReviews: Array<Review> = []; + notGivenReviews: Array<Review> = []; + givenReviewPopup: boolean = false; + receivedReviews: Array<Review> = []; + receivedReviewPopup: boolean = false; constructor(private authService: AuthService, private userService: UserService, private postService: PostService, private router: Router) { } ngOnInit(): void { @@ -28,10 +35,25 @@ export class UserProfileComponent implements OnInit { this.getFavouritedPosts(); } - getPostsByUserId() { - // Gets all posts from database, and displays them - this.postService.getPostsByUserId(this.user.getUserId).then(posts => { - this.allPosts = posts; + showReceivedUserReviews() { + this.getUserReceivedReviewsByUserId(); + this.receivedReviewPopup = true; + } + showGivenUserReviews() { + this.getUserGivenReviewsByUserId(); + this.givenReviewPopup = true; + } + closePopup() { + this.receivedReviewPopup = false; + this.givenReviewPopup = false; + } + + /** + * Gets all received reviews from database + */ + getUserReceivedReviewsByUserId() { + this.userService.getAllReceivedUserReviews(this.user.getUserId).then(reviews => { + this.receivedReviews = reviews; }).catch(error => { console.log(error); }); @@ -48,16 +70,26 @@ export class UserProfileComponent implements OnInit { } /** - * Deletes user in database and navigates to login + * Gets all given reviews from database */ - deleteUser() { - this.userService.deleteUser(this.user.getUserId).then(data => { - console.log("Successfully deleted user: " + this.user.getUserId); - this.authService.logout(); - this.router.navigateByUrl("/login"); + getUserGivenReviewsByUserId() { + this.userService.getAllGivenReviews(this.user.getUserId).then(reviews => { + this.givenReviews = reviews.filter((review: Review) => review.getStars > -1); + this.notGivenReviews = reviews.filter((review: Review) => review.getStars == -1); }).catch(error => { console.log(error); }); } + /** + * Gets all posts from database, and displays them + */ + getPostsByUserId() { + this.postService.getPostsByUserId(this.user.getUserId).then(posts => { + this.allPosts = posts.filter((post: Post) => post.getStatus == 0); // Active posts + }).catch(error => { + console.log(error); + }); + } + } diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.html b/client/src/app/users/user-registration-form/user-registration-form.component.html index 48a94bd69b5321aba4ebf615374662c3f44303b3..06c00e23e10238a97fad560468ade1aad331b531 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.html +++ b/client/src/app/users/user-registration-form/user-registration-form.component.html @@ -9,6 +9,20 @@ <app-input [(inputModel)]="username" label="Brukernavn" (blur)="checkForm()"></app-input> <app-input [(inputModel)]="email" label="Epost" (blur)="checkForm()"></app-input> <app-input [(inputModel)]="phone_number" label="Mobilnummer" (blur)="checkForm()"></app-input> + <app-select [(inputModel)]="location"> + <option selected>Velg fylke . . .</option> + <option>Agder</option> + <option>Innlandet</option> + <option>Møre og Romsdal</option> + <option>Nordland</option> + <option>Oslo</option> + <option>Rogaland</option> + <option>Troms og Finnmark</option> + <option>Trøndelag</option> + <option>Vestfold og Telemark</option> + <option>Vestland</option> + <option>Viken</option> + </app-select> <app-input [(inputModel)]="password" type="password" label="Passord" (blur)="checkForm()"></app-input> <app-input [(inputModel)]="confirm_password" type="password" label="Bekreft passord" (blur)="checkForm()"></app-input> <p class="status">{{statusMessage}}</p> diff --git a/client/src/app/users/user-registration-form/user-registration-form.component.ts b/client/src/app/users/user-registration-form/user-registration-form.component.ts index 0a8bcdec00d02148ae25cd426f096df42d5d2d8f..f25cac2c21b561a15add323483c7c311783adf0f 100644 --- a/client/src/app/users/user-registration-form/user-registration-form.component.ts +++ b/client/src/app/users/user-registration-form/user-registration-form.component.ts @@ -15,6 +15,7 @@ export class UserRegistrationFormComponent implements OnInit { username: string = ""; email: string = ""; phone_number: string = ""; + location: string = "Velg fylke . . ."; password: string = ""; confirm_password: string = ""; @@ -49,6 +50,10 @@ export class UserRegistrationFormComponent implements OnInit { this.setStatusMessage("Mobilnummer kan ikke være tom"); return false; } + else if (this.location == "Velg fylke . . .") { + this.setStatusMessage("Fylke må være valgt"); + return false; + } else if (this.password == "") { this.setStatusMessage("Passordet kan ikke være tom"); return false; @@ -76,6 +81,7 @@ export class UserRegistrationFormComponent implements OnInit { email: this.email, password: this.password, isAdmin: 0, + location: this.location }); // Adds user to database and redirects to the homepage afterwards diff --git a/client/src/app/users/user-review-detail/user-review-detail.component.html b/client/src/app/users/user-review-detail/user-review-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..33d30d9e917dcecd520e5d0fa2aea9ed2ee3a110 --- /dev/null +++ b/client/src/app/users/user-review-detail/user-review-detail.component.html @@ -0,0 +1,20 @@ +<div class="review"> + <div> + <a [href]="'/user/'+review.getUserId"><div class="img"></div></a> + <div> + <a [href]="'/user/'+review.getUserId"><p>SellPoint bruker</p></a> + <div [class]="'stars ' + (editable ? 'edit' : '')"> + <i class="material-icons" (click)="updateStars(1)">{{stars >= 1 ? 'star' : 'star_border'}}</i> + <i class="material-icons" (click)="updateStars(2)">{{stars >= 2 ? 'star' : 'star_border'}}</i> + <i class="material-icons" (click)="updateStars(3)">{{stars >= 3 ? 'star' : 'star_border'}}</i> + <i class="material-icons" (click)="updateStars(4)">{{stars >= 4 ? 'star' : 'star_border'}}</i> + <i class="material-icons" (click)="updateStars(5)">{{stars >= 5 ? 'star' : 'star_border'}}</i> + </div> + </div> + </div> + <p> + <a [href]="'/annonse/'+review.getId">For annonsen: {{review.getId}}</a> + </p> + <textarea [(ngModel)]="comment" [disabled]="(editable ? false : true)">{{comment}}</textarea> + <app-button *ngIf="editable" class="btn pink" text="Oppdater omtale" (click)="updateReview()"></app-button> +</div> \ No newline at end of file diff --git a/client/src/app/users/user-review-detail/user-review-detail.component.scss b/client/src/app/users/user-review-detail/user-review-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..7791a6f5e712702f8c27d9d660beb6dd83593e70 --- /dev/null +++ b/client/src/app/users/user-review-detail/user-review-detail.component.scss @@ -0,0 +1,26 @@ +.review { + font-size: 1rem; + & > div { + display: flex; + align-items: center; + } + & > p { + margin-bottom: 10px; + } + div.img { + min-width: 50px; + width: 50px; + height: 50px; + background-color: #FFA1A1; + border-radius: 50%; + margin: 10px; + } + textarea { + margin: 0px; + width: 100%; + height: 60px; + } + .edit .material-icons { + cursor: pointer; + } +} \ No newline at end of file diff --git a/client/src/app/users/user-review-detail/user-review-detail.component.spec.ts b/client/src/app/users/user-review-detail/user-review-detail.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..33a03f36f2b095e9dec81ac9a6571bcf07d74057 --- /dev/null +++ b/client/src/app/users/user-review-detail/user-review-detail.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { UserReviewDetailComponent } from './user-review-detail.component'; + +describe('UserReviewDetailComponent', () => { + let component: UserReviewDetailComponent; + let fixture: ComponentFixture<UserReviewDetailComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserReviewDetailComponent ], + imports: [ HttpClientTestingModule, RouterTestingModule ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserReviewDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + diff --git a/client/src/app/users/user-review-detail/user-review-detail.component.ts b/client/src/app/users/user-review-detail/user-review-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..745276a16a61d4bda48df60a7a757b2e1ad53e1c --- /dev/null +++ b/client/src/app/users/user-review-detail/user-review-detail.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { Review } from 'src/app/models/review.model'; +import { PostService } from 'src/app/posts/post.service'; +import { UserService } from '../user.service'; + +@Component({ + selector: 'app-user-review-detail', + templateUrl: './user-review-detail.component.html', + styleUrls: ['./user-review-detail.component.scss'] +}) +export class UserReviewDetailComponent implements OnInit { + + @Input() + review: Review = new Review(); + @Input() + editable: boolean = false; + + stars: number = Math.floor(this.review.getStars); + comment: string = this.review.getComment; + constructor(private userService: UserService, private postService: PostService, private router: Router) { } + + ngOnInit(): void { + this.comment = this.review.getComment; + this.stars = Math.floor(this.review.getStars); + } + + updateStars(stars: number) { + if (this.editable) { + this.review.setStars = stars; + this.stars = Math.floor(this.review.getStars); + } + } + + /** + * Give user reviews in database + */ + updateReview() { + if (this.review.getStars > 0) { + this.review.setComment = this.comment; + this.postService.updateReview(this.review).then(data => { + console.log("Successfully given review to post: " + this.review.getId); + }).catch(error => { + console.log(error); + }); + } + } + +} diff --git a/client/src/app/users/user.module.ts b/client/src/app/users/user.module.ts index 5fe2967b31985a280ae65715cc5740a772058ede..1eb33f9f6e76a708bd456704d8a9b4c0385e1304 100644 --- a/client/src/app/users/user.module.ts +++ b/client/src/app/users/user.module.ts @@ -8,6 +8,7 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserLoginFormComponent } from './user-login-form/user-login-form.component'; import { UserGuestProfileComponent } from './user-guest-profile/user-guest-profile.component'; import { UserProfileEditFormComponent } from './user-profile-edit-form/user-profile-edit-form.component'; +import { UserReviewDetailComponent } from './user-review-detail/user-review-detail.component'; @@ -17,7 +18,8 @@ import { UserProfileEditFormComponent } from './user-profile-edit-form/user-prof UserProfileComponent, UserLoginFormComponent, UserGuestProfileComponent, - UserProfileEditFormComponent + UserProfileEditFormComponent, + UserReviewDetailComponent ], imports: [ CommonModule, diff --git a/client/src/app/users/user.service.spec.ts b/client/src/app/users/user.service.spec.ts index f9625bfa8cabb790a2eaae9958fcf7edef91f6e9..e33b1c0963c79af16058a1396add437b93953490 100644 --- a/client/src/app/users/user.service.spec.ts +++ b/client/src/app/users/user.service.spec.ts @@ -1,5 +1,6 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; +import { Review } from '../models/review.model'; import { User } from '../models/user.model'; import { UserService } from './user.service'; @@ -107,5 +108,127 @@ describe('UserService', () => { req.error(new ErrorEvent("400")); }); }); + + describe('getAllReceivedUserReviews', () => { + it('should get users received reviews', () => { + // Gets posts by category and checks values + service.getAllReceivedUserReviews(2).then(reviews => { + for (let i = 0; i < reviews.length; i++) { + expect(reviews[i].getId).toBe(i + 1); + expect(reviews[i].getUserId).toBe(2); + expect(reviews[i].getStars).toBe(5); + expect(reviews[i].getComment).toBe("Test comment"); + } + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/received/2"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + id: 1, + userId: 2, + stars: 5, + comment: "Test comment", + }, { + id: 2, + userId: 2, + stars: 5, + comment: "Test comment", + }] + }); + }); + + it('should receive empty list on invalid user', () => { + // Gets invalid user + service.getAllReceivedUserReviews(420).then(reviews => { + reviews.length == 0 + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/received/420"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + // Gets HTTP error, should catch + service.getAllReceivedUserReviews(2).then(posts => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/received/2"); + expect(req.request.method).toBe("GET"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('getAllGivenReviews', () => { + it('should get users given reviews', () => { + // Gets posts by category and checks values + service.getAllGivenReviews(2).then(reviews => { + for (let i = 0; i < reviews.length; i++) { + expect(reviews[i].getId).toBe(i + 1); + expect(reviews[i].getUserId).toBe(2); + expect(reviews[i].getStars).toBe(5); + expect(reviews[i].getComment).toBe("Test comment"); + } + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/given/2"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + id: 1, + userId: 2, + stars: 5, + comment: "Test comment", + }, { + id: 2, + userId: 2, + stars: 5, + comment: "Test comment", + }] + }); + }); + + it('should receive empty list on invalid user', () => { + // Gets invalid user + service.getAllGivenReviews(420).then(reviews => { + reviews.length == 0 + }).catch(error => { + fail(); + }); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/given/420"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [] + }); + }); + + it('should reject on http error', () => { + // Gets HTTP error instead of post, should catch + service.getAllGivenReviews(2).then(posts => { + fail(); + }).catch(error => {}); + + // Mocks and checks HTTP request + const req = httpMock.expectOne("api/post/review/given/2"); + expect(req.request.method).toBe("GET"); + req.error(new ErrorEvent("400")); + }); + }); }); diff --git a/client/src/app/users/user.service.ts b/client/src/app/users/user.service.ts index 388ef9b791564c8aab4690f9945aaf733ba824ae..cb0d012dd7c0054d1481c34258e134c25ee1a13b 100644 --- a/client/src/app/users/user.service.ts +++ b/client/src/app/users/user.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { User } from '../models/user.model'; +import { Review } from '../models/review.model'; interface IUserLogin { username: string; @@ -11,7 +12,10 @@ interface IUserLogin { providedIn: 'root' }) export class UserService { - userUrl = "api/user/" + + userUrl = "api/user/"; + contactUrl = "api/post/contact/"; + reviewUrl = "api/post/review/"; constructor(private http: HttpClient) { } @@ -77,6 +81,39 @@ export class UserService { return this.http.get(this.userUrl); } + /** + * Get all users relating to contact post given postId from database. + */ + getContactPostUsers(postId: number): Promise<Array<User>> { + return new Promise<Array<User>>( + (resolve, reject) => { + this.get_contact_post_users(postId).subscribe((data: any) => { + try { + let outputUsers = []; // array of users + for (let user of data.data) { + outputUsers.push(new User(user)); + + if (user.getId == 0) { + reject("Could not deserialize User"); + return; + } + } + resolve(outputUsers); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + private get_contact_post_users(postId: number) { + return this.http.get(this.contactUrl+postId); + } + /** * Deletes an user from the database by id. */ @@ -122,4 +159,70 @@ export class UserService { private update_user(user: User, userId: number) { return this.http.put(this.userUrl + userId, user.serialize()); } + + /** + * Get all received user reviews from database. + */ + getAllReceivedUserReviews(userId: number): Promise<Array<Review>> { + return new Promise<Array<Review>>( + (resolve, reject) => { + this.get_all_received_user_reviews(userId).subscribe((data: any) => { + try { + let outputReview = []; // array of reviews + for (let review of data.data) { + outputReview.push(new Review(review)); + + if (review.getId == 0 || review.getUserId == 0) { + reject("Could not deserialize Review"); + return; + } + } + resolve(outputReview); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + private get_all_received_user_reviews(userId: number) { + return this.http.get(this.reviewUrl + 'received/' + userId); + } + + /** + * Get all user reviews from database. + */ + getAllGivenReviews(userId: number): Promise<Array<Review>> { + return new Promise<Array<Review>>( + (resolve, reject) => { + this.get_all_given_user_reviews(userId).subscribe((data: any) => { + try { + let outputReview = []; // array of reviews + for (let review of data.data) { + outputReview.push(new Review(review)); + + if (review.getId == 0 || review.getUserId == 0) { + reject("Could not deserialize Review"); + return; + } + } + resolve(outputReview); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + private get_all_given_user_reviews(userId: number) { + return this.http.get(this.reviewUrl + 'given/' + userId); + } } diff --git a/server/src/controllers/authController/index.ts b/server/src/controllers/authController/index.ts index cbc1157304576bc170b13b29590c0235da90f8aa..8f855f61f2e433b5a0e9de785c2800630c68808f 100644 --- a/server/src/controllers/authController/index.ts +++ b/server/src/controllers/authController/index.ts @@ -10,7 +10,7 @@ const router = express.Router(); // Post register user `/api/auth/register` router.route('/register').post(async (request: Request, response: Response) => { - const {username, email, password, isAdmin, create_time} = request.body; + const {username, email, password, isAdmin, location, create_time} = request.body; try { // Check valid request data parameters const user_data: IUser = { @@ -18,6 +18,7 @@ router.route('/register').post(async (request: Request, response: Response) => { "email": email, "password": password, "isAdmin": isAdmin || 0, + "location": location || null }; if (Object.values(user_data).filter(p => p == undefined).length > 0) return response.status(500).send("Error"); // Check for user duplicates @@ -28,7 +29,7 @@ router.route('/register').post(async (request: Request, response: Response) => { return response.status(403).send("There exists an user with the same username or emails given!"); } // If there is no duplicates, create new user - const input = (`INSERT INTO user(username, email, password, isAdmin) VALUES (?,?,?,?)`) + const input = (`INSERT INTO user(username, email, password, isAdmin, location) VALUES (?,?,?,?,?)`) return response.status(200).json( await query(input,Object.values(user_data)) ); @@ -41,7 +42,7 @@ router.route('/register').post(async (request: Request, response: Response) => { router.route('/login').post(async (request: Request, response: Response) => { const {username, password} = request.body; try { - const input = "SELECT userId, username, email, isAdmin, create_time FROM user WHERE username=? AND password=?;" + const input = "SELECT userId, username, email, isAdmin, create_time, location FROM user WHERE username=? AND password=?;" const user = await query(input,[username, password]); // Check if an user object is retrieved const userObj = Object.values(JSON.parse(JSON.stringify(user.data)))[0]; diff --git a/server/src/controllers/postController/index.ts b/server/src/controllers/postController/index.ts index bf94301c9ea473422a19a227306df9785256ca09..b2efec37dcca2e8121ff45850d42b12f62e5ba7b 100644 --- a/server/src/controllers/postController/index.ts +++ b/server/src/controllers/postController/index.ts @@ -2,6 +2,7 @@ import { Response, Request } from "express"; import query from "../../services/db_query"; import express from "express"; import IPost from "../../models/post"; +import IReview from "../../models/review"; import Category from "../../models/category"; import authenticateToken from '../../middlewares/auth'; @@ -10,7 +11,6 @@ const category = new Category(); /* ============================= CREATE ============================= */ // Create posts `/api/post/` -//'{"title":"test3","description":"test3","timestamp":123123,"owner":"test3","category":"test3","imageUrl":"test3"}' router.route("/").post(async (request: Request, response: Response) => { const { title, @@ -20,6 +20,7 @@ router.route("/").post(async (request: Request, response: Response) => { owner, categoryid, imageUrl, + status, } = request.body; try { const post: IPost = { @@ -30,11 +31,12 @@ router.route("/").post(async (request: Request, response: Response) => { owner: owner, categoryid: categoryid, imageUrl: imageUrl, + status: status, }; if (Object.values(post).filter((p) => p == undefined).length > 0) return response.status(500).send("Error"); - const input = `INSERT INTO post(title, description, price, timestamp, owner, categoryid, imageUrl) VALUES (?,?,?,?,?,?,?)`; + const input = `INSERT INTO post(title, description, price, timestamp, owner, categoryid, imageUrl, status) VALUES (?,?,?,?,?,?,?,?)`; return response.status(200).json(await query(input, Object.values(post))); } catch (error) { return response.status(400).send("Bad Request"); @@ -60,32 +62,130 @@ router.route("/favourite").post(authenticateToken, async (request: Request, resp } }); +// Contact post with body id and userId`/api/post/contact/` +router.route("/contact").post(authenticateToken, async (request: Request, response: Response) => { + const {id, userId} = request.body; + if (!(id && userId)) return response.status(400).send("Bad Request"); + try { + // Check for duplicates + const duplicate_input = "SELECT * FROM postContacted WHERE id=? AND userId=?;" + const contact = await query(duplicate_input,[id, userId]); + const contactObj = Object.values(JSON.parse(JSON.stringify(contact.data)))[0]; + if (contactObj) { + return response.status(200); + } + // If there is no duplicates, create new relation + return response + .status(200) + .json(await query("INSERT INTO postContacted (id, userId) VALUES (?, ?)", [id, userId])); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + +// Review post with body id, userId, stars and comment`/api/post/review/` +router.route("/review").post(authenticateToken, async (request: Request, response: Response) => { + const {id, userId, stars, comment} = request.body; + if (!(id && userId)) return response.status(400).send("Bad Request"); + try { + // Check for duplicates + const duplicate_input = "SELECT * FROM postReview WHERE id=? AND userId=?;" + const review = await query(duplicate_input,[id, userId]); + const reviewObj = Object.values(JSON.parse(JSON.stringify(review.data)))[0]; + if (reviewObj) { + return response.status(200); + } + // If there is no duplicates, create new relation + return response + .status(200) + .json(await query("INSERT INTO postReview (id, userId, stars, comment) VALUES (?, ?, ?, ?)", [id, userId, stars, comment])); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + /* ============================= READ ============================= */ -// Get all posts `/api/post/?categoryid=:categoryid&userId=:userId` +// Get all posts `/api/post/?categoryid=:categoryid&userId=:userId&sort=:sort&min_price=:min_price&max_price=:max_price` router.route("/").get(async (request: Request, response: Response) => { - const { categoryid, userId } = request.query as { [key: string]: string }; + let { categoryid, userId, sort, min_price, max_price } = request.query as { [key: string]: string }; try { - let input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl + let input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl, p.status FROM post as p`; - if (categoryid || userId) input += ` WHERE `; + + if (categoryid == "undefined" || categoryid == "0") { + categoryid = ""; + } + + if (categoryid || userId || min_price || max_price) input += ` WHERE `; const params = Object.entries({ categoryId: categoryid, owner: userId }).filter((param) => param[1]) - // Add p.categoryId = ? AND p.userId = ? respectively if it is not undefined + // Add p.categoryId = ? AND p.owner = ? respectively if it is not undefined input += params.map((param) => `p.${param[0]} = ?`).join(" AND ") - console.log(input, params.map((param) => param[1])); + + // Filters posts by price + if (min_price) { + if (categoryid) { + input += ` AND`; + } + input += ` p.price >= ${min_price}` + } + if (max_price) { + input += ` AND p.price <= ${max_price}` + } + + // Sorts posts + if (sort && sort != "0") { + switch(sort){ + case "1": + input += " ORDER BY p.price ASC" + break; + case "2": + input += " ORDER BY p.price DESC" + break; + case "3": + input += " ORDER BY p.title ASC" + break; + case "4": + input += " ORDER BY p.title DESC" + break; + case "5": + input += " ORDER BY p.timestamp DESC" + break; + case "6": + input += " ORDER BY p.timestamp ASC" + break; + } + } + response.status(200).json(await query(input, params.map((param) => param[1]))); } catch (error) { response.status(400).send("Bad Request"); } }); +// Get max post price of category `/api/post/max?categoryid=:categoryid` +router.route("/max").get(async (request: Request, response: Response) => { + const { categoryid } = request.query as { [key: string]: string }; + try { + let input = `SELECT MAX(p.price) as maxPrice FROM post as p `; + + if (categoryid && categoryid != "undefined" && categoryid != "0") { + input += `WHERE p.categoryid=?`; + } + + response.status(200).json(await query(input, [categoryid])); + } catch (error) { + response.status(400).send("Bad Request"); + } +}); + // Get post with id `/api/post/:id` router.route("/:id").get(async (request: Request, response: Response) => { const postId: string = request.params.id as string; try { - const input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl + const input = `SELECT p.id, p.title, p.description, p.price, p.timestamp, p.owner, p.categoryid, p.imageUrl, p.status FROM post as p WHERE p.id=?;`; response.status(200).json(await query(input, [postId])); @@ -119,7 +219,69 @@ router.route("/favourite/:userId").get(authenticateToken, async (request: Reques } }); +// Get users relating to contact post with params postId: `/api/post/contact/:id` +router.route("/contact/:id").get(authenticateToken, async (request: Request, response: Response) => { + const postId: string = request.params.id as string; + if (!postId) return response.status(400).send("Bad Request"); + try { + return response + .status(200) + .json(await query("SELECT U.userId, U.username, U.email, U.password, U.create_time, U.isAdmin FROM postContacted as PC INNER JOIN user as U ON PC.userId = U.userId WHERE PC.id= ?", [postId])); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + +// Get user given reviews with params postId: `/api/post/review/given/:id` +router.route("/review/given/:id").get(authenticateToken, async (request: Request, response: Response) => { + const userId: string = request.params.id as string; + if (!userId) return response.status(400).send("Bad Request"); + try { + return response + .status(200) + .json(await query("SELECT * FROM postReview WHERE userId = ?;", [userId])); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + +// Get user received reviews with params postId: `/api/post/review/received/:id` +router.route("/review/received/:id").get(authenticateToken, async (request: Request, response: Response) => { + const userId: string = request.params.id as string; + if (!userId) return response.status(400).send("Bad Request"); + try { + return response + .status(200) + .json(await query("SELECT PR.id, PR.userId, PR.stars, PR.comment FROM postReview as PR INNER JOIN post as P ON P.id = PR.id where P.owner = ?;", [userId])); + } catch (error) { + return response.status(400).send("Bad Request"); + } +}); + /* ============================= UPDATE ============================= */ +// Edit review with id `/api/review/` +router.route("/review").put(authenticateToken, async (request: Request, response: Response) => { + const { + id, + userId, + stars, + comment, + } = request.body; + try { + const review: IReview = { + id: id, + userId: userId, + stars: stars, + comment: comment + }; + response + .status(200) + .json(await query("UPDATE postReview SET stars = ?, comment = ? WHERE (id = ?) and (userId = ?);", [stars, comment, id, userId])); + } catch (error) { + response.status(400).send("Bad Request"); + } +}); + // Edit post with id `/api/post/:id` router.route("/:id").put(authenticateToken, async (request: Request, response: Response) => { const postId: string = request.params.id as string; @@ -131,6 +293,7 @@ router.route("/:id").put(authenticateToken, async (request: Request, response: R owner, categoryid, imageUrl, + status, } = request.body; try { const post: IPost = { @@ -141,16 +304,16 @@ router.route("/:id").put(authenticateToken, async (request: Request, response: R owner: owner, categoryid: categoryid, imageUrl: imageUrl, + status: status, }; response .status(200) - .json(await query("UPDATE post SET title=?, description=?, price=?, timestamp=?, categoryid=?, imageUrl=? WHERE id=?;", [title, description, price, timestamp, categoryid, imageUrl, postId])); + .json(await query("UPDATE post SET title=?, description=?, price=?, timestamp=?, categoryid=?, imageUrl=?, status=? WHERE id=?;", [title, description, price, timestamp, categoryid, imageUrl, status, postId])); } catch (error) { response.status(400).send("Bad Request"); } }); - /* ============================= DELETE ============================= */ // Remove post with id `/api/post/:id` router.route("/:id").delete(authenticateToken, async (request: Request, response: Response) => { diff --git a/server/src/controllers/userController/index.ts b/server/src/controllers/userController/index.ts index d1d41e7159f568489a84ff17fae7751d0b1cf7c3..4586bc6bdf79213a5b914fc7330a70dbaad0d7ac 100644 --- a/server/src/controllers/userController/index.ts +++ b/server/src/controllers/userController/index.ts @@ -8,18 +8,19 @@ const router = express.Router(); /* ============================= CREATE ============================= */ // Create an user `/api/user/` router.route('/').post(async (request: Request, response: Response) => { - const {username, email, password, isAdmin, create_time} = request.body; // destructuring + const {username, email, password, isAdmin, location, create_time} = request.body; // destructuring try { const user: IUser = { "username": username, "email": email, "password": password, "isAdmin": isAdmin || 0, + "location": location || null }; if (Object.values(user).filter(p => p == undefined).length > 0) return response.status(500).send("Error"); - const input = (`INSERT INTO user(username, email, password, isAdmin) VALUES (?,?,?,?)`); + const input = (`INSERT INTO user(username, email, password, isAdmin, location) VALUES (?,?,?,?,?)`); return response.status(200).json( - await query(input,Object.values(user)) + await query(input, Object.values(user)) ); } catch (error) { return response.status(400).send("Bad Request"); @@ -40,7 +41,7 @@ router.route('/').get(async (_: Request, response: Response) => { router.route('/:userId').get(async (request: Request, response: Response) => { const userId = request.params.userId; try { - const input = `SELECT userId, username, email, create_time FROM user WHERE userId=?;` + const input = `SELECT userId, username, email, create_time, location FROM user WHERE userId=?;` response.status(200).json(await query(input,[userId])); } catch (error) { response.status(400).send("Bad Request"); diff --git a/server/src/models/post.ts b/server/src/models/post.ts index f4038f8b47f04fc9decbba8abff1f8d2c0188cdd..108cb3c58690752f7b97914d604812c9b587c756 100644 --- a/server/src/models/post.ts +++ b/server/src/models/post.ts @@ -7,6 +7,7 @@ interface IPost { owner: number; categoryid: number; imageUrl: string; + status: number; } // Eksporterer IPost til bruk i andre filer. diff --git a/server/src/models/review.ts b/server/src/models/review.ts new file mode 100644 index 0000000000000000000000000000000000000000..e99f9d50f6fc133a6468a3d15bf4c4a71ff87e19 --- /dev/null +++ b/server/src/models/review.ts @@ -0,0 +1,10 @@ +// Review' modell, med typene: +interface IReview { + id: number; + userId: number; + stars: number; + comment: string; +} + +// Eksporterer IReview til bruk i andre filer. +export default IReview; diff --git a/server/src/models/user.ts b/server/src/models/user.ts index 80fd9651765a635aba14a6ca53bcf90a7392b4e8..041f4e521261456476596b4ade95202fe509454c 100644 --- a/server/src/models/user.ts +++ b/server/src/models/user.ts @@ -6,6 +6,7 @@ interface IUser{ password: string; create_time?: Date; isAdmin: number; + location: string; } export default IUser;