diff options
| author | Kamen Mladenov <kamen.d.mladenov@protonmail.com> | 2021-04-09 19:55:59 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-04-09 19:55:59 +0300 |
| commit | f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3 (patch) | |
| tree | 83b88a773bb7dc053bb3aced35bce302264ec925 /src/app | |
| parent | bcd88af53b1a920d728ec98b45daa9ac2e2c0917 (diff) | |
| parent | c13889759d70687de6833c505221c203f65fedb8 (diff) | |
| download | DevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.tar DevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.tar.gz DevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.zip | |
Second Stage: Complete
Diffstat (limited to 'src/app')
59 files changed, 1571 insertions, 1825 deletions
diff --git a/src/app/app-constants.module.ts b/src/app/app-constants.module.ts index d72af53..67091d9 100644 --- a/src/app/app-constants.module.ts +++ b/src/app/app-constants.module.ts @@ -2,6 +2,8 @@ export class AppConstants { public static BASE_API_URL = 'http://localhost:5000/api'; public static API_USER_URL = AppConstants.BASE_API_URL + '/User'; + public static API_PROFILE_PICTURE_URL = AppConstants.BASE_API_URL + '/ProfilePicture'; + public static API_FRIENDS_URL = AppConstants.BASE_API_URL + '/Friends'; public static API_USER_LOGIN_URL = AppConstants.API_USER_URL + '/login'; public static API_USER_REGISTER_URL = AppConstants.API_USER_URL + '/register'; @@ -9,11 +11,12 @@ export class AppConstants { public static API_TECHNOLOGY_URL = AppConstants.BASE_API_URL + '/Technology'; public static API_POST_URL = AppConstants.BASE_API_URL + '/Post'; + public static API_RATING_URL = AppConstants.BASE_API_URL + '/Rating'; public static API_FEED_URL = AppConstants.BASE_API_URL + '/Feed'; public static API_COMMENT_URL = AppConstants.BASE_API_URL + '/Comment'; public static PAGE_SIZE = 10; - public static FALLBACK_PROFILE_ICON = 'assets/images/feed/profile-pic.png'; + public static FALLBACK_PROFILE_ICON = 'assets/icons/tabler-icon-user.svg'; public static SESSION_TOKEN_KEY = 'UserCred'; public static ADMIN_ROLE_NAME = 'Admin'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 0d83079..b319abd 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -7,8 +7,8 @@ import { ProfileComponent } from './components/profile/profile.component'; import { ProfileSettingsComponent } from './components/profile-settings/profile-settings.component'; import { NotFoundComponent } from './components/not-found/not-found.component'; import { PostPageComponent } from './components/post-page/post-page.component'; -import {AdminPanelPageComponent} from './components/admin-panel-page/admin-panel-page.component'; -import {CommentPageComponent} from './components/comment-page/comment-page.component'; +import { AdminPanelPageComponent } from './components/admin-panel-page/admin-panel-page.component'; +import { CommentPageComponent } from './components/comment-page/comment-page.component'; const routes: Routes = [ { path: '', component: FeedComponent }, diff --git a/src/app/app-shell/app-shell.component.css b/src/app/app-shell/app-shell.component.css deleted file mode 100644 index e69de29..0000000 --- a/src/app/app-shell/app-shell.component.css +++ /dev/null diff --git a/src/app/app-shell/app-shell.component.html b/src/app/app-shell/app-shell.component.html deleted file mode 100644 index b5f713a..0000000 --- a/src/app/app-shell/app-shell.component.html +++ /dev/null @@ -1 +0,0 @@ -<p>app-shell works!</p> diff --git a/src/app/app-shell/app-shell.component.spec.ts b/src/app/app-shell/app-shell.component.spec.ts deleted file mode 100644 index 2c4fabc..0000000 --- a/src/app/app-shell/app-shell.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AppShellComponent } from './app-shell.component'; - -describe('AppShellComponent', () => { - let component: AppShellComponent; - let fixture: ComponentFixture<AppShellComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ AppShellComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(AppShellComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/app-shell/app-shell.component.ts b/src/app/app-shell/app-shell.component.ts deleted file mode 100644 index f1bca89..0000000 --- a/src/app/app-shell/app-shell.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'app-app-shell', - templateUrl: './app-shell.component.html', - styleUrls: ['./app-shell.component.css'] -}) -export class AppShellComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - -} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1bf44ad..39f8ab3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,10 +2,8 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; import { HttpClientModule } from '@angular/common/http'; +import { ClipboardModule } from 'ngx-clipboard'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -22,9 +20,10 @@ import { SuccessBarComponent } from './components/success-bar/success-bar.compon import { PostPageComponent } from './components/post-page/post-page.component'; import { AdminPanelPageComponent } from './components/admin-panel-page/admin-panel-page.component'; import { CommentComponent } from './components/comment/comment.component'; -import { CommentPageComponent } from './components/comment-page/comment-page.component'; import { PostAttachmentComponent } from './components/post-attachment/post-attachment.component'; import { RouterModule } from '@angular/router'; +import { NavbarComponent } from './components/navbar/navbar.component'; +import { CommentPageComponent } from './components/comment-page/comment-page.component'; @NgModule({ declarations: [ @@ -42,19 +41,18 @@ import { RouterModule } from '@angular/router'; PostPageComponent, AdminPanelPageComponent, CommentComponent, + PostAttachmentComponent, + NavbarComponent, CommentPageComponent, - PostAttachmentComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), AppRoutingModule, ReactiveFormsModule, BrowserAnimationsModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, HttpClientModule, - RouterModule + RouterModule, + ClipboardModule ], providers: [], bootstrap: [AppComponent] 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 index 1f98e20..ec7aa5d 100644 --- a/src/app/components/admin-panel-page/admin-panel-page.component.css +++ b/src/app/components/admin-panel-page/admin-panel-page.component.css @@ -1,42 +1,5 @@ -#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; +.sec-info { + width: fit-content; + background-color: #424242; + margin: 0 0.3em 0.3em 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 index 980f12c..1074e51 100644 --- a/src/app/components/admin-panel-page/admin-panel-page.component.html +++ b/src/app/components/admin-panel-page/admin-panel-page.component.html @@ -1,85 +1,118 @@ -<!-- <app-loading *ngIf="!dataArrived"></app-loading> --> +<app-navbar></app-navbar> -<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> +<app-loading *ngIf="!dataArrived"></app-loading> - <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 }} +<main class="scroll-standalone under-navbar centered-content flex-col" *ngIf="dataArrived"> + <app-success-bar></app-success-bar> + <app-error-bar></app-error-bar> + + <div class="card width-full"> + <section> + <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect" (click)="toggleLanguages()"> + ▼ Edit Languages ▼ + </button> + <form class="flex-col margin-top-bot-dot3" [formGroup]="languageForm" (ngSubmit)="submitLanguages()" *ngIf="showLanguages"> + <label class="fg-focus"> + Create language: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="languageCreate" placeholder="New language name"> + + <label class="fg-focus"> + Update language: + </label> + <div class="flex-row flexible-children"> + <input type="text" class="fancy-input border-faded-slim border-bottom-only margin-right-dot2" formControlName="updateLanguageOldName" placeholder="Old language name"> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="updateLanguageNewName" placeholder="New language 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 }} + <label class="fg-focus"> + Delete language: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteLanguageName" placeholder="Language name"> + + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit"> + Modify languages + </button> + <section class="border-faded-slim padding-dot4 margin-top-dot4"> + <div class="none-message" *ngIf="availableLanguages.length === 0"> + No languages in database! + </div> + <div *ngIf="availableLanguages.length > 0"> + Available languages: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let lang of availableLanguages"> + {{ lang.name }} + </div> + </div> + </section> + </form> + </section> + <section> + <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleTechnologies()"> + ▼ Edit Technologies ▼ + </button> + <form class="flex-col margin-top-bot-dot3" [formGroup]="technologyForm" (ngSubmit)="submitTechnologies()" *ngIf="showTechnologies"> + <label class="fg-focus"> + Create technology: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="technologyCreate" placeholder="New technology name"> + + <label class="fg-focus"> + Update technology: + </label> + <div class="flex-row flexible-children"> + <input type="text" class="fancy-input border-faded-slim border-bottom-only margin-right-dot2" formControlName="updateTechnologyOldName" placeholder="Old technology name"> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="updateTechnologyNewName" placeholder="New technology 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> + <label class="fg-focus"> + Delete technology: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteTechnologyName" placeholder="Technology name"> + + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit"> + Modify technology + </button> + <section class="border-faded-slim padding-dot4 margin-top-dot4"> + <div class="none-message" *ngIf="availableTechnologies.length === 0"> + No technologies in database! + </div> + <div *ngIf="availableTechnologies.length > 0"> + Available technologies: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let tech of availableTechnologies"> + {{ tech.name }} + </div> + </div> + </section> + </form> + </section> + <section> + <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleDeletions()"> + ▼ Deletions ▼ + </button> + <form class="flex-col margin-top-bot-dot3" [formGroup]="deleteForm" (ngSubmit)="submitDelete()" *ngIf="showDeletions"> + <label class="fg-focus"> + Delete user by Id: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteUser" placeholder="User Id"> + + <label class="fg-focus"> + Delete post by Id: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deletePost" placeholder="Post Id"> + + <label class="fg-focus"> + Delete comment by Id: + </label> + <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteComment" placeholder="Comment Id"> - <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> + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit"> + Delete + </button> + </form> + </section> </div> -</div> +</main> 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 index 99c0721..d1070b5 100644 --- a/src/app/components/admin-panel-page/admin-panel-page.component.ts +++ b/src/app/components/admin-panel-page/admin-panel-page.component.ts @@ -11,9 +11,9 @@ 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 { User } from 'src/models/identity/user.model'; +import { Language } from 'src/models/language.model'; +import { Technology } from 'src/models/technology.model'; import { ErrorBarComponent } from '../error-bar/error-bar.component'; import { SuccessBarComponent } from '../success-bar/success-bar.component'; @@ -46,17 +46,17 @@ export class AdminPanelPageComponent implements OnInit { return; } - this._userService.getUserFromSessionStorageRequest().subscribe( - (result: object) => { + this._userService.getUserFromSessionStorageRequest().subscribe({ + next: (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) => { + error: () => { this._router.navigate(['/login']); } - ); + }); this.languageForm = this._fb.group({ languageCreate: new FormControl(''), @@ -65,9 +65,11 @@ export class AdminPanelPageComponent implements OnInit { deleteLanguageName: new FormControl('') }); - this.languageForm.valueChanges.subscribe(() => { - this._successBar?.hideMsg(); - this._errorBar?.hideError(); + this.languageForm.valueChanges.subscribe({ + next: () => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + } }); this.technologyForm = this._fb.group({ @@ -77,9 +79,11 @@ export class AdminPanelPageComponent implements OnInit { deleteTechnologyName: new FormControl('') }); - this.technologyForm.valueChanges.subscribe(() => { - this._successBar?.hideMsg(); - this._errorBar?.hideError(); + this.technologyForm.valueChanges.subscribe({ + next: () => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + } }); this.deleteForm = this._fb.group({ @@ -88,15 +92,35 @@ export class AdminPanelPageComponent implements OnInit { deleteComment: new FormControl('') }); - this.deleteForm.valueChanges.subscribe(() => { - this._successBar?.hideMsg(); - this._errorBar?.hideError(); + this.deleteForm.valueChanges.subscribe({ + next: () => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + } }); this.loadAvailableLanguages(); this.loadAvailableTechnologies(); } + private loadAvailableLanguages(): void { + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe({ + next: (result: object) => { + this.availableLanguages = result as Language[]; + this.dataArrived = true; + } + }); + } + + private loadAvailableTechnologies(): void { + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe({ + next: (result: object) => { + this.availableTechnologies = result as Technology[]; + this.dataArrived = true; + } + }); + } + // Navigation backToProfile(): void { @@ -128,14 +152,14 @@ export class AdminPanelPageComponent implements OnInit { const languageCreate: string = this.languageForm.get('languageCreate')?.value; if (languageCreate !== '' && languageCreate !== null) { - this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe( - (result: object) => { + this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe({ + next: () => { this.languageModifiedSuccess('Successfully updated languages!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -146,14 +170,14 @@ export class AdminPanelPageComponent implements OnInit { 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._languageService.putLanguageWithSessionStorageRequest(langId, updateLanguageNewName.trim()).subscribe({ + next: () => { this.languageModifiedSuccess('Successfully updated languages!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -163,14 +187,14 @@ export class AdminPanelPageComponent implements OnInit { if (deleteLanguageName !== '' && deleteLanguageName !== null) { const langId = this.availableLanguages.filter(x => x.name === deleteLanguageName.trim())[0].id; - this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe( - (result: object) => { + this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe({ + next: () => { this.languageModifiedSuccess('Successfully deleted language!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -180,13 +204,6 @@ export class AdminPanelPageComponent implements OnInit { this.loadAvailableLanguages(); } - private loadAvailableLanguages(): void { - this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe( - (result: object) => { - this.availableLanguages = result as Language[]; - } - ); - } // Technology modifying @@ -204,14 +221,14 @@ export class AdminPanelPageComponent implements OnInit { const technologyCreate: string = this.technologyForm.get('technologyCreate')?.value; if (technologyCreate !== '' && technologyCreate !== null) { - this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe( - (result: object) => { + this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe({ + next: () => { this.technologyModifiedSuccess('Successfully updated technologies!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -222,14 +239,14 @@ export class AdminPanelPageComponent implements OnInit { 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._technologyService.putTechnologyWithSessionStorageRequest(techId, updateTechnologyNewName.trim()).subscribe({ + next: () => { this.technologyModifiedSuccess('Successfully updated technologies!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -239,14 +256,14 @@ export class AdminPanelPageComponent implements OnInit { if (deleteTechnologyName !== '' && deleteTechnologyName !== null) { const techId = this.availableTechnologies.filter(x => x.name === deleteTechnologyName.trim())[0].id; - this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe( - (result: object) => { + this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe({ + next: () => { this.technologyModifiedSuccess('Successfully deleted technology!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -256,14 +273,6 @@ export class AdminPanelPageComponent implements OnInit { this.loadAvailableTechnologies(); } - private loadAvailableTechnologies(): void { - this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe( - (result: object) => { - this.availableTechnologies = result as Technology[]; - } - ); - } - // Deletions toggleDeletions(): void { @@ -282,14 +291,14 @@ export class AdminPanelPageComponent implements OnInit { if (deleteUser !== '' && deleteUser !== null) { const userId: Guid = Guid.parse(deleteUser); - this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe( - (result: object) => { + this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe({ + next: () => { this.deletionSuccess('Successfully deleted user!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -299,14 +308,14 @@ export class AdminPanelPageComponent implements OnInit { if (deletePost !== '' && deletePost !== null) { const postId: Guid = Guid.parse(deletePost); - this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe( - (result: object) => { + this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe({ + next: () => { this.deletionSuccess('Successfully deleted user!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } @@ -316,14 +325,14 @@ export class AdminPanelPageComponent implements OnInit { if (deleteComment !== '' && deleteComment !== null) { const commentId: Guid = Guid.parse(deleteComment); - this._commentService.deleteCommentWithSessionStorage(commentId).subscribe( - (result: object) => { + this._commentService.deleteCommentWithSessionStorage(commentId).subscribe({ + next: () => { this.deletionSuccess('Successfully deleted comment!'); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } } diff --git a/src/app/components/comment-page/comment-page.component.css b/src/app/components/comment-page/comment-page.component.css index b886bc1..e69de29 100644 --- a/src/app/components/comment-page/comment-page.component.css +++ b/src/app/components/comment-page/comment-page.component.css @@ -1,27 +0,0 @@ -#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 index 2d110d6..8a2ffe1 100644 --- a/src/app/components/comment-page/comment-page.component.html +++ b/src/app/components/comment-page/comment-page.component.html @@ -1,14 +1,18 @@ -<app-loading *ngIf="!loaded"></app-loading> +<app-navbar></app-navbar> -<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> +<app-loading *ngIf="!dataArrived"></app-loading> + +<main class="centered-content scroll-standalone under-navbar flex-col" *ngIf="dataArrived"> + <app-post [paramId]="postId.toString()"></app-post> + <hr class="card-hr"> + <section class="card flex-col width-full margin-0-top"> + <button class="fg-focus border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="goToPostPage()"> + Show all comments + </button> + </section> + <div class="text-centered"> + ... </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> + + <app-comment [paramId]="commentId.toString()"></app-comment> +</main> diff --git a/src/app/components/comment-page/comment-page.component.ts b/src/app/components/comment-page/comment-page.component.ts index bd4cfe5..4281e1c 100644 --- a/src/app/components/comment-page/comment-page.component.ts +++ b/src/app/components/comment-page/comment-page.component.ts @@ -1,12 +1,8 @@ -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', @@ -15,77 +11,31 @@ import { Comment } from 'src/models/comment'; }) export class CommentPageComponent implements OnInit { private _title = 'Comment'; - public loaded = false; - public loggedIn = false; - public editable = false; - public editingComment = false; + public dataArrived = false; + public postId: Guid; 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){ + constructor(private _titleService: Title, private _router: Router, private _commentService: CommentService) { this._titleService.setTitle(this._title); } ngOnInit(): void { - this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; this.commentId = Guid.parse(this._router.url.substring(9)); + this.postId = Guid.createEmpty(); - // 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; + this._commentService.getCommentRequest(this.commentId).subscribe({ + next: (result: object) => { + this.postId = Object.values(result)[1]; + + this.dataArrived = true; }, - (err: HttpErrorResponse) => { + error: () => { 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]); + goToPostPage(): void { + this._router.navigate(['/post/' + this.postId]); } } diff --git a/src/app/components/comment/comment.component.css b/src/app/components/comment/comment.component.css index d82f10e..48d9174 100644 --- a/src/app/components/comment/comment.component.css +++ b/src/app/components/comment/comment.component.css @@ -1,59 +1,30 @@ -.comment { - display: flex; - width: 100%; - - margin: .5em auto; +.left-pane { 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; +.author-picture { + width: 2.2em; + height: 2.2em; } /* Content */ .content { - flex: 1; + padding: 0 var(--card-padding); } .message { - margin: .3em 0; - word-break: break-all; + word-break: break-word; } -.timestamp { - font-size: .5em; - color: gray; +.comment-details { + margin-bottom: calc(var(--card-padding) / 1.5); + padding: 0.2em 0; } -.message:hover, .timestamp:hover { - cursor: pointer; +.comment-details > * { + margin-left: 1.1em; } + diff --git a/src/app/components/comment/comment.component.html b/src/app/components/comment/comment.component.html index 718e25c..9403ea2 100644 --- a/src/app/components/comment/comment.component.html +++ b/src/app/components/comment/comment.component.html @@ -1,23 +1,46 @@ <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()"> +<section class="card flex-row" [hidden]="loaded" (mouseleave)="resetShareBtn()"> + <aside class="left-pane"> + <img class="author-picture round-image hover-half-opacity" [src]="user.profilePictureURL" (click)="goToAuthorProfile()"> + </aside> + <main class="content flexible"> + <summary class="font-size-dot8 text-vertical-middle hover-half-opacity" (click)="goToAuthorProfile()"> + <span> + {{ user.firstName }} {{ user.lastName }} + </span> + <span class="fg-faded"> + @{{ user.userName }} + </span> + </summary> + <form [formGroup]="editCommentFormGroup" *ngIf="editingComment" (ngSubmit)="editComment()"> + <textarea class="textarea-new-msg width-full border-faded-slim border-bottom-only padding-dot2 margin-bot-dot5" rows="1" formControlName="newCommentMessage" placeholder="What's on your mind?"></textarea> + <button type="submit" class="border-faded-slim width-full padding-dot2 lighter-hover click-effect border-radius-dot3 margin-bot-dot5"> + Update Comment + </button> + </form> + <article class="message margin-top-bot-dot2" *ngIf="!editingComment"> {{ comment.message }} - </div> - <div class="timestamp" (click)="goToCommentPage()"> - {{ timeCreated }} - </div> - </div> -</div> + </article> + <section class="comment-details flex-row flex-justify-end font-size-dot7 border-faded-slim border-bottom-only"> + <time class="flex-row flex-center-align-items"> + <img class="height-font" src="/assets/icons/tabler-icon-calendar-time.svg"> + <span> + {{ timeCreated }} + </span> + </time> + </section> + <section class="flex-row justify-children-center align-children-center"> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="toggleEditing()"> + <img src="/assets/icons/tabler-icon-edit.svg"> + </button> + <button #share ngxClipboard [cbContent]="getPostLink()" class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="showCopiedMessage()"> + <img src="/assets/icons/tabler-icon-link.svg"> + Share + </button> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="deleteComment()"> + <img src="/assets/icons/tabler-icon-trash.svg"> + </button> + </section> + </main> +</section> diff --git a/src/app/components/comment/comment.component.ts b/src/app/components/comment/comment.component.ts index 5076769..2a54f92 100644 --- a/src/app/components/comment/comment.component.ts +++ b/src/app/components/comment/comment.component.ts @@ -1,54 +1,126 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { Guid } from 'guid-typescript'; import { CommentService } from 'src/app/services/comment.service'; +import { TokenService } from 'src/app/services/token.service'; import { UserService } from 'src/app/services/user.service'; -import { Comment } from 'src/models/comment'; -import { User } from 'src/models/identity/user'; +import { Comment } from 'src/models/comment.model'; +import { User } from 'src/models/identity/user.model'; @Component({ selector: 'app-comment', templateUrl: './comment.component.html', styleUrls: ['./comment.component.css'] }) -export class CommentComponent implements OnInit { +export class CommentComponent implements OnInit, AfterViewInit { public loaded = false; + public loggedInAuthor = false; + public editingComment = false; public user: User; public comment: Comment; public timeCreated: string; @Input() paramId: string; + public editCommentFormGroup: FormGroup; + @ViewChild('share') shareBtn: ElementRef; + private _defaultShareBtnInnerHTML: string; + private _linkShared = false; - constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService) + constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService, private _tokenService: TokenService, private _fb: FormBuilder, private _elem: ElementRef, private _renderer: Renderer2) { } ngOnInit(): void { this.comment = this._commentService.getDefaultComment(); this.user = this._userService.getDefaultUser(); - this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe( - (result: object) => { + this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe({ + next: (result: object) => { Object.assign(this.comment, result); this.timeCreated = new Date(this.comment.timeCreated).toLocaleString('en-GB'); this.loadUser(); } - ); + }); + + this.editCommentFormGroup = this._fb.group({ + newCommentMessage: new FormControl('') + }); } private loadUser(): void { - this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe( - (result: object) => { + this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe({ + next: (result: object) => { Object.assign(this.user, result); + + if (this._tokenService.getTokenFromSessionStorage() !== '') { + this.loggedInAuthor = this._tokenService.getUsernameFromSessionStorageToken() === this.comment.issuerUsername; + this.editCommentFormGroup.get('newCommentMessage')?.setValue(this.comment.message); + } + this.loaded = true; } - ); + }); + } + + ngAfterViewInit(): void { + this._defaultShareBtnInnerHTML = this.shareBtn.nativeElement.innerHTML; } goToAuthorProfile(): void { this._router.navigate(['/profile/' + this.comment.issuerUsername]); } - goToCommentPage(): void { - this._router.navigate(['/comment/' + this.comment.commentId]); + toggleEditing(): void { + this.editingComment = !this.editingComment; + } + + editComment(): void { + if (this._tokenService.getTokenFromSessionStorage() === '') { + this._router.navigate(['/login']); + return; + } + + if (this.editingComment) { + const newMessage = this.editCommentFormGroup.get('newCommentMessage')?.value; + + if (newMessage !== '' && newMessage !== this.comment.message) { + this._commentService.putCommentWithSessionStorageRequest(Guid.parse(this.paramId), this.comment.postId, newMessage).subscribe({ + next: () => { + this.reloadPage(); + } + }); + } + } + this.editingComment = !this.editingComment; + } + + deleteComment(): void { + this._commentService.deleteCommentWithSessionStorage(Guid.parse(this.paramId)).subscribe({ + next: () => { + this.reloadPage(); + } + }); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } + + resetShareBtn(): void { + if (this._linkShared) { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', this._defaultShareBtnInnerHTML); + this._linkShared = false; + } + } + + showCopiedMessage(): void { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', 'Link copied to clipboard!'); + this._linkShared = true; + } + + getPostLink(): string { + return location.origin + '/comment/' + this.paramId; } } diff --git a/src/app/components/error-bar/error-bar.component.css b/src/app/components/error-bar/error-bar.component.css index 8f8edd9..880f205 100644 --- a/src/app/components/error-bar/error-bar.component.css +++ b/src/app/components/error-bar/error-bar.component.css @@ -5,6 +5,8 @@ color: white; padding: .2em; text-align: center; + word-wrap: break-word; + white-space: pre-wrap; } #error-bar:empty { diff --git a/src/app/components/error-bar/error-bar.component.ts b/src/app/components/error-bar/error-bar.component.ts index 111bac8..277552b 100644 --- a/src/app/components/error-bar/error-bar.component.ts +++ b/src/app/components/error-bar/error-bar.component.ts @@ -1,6 +1,5 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; -import { IApiError } from 'src/interfaces/api-error'; @Component({ selector: 'app-error-bar', @@ -18,14 +17,15 @@ export class ErrorBarComponent implements OnInit { } showError(error: HttpErrorResponse): void { - const test: IApiError = { - type: '', - title: 'Error!', - status: 0, - traceId: '' - }; - Object.assign(test, error.error); - this.errorMsg = test.title; + this.errorMsg = ''; + + const errors: string[][] = Object.values(Object.values(error.error)[0] as any); + + for (const errorArr of errors) { + for (const errorMsg of errorArr) { + this.errorMsg += errorMsg + '\n'; + } + } } hideError(): void { diff --git a/src/app/components/feed/feed.component.css b/src/app/components/feed/feed.component.css index cb155c6..295a379 100644 --- a/src/app/components/feed/feed.component.css +++ b/src/app/components/feed/feed.component.css @@ -1,179 +1,27 @@ -#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 */ +section:empty { 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; +#create-post { 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; + margin-top: 1em; } -#file-upload:hover { - cursor: pointer; +#attachments-btns img, .file-button > input { + height: 1.4em; + width: 1.4em; } -#file-upload::-webkit-file-upload-button { - visibility: hidden; +.file-button { + position: relative; } -#attachment-img { - height: 1.5em; - width: 1.5em; +.file-button > img { 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 */ +.file-button > input { + font-size: inherit; + color: transparent; } diff --git a/src/app/components/feed/feed.component.html b/src/app/components/feed/feed.component.html index 1a03dcc..4832456 100644 --- a/src/app/components/feed/feed.component.html +++ b/src/app/components/feed/feed.component.html @@ -1,52 +1,38 @@ +<app-navbar></app-navbar> + <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 }} +<main class="centered-content scroll-standalone under-navbar flex-col flex-justify-start" *ngIf="dataArrived"> + <form id="create-post" class="card flex-col" [formGroup]="createPostFormGroup" (ngSubmit)="createPost()"> + <textarea class="textarea-new-msg border-faded-slim border-bottom-only padding-dot2" rows="1" formControlName="newPostMessage" placeholder="What's on your mind?"></textarea> + <section class="flex-row flex-justify-start align-children-center padding-top-bot-dot5"> + <div class="file-button hover-half-opacity click-effect"> + <img src="/assets/icons/tabler-icon-paperclip.svg"> + <input type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> </div> - <div id="profile-bar-username"> - @{{ user.userName }} + </section> + <section class="flex-row bot-padding-dot6ger"> + <div *ngFor="let file of files" class="form-attachment border-faded-slim flexible flex-row flex-no-wrap flex-center-align-items padding-dot2 margin-top-bot-dot2"> + <div class="flexible"> + {{ file.name ? file.name : 'Attachment' }} + </div> + <div class="flex-col hover-half-opacity border-radius-dot2 click-effect" (click)="removeAttachment(file.name)"> + <img src="/assets/icons/tabler-icon-x.svg"> + </div> </div> + </section> + <button class="border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3"> + Post + </button> + </form> + <hr class="card-hr"> + <section id="posts" (scroll)="onScroll($event)"> + <div class="text-centered" *ngIf="posts.length === 0"> + None of your friends have posted anything yet!<br> + Try refreshing your page! </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 *ngFor="let friendPost of posts" class="post"> + <app-post [paramId]="friendPost.postId.toString()"></app-post> </div> - </div> -</div> + </section> +</main> diff --git a/src/app/components/feed/feed.component.ts b/src/app/components/feed/feed.component.ts index b412b3c..4100e41 100644 --- a/src/app/components/feed/feed.component.ts +++ b/src/app/components/feed/feed.component.ts @@ -1,12 +1,11 @@ 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 { User } from 'src/models/identity/user.model'; 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 { Post } from 'src/models/post.model'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { PostService } from 'src/app/services/post.service'; import { TokenService } from 'src/app/services/token.service'; @@ -42,7 +41,7 @@ export class FeedComponent implements OnInit { this.files = []; const now = new Date(); - now.setHours(now.getHours() + 2); // accounting for eastern european timezone + now.setHours(now.getHours() + 3); // accounting for eastern european timezone this._timeLoaded = now.toISOString(); this.createPostFormGroup = this._fb.group({ @@ -50,27 +49,27 @@ export class FeedComponent implements OnInit { fileUpload: new FormControl('') }); - this._userService.getUserFromSessionStorageRequest().subscribe( - (res: object) => { + this._userService.getUserFromSessionStorageRequest().subscribe({ + next: (res: object) => { Object.assign(this.user, res); this.loadFeed(); }, - (err: HttpErrorResponse) => { + error: () => { this.logout(); } - ); + }); } private loadFeed(): void { - this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( - (result: object) => { + this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe({ + next: (result: object) => { this.posts.push(...Object.values(result)[0]); this.finishUserLoading(); }, - (err) => { + error: () => { this.finishUserLoading(); } - ); + }); } private finishUserLoading(): void { @@ -103,14 +102,14 @@ export class FeedComponent implements OnInit { const postMessage = this.createPostFormGroup.get('newPostMessage')?.value; this.dataArrived = false; - this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe( - (result: object) => { + this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe({ + next: () => { this.goToProfile(); }, - (err: HttpErrorResponse) => { + error: () => { this.dataArrived = true; } - ); + }); } onScroll(event: any): void { diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.css b/src/app/components/kaleidoscope/kaleidoscope.component.css deleted file mode 100644 index e69de29..0000000 --- a/src/app/components/kaleidoscope/kaleidoscope.component.css +++ /dev/null diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.html b/src/app/components/kaleidoscope/kaleidoscope.component.html deleted file mode 100644 index 7392a21..0000000 --- a/src/app/components/kaleidoscope/kaleidoscope.component.html +++ /dev/null @@ -1,8 +0,0 @@ -<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 deleted file mode 100644 index 1c5cda1..0000000 --- a/src/app/components/kaleidoscope/kaleidoscope.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -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.html b/src/app/components/loading/loading.component.html index 8440f4e..bdc795b 100644 --- a/src/app/components/loading/loading.component.html +++ b/src/app/components/loading/loading.component.html @@ -1,3 +1,3 @@ -<div id="content"> +<main id="content"> Loading... -</div> +</main> diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css index 766522e..e69de29 100644 --- a/src/app/components/login/login.component.css +++ b/src/app/components/login/login.component.css @@ -1,32 +0,0 @@ -* { - 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 index 13f9bbf..95075c9 100644 --- a/src/app/components/login/login.component.html +++ b/src/app/components/login/login.component.html @@ -1,30 +1,28 @@ -<div id="content"> - <div class="title">Login</div> +<main class="centered-content scroll-standalone flex-col flex-center-align-items flex-justify-center height-full"> + <summary class="title card width-full margin-0 padding-dot2">Login</summary> - <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> + <app-error-bar class="width-full margin-top-dot4"></app-error-bar> + <form class="width-full card padding-dot6" [formGroup]="loginUserFormGroup" (ngSubmit)="onSubmit()"> + <section class="input-selection width-full"> + <input type="text" placeholder="Username" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="username" required> + <label class="fancy-input-label width-full">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> - + </section> + <section class="input-selection"> + <input [type]="showingPassword ? 'text' : 'password'" placeholder="Password" class="fancy-input width-full border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required> + <label class="fancy-input-label width-full">Password</label> + <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword()"> + <img [src]="showingPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'"> + </button> <div class="input-errors"> <label *ngIf="loginUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> </div> - </div> + </section> - <hr> - <button class="submit-btn" type="submit">Submit</button> - <app-error-bar></app-error-bar> + <button class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full" type="submit">Submit</button> </form> - <button class="submit-btn redirect-to-register" (click)="onRedirectRegister()">New to DevHive? Register instead</button> -</div> + <button class="fg-focus border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full margin-top-dot4" (click)="onRedirectRegister()">New to DevHive? Register instead</button> +</main> diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts index c3fb79c..a0ce730 100644 --- a/src/app/components/login/login.component.ts +++ b/src/app/components/login/login.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; import { Router } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { UserService } from 'src/app/services/user.service'; @@ -16,6 +16,7 @@ export class LoginComponent implements OnInit { @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; private _title = 'Login'; public loginUserFormGroup: FormGroup; + public showingPassword = false; constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { this._titleService.setTitle(this._title); @@ -32,28 +33,24 @@ export class LoginComponent implements OnInit { }); } + toggleShowPassword(): void { + this.showingPassword = !this.showingPassword; + } + onSubmit(): void { this._errorBar.hideError(); - this._userService.loginUserRequest(this.loginUserFormGroup).subscribe( - (res: object) => { + this._userService.loginUserRequest(this.loginUserFormGroup).subscribe({ + next: (res: object) => { this._tokenService.setUserTokenToSessionStorage(res); this._router.navigate(['/']); }, - (err: HttpErrorResponse) => { + error: (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/navbar/navbar.component.css b/src/app/components/navbar/navbar.component.css new file mode 100644 index 0000000..d02e928 --- /dev/null +++ b/src/app/components/navbar/navbar.component.css @@ -0,0 +1,38 @@ +#navbar { + height: var(--navbar-height); + width: 100%; + background-color: var(--card-bg); +} + +#nav-contents img { + height: 1.9em; + width: 1.9em; +} + +@media screen and (max-width: 30rem) { + #nav-username { + display: none; + } + + #navbar { + font-size: 0.8em; + } +} + +#nav-profile-picture { + padding: 0.1em; +} + +#nav-profile-picture img { + height: 1.8em; + width: 1.8em; +} + +.nav-item { + margin-left: 0.2em; + padding: 0 0.1em; +} + +.nav-item:first-child { + margin-left: 0; +} diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html new file mode 100644 index 0000000..d05f6e1 --- /dev/null +++ b/src/app/components/navbar/navbar.component.html @@ -0,0 +1,45 @@ +<nav id="navbar"> + <div id="nav-contents" class="centered-content flex-row padding-dot3 flex-center-align-items"> + <div class="nav-item border-radius-dot2 flex-row flex-center-align-items light-hover hover-half-opacity click-effect" (click)="goToProfile()" *ngIf="loggedIn"> + <div id="nav-profile-picture" class="flex-col"> + <img class="round-image" [src]="user.profilePictureURL"> + </div> + <div id="nav-username" class="font-size-dot9 flex-col"> + <div class="padding-dot2"> + @{{ user.userName }} + </div> + </div> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="goToFeed()" *ngIf="loggedIn"> + <img src="/assets/icons/tabler-icon-home.svg"> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none"> + <!-- Trending functionality isn't implemented yet! --> + <img src="/assets/icons/tabler-icon-trending-up.svg"> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none"> + <!-- Chat functionality isn't implemented yet! --> + <img src="/assets/icons/tabler-icon-message-circle.svg"> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none"> + <!-- Search functionality isn't implemented yet! --> + <img src="/assets/icons/tabler-icon-search.svg"> + </div> + <div class="flexible"> + <!-- This element serves as a spacer --> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="goToSettings()" *ngIf="loggedIn"> + <img src="/assets/icons/tabler-icon-settings.svg"> + </div> + <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="logout()" *ngIf="loggedIn"> + <img src="/assets/icons/tabler-icon-logout.svg"> + </div> + <div class="nav-item flex-row flex-center-align-items border-radius-dot2 light-hover hover-half-opacity click-effect side-padding-dot3" (click)="goToLogin()" *ngIf="!loggedIn"> + Login + <img src="/assets/icons/tabler-icon-login.svg"> + </div> + <div class="flexible" *ngIf="!loggedIn"> + <!-- This element serves as a spacer --> + </div> + </div> +</nav> diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts new file mode 100644 index 0000000..14b8f52 --- /dev/null +++ b/src/app/components/navbar/navbar.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { TokenService } from 'src/app/services/token.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user.model'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.css'] +}) +export class NavbarComponent implements OnInit { + public user: User; + public loggedIn: Boolean; + + constructor(private _router: Router, private _userService: UserService, private _tokenService: TokenService) + { } + + ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + + this.user = this._userService.getDefaultUser(); + + if (this.loggedIn) { + this._userService.getUserFromSessionStorageRequest().subscribe({ + next: (res: object) => { + Object.assign(this.user, res); + }, + }); + } + } + + goToProfile(): void { + // Properly reload the page + // Needed because if you're on someone's profile and go to yours, angular won't refresh the page (with your info) + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + + this._router.navigate(['/profile/' + this.user.userName]); + } + + goToFeed(): void { + if (this.loggedIn) { + this._router.navigate(['/']); + } + else { + this.goToLogin(); + } + } + + goToSettings(): void { + this._router.navigate(['/profile/' + this.user.userName + '/settings']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this.goToLogin(); + } + + goToLogin(): void { + this._router.navigate(['/login']); + } +} diff --git a/src/app/components/not-found/not-found.component.html b/src/app/components/not-found/not-found.component.html index 8394810..4d1165d 100644 --- a/src/app/components/not-found/not-found.component.html +++ b/src/app/components/not-found/not-found.component.html @@ -1,10 +1,18 @@ -<div id="content"> - <div class="title"> - Page not found! +<main class="centered-content flex-col flex-center-align-items flex-justify-center height-full"> + <div class="card width-full padding-dot6"> + <summary class="title"> + Page not found! + </summary> + <hr class="card-hr"> + <nav class="flex-row flexible-children padding-dot2"> + <button class="flex-row flex-justify-center flex-center-align-items border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 margin-right-dot2" (click)="backToFeed()"> + <img src="/assets/icons/tabler-icon-home.svg"> + Back to feed + </button> + <button class="flex-row flex-justify-center flex-center-align-items border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" (click)="backToLogin()"> + <img src="/assets/icons/tabler-icon-login.svg"> + Back to login + </button> + </nav> </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> +</main> diff --git a/src/app/components/post-attachment/post-attachment.component.css b/src/app/components/post-attachment/post-attachment.component.css index 572cc99..6cdab0b 100644 --- a/src/app/components/post-attachment/post-attachment.component.css +++ b/src/app/components/post-attachment/post-attachment.component.css @@ -1,32 +1,3 @@ -/* 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 { @@ -36,9 +7,6 @@ width: 100vw; height: 100vh; background-color: #000000AF; - display: flex; - justify-content: center; - align-items: center; z-index: 2; } @@ -49,27 +17,7 @@ .attachment-download { max-width: 420px !important; - margin: 0 .4em; + background-color: var(--card-bg); 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; + color: inherit; } diff --git a/src/app/components/post-attachment/post-attachment.component.html b/src/app/components/post-attachment/post-attachment.component.html index 4d381d1..e5e4b5a 100644 --- a/src/app/components/post-attachment/post-attachment.component.html +++ b/src/app/components/post-attachment/post-attachment.component.html @@ -1,18 +1,16 @@ -<div class="attachment"> - <div class="clickable" (click)="toggleShowFull()"> - <div class="attachment-name"> - {{ fileName }} - </div> - <div class="attachment-type"> - {{ fileType }} - </div> +<figure class="form-attachment border-faded-slim flexible flex-row flex-no-wrap padding-dot2 hover-half-opacity margin-top-bot-dot2" (click)="toggleShowFull()"> + <div class="flex-col flex-justify-center margin-right-dot2" [ngSwitch]="fileType"> + <img *ngSwitchCase="'img'" src="/assets/icons/tabler-icon-photo.svg"> + <img *ngSwitchDefault src="/assets/icons/tabler-icon-file.svg"> </div> -</div> + <summary class="flex-col flex-justify-center"> + {{ fileName }} + </summary> +</figure> -<div class="show-full-attachment" *ngIf="showFull" (click)="toggleShowFull()"> +<div class="show-full-attachment flex-row flex-justify-center flex-center-align-items" *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> + <a class="attachment-download border-faded-slim padding-dot4 hover-half-opacity click-effect border-radius-dot3" *ngIf="!isImage" href="{{paramURL}}"> + Download attachment + </a> </div> diff --git a/src/app/components/post-attachment/post-attachment.component.ts b/src/app/components/post-attachment/post-attachment.component.ts index 1d00def..1aeca37 100644 --- a/src/app/components/post-attachment/post-attachment.component.ts +++ b/src/app/components/post-attachment/post-attachment.component.ts @@ -18,7 +18,12 @@ export class PostAttachmentComponent implements OnInit { 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'; + if (this.fileType === 'img') { + this.fileName = this.paramURL.match(/(?!\/)+?[^\/]+?(?=\.)/g)?.pop() ?? 'Attachment'; + } + else { + this.fileName = this.paramURL.match(/[^\/]+?$/g)?.pop() ?? 'Attachment'; + } } toggleShowFull(): void { diff --git a/src/app/components/post-page/post-page.component.css b/src/app/components/post-page/post-page.component.css index 3eec851..7405240 100644 --- a/src/app/components/post-page/post-page.component.css +++ b/src/app/components/post-page/post-page.component.css @@ -1,62 +1,3 @@ -#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; +main { + padding-top: 0.5em; } diff --git a/src/app/components/post-page/post-page.component.html b/src/app/components/post-page/post-page.component.html index 8665865..cfca6c0 100644 --- a/src/app/components/post-page/post-page.component.html +++ b/src/app/components/post-page/post-page.component.html @@ -1,37 +1,23 @@ +<app-navbar></app-navbar> + <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> +<main class="centered-content scroll-standalone under-navbar flex-col" *ngIf="dataArrived"> <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 class="card flex-col width-full margin-0-top" [formGroup]="addCommentFormGroup" (ngSubmit)="addComment()"> + <textarea rows="1" placeholder="Add a new comment" class="textarea-new-msg border-faded-slim border-bottom-only padding-dot2 margin-bot-dot5" formControlName="newComment"></textarea> + <button class="border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3" type="submit"> + Add Comment + </button> </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> + <hr class="card-hr"> + <section> + <div class="text-centered" *ngIf="post?.comments?.length === 0"> + Nobody has comented on this post yet!<br> + Try refreshing the page! </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> + </section> +</main> diff --git a/src/app/components/post-page/post-page.component.ts b/src/app/components/post-page/post-page.component.ts index 413ff80..0babfdf 100644 --- a/src/app/components/post-page/post-page.component.ts +++ b/src/app/components/post-page/post-page.component.ts @@ -1,4 +1,3 @@ -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'; @@ -7,8 +6,7 @@ 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'; +import { Post } from 'src/models/post.model'; @Component({ selector: 'app-post-page', @@ -19,47 +17,30 @@ 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){ + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService) { 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._postService.getPostRequest(this.postId).subscribe({ + next: (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; - } + + this.dataArrived = true; }, - (err: HttpErrorResponse) => { + error: () => { this._router.navigate(['/not-found']); } - ); - - this.editPostFormGroup = this._fb.group({ - newPostMessage: new FormControl(''), - fileUpload: new FormControl('') }); this.addCommentFormGroup = this._fb.group({ @@ -67,68 +48,6 @@ export class PostPageComponent implements OnInit { }); } - 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']); @@ -137,23 +56,14 @@ export class PostPageComponent implements OnInit { const newComment = this.addCommentFormGroup.get('newComment')?.value; if (newComment !== '' && newComment !== null) { - this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe( - (result: object) => { - this.editPostFormGroup.reset(); + this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe({ + next: () => { 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'; diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css index 1b88c7d..1015564 100644 --- a/src/app/components/post/post.component.css +++ b/src/app/components/post/post.component.css @@ -1,134 +1,55 @@ -.post { - display: flex; - width: 98%; - - margin: .5em auto; +.left-pane { box-sizing: border-box; - padding: .5em; - background-color: var(--card-bg); - position: relative; -} - -.post:first-child { - margin-top: 0; -} - -hr { - border: 1px solid black; - width: 90%; } /* Author */ -.author { - display: flex; - margin-bottom: .2em; -} - -.author:hover { - cursor: pointer; -} - -.author > img { +.author-picture { width: 2.2em; height: 2.2em; - margin-right: .2em; -} - -.author-info > .handle { - font-size: .9em; - color: gray; } /* Content */ .content { - flex: 1; + padding: 0 var(--card-padding); } .message { - margin: .3em 0; - word-break: break-all; -} - -.bottom-post { - font-size: .5em; - color: gray; - display: flex; - align-items: center; -} - -.separator { - margin: 0 .5em; -} - -.comment-count { - font-size: 1em; + word-break: break-word; } -.comment-count > img { - height: .8em; +.post-details { + margin-bottom: calc(var(--card-padding) / 1.5); + padding: 0.2em 0; } -.message:hover, .timestamp:hover { - cursor: pointer; +.post-details > * { + margin-left: 1.1em; } -/* Rating */ - -/* Temporary, until ratings are implemented fully */ -.rating { - display: none !important; +.rating img { + height: 1.2em; + width: 1.2em; } -.rating { - display: flex; - flex-direction: column; - align-items: center; - min-height: 4.4em; - margin: auto -.1em auto 0; -} +/* Edit */ -.score { - flex: 1; - display: flex; - align-items: center; +#attachments-btns img, .file-button > input { + height: 1.4em; + width: 1.4em; } - -.vote { - display: flex; - align-items: center; - flex: 1; - - background: var(--card-bg); - font-size: 1em; - - border: 1px solid var(--card-bg); - box-sizing: border-box; - border-radius: .2em; - - } - -.vote:hover { - border: 1px solid var(--focus-color); - color: var(--focus-color); - cursor: pointer; -} - -/* Attachments */ - -.attachments { - display: flex; - width: 98%; - margin: -.3em auto .5em auto; - flex-wrap: wrap; +.file-button { + position: relative; } -.attachments:empty { - display: none; +.file-button > img { + position: absolute; + pointer-events: none; } -.attachments > * { - flex: 1; +.file-button > input { + font-size: inherit; + color: transparent; } diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html index bc0d84a..0c1cce2 100644 --- a/src/app/components/post/post.component.html +++ b/src/app/components/post/post.component.html @@ -1,49 +1,88 @@ <app-loading *ngIf="!loaded"></app-loading> -<div class="post rounded-border" *ngIf="loaded"> - <div class="content"> - <div class="author" (click)="goToAuthorProfile()"> - <img class="round-image" [src]="user.profilePictureURL"> - <div class="author-info"> - <div class="name"> - {{ user.firstName }} {{ user.lastName }} +<section class="card flex-row" [hidden]="loaded" (mouseleave)="resetShareBtn()"> + <aside class="left-pane"> + <img class="author-picture round-image hover-half-opacity" [src]="user.profilePictureURL" (click)="goToAuthorProfile()"> + </aside> + <main class="content flexible"> + <summary class="font-size-dot8 text-vertical-middle hover-half-opacity" (click)="goToAuthorProfile()"> + <span> + {{ user.firstName }} {{ user.lastName }} + </span> + <span class="fg-faded"> + @{{ user.userName }} + </span> + </summary> + <article class="message margin-top-bot-dot2" *ngIf="!editingPost"> + {{ post.message }} + </article> + <section class="flex-row flexible-children" *ngIf="!editingPost"> + <figure *ngFor="let fileURL of post.fileURLs"> + <app-post-attachment [paramURL]="fileURL"></app-post-attachment> + </figure> + </section> + <form [formGroup]="editPostFormGroup" *ngIf="editingPost"> + <textarea class="textarea-new-msg width-full border-faded-slim border-bottom-only padding-dot2" rows="1" formControlName="newPostMessage" placeholder="What's on your mind?"></textarea> + <section class="flex-row flex-justify-start align-children-center top-bot-padding-dot6ger"> + <div class="file-button hover-half-opacity click-effect"> + <img src="/assets/icons/tabler-icon-paperclip.svg"> + <input type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> </div> - <div class="handle"> - @{{ user.userName }} + </section> + </form> + <section class="flex-row bot-padding-dot6ger" *ngIf="editingPost"> + <div *ngFor="let file of files" class="form-attachment border-faded-slim flexible flex-row flex-no-wrap flex-center-align-items padding-dot2 margin-top-bot-dot2"> + <div class="flexible"> + {{ file.name ? file.name : 'Attachment' }} + </div> + <div class="flex-col hover-half-opacity border-radius-dot2 click-effect" (click)="removeAttachment(file.name)"> + <img src="/assets/icons/tabler-icon-x.svg"> </div> </div> - </div> - <div class="message" (click)="goToPostPage()"> - {{ post.message }} - </div> - <div class="bottom-post" (click)="goToPostPage()"> - <div class="timestamp"> - {{ timeCreated }} - </div> - <div class="separator"> - ║ - </div> - <div class="comment-count"> - {{ post.comments.length }} - <img src="assets/images/comment.png"> - </div> - </div> - - </div> - <div class="rating"> - <button class="vote"> - ᐃ + </section> + <button class="border-faded-slim width-full padding-dot2 lighter-hover click-effect border-radius-dot3 margin-bot-dot5" *ngIf="editingPost" (click)="editPost()"> + Update Post + </button> + <section class="post-details flex-row flex-justify-end font-size-dot7 border-faded-slim border-bottom-only"> + <time class="flex-row flex-center-align-items"> + <img class="height-font" src="/assets/icons/tabler-icon-calendar-time.svg"> + <span> + {{ timeCreated }} + </span> + </time> + <summary class="flex-row flex-center-align-items"> + <img class="height-font" src="/assets/icons/tabler-icon-message-2.svg"> + <span> + {{ post.comments.length }} + </span> + </summary> + </section> + <section class="flex-row justify-children-center align-children-center"> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="toggleEditing()"> + <img src="/assets/icons/tabler-icon-edit.svg"> + </button> + <button class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="goToPostPage()"> + <img src="/assets/icons/tabler-icon-message-2.svg"> + Comment + </button> + <button #share ngxClipboard [cbContent]="getPostLink()" class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="showCopiedMessage()"> + <img src="/assets/icons/tabler-icon-link.svg"> + Share + </button> + <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="deletePost()"> + <img src="/assets/icons/tabler-icon-trash.svg"> + </button> + </section> + </main> + <aside class="rating flex-col flex-center-align-items"> + <button #upvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(true)"> + <img src="/assets/icons/tabler-icon-chevron-up.svg"> </button> - <div class="score"> + <summary class="top-bot-padding-dot2"> {{ votesNumber }} - </div> - <button class="vote"> - ᐁ + </summary> + <button #downvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(false)"> + <img src="/assets/icons/tabler-icon-chevron-down.svg"> </button> - </div> -</div> -<div class="attachments"> - <div *ngFor="let fileURL of post.fileURLs"> - <app-post-attachment class="no-events" [paramURL]="fileURL"></app-post-attachment> - </div> -</div> + </aside> +</section> diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts index 387f56f..e8ba430 100644 --- a/src/app/components/post/post.component.ts +++ b/src/app/components/post/post.component.ts @@ -1,50 +1,123 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { Guid } from 'guid-typescript'; +import { CloudinaryService } from 'src/app/services/cloudinary.service'; import { PostService } from 'src/app/services/post.service'; +import { RatingService } from 'src/app/services/rating.service'; import { UserService } from 'src/app/services/user.service'; -import { User } from 'src/models/identity/user'; -import { Post } from 'src/models/post'; +import { User } from 'src/models/identity/user.model'; +import { Post } from 'src/models/post.model'; +import { TokenService } from '../../services/token.service'; @Component({ selector: 'app-post', templateUrl: './post.component.html', styleUrls: ['./post.component.css'], }) -export class PostComponent implements OnInit { +export class PostComponent implements OnInit, AfterViewInit { public loaded = false; public user: User; public post: Post; public votesNumber: number; public timeCreated: string; @Input() paramId: string; + @ViewChild('upvote') upvoteBtn: ElementRef; + @ViewChild('downvote') downvoteBtn: ElementRef; + @ViewChild('share') shareBtn: ElementRef; + private _defaultShareBtnInnerHTML: string; + private _linkShared = false; // For optimisation purposes + public loggedIn = false; + public loggedInAuthor = false; + public editingPost = false; + public files: File[]; + public editPostFormGroup: FormGroup; - constructor(private _postService: PostService, private _userService: UserService, private _router: Router) + constructor(private _postService: PostService, private _ratingServe: RatingService, private _userService: UserService, private _router: Router, private _tokenService: TokenService, private _cloudinaryService: CloudinaryService, private _fb: FormBuilder, private _elem: ElementRef, private _renderer: Renderer2) { } ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.post = this._postService.getDefaultPost(); this.user = this._userService.getDefaultUser(); + this.files = []; - this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe( - (result: object) => { + this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe({ + next: (result: object) => { Object.assign(this.post, result); + this.post.fileURLs = Object.values(result)[7]; - this.votesNumber = 23; + this.votesNumber = this.post.currentRating; this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB'); + this.loadUser(); } - ); + }); + + this.editPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); } private loadUser(): void { - this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe( - (result: object) => { + this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe({ + next: (result: object) => { Object.assign(this.user, result); + + if (this.loggedIn) { + this.loggedInAuthor = this._tokenService.getUsernameFromSessionStorageToken() === this.post.creatorUsername; + this.editPostFormGroup.get('newPostMessage')?.setValue(this.post.message); + + if (this.post.fileURLs.length > 0) { + this.loadFiles(); + return; + } + } + this.loaded = true; } - ); + }); + } + + private loadFiles(): void { + for (const fileURL of this.post.fileURLs) { + this._cloudinaryService.getFileRequest(fileURL).subscribe({ + next: (result: object) => { + const file = result as File; + const tmp = { + name: fileURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment' + }; + + Object.assign(file, tmp); + this.files.push(file); + + if (this.files.length === this.post.fileURLs.length) { + this.loaded = true; + } + } + }); + } + } + + ngAfterViewInit(): void { + if (this.loggedIn) { + this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({ + next: (x: object) => { + if (!x) { + return; + } + + const isLike: boolean = Object.values(x)[3]; + + this.changeColorOfVoteButton(isLike, !isLike); + } + }); + } + + this._defaultShareBtnInnerHTML = this.shareBtn.nativeElement.innerHTML; } goToAuthorProfile(): void { @@ -54,4 +127,117 @@ export class PostComponent implements OnInit { goToPostPage(): void { this._router.navigate(['/post/' + this.post.postId]); } + + toggleEditing(): void { + this.editingPost = !this.editingPost; + } + + onFileUpload(event: any): void { + this.files.push(...event.target.files); + this.editPostFormGroup.get('fileUpload')?.reset(); + } + + removeAttachment(fileName: string): void { + this.files = this.files.filter(x => x.name !== fileName); + } + + editPost(): void { + const newMessage = this.editPostFormGroup.get('newPostMessage')?.value; + + if (newMessage !== '') { + this._postService.putPostWithSessionStorageRequest(Guid.parse(this.paramId), newMessage, this.files).subscribe({ + next: () => { + this.reloadPage(); + } + }); + this.loaded = false; + } + } + + deletePost(): void { + this._postService.deletePostWithSessionStorage(Guid.parse(this.paramId)).subscribe({ + next: () => { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + }); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } + + votePost(isLike: boolean): void { + if (!this.loggedIn) { + this._router.navigate(['/login']); + return; + } + + this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({ + next: (x: object) => { + if (x == null) { // checks if result is null or undefined + this.createRating(isLike); + + this.changeColorOfVoteButton(isLike, !isLike); + } + else if (Object.values(x)[3] === isLike) { + this.deleteRating(Object.values(x)[0], isLike); + + this.changeColorOfVoteButton(false, false); + } + else { + this.putRating(isLike); + + this.changeColorOfVoteButton(isLike, !isLike); + } + } + }); + } + + private createRating(isLike: boolean): void { + this._ratingServe.createRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({ + next: () => { + this.votesNumber += -1 + Number(isLike) * 2; + } + }); + } + + private putRating(isLike: boolean): void { + this._ratingServe.putRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({ + next: () => { + // when false -2 + 0 wjen true -2 + 4 + this.votesNumber += -2 + Number(isLike) * 4; + } + }); + } + + private deleteRating(ratingId: string, isLike: boolean): void { + this._ratingServe.deleteRatingFromSessionStorageRequest(Guid.parse(ratingId)).subscribe({ + next: () => { + this.votesNumber += 1 - Number(isLike) * 2; + } + }); + } + + private changeColorOfVoteButton(isUpvoted: boolean, isDownvoted: boolean): void { + this._renderer.setStyle(this.upvoteBtn.nativeElement, 'backgroundColor', (isUpvoted) ? 'var(--upvote-highlight)' : 'inherit'); + this._renderer.setStyle(this.downvoteBtn.nativeElement, 'backgroundColor', (isDownvoted) ? 'var(--downvote-highlight)' : 'inherit'); + } + + resetShareBtn(): void { + if (this._linkShared) { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', this._defaultShareBtnInnerHTML); + this._linkShared = false; + } + } + + showCopiedMessage(): void { + this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', 'Link copied to clipboard!'); + this._linkShared = true; + } + + getPostLink(): string { + return location.origin + '/post/' + this.paramId; + } } diff --git a/src/app/components/profile-settings/profile-settings.component.css b/src/app/components/profile-settings/profile-settings.component.css index 1c07d9f..a8451c2 100644 --- a/src/app/components/profile-settings/profile-settings.component.css +++ b/src/app/components/profile-settings/profile-settings.component.css @@ -1,124 +1,10 @@ -* { - 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; +.sec-info { + width: fit-content; + background-color: #424242; + margin: 0 0.3em 0.3em 0; } diff --git a/src/app/components/profile-settings/profile-settings.component.html b/src/app/components/profile-settings/profile-settings.component.html index 502697d..1471859 100644 --- a/src/app/components/profile-settings/profile-settings.component.html +++ b/src/app/components/profile-settings/profile-settings.component.html @@ -1,116 +1,149 @@ +<app-navbar></app-navbar> + <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> +<main class="scroll-standalone under-navbar centered-content flex-col"> + <section class="card width-full" *ngIf="this.isAdminUser"> + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" (click)="goToAdminPanel()"> + Go to admin panel + </button> + </section> + <form class="flex-row card width-full font-size-dot9 margin-top-bot-dot7" [formGroup]="updateProfilePictureFormGroup" (ngSubmit)="updateProfilePicture()"> + <img id="profile-picture" class="round-image" [src]="user.profilePictureURL"> + <section class="flexible flex-col flex-center-align-items flex-justify-center padding-side-font"> + <input class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="file" accept="image/*" formControlName="fileUpload" (change)="onFileUpload($event)"> + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 margin-top-dot4" type="submit" *ngIf="newProfilePicture.size > 0"> + Update profile picture + </button> + </section> + </form> + <form class="flex-col card width-full padding-dot6" [formGroup]="updateUserFormGroup" (ngSubmit)="onSubmit()"> + <section class="flex-col"> + <div class="flex-row"> + <label class="flexible fg-focus">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> + <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> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="firstName" required> + </section> + <section class="flex-col"> + <div class="flex-row"> + <label class="flexible fg-focus">Last Name</label> - <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> + <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> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="lastName" required> + </section> + <section class="flex-col"> + <div class="flex-row"> + <label class="flexible fg-focus">Username</label> - <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> + <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> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="username" required> + </section> + <section class="flex-col"> + <div class="flex-row"> + <label class="flexible fg-focus">Email</label> - <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> + <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 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> + <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="email" required> + </section> + <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleLanguages()"> + ▼ Edit Languages ▼ + </button> + <section class="flex-row flexible-children border-faded-slim padding-dot3 margin-top-dot5" *ngIf="showLanguages"> + <section class="padding-right-1"> + <div class="none-message" *ngIf="chosenLanguages.length === 0"> + You haven't chosen any languages! </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 class="border-faded-slim border-bottom-only" *ngIf="chosenLanguages.length > 0"> + Chosen languages: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let lang of chosenLanguages" (click)="langClick(lang.name)"> + {{ lang.name }} </div> </div> - Available languages: - <div id="all-languages"> - <div class="user-language" *ngFor="let lang of availableLanguages"> + </section> + <section> + <div class="none-message" *ngIf="availableLanguages.length === 0"> + No other languages available! + </div> + <div class="border-faded-slim border-bottom-only" *ngIf="availableLanguages.length > 0"> + Available languages: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let lang of availableLanguages" (click)="langClick(lang.name)"> {{ 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> + </section> + </section> + <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleTechnologies()"> + ▼ Edit Technologies ▼ + </button> + <section class="flex-row flexible-children border-faded-slim padding-dot3 margin-top-dot5" *ngIf="showTechnologies"> + <section class="padding-right-1"> + <div class="none-message" *ngIf="chosenTechnologies.length === 0"> + You haven't chosen any technologies! + </div> + <div class="border-faded-slim border-bottom-only" *ngIf="chosenTechnologies.length > 0"> + Chosen technologies: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let tech of chosenTechnologies" (click)="techClick(tech.name)"> + {{ tech.name }} </div> </div> - Available technologies: - <div id="all-technologies"> - <div class="user-technology" *ngFor="let tech of availableTechnologies"> + </section> + <section> + <div class="none-message" *ngIf="availableTechnologies.length === 0"> + No other technologies available! + </div> + <div class="border-faded-slim border-bottom-only" *ngIf="availableTechnologies.length > 0"> + Available technologies: + </div> + <div class="flex-row margin-top-dot4"> + <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let tech of availableTechnologies" (click)="techClick(tech.name)"> {{ tech.name }} </div> </div> - </div> + </section> + </section> + <section class="flex-col input-selection"> + <div class="flex-row"> + <label class="flexible fg-focus">Current Password</label> - <button id="update-profile-btn" class="submit-btn" type="submit">Update profile</button> + <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> + <input [type]="showCurrentPassword ? 'text' : 'password'" class="fancy-input border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required> + <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword(0)"> + <img [src]="showCurrentPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'"> + </button> + </section> + <section class="margin-top-bot-dot3"> <app-success-bar></app-success-bar> <app-error-bar></app-error-bar> - </form> - <hr> - <div id="confirm-delete" *ngIf="deleteAccountConfirm"> + </section> + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit"> + Update profile + </button> + </form> + <section class="card width-full"> + <div class="margin-bot-dot5 text-centered fg-error" *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> + <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 fg-error" (click)="deleteAccount()"> + Delete account + </button> + </section> + </main> + diff --git a/src/app/components/profile-settings/profile-settings.component.ts b/src/app/components/profile-settings/profile-settings.component.ts index a484665..3d6e491 100644 --- a/src/app/components/profile-settings/profile-settings.component.ts +++ b/src/app/components/profile-settings/profile-settings.component.ts @@ -6,14 +6,15 @@ 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 { User } from 'src/models/identity/user.model'; 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 { Language } from 'src/models/language.model'; +import { Technology } from 'src/models/technology.model'; import { TokenService } from 'src/app/services/token.service'; import { Title } from '@angular/platform-browser'; import { AppConstants } from 'src/app/app-constants.module'; +import { ProfilePictureService } from 'src/app/services/profile-picture.service'; @Component({ selector: 'app-profile-settings', @@ -34,10 +35,13 @@ export class ProfileSettingsComponent implements OnInit { public updateProfilePictureFormGroup: FormGroup; public newProfilePicture: File; public user: User; + public chosenLanguages: Language[]; + public chosenTechnologies: Technology[]; public availableLanguages: Language[]; public availableTechnologies: Technology[]; + public showCurrentPassword = false; - 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) { + constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _profilePictureService: ProfilePictureService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _fb: FormBuilder, private _location: Location) { this._titleService.setTitle(this._title); } @@ -50,38 +54,40 @@ export class ProfileSettingsComponent implements OnInit { this.availableTechnologies = []; this.newProfilePicture = new File([], ''); - this._userService.getUserByUsernameRequest(this._urlUsername).subscribe( - (res: object) => { + // Initializing forms with blank (default) values + this.updateUserFormGroup = this._fb.group({ + firstName: new FormControl(''), + lastName: new FormControl(''), + username: new FormControl(''), + email: new FormControl(''), + password: new FormControl(''), + }); + this.updateProfilePictureFormGroup = this._fb.group({ + fileUpload: new FormControl('') + }); + + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe({ + next: (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) => { + error: () => { 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) => { + this._userService.getUserFromSessionStorageRequest().subscribe({ + next: (tokenRes: object) => { Object.assign(userFromToken, tokenRes); if (userFromToken.userName === this._urlUsername) { + this.loadUserSecondaryInfo(); this.initForms(); this.dataArrived = true; } @@ -89,16 +95,53 @@ export class ProfileSettingsComponent implements OnInit { this.goToProfile(); } }, - (err: HttpErrorResponse) => { + error: () => { this.logout(); } - ); + }); } else { this.goToProfile(); } } + private loadUserSecondaryInfo(): void { + // Load languages and tehnologies of user + this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then( + (result) => { + this.chosenLanguages = result as Language[]; + this.loadAvailableLanguages(); + } + ); + + this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then( + (result) => { + this.chosenTechnologies = result as Technology[]; + this.loadAvailableTechnologies(); + } + ); + } + + private loadAvailableLanguages(): void { + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe({ + next: (result: object) => { + const allAvailable = result as Language[]; + // Remove the chosen languages from all of the avaiable ones + this.availableLanguages = allAvailable.filter(a => !this.user.languages.some(l => l.name === a.name)); + } + }); + } + + private loadAvailableTechnologies(): void { + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe({ + next: (result: object) => { + const allAvailable = result as Technology[]; + // Remove the chosen technologies from all of the avaiable ones + this.availableTechnologies = allAvailable.filter(a => !this.user.technologies.some(t => t.name === a.name)); + } + }); + } + private initForms(): void { this.updateUserFormGroup = this._fb.group({ firstName: new FormControl(this.user.firstName, [ @@ -122,52 +165,17 @@ export class ProfileSettingsComponent implements OnInit { 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(' ')); - }); + this.updateUserFormGroup.valueChanges.subscribe({ + next: () => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + } }); } @@ -180,11 +188,11 @@ export class ProfileSettingsComponent implements OnInit { return; } - this._userService.putProfilePictureFromSessionStorageRequest(this.newProfilePicture).subscribe( - (result: object) => { + this._profilePictureService.putPictureWithSessionStorageRequest(this.newProfilePicture).subscribe({ + next: () => { this.reloadPage(); } - ); + }); this.dataArrived = false; } @@ -195,14 +203,20 @@ export class ProfileSettingsComponent implements OnInit { this.patchLanguagesControl(); this.patchTechnologiesControl(); - this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.user.roles, this.user.friends).subscribe( - (result: object) => { + this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.chosenLanguages, this.chosenTechnologies, this.user.roles, this.user.friends).subscribe({ + next: () => { this._successBar.showMsg('Profile updated successfully!'); + + // "Reload" page when changing username + const newUsername = this.updateUserFormGroup.get('username')?.value; + if (newUsername !== this._urlUsername) { + this._router.navigate(['/profile/' + newUsername + '/settings']); + } }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); } private patchLanguagesControl(): void { @@ -261,17 +275,47 @@ export class ProfileSettingsComponent implements OnInit { } } + langClick(name: string): void { + if (this.chosenLanguages.some(c => c.name === name)) { + const index = this.chosenLanguages.findIndex(t => t.name === name); + + this.availableLanguages.push(this.chosenLanguages[index]); + this.chosenLanguages.splice(index, 1); + } + else { + const index = this.availableLanguages.findIndex(t => t.name === name); + + this.chosenLanguages.push(this.availableLanguages[index]); + this.availableLanguages.splice(index, 1); + } + } + + techClick(name: string): void { + if (this.chosenTechnologies.some(c => c.name === name)) { + const index = this.chosenTechnologies.findIndex(t => t.name === name); + + this.availableTechnologies.push(this.chosenTechnologies[index]); + this.chosenTechnologies.splice(index, 1); + } + else { + const index = this.availableTechnologies.findIndex(t => t.name === name); + + this.chosenTechnologies.push(this.availableTechnologies[index]); + this.availableTechnologies.splice(index, 1); + } + } + goToProfile(): void { this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]); } - navigateToAdminPanel(): void { + goToAdminPanel(): void { this._router.navigate(['/admin-panel']); } logout(): void { this._tokenService.logoutUserFromSessionStorage(); - this.goToProfile(); + this._router.navigate(['/login']); } toggleLanguages(): void { @@ -284,14 +328,14 @@ export class ProfileSettingsComponent implements OnInit { deleteAccount(): void { if (this.deleteAccountConfirm) { - this._userService.deleteUserFromSessionStorageRequest().subscribe( - (res: object) => { + this._userService.deleteUserFromSessionStorageRequest().subscribe({ + next: () => { this.logout(); }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { this._errorBar.showError(err); } - ); + }); this.dataArrived = false; } else { @@ -304,4 +348,10 @@ export class ProfileSettingsComponent implements OnInit { this._router.onSameUrlNavigation = 'reload'; this._router.navigate([this._router.url]); } + + toggleShowPassword(index: number): void { + switch (index) { + case 0: this.showCurrentPassword = !this.showCurrentPassword; + } + } } diff --git a/src/app/components/profile/profile.component.css b/src/app/components/profile/profile.component.css index ebcd406..c7e112f 100644 --- a/src/app/components/profile/profile.component.css +++ b/src/app/components/profile/profile.component.css @@ -1,105 +1,27 @@ -* { - box-sizing: border-box; +#user-info { + font-size: 1.3em; } -#content { - max-width: 22em; - justify-content: start; +#profile-picture { + height: 5rem; + width: 5rem; } -hr { - width: calc(100% - 1em); - color: black; - border: 1px solid black; +.sec-info-card { + padding-bottom: calc(var(--card-padding) - 0.3em); } -form { - width: 100%; +.sec-info-title { + padding-bottom: 0.2em; + margin-bottom: 0.3em; } -/* 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; +.none-message { + margin-bottom: 0.3em; } -#posts { - width: 100%; - height: 100%; +.sec-info { + width: fit-content; + background-color: #424242; + margin: 0 0.3em 0.3em 0; } diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html index 0e5f633..c6fdee6 100644 --- a/src/app/components/profile/profile.component.html +++ b/src/app/components/profile/profile.component.html @@ -1,60 +1,57 @@ +<app-navbar></app-navbar> + <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> +<main class="scroll-standalone under-navbar flex-col centered-content" *ngIf="dataArrived" (scroll)="onScroll($event)"> + <section id="user-info" class="card flex-row width-full flex-justify-center"> + <div> + <img id="profile-picture" class="round-image" [src]="user.profilePictureURL"> </div> - <div class="secondary-info rounded-border"> - Languages: - <div *ngFor="let lang of user.languages"> - <div class="user-language"> - {{ lang.name }} - </div> + <div class="flexible flex-col flex-center-align-items flex-justify-center side-padding-dot3"> + <div class="text-centered"> + {{ user.firstName }} {{ user.lastName }} </div> - <div *ngIf="user.languages.length === 0"> - None + <div> + @{{ user.userName }} </div> + <button id="add-friend" class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 font-size-dot8 margin-top-dot4" (click)="modifyFriend()" *ngIf="!isTheLoggedInUser && isUserLoggedIn"> + {{ friendOfUser ? 'Unfriend' : 'Add friend' }} + </button> </div> - <div class="secondary-info rounded-border"> - Technologies: - <div *ngFor="let tech of user.technologies"> - <div class="user-language"> - {{ tech.name }} - </div> - </div> - <div *ngIf="user.technologies.length === 0"> - None - </div> + </section> + <section class="card sec-info-card flex-col width-full"> + <div class="sec-info-title border-faded-slim border-bottom-only"> + Languages + </div> + <div class="none-message" *ngIf="user.languages.length === 0"> + None </div> - <hr> - <div id="posts"> - <div id="no-posts" *ngIf="userPosts.length === 0"> - {{ user.firstName }} {{ user.lastName }} hasn't posted anything yet! + <div class="flex-row"> + <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let lang of user.languages"> + {{ lang.name }} </div> - <div *ngFor="let userPost of userPosts"> - <app-post [paramId]="userPost.postId.toString()"></app-post> + </div> + </section> + <section class="card sec-info-card flex-col width-full"> + <div class="sec-info-title border-faded-slim border-bottom-only"> + Technologies + </div> + <div class="none-message" *ngIf="user.technologies.length === 0"> + None + </div> + <div class="flex-row"> + <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let tech of user.technologies"> + {{ tech.name }} </div> </div> - </div> -</div> + </section> + <hr class="card-hr"> + <section> + <div class="text-centered" *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> + </section> +</main> diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts index bbf8585..f9f64b5 100644 --- a/src/app/components/profile/profile.component.ts +++ b/src/app/components/profile/profile.component.ts @@ -1,18 +1,16 @@ 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 { User } from 'src/models/identity/user.model'; 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 { Post } from 'src/models/post.model'; 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'; +import { FriendService } from 'src/app/services/friend.service'; @Component({ selector: 'app-profile', @@ -29,44 +27,34 @@ export class ProfileComponent implements OnInit { 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) { + constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _friendService: FriendService, 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 + now.setHours(now.getHours() + 3); // 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) => { + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe({ + next: (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) => { + error: () => { this._router.navigate(['/not-found']); } - ); + }); } private loadLanguages(): void { @@ -96,17 +84,17 @@ export class ProfileComponent implements OnInit { } private loadPosts(): void { - this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( - (result: object) => { + this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe({ + next: (result: object) => { const resultArr: Post[] = Object.values(result)[0]; this.userPosts.push(...resultArr); this.finishUserLoading(); }, - (err: HttpErrorResponse) => { + error: () => { this._currentPage = -1; this.finishUserLoading(); } - ); + }); } private finishUserLoading(): void { @@ -114,8 +102,8 @@ export class ProfileComponent implements OnInit { this.isUserLoggedIn = true; const userFromToken: User = this._userService.getDefaultUser(); - this._userService.getUserFromSessionStorageRequest().subscribe( - (tokenRes: object) => { + this._userService.getUserFromSessionStorageRequest().subscribe({ + next: (tokenRes: object) => { Object.assign(userFromToken, tokenRes); if (userFromToken.friends.map(x => x.userName).includes(this._urlUsername)) { @@ -126,28 +114,16 @@ export class ProfileComponent implements OnInit { } this.dataArrived = true; }, - (err: HttpErrorResponse) => { + error: () => { 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(); @@ -158,34 +134,35 @@ export class ProfileComponent implements OnInit { } modifyFriend(): void { - if (this.updatingFriendship) { - this.dataArrived = false; - - this._userService.getUserFromSessionStorageRequest().subscribe( - (result: object) => { - const loggedInUser: User = result as User; + this.dataArrived = false; + if (this.friendOfUser) { + this.removeFriendFromLoggedInUser(); + } + else { + this.addFriendToLoggedInUser(); + } + } - 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); - } + private addFriendToLoggedInUser(): void { + this._friendService.postFriendWithSessionStorageRequest(this.user.userName).subscribe({ + next: () => { + this.reloadPage(); + }, + error: () => { + this._router.navigate(['/']); + } + }); + } - this._userService.putBareUserFromSessionStorageRequest(loggedInUser, this.updateFrienship.get('password')?.value).subscribe( - (resultUpdate: object) => { - this.reloadPage(); - }, - (err: HttpErrorResponse) => { - this._router.navigate(['/']); - } - ); - } - ); - } - this.updatingFriendship = !this.updatingFriendship; + private removeFriendFromLoggedInUser(): void { + this._friendService.deleteFriendWithSessionStorageRequest(this.user.userName).subscribe({ + next: () => { + this.reloadPage(); + }, + error: () => { + this._router.navigate(['/']); + } + }); } onScroll(event: any): void { diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css index 93d8006..e69de29 100644 --- a/src/app/components/register/register.component.css +++ b/src/app/components/register/register.component.css @@ -1,40 +0,0 @@ -/* 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 index 4e67e0e..d8fa7c4 100644 --- a/src/app/components/register/register.component.html +++ b/src/app/components/register/register.component.html @@ -1,65 +1,57 @@ -<div id="content"> - <div class="title">Register</div> +<main class="centered-content scroll-standalone height-full flex-col flex-center-align-items flex-justify-center"> + <summary class="title card width-full margin-0 padding-dot2">Register</summary> - <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> + <app-error-bar class="width-full margin-top-dot4"></app-error-bar> + <form class="width-full card padding-dot6" [formGroup]="registerUserFormGroup" (ngSubmit)="onSubmit()"> + <section class="input-selection width-full"> + <input type="text" placeholder="First Name" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="firstName" required> + <label class="fancy-input-label width-full">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> - + </section> + <section class="input-selection"> + <input type="text" placeholder="Last Name" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="lastName" required> + <label class="fancy-input-label width-full">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> - + </section> + <section class="input-selection"> + <input type="text" placeholder="Username" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="username" required> + <label class="fancy-input-label width-full">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> - + </section> + <section class="input-selection"> + <input type="text" placeholder="Email" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="email" required> + <label class="fancy-input-label width-full">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> - + </section> + <section class="input-selection"> + <input [type]="showingPassword ? 'text' : 'password'" placeholder="Password" class="fancy-input width-full border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required> + <label class="fancy-input-label width-full">Password</label> + <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword()"> + <img [src]="showingPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'"> + </button> <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> + </section> - <hr> - <button class="submit-btn" type="submit">Submit</button> - <app-error-bar></app-error-bar> + <button class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full" type="submit">Submit</button> </form> - <button class="submit-btn redirect-to-login" (click)="onRedirectLogin()">Already have an account? Login here</button> -</div> + <button class="fg-focus border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full margin-top-dot4" (click)="onRedirectLogin()"> + Already have an account? Login here + </button> +</main> diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts index 36eaa55..6d84e11 100644 --- a/src/app/components/register/register.component.ts +++ b/src/app/components/register/register.component.ts @@ -1,6 +1,6 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnInit, ViewChild } from '@angular/core'; -import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { 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'; @@ -16,6 +16,7 @@ export class RegisterComponent implements OnInit { @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; private _title = 'Register'; public registerUserFormGroup: FormGroup; + public showingPassword = false; constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { this._titleService.setTitle(this._title); @@ -49,38 +50,23 @@ export class RegisterComponent implements OnInit { // this.registerUserFormGroup.valueChanges.subscribe(console.log); } + toggleShowPassword(): void { + this.showingPassword = !this.showingPassword; + } + onSubmit(): void { - this._userService.registerUserRequest(this.registerUserFormGroup).subscribe( - res => { + this._userService.registerUserRequest(this.registerUserFormGroup).subscribe({ + next: (res: object) => { this._tokenService.setUserTokenToSessionStorage(res); this._router.navigate(['/']); }, - (err: HttpErrorResponse) => { + error: (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 index bee634d..32fe099 100644 --- a/src/app/components/success-bar/success-bar.component.css +++ b/src/app/components/success-bar/success-bar.component.css @@ -4,6 +4,7 @@ color: white; padding: .2em; text-align: center; + box-sizing: border-box; } #success-bar:empty { diff --git a/src/app/services/comment.service.ts b/src/app/services/comment.service.ts index c9dbf35..9839b01 100644 --- a/src/app/services/comment.service.ts +++ b/src/app/services/comment.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Guid } from 'guid-typescript'; import { Observable } from 'rxjs'; -import { Comment } from 'src/models/comment'; +import { Comment } from 'src/models/comment.model'; import { AppConstants } from '../app-constants.module'; import { TokenService } from './token.service'; @@ -14,7 +14,7 @@ export class CommentService { { } getDefaultComment(): Comment { - return new Comment(Guid.createEmpty(), Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date()); + return new Comment(Guid.createEmpty(), Guid.createEmpty(), '', '', '', '', new Date()); } /* Requests from session storage */ @@ -55,7 +55,7 @@ export class CommentService { getCommentRequest(id: Guid): Observable<object> { const options = { - params: new HttpParams().set('Id', id.toString()) + params: new HttpParams().set('CommentId', id.toString()) }; return this._http.get(AppConstants.API_COMMENT_URL, options); } @@ -75,7 +75,7 @@ export class CommentService { deleteCommentRequest(commentId: Guid, authToken: string): Observable<object> { const options = { - params: new HttpParams().set('Id', commentId.toString()), + params: new HttpParams().set('CommentId', commentId.toString()), headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) }; return this._http.delete(AppConstants.API_COMMENT_URL, options); diff --git a/src/app/services/friend.service.ts b/src/app/services/friend.service.ts new file mode 100644 index 0000000..6821606 --- /dev/null +++ b/src/app/services/friend.service.ts @@ -0,0 +1,44 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Guid } from 'guid-typescript'; +import { Observable } from 'rxjs'; +import { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class FriendService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + postFriendWithSessionStorageRequest(friendUsername: string): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.postFriendRequest(userId, token, friendUsername); + } + + deleteFriendWithSessionStorageRequest(friendUsername: string): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteFriendRequest(userId, token, friendUsername); + } + + postFriendRequest(userId: Guid, authToken: string, friendUsername: string): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()).set('FriendUsername', friendUsername), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_FRIENDS_URL, {}, options); + } + + deleteFriendRequest(userId: Guid, authToken: string, friendUsername: string): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()).set('FriendUsername', friendUsername), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.delete(AppConstants.API_FRIENDS_URL, options); + } +} diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts index 15e241f..ec368da 100644 --- a/src/app/services/language.service.ts +++ b/src/app/services/language.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Guid } from 'guid-typescript'; import { Observable } from 'rxjs'; -import { Language } from 'src/models/language'; +import { Language } from 'src/models/language.model'; import { AppConstants } from '../app-constants.module'; import { TokenService } from './token.service'; @@ -105,7 +105,7 @@ export class LanguageService { deleteLanguageRequest(authToken: string, langId: Guid): Observable<object> { const options = { - params: new HttpParams().set('Id', langId.toString()), + params: new HttpParams().set('LanguageId', langId.toString()), headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) }; return this._http.delete(AppConstants.API_LANGUAGE_URL, options); diff --git a/src/app/services/post.service.ts b/src/app/services/post.service.ts index 7b2a539..1a6e021 100644 --- a/src/app/services/post.service.ts +++ b/src/app/services/post.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import * as FormData from 'form-data'; import { Guid } from 'guid-typescript'; import { Observable } from 'rxjs'; -import { Post } from 'src/models/post'; +import { Post } from 'src/models/post.model'; import { AppConstants } from '../app-constants.module'; import { TokenService } from './token.service'; @@ -15,7 +15,7 @@ export class PostService { { } getDefaultPost(): Post { - return new Post(Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date(), [], []); + return new Post(Guid.createEmpty(), '', '', '', '', new Date(), [], [], 0); } /* Requests from session storage */ @@ -78,7 +78,7 @@ export class PostService { deletePostRequest(postId: Guid, authToken: string): Observable<object> { const options = { - params: new HttpParams().set('Id', postId.toString()), + params: new HttpParams().set('PostId', postId.toString()), headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) }; return this._http.delete(AppConstants.API_POST_URL, options); diff --git a/src/app/services/profile-picture.service.ts b/src/app/services/profile-picture.service.ts new file mode 100644 index 0000000..8b9d0a3 --- /dev/null +++ b/src/app/services/profile-picture.service.ts @@ -0,0 +1,32 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Guid } from 'guid-typescript'; +import { Observable } from 'rxjs'; +import { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProfilePictureService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + putPictureWithSessionStorageRequest(newPicture: File): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putRatingRequest(userId, token, newPicture); + } + + putRatingRequest(userId: Guid, authToken: string, newPicture: File): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + const form = new FormData(); + form.append('picture', newPicture); + + return this._http.put(AppConstants.API_PROFILE_PICTURE_URL, form, options); + } +} diff --git a/src/app/services/rating.service.ts b/src/app/services/rating.service.ts new file mode 100644 index 0000000..be35a81 --- /dev/null +++ b/src/app/services/rating.service.ts @@ -0,0 +1,86 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Guid } from 'guid-typescript'; +import { Observable } from 'rxjs'; +import { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class RatingService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + createRatingWithSessionStorageRequest(postId: Guid, isLike: boolean): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.createRatingRequest(userId, token, postId, isLike); + } + + putRatingWithSessionStorageRequest(postId: Guid, isLike: boolean): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putRatingRequest(userId, token, postId, isLike); + } + + getRatingByUserAndPostWithSessionStorageRequest(postId: Guid): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.getRatingByUserAndPostRequest(userId, token, postId); + } + + deleteRatingFromSessionStorageRequest(ratingId: Guid): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteRatingRequest(userId, token, ratingId); + } + + createRatingRequest(userId: Guid, authToken: string, postId: Guid, isLike: boolean): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + const body = { + postId: postId.toString(), + isLike: isLike + }; + + return this._http.post(AppConstants.API_RATING_URL, body, options); + } + + putRatingRequest(userId: Guid, authToken: string, postId: Guid, isLike: boolean): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()).set('PostId', postId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + const body = { + isLike: isLike + }; + + return this._http.put(AppConstants.API_RATING_URL, body, options); + } + + getRatingByUserAndPostRequest(userId: Guid, authToken: string, postId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()).set('PostId', postId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + + return this._http.get(AppConstants.API_RATING_URL + '/GetByUserAndPost', options); + } + + deleteRatingRequest(userId: Guid, authToken: string, ratingId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('UserId', userId.toString()).set('RatingId', ratingId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + + return this._http.delete(AppConstants.API_RATING_URL, options); + } +} diff --git a/src/app/services/technology.service.ts b/src/app/services/technology.service.ts index dbdc039..fcc3d4c 100644 --- a/src/app/services/technology.service.ts +++ b/src/app/services/technology.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Guid } from 'guid-typescript'; import { Observable } from 'rxjs'; -import { Technology } from 'src/models/technology'; +import { Technology } from 'src/models/technology.model'; import { AppConstants } from '../app-constants.module'; import { TokenService } from './token.service'; diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index 31862c4..690fff5 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core'; import { Guid } from 'guid-typescript'; -import { User } from '../../models/identity/user'; +import { User } from '../../models/identity/user.model'; import { FormGroup } from '@angular/forms'; import { AppConstants } from 'src/app/app-constants.module'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { Role } from 'src/models/identity/role'; -import { Friend } from 'src/models/identity/friend'; +import { Role } from 'src/models/identity/role.model'; +import { Friend } from 'src/models/identity/friend.model'; import { TokenService } from './token.service'; +import { Language } from 'src/models/language.model'; +import { Technology } from 'src/models/technology.model'; @Injectable({ providedIn: 'root' @@ -17,7 +19,7 @@ export class UserService { { } getDefaultUser(): User { - return new User(Guid.createEmpty(), 'gosho_trapov', 'Gosho', 'Trapov', 'gotra@bg.com', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []); + return new User(Guid.createEmpty(), '', '', '', '', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []); } /* Requests from session storage */ @@ -36,25 +38,11 @@ export class UserService { return this.addFriendToUserRequest(userUserName, token, newFriendUserName); } - putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> { + putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, languages: Language[], technologies: Technology[], userRoles: Role[], userFriends: Friend[]): Observable<object> { const userId = this._tokenService.getUserIdFromSessionStorageToken(); const token = this._tokenService.getTokenFromSessionStorage(); - return this.putUserRequest(userId, token, updateUserFormGroup, userRoles, userFriends); - } - - putProfilePictureFromSessionStorageRequest(newPicture: File): Observable<object> { - const userId = this._tokenService.getUserIdFromSessionStorageToken(); - const token = this._tokenService.getTokenFromSessionStorage(); - - return this.putProfilePictureRequest(userId, token, newPicture); - } - - putBareUserFromSessionStorageRequest(user: User, password: string): Observable<object> { - const userId = this._tokenService.getUserIdFromSessionStorageToken(); - const token = this._tokenService.getTokenFromSessionStorage(); - - return this.putBareUserRequest(userId, token, user, password); + return this.putUserRequest(userId, token, updateUserFormGroup, languages, technologies, userRoles, userFriends); } deleteUserFromSessionStorageRequest(): Observable<object> { @@ -119,7 +107,7 @@ export class UserService { return this._http.get(AppConstants.API_USER_URL + '/GetUser', options); } - putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> { + putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, languages: Language[], technologies: Technology[], userRoles: Role[], userFriends: Friend[]): Observable<object> { const body = { UserName: updateUserFormGroup.get('username')?.value, Email: updateUserFormGroup.get('email')?.value, @@ -128,19 +116,9 @@ export class UserService { Password: updateUserFormGroup.get('password')?.value, Roles: userRoles, Friends: userFriends, - Languages: updateUserFormGroup.get('languages')?.value, - Technologies: updateUserFormGroup.get('technologies')?.value - }; - const options = { - params: new HttpParams().set('Id', userId.toString()), - headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + Languages: languages, + Technologies: technologies }; - return this._http.put(AppConstants.API_USER_URL, body, options); - } - - putBareUserRequest(userId: Guid, authToken: string, user: User, password: string): Observable<object> { - const body: object = user; - Object.assign(body, { password: password }); const options = { params: new HttpParams().set('Id', userId.toString()), headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) @@ -148,16 +126,6 @@ export class UserService { return this._http.put(AppConstants.API_USER_URL, body, options); } - putProfilePictureRequest(userId: Guid, authToken: string, newPicture: File): Observable<object> { - const form = new FormData(); - form.append('picture', newPicture); - const options = { - params: new HttpParams().set('UserId', userId.toString()), - headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) - }; - return this._http.put(AppConstants.API_USER_URL + '/ProfilePicture', form, options); - } - deleteUserRequest(userId: Guid, authToken: string): Observable<object> { const options = { params: new HttpParams().set('Id', userId.toString()), |
