diff options
Diffstat (limited to 'src/app/components')
48 files changed, 3132 insertions, 0 deletions
diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.css b/src/app/components/admin-panel-page/admin-panel-page.component.css new file mode 100644 index 0000000..1f98e20 --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.css @@ -0,0 +1,42 @@ +#content { + max-width: 22em; + justify-content: start; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +#navigation { + width: 100%; + display: flex; +} + +#navigation > * { + flex: 1; + margin-left: .4em; +} + +.submit-btn:first-of-type { + margin-left: 0 !important; +} + +#all-languages, #all-technologies { + display: flex; + flex-wrap: wrap; +} + +.flexbox { + display: flex; +} + +.flexbox > * { + flex: 1; + margin-left: 1em; +} + +.flexbox > *:first-child { + margin-left: 0; +} diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.html b/src/app/components/admin-panel-page/admin-panel-page.component.html new file mode 100644 index 0000000..980f12c --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.html @@ -0,0 +1,85 @@ +<!-- <app-loading *ngIf="!dataArrived"></app-loading> --> + +<div id="content" *ngIf="!dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="backToProfile()">ᐊ Back to profile</button> + <button class="submit-btn" (click)="backToFeed()">ᐊ Back to feed</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <hr> + <div class="scroll-standalone"> + <app-success-bar></app-success-bar> + <app-error-bar></app-error-bar> + + <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button> + <form [formGroup]="languageForm" (ngSubmit)="submitLanguages()" *ngIf="showLanguages"> + <div class="input-selection"> + <label>Create language:</label> + <input type="text" class="input-field" formControlName="languageCreate" placeholder="New language name"> + </div> + <label>Update language:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="updateLanguageOldName" placeholder="Old language name"> + <input type="text" class="input-field" formControlName="updateLanguageNewName" placeholder="New language name"> + </div> + <label>Delete language:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="deleteLanguageName" placeholder="Language name"> + </div> + <button class="submit-btn" type="submit">Modify languages</button> + <hr> + Available languages: + <div id="all-languages"> + <div class="user-language" *ngFor="let lang of availableLanguages"> + {{ lang.name }} + </div> + </div> + <hr> + </form> + + <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button> + <form [formGroup]="technologyForm" (ngSubmit)="submitTechnologies()" *ngIf="showTechnologies"> + <div class="input-selection"> + <label>Create technology:</label> + <input type="text" class="input-field" formControlName="technologyCreate" placeholder="New technology name"> + </div> + <label>Update technology:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="updateTechnologyOldName" placeholder="Old technology name"> + <input type="text" class="input-field" formControlName="updateTechnologyNewName" placeholder="New technology name"> + </div> + <label>Delete technology:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="deleteTechnologyName" placeholder="Technology name"> + </div> + <button class="submit-btn" type="submit">Modify technologies</button> + <hr> + Available technologies: + <div id="all-technologies"> + <div class="user-technology" *ngFor="let tech of availableTechnologies"> + {{ tech.name }} + </div> + </div> + <hr> + </form> + + <button type="button" class="submit-btn delete-btn" (click)="toggleDeletions()">▼ Deletions ▼</button> + <form [formGroup]="deleteForm" (ngSubmit)="submitDelete()" *ngIf="showDeletions"> + <div class="input-selection"> + <label>Delete user by Id:</label> + <input type="text" class="input-field" formControlName="deleteUser" placeholder="User Id"> + </div> + + <div class="input-selection"> + <label>Delete post by Id:</label> + <input type="text" class="input-field" formControlName="deletePost" placeholder="Post Id"> + </div> + <div class="input-selection"> + <label>Delete comment by Id:</label> + <input type="text" class="input-field" formControlName="deleteComment" placeholder="Comment Id"> + </div> + <button class="submit-btn" type="submit">Delete</button> + <hr> + </form> + </div> +</div> diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.ts b/src/app/components/admin-panel-page/admin-panel-page.component.ts new file mode 100644 index 0000000..99c0721 --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.ts @@ -0,0 +1,334 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { AppConstants } from 'src/app/app-constants.module'; +import { CommentService } from 'src/app/services/comment.service'; +import { LanguageService } from 'src/app/services/language.service'; +import { PostService } from 'src/app/services/post.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { TokenService } from 'src/app/services/token.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { Language } from 'src/models/language'; +import { Technology } from 'src/models/technology'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { SuccessBarComponent } from '../success-bar/success-bar.component'; + +@Component({ + selector: 'app-admin-panel-page', + templateUrl: './admin-panel-page.component.html', + styleUrls: ['./admin-panel-page.component.css'] +}) +export class AdminPanelPageComponent implements OnInit { + private _title = 'Admin Panel'; + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent; + public dataArrived = false; + public showLanguages = false; + public showTechnologies = false; + public showDeletions = false; + public availableLanguages: Language[]; + public availableTechnologies: Technology[]; + public languageForm: FormGroup; + public technologyForm: FormGroup; + public deleteForm: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + if (!this._tokenService.getTokenFromSessionStorage()) { + this._router.navigate(['/login']); + return; + } + + this._userService.getUserFromSessionStorageRequest().subscribe( + (result: object) => { + const user = result as User; + if (!user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME)) { + this._router.navigate(['/login']); + } + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/login']); + } + ); + + this.languageForm = this._fb.group({ + languageCreate: new FormControl(''), + updateLanguageOldName: new FormControl(''), + updateLanguageNewName: new FormControl(''), + deleteLanguageName: new FormControl('') + }); + + this.languageForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.technologyForm = this._fb.group({ + technologyCreate: new FormControl(''), + updateTechnologyOldName: new FormControl(''), + updateTechnologyNewName: new FormControl(''), + deleteTechnologyName: new FormControl('') + }); + + this.technologyForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.deleteForm = this._fb.group({ + deleteUser: new FormControl(''), + deletePost: new FormControl(''), + deleteComment: new FormControl('') + }); + + this.deleteForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.loadAvailableLanguages(); + this.loadAvailableTechnologies(); + } + + // Navigation + + backToProfile(): void { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this._router.navigate(['/login']); + } + + // Language modifying + + toggleLanguages(): void { + this.showLanguages = !this.showLanguages; + } + + submitLanguages(): void { + this.tryCreateLanguage(); + this.tryUpdateLanguage(); + this.tryDeleteLanguage(); + } + + private tryCreateLanguage(): void { + const languageCreate: string = this.languageForm.get('languageCreate')?.value; + + if (languageCreate !== '' && languageCreate !== null) { + this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully updated languages!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryUpdateLanguage(): void { + const updateLanguageOldName: string = this.languageForm.get('updateLanguageOldName')?.value; + const updateLanguageNewName: string = this.languageForm.get('updateLanguageNewName')?.value; + + if (updateLanguageOldName !== '' && updateLanguageOldName !== null && updateLanguageNewName !== '' && updateLanguageNewName !== null) { + const langId = this.availableLanguages.filter(x => x.name === updateLanguageOldName.trim())[0].id; + + this._languageService.putLanguageWithSessionStorageRequest(langId, updateLanguageNewName.trim()).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully updated languages!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteLanguage(): void { + const deleteLanguageName: string = this.languageForm.get('deleteLanguageName')?.value; + + if (deleteLanguageName !== '' && deleteLanguageName !== null) { + const langId = this.availableLanguages.filter(x => x.name === deleteLanguageName.trim())[0].id; + + this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully deleted language!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private languageModifiedSuccess(successMsg: string): void { + this.languageForm.reset(); + this._successBar.showMsg(successMsg); + this.loadAvailableLanguages(); + } + + private loadAvailableLanguages(): void { + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableLanguages = result as Language[]; + } + ); + } + + // Technology modifying + + toggleTechnologies(): void { + this.showTechnologies = !this.showTechnologies; + } + + submitTechnologies(): void { + this.tryCreateTechnology(); + this.tryUpdateTechnology(); + this.tryDeleteTechnology(); + } + + private tryCreateTechnology(): void { + const technologyCreate: string = this.technologyForm.get('technologyCreate')?.value; + + if (technologyCreate !== '' && technologyCreate !== null) { + this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully updated technologies!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryUpdateTechnology(): void { + const updateTechnologyOldName: string = this.technologyForm.get('updateTechnologyOldName')?.value; + const updateTechnologyNewName: string = this.technologyForm.get('updateTechnologyNewName')?.value; + + if (updateTechnologyOldName !== '' && updateTechnologyOldName !== null && updateTechnologyNewName !== '' && updateTechnologyNewName !== null) { + const techId = this.availableTechnologies.filter(x => x.name === updateTechnologyOldName.trim())[0].id; + + this._technologyService.putTechnologyWithSessionStorageRequest(techId, updateTechnologyNewName.trim()).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully updated technologies!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteTechnology(): void { + const deleteTechnologyName: string = this.technologyForm.get('deleteTechnologyName')?.value; + + if (deleteTechnologyName !== '' && deleteTechnologyName !== null) { + const techId = this.availableTechnologies.filter(x => x.name === deleteTechnologyName.trim())[0].id; + + this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully deleted technology!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private technologyModifiedSuccess(successMsg: string): void { + this.technologyForm.reset(); + this._successBar.showMsg(successMsg); + this.loadAvailableTechnologies(); + } + + private loadAvailableTechnologies(): void { + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableTechnologies = result as Technology[]; + } + ); + } + + // Deletions + + toggleDeletions(): void { + this.showDeletions = !this.showDeletions; + } + + submitDelete(): void { + this.tryDeleteUser(); + this.tryDeletePost(); + this.tryDeleteComment(); + } + + private tryDeleteUser(): void { + const deleteUser: string = this.deleteForm.get('deleteUser')?.value; + + if (deleteUser !== '' && deleteUser !== null) { + const userId: Guid = Guid.parse(deleteUser); + + this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted user!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeletePost(): void { + const deletePost: string = this.deleteForm.get('deletePost')?.value; + + if (deletePost !== '' && deletePost !== null) { + const postId: Guid = Guid.parse(deletePost); + + this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted user!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteComment(): void { + const deleteComment: string = this.deleteForm.get('deleteComment')?.value; + + if (deleteComment !== '' && deleteComment !== null) { + const commentId: Guid = Guid.parse(deleteComment); + + this._commentService.deleteCommentWithSessionStorage(commentId).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted comment!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private deletionSuccess(successMsg: string): void { + this.deleteForm.reset(); + this._successBar.showMsg(successMsg); + } +} diff --git a/src/app/components/comment-page/comment-page.component.css b/src/app/components/comment-page/comment-page.component.css new file mode 100644 index 0000000..b886bc1 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.css @@ -0,0 +1,27 @@ +#content { + justify-content: flex-start !important; +} + +#content > * { + width: 100%; +} + +.many-buttons { + width: 100%; + display: flex; +} + +.many-buttons > * { + flex: 1; + margin-right: .3em; +} + +.many-buttons > *:last-of-type { + margin-right: 0; +} + +.submit-btn { + max-width: 98%; + margin: 0 auto; + margin-bottom: .5em; +} diff --git a/src/app/components/comment-page/comment-page.component.html b/src/app/components/comment-page/comment-page.component.html new file mode 100644 index 0000000..2d110d6 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.html @@ -0,0 +1,14 @@ +<app-loading *ngIf="!loaded"></app-loading> + +<div id="content" *ngIf="loaded"> + <button class="submit-btn" type="submit" (click)="toPost()">ᐊ Back to post</button> + <app-comment [paramId]="commentId.toString()"></app-comment> + <div class="many-buttons" *ngIf="editable"> + <button class="submit-btn" (click)="editComment()">Edit comment</button> + <button class="submit-btn delete-btn" (click)="deleteComment()">Delete comment</button> + </div> + <form [formGroup]="editCommentFormGroup" (ngSubmit)="editComment()"> + <input type="text" *ngIf="editingComment" placeholder="New comment message" class="input-field" formControlName="newCommentMessage"> + <input type="submit" style="display: none" /> + </form> +<div> diff --git a/src/app/components/comment-page/comment-page.component.ts b/src/app/components/comment-page/comment-page.component.ts new file mode 100644 index 0000000..bd4cfe5 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.ts @@ -0,0 +1,91 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Comment } from 'src/models/comment'; + +@Component({ + selector: 'app-comment-page', + templateUrl: './comment-page.component.html', + styleUrls: ['./comment-page.component.css'] +}) +export class CommentPageComponent implements OnInit { + private _title = 'Comment'; + public loaded = false; + public loggedIn = false; + public editable = false; + public editingComment = false; + public commentId: Guid; + public comment: Comment; + public editCommentFormGroup: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _commentService: CommentService){ + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.commentId = Guid.parse(this._router.url.substring(9)); + + // Gets the post and the logged in user and compares them, + // to determine if the current post is made by the user + this._commentService.getCommentRequest(this.commentId).subscribe( + (result: object) => { + this.comment = result as Comment; + if (this.loggedIn) { + this.editable = this.comment.issuerUsername === this._tokenService.getUsernameFromSessionStorageToken(); + } + this.loaded = true; + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this.editCommentFormGroup = this._fb.group({ + newCommentMessage: new FormControl('') + }); + } + + toPost(): void { + this._router.navigate(['/post/' + this.comment.postId]); + } + + editComment(): void { + if (this._tokenService.getTokenFromSessionStorage() === '') { + this._router.navigate(['/login']); + return; + } + + if (this.editingComment) { + const newMessage = this.editCommentFormGroup.get('newCommentMessage')?.value; + if (newMessage !== '') { + console.log(this.commentId); + this._commentService.putCommentWithSessionStorageRequest(this.commentId, this.comment.postId, newMessage).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + } + } + this.editingComment = !this.editingComment; + } + + deleteComment(): void { + this._commentService.deleteCommentWithSessionStorage(this.commentId).subscribe( + (result: object) => { + this.toPost(); + } + ); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/comment/comment.component.css b/src/app/components/comment/comment.component.css new file mode 100644 index 0000000..d82f10e --- /dev/null +++ b/src/app/components/comment/comment.component.css @@ -0,0 +1,59 @@ +.comment { + display: flex; + width: 100%; + + margin: .5em auto; + box-sizing: border-box; + padding: .5em; + background-color: var(--card-bg); +} + +.comment:first-child { + margin-top: 0; +} + +/* Author */ + +.author { + display: flex; + margin-bottom: .2em; +} + +.author:hover { + cursor: pointer; +} + +.author > img { + width: 1.7em; + height: 1.7em; + margin-right: .2em; +} + +.author-info > .name { + font-size: .8em; +} + +.author-info > .handle { + font-size: .6em; + color: gray; +} + +/* Content */ + +.content { + flex: 1; +} + +.message { + margin: .3em 0; + word-break: break-all; +} + +.timestamp { + font-size: .5em; + color: gray; +} + +.message:hover, .timestamp:hover { + cursor: pointer; +} diff --git a/src/app/components/comment/comment.component.html b/src/app/components/comment/comment.component.html new file mode 100644 index 0000000..718e25c --- /dev/null +++ b/src/app/components/comment/comment.component.html @@ -0,0 +1,23 @@ +<app-loading *ngIf="!loaded"></app-loading> + +<div class="comment 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 }} + </div> + <div class="handle"> + @{{ user.userName }} + </div> + </div> + </div> + <div class="message" (click)="goToCommentPage()"> + {{ comment.message }} + </div> + <div class="timestamp" (click)="goToCommentPage()"> + {{ timeCreated }} + </div> + </div> +</div> diff --git a/src/app/components/comment/comment.component.ts b/src/app/components/comment/comment.component.ts new file mode 100644 index 0000000..5076769 --- /dev/null +++ b/src/app/components/comment/comment.component.ts @@ -0,0 +1,54 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { UserService } from 'src/app/services/user.service'; +import { Comment } from 'src/models/comment'; +import { User } from 'src/models/identity/user'; + +@Component({ + selector: 'app-comment', + templateUrl: './comment.component.html', + styleUrls: ['./comment.component.css'] +}) +export class CommentComponent implements OnInit { + public loaded = false; + public user: User; + public comment: Comment; + public timeCreated: string; + @Input() paramId: string; + + constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService) + { } + + ngOnInit(): void { + this.comment = this._commentService.getDefaultComment(); + this.user = this._userService.getDefaultUser(); + + this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe( + (result: object) => { + Object.assign(this.comment, result); + + this.timeCreated = new Date(this.comment.timeCreated).toLocaleString('en-GB'); + this.loadUser(); + } + ); + } + + private loadUser(): void { + this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe( + (result: object) => { + Object.assign(this.user, result); + this.loaded = true; + } + ); + } + + goToAuthorProfile(): void { + this._router.navigate(['/profile/' + this.comment.issuerUsername]); + } + + goToCommentPage(): void { + this._router.navigate(['/comment/' + this.comment.commentId]); + } +} diff --git a/src/app/components/error-bar/error-bar.component.css b/src/app/components/error-bar/error-bar.component.css new file mode 100644 index 0000000..8f8edd9 --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.css @@ -0,0 +1,12 @@ +#error-bar { + box-sizing: border-box; + width: 100%; + background-color: var(--failure); + color: white; + padding: .2em; + text-align: center; +} + +#error-bar:empty { + display: none; +} diff --git a/src/app/components/error-bar/error-bar.component.html b/src/app/components/error-bar/error-bar.component.html new file mode 100644 index 0000000..f1995ab --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.html @@ -0,0 +1 @@ +<div id="error-bar">{{errorMsg}}</div> diff --git a/src/app/components/error-bar/error-bar.component.ts b/src/app/components/error-bar/error-bar.component.ts new file mode 100644 index 0000000..111bac8 --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.ts @@ -0,0 +1,34 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { IApiError } from 'src/interfaces/api-error'; + +@Component({ + selector: 'app-error-bar', + templateUrl: './error-bar.component.html', + styleUrls: ['./error-bar.component.css'] +}) +export class ErrorBarComponent implements OnInit { + public errorMsg = ''; + + constructor() + { } + + ngOnInit(): void { + this.hideError(); + } + + showError(error: HttpErrorResponse): void { + const test: IApiError = { + type: '', + title: 'Error!', + status: 0, + traceId: '' + }; + Object.assign(test, error.error); + this.errorMsg = test.title; + } + + hideError(): void { + this.errorMsg = ''; + } +} diff --git a/src/app/components/feed/feed.component.css b/src/app/components/feed/feed.component.css new file mode 100644 index 0000000..cb155c6 --- /dev/null +++ b/src/app/components/feed/feed.component.css @@ -0,0 +1,179 @@ +#feed-page { + height: 100%; + max-width: 40em; + box-sizing: border-box; + border: .5em solid var(--bg-color); + + margin: 0 auto; + + display: flex; +} + +@media screen and (max-width: 750px) { + #profile-bar { + display: none !important; + } + #top-bar-profile-pic { + display: initial; + } +} +@media screen and (min-width: 750px) { + #profile-bar { + display: initial; + } + #top-bar-profile-pic { + display: none !important; + } +} + +/* Content */ + +#feed-content { + flex: 1; + display: flex; + flex-direction: column; +} + +/* Profile bar */ + +#profile-bar { + width: 20%; + height: fit-content; + display: flex; + flex-direction: column; + align-items: center; + + box-sizing: border-box; + background-color: var(--bg-color); + + margin-right: .5em; + padding-bottom: 1em; + padding: .5em; + border-radius: .6em; +} + +#profile-bar-profile-pic { + width: 7em; + height: 7em; + box-sizing: border-box; + padding: .5em; +} + +#profile-bar-name { + text-align: center; +} + +#profile-bar-username { + margin: .5em 0; +} + +#profile-bar > #profile-info { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Top bar */ + +#top-bar { + display: flex; + flex-direction: column; + width: 98%; + margin: 0 auto; + margin-bottom: .5em; + box-sizing: border-box; +} + +#top-bar-profile-pic { + height: 2.5em; + width: 2.5em; + margin-right: .5em; +} + +#top-bar-profile-pic > img { + height: inherit; + width: inherit; +} + +#top-bar-open-chat { + /* Until implemented */ + display: none; +} + +#main-content { + display: flex; +} + +/* Create post */ + +#create-post-form { + width: 100%; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +#form-inputs { + display: flex; +} + +#top-bar-create-post { + flex: 1; + font-size: inherit; + width: 100%; + height: 100%; + margin: 0 auto; + box-sizing: border-box; + border: 2px solid var(--bg-color); + border-radius: .6em; +} + +#file-upload { + font-size: inherit; + color: transparent; + width: 1.5em; + height: 1.5em; +} + +#file-upload:hover { + cursor: pointer; +} + +#file-upload::-webkit-file-upload-button { + visibility: hidden; +} + +#attachment-img { + height: 1.5em; + width: 1.5em; + position: absolute; + right: .4em; + pointer-events: none; +} + +/* Posts */ + +#no-posts-msg { + width: 100%; + margin-top: 1em; + color: gray; + text-align: center; +} + +/* Elements, that act as buttons */ + +#profile-bar > #profile-info:hover, +#top-bar-profile-pic:hover { + cursor: pointer; +} + +/* Can't copy text from */ + +#profile-bar, +.vote { + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+/Edge */ + user-select: none; /* Standard */ +} diff --git a/src/app/components/feed/feed.component.html b/src/app/components/feed/feed.component.html new file mode 100644 index 0000000..1a03dcc --- /dev/null +++ b/src/app/components/feed/feed.component.html @@ -0,0 +1,52 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="feed-page" *ngIf="dataArrived"> + <nav id="profile-bar" class="round-image rounded-border"> + <div id="profile-info" (click)="goToProfile()"> + <img id="profile-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt=""/> + <div id="profile-bar-name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div id="profile-bar-username"> + @{{ user.userName }} + </div> + </div> + <button class="submit-btn" (click)="goToSettings()">Settings</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <div id="feed-content"> + <nav id="top-bar"> + <div id="main-content"> + <img id="top-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt="" (click)="goToProfile()"> + <form id="create-post-form" class="rounded-border" [formGroup]="createPostFormGroup" (ngSubmit)="createPost()"> + <div id="form-inputs"> + <input id="top-bar-create-post" type="text" formControlName="newPostMessage" placeholder="What's on your mind?"/> + <input type="submit" style="display: none" /> <!-- You need this element, so when you press enter the request is sent --> + <img id="attachment-img" src="assets/images/paper-clip.png"> + <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> + </div> + <div class="form-attachments"> + <div *ngFor="let file of files" class="form-attachment"> + {{ file.name ? file.name : 'Attachment' }} + <div class="remove-form-attachment" (click)="removeAttachment(file.name)"> + ☒ + </div> + </div> + </div> + </form> + <a id="top-bar-open-chat" href=""> + <img src="assets/images/feed/chat-pic.png" alt=""/> + </a> + </div> + </nav> + <div id="posts" class="scroll-standalone" (scroll)="onScroll($event)"> + <div id="no-posts-msg" *ngIf="posts.length === 0"> + None of your friends have posted anything yet!<br> + Try refreshing your page! + </div> + <div *ngFor="let friendPost of posts" class="post"> + <app-post [paramId]="friendPost.postId.toString()"></app-post> + </div> + </div> + </div> +</div> diff --git a/src/app/components/feed/feed.component.ts b/src/app/components/feed/feed.component.ts new file mode 100644 index 0000000..b412b3c --- /dev/null +++ b/src/app/components/feed/feed.component.ts @@ -0,0 +1,122 @@ +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { User } from 'src/models/identity/user'; +import { UserService } from '../../services/user.service'; +import { AppConstants } from 'src/app/app-constants.module'; +import { HttpErrorResponse } from '@angular/common/http'; +import { FeedService } from 'src/app/services/feed.service'; +import { Post } from 'src/models/post'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { PostService } from 'src/app/services/post.service'; +import { TokenService } from 'src/app/services/token.service'; + +@Component({ + selector: 'app-feed', + templateUrl: './feed.component.html', + styleUrls: ['./feed.component.css'] +}) +export class FeedComponent implements OnInit { + private _title = 'Feed'; + private _timeLoaded: string; // we send the time to the api as a string + private _currentPage: number; + public dataArrived = false; + public user: User; + public posts: Post[]; + public createPostFormGroup: FormGroup; + public files: File[]; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _feedService: FeedService, private _postService: PostService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + if (!this._tokenService.getTokenFromSessionStorage()) { + this._router.navigate(['/login']); + return; + } + + this._currentPage = 1; + this.posts = []; + this.user = this._userService.getDefaultUser(); + this.files = []; + + const now = new Date(); + now.setHours(now.getHours() + 2); // accounting for eastern european timezone + this._timeLoaded = now.toISOString(); + + this.createPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (res: object) => { + Object.assign(this.user, res); + this.loadFeed(); + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + + private loadFeed(): void { + this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( + (result: object) => { + this.posts.push(...Object.values(result)[0]); + this.finishUserLoading(); + }, + (err) => { + this.finishUserLoading(); + } + ); + } + + private finishUserLoading(): void { + this.dataArrived = true; + } + + goToProfile(): void { + this._router.navigate(['/profile/' + this.user.userName]); + } + + goToSettings(): void { + this._router.navigate(['/profile/' + this.user.userName + '/settings']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this._router.navigate(['/login']); + } + + onFileUpload(event: any): void { + this.files.push(...event.target.files); + this.createPostFormGroup.get('fileUpload')?.reset(); + } + + removeAttachment(fileName: string): void { + this.files = this.files.filter(x => x.name !== fileName); + } + + createPost(): void { + const postMessage = this.createPostFormGroup.get('newPostMessage')?.value; + this.dataArrived = false; + + this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe( + (result: object) => { + this.goToProfile(); + }, + (err: HttpErrorResponse) => { + this.dataArrived = true; + } + ); + } + + onScroll(event: any): void { + // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073 + if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) { + this.loadFeed(); + } + } +} diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.css b/src/app/components/kaleidoscope/kaleidoscope.component.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.css diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.html b/src/app/components/kaleidoscope/kaleidoscope.component.html new file mode 100644 index 0000000..7392a21 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.html @@ -0,0 +1,8 @@ +<div class="kaleidoscope"> + <ng-container *ngComponentOutlet="component"></ng-container> + <app-login *ngIf="logged!"></app-login> + <app-register *ngIf="!logged!"></app-register> + <!-- <app-feed></app-feed> --> + <script>var logged = false;</script> + <!-- to be fixed tomorow --> +</div>
\ No newline at end of file diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.ts b/src/app/components/kaleidoscope/kaleidoscope.component.ts new file mode 100644 index 0000000..1c5cda1 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { LoginComponent } from '../login/login.component'; + +@Component({ + selector: 'app-kaleidoscope', + templateUrl: './kaleidoscope.component.html', + styleUrls: ['./kaleidoscope.component.css'] +}) +export class KaleidoscopeComponent implements OnInit { + + public _component: Component; + + constructor(loginComponent: LoginComponent) { + this._component = loginComponent as Component; + } + + ngOnInit(): void { + + } + + assignComponent(component: Component) { + this._component = component; + } +} diff --git a/src/app/components/loading/loading.component.css b/src/app/components/loading/loading.component.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/app/components/loading/loading.component.css diff --git a/src/app/components/loading/loading.component.html b/src/app/components/loading/loading.component.html new file mode 100644 index 0000000..8440f4e --- /dev/null +++ b/src/app/components/loading/loading.component.html @@ -0,0 +1,3 @@ +<div id="content"> + Loading... +</div> diff --git a/src/app/components/loading/loading.component.ts b/src/app/components/loading/loading.component.ts new file mode 100644 index 0000000..e203484 --- /dev/null +++ b/src/app/components/loading/loading.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-loading', + templateUrl: './loading.component.html', + styleUrls: ['./loading.component.css'] +}) +export class LoadingComponent implements OnInit { + + constructor() + { } + + ngOnInit(): void { + } +} diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css new file mode 100644 index 0000000..766522e --- /dev/null +++ b/src/app/components/login/login.component.css @@ -0,0 +1,32 @@ +* { + transition: .2s; +} + +form { + width: 100%; +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +.input-selection:nth-of-type(1) { + margin-top: 1.2em; +} + +.submit-btn { + margin-bottom: .2em; +} + +.redirect-to-register { + color: var(--focus-color); + background-color: var(--bg-color); + border-color: var(--focus-color); +} + +.redirect-to-register:hover { + border-color: black !important; + color: black; +} diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html new file mode 100644 index 0000000..13f9bbf --- /dev/null +++ b/src/app/components/login/login.component.html @@ -0,0 +1,30 @@ +<div id="content"> + <div class="title">Login</div> + + <form [formGroup]="loginUserFormGroup" (ngSubmit)="onSubmit()"> + <hr> + + <div class="input-selection"> + <input type="text" placeholder="Username" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="loginUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" placeholder="Password" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="loginUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + </div> + </div> + + <hr> + <button class="submit-btn" type="submit">Submit</button> + <app-error-bar></app-error-bar> + </form> + <button class="submit-btn redirect-to-register" (click)="onRedirectRegister()">New to DevHive? Register instead</button> +</div> diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..c3fb79c --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { UserService } from 'src/app/services/user.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { TokenService } from 'src/app/services/token.service'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit { + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + private _title = 'Login'; + public loginUserFormGroup: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loginUserFormGroup = this._fb.group({ + username: new FormControl('', [ + Validators.required + ]), + password: new FormControl('', [ + Validators.required + ]) + }); + } + + onSubmit(): void { + this._errorBar.hideError(); + this._userService.loginUserRequest(this.loginUserFormGroup).subscribe( + (res: object) => { + this._tokenService.setUserTokenToSessionStorage(res); + this._router.navigate(['/']); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + + onRedirectRegister(): void { + this._router.navigate(['/register']); + } + + get username(): AbstractControl | null { + return this.loginUserFormGroup.get('username'); + } + + get password(): AbstractControl | null { + return this.loginUserFormGroup.get('password'); + } +} diff --git a/src/app/components/not-found/not-found.component.css b/src/app/components/not-found/not-found.component.css new file mode 100644 index 0000000..ef6231d --- /dev/null +++ b/src/app/components/not-found/not-found.component.css @@ -0,0 +1,23 @@ +#content { + font-size: 1.3em; +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +#back-btns { + width: 100%; + display: flex; +} + +#back-btns > * { + flex: 1; + margin-right: .2em; +} + +#back-btns > *:nth-last-child(1) { + margin-right: 0; +} diff --git a/src/app/components/not-found/not-found.component.html b/src/app/components/not-found/not-found.component.html new file mode 100644 index 0000000..8394810 --- /dev/null +++ b/src/app/components/not-found/not-found.component.html @@ -0,0 +1,10 @@ +<div id="content"> + <div class="title"> + Page not found! + </div> + <hr> + <div id="back-btns"> + <button class="submit-btn" type="submit" (click)="backToFeed()">Back to feed</button> + <button class="submit-btn" type="submit" (click)="backToLogin()">Back to login</button> + </div> +</div> diff --git a/src/app/components/not-found/not-found.component.ts b/src/app/components/not-found/not-found.component.ts new file mode 100644 index 0000000..b1f8cc1 --- /dev/null +++ b/src/app/components/not-found/not-found.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-not-found', + templateUrl: './not-found.component.html', + styleUrls: ['./not-found.component.css'] +}) +export class NotFoundComponent implements OnInit { + private _title = 'Not Found!'; + + constructor(private _titleService: Title, private _router: Router) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + backToLogin(): void { + this._router.navigate(['/login']); + } +} diff --git a/src/app/components/post-attachment/post-attachment.component.css b/src/app/components/post-attachment/post-attachment.component.css new file mode 100644 index 0000000..572cc99 --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.css @@ -0,0 +1,75 @@ +/* Attachment */ + +.attachment { + border: 2px solid black; + border-top: 0; + border-radius: 0 0 .6em .6em; + padding: .4em; + padding-top: 1em; + margin-top: calc(-0.8em - 2px); +} + +.clickable { + width: 100%; + height: 100%; + display: flex; +} + +.clickable:hover { + cursor: pointer; +} + +.attachment-name { + flex: 1; +} + +.attachment-type { + margin-left: .2em; +} + +/* Full attachment */ + +.show-full-attachment { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: #000000AF; + display: flex; + justify-content: center; + align-items: center; + z-index: 2; +} + +.show-full-attachment > * { + max-height: 100vh; + max-width: 100vw; +} + +.attachment-download { + max-width: 420px !important; + margin: 0 .4em; + text-decoration: none; + color: white; + border-color: white; + background-color: black; +} + +.attachment-download:hover { + background-color: white; + color: var(--focus-color); +} + +.close { + position: fixed; + top: .2em; + right: .2em; + font-size: 2em; + color: whitesmoke; +} + +.close:hover { + color: var(--failure); + cursor: pointer; +} diff --git a/src/app/components/post-attachment/post-attachment.component.html b/src/app/components/post-attachment/post-attachment.component.html new file mode 100644 index 0000000..4d381d1 --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.html @@ -0,0 +1,18 @@ +<div class="attachment"> + <div class="clickable" (click)="toggleShowFull()"> + <div class="attachment-name"> + {{ fileName }} + </div> + <div class="attachment-type"> + {{ fileType }} + </div> + </div> +</div> + +<div class="show-full-attachment" *ngIf="showFull" (click)="toggleShowFull()"> + <img class="attachment-img" *ngIf="isImage" src="{{paramURL}}"> + <a class="attachment-download submit-btn" *ngIf="!isImage" href="{{paramURL}}">Download attachment</a> + <div class="close"> + ☒ + </div> +</div> diff --git a/src/app/components/post-attachment/post-attachment.component.ts b/src/app/components/post-attachment/post-attachment.component.ts new file mode 100644 index 0000000..1d00def --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-post-attachment', + templateUrl: './post-attachment.component.html', + styleUrls: ['./post-attachment.component.css'] +}) +export class PostAttachmentComponent implements OnInit { + @Input() paramURL: string; + public isImage = false; + public showFull = false; + public fileName: string; + public fileType: string; + + constructor() + { } + + ngOnInit(): void { + this.isImage = this.paramURL.includes('image') && !this.paramURL.endsWith('pdf'); + this.fileType = this.isImage ? 'img' : 'raw'; + this.fileName = this.paramURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment'; + } + + toggleShowFull(): void { + this.showFull = !this.showFull; + } +} diff --git a/src/app/components/post-page/post-page.component.css b/src/app/components/post-page/post-page.component.css new file mode 100644 index 0000000..3eec851 --- /dev/null +++ b/src/app/components/post-page/post-page.component.css @@ -0,0 +1,62 @@ +#content { + justify-content: flex-start !important; +} + +#content > * { + width: 100%; +} + +.many-buttons { + width: 100%; + display: flex; +} + +.many-buttons > * { + flex: 1; + margin-right: .3em; +} + +.many-buttons > *:last-of-type { + margin-right: 0; +} + +#editPost { + display: flex; + position: relative; +} + +#new-message-input { + flex: 1; + box-sizing: border-box; +} + +#file-upload { + font-size: inherit; + color: transparent; + width: 1.99em; + height: 1.99em; + margin-left: .3em; +} + +#file-upload:hover { + cursor: pointer; +} + +#file-upload::-webkit-file-upload-button { + visibility: hidden; +} + +#attachment-img { + height: 1.99em; + width: 1.99em; + position: absolute; + right: 0; + pointer-events: none; +} + + +.submit-btn { + max-width: 98%; + margin: 0 auto; + margin-bottom: .5em; +} diff --git a/src/app/components/post-page/post-page.component.html b/src/app/components/post-page/post-page.component.html new file mode 100644 index 0000000..8665865 --- /dev/null +++ b/src/app/components/post-page/post-page.component.html @@ -0,0 +1,37 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <div class="many-buttons" *ngIf="loggedIn"> + <button class="submit-btn" type="submit" (click)="backToFeed()">ᐊ Back to feed</button> + <button class="submit-btn" type="submit" (click)="backToProfile()">ᐊ Back to profile</button> + </div> + <button class="submit-btn" type="submit" (click)="toLogin()" *ngIf="!loggedIn">Login</button> + <app-post [paramId]="postId.toString()"></app-post> + <div class="many-buttons" *ngIf="editable"> + <button class="submit-btn" (click)="editPost()">Edit post</button> + <button class="submit-btn delete-btn" (click)="deletePost()">Delete post</button> + </div> + <form id="editPost" [formGroup]="editPostFormGroup" *ngIf="editingPost" (ngSubmit)="editPost()"> + <input id="new-message-input" type="text" placeholder="New post message" class="input-field" formControlName="newPostMessage"> + <img id="attachment-img" src="assets/images/paper-clip.png"> + <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> + <input type="submit" style="display: none" /> + </form> + <div class="form-attachments" *ngIf="editingPost"> + <div *ngFor="let file of files" class="form-attachment"> + {{ file.name ? file.name : 'Attachment' }} + <div class="remove-form-attachment" (click)="removeAttachment(file.name)"> + ☒ + </div> + </div> + </div> + <form [formGroup]="addCommentFormGroup" (ngSubmit)="addComment()"> + <input type="text" placeholder="Add comment" class="input-field" formControlName="newComment"> + <input type="submit" style="display: none" /> + </form> + <div> + <div class="comment" *ngFor="let comm of post?.comments"> + <app-comment [paramId]="comm.id.toString()"></app-comment> + </div> + </div> +<div> diff --git a/src/app/components/post-page/post-page.component.ts b/src/app/components/post-page/post-page.component.ts new file mode 100644 index 0000000..413ff80 --- /dev/null +++ b/src/app/components/post-page/post-page.component.ts @@ -0,0 +1,162 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { PostService } from 'src/app/services/post.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Post } from 'src/models/post'; +import { CloudinaryService } from 'src/app/services/cloudinary.service'; + +@Component({ + selector: 'app-post-page', + templateUrl: './post-page.component.html', + styleUrls: ['./post-page.component.css'] +}) +export class PostPageComponent implements OnInit { + private _title = 'Post'; + public dataArrived = false; + public loggedIn = false; + public editable = false; + public editingPost = false; + public postId: Guid; + public post: Post; + public files: File[]; + public editPostFormGroup: FormGroup; + public addCommentFormGroup: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService, private _cloudinaryService: CloudinaryService){ + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.postId = Guid.parse(this._router.url.substring(6)); + this.files = []; + + // Gets the post and the logged in user and compares them, + // to determine if the current post is made by the user + this._postService.getPostRequest(this.postId).subscribe( + (result: object) => { + this.post = result as Post; + this.post.fileURLs = Object.values(result)[7]; + if (this.loggedIn) { + this.editable = this.post.creatorUsername === this._tokenService.getUsernameFromSessionStorageToken(); + } + if (this.post.fileURLs.length > 0) { + this.loadFiles(); + } + else { + this.dataArrived = true; + } + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this.editPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); + + this.addCommentFormGroup = this._fb.group({ + newComment: new FormControl('') + }); + } + + private loadFiles(): void { + for (const fileURL of this.post.fileURLs) { + this._cloudinaryService.getFileRequest(fileURL).subscribe( + (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.dataArrived = true; + } + } + ); + } + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + backToProfile(): void { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + + toLogin(): void { + this._router.navigate(['/login']); + } + + 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 { + if (this._tokenService.getTokenFromSessionStorage() === '') { + this.toLogin(); + return; + } + + if (this.editingPost) { + let newMessage = this.editPostFormGroup.get('newPostMessage')?.value; + if (newMessage === '') { + newMessage = this.post.message; + } + this._postService.putPostWithSessionStorageRequest(this.postId, newMessage, this.files).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + this.dataArrived = false; + } + this.editingPost = !this.editingPost; + } + + addComment(): void { + if (!this.loggedIn) { + this._router.navigate(['/login']); + return; + } + + const newComment = this.addCommentFormGroup.get('newComment')?.value; + if (newComment !== '' && newComment !== null) { + this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe( + (result: object) => { + this.editPostFormGroup.reset(); + this.reloadPage(); + } + ); + } + } + + deletePost(): void { + this._postService.deletePostWithSessionStorage(this.postId).subscribe( + (result: object) => { + 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]); + } +} diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css new file mode 100644 index 0000000..1b88c7d --- /dev/null +++ b/src/app/components/post/post.component.css @@ -0,0 +1,134 @@ +.post { + display: flex; + width: 98%; + + margin: .5em auto; + 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 { + width: 2.2em; + height: 2.2em; + margin-right: .2em; +} + +.author-info > .handle { + font-size: .9em; + color: gray; +} + +/* Content */ + +.content { + flex: 1; +} + +.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; +} + +.comment-count > img { + height: .8em; +} + +.message:hover, .timestamp:hover { + cursor: pointer; +} + +/* Rating */ + +/* Temporary, until ratings are implemented fully */ +.rating { + display: none !important; +} + +.rating { + display: flex; + flex-direction: column; + align-items: center; + min-height: 4.4em; + margin: auto -.1em auto 0; +} + +.score { + flex: 1; + display: flex; + align-items: center; +} + + +.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; +} + +.attachments:empty { + display: none; +} + +.attachments > * { + flex: 1; +} diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html new file mode 100644 index 0000000..bc0d84a --- /dev/null +++ b/src/app/components/post/post.component.html @@ -0,0 +1,49 @@ +<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 }} + </div> + <div class="handle"> + @{{ user.userName }} + </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"> + ᐃ + </button> + <div class="score"> + {{ votesNumber }} + </div> + <button class="vote"> + ᐁ + </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> diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts new file mode 100644 index 0000000..387f56f --- /dev/null +++ b/src/app/components/post/post.component.ts @@ -0,0 +1,57 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { PostService } from 'src/app/services/post.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { Post } from 'src/models/post'; + +@Component({ + selector: 'app-post', + templateUrl: './post.component.html', + styleUrls: ['./post.component.css'], +}) +export class PostComponent implements OnInit { + public loaded = false; + public user: User; + public post: Post; + public votesNumber: number; + public timeCreated: string; + @Input() paramId: string; + + constructor(private _postService: PostService, private _userService: UserService, private _router: Router) + { } + + ngOnInit(): void { + this.post = this._postService.getDefaultPost(); + this.user = this._userService.getDefaultUser(); + + this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe( + (result: object) => { + Object.assign(this.post, result); + this.post.fileURLs = Object.values(result)[7]; + this.votesNumber = 23; + + this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB'); + this.loadUser(); + } + ); + } + + private loadUser(): void { + this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe( + (result: object) => { + Object.assign(this.user, result); + this.loaded = true; + } + ); + } + + goToAuthorProfile(): void { + this._router.navigate(['/profile/' + this.user.userName]); + } + + goToPostPage(): void { + this._router.navigate(['/post/' + this.post.postId]); + } +} diff --git a/src/app/components/profile-settings/profile-settings.component.css b/src/app/components/profile-settings/profile-settings.component.css new file mode 100644 index 0000000..1c07d9f --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.css @@ -0,0 +1,124 @@ +* { + box-sizing: border-box; +} + +#content { + max-width: 22em; + justify-content: start; +} + +form { + width: 100%; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +/* Navigation bar (for loggedin user) */ + +#navigation { + display: flex; + width: 100%; +} + +#navigation > .submit-btn { + flex: 1; + margin-top: 0; + margin-left: .5em; + font-size: inherit; +} + +#navigation > .submit-btn:nth-of-type(1) { + margin-left: 0; +} + +/* Form */ + +#update-profile-picture { + display: flex; + align-items: center; + justify-content: center; + padding: 0 .5em; + flex-wrap: wrap; +} + +#profile-picture { + width: 5em; + height: 5em; +} + +#submit-file { + display: flex; + flex-direction: column; + align-items: center; + padding: 1em; +} + +#upload-file:hover { + cursor: inherit; +} + +#upload-file > input:hover { + cursor: pointer; +} + +#upload-file > input::-webkit-file-upload-button { + visibility: hidden; +} + +#update-user { + margin-top: 1.1em; +} + +.input-field { + border-color: var(--focus-color) !important; + caret-color: var(--focus-color); + border-width: 2px !important; + margin-top: -1px !important; +} + +.input-field-label { + font-size: .7em; + color: var(--focus-color); + transform: translate(0, -1.2em); +} + +#all-languages, #all-technologies { + display: flex; + flex-wrap: wrap; +} + +/* Buttons */ + +.edit-btn { + border-radius: 0 !important; + color: var(--focus-color); + background-color: white; + border-color: var(--focus-color); +} + +.edit-btn:hover { + color: white; + background-color: black; + border-color: black !important; +} + +.submit-btn { + margin-bottom: .5em; +} + +#update-profile-btn { + margin-top: 1em; +} + +#confirm-delete { + box-sizing: border-box; + width: 100%; + background-color: var(--failure); + color: white; + padding: .2em; + text-align: center; +} diff --git a/src/app/components/profile-settings/profile-settings.component.html b/src/app/components/profile-settings/profile-settings.component.html new file mode 100644 index 0000000..502697d --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.html @@ -0,0 +1,116 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="goToProfile()">ᐊ Back</button> + <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isAdminUser">Panel</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <hr> + <div class="scroll-standalone"> + <form id="update-profile-picture" [formGroup]="updateProfilePictureFormGroup" (ngSubmit)="updateProfilePicture()"> + <img id="profile-picture" class="round-image" [src]="user.profilePictureURL"> + <div id="submit-file"> + <div id="upload-file" class="submit-btn"> + <input type="file" accept="image/*" formControlName="fileUpload" (change)="onFileUpload($event)"> + </div> + <button class="submit-btn" type="submit">Update profile picture</button> + </div> + </form> + <hr> + <form id="update-user" [formGroup]="updateUserFormGroup" (ngSubmit)="onSubmit()"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="firstName" required> + <label class="input-field-label">First Name</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="lastName" required> + <label class="input-field-label">Last Name</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="email" required> + <label class="input-field-label">Email</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('email')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label> + </div> + </div> + <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button> + <div *ngIf="showLanguages"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="languageInput" required> + + <div class="input-errors"> + <label class="error">Type in your desired languages, separated by a space</label> + </div> + </div> + Available languages: + <div id="all-languages"> + <div class="user-language" *ngFor="let lang of availableLanguages"> + {{ lang.name }} + </div> + </div> + </div> + + <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button> + <div *ngIf="showTechnologies"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="technologyInput" required> + + <div class="input-errors"> + <label class="error">Type in your desired technologies, separated by a space</label> + </div> + </div> + Available technologies: + <div id="all-technologies"> + <div class="user-technology" *ngFor="let tech of availableTechnologies"> + {{ tech.name }} + </div> + </div> + </div> + + <button id="update-profile-btn" class="submit-btn" type="submit">Update profile</button> + <app-success-bar></app-success-bar> + <app-error-bar></app-error-bar> + </form> + <hr> + <div id="confirm-delete" *ngIf="deleteAccountConfirm"> + Are you sure you want to delete your account?<br>This is permanent! + </div> + <button class="submit-btn delete-btn" (click)="deleteAccount()">Delete account</button> + </div> +</div> diff --git a/src/app/components/profile-settings/profile-settings.component.ts b/src/app/components/profile-settings/profile-settings.component.ts new file mode 100644 index 0000000..a484665 --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.ts @@ -0,0 +1,307 @@ +import { Location } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { LanguageService } from 'src/app/services/language.service'; +import { UserService } from 'src/app/services/user.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { User } from 'src/models/identity/user'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { SuccessBarComponent } from '../success-bar/success-bar.component'; +import { Language } from 'src/models/language'; +import { Technology } from 'src/models/technology'; +import { TokenService } from 'src/app/services/token.service'; +import { Title } from '@angular/platform-browser'; +import { AppConstants } from 'src/app/app-constants.module'; + +@Component({ + selector: 'app-profile-settings', + templateUrl: './profile-settings.component.html', + styleUrls: ['./profile-settings.component.css'] +}) +export class ProfileSettingsComponent implements OnInit { + private _title = 'Profile Settings'; + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent; + private _urlUsername: string; + public isAdminUser = false; + public dataArrived = false; + public deleteAccountConfirm = false; + public showLanguages = false; + public showTechnologies = false; + public updateUserFormGroup: FormGroup; + public updateProfilePictureFormGroup: FormGroup; + public newProfilePicture: File; + public user: User; + public availableLanguages: Language[]; + public availableTechnologies: Technology[]; + + constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _fb: FormBuilder, private _location: Location) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this._urlUsername = this._router.url.substring(9); + this._urlUsername = this._urlUsername.substring(0, this._urlUsername.length - 9); + + this.user = this._userService.getDefaultUser(); + this.availableLanguages = []; + this.availableTechnologies = []; + this.newProfilePicture = new File([], ''); + + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe( + (res: object) => { + Object.assign(this.user, res); + this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME); + this.finishUserLoading(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableLanguages = result as Language[]; + } + ); + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableTechnologies = result as Technology[]; + } + ); + } + + private finishUserLoading(): void { + if (sessionStorage.getItem('UserCred')) { + const userFromToken: User = this._userService.getDefaultUser(); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (tokenRes: object) => { + Object.assign(userFromToken, tokenRes); + + if (userFromToken.userName === this._urlUsername) { + this.initForms(); + this.dataArrived = true; + } + else { + this.goToProfile(); + } + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + else { + this.goToProfile(); + } + } + + private initForms(): void { + this.updateUserFormGroup = this._fb.group({ + firstName: new FormControl(this.user.firstName, [ + Validators.required, + Validators.minLength(3) + ]), + lastName: new FormControl(this.user.lastName, [ + Validators.required, + Validators.minLength(3) + ]), + username: new FormControl(this.user.userName, [ + Validators.required, + Validators.minLength(3) + ]), + email: new FormControl(this.user.email, [ + Validators.required, + Validators.email, + ]), + password: new FormControl('', [ + Validators.required, + Validators.minLength(3), + Validators.pattern('.*[0-9].*') // Check if password contains atleast one number + ]), + + // For language we have two different controls, + // the first one is used for input, the other one for sending data + // because if we edit the control for input, + // we're also gonna change the input field in the HTML + languageInput: new FormControl(''), // The one for input + languages: new FormControl(''), // The one that is sent + + // For technologies it's the same as it is with languages + technologyInput: new FormControl(''), + technologies: new FormControl('') + }); + + this.getLanguagesForShowing().then(value => { + this.updateUserFormGroup.patchValue({ languageInput : value }); + }); + + this.getTechnologiesForShowing().then(value => { + this.updateUserFormGroup.patchValue({ technologyInput : value }); + }); + + this.updateProfilePictureFormGroup = this._fb.group({ + fileUpload: new FormControl('') + }); + + this.updateUserFormGroup.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + } + + private getLanguagesForShowing(): Promise<string> { + return new Promise(resolve => { + this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => { + this.user.languages = value; + resolve(value.map(x => x.name).join(' ')); + }); + }); + } + + private getTechnologiesForShowing(): Promise<string> { + return new Promise(resolve => { + this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => { + this.user.technologies = value; + resolve(value.map(x => x.name).join(' ')); + }); + }); + } + + onFileUpload(event: any): void { + this.newProfilePicture = event.target.files[0]; + } + + updateProfilePicture(): void { + if (this.newProfilePicture.size === 0) { + return; + } + + this._userService.putProfilePictureFromSessionStorageRequest(this.newProfilePicture).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + this.dataArrived = false; + } + + onSubmit(): void { + this._successBar.hideMsg(); + this._errorBar.hideError(); + + this.patchLanguagesControl(); + this.patchTechnologiesControl(); + + this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.user.roles, this.user.friends).subscribe( + (result: object) => { + this._successBar.showMsg('Profile updated successfully!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + + private patchLanguagesControl(): void { + // Get user input + const langControl = this.updateUserFormGroup.get('languageInput')?.value as string ?? ''; + + if (langControl === '') { + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + languages : [] + }); + } + else { + const names = langControl.split(' '); + + // Transfer user input to objects of type { "name": "value" } + const actualLanguages = []; + for (const lName of names) { + if (lName !== '') { + actualLanguages.push({ name : lName }); + } + } + + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + languages : actualLanguages + }); + } + } + + private patchTechnologiesControl(): void { + // Get user input + const techControl = this.updateUserFormGroup.get('technologyInput')?.value as string ?? ''; + + if (techControl === '') { + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + technologies : [] + }); + } + else { + const names = techControl.split(' '); + + // Transfer user input to objects of type { "name": "value" } + const actualTechnologies = []; + for (const tName of names) { + if (tName !== '') { + actualTechnologies.push({ name : tName }); + } + } + + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + technologies : actualTechnologies + }); + } + } + + goToProfile(): void { + this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]); + } + + navigateToAdminPanel(): void { + this._router.navigate(['/admin-panel']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this.goToProfile(); + } + + toggleLanguages(): void { + this.showLanguages = !this.showLanguages; + } + + toggleTechnologies(): void { + this.showTechnologies = !this.showTechnologies; + } + + deleteAccount(): void { + if (this.deleteAccountConfirm) { + this._userService.deleteUserFromSessionStorageRequest().subscribe( + (res: object) => { + this.logout(); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + this.dataArrived = false; + } + else { + this.deleteAccountConfirm = true; + } + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/profile/profile.component.css b/src/app/components/profile/profile.component.css new file mode 100644 index 0000000..ebcd406 --- /dev/null +++ b/src/app/components/profile/profile.component.css @@ -0,0 +1,105 @@ +* { + box-sizing: border-box; +} + +#content { + max-width: 22em; + justify-content: start; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +form { + width: 100%; +} + +/* Navigation bar (for loggedin user) */ + +#navigation { + display: flex; + width: 100%; +} + +.submit-btn { + flex: 1; + margin-top: 0; + margin-left: .5em; + font-size: inherit; +} + +#navigation > .submit-btn:first-child { + margin-left: 0; +} + +/* Top card */ + +#main-info { + display: flex; + width: 100%; + margin-bottom: .25em +} + +#main-info > img { + width: 5em; + height: 5em; +} + +#other-main-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +#other-main-info > * { + font-size: 1.4em; +} + +#other-main-info *:nth-child(1) { + margin-top: auto; +} + +#other-main-info *:nth-last-child(1) { + margin-bottom: auto; +} + +#add-friend, #loggedin-password { + flex: 0 !important; + margin-top: .4em; + max-width: 8em; + font-size: .6em !important; +} + +#loggedin-password { + max-width: 100%; +} + +/* Languages and technologies */ + +.secondary-info { + margin-top: .25em; + margin-bottom: .25em; + width: 100%; + display: flex; + align-items: center; + flex-wrap: wrap; +} + +/* Posts */ + +#no-posts { + width: 100%; + text-align: center; + color: gray; + margin-top: .2em; +} + +#posts { + width: 100%; + height: 100%; +} diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html new file mode 100644 index 0000000..0e5f633 --- /dev/null +++ b/src/app/components/profile/profile.component.html @@ -0,0 +1,60 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="goBack()">ᐊ Back</button> + <button class="submit-btn" (click)="navigateToSettings()" *ngIf="isTheLoggedInUser">Settings</button> + <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isTheLoggedInUser && isAdminUser">Panel</button> + <button class="submit-btn" (click)="logout()" *ngIf="isTheLoggedInUser">Logout</button> + </nav> + <hr> + <div class="scroll-standalone" (scroll)="onScroll($event)"> + <div id="main-info" class="rounded-border"> + <img class="round-image" [src]="user.profilePictureURL" alt=""/> + <div id="other-main-info"> + <div id="name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div id="username"> + @{{ user.userName }} + </div> + <form [formGroup]="updateFrienship" (ngSubmit)="modifyFriend()" *ngIf="!isTheLoggedInUser && isUserLoggedIn"> + <button id="add-friend" type="submit" class="submit-btn">{{ friendOfUser ? 'Unfriend' : 'Add friend' }}</button> + <br> + <input id="loggedin-password" type="password" formControlName="password" class="input-field" *ngIf="updatingFriendship" placeholder="Type in password to confirm"> + </form> + </div> + </div> + <div class="secondary-info rounded-border"> + Languages: + <div *ngFor="let lang of user.languages"> + <div class="user-language"> + {{ lang.name }} + </div> + </div> + <div *ngIf="user.languages.length === 0"> + None + </div> + </div> + <div class="secondary-info rounded-border"> + Technologies: + <div *ngFor="let tech of user.technologies"> + <div class="user-language"> + {{ tech.name }} + </div> + </div> + <div *ngIf="user.technologies.length === 0"> + None + </div> + </div> + <hr> + <div id="posts"> + <div id="no-posts" *ngIf="userPosts.length === 0"> + {{ user.firstName }} {{ user.lastName }} hasn't posted anything yet! + </div> + <div *ngFor="let userPost of userPosts"> + <app-post [paramId]="userPost.postId.toString()"></app-post> + </div> + </div> + </div> +</div> diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts new file mode 100644 index 0000000..bbf8585 --- /dev/null +++ b/src/app/components/profile/profile.component.ts @@ -0,0 +1,203 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { AppConstants } from 'src/app/app-constants.module'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Location } from '@angular/common'; +import { LanguageService } from 'src/app/services/language.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { Post } from 'src/models/post'; +import { FeedService } from 'src/app/services/feed.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Title } from '@angular/platform-browser'; +import { Friend } from 'src/models/identity/friend'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.css'] +}) +export class ProfileComponent implements OnInit { + private _title = 'Profile'; + private _urlUsername: string; + private _timeLoaded: string; + private _currentPage: number; + public isTheLoggedInUser = false; + public isUserLoggedIn = false; + public isAdminUser = false; + public dataArrived = false; + public friendOfUser = false; + public updatingFriendship = false; + public user: User; + public userPosts: Post[]; + public updateFrienship: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _feedService: FeedService, private _location: Location, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + private setDefaultUser(): void { + this.user = this._userService.getDefaultUser(); + } + + ngOnInit(): void { + this._urlUsername = this._router.url.substring(9); + + const now = new Date(); + now.setHours(now.getHours() + 2); // accounting for eastern europe timezone + this._timeLoaded = now.toISOString(); + this._currentPage = 1; + + this.user = this._userService.getDefaultUser(); + this.userPosts = []; + + this.updateFrienship = this._fb.group({ + password: new FormControl('') + }); + + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe( + (res: object) => { + Object.assign(this.user, res); + this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME); + this.loadLanguages(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + } + + private loadLanguages(): void { + if (this.user.languages.length > 0) { + // When user has languages, get their names and load technologies + this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => { + this.user.languages = value; + this.loadTechnologies(); + }); + } + else { + this.loadTechnologies(); + } + } + + private loadTechnologies(): void { + if (this.user.technologies.length > 0) { + // When user has technologies, get their names and then load posts + this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => { + this.user.technologies = value; + this.loadPosts(); + }); + } + else { + this.loadPosts(); + } + } + + private loadPosts(): void { + this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( + (result: object) => { + const resultArr: Post[] = Object.values(result)[0]; + this.userPosts.push(...resultArr); + this.finishUserLoading(); + }, + (err: HttpErrorResponse) => { + this._currentPage = -1; + this.finishUserLoading(); + } + ); + } + + private finishUserLoading(): void { + if (sessionStorage.getItem('UserCred')) { + this.isUserLoggedIn = true; + const userFromToken: User = this._userService.getDefaultUser(); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (tokenRes: object) => { + Object.assign(userFromToken, tokenRes); + + if (userFromToken.friends.map(x => x.userName).includes(this._urlUsername)) { + this.friendOfUser = true; + } + if (userFromToken.userName === this._urlUsername) { + this.isTheLoggedInUser = true; + } + this.dataArrived = true; + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + else { + this.dataArrived = true; + } + } + + goBack(): void { + this._router.navigate(['/']); + } + + navigateToAdminPanel(): void { + this._router.navigate(['/admin-panel']); + } + + navigateToSettings(): void { + this._router.navigate([this._router.url + '/settings']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + + // Reload the page + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } + + modifyFriend(): void { + if (this.updatingFriendship) { + this.dataArrived = false; + + this._userService.getUserFromSessionStorageRequest().subscribe( + (result: object) => { + const loggedInUser: User = result as User; + + if (this.friendOfUser) { + loggedInUser.friends = loggedInUser.friends.filter(x => x.userName !== this.user.userName); + } + else { + const newFriend = new Friend(); + newFriend.userName = this.user.userName; + loggedInUser.friends.push(newFriend); + } + + this._userService.putBareUserFromSessionStorageRequest(loggedInUser, this.updateFrienship.get('password')?.value).subscribe( + (resultUpdate: object) => { + this.reloadPage(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/']); + } + ); + } + ); + } + this.updatingFriendship = !this.updatingFriendship; + } + + onScroll(event: any): void { + // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073 + if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) { + this.loadPosts(); + } + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css new file mode 100644 index 0000000..93d8006 --- /dev/null +++ b/src/app/components/register/register.component.css @@ -0,0 +1,40 @@ +/* A lot of stuff are moved to the global styles! */ + +* { + transition: 0.2s; +} + +form { + width: 100%; +} + +@media screen and (max-height: 630px) { + #content { + height: fit-content !important; + } +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +.input-selection:nth-of-type(1) { + margin-top: 1.2em; +} + +.submit-btn { + margin-bottom: .2em; +} + +.redirect-to-login { + color: var(--focus-color); + background-color: var(--bg-color); + border-color: var(--focus-color); +} + +.redirect-to-login:hover { + border-color: black !important; + color: black; +} diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html new file mode 100644 index 0000000..4e67e0e --- /dev/null +++ b/src/app/components/register/register.component.html @@ -0,0 +1,65 @@ +<div id="content"> + <div class="title">Register</div> + + <form [formGroup]="registerUserFormGroup" (ngSubmit)="onSubmit()"> + <hr> + <!-- Value: {{ registerUserFormGroup.value | json }} + <hr> --> + + <div class="input-selection"> + <input type="text" placeholder="Goshko, is that u?" class="input-field" formControlName="firstName" required> + <label class="input-field-label">First Name</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="Trapov? Really??" class="input-field" formControlName="lastName" required> + <label class="input-field-label">Last Name</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="Think of something cool to flex on other kids" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="You expect an email joke? I have none, mail me one" class="input-field" formControlName="email" required> + <label class="input-field-label">Email</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('email')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" placeholder="Make sure it's long & strong (just like my d***)" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label> + </div> + </div> + + <hr> + <button class="submit-btn" type="submit">Submit</button> + <app-error-bar></app-error-bar> + </form> + <button class="submit-btn redirect-to-login" (click)="onRedirectLogin()">Already have an account? Login here</button> +</div> diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts new file mode 100644 index 0000000..36eaa55 --- /dev/null +++ b/src/app/components/register/register.component.ts @@ -0,0 +1,86 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { TokenService } from 'src/app/services/token.service'; +import { UserService } from 'src/app/services/user.service'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.css'] +}) +export class RegisterComponent implements OnInit { + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + private _title = 'Register'; + public registerUserFormGroup: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.registerUserFormGroup = this._fb.group({ + firstName: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + lastName: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + username: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + email: new FormControl('', [ + Validators.required, + Validators.email, + ]), + password: new FormControl('', [ + Validators.required, + Validators.minLength(3), + Validators.pattern('.*[0-9].*') // Check if password contains atleast one number + ]), + }); + + // this.registerUserFormGroup.valueChanges.subscribe(console.log); + } + + onSubmit(): void { + this._userService.registerUserRequest(this.registerUserFormGroup).subscribe( + res => { + this._tokenService.setUserTokenToSessionStorage(res); + this._router.navigate(['/']); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + onRedirectLogin(): void { + this._router.navigate(['/login']); + } + + get firstName(): AbstractControl | null { + return this.registerUserFormGroup.get('firstName'); + } + + get lastName(): AbstractControl | null { + return this.registerUserFormGroup.get('lastName'); + } + + get username(): AbstractControl | null { + return this.registerUserFormGroup.get('username'); + } + + get email(): AbstractControl | null { + return this.registerUserFormGroup.get('email'); + } + + get password(): AbstractControl | null { + return this.registerUserFormGroup.get('password'); + } +} diff --git a/src/app/components/success-bar/success-bar.component.css b/src/app/components/success-bar/success-bar.component.css new file mode 100644 index 0000000..bee634d --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.css @@ -0,0 +1,11 @@ +#success-bar { + width: 100%; + background-color: var(--success); + color: white; + padding: .2em; + text-align: center; +} + +#success-bar:empty { + display: none; +} diff --git a/src/app/components/success-bar/success-bar.component.html b/src/app/components/success-bar/success-bar.component.html new file mode 100644 index 0000000..026e955 --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.html @@ -0,0 +1 @@ +<div id="success-bar">{{successMsg}}</div> diff --git a/src/app/components/success-bar/success-bar.component.ts b/src/app/components/success-bar/success-bar.component.ts new file mode 100644 index 0000000..f7c7e54 --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-success-bar', + templateUrl: './success-bar.component.html', + styleUrls: ['./success-bar.component.css'] +}) +export class SuccessBarComponent implements OnInit { + public successMsg = ''; + + constructor() + { } + + ngOnInit(): void { + this.hideMsg(); + } + + showMsg(msg?: string | undefined): void { + if (msg === undefined) { + this.successMsg = 'Success!'; + } + else if (msg.trim() === '') { + this.successMsg = 'Success!'; + } + else { + this.successMsg = msg; + } + } + + hideMsg(): void { + this.successMsg = ''; + } +} |
