From 968134ce3adccf2a872b02ebde69dea726f5346f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Louie=20Bo=C3=B1on?= Date: Sun, 11 Jul 2021 00:24:16 +0800 Subject: [PATCH 1/2] Implement Welcome page - Default list creation included --- run-mac-os.command | 4 +- src/app/app-routing.module.ts | 5 ++ .../add-task-sheet.component.ts | 3 +- src/app/my-tasks/list.ts | 6 ++ src/app/register/register.component.ts | 2 +- src/app/services/list.service.spec.ts | 16 ++++++ src/app/services/list.service.ts | 24 ++++++++ src/app/shared/toolbar/toolbar.component.html | 3 +- src/app/welcome/welcome-routing.module.ts | 16 ++++++ src/app/welcome/welcome.component.html | 39 +++++++++++++ src/app/welcome/welcome.component.scss | 0 src/app/welcome/welcome.component.spec.ts | 25 ++++++++ src/app/welcome/welcome.component.ts | 57 +++++++++++++++++++ src/app/welcome/welcome.module.ts | 36 ++++++++++++ 14 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 src/app/my-tasks/list.ts create mode 100644 src/app/services/list.service.spec.ts create mode 100644 src/app/services/list.service.ts create mode 100644 src/app/welcome/welcome-routing.module.ts create mode 100644 src/app/welcome/welcome.component.html create mode 100644 src/app/welcome/welcome.component.scss create mode 100644 src/app/welcome/welcome.component.spec.ts create mode 100644 src/app/welcome/welcome.component.ts create mode 100644 src/app/welcome/welcome.module.ts diff --git a/run-mac-os.command b/run-mac-os.command index c9604b3..03331ac 100755 --- a/run-mac-os.command +++ b/run-mac-os.command @@ -5,5 +5,5 @@ # Serve Front-end (Angular) cd "$(dirname "$0")" echo $PWD -echo "Executing... ng serve --port 8081" -ng serve --port 8081 \ No newline at end of file +echo "Executing... ng serve --port 4200" +ng serve --port 4200 \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 027cb44..4b6f750 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -24,6 +24,11 @@ const routes: Routes = [ canActivate: [AuthGuard], // data: { animation: 'EditAccount' } // @todo For router animation }, + { + path: 'welcome', + loadChildren: () => import('./welcome/welcome.module').then(m => m.WelcomeModule), + canActivate: [AuthGuard], + }, { path: '', component: StartComponent }, // { path: '**', component: PageNotFoundComponent }, @TODO ]; diff --git a/src/app/my-tasks/add-task-sheet/add-task-sheet.component.ts b/src/app/my-tasks/add-task-sheet/add-task-sheet.component.ts index 6298787..6517002 100644 --- a/src/app/my-tasks/add-task-sheet/add-task-sheet.component.ts +++ b/src/app/my-tasks/add-task-sheet/add-task-sheet.component.ts @@ -67,7 +67,8 @@ export class AddTaskSheetComponent implements OnInit, AfterViewInit { }; this.isSaving = true; - this.taskService.create(data) + this.taskService + .create(data) .subscribe( response => { console.log(response); diff --git a/src/app/my-tasks/list.ts b/src/app/my-tasks/list.ts new file mode 100644 index 0000000..e74ba2c --- /dev/null +++ b/src/app/my-tasks/list.ts @@ -0,0 +1,6 @@ +export interface List { + name: string; + owner: string; + isDefault: boolean; +} + \ No newline at end of file diff --git a/src/app/register/register.component.ts b/src/app/register/register.component.ts index 91ccbff..b03c67e 100644 --- a/src/app/register/register.component.ts +++ b/src/app/register/register.component.ts @@ -67,7 +67,7 @@ export class RegisterComponent implements OnInit { response => { console.log(response); this.snackBar.open('Signed up successfully', null, {duration: 2000}); - this.router.navigate(['/my-tasks']); + this.router.navigate(['/welcome']); }, error => { this.isSaving = false; diff --git a/src/app/services/list.service.spec.ts b/src/app/services/list.service.spec.ts new file mode 100644 index 0000000..4cce069 --- /dev/null +++ b/src/app/services/list.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ListService } from './list.service'; + +describe('ListService', () => { + let service: ListService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ListService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/list.service.ts b/src/app/services/list.service.ts new file mode 100644 index 0000000..d34181c --- /dev/null +++ b/src/app/services/list.service.ts @@ -0,0 +1,24 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AuthService } from './auth.service'; +import { List } from '../my-tasks/list' + +const baseUrl = 'http://localhost:8080/api/lists' + +@Injectable({ + providedIn: 'root' +}) +export class ListService { + + constructor( + private http: HttpClient, + private auth: AuthService + ) { } + + create(data: List): Observable { + return this.http.post(baseUrl, data, { headers: this.auth.getAuthHeader()}); + } + + +} diff --git a/src/app/shared/toolbar/toolbar.component.html b/src/app/shared/toolbar/toolbar.component.html index bf368f5..a0096b2 100644 --- a/src/app/shared/toolbar/toolbar.component.html +++ b/src/app/shared/toolbar/toolbar.component.html @@ -5,13 +5,12 @@ mat-icon-button aria-label="back-btn" color="primary" - class="margin-right-x-small" (click)="back()" > arrow_back
- + {{title}} diff --git a/src/app/welcome/welcome-routing.module.ts b/src/app/welcome/welcome-routing.module.ts new file mode 100644 index 0000000..48bae43 --- /dev/null +++ b/src/app/welcome/welcome-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { WelcomeComponent } from './welcome.component'; + +const routes: Routes = [ + { + path: '', + component: WelcomeComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class WelcomeRoutingModule { } diff --git a/src/app/welcome/welcome.component.html b/src/app/welcome/welcome.component.html new file mode 100644 index 0000000..4b3e9ec --- /dev/null +++ b/src/app/welcome/welcome.component.html @@ -0,0 +1,39 @@ + +
+ +

Hello {{user.nickname}},

+

We are so excited to have you here! Fret no more because we are here to help you be the most productive you.

+

To get started, let's create your default list.

+
+ + List Name + + + + +
+
+
\ No newline at end of file diff --git a/src/app/welcome/welcome.component.scss b/src/app/welcome/welcome.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/welcome/welcome.component.spec.ts b/src/app/welcome/welcome.component.spec.ts new file mode 100644 index 0000000..949ca3c --- /dev/null +++ b/src/app/welcome/welcome.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WelcomeComponent } from './welcome.component'; + +describe('WelcomeComponent', () => { + let component: WelcomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ WelcomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(WelcomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/welcome/welcome.component.ts b/src/app/welcome/welcome.component.ts new file mode 100644 index 0000000..c53bce5 --- /dev/null +++ b/src/app/welcome/welcome.component.ts @@ -0,0 +1,57 @@ +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { fade } from 'src/app/utilities/animations'; +import { AuthService, User } from '../services/auth.service'; +import { ListService } from '../services/list.service'; + +@Component({ + selector: 'app-welcome', + templateUrl: './welcome.component.html', + styleUrls: ['./welcome.component.scss'], + animations: [fade] +}) +export class WelcomeComponent implements OnInit { + + createListForm: FormGroup; + listNameFormCtrl = new FormControl('Default'); + user: User; + isSaving: boolean; + + constructor( + private auth: AuthService, + private listService: ListService, + private router: Router, + private snackBar: MatSnackBar + ) { } + + ngOnInit(): void { + this.createListForm = new FormGroup({ + 'listName': this.listNameFormCtrl + }); + this.user = this.auth.getUser(); + } + + onSubmit(): void { + const data = { name: this.listNameFormCtrl.value, owner: this.user.id, isDefault: true }; + this.isSaving = true; + this.listService + .create(data) + .subscribe( + response => { + // @todo: prevent user from going back to welcome page by manuualy entering url + console.log(response); + this.snackBar.open('Time to get to work...', null, { duration: 2000 }); // move duration into constant in a shared class + this.router.navigate(['/my-tasks']) + }, + (error: HttpErrorResponse) => { + this.isSaving = false; + this.snackBar.open('An error occured while adding the task. Please try again later.', null, { duration: 4000 }); // move duration into constant in a shared class + console.log(error); + } + ); + } + +} diff --git a/src/app/welcome/welcome.module.ts b/src/app/welcome/welcome.module.ts new file mode 100644 index 0000000..e1e9c12 --- /dev/null +++ b/src/app/welcome/welcome.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { WelcomeComponent } from './welcome.component'; +import { WelcomeRoutingModule } from './welcome-routing.module'; +import { ToolbarModule } from '../shared/toolbar/toolbar.module'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + + +@NgModule({ + declarations: [ + WelcomeComponent + ], + imports: [ + CommonModule, + WelcomeRoutingModule, + ToolbarModule, + MatButtonModule, + MatCardModule, + MatFormFieldModule, + ReactiveFormsModule, + MatInputModule, + MatDividerModule, + MatIconModule, + MatSnackBarModule, + MatProgressSpinnerModule + ] +}) +export class WelcomeModule { } From 546f6b89c22f0be3e7c9ff7bf408e75642799bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Louie=20Bo=C3=B1on?= Date: Tue, 27 Jul 2021 23:13:01 +0800 Subject: [PATCH 2/2] Changes to filters and implement lists (partial) - Split AuthService to SessionService, AuthService and UserService - Add list to filters - Create list model - Implement new filters sidebar - Revamp filters summary - Move filters business logic to Filters Service --- src/app/login/login.component.ts | 16 ++- .../delete-account-dialog.component.ts | 14 +- .../edit-account/edit-account.component.ts | 26 ++-- src/app/my-tasks/filters.ts | 6 +- src/app/my-tasks/list.ts | 4 +- .../main-toolbar/main-toolbar.component.ts | 2 +- src/app/my-tasks/my-tasks.component.html | 8 +- src/app/my-tasks/my-tasks.component.ts | 39 ++++- src/app/my-tasks/my-tasks.module.ts | 2 + src/app/my-tasks/navbar/navbar.component.html | 72 +++++----- src/app/my-tasks/navbar/navbar.component.ts | 12 +- .../task-filters-bar.component.html | 77 ++++++++++ .../task-filters-bar.component.scss | 0 .../task-filters-bar.component.spec.ts | 25 ++++ .../task-filters-bar.component.ts | 89 ++++++++++++ .../task-filters/task-filters.component.html | 57 +++----- .../task-filters/task-filters.component.ts | 55 ++++---- .../task-list/task-list.component.html | 2 +- .../my-tasks/task-list/task-list.component.ts | 31 ++-- src/app/register/register.component.ts | 20 ++- src/app/services/auth.guard.ts | 26 ++-- src/app/services/auth.service.ts | 133 +++--------------- src/app/services/filters.service.spec.ts | 16 +++ src/app/services/filters.service.ts | 43 ++++++ src/app/services/list.service.ts | 25 +++- src/app/services/notifier.service.ts | 12 +- src/app/services/session.service.spec.ts | 16 +++ src/app/services/session.service.ts | 96 +++++++++++++ src/app/services/task.service.ts | 4 - src/app/services/user.service.spec.ts | 16 +++ src/app/services/user.service.ts | 79 +++++++++++ src/app/start/start.component.ts | 18 ++- src/app/welcome/welcome.component.ts | 13 +- src/styles.scss | 14 +- 34 files changed, 752 insertions(+), 316 deletions(-) create mode 100644 src/app/my-tasks/task-filters-bar/task-filters-bar.component.html create mode 100644 src/app/my-tasks/task-filters-bar/task-filters-bar.component.scss create mode 100644 src/app/my-tasks/task-filters-bar/task-filters-bar.component.spec.ts create mode 100644 src/app/my-tasks/task-filters-bar/task-filters-bar.component.ts create mode 100644 src/app/services/filters.service.spec.ts create mode 100644 src/app/services/filters.service.ts create mode 100644 src/app/services/session.service.spec.ts create mode 100644 src/app/services/session.service.ts create mode 100644 src/app/services/user.service.spec.ts create mode 100644 src/app/services/user.service.ts diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 083e44f..24b7693 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -5,6 +5,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { AuthService, LoginPayload } from 'src/app/services/auth.service'; import { fade } from 'src/app/utilities/animations'; +import { SessionService } from '../services/session.service'; @Component({ selector: 'app-login', @@ -18,11 +19,16 @@ export class LoginComponent implements OnInit { private router: Router, private snackBar: MatSnackBar, private auth: AuthService, + private sessionService: SessionService ) { - if (this.auth.isLoggedIn()) { - this.snackBar.open('Redirecting to My Tasks...', null, {duration: 2000}); - this.router.navigate(['/my-tasks']); - } + this.sessionService.isLoggedIn().subscribe( + response => { + if (response) { + this.snackBar.open('Redirecting to My Tasks...', null, {duration: 2000}); + this.router.navigate(['/my-tasks']); + } + } + ); } loginFormGroup: FormGroup; @@ -51,7 +57,7 @@ export class LoginComponent implements OnInit { username: this.username.value, password: this.password.value }; - this.auth.login(payload).subscribe( + this.sessionService.login(payload).subscribe( response => { console.log(response); this.snackBar.open('Signed in successfully', null, {duration: 1500}); diff --git a/src/app/manage-account/delete-account-dialog/delete-account-dialog.component.ts b/src/app/manage-account/delete-account-dialog/delete-account-dialog.component.ts index 7be931a..b295f0d 100644 --- a/src/app/manage-account/delete-account-dialog/delete-account-dialog.component.ts +++ b/src/app/manage-account/delete-account-dialog/delete-account-dialog.component.ts @@ -1,10 +1,11 @@ -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; -import { AuthService } from 'src/app/services/auth.service'; +import { SessionService } from 'src/app/services/session.service'; +import { UserService } from 'src/app/services/user.service'; @Component({ selector: 'app-delete-account-dialog', @@ -18,9 +19,10 @@ export class DeleteAccountDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, - private auth: AuthService, private snackBar: MatSnackBar, - private router: Router + private router: Router, + private userService: UserService, + private sessionService: SessionService ) { } ngOnInit(): void { @@ -28,9 +30,9 @@ export class DeleteAccountDialogComponent implements OnInit { onDelete(): void { this.isDeleting = true; - this.auth.deleteAccount(this.password.value).subscribe( + this.userService.delete(this.password.value, this.sessionService.getUser()).subscribe( () => { - this.auth.removeSession(); + this.sessionService.removeSession(); this.dialogRef.close(); this.snackBar.open('Account was deleted successfully', null, {duration: 1500 }); this.router.navigate(['/']); diff --git a/src/app/manage-account/edit-account/edit-account.component.ts b/src/app/manage-account/edit-account/edit-account.component.ts index fbb9e3b..e76c3fb 100644 --- a/src/app/manage-account/edit-account/edit-account.component.ts +++ b/src/app/manage-account/edit-account/edit-account.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { ActivatedRoute, Router } from '@angular/router'; -import { AuthService, EditAccountPayload, User } from 'src/app/services/auth.service'; +import { ActivatedRoute } from '@angular/router'; +import { SessionService } from 'src/app/services/session.service'; +import { UpdateUserPayload, User, UserService } from 'src/app/services/user.service'; import { fade } from 'src/app/utilities/animations'; @Component({ @@ -20,13 +21,12 @@ export class EditAccountComponent implements OnInit { email = new FormControl({value: '', disabled: true}); username = new FormControl({value: '', disabled: true}); isSaving: boolean; - user: User; constructor( - private router: Router, private route: ActivatedRoute, - private auth: AuthService, - private snackBar: MatSnackBar + private snackBar: MatSnackBar, + private sessionService: SessionService, + private userService: UserService ) { } ngOnInit(): void { @@ -40,20 +40,20 @@ export class EditAccountComponent implements OnInit { 'email': this.email, 'username': this.username, }); - this.user = this.auth.getUser(); - this.fullName.setValue(this.user.fullName); - this.nickname.setValue(this.user.nickname); - this.email.setValue(this.user.email); - this.username.setValue(this.user.username); + const user = this.sessionService.getUser(); + this.fullName.setValue(user.fullName); + this.nickname.setValue(user.nickname); + this.email.setValue(user.email); + this.username.setValue(user.username); } onSubmit(): void { this.isSaving = true; - const payload: EditAccountPayload = { + const payload: UpdateUserPayload = { fullName: this.fullName.value, nickname: this.nickname.value }; - this.auth.editAccount(payload).subscribe( + this.userService.update(payload, this.sessionService.getUser()).subscribe( response => { this.isSaving = false; console.log('yey', response); // @todo diff --git a/src/app/my-tasks/filters.ts b/src/app/my-tasks/filters.ts index 2b8b889..b3c1ebd 100644 --- a/src/app/my-tasks/filters.ts +++ b/src/app/my-tasks/filters.ts @@ -1,6 +1,8 @@ +import { List } from "./list"; + export interface Filters { - dueDate: string; - dueDateDisplay: string; + list: { id: string, name: string }; + dueDate: { code: string, displayText: string }; showCompleted: boolean; } \ No newline at end of file diff --git a/src/app/my-tasks/list.ts b/src/app/my-tasks/list.ts index e74ba2c..71d3a4a 100644 --- a/src/app/my-tasks/list.ts +++ b/src/app/my-tasks/list.ts @@ -1,6 +1,6 @@ export interface List { + id?: string; name: string; - owner: string; - isDefault: boolean; + owner: string; // @todo: i think not necessary in the future, should not be returned from server also } \ No newline at end of file diff --git a/src/app/my-tasks/main-toolbar/main-toolbar.component.ts b/src/app/my-tasks/main-toolbar/main-toolbar.component.ts index 3a6370b..bcaf965 100644 --- a/src/app/my-tasks/main-toolbar/main-toolbar.component.ts +++ b/src/app/my-tasks/main-toolbar/main-toolbar.component.ts @@ -31,7 +31,7 @@ export class MainToolbarComponent implements OnInit { this.isSearching = false; this.isTitleVisible = true; this.notifierService.taskListObs.subscribe(data => { - if (data.name) { + if (data?.name) { this.isSearching = true; } }); diff --git a/src/app/my-tasks/my-tasks.component.html b/src/app/my-tasks/my-tasks.component.html index 0989993..61e5434 100644 --- a/src/app/my-tasks/my-tasks.component.html +++ b/src/app/my-tasks/my-tasks.component.html @@ -3,8 +3,12 @@ + + + + - +
@@ -20,7 +24,7 @@ 'fixed': toolbarFixed }" > - +
diff --git a/src/app/my-tasks/my-tasks.component.ts b/src/app/my-tasks/my-tasks.component.ts index 6bff447..9592205 100644 --- a/src/app/my-tasks/my-tasks.component.ts +++ b/src/app/my-tasks/my-tasks.component.ts @@ -1,31 +1,44 @@ -import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatDrawer } from '@angular/material/sidenav'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import { DateUtil } from 'src/app/utilities/date-util'; +import { NotifierService } from '../services/notifier.service'; import { AddTaskSheetComponent } from './add-task-sheet/add-task-sheet.component'; +enum Sidenav { + LEFT, + RIGHT +} @Component({ selector: 'app-todo-list', templateUrl: './my-tasks.component.html', styleUrls: ['./my-tasks.component.scss'], }) -export class MyTasksComponent implements OnInit { +export class MyTasksComponent implements OnInit, OnDestroy { hideToolbar: boolean; toolbarFixed: boolean; noAnimation: boolean; scrollSubject = new Subject(); prevScrollVal: number; + Sidenav = Sidenav; + showFiltersSubscription: Subscription; - @ViewChild('drawer') drawer: MatDrawer; + @ViewChild('drawer') drawer: MatDrawer; // @todo: change to drawerRight + @ViewChild('drawerLeft') drawerLeft: MatDrawer; @ViewChild('headerDiv') header: ElementRef; constructor( - private bottomSheet: MatBottomSheet + private bottomSheet: MatBottomSheet, + private notifierService: NotifierService ) { } + ngOnDestroy(): void { + this.showFiltersSubscription.unsubscribe(); + } + ngOnInit(): void { DateUtil.initDate(); // @todo: Create a warning if DateUtil is not initialized this.hideToolbar = false; @@ -55,17 +68,29 @@ export class MyTasksComponent implements OnInit { } this.prevScrollVal = scrollTopVal; }); + this.showFiltersSubscription = this.notifierService.showFilters.subscribe(() => { + this.drawerLeft.open(); + }); } onScroll(event: any): void { this.scrollSubject.next(event.target.scrollTop); } - toggleSidenav(): void { - this.drawer.toggle(); + toggleSidenav(sidenav: Sidenav): void { + if (sidenav === Sidenav.LEFT) { + this.drawerLeft.toggle(); + } + else { + this.drawer.toggle(); + } } openAddTaskSheet(): void { const bottomSheetRef = this.bottomSheet.open(AddTaskSheetComponent); } + + closeFilterPanels(): void { + this.notifierService.filtersSidebarClosing.emit(); + } } diff --git a/src/app/my-tasks/my-tasks.module.ts b/src/app/my-tasks/my-tasks.module.ts index 07201bd..08fd8d4 100644 --- a/src/app/my-tasks/my-tasks.module.ts +++ b/src/app/my-tasks/my-tasks.module.ts @@ -38,6 +38,7 @@ import { SubToolbarComponent } from './sub-toolbar/sub-toolbar.component'; import { AddTaskSheetComponent } from './add-task-sheet/add-task-sheet.component'; import { EditTaskSheetComponent } from './edit-task-sheet/edit-task-sheet.component'; import { MyTasksRoutingModule } from './my-tasks-routing.module'; +import { TaskFiltersBarComponent } from './task-filters-bar/task-filters-bar.component'; @NgModule({ declarations: [ MyTasksComponent, @@ -56,6 +57,7 @@ import { MyTasksRoutingModule } from './my-tasks-routing.module'; SubToolbarComponent, AddTaskSheetComponent, EditTaskSheetComponent, + TaskFiltersBarComponent, ], imports: [ CommonModule, diff --git a/src/app/my-tasks/navbar/navbar.component.html b/src/app/my-tasks/navbar/navbar.component.html index 49d7e54..ff1aa03 100644 --- a/src/app/my-tasks/navbar/navbar.component.html +++ b/src/app/my-tasks/navbar/navbar.component.html @@ -1,41 +1,41 @@ -
- account_circle - -
{{user.fullName}}
-
@{{user.username}}
-
-
- - - - - Manage Account - - - - - -
- edit - Edit Account -
-
- -
- logout - Sign Out -
-
- -
- delete - Delete Account -
-
-
-
+
+ account_circle + +
{{user.fullName}}
+
@{{user.username}}
+
+
+ + + + + Manage Account + + + + +
+ edit + Edit Account +
+
+ +
+ logout + Sign Out +
+
+ +
+ delete + Delete Account +
+
+
+
+ +
+ + + + + + List + {{filterService.filters.list.name}} + + + + + +
+ add + New List +
+
+ + +
+ list + {{list.name | titlecase}} +
+
+
+
+
+ + + + + Due Date + {{this.filterService.filters.dueDate.displayText}} + + + + + + +
+ calendar_today + {{option.displayText}} +
+
+
+
+
+ + + + + Completed + {{this.filterService.filters.showCompleted ? 'Show' : 'Hide'}} + + + +
+ check_box + Show Completed + + + +
+
+
+ \ No newline at end of file diff --git a/src/app/my-tasks/task-filters-bar/task-filters-bar.component.scss b/src/app/my-tasks/task-filters-bar/task-filters-bar.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/my-tasks/task-filters-bar/task-filters-bar.component.spec.ts b/src/app/my-tasks/task-filters-bar/task-filters-bar.component.spec.ts new file mode 100644 index 0000000..a465e69 --- /dev/null +++ b/src/app/my-tasks/task-filters-bar/task-filters-bar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TaskFiltersBarComponent } from './task-filters-bar.component'; + +describe('TaskFiltersBarComponent', () => { + let component: TaskFiltersBarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TaskFiltersBarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TaskFiltersBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/my-tasks/task-filters-bar/task-filters-bar.component.ts b/src/app/my-tasks/task-filters-bar/task-filters-bar.component.ts new file mode 100644 index 0000000..c04769c --- /dev/null +++ b/src/app/my-tasks/task-filters-bar/task-filters-bar.component.ts @@ -0,0 +1,89 @@ +import { ThrowStmt } from '@angular/compiler'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { MatExpansionPanel } from '@angular/material/expansion'; +import { Subscription } from 'rxjs'; +import { AuthService } from 'src/app/services/auth.service'; +import { FiltersService } from 'src/app/services/filters.service'; +import { ListService } from 'src/app/services/list.service'; +import { NotifierService } from 'src/app/services/notifier.service'; + +@Component({ + selector: 'app-task-filters-bar', + templateUrl: './task-filters-bar.component.html', + styleUrls: ['./task-filters-bar.component.scss'] +}) +export class TaskFiltersBarComponent implements OnInit, OnDestroy { + + // listFilter = new FormControl(); + // dueDateFilter = new FormControl(); + dueDateOptions = [ + { code: 'default', displayText: 'All' }, + { code: 'overdue', displayText: 'Overdue' }, + { code: 'today', displayText: 'Today' }, + { code: 'tomorrow', displayText: 'Tomorrow' }, + { code: 'upcoming', displayText: 'Upcoming' }, + { code: 'unplanned', displayText: 'Unplanned' }, + ]; + listFilterClickedSubs: Subscription; + dueDateFilterClickedSubs: Subscription; + showCompletedFilterClickedSubs: Subscription; + filterSidebarClosingSubs: Subscription; + + @ViewChild('listPanel') listPanel: MatExpansionPanel; + @ViewChild('dueDatePanel') dueDatePanel: MatExpansionPanel; + @ViewChild('showCompletedPanel') showCompletedPanel: MatExpansionPanel; + + constructor( + public listService: ListService, + public authService: AuthService, + public filterService: FiltersService, + private notifierService: NotifierService + ) { } + + /* Angular Lifecycle */ + ngOnInit(): void { + this.listService.getLists(); + this.listFilterClickedSubs = this.notifierService.listFilterClicked.subscribe(() => { + this.listPanel.open(); + }); + this.dueDateFilterClickedSubs = this.notifierService.dueDateFilterClicked.subscribe(() => { + this.dueDatePanel.open(); + }); + this.showCompletedFilterClickedSubs = this.notifierService.showCompletedFilterClicked.subscribe(() => { + this.showCompletedPanel.open(); + }); + this.filterSidebarClosingSubs = this.notifierService.filtersSidebarClosing.subscribe(() => { + this.closePanels(); + }); + } + + ngOnDestroy(): void { + this.listFilterClickedSubs.unsubscribe(); + this.dueDateFilterClickedSubs.unsubscribe(); + this.showCompletedFilterClickedSubs.unsubscribe(); + this.filterSidebarClosingSubs.unsubscribe(); + } + + updateListFilter(listId: string, name: string) { + this.filterService.setListFilter(listId, name); + } + + updateDueDateFilter(dueDate: string, displayText: string) { + this.filterService.setDueDateFilter(dueDate, displayText); + } + + toggleShowCompleted() { + this.filterService.toggleShowCompleted(); + } + + showFilters() { + this.notifierService.showFilters.emit(); + } + + closePanels() { + this.listPanel.close(); + this.dueDatePanel.close(); + this.showCompletedPanel.close(); + } +} diff --git a/src/app/my-tasks/task-filters/task-filters.component.html b/src/app/my-tasks/task-filters/task-filters.component.html index 62d9dad..abcedf6 100644 --- a/src/app/my-tasks/task-filters/task-filters.component.html +++ b/src/app/my-tasks/task-filters/task-filters.component.html @@ -1,60 +1,35 @@
Filters    - + - List - + {{filtersService.filters.list.name | titlecase}} - {{filters.dueDateDisplay}} - + {{filtersService.filters.dueDate.displayText}} + cancel - Show Completed + ShowHide Completed + cancel
- - - - - - - - - - -
\ No newline at end of file diff --git a/src/app/my-tasks/task-filters/task-filters.component.ts b/src/app/my-tasks/task-filters/task-filters.component.ts index 6b95454..282c392 100644 --- a/src/app/my-tasks/task-filters/task-filters.component.ts +++ b/src/app/my-tasks/task-filters/task-filters.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; -import { Filters } from '../filters'; -import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet'; -import { MAT_BOTTOM_SHEET_DATA } from '@angular/material/bottom-sheet'; import { NotifierService, RepollData } from '../../services/notifier.service' +import { FiltersService } from 'src/app/services/filters.service'; @Component({ selector: 'app-task-filters', @@ -11,44 +9,49 @@ import { NotifierService, RepollData } from '../../services/notifier.service' }) export class TaskFiltersComponent implements OnInit { - filters: Filters; - @ViewChild('dueDateChip') dueDateChip: ElementRef; @ViewChild('showCompletedChip') showCompletedChip: ElementRef; @ViewChild('listChip') listChip: ElementRef; constructor( - private NotifierService: NotifierService + private notifierService: NotifierService, + public filtersService: FiltersService ) { } ngOnInit(): void { - this.filters = { - dueDate: 'default', - dueDateDisplay: 'Due Date', - showCompleted: false + } + + toggleCompleted() { + this.filtersService.filters.showCompleted = !this.filtersService.filters.showCompleted; + this.notifierService.notify({}); + } + + changeDueDate(code: string, displayText: string): void { + if (this.filtersService.filters.dueDate.code != code) { + this.filtersService.filters.dueDate.code = code; + this.filtersService.filters.dueDate.displayText = displayText; + this.notifierService.notify({}); } - this.NotifierService.notify({ filters: this.filters}); } + resetDueDateFilter() { + // @todo move this logic to filters service + this.changeDueDate('default', 'All'); + } - toggleCompleted() { - this.filters.showCompleted = !this.filters.showCompleted; - this.NotifierService.notify({}); + resetShowCompletedFilter() { + this.toggleCompleted(); } - onFocus(event: FocusEvent): void { - //event.preventDefault(); - this.listChip.nativeElement.blur(); - this.dueDateChip.nativeElement.blur(); - this.showCompletedChip.nativeElement.blur(); + selectListFilter() { + this.notifierService.listFilterClicked.emit(); } - changeDueDate(code: string, displayText: string): void { - if (this.filters.dueDate != code) { - this.filters.dueDate = code; - this.filters.dueDateDisplay = displayText; - //this.bottomSheetRef.dismiss(); - this.NotifierService.notify({}); - } + selectDueDateFilter() { + this.notifierService.dueDateFilterClicked.emit(); + } + + selectShowCompletedFilter() { + this.notifierService.showCompletedFilterClicked.emit(); } } \ No newline at end of file diff --git a/src/app/my-tasks/task-list/task-list.component.html b/src/app/my-tasks/task-list/task-list.component.html index bcaf7ef..55eccb6 100644 --- a/src/app/my-tasks/task-list/task-list.component.html +++ b/src/app/my-tasks/task-list/task-list.component.html @@ -12,7 +12,7 @@ class="task-item" [task]=task [index]="i" - [@slideOut]="tasks[i].completed && !filters.showCompleted ? 'completed' : ''" + [@slideOut]="tasks[i].completed && !filtersService.filters.showCompleted ? 'completed' : ''" (completed)="onTaskComplete($event)" (updated)="onTaskUpdate($event)" > diff --git a/src/app/my-tasks/task-list/task-list.component.ts b/src/app/my-tasks/task-list/task-list.component.ts index c77ddae..e319ca6 100644 --- a/src/app/my-tasks/task-list/task-list.component.ts +++ b/src/app/my-tasks/task-list/task-list.component.ts @@ -1,10 +1,10 @@ - import { animate, state, style, transition, trigger } from '@angular/animations'; +import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; +import { FiltersService } from 'src/app/services/filters.service'; import { Language } from 'src/app/utilities/language'; import { NotifierService } from '../../services/notifier.service' import { TaskService } from '../../services/task.service'; -import { Filters } from '../filters'; import { Task } from '../task'; @Component({ @@ -30,33 +30,24 @@ export class TaskListComponent implements OnInit, OnDestroy { tasks: Task[]; private repollSubscription: Subscription; nameSearch: String; - filters: Filters; processCount = Language.processCount; constructor( private taskService: TaskService, - private NotifierService: NotifierService, + private notifierService: NotifierService, + public filtersService: FiltersService ) { } ngOnInit(): void { - this.filters = { - dueDate: 'default', - dueDateDisplay: 'Any Date', - showCompleted: false - } this.nameSearch = null; - this.repollSubscription = this.NotifierService.taskListObs.subscribe(data => { - console.log('repoll', data); + this.repollSubscription = this.notifierService.taskListObs.subscribe(data => { if (data?.name || data?.name === '') { this.nameSearch = data.name; } - if (data?.filters) { - this.filters = data.filters; - } - this.tasks = null; this.getTasks(); }); + this.getTasks(); } ngOnDestroy() { @@ -67,8 +58,8 @@ export class TaskListComponent implements OnInit, OnDestroy { this.taskService.find( null, this.nameSearch, - this.filters.showCompleted ? null : false, - this.filters.dueDate === 'default' ? null : this.filters.dueDate + this.filtersService.filters.showCompleted ? null : false, + this.filtersService.filters.dueDate.code === 'default' ? null : this.filtersService.filters.dueDate.code ) .subscribe( data => { @@ -91,7 +82,7 @@ export class TaskListComponent implements OnInit, OnDestroy { } onTaskComplete(index: number): void { - if (!this.filters.showCompleted && index > -1) { + if (!this.filtersService.filters.showCompleted && index > -1) { this.tasks.splice(index, 1); } } @@ -101,8 +92,8 @@ export class TaskListComponent implements OnInit, OnDestroy { this.taskService.find( event.id, this.nameSearch, - this.filters.showCompleted ? null : false, - this.filters.dueDate === 'default' ? null : this.filters.dueDate + this.filtersService.filters.showCompleted ? null : false, + this.filtersService.filters.dueDate.code === 'default' ? null : this.filtersService.filters.dueDate.code ) .subscribe( data => { diff --git a/src/app/register/register.component.ts b/src/app/register/register.component.ts index b03c67e..55bbf57 100644 --- a/src/app/register/register.component.ts +++ b/src/app/register/register.component.ts @@ -6,6 +6,7 @@ import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService, RegisterPayload } from 'src/app/services/auth.service'; import { fade } from 'src/app/utilities/animations'; +import { SessionService } from '../services/session.service'; @Component({ selector: 'app-register', @@ -27,12 +28,17 @@ export class RegisterComponent implements OnInit { constructor( private auth: AuthService, private router: Router, - private snackBar: MatSnackBar + private snackBar: MatSnackBar, + private sessionService: SessionService ) { - if (this.auth.isLoggedIn()) { - this.snackBar.open('Redirecting to My Tasks...', null, {duration: 2000}); - this.router.navigate(['/my-tasks']); - } + this.sessionService.isLoggedIn().subscribe( + response => { + if (response) { + this.snackBar.open('Redirecting to My Tasks...', null, {duration: 2000}); + this.router.navigate(['/my-tasks']); + } + } + ); } ngOnInit(): void { @@ -63,9 +69,9 @@ export class RegisterComponent implements OnInit { fullName: this.fullName.value, nickname: this.nickname.value, }; - this.auth.register(payload).subscribe( + this.sessionService.register(payload).subscribe( response => { - console.log(response); + console.log('registration successful', response); this.snackBar.open('Signed up successfully', null, {duration: 2000}); this.router.navigate(['/welcome']); }, diff --git a/src/app/services/auth.guard.ts b/src/app/services/auth.guard.ts index dde0a1e..9f6875d 100644 --- a/src/app/services/auth.guard.ts +++ b/src/app/services/auth.guard.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { last } from 'rxjs/operators'; -import { AuthService } from './auth.service'; +import { map } from 'rxjs/operators'; +// import { AuthService } from './auth.service'; +import { SessionService } from './session.service'; @Injectable({ providedIn: 'root' @@ -11,18 +12,25 @@ export class AuthGuard implements CanActivate { constructor( private router: Router, - private auth: AuthService + // private auth: AuthService, + private sessionService: SessionService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - if (!this.auth.isLoggedIn()) { - this.router.navigate(['/login']); - return false; - } else { - return true; - } + return this.sessionService.isLoggedIn().pipe( + map(response => { + console.log('auth guard session service id', this.sessionService.getId()); + console.log('isLoggedIn', response); + if (!response) { + this.router.navigate(['/login']); + return false; + } else { + return true; + } + }) + ); } } diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 2254e68..d25f147 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,19 +1,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { Router } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; - -export interface User { - id: string, - email: string, - username: string, - nickname?: string, - fullName?: string - exp: number, - iat: number -} +import { Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; export interface LoginPayload { username: string, @@ -28,12 +16,7 @@ export interface RegisterPayload { nickname: string } -export interface EditAccountPayload { - fullName: string, - nickname: string -} - -interface TokenResponse { +export interface TokenResponse { token: string; } @@ -44,12 +27,12 @@ const baseUrl = 'http://localhost:8080/api' }) // @todo: check if necessary to provide in root; otherwise, @Injectable() export class AuthService { private token: string; //@todo merge token and user - public user: User; + // private user: User; constructor( private http: HttpClient, - private router: Router, - private snackBar: MatSnackBar + // private sessionService: SessionService + // private userService: UserService // @todo fix circular dependency ) { } private saveToken(token: string): void { @@ -57,114 +40,42 @@ export class AuthService { this.token = token; } - private getToken(): string { + public getToken(): string { if (!this.token) { this.token = localStorage.getItem('mean-token'); } return this.token; } - private saveUser(user: User) { - console.log('saving user'); - localStorage.setItem('user', JSON.stringify(user)); - this.user = user; - } - - public getUser(): User { - if (!this.user) { - this.user = JSON.parse(localStorage.getItem('user')); - } - return this.user; - } - - private loadUserFromToken(token: string): User { - var payload = token.split('.')[1]; - payload = window.atob(payload); - console.log(JSON.parse(window.atob(token.split('.')[0]))) - // console.log(JSON.parse(window.atob(token.split('.')[2]))) - return JSON.parse(payload); - } - - private loadUserFromDB(user: User): Observable { - console.log(user); - return this.http.get(`${baseUrl}/user/${user.id}`, {headers: this.getAuthHeader()}).pipe( - map((result: User) => { - user.fullName = result.fullName; - user.username = result.username; - user.nickname = result.nickname; - user.email = result.email; - this.saveUser(user); - }) - ); - } - - public isLoggedIn(): boolean { - const user = this.getUser(); - console.log('user', user); - if (user) { - return user.exp > Date.now() / 1000; - } else { - return false; - } - } - public getAuthHeader(): HttpHeaders { - return new HttpHeaders() - .set('Authorization', `Bearer ${this.getToken()}`); + const token = this.getToken(); + if (token) + return new HttpHeaders().set('Authorization', `Bearer ${token}`); + else + return null; } public login(user: LoginPayload): Observable { - return this.http.post(`${baseUrl}/auth/login`, user).pipe( - switchMap((data: TokenResponse) => { - this.saveToken(data.token); - return this.loadUserFromDB(this.loadUserFromToken(data.token)); - }) - ); + return this.http.post(`${baseUrl}/auth/login`, user) + .pipe( + switchMap((data: TokenResponse) => { + this.saveToken(data.token); + return of(data); + }) + ); } public register(user: RegisterPayload): Observable { return this.http.post(`${baseUrl}/auth/register`, user).pipe( switchMap((data: TokenResponse) => { this.saveToken(data.token); - return this.loadUserFromDB(this.loadUserFromToken(data.token)); + return of(data); }) ); } - public editAccount(payload: EditAccountPayload): Observable { - const user: User = this.getUser(); - return this.http.put(`${baseUrl}/user/${user.id}`, payload, { headers: this.getAuthHeader()}).pipe( - switchMap(() => { - return this.loadUserFromDB(user); - }) - ); - } - - public deleteAccount(password: string): Observable { - const user: User = this.getUser(); - return this.http.request( - 'delete', - `${baseUrl}/user/${user.id}`, - { - body: { - username: user.username, - password: password - }, - headers: this.getAuthHeader() - } - ); - } - - public removeSession(): void { - this.token = ''; - this.user = null; + public removeToken(): void { + this.token = null; window.localStorage.removeItem('mean-token'); - window.localStorage.removeItem('user'); - } - - public logout(): void { - this.removeSession(); - this.snackBar.open('Signed out successfully', null, {duration: 1500}); - this.router.navigateByUrl('/'); } } \ No newline at end of file diff --git a/src/app/services/filters.service.spec.ts b/src/app/services/filters.service.spec.ts new file mode 100644 index 0000000..0c2fbb0 --- /dev/null +++ b/src/app/services/filters.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FiltersService } from './filters.service'; + +describe('FiltersService', () => { + let service: FiltersService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FiltersService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/filters.service.ts b/src/app/services/filters.service.ts new file mode 100644 index 0000000..1f467fe --- /dev/null +++ b/src/app/services/filters.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { Filters } from '../my-tasks/filters'; +import { NotifierService } from './notifier.service'; +import { SessionService } from './session.service'; + +@Injectable({ + providedIn: 'root' +}) +export class FiltersService { + + filters: Filters; + + constructor( + private notifierService: NotifierService, + private sessionService: SessionService + ) { + const user = this.sessionService.getUser(); + this.filters = { + list: { id: user.preferences.defaultList.id, name: user.preferences.defaultList.name}, // to prevent errors, create component that will check integrity of user + dueDate: { code: 'default', displayText: 'All' }, + showCompleted: false + } + } + + setListFilter(listId: string, name: string) { + this.filters.list = { + id: listId, + name: name + }; + this.notifierService.notify(); + } + + setDueDateFilter(code: string, displayText: string) { + this.filters.dueDate.code = code; + this.filters.dueDate.displayText = displayText; + this.notifierService.notify(); + } + + toggleShowCompleted() { + this.filters.showCompleted = !this.filters.showCompleted; + this.notifierService.notify(); + } +} diff --git a/src/app/services/list.service.ts b/src/app/services/list.service.ts index d34181c..c18f615 100644 --- a/src/app/services/list.service.ts +++ b/src/app/services/list.service.ts @@ -3,6 +3,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; import { List } from '../my-tasks/list' +import { MatSnackBar } from '@angular/material/snack-bar'; +import { finalize } from 'rxjs/operators'; const baseUrl = 'http://localhost:8080/api/lists' @@ -11,14 +13,35 @@ const baseUrl = 'http://localhost:8080/api/lists' }) export class ListService { + lists: List[] = []; + isLoading: boolean = false; + constructor( private http: HttpClient, - private auth: AuthService + private auth: AuthService, + private snackBar: MatSnackBar ) { } create(data: List): Observable { return this.http.post(baseUrl, data, { headers: this.auth.getAuthHeader()}); } + getLists(): void { + this.isLoading = true; + this.http.get(baseUrl, { headers: this.auth.getAuthHeader()}) + .pipe( + finalize(() => this.isLoading = false) + ) + .subscribe( + lists => { + this.lists = lists; + }, + error => { + console.log(error); + this.snackBar.open('An error occured while retrieving the lists. Please try again later.', null, {duration: 4000}); + } + ); + } + } diff --git a/src/app/services/notifier.service.ts b/src/app/services/notifier.service.ts index 53639a1..6de136a 100644 --- a/src/app/services/notifier.service.ts +++ b/src/app/services/notifier.service.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from '@angular/core'; import { Observable, ReplaySubject, Subject } from 'rxjs'; import { Filters } from '../my-tasks/filters'; import { Task } from '../my-tasks/task'; @@ -7,6 +8,7 @@ export class NotifierService { taskListSubject: Subject = new Subject(); taskListObs: Observable = this.taskListSubject.asObservable(); + /* Edit task */ taskEditNameSubject: ReplaySubject = new ReplaySubject(); taskNameChanged: Observable = this.taskEditNameSubject.asObservable(); taskEditDescSubject: ReplaySubject = new ReplaySubject(); @@ -16,12 +18,20 @@ export class NotifierService { taskIsDoneSubject: ReplaySubject = new ReplaySubject(); taskIsDoneChanged: Observable = this.taskIsDoneSubject.asObservable(); + /* Add task */ taskUpdatedSubject: Subject = new Subject(); taskUpdated: Observable = this.taskUpdatedSubject.asObservable(); taskAddedSubject: Subject = new Subject(); taskAdded: Observable = this.taskAddedSubject.asObservable(); - notify = (data: RepollData) => { + /* Filter tasks */ + showFilters: EventEmitter = new EventEmitter(); + listFilterClicked: EventEmitter = new EventEmitter(); + dueDateFilterClicked: EventEmitter = new EventEmitter(); + showCompletedFilterClicked: EventEmitter = new EventEmitter(); + filtersSidebarClosing: EventEmitter = new EventEmitter(); + + notify = (data?: RepollData) => { this.taskListSubject.next(data) } diff --git a/src/app/services/session.service.spec.ts b/src/app/services/session.service.spec.ts new file mode 100644 index 0000000..4238e14 --- /dev/null +++ b/src/app/services/session.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SessionService } from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/session.service.ts b/src/app/services/session.service.ts new file mode 100644 index 0000000..a3625d0 --- /dev/null +++ b/src/app/services/session.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { AuthService, LoginPayload, RegisterPayload, TokenResponse } from './auth.service'; +import { User, UserService } from './user.service'; + +@Injectable({ + providedIn: 'root' +}) +export class SessionService { + + private user: User; + public id = Math.random(); + + constructor( + private authService: AuthService, + private userService: UserService, + private router: Router, + private snackBar: MatSnackBar, + ) { + this.user = this.loadUserFromToken(this.authService.getToken()); + } + + public getId() { + return this.id; + } + + public loadUserFromToken(token: string): User { + if (!token) return null; + var payload = token.split('.')[1]; + payload = window.atob(payload); + return JSON.parse(payload); + } + + public loadUserFromDB(user: User): Observable { + return this.userService.get(user.id).pipe( + map((result: User) => { + this.user = Object.assign(this.user ?? {}, user, result); + }) + ); + } + + public getUser(): User { + return this.user; + } + + public removeUser() { + this.user = null; + } + + public reloadUser(): Observable { + console.log('reloading', this.user); + return this.userService.get(this.user.id).pipe( + map(user => this.user = Object.assign(this.user, user)) + ); + } + + public isLoggedIn(): Observable { + const user = this.getUser(); + console.log('user', user); + if (user && user.exp > Date.now() / 1000) { + return this.loadUserFromDB(user).pipe(map(() => true)); + } else { + return of(false); + } + } + + public login(payload: LoginPayload): Observable { + return this.authService.login(payload).pipe( + switchMap((response: TokenResponse) => { + return this.loadUserFromDB(this.loadUserFromToken(response.token)) + }) + ); + } + + public register(payload: RegisterPayload): Observable { + return this.authService.register(payload).pipe( + switchMap((data: TokenResponse) => { + return this.loadUserFromDB(this.loadUserFromToken(data.token)); + }) + ); + } + + public removeSession(): void { + this.removeUser(); + this.authService.removeToken(); + } + + public logout(): void { + this.removeSession(); + this.snackBar.open('Signed out successfully', null, {duration: 1500}); + this.router.navigateByUrl('/'); + } +} diff --git a/src/app/services/task.service.ts b/src/app/services/task.service.ts index 2f5acad..d4815d5 100644 --- a/src/app/services/task.service.ts +++ b/src/app/services/task.service.ts @@ -15,10 +15,6 @@ export class TaskService { private auth: AuthService ) { } - // getAll(): Observable { - // return this.http.get(baseUrl); - // } - get(id: String): Observable { return this.http.get(`${baseUrl}/${id}`, { headers: this.auth.getAuthHeader()}); } diff --git a/src/app/services/user.service.spec.ts b/src/app/services/user.service.spec.ts new file mode 100644 index 0000000..3f804c9 --- /dev/null +++ b/src/app/services/user.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts new file mode 100644 index 0000000..6e978d2 --- /dev/null +++ b/src/app/services/user.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { AuthService } from './auth.service'; +import { switchMap } from 'rxjs/operators'; + +const baseUrl = 'http://localhost:8080/api/user' + +export interface User { + id: string, + email: string, + username: string, + nickname?: string, + fullName?: string + exp: number, + iat: number, + preferences: UserPreferences +} + +export interface UserPreferences { + defaultList: { + id: string, + name: string + } +} + +export interface UpdateUserPayload { + fullName: string, + nickname: string +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor( + private http: HttpClient, + private auth: AuthService, + ) { } + + public get(id: string): Observable { + const url = `${baseUrl}/${id}`; + const options = {headers: this.auth.getAuthHeader()}; + return this.http.get( + url, + options + ); + } + + public update(payload: UpdateUserPayload, user: User): Observable { + const url = `${baseUrl}/${user.id}`; + const options = { headers: this.auth.getAuthHeader()}; + return this.http.put( + url, + payload, + options + ); + } + + public delete(password: string, user: User): Observable { + const method = 'delete'; + const url = `${baseUrl}/${user.id}`; + const body = { + username: user.username, + password: password + }; + const options = { + body: body, + headers: this.auth.getAuthHeader() + }; + return this.http.request( + method, + url, + options + ); + } + +} diff --git a/src/app/start/start.component.ts b/src/app/start/start.component.ts index 308a1ee..e1cb716 100644 --- a/src/app/start/start.component.ts +++ b/src/app/start/start.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../services/auth.service'; +import { SessionService } from '../services/session.service'; @Component({ selector: 'app-start', @@ -10,15 +11,20 @@ import { AuthService } from '../services/auth.service'; export class StartComponent implements OnInit { constructor( private auth: AuthService, - private router: Router + private router: Router, + private sessionService: SessionService ) { console.log('Verifying session...'); - if (this.auth.isLoggedIn()) { - this.router.navigate(['/my-tasks']); - } else { - this.router.navigate(['/login']) - } + this.sessionService.isLoggedIn().subscribe( + response => { + if (response) { + this.router.navigate(['/my-tasks']); + } else { + this.router.navigate(['/login']) + } + } + ); } ngOnInit(): void { diff --git a/src/app/welcome/welcome.component.ts b/src/app/welcome/welcome.component.ts index c53bce5..c01fdfc 100644 --- a/src/app/welcome/welcome.component.ts +++ b/src/app/welcome/welcome.component.ts @@ -3,9 +3,11 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; +import { switchMap } from 'rxjs/operators'; import { fade } from 'src/app/utilities/animations'; -import { AuthService, User } from '../services/auth.service'; import { ListService } from '../services/list.service'; +import { SessionService } from '../services/session.service'; +import { User } from '../services/user.service'; @Component({ selector: 'app-welcome', @@ -21,17 +23,17 @@ export class WelcomeComponent implements OnInit { isSaving: boolean; constructor( - private auth: AuthService, private listService: ListService, private router: Router, - private snackBar: MatSnackBar + private snackBar: MatSnackBar, + private sessionService: SessionService ) { } ngOnInit(): void { this.createListForm = new FormGroup({ 'listName': this.listNameFormCtrl }); - this.user = this.auth.getUser(); + this.user = this.sessionService.getUser(); } onSubmit(): void { @@ -39,6 +41,9 @@ export class WelcomeComponent implements OnInit { this.isSaving = true; this.listService .create(data) + .pipe( + switchMap(() => this.sessionService.reloadUser()) + ) .subscribe( response => { // @todo: prevent user from going back to welcome page by manuualy entering url diff --git a/src/styles.scss b/src/styles.scss index 810ec12..580707b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -34,6 +34,10 @@ body { flex: 1 1 auto; } +.flex-0 { + flex: 0; +} + .flex-column { flex-direction: column; } @@ -78,9 +82,9 @@ body { text-align: center; } -// .text-end { -// text-align: end; -// } +.text-end { + text-align: end; +} /* Appearance and Layouting */ @@ -700,8 +704,8 @@ body { } .action-typo { - font-size: 14px; - font-weight: 500; + font-size: 14px !important; + font-weight: 500 !important; } .mat-chip.mat-standard-chip {