diff options
Diffstat (limited to 'src')
94 files changed, 4784 insertions, 0 deletions
diff --git a/src/app/app-constants.module.ts b/src/app/app-constants.module.ts new file mode 100644 index 0000000..d72af53 --- /dev/null +++ b/src/app/app-constants.module.ts @@ -0,0 +1,20 @@ +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_USER_LOGIN_URL = AppConstants.API_USER_URL + '/login'; + public static API_USER_REGISTER_URL = AppConstants.API_USER_URL + '/register'; + + public static API_LANGUAGE_URL = AppConstants.BASE_API_URL + '/Language'; + public static API_TECHNOLOGY_URL = AppConstants.BASE_API_URL + '/Technology'; + + public static API_POST_URL = AppConstants.BASE_API_URL + '/Post'; + 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 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 new file mode 100644 index 0000000..0d83079 --- /dev/null +++ b/src/app/app-routing.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { FeedComponent } from './components/feed/feed.component'; +import { LoginComponent } from './components/login/login.component'; +import { RegisterComponent } from './components/register/register.component'; +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'; + +const routes: Routes = [ + { path: '', component: FeedComponent }, + { path: 'login', component: LoginComponent }, + { path: 'register', component: RegisterComponent }, + { path: 'profile/:username', component: ProfileComponent }, + { path: 'profile/:username/settings', component: ProfileSettingsComponent }, + { path: 'post/:id', component: PostPageComponent }, + { path: 'comment/:id', component: CommentPageComponent }, + { path: 'admin-panel', component: AdminPanelPageComponent }, + { path: 'not-found', component: NotFoundComponent }, + { path: '**', component: NotFoundComponent } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/src/app/app-shell/app-shell.component.css b/src/app/app-shell/app-shell.component.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/app/app-shell/app-shell.component.css diff --git a/src/app/app-shell/app-shell.component.html b/src/app/app-shell/app-shell.component.html new file mode 100644 index 0000000..b5f713a --- /dev/null +++ b/src/app/app-shell/app-shell.component.html @@ -0,0 +1 @@ +<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 new file mode 100644 index 0000000..2c4fabc --- /dev/null +++ b/src/app/app-shell/app-shell.component.spec.ts @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..f1bca89 --- /dev/null +++ b/src/app/app-shell/app-shell.component.ts @@ -0,0 +1,15 @@ +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.component.css b/src/app/app.component.css new file mode 100644 index 0000000..aaebe5b --- /dev/null +++ b/src/app/app.component.css @@ -0,0 +1,4 @@ +.main { + width: 100%; + height: 100%; +} diff --git a/src/app/app.component.html b/src/app/app.component.html new file mode 100644 index 0000000..6c51026 --- /dev/null +++ b/src/app/app.component.html @@ -0,0 +1,3 @@ +<div class="main"> + <router-outlet></router-outlet> +</div> diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts new file mode 100644 index 0000000..e4bf775 --- /dev/null +++ b/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'Angular'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('Angular'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('Angular app is running!'); + }); +}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts new file mode 100644 index 0000000..b9f46ae --- /dev/null +++ b/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'Angular'; +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..1bf44ad --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,62 @@ +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 { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { LoginComponent } from './components/login/login.component'; +import { RegisterComponent } from './components/register/register.component'; +import { FeedComponent } from './components/feed/feed.component'; +import { PostComponent } from './components/post/post.component'; +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 { LoadingComponent } from './components/loading/loading.component'; +import { ErrorBarComponent } from './components/error-bar/error-bar.component'; +import { SuccessBarComponent } from './components/success-bar/success-bar.component'; +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'; + +@NgModule({ + declarations: [ + AppComponent, + LoginComponent, + RegisterComponent, + FeedComponent, + PostComponent, + ProfileComponent, + ProfileSettingsComponent, + NotFoundComponent, + LoadingComponent, + ErrorBarComponent, + SuccessBarComponent, + PostPageComponent, + AdminPanelPageComponent, + CommentComponent, + CommentPageComponent, + PostAttachmentComponent + ], + imports: [ + BrowserModule.withServerTransition({ appId: 'serverApp' }), + AppRoutingModule, + ReactiveFormsModule, + BrowserAnimationsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + HttpClientModule, + RouterModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/src/app/app.server.module.ts b/src/app/app.server.module.ts new file mode 100644 index 0000000..18b08b1 --- /dev/null +++ b/src/app/app.server.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; +import { Routes, RouterModule } from '@angular/router'; +import { AppShellComponent } from './app-shell/app-shell.component'; + +const routes: Routes = [ { path: 'shell', component: AppShellComponent }]; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + RouterModule.forRoot(routes), + ], + bootstrap: [AppComponent], + declarations: [AppShellComponent], +}) +export class AppServerModule {} diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.css b/src/app/components/admin-panel-page/admin-panel-page.component.css new file mode 100644 index 0000000..1f98e20 --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.css @@ -0,0 +1,42 @@ +#content { + max-width: 22em; + justify-content: start; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +#navigation { + width: 100%; + display: flex; +} + +#navigation > * { + flex: 1; + margin-left: .4em; +} + +.submit-btn:first-of-type { + margin-left: 0 !important; +} + +#all-languages, #all-technologies { + display: flex; + flex-wrap: wrap; +} + +.flexbox { + display: flex; +} + +.flexbox > * { + flex: 1; + margin-left: 1em; +} + +.flexbox > *:first-child { + margin-left: 0; +} diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.html b/src/app/components/admin-panel-page/admin-panel-page.component.html new file mode 100644 index 0000000..980f12c --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.html @@ -0,0 +1,85 @@ +<!-- <app-loading *ngIf="!dataArrived"></app-loading> --> + +<div id="content" *ngIf="!dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="backToProfile()">ᐊ Back to profile</button> + <button class="submit-btn" (click)="backToFeed()">ᐊ Back to feed</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <hr> + <div class="scroll-standalone"> + <app-success-bar></app-success-bar> + <app-error-bar></app-error-bar> + + <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button> + <form [formGroup]="languageForm" (ngSubmit)="submitLanguages()" *ngIf="showLanguages"> + <div class="input-selection"> + <label>Create language:</label> + <input type="text" class="input-field" formControlName="languageCreate" placeholder="New language name"> + </div> + <label>Update language:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="updateLanguageOldName" placeholder="Old language name"> + <input type="text" class="input-field" formControlName="updateLanguageNewName" placeholder="New language name"> + </div> + <label>Delete language:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="deleteLanguageName" placeholder="Language name"> + </div> + <button class="submit-btn" type="submit">Modify languages</button> + <hr> + Available languages: + <div id="all-languages"> + <div class="user-language" *ngFor="let lang of availableLanguages"> + {{ lang.name }} + </div> + </div> + <hr> + </form> + + <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button> + <form [formGroup]="technologyForm" (ngSubmit)="submitTechnologies()" *ngIf="showTechnologies"> + <div class="input-selection"> + <label>Create technology:</label> + <input type="text" class="input-field" formControlName="technologyCreate" placeholder="New technology name"> + </div> + <label>Update technology:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="updateTechnologyOldName" placeholder="Old technology name"> + <input type="text" class="input-field" formControlName="updateTechnologyNewName" placeholder="New technology name"> + </div> + <label>Delete technology:</label> + <div class="flexbox input-selection"> + <input type="text" class="input-field" formControlName="deleteTechnologyName" placeholder="Technology name"> + </div> + <button class="submit-btn" type="submit">Modify technologies</button> + <hr> + Available technologies: + <div id="all-technologies"> + <div class="user-technology" *ngFor="let tech of availableTechnologies"> + {{ tech.name }} + </div> + </div> + <hr> + </form> + + <button type="button" class="submit-btn delete-btn" (click)="toggleDeletions()">▼ Deletions ▼</button> + <form [formGroup]="deleteForm" (ngSubmit)="submitDelete()" *ngIf="showDeletions"> + <div class="input-selection"> + <label>Delete user by Id:</label> + <input type="text" class="input-field" formControlName="deleteUser" placeholder="User Id"> + </div> + + <div class="input-selection"> + <label>Delete post by Id:</label> + <input type="text" class="input-field" formControlName="deletePost" placeholder="Post Id"> + </div> + <div class="input-selection"> + <label>Delete comment by Id:</label> + <input type="text" class="input-field" formControlName="deleteComment" placeholder="Comment Id"> + </div> + <button class="submit-btn" type="submit">Delete</button> + <hr> + </form> + </div> +</div> diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.ts b/src/app/components/admin-panel-page/admin-panel-page.component.ts new file mode 100644 index 0000000..99c0721 --- /dev/null +++ b/src/app/components/admin-panel-page/admin-panel-page.component.ts @@ -0,0 +1,334 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { AppConstants } from 'src/app/app-constants.module'; +import { CommentService } from 'src/app/services/comment.service'; +import { LanguageService } from 'src/app/services/language.service'; +import { PostService } from 'src/app/services/post.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { TokenService } from 'src/app/services/token.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { Language } from 'src/models/language'; +import { Technology } from 'src/models/technology'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { SuccessBarComponent } from '../success-bar/success-bar.component'; + +@Component({ + selector: 'app-admin-panel-page', + templateUrl: './admin-panel-page.component.html', + styleUrls: ['./admin-panel-page.component.css'] +}) +export class AdminPanelPageComponent implements OnInit { + private _title = 'Admin Panel'; + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent; + public dataArrived = false; + public showLanguages = false; + public showTechnologies = false; + public showDeletions = false; + public availableLanguages: Language[]; + public availableTechnologies: Technology[]; + public languageForm: FormGroup; + public technologyForm: FormGroup; + public deleteForm: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + if (!this._tokenService.getTokenFromSessionStorage()) { + this._router.navigate(['/login']); + return; + } + + this._userService.getUserFromSessionStorageRequest().subscribe( + (result: object) => { + const user = result as User; + if (!user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME)) { + this._router.navigate(['/login']); + } + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/login']); + } + ); + + this.languageForm = this._fb.group({ + languageCreate: new FormControl(''), + updateLanguageOldName: new FormControl(''), + updateLanguageNewName: new FormControl(''), + deleteLanguageName: new FormControl('') + }); + + this.languageForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.technologyForm = this._fb.group({ + technologyCreate: new FormControl(''), + updateTechnologyOldName: new FormControl(''), + updateTechnologyNewName: new FormControl(''), + deleteTechnologyName: new FormControl('') + }); + + this.technologyForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.deleteForm = this._fb.group({ + deleteUser: new FormControl(''), + deletePost: new FormControl(''), + deleteComment: new FormControl('') + }); + + this.deleteForm.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + + this.loadAvailableLanguages(); + this.loadAvailableTechnologies(); + } + + // Navigation + + backToProfile(): void { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this._router.navigate(['/login']); + } + + // Language modifying + + toggleLanguages(): void { + this.showLanguages = !this.showLanguages; + } + + submitLanguages(): void { + this.tryCreateLanguage(); + this.tryUpdateLanguage(); + this.tryDeleteLanguage(); + } + + private tryCreateLanguage(): void { + const languageCreate: string = this.languageForm.get('languageCreate')?.value; + + if (languageCreate !== '' && languageCreate !== null) { + this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully updated languages!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryUpdateLanguage(): void { + const updateLanguageOldName: string = this.languageForm.get('updateLanguageOldName')?.value; + const updateLanguageNewName: string = this.languageForm.get('updateLanguageNewName')?.value; + + if (updateLanguageOldName !== '' && updateLanguageOldName !== null && updateLanguageNewName !== '' && updateLanguageNewName !== null) { + const langId = this.availableLanguages.filter(x => x.name === updateLanguageOldName.trim())[0].id; + + this._languageService.putLanguageWithSessionStorageRequest(langId, updateLanguageNewName.trim()).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully updated languages!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteLanguage(): void { + const deleteLanguageName: string = this.languageForm.get('deleteLanguageName')?.value; + + if (deleteLanguageName !== '' && deleteLanguageName !== null) { + const langId = this.availableLanguages.filter(x => x.name === deleteLanguageName.trim())[0].id; + + this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe( + (result: object) => { + this.languageModifiedSuccess('Successfully deleted language!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private languageModifiedSuccess(successMsg: string): void { + this.languageForm.reset(); + this._successBar.showMsg(successMsg); + this.loadAvailableLanguages(); + } + + private loadAvailableLanguages(): void { + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableLanguages = result as Language[]; + } + ); + } + + // Technology modifying + + toggleTechnologies(): void { + this.showTechnologies = !this.showTechnologies; + } + + submitTechnologies(): void { + this.tryCreateTechnology(); + this.tryUpdateTechnology(); + this.tryDeleteTechnology(); + } + + private tryCreateTechnology(): void { + const technologyCreate: string = this.technologyForm.get('technologyCreate')?.value; + + if (technologyCreate !== '' && technologyCreate !== null) { + this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully updated technologies!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryUpdateTechnology(): void { + const updateTechnologyOldName: string = this.technologyForm.get('updateTechnologyOldName')?.value; + const updateTechnologyNewName: string = this.technologyForm.get('updateTechnologyNewName')?.value; + + if (updateTechnologyOldName !== '' && updateTechnologyOldName !== null && updateTechnologyNewName !== '' && updateTechnologyNewName !== null) { + const techId = this.availableTechnologies.filter(x => x.name === updateTechnologyOldName.trim())[0].id; + + this._technologyService.putTechnologyWithSessionStorageRequest(techId, updateTechnologyNewName.trim()).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully updated technologies!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteTechnology(): void { + const deleteTechnologyName: string = this.technologyForm.get('deleteTechnologyName')?.value; + + if (deleteTechnologyName !== '' && deleteTechnologyName !== null) { + const techId = this.availableTechnologies.filter(x => x.name === deleteTechnologyName.trim())[0].id; + + this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe( + (result: object) => { + this.technologyModifiedSuccess('Successfully deleted technology!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private technologyModifiedSuccess(successMsg: string): void { + this.technologyForm.reset(); + this._successBar.showMsg(successMsg); + this.loadAvailableTechnologies(); + } + + private loadAvailableTechnologies(): void { + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableTechnologies = result as Technology[]; + } + ); + } + + // Deletions + + toggleDeletions(): void { + this.showDeletions = !this.showDeletions; + } + + submitDelete(): void { + this.tryDeleteUser(); + this.tryDeletePost(); + this.tryDeleteComment(); + } + + private tryDeleteUser(): void { + const deleteUser: string = this.deleteForm.get('deleteUser')?.value; + + if (deleteUser !== '' && deleteUser !== null) { + const userId: Guid = Guid.parse(deleteUser); + + this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted user!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeletePost(): void { + const deletePost: string = this.deleteForm.get('deletePost')?.value; + + if (deletePost !== '' && deletePost !== null) { + const postId: Guid = Guid.parse(deletePost); + + this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted user!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private tryDeleteComment(): void { + const deleteComment: string = this.deleteForm.get('deleteComment')?.value; + + if (deleteComment !== '' && deleteComment !== null) { + const commentId: Guid = Guid.parse(deleteComment); + + this._commentService.deleteCommentWithSessionStorage(commentId).subscribe( + (result: object) => { + this.deletionSuccess('Successfully deleted comment!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + } + + private deletionSuccess(successMsg: string): void { + this.deleteForm.reset(); + this._successBar.showMsg(successMsg); + } +} diff --git a/src/app/components/comment-page/comment-page.component.css b/src/app/components/comment-page/comment-page.component.css new file mode 100644 index 0000000..b886bc1 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.css @@ -0,0 +1,27 @@ +#content { + justify-content: flex-start !important; +} + +#content > * { + width: 100%; +} + +.many-buttons { + width: 100%; + display: flex; +} + +.many-buttons > * { + flex: 1; + margin-right: .3em; +} + +.many-buttons > *:last-of-type { + margin-right: 0; +} + +.submit-btn { + max-width: 98%; + margin: 0 auto; + margin-bottom: .5em; +} diff --git a/src/app/components/comment-page/comment-page.component.html b/src/app/components/comment-page/comment-page.component.html new file mode 100644 index 0000000..2d110d6 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.html @@ -0,0 +1,14 @@ +<app-loading *ngIf="!loaded"></app-loading> + +<div id="content" *ngIf="loaded"> + <button class="submit-btn" type="submit" (click)="toPost()">ᐊ Back to post</button> + <app-comment [paramId]="commentId.toString()"></app-comment> + <div class="many-buttons" *ngIf="editable"> + <button class="submit-btn" (click)="editComment()">Edit comment</button> + <button class="submit-btn delete-btn" (click)="deleteComment()">Delete comment</button> + </div> + <form [formGroup]="editCommentFormGroup" (ngSubmit)="editComment()"> + <input type="text" *ngIf="editingComment" placeholder="New comment message" class="input-field" formControlName="newCommentMessage"> + <input type="submit" style="display: none" /> + </form> +<div> diff --git a/src/app/components/comment-page/comment-page.component.ts b/src/app/components/comment-page/comment-page.component.ts new file mode 100644 index 0000000..bd4cfe5 --- /dev/null +++ b/src/app/components/comment-page/comment-page.component.ts @@ -0,0 +1,91 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Comment } from 'src/models/comment'; + +@Component({ + selector: 'app-comment-page', + templateUrl: './comment-page.component.html', + styleUrls: ['./comment-page.component.css'] +}) +export class CommentPageComponent implements OnInit { + private _title = 'Comment'; + public loaded = false; + public loggedIn = false; + public editable = false; + public editingComment = false; + public commentId: Guid; + public comment: Comment; + public editCommentFormGroup: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _commentService: CommentService){ + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.commentId = Guid.parse(this._router.url.substring(9)); + + // Gets the post and the logged in user and compares them, + // to determine if the current post is made by the user + this._commentService.getCommentRequest(this.commentId).subscribe( + (result: object) => { + this.comment = result as Comment; + if (this.loggedIn) { + this.editable = this.comment.issuerUsername === this._tokenService.getUsernameFromSessionStorageToken(); + } + this.loaded = true; + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this.editCommentFormGroup = this._fb.group({ + newCommentMessage: new FormControl('') + }); + } + + toPost(): void { + this._router.navigate(['/post/' + this.comment.postId]); + } + + editComment(): void { + if (this._tokenService.getTokenFromSessionStorage() === '') { + this._router.navigate(['/login']); + return; + } + + if (this.editingComment) { + const newMessage = this.editCommentFormGroup.get('newCommentMessage')?.value; + if (newMessage !== '') { + console.log(this.commentId); + this._commentService.putCommentWithSessionStorageRequest(this.commentId, this.comment.postId, newMessage).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + } + } + this.editingComment = !this.editingComment; + } + + deleteComment(): void { + this._commentService.deleteCommentWithSessionStorage(this.commentId).subscribe( + (result: object) => { + this.toPost(); + } + ); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/comment/comment.component.css b/src/app/components/comment/comment.component.css new file mode 100644 index 0000000..d82f10e --- /dev/null +++ b/src/app/components/comment/comment.component.css @@ -0,0 +1,59 @@ +.comment { + display: flex; + width: 100%; + + margin: .5em auto; + box-sizing: border-box; + padding: .5em; + background-color: var(--card-bg); +} + +.comment:first-child { + margin-top: 0; +} + +/* Author */ + +.author { + display: flex; + margin-bottom: .2em; +} + +.author:hover { + cursor: pointer; +} + +.author > img { + width: 1.7em; + height: 1.7em; + margin-right: .2em; +} + +.author-info > .name { + font-size: .8em; +} + +.author-info > .handle { + font-size: .6em; + color: gray; +} + +/* Content */ + +.content { + flex: 1; +} + +.message { + margin: .3em 0; + word-break: break-all; +} + +.timestamp { + font-size: .5em; + color: gray; +} + +.message:hover, .timestamp:hover { + cursor: pointer; +} diff --git a/src/app/components/comment/comment.component.html b/src/app/components/comment/comment.component.html new file mode 100644 index 0000000..718e25c --- /dev/null +++ b/src/app/components/comment/comment.component.html @@ -0,0 +1,23 @@ +<app-loading *ngIf="!loaded"></app-loading> + +<div class="comment rounded-border" *ngIf="loaded"> + <div class="content"> + <div class="author" (click)="goToAuthorProfile()"> + <img class="round-image" [src]="user.profilePictureURL"> + <div class="author-info"> + <div class="name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div class="handle"> + @{{ user.userName }} + </div> + </div> + </div> + <div class="message" (click)="goToCommentPage()"> + {{ comment.message }} + </div> + <div class="timestamp" (click)="goToCommentPage()"> + {{ timeCreated }} + </div> + </div> +</div> diff --git a/src/app/components/comment/comment.component.ts b/src/app/components/comment/comment.component.ts new file mode 100644 index 0000000..5076769 --- /dev/null +++ b/src/app/components/comment/comment.component.ts @@ -0,0 +1,54 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { UserService } from 'src/app/services/user.service'; +import { Comment } from 'src/models/comment'; +import { User } from 'src/models/identity/user'; + +@Component({ + selector: 'app-comment', + templateUrl: './comment.component.html', + styleUrls: ['./comment.component.css'] +}) +export class CommentComponent implements OnInit { + public loaded = false; + public user: User; + public comment: Comment; + public timeCreated: string; + @Input() paramId: string; + + constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService) + { } + + ngOnInit(): void { + this.comment = this._commentService.getDefaultComment(); + this.user = this._userService.getDefaultUser(); + + this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe( + (result: object) => { + Object.assign(this.comment, result); + + this.timeCreated = new Date(this.comment.timeCreated).toLocaleString('en-GB'); + this.loadUser(); + } + ); + } + + private loadUser(): void { + this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe( + (result: object) => { + Object.assign(this.user, result); + this.loaded = true; + } + ); + } + + goToAuthorProfile(): void { + this._router.navigate(['/profile/' + this.comment.issuerUsername]); + } + + goToCommentPage(): void { + this._router.navigate(['/comment/' + this.comment.commentId]); + } +} diff --git a/src/app/components/error-bar/error-bar.component.css b/src/app/components/error-bar/error-bar.component.css new file mode 100644 index 0000000..8f8edd9 --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.css @@ -0,0 +1,12 @@ +#error-bar { + box-sizing: border-box; + width: 100%; + background-color: var(--failure); + color: white; + padding: .2em; + text-align: center; +} + +#error-bar:empty { + display: none; +} diff --git a/src/app/components/error-bar/error-bar.component.html b/src/app/components/error-bar/error-bar.component.html new file mode 100644 index 0000000..f1995ab --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.html @@ -0,0 +1 @@ +<div id="error-bar">{{errorMsg}}</div> diff --git a/src/app/components/error-bar/error-bar.component.ts b/src/app/components/error-bar/error-bar.component.ts new file mode 100644 index 0000000..111bac8 --- /dev/null +++ b/src/app/components/error-bar/error-bar.component.ts @@ -0,0 +1,34 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { IApiError } from 'src/interfaces/api-error'; + +@Component({ + selector: 'app-error-bar', + templateUrl: './error-bar.component.html', + styleUrls: ['./error-bar.component.css'] +}) +export class ErrorBarComponent implements OnInit { + public errorMsg = ''; + + constructor() + { } + + ngOnInit(): void { + this.hideError(); + } + + showError(error: HttpErrorResponse): void { + const test: IApiError = { + type: '', + title: 'Error!', + status: 0, + traceId: '' + }; + Object.assign(test, error.error); + this.errorMsg = test.title; + } + + hideError(): void { + this.errorMsg = ''; + } +} diff --git a/src/app/components/feed/feed.component.css b/src/app/components/feed/feed.component.css new file mode 100644 index 0000000..cb155c6 --- /dev/null +++ b/src/app/components/feed/feed.component.css @@ -0,0 +1,179 @@ +#feed-page { + height: 100%; + max-width: 40em; + box-sizing: border-box; + border: .5em solid var(--bg-color); + + margin: 0 auto; + + display: flex; +} + +@media screen and (max-width: 750px) { + #profile-bar { + display: none !important; + } + #top-bar-profile-pic { + display: initial; + } +} +@media screen and (min-width: 750px) { + #profile-bar { + display: initial; + } + #top-bar-profile-pic { + display: none !important; + } +} + +/* Content */ + +#feed-content { + flex: 1; + display: flex; + flex-direction: column; +} + +/* Profile bar */ + +#profile-bar { + width: 20%; + height: fit-content; + display: flex; + flex-direction: column; + align-items: center; + + box-sizing: border-box; + background-color: var(--bg-color); + + margin-right: .5em; + padding-bottom: 1em; + padding: .5em; + border-radius: .6em; +} + +#profile-bar-profile-pic { + width: 7em; + height: 7em; + box-sizing: border-box; + padding: .5em; +} + +#profile-bar-name { + text-align: center; +} + +#profile-bar-username { + margin: .5em 0; +} + +#profile-bar > #profile-info { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Top bar */ + +#top-bar { + display: flex; + flex-direction: column; + width: 98%; + margin: 0 auto; + margin-bottom: .5em; + box-sizing: border-box; +} + +#top-bar-profile-pic { + height: 2.5em; + width: 2.5em; + margin-right: .5em; +} + +#top-bar-profile-pic > img { + height: inherit; + width: inherit; +} + +#top-bar-open-chat { + /* Until implemented */ + display: none; +} + +#main-content { + display: flex; +} + +/* Create post */ + +#create-post-form { + width: 100%; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +#form-inputs { + display: flex; +} + +#top-bar-create-post { + flex: 1; + font-size: inherit; + width: 100%; + height: 100%; + margin: 0 auto; + box-sizing: border-box; + border: 2px solid var(--bg-color); + border-radius: .6em; +} + +#file-upload { + font-size: inherit; + color: transparent; + width: 1.5em; + height: 1.5em; +} + +#file-upload:hover { + cursor: pointer; +} + +#file-upload::-webkit-file-upload-button { + visibility: hidden; +} + +#attachment-img { + height: 1.5em; + width: 1.5em; + position: absolute; + right: .4em; + pointer-events: none; +} + +/* Posts */ + +#no-posts-msg { + width: 100%; + margin-top: 1em; + color: gray; + text-align: center; +} + +/* Elements, that act as buttons */ + +#profile-bar > #profile-info:hover, +#top-bar-profile-pic:hover { + cursor: pointer; +} + +/* Can't copy text from */ + +#profile-bar, +.vote { + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+/Edge */ + user-select: none; /* Standard */ +} diff --git a/src/app/components/feed/feed.component.html b/src/app/components/feed/feed.component.html new file mode 100644 index 0000000..1a03dcc --- /dev/null +++ b/src/app/components/feed/feed.component.html @@ -0,0 +1,52 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="feed-page" *ngIf="dataArrived"> + <nav id="profile-bar" class="round-image rounded-border"> + <div id="profile-info" (click)="goToProfile()"> + <img id="profile-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt=""/> + <div id="profile-bar-name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div id="profile-bar-username"> + @{{ user.userName }} + </div> + </div> + <button class="submit-btn" (click)="goToSettings()">Settings</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <div id="feed-content"> + <nav id="top-bar"> + <div id="main-content"> + <img id="top-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt="" (click)="goToProfile()"> + <form id="create-post-form" class="rounded-border" [formGroup]="createPostFormGroup" (ngSubmit)="createPost()"> + <div id="form-inputs"> + <input id="top-bar-create-post" type="text" formControlName="newPostMessage" placeholder="What's on your mind?"/> + <input type="submit" style="display: none" /> <!-- You need this element, so when you press enter the request is sent --> + <img id="attachment-img" src="assets/images/paper-clip.png"> + <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> + </div> + <div class="form-attachments"> + <div *ngFor="let file of files" class="form-attachment"> + {{ file.name ? file.name : 'Attachment' }} + <div class="remove-form-attachment" (click)="removeAttachment(file.name)"> + ☒ + </div> + </div> + </div> + </form> + <a id="top-bar-open-chat" href=""> + <img src="assets/images/feed/chat-pic.png" alt=""/> + </a> + </div> + </nav> + <div id="posts" class="scroll-standalone" (scroll)="onScroll($event)"> + <div id="no-posts-msg" *ngIf="posts.length === 0"> + None of your friends have posted anything yet!<br> + Try refreshing your page! + </div> + <div *ngFor="let friendPost of posts" class="post"> + <app-post [paramId]="friendPost.postId.toString()"></app-post> + </div> + </div> + </div> +</div> diff --git a/src/app/components/feed/feed.component.ts b/src/app/components/feed/feed.component.ts new file mode 100644 index 0000000..b412b3c --- /dev/null +++ b/src/app/components/feed/feed.component.ts @@ -0,0 +1,122 @@ +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { User } from 'src/models/identity/user'; +import { UserService } from '../../services/user.service'; +import { AppConstants } from 'src/app/app-constants.module'; +import { HttpErrorResponse } from '@angular/common/http'; +import { FeedService } from 'src/app/services/feed.service'; +import { Post } from 'src/models/post'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { PostService } from 'src/app/services/post.service'; +import { TokenService } from 'src/app/services/token.service'; + +@Component({ + selector: 'app-feed', + templateUrl: './feed.component.html', + styleUrls: ['./feed.component.css'] +}) +export class FeedComponent implements OnInit { + private _title = 'Feed'; + private _timeLoaded: string; // we send the time to the api as a string + private _currentPage: number; + public dataArrived = false; + public user: User; + public posts: Post[]; + public createPostFormGroup: FormGroup; + public files: File[]; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _feedService: FeedService, private _postService: PostService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + if (!this._tokenService.getTokenFromSessionStorage()) { + this._router.navigate(['/login']); + return; + } + + this._currentPage = 1; + this.posts = []; + this.user = this._userService.getDefaultUser(); + this.files = []; + + const now = new Date(); + now.setHours(now.getHours() + 2); // accounting for eastern european timezone + this._timeLoaded = now.toISOString(); + + this.createPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (res: object) => { + Object.assign(this.user, res); + this.loadFeed(); + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + + private loadFeed(): void { + this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( + (result: object) => { + this.posts.push(...Object.values(result)[0]); + this.finishUserLoading(); + }, + (err) => { + this.finishUserLoading(); + } + ); + } + + private finishUserLoading(): void { + this.dataArrived = true; + } + + goToProfile(): void { + this._router.navigate(['/profile/' + this.user.userName]); + } + + goToSettings(): void { + this._router.navigate(['/profile/' + this.user.userName + '/settings']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this._router.navigate(['/login']); + } + + onFileUpload(event: any): void { + this.files.push(...event.target.files); + this.createPostFormGroup.get('fileUpload')?.reset(); + } + + removeAttachment(fileName: string): void { + this.files = this.files.filter(x => x.name !== fileName); + } + + createPost(): void { + const postMessage = this.createPostFormGroup.get('newPostMessage')?.value; + this.dataArrived = false; + + this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe( + (result: object) => { + this.goToProfile(); + }, + (err: HttpErrorResponse) => { + this.dataArrived = true; + } + ); + } + + onScroll(event: any): void { + // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073 + if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) { + this.loadFeed(); + } + } +} diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.css b/src/app/components/kaleidoscope/kaleidoscope.component.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.css diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.html b/src/app/components/kaleidoscope/kaleidoscope.component.html new file mode 100644 index 0000000..7392a21 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.html @@ -0,0 +1,8 @@ +<div class="kaleidoscope"> + <ng-container *ngComponentOutlet="component"></ng-container> + <app-login *ngIf="logged!"></app-login> + <app-register *ngIf="!logged!"></app-register> + <!-- <app-feed></app-feed> --> + <script>var logged = false;</script> + <!-- to be fixed tomorow --> +</div>
\ No newline at end of file diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.ts b/src/app/components/kaleidoscope/kaleidoscope.component.ts new file mode 100644 index 0000000..1c5cda1 --- /dev/null +++ b/src/app/components/kaleidoscope/kaleidoscope.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { LoginComponent } from '../login/login.component'; + +@Component({ + selector: 'app-kaleidoscope', + templateUrl: './kaleidoscope.component.html', + styleUrls: ['./kaleidoscope.component.css'] +}) +export class KaleidoscopeComponent implements OnInit { + + public _component: Component; + + constructor(loginComponent: LoginComponent) { + this._component = loginComponent as Component; + } + + ngOnInit(): void { + + } + + assignComponent(component: Component) { + this._component = component; + } +} diff --git a/src/app/components/loading/loading.component.css b/src/app/components/loading/loading.component.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/app/components/loading/loading.component.css diff --git a/src/app/components/loading/loading.component.html b/src/app/components/loading/loading.component.html new file mode 100644 index 0000000..8440f4e --- /dev/null +++ b/src/app/components/loading/loading.component.html @@ -0,0 +1,3 @@ +<div id="content"> + Loading... +</div> diff --git a/src/app/components/loading/loading.component.ts b/src/app/components/loading/loading.component.ts new file mode 100644 index 0000000..e203484 --- /dev/null +++ b/src/app/components/loading/loading.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-loading', + templateUrl: './loading.component.html', + styleUrls: ['./loading.component.css'] +}) +export class LoadingComponent implements OnInit { + + constructor() + { } + + ngOnInit(): void { + } +} diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css new file mode 100644 index 0000000..766522e --- /dev/null +++ b/src/app/components/login/login.component.css @@ -0,0 +1,32 @@ +* { + transition: .2s; +} + +form { + width: 100%; +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +.input-selection:nth-of-type(1) { + margin-top: 1.2em; +} + +.submit-btn { + margin-bottom: .2em; +} + +.redirect-to-register { + color: var(--focus-color); + background-color: var(--bg-color); + border-color: var(--focus-color); +} + +.redirect-to-register:hover { + border-color: black !important; + color: black; +} diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html new file mode 100644 index 0000000..13f9bbf --- /dev/null +++ b/src/app/components/login/login.component.html @@ -0,0 +1,30 @@ +<div id="content"> + <div class="title">Login</div> + + <form [formGroup]="loginUserFormGroup" (ngSubmit)="onSubmit()"> + <hr> + + <div class="input-selection"> + <input type="text" placeholder="Username" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="loginUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" placeholder="Password" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="loginUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + </div> + </div> + + <hr> + <button class="submit-btn" type="submit">Submit</button> + <app-error-bar></app-error-bar> + </form> + <button class="submit-btn redirect-to-register" (click)="onRedirectRegister()">New to DevHive? Register instead</button> +</div> diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..c3fb79c --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { UserService } from 'src/app/services/user.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { TokenService } from 'src/app/services/token.service'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit { + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + private _title = 'Login'; + public loginUserFormGroup: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loginUserFormGroup = this._fb.group({ + username: new FormControl('', [ + Validators.required + ]), + password: new FormControl('', [ + Validators.required + ]) + }); + } + + onSubmit(): void { + this._errorBar.hideError(); + this._userService.loginUserRequest(this.loginUserFormGroup).subscribe( + (res: object) => { + this._tokenService.setUserTokenToSessionStorage(res); + this._router.navigate(['/']); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + + onRedirectRegister(): void { + this._router.navigate(['/register']); + } + + get username(): AbstractControl | null { + return this.loginUserFormGroup.get('username'); + } + + get password(): AbstractControl | null { + return this.loginUserFormGroup.get('password'); + } +} diff --git a/src/app/components/not-found/not-found.component.css b/src/app/components/not-found/not-found.component.css new file mode 100644 index 0000000..ef6231d --- /dev/null +++ b/src/app/components/not-found/not-found.component.css @@ -0,0 +1,23 @@ +#content { + font-size: 1.3em; +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +#back-btns { + width: 100%; + display: flex; +} + +#back-btns > * { + flex: 1; + margin-right: .2em; +} + +#back-btns > *:nth-last-child(1) { + margin-right: 0; +} diff --git a/src/app/components/not-found/not-found.component.html b/src/app/components/not-found/not-found.component.html new file mode 100644 index 0000000..8394810 --- /dev/null +++ b/src/app/components/not-found/not-found.component.html @@ -0,0 +1,10 @@ +<div id="content"> + <div class="title"> + Page not found! + </div> + <hr> + <div id="back-btns"> + <button class="submit-btn" type="submit" (click)="backToFeed()">Back to feed</button> + <button class="submit-btn" type="submit" (click)="backToLogin()">Back to login</button> + </div> +</div> diff --git a/src/app/components/not-found/not-found.component.ts b/src/app/components/not-found/not-found.component.ts new file mode 100644 index 0000000..b1f8cc1 --- /dev/null +++ b/src/app/components/not-found/not-found.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-not-found', + templateUrl: './not-found.component.html', + styleUrls: ['./not-found.component.css'] +}) +export class NotFoundComponent implements OnInit { + private _title = 'Not Found!'; + + constructor(private _titleService: Title, private _router: Router) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + backToLogin(): void { + this._router.navigate(['/login']); + } +} diff --git a/src/app/components/post-attachment/post-attachment.component.css b/src/app/components/post-attachment/post-attachment.component.css new file mode 100644 index 0000000..572cc99 --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.css @@ -0,0 +1,75 @@ +/* Attachment */ + +.attachment { + border: 2px solid black; + border-top: 0; + border-radius: 0 0 .6em .6em; + padding: .4em; + padding-top: 1em; + margin-top: calc(-0.8em - 2px); +} + +.clickable { + width: 100%; + height: 100%; + display: flex; +} + +.clickable:hover { + cursor: pointer; +} + +.attachment-name { + flex: 1; +} + +.attachment-type { + margin-left: .2em; +} + +/* Full attachment */ + +.show-full-attachment { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: #000000AF; + display: flex; + justify-content: center; + align-items: center; + z-index: 2; +} + +.show-full-attachment > * { + max-height: 100vh; + max-width: 100vw; +} + +.attachment-download { + max-width: 420px !important; + margin: 0 .4em; + text-decoration: none; + color: white; + border-color: white; + background-color: black; +} + +.attachment-download:hover { + background-color: white; + color: var(--focus-color); +} + +.close { + position: fixed; + top: .2em; + right: .2em; + font-size: 2em; + color: whitesmoke; +} + +.close:hover { + color: var(--failure); + cursor: pointer; +} diff --git a/src/app/components/post-attachment/post-attachment.component.html b/src/app/components/post-attachment/post-attachment.component.html new file mode 100644 index 0000000..4d381d1 --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.html @@ -0,0 +1,18 @@ +<div class="attachment"> + <div class="clickable" (click)="toggleShowFull()"> + <div class="attachment-name"> + {{ fileName }} + </div> + <div class="attachment-type"> + {{ fileType }} + </div> + </div> +</div> + +<div class="show-full-attachment" *ngIf="showFull" (click)="toggleShowFull()"> + <img class="attachment-img" *ngIf="isImage" src="{{paramURL}}"> + <a class="attachment-download submit-btn" *ngIf="!isImage" href="{{paramURL}}">Download attachment</a> + <div class="close"> + ☒ + </div> +</div> diff --git a/src/app/components/post-attachment/post-attachment.component.ts b/src/app/components/post-attachment/post-attachment.component.ts new file mode 100644 index 0000000..1d00def --- /dev/null +++ b/src/app/components/post-attachment/post-attachment.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-post-attachment', + templateUrl: './post-attachment.component.html', + styleUrls: ['./post-attachment.component.css'] +}) +export class PostAttachmentComponent implements OnInit { + @Input() paramURL: string; + public isImage = false; + public showFull = false; + public fileName: string; + public fileType: string; + + constructor() + { } + + ngOnInit(): void { + this.isImage = this.paramURL.includes('image') && !this.paramURL.endsWith('pdf'); + this.fileType = this.isImage ? 'img' : 'raw'; + this.fileName = this.paramURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment'; + } + + toggleShowFull(): void { + this.showFull = !this.showFull; + } +} diff --git a/src/app/components/post-page/post-page.component.css b/src/app/components/post-page/post-page.component.css new file mode 100644 index 0000000..3eec851 --- /dev/null +++ b/src/app/components/post-page/post-page.component.css @@ -0,0 +1,62 @@ +#content { + justify-content: flex-start !important; +} + +#content > * { + width: 100%; +} + +.many-buttons { + width: 100%; + display: flex; +} + +.many-buttons > * { + flex: 1; + margin-right: .3em; +} + +.many-buttons > *:last-of-type { + margin-right: 0; +} + +#editPost { + display: flex; + position: relative; +} + +#new-message-input { + flex: 1; + box-sizing: border-box; +} + +#file-upload { + font-size: inherit; + color: transparent; + width: 1.99em; + height: 1.99em; + margin-left: .3em; +} + +#file-upload:hover { + cursor: pointer; +} + +#file-upload::-webkit-file-upload-button { + visibility: hidden; +} + +#attachment-img { + height: 1.99em; + width: 1.99em; + position: absolute; + right: 0; + pointer-events: none; +} + + +.submit-btn { + max-width: 98%; + margin: 0 auto; + margin-bottom: .5em; +} diff --git a/src/app/components/post-page/post-page.component.html b/src/app/components/post-page/post-page.component.html new file mode 100644 index 0000000..8665865 --- /dev/null +++ b/src/app/components/post-page/post-page.component.html @@ -0,0 +1,37 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <div class="many-buttons" *ngIf="loggedIn"> + <button class="submit-btn" type="submit" (click)="backToFeed()">ᐊ Back to feed</button> + <button class="submit-btn" type="submit" (click)="backToProfile()">ᐊ Back to profile</button> + </div> + <button class="submit-btn" type="submit" (click)="toLogin()" *ngIf="!loggedIn">Login</button> + <app-post [paramId]="postId.toString()"></app-post> + <div class="many-buttons" *ngIf="editable"> + <button class="submit-btn" (click)="editPost()">Edit post</button> + <button class="submit-btn delete-btn" (click)="deletePost()">Delete post</button> + </div> + <form id="editPost" [formGroup]="editPostFormGroup" *ngIf="editingPost" (ngSubmit)="editPost()"> + <input id="new-message-input" type="text" placeholder="New post message" class="input-field" formControlName="newPostMessage"> + <img id="attachment-img" src="assets/images/paper-clip.png"> + <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple> + <input type="submit" style="display: none" /> + </form> + <div class="form-attachments" *ngIf="editingPost"> + <div *ngFor="let file of files" class="form-attachment"> + {{ file.name ? file.name : 'Attachment' }} + <div class="remove-form-attachment" (click)="removeAttachment(file.name)"> + ☒ + </div> + </div> + </div> + <form [formGroup]="addCommentFormGroup" (ngSubmit)="addComment()"> + <input type="text" placeholder="Add comment" class="input-field" formControlName="newComment"> + <input type="submit" style="display: none" /> + </form> + <div> + <div class="comment" *ngFor="let comm of post?.comments"> + <app-comment [paramId]="comm.id.toString()"></app-comment> + </div> + </div> +<div> diff --git a/src/app/components/post-page/post-page.component.ts b/src/app/components/post-page/post-page.component.ts new file mode 100644 index 0000000..413ff80 --- /dev/null +++ b/src/app/components/post-page/post-page.component.ts @@ -0,0 +1,162 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { CommentService } from 'src/app/services/comment.service'; +import { PostService } from 'src/app/services/post.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Post } from 'src/models/post'; +import { CloudinaryService } from 'src/app/services/cloudinary.service'; + +@Component({ + selector: 'app-post-page', + templateUrl: './post-page.component.html', + styleUrls: ['./post-page.component.css'] +}) +export class PostPageComponent implements OnInit { + private _title = 'Post'; + public dataArrived = false; + public loggedIn = false; + public editable = false; + public editingPost = false; + public postId: Guid; + public post: Post; + public files: File[]; + public editPostFormGroup: FormGroup; + public addCommentFormGroup: FormGroup; + + constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService, private _cloudinaryService: CloudinaryService){ + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== ''; + this.postId = Guid.parse(this._router.url.substring(6)); + this.files = []; + + // Gets the post and the logged in user and compares them, + // to determine if the current post is made by the user + this._postService.getPostRequest(this.postId).subscribe( + (result: object) => { + this.post = result as Post; + this.post.fileURLs = Object.values(result)[7]; + if (this.loggedIn) { + this.editable = this.post.creatorUsername === this._tokenService.getUsernameFromSessionStorageToken(); + } + if (this.post.fileURLs.length > 0) { + this.loadFiles(); + } + else { + this.dataArrived = true; + } + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this.editPostFormGroup = this._fb.group({ + newPostMessage: new FormControl(''), + fileUpload: new FormControl('') + }); + + this.addCommentFormGroup = this._fb.group({ + newComment: new FormControl('') + }); + } + + private loadFiles(): void { + for (const fileURL of this.post.fileURLs) { + this._cloudinaryService.getFileRequest(fileURL).subscribe( + (result: object) => { + const file = result as File; + const tmp = { + name: fileURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment' + }; + + Object.assign(file, tmp); + this.files.push(file); + + if (this.files.length === this.post.fileURLs.length) { + this.dataArrived = true; + } + } + ); + } + } + + backToFeed(): void { + this._router.navigate(['/']); + } + + backToProfile(): void { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + + toLogin(): void { + this._router.navigate(['/login']); + } + + onFileUpload(event: any): void { + this.files.push(...event.target.files); + this.editPostFormGroup.get('fileUpload')?.reset(); + } + + removeAttachment(fileName: string): void { + this.files = this.files.filter(x => x.name !== fileName); + } + + editPost(): void { + if (this._tokenService.getTokenFromSessionStorage() === '') { + this.toLogin(); + return; + } + + if (this.editingPost) { + let newMessage = this.editPostFormGroup.get('newPostMessage')?.value; + if (newMessage === '') { + newMessage = this.post.message; + } + this._postService.putPostWithSessionStorageRequest(this.postId, newMessage, this.files).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + this.dataArrived = false; + } + this.editingPost = !this.editingPost; + } + + addComment(): void { + if (!this.loggedIn) { + this._router.navigate(['/login']); + return; + } + + const newComment = this.addCommentFormGroup.get('newComment')?.value; + if (newComment !== '' && newComment !== null) { + this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe( + (result: object) => { + this.editPostFormGroup.reset(); + this.reloadPage(); + } + ); + } + } + + deletePost(): void { + this._postService.deletePostWithSessionStorage(this.postId).subscribe( + (result: object) => { + this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]); + } + ); + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css new file mode 100644 index 0000000..1b88c7d --- /dev/null +++ b/src/app/components/post/post.component.css @@ -0,0 +1,134 @@ +.post { + display: flex; + width: 98%; + + margin: .5em auto; + box-sizing: border-box; + padding: .5em; + background-color: var(--card-bg); + position: relative; +} + +.post:first-child { + margin-top: 0; +} + +hr { + border: 1px solid black; + width: 90%; +} + +/* Author */ + +.author { + display: flex; + margin-bottom: .2em; +} + +.author:hover { + cursor: pointer; +} + +.author > img { + width: 2.2em; + height: 2.2em; + margin-right: .2em; +} + +.author-info > .handle { + font-size: .9em; + color: gray; +} + +/* Content */ + +.content { + flex: 1; +} + +.message { + margin: .3em 0; + word-break: break-all; +} + +.bottom-post { + font-size: .5em; + color: gray; + display: flex; + align-items: center; +} + +.separator { + margin: 0 .5em; +} + +.comment-count { + font-size: 1em; +} + +.comment-count > img { + height: .8em; +} + +.message:hover, .timestamp:hover { + cursor: pointer; +} + +/* Rating */ + +/* Temporary, until ratings are implemented fully */ +.rating { + display: none !important; +} + +.rating { + display: flex; + flex-direction: column; + align-items: center; + min-height: 4.4em; + margin: auto -.1em auto 0; +} + +.score { + flex: 1; + display: flex; + align-items: center; +} + + +.vote { + display: flex; + align-items: center; + flex: 1; + + background: var(--card-bg); + font-size: 1em; + + border: 1px solid var(--card-bg); + box-sizing: border-box; + border-radius: .2em; + + } + +.vote:hover { + border: 1px solid var(--focus-color); + color: var(--focus-color); + cursor: pointer; +} + +/* Attachments */ + +.attachments { + display: flex; + width: 98%; + margin: -.3em auto .5em auto; + flex-wrap: wrap; +} + +.attachments:empty { + display: none; +} + +.attachments > * { + flex: 1; +} diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html new file mode 100644 index 0000000..bc0d84a --- /dev/null +++ b/src/app/components/post/post.component.html @@ -0,0 +1,49 @@ +<app-loading *ngIf="!loaded"></app-loading> + +<div class="post rounded-border" *ngIf="loaded"> + <div class="content"> + <div class="author" (click)="goToAuthorProfile()"> + <img class="round-image" [src]="user.profilePictureURL"> + <div class="author-info"> + <div class="name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div class="handle"> + @{{ user.userName }} + </div> + </div> + </div> + <div class="message" (click)="goToPostPage()"> + {{ post.message }} + </div> + <div class="bottom-post" (click)="goToPostPage()"> + <div class="timestamp"> + {{ timeCreated }} + </div> + <div class="separator"> + ║ + </div> + <div class="comment-count"> + {{ post.comments.length }} + <img src="assets/images/comment.png"> + </div> + </div> + + </div> + <div class="rating"> + <button class="vote"> + ᐃ + </button> + <div class="score"> + {{ votesNumber }} + </div> + <button class="vote"> + ᐁ + </button> + </div> +</div> +<div class="attachments"> + <div *ngFor="let fileURL of post.fileURLs"> + <app-post-attachment class="no-events" [paramURL]="fileURL"></app-post-attachment> + </div> +</div> diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts new file mode 100644 index 0000000..387f56f --- /dev/null +++ b/src/app/components/post/post.component.ts @@ -0,0 +1,57 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Guid } from 'guid-typescript'; +import { PostService } from 'src/app/services/post.service'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { Post } from 'src/models/post'; + +@Component({ + selector: 'app-post', + templateUrl: './post.component.html', + styleUrls: ['./post.component.css'], +}) +export class PostComponent implements OnInit { + public loaded = false; + public user: User; + public post: Post; + public votesNumber: number; + public timeCreated: string; + @Input() paramId: string; + + constructor(private _postService: PostService, private _userService: UserService, private _router: Router) + { } + + ngOnInit(): void { + this.post = this._postService.getDefaultPost(); + this.user = this._userService.getDefaultUser(); + + this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe( + (result: object) => { + Object.assign(this.post, result); + this.post.fileURLs = Object.values(result)[7]; + this.votesNumber = 23; + + this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB'); + this.loadUser(); + } + ); + } + + private loadUser(): void { + this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe( + (result: object) => { + Object.assign(this.user, result); + this.loaded = true; + } + ); + } + + goToAuthorProfile(): void { + this._router.navigate(['/profile/' + this.user.userName]); + } + + goToPostPage(): void { + this._router.navigate(['/post/' + this.post.postId]); + } +} diff --git a/src/app/components/profile-settings/profile-settings.component.css b/src/app/components/profile-settings/profile-settings.component.css new file mode 100644 index 0000000..1c07d9f --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.css @@ -0,0 +1,124 @@ +* { + box-sizing: border-box; +} + +#content { + max-width: 22em; + justify-content: start; +} + +form { + width: 100%; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +/* Navigation bar (for loggedin user) */ + +#navigation { + display: flex; + width: 100%; +} + +#navigation > .submit-btn { + flex: 1; + margin-top: 0; + margin-left: .5em; + font-size: inherit; +} + +#navigation > .submit-btn:nth-of-type(1) { + margin-left: 0; +} + +/* Form */ + +#update-profile-picture { + display: flex; + align-items: center; + justify-content: center; + padding: 0 .5em; + flex-wrap: wrap; +} + +#profile-picture { + width: 5em; + height: 5em; +} + +#submit-file { + display: flex; + flex-direction: column; + align-items: center; + padding: 1em; +} + +#upload-file:hover { + cursor: inherit; +} + +#upload-file > input:hover { + cursor: pointer; +} + +#upload-file > input::-webkit-file-upload-button { + visibility: hidden; +} + +#update-user { + margin-top: 1.1em; +} + +.input-field { + border-color: var(--focus-color) !important; + caret-color: var(--focus-color); + border-width: 2px !important; + margin-top: -1px !important; +} + +.input-field-label { + font-size: .7em; + color: var(--focus-color); + transform: translate(0, -1.2em); +} + +#all-languages, #all-technologies { + display: flex; + flex-wrap: wrap; +} + +/* Buttons */ + +.edit-btn { + border-radius: 0 !important; + color: var(--focus-color); + background-color: white; + border-color: var(--focus-color); +} + +.edit-btn:hover { + color: white; + background-color: black; + border-color: black !important; +} + +.submit-btn { + margin-bottom: .5em; +} + +#update-profile-btn { + margin-top: 1em; +} + +#confirm-delete { + box-sizing: border-box; + width: 100%; + background-color: var(--failure); + color: white; + padding: .2em; + text-align: center; +} diff --git a/src/app/components/profile-settings/profile-settings.component.html b/src/app/components/profile-settings/profile-settings.component.html new file mode 100644 index 0000000..502697d --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.html @@ -0,0 +1,116 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="goToProfile()">ᐊ Back</button> + <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isAdminUser">Panel</button> + <button class="submit-btn" (click)="logout()">Logout</button> + </nav> + <hr> + <div class="scroll-standalone"> + <form id="update-profile-picture" [formGroup]="updateProfilePictureFormGroup" (ngSubmit)="updateProfilePicture()"> + <img id="profile-picture" class="round-image" [src]="user.profilePictureURL"> + <div id="submit-file"> + <div id="upload-file" class="submit-btn"> + <input type="file" accept="image/*" formControlName="fileUpload" (change)="onFileUpload($event)"> + </div> + <button class="submit-btn" type="submit">Update profile picture</button> + </div> + </form> + <hr> + <form id="update-user" [formGroup]="updateUserFormGroup" (ngSubmit)="onSubmit()"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="firstName" required> + <label class="input-field-label">First Name</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="lastName" required> + <label class="input-field-label">Last Name</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" class="input-field" formControlName="email" required> + <label class="input-field-label">Email</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('email')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label> + <label *ngIf="updateUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label> + </div> + </div> + <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button> + <div *ngIf="showLanguages"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="languageInput" required> + + <div class="input-errors"> + <label class="error">Type in your desired languages, separated by a space</label> + </div> + </div> + Available languages: + <div id="all-languages"> + <div class="user-language" *ngFor="let lang of availableLanguages"> + {{ lang.name }} + </div> + </div> + </div> + + <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button> + <div *ngIf="showTechnologies"> + <div class="input-selection"> + <input type="text" class="input-field" formControlName="technologyInput" required> + + <div class="input-errors"> + <label class="error">Type in your desired technologies, separated by a space</label> + </div> + </div> + Available technologies: + <div id="all-technologies"> + <div class="user-technology" *ngFor="let tech of availableTechnologies"> + {{ tech.name }} + </div> + </div> + </div> + + <button id="update-profile-btn" class="submit-btn" type="submit">Update profile</button> + <app-success-bar></app-success-bar> + <app-error-bar></app-error-bar> + </form> + <hr> + <div id="confirm-delete" *ngIf="deleteAccountConfirm"> + Are you sure you want to delete your account?<br>This is permanent! + </div> + <button class="submit-btn delete-btn" (click)="deleteAccount()">Delete account</button> + </div> +</div> diff --git a/src/app/components/profile-settings/profile-settings.component.ts b/src/app/components/profile-settings/profile-settings.component.ts new file mode 100644 index 0000000..a484665 --- /dev/null +++ b/src/app/components/profile-settings/profile-settings.component.ts @@ -0,0 +1,307 @@ +import { Location } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { LanguageService } from 'src/app/services/language.service'; +import { UserService } from 'src/app/services/user.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { User } from 'src/models/identity/user'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; +import { SuccessBarComponent } from '../success-bar/success-bar.component'; +import { Language } from 'src/models/language'; +import { Technology } from 'src/models/technology'; +import { TokenService } from 'src/app/services/token.service'; +import { Title } from '@angular/platform-browser'; +import { AppConstants } from 'src/app/app-constants.module'; + +@Component({ + selector: 'app-profile-settings', + templateUrl: './profile-settings.component.html', + styleUrls: ['./profile-settings.component.css'] +}) +export class ProfileSettingsComponent implements OnInit { + private _title = 'Profile Settings'; + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent; + private _urlUsername: string; + public isAdminUser = false; + public dataArrived = false; + public deleteAccountConfirm = false; + public showLanguages = false; + public showTechnologies = false; + public updateUserFormGroup: FormGroup; + public updateProfilePictureFormGroup: FormGroup; + public newProfilePicture: File; + public user: User; + public availableLanguages: Language[]; + public availableTechnologies: Technology[]; + + constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _fb: FormBuilder, private _location: Location) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this._urlUsername = this._router.url.substring(9); + this._urlUsername = this._urlUsername.substring(0, this._urlUsername.length - 9); + + this.user = this._userService.getDefaultUser(); + this.availableLanguages = []; + this.availableTechnologies = []; + this.newProfilePicture = new File([], ''); + + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe( + (res: object) => { + Object.assign(this.user, res); + this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME); + this.finishUserLoading(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + + this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableLanguages = result as Language[]; + } + ); + this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe( + (result: object) => { + this.availableTechnologies = result as Technology[]; + } + ); + } + + private finishUserLoading(): void { + if (sessionStorage.getItem('UserCred')) { + const userFromToken: User = this._userService.getDefaultUser(); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (tokenRes: object) => { + Object.assign(userFromToken, tokenRes); + + if (userFromToken.userName === this._urlUsername) { + this.initForms(); + this.dataArrived = true; + } + else { + this.goToProfile(); + } + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + else { + this.goToProfile(); + } + } + + private initForms(): void { + this.updateUserFormGroup = this._fb.group({ + firstName: new FormControl(this.user.firstName, [ + Validators.required, + Validators.minLength(3) + ]), + lastName: new FormControl(this.user.lastName, [ + Validators.required, + Validators.minLength(3) + ]), + username: new FormControl(this.user.userName, [ + Validators.required, + Validators.minLength(3) + ]), + email: new FormControl(this.user.email, [ + Validators.required, + Validators.email, + ]), + password: new FormControl('', [ + Validators.required, + Validators.minLength(3), + Validators.pattern('.*[0-9].*') // Check if password contains atleast one number + ]), + + // For language we have two different controls, + // the first one is used for input, the other one for sending data + // because if we edit the control for input, + // we're also gonna change the input field in the HTML + languageInput: new FormControl(''), // The one for input + languages: new FormControl(''), // The one that is sent + + // For technologies it's the same as it is with languages + technologyInput: new FormControl(''), + technologies: new FormControl('') + }); + + this.getLanguagesForShowing().then(value => { + this.updateUserFormGroup.patchValue({ languageInput : value }); + }); + + this.getTechnologiesForShowing().then(value => { + this.updateUserFormGroup.patchValue({ technologyInput : value }); + }); + + this.updateProfilePictureFormGroup = this._fb.group({ + fileUpload: new FormControl('') + }); + + this.updateUserFormGroup.valueChanges.subscribe(() => { + this._successBar?.hideMsg(); + this._errorBar?.hideError(); + }); + } + + private getLanguagesForShowing(): Promise<string> { + return new Promise(resolve => { + this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => { + this.user.languages = value; + resolve(value.map(x => x.name).join(' ')); + }); + }); + } + + private getTechnologiesForShowing(): Promise<string> { + return new Promise(resolve => { + this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => { + this.user.technologies = value; + resolve(value.map(x => x.name).join(' ')); + }); + }); + } + + onFileUpload(event: any): void { + this.newProfilePicture = event.target.files[0]; + } + + updateProfilePicture(): void { + if (this.newProfilePicture.size === 0) { + return; + } + + this._userService.putProfilePictureFromSessionStorageRequest(this.newProfilePicture).subscribe( + (result: object) => { + this.reloadPage(); + } + ); + this.dataArrived = false; + } + + onSubmit(): void { + this._successBar.hideMsg(); + this._errorBar.hideError(); + + this.patchLanguagesControl(); + this.patchTechnologiesControl(); + + this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.user.roles, this.user.friends).subscribe( + (result: object) => { + this._successBar.showMsg('Profile updated successfully!'); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + + private patchLanguagesControl(): void { + // Get user input + const langControl = this.updateUserFormGroup.get('languageInput')?.value as string ?? ''; + + if (langControl === '') { + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + languages : [] + }); + } + else { + const names = langControl.split(' '); + + // Transfer user input to objects of type { "name": "value" } + const actualLanguages = []; + for (const lName of names) { + if (lName !== '') { + actualLanguages.push({ name : lName }); + } + } + + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + languages : actualLanguages + }); + } + } + + private patchTechnologiesControl(): void { + // Get user input + const techControl = this.updateUserFormGroup.get('technologyInput')?.value as string ?? ''; + + if (techControl === '') { + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + technologies : [] + }); + } + else { + const names = techControl.split(' '); + + // Transfer user input to objects of type { "name": "value" } + const actualTechnologies = []; + for (const tName of names) { + if (tName !== '') { + actualTechnologies.push({ name : tName }); + } + } + + // Add the data to the form (to the value that is going to be sent) + this.updateUserFormGroup.patchValue({ + technologies : actualTechnologies + }); + } + } + + goToProfile(): void { + this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]); + } + + navigateToAdminPanel(): void { + this._router.navigate(['/admin-panel']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + this.goToProfile(); + } + + toggleLanguages(): void { + this.showLanguages = !this.showLanguages; + } + + toggleTechnologies(): void { + this.showTechnologies = !this.showTechnologies; + } + + deleteAccount(): void { + if (this.deleteAccountConfirm) { + this._userService.deleteUserFromSessionStorageRequest().subscribe( + (res: object) => { + this.logout(); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + this.dataArrived = false; + } + else { + this.deleteAccountConfirm = true; + } + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/profile/profile.component.css b/src/app/components/profile/profile.component.css new file mode 100644 index 0000000..ebcd406 --- /dev/null +++ b/src/app/components/profile/profile.component.css @@ -0,0 +1,105 @@ +* { + box-sizing: border-box; +} + +#content { + max-width: 22em; + justify-content: start; +} + +hr { + width: calc(100% - 1em); + color: black; + border: 1px solid black; +} + +form { + width: 100%; +} + +/* Navigation bar (for loggedin user) */ + +#navigation { + display: flex; + width: 100%; +} + +.submit-btn { + flex: 1; + margin-top: 0; + margin-left: .5em; + font-size: inherit; +} + +#navigation > .submit-btn:first-child { + margin-left: 0; +} + +/* Top card */ + +#main-info { + display: flex; + width: 100%; + margin-bottom: .25em +} + +#main-info > img { + width: 5em; + height: 5em; +} + +#other-main-info { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +#other-main-info > * { + font-size: 1.4em; +} + +#other-main-info *:nth-child(1) { + margin-top: auto; +} + +#other-main-info *:nth-last-child(1) { + margin-bottom: auto; +} + +#add-friend, #loggedin-password { + flex: 0 !important; + margin-top: .4em; + max-width: 8em; + font-size: .6em !important; +} + +#loggedin-password { + max-width: 100%; +} + +/* Languages and technologies */ + +.secondary-info { + margin-top: .25em; + margin-bottom: .25em; + width: 100%; + display: flex; + align-items: center; + flex-wrap: wrap; +} + +/* Posts */ + +#no-posts { + width: 100%; + text-align: center; + color: gray; + margin-top: .2em; +} + +#posts { + width: 100%; + height: 100%; +} diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html new file mode 100644 index 0000000..0e5f633 --- /dev/null +++ b/src/app/components/profile/profile.component.html @@ -0,0 +1,60 @@ +<app-loading *ngIf="!dataArrived"></app-loading> + +<div id="content" *ngIf="dataArrived"> + <nav id="navigation"> + <button class="submit-btn" (click)="goBack()">ᐊ Back</button> + <button class="submit-btn" (click)="navigateToSettings()" *ngIf="isTheLoggedInUser">Settings</button> + <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isTheLoggedInUser && isAdminUser">Panel</button> + <button class="submit-btn" (click)="logout()" *ngIf="isTheLoggedInUser">Logout</button> + </nav> + <hr> + <div class="scroll-standalone" (scroll)="onScroll($event)"> + <div id="main-info" class="rounded-border"> + <img class="round-image" [src]="user.profilePictureURL" alt=""/> + <div id="other-main-info"> + <div id="name"> + {{ user.firstName }} {{ user.lastName }} + </div> + <div id="username"> + @{{ user.userName }} + </div> + <form [formGroup]="updateFrienship" (ngSubmit)="modifyFriend()" *ngIf="!isTheLoggedInUser && isUserLoggedIn"> + <button id="add-friend" type="submit" class="submit-btn">{{ friendOfUser ? 'Unfriend' : 'Add friend' }}</button> + <br> + <input id="loggedin-password" type="password" formControlName="password" class="input-field" *ngIf="updatingFriendship" placeholder="Type in password to confirm"> + </form> + </div> + </div> + <div class="secondary-info rounded-border"> + Languages: + <div *ngFor="let lang of user.languages"> + <div class="user-language"> + {{ lang.name }} + </div> + </div> + <div *ngIf="user.languages.length === 0"> + None + </div> + </div> + <div class="secondary-info rounded-border"> + Technologies: + <div *ngFor="let tech of user.technologies"> + <div class="user-language"> + {{ tech.name }} + </div> + </div> + <div *ngIf="user.technologies.length === 0"> + None + </div> + </div> + <hr> + <div id="posts"> + <div id="no-posts" *ngIf="userPosts.length === 0"> + {{ user.firstName }} {{ user.lastName }} hasn't posted anything yet! + </div> + <div *ngFor="let userPost of userPosts"> + <app-post [paramId]="userPost.postId.toString()"></app-post> + </div> + </div> + </div> +</div> diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts new file mode 100644 index 0000000..bbf8585 --- /dev/null +++ b/src/app/components/profile/profile.component.ts @@ -0,0 +1,203 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/models/identity/user'; +import { AppConstants } from 'src/app/app-constants.module'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Location } from '@angular/common'; +import { LanguageService } from 'src/app/services/language.service'; +import { TechnologyService } from 'src/app/services/technology.service'; +import { Post } from 'src/models/post'; +import { FeedService } from 'src/app/services/feed.service'; +import { TokenService } from 'src/app/services/token.service'; +import { Title } from '@angular/platform-browser'; +import { Friend } from 'src/models/identity/friend'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.css'] +}) +export class ProfileComponent implements OnInit { + private _title = 'Profile'; + private _urlUsername: string; + private _timeLoaded: string; + private _currentPage: number; + public isTheLoggedInUser = false; + public isUserLoggedIn = false; + public isAdminUser = false; + public dataArrived = false; + public friendOfUser = false; + public updatingFriendship = false; + public user: User; + public userPosts: Post[]; + public updateFrienship: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _feedService: FeedService, private _location: Location, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + private setDefaultUser(): void { + this.user = this._userService.getDefaultUser(); + } + + ngOnInit(): void { + this._urlUsername = this._router.url.substring(9); + + const now = new Date(); + now.setHours(now.getHours() + 2); // accounting for eastern europe timezone + this._timeLoaded = now.toISOString(); + this._currentPage = 1; + + this.user = this._userService.getDefaultUser(); + this.userPosts = []; + + this.updateFrienship = this._fb.group({ + password: new FormControl('') + }); + + this._userService.getUserByUsernameRequest(this._urlUsername).subscribe( + (res: object) => { + Object.assign(this.user, res); + this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME); + this.loadLanguages(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/not-found']); + } + ); + } + + private loadLanguages(): void { + if (this.user.languages.length > 0) { + // When user has languages, get their names and load technologies + this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => { + this.user.languages = value; + this.loadTechnologies(); + }); + } + else { + this.loadTechnologies(); + } + } + + private loadTechnologies(): void { + if (this.user.technologies.length > 0) { + // When user has technologies, get their names and then load posts + this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => { + this.user.technologies = value; + this.loadPosts(); + }); + } + else { + this.loadPosts(); + } + } + + private loadPosts(): void { + this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe( + (result: object) => { + const resultArr: Post[] = Object.values(result)[0]; + this.userPosts.push(...resultArr); + this.finishUserLoading(); + }, + (err: HttpErrorResponse) => { + this._currentPage = -1; + this.finishUserLoading(); + } + ); + } + + private finishUserLoading(): void { + if (sessionStorage.getItem('UserCred')) { + this.isUserLoggedIn = true; + const userFromToken: User = this._userService.getDefaultUser(); + + this._userService.getUserFromSessionStorageRequest().subscribe( + (tokenRes: object) => { + Object.assign(userFromToken, tokenRes); + + if (userFromToken.friends.map(x => x.userName).includes(this._urlUsername)) { + this.friendOfUser = true; + } + if (userFromToken.userName === this._urlUsername) { + this.isTheLoggedInUser = true; + } + this.dataArrived = true; + }, + (err: HttpErrorResponse) => { + this.logout(); + } + ); + } + else { + this.dataArrived = true; + } + } + + goBack(): void { + this._router.navigate(['/']); + } + + navigateToAdminPanel(): void { + this._router.navigate(['/admin-panel']); + } + + navigateToSettings(): void { + this._router.navigate([this._router.url + '/settings']); + } + + logout(): void { + this._tokenService.logoutUserFromSessionStorage(); + + // Reload the page + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } + + modifyFriend(): void { + if (this.updatingFriendship) { + this.dataArrived = false; + + this._userService.getUserFromSessionStorageRequest().subscribe( + (result: object) => { + const loggedInUser: User = result as User; + + if (this.friendOfUser) { + loggedInUser.friends = loggedInUser.friends.filter(x => x.userName !== this.user.userName); + } + else { + const newFriend = new Friend(); + newFriend.userName = this.user.userName; + loggedInUser.friends.push(newFriend); + } + + this._userService.putBareUserFromSessionStorageRequest(loggedInUser, this.updateFrienship.get('password')?.value).subscribe( + (resultUpdate: object) => { + this.reloadPage(); + }, + (err: HttpErrorResponse) => { + this._router.navigate(['/']); + } + ); + } + ); + } + this.updatingFriendship = !this.updatingFriendship; + } + + onScroll(event: any): void { + // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073 + if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) { + this.loadPosts(); + } + } + + private reloadPage(): void { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate([this._router.url]); + } +} diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css new file mode 100644 index 0000000..93d8006 --- /dev/null +++ b/src/app/components/register/register.component.css @@ -0,0 +1,40 @@ +/* A lot of stuff are moved to the global styles! */ + +* { + transition: 0.2s; +} + +form { + width: 100%; +} + +@media screen and (max-height: 630px) { + #content { + height: fit-content !important; + } +} + +#content hr { + width: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +.input-selection:nth-of-type(1) { + margin-top: 1.2em; +} + +.submit-btn { + margin-bottom: .2em; +} + +.redirect-to-login { + color: var(--focus-color); + background-color: var(--bg-color); + border-color: var(--focus-color); +} + +.redirect-to-login:hover { + border-color: black !important; + color: black; +} diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html new file mode 100644 index 0000000..4e67e0e --- /dev/null +++ b/src/app/components/register/register.component.html @@ -0,0 +1,65 @@ +<div id="content"> + <div class="title">Register</div> + + <form [formGroup]="registerUserFormGroup" (ngSubmit)="onSubmit()"> + <hr> + <!-- Value: {{ registerUserFormGroup.value | json }} + <hr> --> + + <div class="input-selection"> + <input type="text" placeholder="Goshko, is that u?" class="input-field" formControlName="firstName" required> + <label class="input-field-label">First Name</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="Trapov? Really??" class="input-field" formControlName="lastName" required> + <label class="input-field-label">Last Name</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="Think of something cool to flex on other kids" class="input-field" formControlName="username" required> + <label class="input-field-label">Username</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('username')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label> + </div> + </div> + + <div class="input-selection"> + <input type="text" placeholder="You expect an email joke? I have none, mail me one" class="input-field" formControlName="email" required> + <label class="input-field-label">Email</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('email')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label> + </div> + </div> + + <div class="input-selection"> + <input type="password" placeholder="Make sure it's long & strong (just like my d***)" class="input-field" formControlName="password" required> + <label class="input-field-label">Password</label> + + <div class="input-errors"> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.required" class="error">*Required</label> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label> + <label *ngIf="registerUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label> + </div> + </div> + + <hr> + <button class="submit-btn" type="submit">Submit</button> + <app-error-bar></app-error-bar> + </form> + <button class="submit-btn redirect-to-login" (click)="onRedirectLogin()">Already have an account? Login here</button> +</div> diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts new file mode 100644 index 0000000..36eaa55 --- /dev/null +++ b/src/app/components/register/register.component.ts @@ -0,0 +1,86 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { TokenService } from 'src/app/services/token.service'; +import { UserService } from 'src/app/services/user.service'; +import { ErrorBarComponent } from '../error-bar/error-bar.component'; + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.css'] +}) +export class RegisterComponent implements OnInit { + @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent; + private _title = 'Register'; + public registerUserFormGroup: FormGroup; + + constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) { + this._titleService.setTitle(this._title); + } + + ngOnInit(): void { + this.registerUserFormGroup = this._fb.group({ + firstName: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + lastName: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + username: new FormControl('', [ + Validators.required, + Validators.minLength(3) + ]), + email: new FormControl('', [ + Validators.required, + Validators.email, + ]), + password: new FormControl('', [ + Validators.required, + Validators.minLength(3), + Validators.pattern('.*[0-9].*') // Check if password contains atleast one number + ]), + }); + + // this.registerUserFormGroup.valueChanges.subscribe(console.log); + } + + onSubmit(): void { + this._userService.registerUserRequest(this.registerUserFormGroup).subscribe( + res => { + this._tokenService.setUserTokenToSessionStorage(res); + this._router.navigate(['/']); + }, + (err: HttpErrorResponse) => { + this._errorBar.showError(err); + } + ); + } + onRedirectLogin(): void { + this._router.navigate(['/login']); + } + + get firstName(): AbstractControl | null { + return this.registerUserFormGroup.get('firstName'); + } + + get lastName(): AbstractControl | null { + return this.registerUserFormGroup.get('lastName'); + } + + get username(): AbstractControl | null { + return this.registerUserFormGroup.get('username'); + } + + get email(): AbstractControl | null { + return this.registerUserFormGroup.get('email'); + } + + get password(): AbstractControl | null { + return this.registerUserFormGroup.get('password'); + } +} diff --git a/src/app/components/success-bar/success-bar.component.css b/src/app/components/success-bar/success-bar.component.css new file mode 100644 index 0000000..bee634d --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.css @@ -0,0 +1,11 @@ +#success-bar { + width: 100%; + background-color: var(--success); + color: white; + padding: .2em; + text-align: center; +} + +#success-bar:empty { + display: none; +} diff --git a/src/app/components/success-bar/success-bar.component.html b/src/app/components/success-bar/success-bar.component.html new file mode 100644 index 0000000..026e955 --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.html @@ -0,0 +1 @@ +<div id="success-bar">{{successMsg}}</div> diff --git a/src/app/components/success-bar/success-bar.component.ts b/src/app/components/success-bar/success-bar.component.ts new file mode 100644 index 0000000..f7c7e54 --- /dev/null +++ b/src/app/components/success-bar/success-bar.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-success-bar', + templateUrl: './success-bar.component.html', + styleUrls: ['./success-bar.component.css'] +}) +export class SuccessBarComponent implements OnInit { + public successMsg = ''; + + constructor() + { } + + ngOnInit(): void { + this.hideMsg(); + } + + showMsg(msg?: string | undefined): void { + if (msg === undefined) { + this.successMsg = 'Success!'; + } + else if (msg.trim() === '') { + this.successMsg = 'Success!'; + } + else { + this.successMsg = msg; + } + } + + hideMsg(): void { + this.successMsg = ''; + } +} diff --git a/src/app/services/cloudinary.service.ts b/src/app/services/cloudinary.service.ts new file mode 100644 index 0000000..999e498 --- /dev/null +++ b/src/app/services/cloudinary.service.ts @@ -0,0 +1,17 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CloudinaryService { + constructor(private _http: HttpClient) + { } + + getFileRequest(fileLink: string): Observable<Blob> { + return this._http.get(fileLink, { + responseType: 'blob' + }); + } +} diff --git a/src/app/services/comment.service.ts b/src/app/services/comment.service.ts new file mode 100644 index 0000000..c9dbf35 --- /dev/null +++ b/src/app/services/comment.service.ts @@ -0,0 +1,83 @@ +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 { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CommentService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + getDefaultComment(): Comment { + return new Comment(Guid.createEmpty(), Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date()); + } + + /* Requests from session storage */ + + createCommentWithSessionStorageRequest(postId: Guid, commentMessage: string): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.createCommentRequest(userId, token, postId, commentMessage); + } + + putCommentWithSessionStorageRequest(commentId: Guid, postId: Guid, newMessage: string): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putCommentRequest(userId, token, commentId, postId, newMessage); + } + + deleteCommentWithSessionStorage(commentId: Guid): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteCommentRequest(commentId, token); + } + + /* Comment requests */ + + createCommentRequest(userId: Guid, authToken: string, postId: Guid, commentMessage: string): Observable<object> { + const body = { + postId: postId.toString(), + message: commentMessage + }; + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_COMMENT_URL, body, options); + } + + getCommentRequest(id: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', id.toString()) + }; + return this._http.get(AppConstants.API_COMMENT_URL, options); + } + + putCommentRequest(userId: Guid, authToken: string, commentId: Guid, postId: Guid, newMessage: string): Observable<object> { + const body = { + commentId: commentId.toString(), + postId: postId.toString(), + newMessage: newMessage + }; + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.put(AppConstants.API_COMMENT_URL, body, options); + } + + deleteCommentRequest(commentId: Guid, authToken: string): Observable<object> { + const options = { + params: new HttpParams().set('Id', commentId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.delete(AppConstants.API_COMMENT_URL, options); + } +} diff --git a/src/app/services/feed.service.ts b/src/app/services/feed.service.ts new file mode 100644 index 0000000..d160f6d --- /dev/null +++ b/src/app/services/feed.service.ts @@ -0,0 +1,50 @@ +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 FeedService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + /* Requests from session storage */ + + getUserFeedFromSessionStorageRequest(pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + + return this.getUserFeedRequest(userId, token, pageNumber, firstTimeIssued, pageSize); + } + + /* Feed requests */ + + getUserFeedRequest(userId: Guid, authToken: string, pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> { + const body = { + pageNumber: pageNumber, + firstPageTimeIssued: firstTimeIssued, + pageSize: pageSize + }; + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_FEED_URL + '/GetPosts', body, options); + } + + getUserPostsRequest(userName: string, pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> { + const body = { + pageNumber: pageNumber, + firstPageTimeIssued: firstTimeIssued, + pageSize: pageSize + }; + const options = { + params: new HttpParams().set('UserName', userName) + }; + return this._http.post(AppConstants.API_FEED_URL + '/GetUserPosts', body, options); + } +} diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts new file mode 100644 index 0000000..15e241f --- /dev/null +++ b/src/app/services/language.service.ts @@ -0,0 +1,113 @@ +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 { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class LanguageService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + /* Requests from session storage */ + + createLanguageWithSessionStorageRequest(name: string): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.createLanguageRequest(name, token); + } + + getAllLanguagesWithSessionStorageRequest(): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.getAllLanguagesRequest(token); + } + + putLanguageWithSessionStorageRequest(langId: Guid, newName: string): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putLanguageRequest(token, langId, newName); + } + + deleteLanguageWithSessionStorageRequest(langId: Guid): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteLanguageRequest(token, langId); + } + + /* Language requests */ + + createLanguageRequest(name: string, authToken: string): Observable<object> { + const body = { + name: name + }; + const options = { + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_LANGUAGE_URL, body, options); + } + + getLanguageRequest(langId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', langId.toString()), + }; + return this._http.get(AppConstants.API_LANGUAGE_URL, options); + } + + getAllLanguagesRequest(authToken: string): Observable<object> { + const options = { + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.get(AppConstants.API_LANGUAGE_URL + '/GetLanguages', options); + } + + getFullLanguagesFromIncomplete(givenLanguages: Language[]): Promise<Language[]> { + if (givenLanguages.length === 0) { + return new Promise(resolve => resolve(givenLanguages)); + } + + // This accepts language array with incomplete languages, meaning + // languages that only have an id, but no name + return new Promise(resolve => { + const lastGuid = givenLanguages[givenLanguages.length - 1].id; + + // For each language, request his name and assign it + for (const lang of givenLanguages) { + this.getLanguageRequest(lang.id).subscribe( + (result: object) => { + // this only assigns the "name" property to the language, + // because only the name is returned from the request + Object.assign(lang, result); + + if (lastGuid === lang.id) { + resolve(givenLanguages); + } + } + ); + } + }); + } + + putLanguageRequest(authToken: string, langId: Guid, newName: string): Observable<object> { + const body = { + name: newName + }; + const options = { + params: new HttpParams().set('Id', langId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.put(AppConstants.API_LANGUAGE_URL, body, options); + } + + deleteLanguageRequest(authToken: string, langId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', 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 new file mode 100644 index 0000000..7b2a539 --- /dev/null +++ b/src/app/services/post.service.ts @@ -0,0 +1,86 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +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 { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PostService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + getDefaultPost(): Post { + return new Post(Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date(), [], []); + } + + /* Requests from session storage */ + + createPostWithSessionStorageRequest(postMessage: string, files: File[]): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.createPostRequest(userId, token, postMessage, files); + } + + putPostWithSessionStorageRequest(postId: Guid, newMessage: string, posts: File[]): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putPostRequest(userId, token, postId, newMessage, posts); + } + + deletePostWithSessionStorage(postId: Guid): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deletePostRequest(postId, token); + } + + /* Post requests */ + + createPostRequest(userId: Guid, authToken: string, postMessage: string, files: File[]): Observable<object> { + const form = new FormData(); + form.append('message', postMessage); + for (const file of files) { + form.append('files', file, file.name); + } + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_POST_URL, form, options); + } + + getPostRequest(id: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', id.toString()) + }; + return this._http.get(AppConstants.API_POST_URL, options); + } + + putPostRequest(userId: Guid, authToken: string, postId: Guid, newMessage: string, files: File[]): Observable<object> { + const form = new FormData(); + form.append('postId', postId); + form.append('newMessage', newMessage); + for (const file of files) { + form.append('files', file, file.name); + } + const options = { + params: new HttpParams().set('UserId', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.put(AppConstants.API_POST_URL, form, options); + } + + deletePostRequest(postId: Guid, authToken: string): Observable<object> { + const options = { + params: new HttpParams().set('Id', postId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.delete(AppConstants.API_POST_URL, options); + } +} diff --git a/src/app/services/technology.service.ts b/src/app/services/technology.service.ts new file mode 100644 index 0000000..dbdc039 --- /dev/null +++ b/src/app/services/technology.service.ts @@ -0,0 +1,113 @@ +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 { AppConstants } from '../app-constants.module'; +import { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TechnologyService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + /* Requests from session storage */ + + createTechnologyWithSessionStorageRequest(name: string): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.createtTechnologyRequest(name, token); + } + + getAllTechnologiesWithSessionStorageRequest(): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.getAllTechnologiesRequest(token); + } + + putTechnologyWithSessionStorageRequest(langId: Guid, newName: string): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.putTechnologyRequest(token, langId, newName); + } + + deleteTechnologyWithSessionStorageRequest(langId: Guid): Observable<object> { + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteTechnologyRequest(token, langId); + } + + /* Technology requests */ + + createtTechnologyRequest(name: string, authToken: string): Observable<object> { + const body = { + name: name + }; + const options = { + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_TECHNOLOGY_URL, body, options); + } + + getTechnologyRequest(techId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', techId.toString()) + }; + return this._http.get(AppConstants.API_TECHNOLOGY_URL, options); + } + + getAllTechnologiesRequest(authToken: string): Observable<object> { + const options = { + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.get(AppConstants.API_TECHNOLOGY_URL + '/GetTechnologies', options); + } + + getFullTechnologiesFromIncomplete(givenTechnologies: Technology[]): Promise<Technology[]> { + if (givenTechnologies.length === 0) { + return new Promise(resolve => resolve(givenTechnologies)); + } + + // This accepts language array with incomplete languages, meaning + // languages that only have an id, but no name + return new Promise(resolve => { + const lastGuid = givenTechnologies[givenTechnologies.length - 1].id; + + // For each language, request his name and assign it + for (const tech of givenTechnologies) { + this.getTechnologyRequest(tech.id).subscribe( + (result: object) => { + // this only assigns the "name" property to the language, + // because only the name is returned from the request + Object.assign(tech, result); + + if (lastGuid === tech.id) { + resolve(givenTechnologies); + } + } + ); + } + }); + } + + putTechnologyRequest(authToken: string, langId: Guid, newName: string): Observable<object> { + const body = { + name: newName + }; + const options = { + params: new HttpParams().set('Id', langId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.put(AppConstants.API_TECHNOLOGY_URL, body, options); + } + + deleteTechnologyRequest(authToken: string, langId: Guid): Observable<object> { + const options = { + params: new HttpParams().set('Id', langId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.delete(AppConstants.API_TECHNOLOGY_URL, options); + } +} diff --git a/src/app/services/token.service.ts b/src/app/services/token.service.ts new file mode 100644 index 0000000..62bc07e --- /dev/null +++ b/src/app/services/token.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { Guid } from 'guid-typescript'; +import jwt_decode from 'jwt-decode'; +import { IJWTPayload } from 'src/interfaces/jwt-payload'; +import { IUserCredentials } from 'src/interfaces/user-credentials'; +import { AppConstants } from '../app-constants.module'; + +@Injectable({ + providedIn: 'root' +}) +export class TokenService { + constructor() + { } + + /* Session storage */ + + setUserTokenToSessionStorage(response: object): void { + const token = JSON.stringify(response); + sessionStorage.setItem(AppConstants.SESSION_TOKEN_KEY, token.substr(10, token.length - 12)); + } + + getTokenFromSessionStorage(): string { + return sessionStorage.getItem(AppConstants.SESSION_TOKEN_KEY) ?? ''; + } + + getUserIdFromSessionStorageToken(): Guid { + const jwt: IJWTPayload = { + token: this.getTokenFromSessionStorage() + }; + const userCred = jwt_decode<IUserCredentials>(jwt.token); + + return userCred.ID; + } + + getUsernameFromSessionStorageToken(): string { + const jwt: IJWTPayload = { + token: this.getTokenFromSessionStorage() + }; + const userCred = jwt_decode<IUserCredentials>(jwt.token); + + return userCred.Username; + } + + logoutUserFromSessionStorage(): void { + sessionStorage.removeItem(AppConstants.SESSION_TOKEN_KEY); + } +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts new file mode 100644 index 0000000..31862c4 --- /dev/null +++ b/src/app/services/user.service.ts @@ -0,0 +1,179 @@ +import { Injectable } from '@angular/core'; +import { Guid } from 'guid-typescript'; +import { User } from '../../models/identity/user'; +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 { TokenService } from './token.service'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + constructor(private _http: HttpClient, private _tokenService: TokenService) + { } + + getDefaultUser(): User { + return new User(Guid.createEmpty(), 'gosho_trapov', 'Gosho', 'Trapov', 'gotra@bg.com', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []); + } + + /* Requests from session storage */ + + getUserFromSessionStorageRequest(): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.getUserRequest(userId, token); + } + + addFriendToUserFromSessionStorageRequest(newFriendUserName: string): Observable<object> { + const userUserName = this._tokenService.getUsernameFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.addFriendToUserRequest(userUserName, token, newFriendUserName); + } + + putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, 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); + } + + deleteUserFromSessionStorageRequest(): Observable<object> { + const userId = this._tokenService.getUserIdFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.deleteUserRequest(userId, token); + } + + removeFriendFromUserFromSessionStorageRequest(friendToRemoveUserName: string): Observable<object> { + const userUserName = this._tokenService.getUsernameFromSessionStorageToken(); + const token = this._tokenService.getTokenFromSessionStorage(); + + return this.removeFriendFromUserRequest(userUserName, token, friendToRemoveUserName); + } + + + /* User requests */ + + loginUserRequest(loginUserFormGroup: FormGroup): Observable<object> { + const body = { + UserName: loginUserFormGroup.get('username')?.value, + Password: loginUserFormGroup.get('password')?.value + }; + return this._http.post(AppConstants.API_USER_LOGIN_URL, body); + } + + registerUserRequest(registerUserFormGroup: FormGroup): Observable<object> { + const body = { + UserName: registerUserFormGroup.get('username')?.value, + Email: registerUserFormGroup.get('email')?.value, + FirstName: registerUserFormGroup.get('firstName')?.value, + LastName: registerUserFormGroup.get('lastName')?.value, + Password: registerUserFormGroup.get('password')?.value + }; + return this._http.post(AppConstants.API_USER_REGISTER_URL, body); + } + + addFriendToUserRequest(userUserName: string, authToken: string, newFriendUserName: string): Observable<object> { + const body = { + newFriendUserName: newFriendUserName + }; + const options = { + params: new HttpParams().set('UserName', userUserName), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.put(AppConstants.API_USER_URL + '/AddFriend', body, options); + } + + getUserRequest(userId: Guid, authToken: string): Observable<object> { + const options = { + params: new HttpParams().set('Id', userId.toString()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.get(AppConstants.API_USER_URL, options); + } + + getUserByUsernameRequest(username: string): Observable<object> { + const options = { + params: new HttpParams().set('UserName', username), + }; + return this._http.get(AppConstants.API_USER_URL + '/GetUser', options); + } + + putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> { + const body = { + UserName: updateUserFormGroup.get('username')?.value, + Email: updateUserFormGroup.get('email')?.value, + FirstName: updateUserFormGroup.get('firstName')?.value, + LastName: updateUserFormGroup.get('lastName')?.value, + 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) + }; + 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) + }; + 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()), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.delete(AppConstants.API_USER_URL, options); + } + + removeFriendFromUserRequest(userUserName: string, authToken: string, friendToRemoveUserName: string): Observable<object> { + const body = { + friendUserNameToRemove: friendToRemoveUserName + }; + const options = { + params: new HttpParams().set('UserName', userUserName), + headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken) + }; + return this._http.post(AppConstants.API_USER_URL + '/RemoveFriend', body, options); + } +} diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/assets/.gitkeep diff --git a/src/assets/images/comment.png b/src/assets/images/comment.png Binary files differnew file mode 100644 index 0000000..5f8e8d9 --- /dev/null +++ b/src/assets/images/comment.png diff --git a/src/assets/images/feed/chat-pic.png b/src/assets/images/feed/chat-pic.png Binary files differnew file mode 100644 index 0000000..60241fa --- /dev/null +++ b/src/assets/images/feed/chat-pic.png diff --git a/src/assets/images/feed/profile-pic.png b/src/assets/images/feed/profile-pic.png Binary files differnew file mode 100644 index 0000000..87f67f5 --- /dev/null +++ b/src/assets/images/feed/profile-pic.png diff --git a/src/assets/images/paper-clip.png b/src/assets/images/paper-clip.png Binary files differnew file mode 100644 index 0000000..46ce0a7 --- /dev/null +++ b/src/assets/images/paper-clip.png diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..7b4f817 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/src/favicon.ico b/src/favicon.ico Binary files differnew file mode 100644 index 0000000..997406a --- /dev/null +++ b/src/favicon.ico diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..d488ca9 --- /dev/null +++ b/src/index.html @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <base href="/"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="icon" type="image/x-icon" href="favicon.ico"> + <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> +</head> + +<body class="mat-typography"> + <app-root></app-root> +</body> + +</html>
\ No newline at end of file diff --git a/src/interfaces/api-error.ts b/src/interfaces/api-error.ts new file mode 100644 index 0000000..4dd68f3 --- /dev/null +++ b/src/interfaces/api-error.ts @@ -0,0 +1,6 @@ +export interface IApiError { + type: string; + title: string; + status: number; + traceId: string; +} diff --git a/src/interfaces/jwt-payload.ts b/src/interfaces/jwt-payload.ts new file mode 100644 index 0000000..a2d0f0d --- /dev/null +++ b/src/interfaces/jwt-payload.ts @@ -0,0 +1,3 @@ +export interface IJWTPayload { + token: string; +} diff --git a/src/interfaces/user-credentials.ts b/src/interfaces/user-credentials.ts new file mode 100644 index 0000000..bb47540 --- /dev/null +++ b/src/interfaces/user-credentials.ts @@ -0,0 +1,6 @@ +import { Guid } from 'guid-typescript'; + +export interface IUserCredentials { + ID: Guid; + Username: string; +} diff --git a/src/main.server.ts b/src/main.server.ts new file mode 100644 index 0000000..10150a7 --- /dev/null +++ b/src/main.server.ts @@ -0,0 +1,10 @@ +import { enableProdMode } from '@angular/core'; + +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +export { AppServerModule } from './app/app.server.module'; +export { renderModule, renderModuleFactory } from '@angular/platform-server'; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ebf5fc9 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,14 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +document.addEventListener('DOMContentLoaded', () => { + platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); +}); diff --git a/src/models/comment.ts b/src/models/comment.ts new file mode 100644 index 0000000..0d1755f --- /dev/null +++ b/src/models/comment.ts @@ -0,0 +1,70 @@ +import { Guid } from 'guid-typescript'; + +export class Comment { + private _commentId: Guid; + private _postId: Guid; + private _issuerFirstName: string; + private _issuerLastName: string; + private _issuerUsername: string; + private _message: string; + private _timeCreated: Date; + + constructor(commentId: Guid, postId: Guid, issuerFirstName: string, issuerLastName: string, issuerUsername: string, message: string, timeCreated: Date) { + this.commentId = commentId; + this.postId = postId; + this.issuerFirstName = issuerFirstName; + this.issuerLastName = issuerLastName; + this.issuerUsername = issuerUsername; + this.message = message; + this.timeCreated = timeCreated; + } + + public get commentId(): Guid { + return this._commentId; + } + public set commentId(v: Guid) { + this._commentId = v; + } + + public get postId(): Guid { + return this._postId; + } + public set postId(v: Guid) { + this._postId = v; + } + + public get issuerFirstName(): string { + return this._issuerFirstName; + } + public set issuerFirstName(v: string) { + this._issuerFirstName = v; + } + + public get issuerLastName(): string { + return this._issuerLastName; + } + public set issuerLastName(v: string) { + this._issuerLastName = v; + } + + public get issuerUsername(): string { + return this._issuerUsername; + } + public set issuerUsername(v: string) { + this._issuerUsername = v; + } + + public get message(): string { + return this._message; + } + public set message(v: string) { + this._message = v; + } + + public get timeCreated(): Date { + return this._timeCreated; + } + public set timeCreated(v: Date) { + this._timeCreated = v; + } +} diff --git a/src/models/identity/friend.ts b/src/models/identity/friend.ts new file mode 100644 index 0000000..22290cd --- /dev/null +++ b/src/models/identity/friend.ts @@ -0,0 +1,3 @@ +export class Friend { + public userName: string; +} diff --git a/src/models/identity/role.ts b/src/models/identity/role.ts new file mode 100644 index 0000000..132b0b0 --- /dev/null +++ b/src/models/identity/role.ts @@ -0,0 +1,3 @@ +export class Role { + public name: string; +} diff --git a/src/models/identity/user.ts b/src/models/identity/user.ts new file mode 100644 index 0000000..e0038e0 --- /dev/null +++ b/src/models/identity/user.ts @@ -0,0 +1,100 @@ +import { Guid } from 'guid-typescript'; +import { Language } from '../language'; +import { Technology } from '../technology'; +import { Friend } from './friend'; +import { Role } from './role'; + +export class User { + private _id : Guid; + private _lastName : string; + private _firstName : string; + private _userName : string; + private _email: string; + private _profilePictureURL : string; + private _languages: Language[]; + private _technologies: Technology[]; + private _roles: Role[]; + private _friends: Friend[]; + + constructor(id: Guid, userName: string, firstName: string, lastName: string, email: string, profilePictureURL: string, languages: Language[], technologies: Technology[], roles: Role[], friends: Friend[]) { + this.id = id; + this.userName = userName; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this._profilePictureURL = profilePictureURL; + this.languages = languages; + this.technologies = technologies; + this.roles = roles; + } + + public get id(): Guid { + return this._id; + } + public set id(v: Guid) { + this._id = v; + } + + public get userName(): string { + return this._userName; + } + public set userName(v: string) { + this._userName = v; + } + + public get firstName(): string { + return this._firstName; + } + public set firstName(v: string) { + this._firstName = v; + } + + public get lastName(): string { + return this._lastName; + } + public set lastName(v: string) { + this._lastName = v; + } + + public get email(): string { + return this._email; + } + public set email(v: string) { + this._email = v; + } + + public get profilePictureURL(): string { + return this._profilePictureURL; + } + public set profilePictureURL(v: string) { + this._profilePictureURL = v; + } + + public get languages(): Language[] { + return this._languages; + } + public set languages(v: Language[]) { + this._languages = v; + } + + public get technologies(): Technology[] { + return this._technologies; + } + public set technologies(v: Technology[]) { + this._technologies = v; + } + + public get roles(): Role[] { + return this._roles; + } + public set roles(v: Role[]) { + this._roles = v; + } + + public get friends(): Friend[] { + return this._friends; + } + public set friends(v: Friend[]) { + this._friends = v; + } +} diff --git a/src/models/language.ts b/src/models/language.ts new file mode 100644 index 0000000..e3aa61e --- /dev/null +++ b/src/models/language.ts @@ -0,0 +1,6 @@ +import { Guid } from 'guid-typescript'; + +export class Language { + public id: Guid; + public name: string; +} diff --git a/src/models/post-comment.ts b/src/models/post-comment.ts new file mode 100644 index 0000000..5d1e346 --- /dev/null +++ b/src/models/post-comment.ts @@ -0,0 +1,5 @@ +import { Guid } from 'guid-typescript'; + +export class PostComment { + public id: Guid; +} diff --git a/src/models/post.ts b/src/models/post.ts new file mode 100644 index 0000000..8e58bea --- /dev/null +++ b/src/models/post.ts @@ -0,0 +1,81 @@ +import { Guid } from 'guid-typescript'; +import { Comment } from './comment'; +import { PostComment } from './post-comment'; + +export class Post { + private _postId: Guid; + private _creatorFirstName: string; + private _creatorLastName: string; + private _creatorUsername: string; + private _message: string; + private _timeCreated: Date; + private _comments: PostComment[]; + private _fileURLs: string[]; + + constructor(postId: Guid, creatorFirstName: string, creatorLastName: string, creatorUsername: string, message: string, timeCreated: Date, comments: PostComment[], fileURLs: string[]) { + this.postId = postId; + this.creatorFirstName = creatorFirstName; + this.creatorLastName = creatorLastName; + this.creatorUsername = creatorUsername; + this.message = message; + this.timeCreated = timeCreated; + this.comments = comments; + this.fileURLs = fileURLs; + } + + public get postId(): Guid { + return this._postId; + } + public set postId(v: Guid) { + this._postId = v; + } + + public get creatorFirstName(): string { + return this._creatorFirstName; + } + public set creatorFirstName(v: string) { + this._creatorFirstName = v; + } + + public get creatorLastName(): string { + return this._creatorLastName; + } + public set creatorLastName(v: string) { + this._creatorLastName = v; + } + + public get creatorUsername(): string { + return this._creatorUsername; + } + public set creatorUsername(v: string) { + this._creatorUsername = v; + } + + public get message(): string { + return this._message; + } + public set message(v: string) { + this._message = v; + } + + public get timeCreated(): Date { + return this._timeCreated; + } + public set timeCreated(v: Date) { + this._timeCreated = v; + } + + public get comments(): PostComment[] { + return this._comments; + } + public set comments(v: PostComment[]) { + this._comments = v; + } + + public get fileURLs(): string[] { + return this._fileURLs; + } + public set fileURLs(v: string[]) { + this._fileURLs = v; + } +} diff --git a/src/models/technology.ts b/src/models/technology.ts new file mode 100644 index 0000000..1869d14 --- /dev/null +++ b/src/models/technology.ts @@ -0,0 +1,6 @@ +import { Guid } from 'guid-typescript'; + +export class Technology { + public id: Guid; + public name: string; +} diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 0000000..9b8f300 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,63 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/src/reset.css b/src/reset.css new file mode 100644 index 0000000..d9ac20b --- /dev/null +++ b/src/reset.css @@ -0,0 +1,52 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v5.0.1 | 20191019 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, menu, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +main, menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, main, menu, nav, section { + display: block; +} +/* HTML5 hidden-attribute fix for newer browsers */ +*[hidden] { + display: none; +} +body { + line-height: 1; +} +menu, ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..eeb93fe --- /dev/null +++ b/src/styles.css @@ -0,0 +1,262 @@ +/* You can add global styles to this file, and also import other style files */ +@import "./reset.css"; + +:root { + --bg-color: white; + --focus-color: forestgreen; + --card-bg: white; + --success: forestgreen; + --failure: indianred; +} + +html, body { + height: 100%; + margin: 0; +} +body { + font: 21px sans-serif !important; + background-color: var(--bg-color); +} + +input:focus, button:focus { + outline: 0; +} + +#content { /* Used for the login and register pages */ + height: 100%; + max-width: 20em; + box-sizing: border-box; + border: .5em solid var(--bg-color); + + margin: 0 auto; + + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.rounded-border { + border: 2px solid black; + border-radius: .6em; + padding: .4em; +} + +.round-image { + border-radius: 50%; + object-fit: cover; +} + +.title { + font-size: 2em; + font-weight: bold; +} + +.error { + color: red; +} + +.scroll-standalone { + width: 100%; + max-height: 100%; + overflow-y: auto; +} + + /* Hide scrollbar for Chrome, Safari and Opera */ +.scroll-standalone::-webkit-scrollbar { + display: none; +} + + /* Hide scrollbar for IE, Edge and Firefox */ +.scroll-standalone { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.user-language, .user-technology { + border-radius: .4em; + background-color: lightgrey; + padding: .26em; + margin: .1em .2em; + width: fit-content; +} + +/* Inputs, the type found in login and register */ + +.input-selection { + position: relative; + margin-top: .7em; +} + + /* Don't show the placeholder when the label is on top + */ +.input-selection .input-field::-webkit-input-placeholder { + opacity: 0; +} + +.input-field { + width: 100%; + background-color: var(--bg-color); + + border: 0; + border-bottom: 1px solid grey; + box-sizing: border-box; + + margin-bottom: .5em; + padding: .4em; + padding-left: 0; + + font-size: inherit; +} + +.input-field-label { + width: inherit; + height: inherit; + position: absolute; + left: 0; + + margin-top: .4em; + color: grey; +} + + /* When hovering, typing or having typed something in an input, + * make the label smaller, color it and then move it up + */ +.input-selection:hover > .input-field-label , +.input-selection > input:not(:placeholder-shown) + .input-field-label , +.input-selection > input:focus + .input-field-label { + font-size: .7em; + color: var(--focus-color); + transform: translate(0, -1.2em); +} + + /* Show the placeholder, when you've hovered or + * focused (typing in) on the input-field + */ +.input-selection:hover > .input-field::-webkit-input-placeholder, +.input-selection > .input-field:focus::-webkit-input-placeholder { + opacity: 1; +} + + /* Make the underline thicker and change it's and the cursors's + * color when hovered or focused (typing in) on the input-field + */ +.input-selection:hover > .input-field, +.input-field:focus { + border-color: var(--focus-color) !important; + caret-color: var(--focus-color); + border-width: 2px !important; + margin-top: -1px !important; +} + + +/* Input errors */ + +.input-errors { + margin-top: -.8em; + font-size: .7em; + + position: absolute; + right: 0; + top: 0; +} + + /* Move the errors above the input when + * using the site on a small screen and + * add some space for them above the input + */ +@media screen and (max-width: 350px) { + .input-errors { + margin-top: -1.8em; + } + .input-selection { + margin-top: 1.6em; + } +} + +.input-errors > .error { + margin-left: .3em; +} + +.input-field:focus ~ .input-errors > .error { + opacity: 1 !important; +} + +.input-field:placeholder-shown ~ .input-errors > .error { + opacity: 0; +} + +/* Submit button */ + +.submit-btn { + width: 100%; + color: white; + background-color: black; + + border: 2px solid black; + border-radius: .4em; + box-sizing: border-box; + + font-size: .8em; + text-align: center; + + margin-top: .5em; + padding: .3em; +} + +.submit-btn:hover { + cursor: pointer; + color: var(--focus-color); + background-color: white; + border-color: var(--focus-color) !important; +} + +.submit-btn:active { + transition: 0s; + transform: scale(.9); +} + +.delete-btn:hover { + color: indianred; + border-color: indianred !important; +} + +/* Form attachments (the ones that are shown while creating and editing a post) */ + +.form-attachments { + display: flex; + flex-wrap: wrap; + color: gray; + font-size: .75em; + margin: 0 .3em; +} + +.form-attachment { + border: 2px solid black; + border-radius: .6em; + margin-top: .2em; + margin-right: .2em; + padding: .2em; + display: flex; + align-items: center; +} + +.form-attachment:last-child { + margin-right: 0; +} + +.remove-form-attachment { + font-size: .9em; + color: var(--failure); + background-color: white; + border-radius: .2em; + margin: 0 .2em; + padding: .2em; +} + +.remove-form-attachment:hover { + color: white; + background-color: var(--failure); + cursor: pointer; +} + diff --git a/src/theme.scss b/src/theme.scss new file mode 100644 index 0000000..a87ae45 --- /dev/null +++ b/src/theme.scss @@ -0,0 +1,14 @@ +// Import theming functions +@import '~@angular/material/theming'; +@import './styles.css'; +@include mat-core(); + +// Custom Angular theme + +// $my-custom-primary: mat-palette($mat-deep-purple); +// $my-custom-accent: mat-palette($mat-pink, 100, 500, A100); +// $my-custom-warn: mat-palette($mat-lime); + +// $my-custom-theme: mat-light-theme($my-custom-primary, $my-custom-accent, $my-custom-warn); + +// @include angular-material-theme($my-custom-theme);
\ No newline at end of file |
