diff options
Diffstat (limited to 'src/app/components/post')
| -rw-r--r-- | src/app/components/post/post.component.css | 127 | ||||
| -rw-r--r-- | src/app/components/post/post.component.html | 121 | ||||
| -rw-r--r-- | src/app/components/post/post.component.ts | 210 |
3 files changed, 302 insertions, 156 deletions
diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css index 1b88c7d..1015564 100644 --- a/src/app/components/post/post.component.css +++ b/src/app/components/post/post.component.css @@ -1,134 +1,55 @@ -.post { - display: flex; - width: 98%; - - margin: .5em auto; +.left-pane { box-sizing: border-box; - padding: .5em; - background-color: var(--card-bg); - position: relative; -} - -.post:first-child { - margin-top: 0; -} - -hr { - border: 1px solid black; - width: 90%; } /* Author */ -.author { - display: flex; - margin-bottom: .2em; -} - -.author:hover { - cursor: pointer; -} - -.author > img { +.author-picture { width: 2.2em; height: 2.2em; - margin-right: .2em; -} - -.author-info > .handle { - font-size: .9em; - color: gray; } /* Content */ .content { - flex: 1; + padding: 0 var(--card-padding); } .message { - margin: .3em 0; - word-break: break-all; -} - -.bottom-post { - font-size: .5em; - color: gray; - display: flex; - align-items: center; -} - -.separator { - margin: 0 .5em; -} - -.comment-count { - font-size: 1em; + word-break: break-word; } -.comment-count > img { - height: .8em; +.post-details { + margin-bottom: calc(var(--card-padding) / 1.5); + padding: 0.2em 0; } -.message:hover, .timestamp:hover { - cursor: pointer; +.post-details > * { + margin-left: 1.1em; } -/* Rating */ - -/* Temporary, until ratings are implemented fully */ -.rating { - display: none !important; +.rating img { + height: 1.2em; + width: 1.2em; } -.rating { - display: flex; - flex-direction: column; - align-items: center; - min-height: 4.4em; - margin: auto -.1em auto 0; -} +/* Edit */ -.score { - flex: 1; - display: flex; - align-items: center; +#attachments-btns img, .file-button > input { + height: 1.4em; + width: 1.4em; } - -.vote { - display: flex; - align-items: center; - flex: 1; - - background: var(--card-bg); - font-size: 1em; - - border: 1px solid var(--card-bg); - box-sizing: border-box; - border-radius: .2em; - - } - -.vote:hover { - border: 1px solid var(--focus-color); - color: var(--focus-color); - cursor: pointer; -} - -/* Attachments */ - -.attachments { - display: flex; - width: 98%; - margin: -.3em auto .5em auto; - flex-wrap: wrap; +.file-button { + position: relative; } -.attachments:empty { - display: none; +.file-button > img { + position: absolute; + pointer-events: none; } -.attachments > * { - flex: 1; +.file-button > input { + font-size: inherit; + color: transparent; } diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html index bc0d84a..0c1cce2 100644 --- a/src/app/components/post/post.component.html +++ b/src/app/components/post/post.component.html @@ -1,49 +1,88 @@ <app-loading *ngIf="!loaded"></app-loading> -<div class="post rounded-border" *ngIf="loaded"> - <div class="content"> - <div class="author" (click)="goToAuthorProfile()"> - <img class="round-image" [src]="user.profilePictureURL"> - <div class="author-info"> - <div class="name"> - {{ user.firstName }} {{ user.lastName }} +<section class="card flex-row" [hidden]="loaded" (mouseleave)="resetShareBtn()"> + <aside class="left-pane"> + <img class="author-picture round-image hover-half-opacity" [src]="user.profilePictureURL" (click)="goToAuthorProfile()"> + </aside> + <main class="content flexible"> + <summary class="font-size-dot8 text-vertical-middle hover-half-opacity" (click)="goToAuthorProfile()"> + <span> + {{ user.firstName }} {{ user.lastName }} + </span> + <span class="fg-faded"> + @{{ user.userName }} + </span> + </summary> + <article class="message margin-top-bot-dot2" *ngIf="!editingPost"> + {{ post.message }} + </article> + <section class="flex-row flexible-children" *ngIf="!editingPost"> + <figure *ngFor="let fileURL of post.fileURLs"> + <app-post-attachment [paramURL]="fileURL"></app-post-attachment> + </figure> + </section> + <form [formGroup]="editPostFormGroup" *ngIf="editingPost"> + <textarea class="textarea-new-msg width-full border-faded-slim border-bottom-only padding-dot2" rows="1" formControlName="newPostMessage" placeholder="What's on your mind?"></textarea> + <section class="flex-row flex-justify-start align-children-center top-bot-padding-dot6ger"> + <div class="file-button hover-half-opacity click-effect"> + <img src="/assets/icons/tabler-icon-paperclip.svg"> + <input type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> </div> - <div class="handle"> - @{{ user.userName }} + </section> + </form> + <section class="flex-row bot-padding-dot6ger" *ngIf="editingPost"> + <div *ngFor="let file of files" class="form-attachment border-faded-slim flexible flex-row flex-no-wrap flex-center-align-items padding-dot2 margin-top-bot-dot2"> + <div class="flexible"> + {{ file.name ? file.name : 'Attachment' }} + </div> + <div class="flex-col hover-half-opacity border-radius-dot2 click-effect" (click)="removeAttachment(file.name)"> + <img src="/assets/icons/tabler-icon-x.svg"> </div> </div> - </div> - <div class="message" (click)="goToPostPage()"> - {{ post.message }} - </div> - <div class="bottom-post" (click)="goToPostPage()"> - <div class="timestamp"> - {{ timeCreated }} - </div> - <div class="separator"> - ║ - </div> - <div class="comment-count"> - {{ post.comments.length }} - <img src="assets/images/comment.png"> - </div> - </div> - - </div> - <div class="rating"> - <button class="vote"> - ᐃ + </section> + <button class="border-faded-slim width-full padding-dot2 lighter-hover click-effect border-radius-dot3 margin-bot-dot5" *ngIf="editingPost" (click)="editPost()"> + Update Post + </button> + <section class="post-details flex-row flex-justify-end font-size-dot7 border-faded-slim border-bottom-only"> + <time class="flex-row flex-center-align-items"> + <img class="height-font" src="/assets/icons/tabler-icon-calendar-time.svg"> + <span> + {{ timeCreated }} + </span> + </time> + <summary class="flex-row flex-center-align-items"> + <img class="height-font" src="/assets/icons/tabler-icon-message-2.svg"> + <span> + {{ post.comments.length }} + </span> + </summary> + </section> + <section class="flex-row justify-children-center align-children-center"> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="toggleEditing()"> + <img src="/assets/icons/tabler-icon-edit.svg"> + </button> + <button class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="goToPostPage()"> + <img src="/assets/icons/tabler-icon-message-2.svg"> + Comment + </button> + <button #share ngxClipboard [cbContent]="getPostLink()" class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="showCopiedMessage()"> + <img src="/assets/icons/tabler-icon-link.svg"> + Share + </button> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="deletePost()"> + <img src="/assets/icons/tabler-icon-trash.svg"> + </button> + </section> + </main> + <aside class="rating flex-col flex-center-align-items"> + <button #upvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(true)"> + <img src="/assets/icons/tabler-icon-chevron-up.svg"> </button> - <div class="score"> + <summary class="top-bot-padding-dot2"> {{ votesNumber }} - </div> - <button class="vote"> - ᐁ + </summary> + <button #downvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(false)"> + <img src="/assets/icons/tabler-icon-chevron-down.svg"> </button> - </div> -</div> -<div class="attachments"> - <div *ngFor="let fileURL of post.fileURLs"> - <app-post-attachment class="no-events" [paramURL]="fileURL"></app-post-attachment> - </div> -</div> + </aside> +</section> diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts index 387f56f..e8ba430 100644 --- a/src/app/components/post/post.component.ts +++ b/src/app/components/post/post.component.ts @@ -1,50 +1,123 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { Guid } from 'guid-typescript'; +import { CloudinaryService } from 'src/app/services/cloudinary.service'; import { PostService } from 'src/app/services/post.service'; +import { RatingService } from 'src/app/services/rating.service'; import { UserService } from 'src/app/services/user.service'; -import { User } from 'src/models/identity/user'; -import { Post } from 'src/models/post'; +import { User } from 'src/models/identity/user.model'; +import { Post } from 'src/models/post.model'; +import { TokenService } from '../../services/token.service'; @Component({ selector: 'app-post', templateUrl: './post.component.html', styleUrls: ['./post.component.css'], }) -export class PostComponent implements OnInit { +export class PostComponent implements OnInit, AfterViewInit { public loaded = false; public user: User; public post: Post; public votesNumber: number; public timeCreated: string; @Input() paramId: string; + @ViewChild('upvote') upvoteBtn: ElementRef; + @ViewChild('downvote') downvoteBtn: ElementRef; + @ViewChild('share') shareBtn: ElementRef; + private _defaultShareBtnInnerHTML: string; + private _linkShared = false; // For optimisation purposes + public loggedIn = false; + public loggedInAuthor = false; + public editingPost = false; + public files: File[]; + public editPostFormGroup: FormGroup; - constructor(private _postService: PostService, private _userService: UserService, private _router: Router) + constructor(private _postService: PostService, private _ratingServe: RatingService, private _userService: UserService, private _router: Router, private _tokenService: TokenService, private _cloudinaryService: CloudinaryService, private _fb: FormBuilder, private _elem: ElementRef, private _renderer: Renderer2) { } ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.post = this._postService.getDefaultPost(); this.user = this._userService.getDefaultUser(); + this.files = []; - this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe( - (result: object) => { + this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe({ + next: (result: object) => { Object.assign(this.post, result); + this.post.fileURLs = Object.values(result)[7]; - this.votesNumber = 23; + this.votesNumber = this.post.currentRating; this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB'); + this.loadUser(); } - ); + }); + + this.editPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); } private loadUser(): void { - this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe( - (result: object) => { + this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe({ + next: (result: object) => { Object.assign(this.user, result); + + if (this.loggedIn) { + this.loggedInAuthor = this._tokenService.getUsernameFromSessionStorageToken() === this.post.creatorUsername; + this.editPostFormGroup.get('newPostMessage')?.setValue(this.post.message); + + if (this.post.fileURLs.length > 0) { + this.loadFiles(); + return; + } + } + this.loaded = true; } - ); + }); + } + + private loadFiles(): void { + for (const fileURL of this.post.fileURLs) { + this._cloudinaryService.getFileRequest(fileURL).subscribe({ + next: (result: object) => { + const file = result as File; + const tmp = { + name: fileURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment' + }; + + Object.assign(file, tmp); + this.files.push(file); + + if (this.files.length === this.post.fileURLs.length) { + this.loaded = true; + } + } + }); + } + } + + ngAfterViewInit(): void { + if (this.loggedIn) { + this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({ + next: (x: object) => { + if (!x) { + return; + } + + const isLike: boolean = Object.values(x)[3]; + + this.changeColorOfVoteButton(isLike, !isLike); + } + }); + } + + this._defaultShareBtnInnerHTML = this.shareBtn.nativeElement.innerHTML; } goToAuthorProfile(): void { @@ -54,4 +127,117 @@ export class PostComponent implements OnInit { goToPostPage(): void { this._router.navigate(['/post/' + this.post.postId]); } + + toggleEditing(): void { + this.editingPost = !this.editingPost; + } + + onFileUpload(event: any): void { + this.files.push(...event.target.files); + this.editPostFormGroup.get('fileUpload')?.reset(); + } + + removeAttachment(fileName: string): void { + this.files = this.files.filter(x => x.name !== fileName); + } + + editPost(): void { + const newMessage = this.editPostFormGroup.get('newPostMessage')?.value; + + if (newMessage !== '') { + this._postService.putPostWithSessionStorageRequest(Guid.parse(this.paramId), newMessage, this.files).subscribe({ + next: () => { + this.reloadPage(); + } + }); + this.loaded = false; + } + } + + deletePost(): void { + this._postService.deletePostWithSessionStorage(Guid.parse(this.paramId)).subscribe({ + next: () => { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + }); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } + + votePost(isLike: boolean): void { + if (!this.loggedIn) { + this._router.navigate(['/login']); + return; + } + + this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({ + next: (x: object) => { + if (x == null) { // checks if result is null or undefined + this.createRating(isLike); + + this.changeColorOfVoteButton(isLike, !isLike); + } + else if (Object.values(x)[3] === isLike) { + this.deleteRating(Object.values(x)[0], isLike); + + this.changeColorOfVoteButton(false, false); + } + else { + this.putRating(isLike); + + this.changeColorOfVoteButton(isLike, !isLike); + } + } + }); + } + + private createRating(isLike: boolean): void { + this._ratingServe.createRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({ + next: () => { + this.votesNumber += -1 + Number(isLike) * 2; + } + }); + } + + private putRating(isLike: boolean): void { + this._ratingServe.putRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({ + next: () => { + // when false -2 + 0 wjen true -2 + 4 + this.votesNumber += -2 + Number(isLike) * 4; + } + }); + } + + private deleteRating(ratingId: string, isLike: boolean): void { + this._ratingServe.deleteRatingFromSessionStorageRequest(Guid.parse(ratingId)).subscribe({ + next: () => { + this.votesNumber += 1 - Number(isLike) * 2; + } + }); + } + + private changeColorOfVoteButton(isUpvoted: boolean, isDownvoted: boolean): void { + this._renderer.setStyle(this.upvoteBtn.nativeElement, 'backgroundColor', (isUpvoted) ? 'var(--upvote-highlight)' : 'inherit'); + this._renderer.setStyle(this.downvoteBtn.nativeElement, 'backgroundColor', (isDownvoted) ? 'var(--downvote-highlight)' : 'inherit'); + } + + resetShareBtn(): void { + if (this._linkShared) { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', this._defaultShareBtnInnerHTML); + this._linkShared = false; + } + } + + showCopiedMessage(): void { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', 'Link copied to clipboard!'); + this._linkShared = true; + } + + getPostLink(): string { + return location.origin + '/post/' + this.paramId; + } } |
