aboutsummaryrefslogtreecommitdiff
path: root/src/app/components
diff options
context:
space:
mode:
authortranstrike <transtrike@gmail.com>2021-02-12 19:04:53 +0200
committertranstrike <transtrike@gmail.com>2021-02-12 19:04:53 +0200
commitbcd88af53b1a920d728ec98b45daa9ac2e2c0917 (patch)
treefd27eef086822b0f02f74364cac0b940956d2458 /src/app/components
parent1d1f05e3f74d70a558b4847a9107afa7952131cf (diff)
downloadDevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.tar
DevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.tar.gz
DevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.zip
Moved from DevHive
Diffstat (limited to 'src/app/components')
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.css42
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.html85
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.ts334
-rw-r--r--src/app/components/comment-page/comment-page.component.css27
-rw-r--r--src/app/components/comment-page/comment-page.component.html14
-rw-r--r--src/app/components/comment-page/comment-page.component.ts91
-rw-r--r--src/app/components/comment/comment.component.css59
-rw-r--r--src/app/components/comment/comment.component.html23
-rw-r--r--src/app/components/comment/comment.component.ts54
-rw-r--r--src/app/components/error-bar/error-bar.component.css12
-rw-r--r--src/app/components/error-bar/error-bar.component.html1
-rw-r--r--src/app/components/error-bar/error-bar.component.ts34
-rw-r--r--src/app/components/feed/feed.component.css179
-rw-r--r--src/app/components/feed/feed.component.html52
-rw-r--r--src/app/components/feed/feed.component.ts122
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.css0
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.html8
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.ts24
-rw-r--r--src/app/components/loading/loading.component.css0
-rw-r--r--src/app/components/loading/loading.component.html3
-rw-r--r--src/app/components/loading/loading.component.ts15
-rw-r--r--src/app/components/login/login.component.css32
-rw-r--r--src/app/components/login/login.component.html30
-rw-r--r--src/app/components/login/login.component.ts59
-rw-r--r--src/app/components/not-found/not-found.component.css23
-rw-r--r--src/app/components/not-found/not-found.component.html10
-rw-r--r--src/app/components/not-found/not-found.component.ts27
-rw-r--r--src/app/components/post-attachment/post-attachment.component.css75
-rw-r--r--src/app/components/post-attachment/post-attachment.component.html18
-rw-r--r--src/app/components/post-attachment/post-attachment.component.ts27
-rw-r--r--src/app/components/post-page/post-page.component.css62
-rw-r--r--src/app/components/post-page/post-page.component.html37
-rw-r--r--src/app/components/post-page/post-page.component.ts162
-rw-r--r--src/app/components/post/post.component.css134
-rw-r--r--src/app/components/post/post.component.html49
-rw-r--r--src/app/components/post/post.component.ts57
-rw-r--r--src/app/components/profile-settings/profile-settings.component.css124
-rw-r--r--src/app/components/profile-settings/profile-settings.component.html116
-rw-r--r--src/app/components/profile-settings/profile-settings.component.ts307
-rw-r--r--src/app/components/profile/profile.component.css105
-rw-r--r--src/app/components/profile/profile.component.html60
-rw-r--r--src/app/components/profile/profile.component.ts203
-rw-r--r--src/app/components/register/register.component.css40
-rw-r--r--src/app/components/register/register.component.html65
-rw-r--r--src/app/components/register/register.component.ts86
-rw-r--r--src/app/components/success-bar/success-bar.component.css11
-rw-r--r--src/app/components/success-bar/success-bar.component.html1
-rw-r--r--src/app/components/success-bar/success-bar.component.ts33
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&#45;loading *ngIf="!dataArrived"></app&#45;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">
+ &nbsp;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">
+ &nbsp;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 = '';
+ }
+}