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/posts/post-details/post-details.component.html b/client/src/app/posts/post-details/post-details.component.html index 2629f2e372adebe0496c0ddbe2417c87a5a3c444..d1fa37802e71829a1ce8ebf7cfae7c2e6c8f674d 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,9 @@ </div> <div class="ownerInfo"> - <p>Owner: {{owner.getUsername}}</p> + <a [href]="'/user/'+owner.getUserId"> + <p>Owner: {{owner.getUsername}}</p> + </a> <p>E-mail: {{owner.getEmail}}</p> </div> @@ -21,9 +36,15 @@ <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> + </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 *ngIf="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 35223ab590dfa1bc8d7c52be7d6eddf7c312a549..f77a1b9f48cbf8ac5483b0a3a64437728185d440 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{ 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 bd2d6051038aa7e4bc9e347354d7597e5eac0a57..acd63818821a9a8e6bdaf28ddd789d5036c8efd9 100644 --- a/client/src/app/posts/post-details/post-details.component.ts +++ b/client/src/app/posts/post-details/post-details.component.ts @@ -13,11 +13,14 @@ 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; + soldToUser: number = 0; constructor(private postService: PostService, private activatedRoute: ActivatedRoute, private router: Router, private authService: AuthService, private userService: UserService) { } @@ -27,8 +30,8 @@ export class PostDetailsComponent implements OnInit { 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,7 +39,7 @@ export class PostDetailsComponent implements OnInit { // Gets Post with id from database this.postService.getPost(id).then(post => { this.post = post; - + // Gets Post owner from database this.userService.getUser(this.post.getOwner).then(user => { this.owner = user; @@ -69,4 +72,75 @@ export class PostDetailsComponent implements OnInit { }); } } + + /** + * 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); + }); + } + // Open popup + this.contactPopup = true; + } + 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); + }); + } + } + + /** + * 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 a1a13c2cade310325183e7d106262f74868f4f4f..598e974d7704dd249b897d677dc3e97ef438e247 100644 --- a/client/src/app/posts/post-list/post-list.component.html +++ b/client/src/app/posts/post-list/post-list.component.html @@ -40,7 +40,6 @@ </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.ts b/client/src/app/posts/post-list/post-list.component.ts index cfce7a1aed3aac7030a2f6eae9edb6fe38f8a941..db23443e4c205cd1b30d47444c8ab6cc72c4078a 100644 --- a/client/src/app/posts/post-list/post-list.component.ts +++ b/client/src/app/posts/post-list/post-list.component.ts @@ -50,7 +50,7 @@ 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); }); diff --git a/client/src/app/posts/post.service.spec.ts b/client/src/app/posts/post.service.spec.ts index 83e6011dcae889c355a45660a83afcf5cbeae2a8..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'; @@ -434,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 332aadac95991f5f3215b6569abe8136217823d3..074f0c6ab8d29edee2bbd7505cd32bd605f5eb4c 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' @@ -10,6 +11,8 @@ export class PostService { postUrl = "api/post/"; categoryUrl = "api/category/"; + contactUrl = "api/post/contact/"; + reviewUrl = "api/post/review/"; categories: Array<Category>; @@ -196,8 +199,85 @@ 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. */ 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..7a15fb1732f4e30af69f9e4a4271da5c7d289eda 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}} Doe</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"> + Geografi: Trondheim, Oslo + </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 7a86d7efb778090b51069d84d9bfa4c27c8b450a..7af062cf35fd2bd6d8d938f8ddbbe5ada86d7eb1 100644 --- a/client/src/app/users/user-profile/user-profile.component.html +++ b/client/src/app/users/user-profile/user-profile.component.html @@ -1,3 +1,29 @@ +<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"> @@ -33,7 +59,9 @@ Fotball er livet ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ </pre> </div> - <app-button (click)="placeholder()" text="Rediger profil" class="btn pink"></app-button> + <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> 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 b19520376a434ac3e53d887dc709765ca0d11869..aa4809bc3709abcadb5a6f62f6507f9c02ec8ff4 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'; @@ -19,35 +20,63 @@ export class UserProfileComponent implements OnInit { allPosts: 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 { this.user = this.authService.getCurrentUser(); this.getPostsByUserId(); } - placeholder() { - console.log(":)"); + + showReceivedUserReviews() { + this.getUserReceivedReviewsByUserId(); + this.receivedReviewPopup = true; } - getPostsByUserId() { - // Gets all posts from database, and displays them - this.postService.getPostsByUserId(this.user.getUserId).then(posts => { - this.allPosts = posts; + 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); }); } - /** - * 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; + }).catch(error => { + console.log(error); + }); + } + } 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/postController/index.ts b/server/src/controllers/postController/index.ts index a0cbabe0bc3a5761497f977159a9a0ec042fc0da..1fa7f55363dfe125abe0cfd5204f70dea521f5ae 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,23 +31,66 @@ 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"); } }); +// 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&sort=:sort&min_price=:min_price&max_price=:max_price` router.route("/").get(async (request: Request, response: Response) => { 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 == "undefined" || categoryid == "0") { @@ -58,8 +102,7 @@ router.route("/").get(async (request: Request, response: Response) => { 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 ") // Filters posts by price @@ -123,7 +166,7 @@ router.route("/max").get(async (request: Request, response: Response) => { 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])); @@ -132,7 +175,69 @@ router.route("/:id").get(async (request: Request, response: Response) => { } }); +// 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; @@ -144,6 +249,7 @@ router.route("/:id").put(authenticateToken, async (request: Request, response: R owner, categoryid, imageUrl, + status, } = request.body; try { const post: IPost = { @@ -154,16 +260,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/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;