Initial Implementation of Help and My Profile Popup

This commit is contained in:
2023-08-26 05:48:08 -03:00
parent 1328ca7c4c
commit bd8719654c
18 changed files with 550 additions and 51 deletions

View File

@@ -26,13 +26,13 @@
</li> </li>
</ul> </ul>
<ul class="user-management" *ngIf="this.user"> <ul class="user-management" *ngIf="this.user">
<li class="dropdown-item"> <li class="dropdown-item" (click)="onMyProfileClicked()">
<div class="icon-box"> <div class="icon-box">
<fa-icon class="fas fa-user" [icon]="userIcon"></fa-icon> <fa-icon class="fas fa-user" [icon]="userIcon"></fa-icon>
</div> </div>
<p>My Profile</p> <p>My Profile</p>
</li> </li>
<li class="dropdown-item"> <li class="dropdown-item" (click)="onHelpClicked()">
<div class="icon-box"> <div class="icon-box">
<fa-icon class="fas fa-question-circle" [icon]="questionCircleIcon"></fa-icon> <fa-icon class="fas fa-question-circle" [icon]="questionCircleIcon"></fa-icon>
</div> </div>

View File

@@ -1,10 +1,12 @@
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {Component, ComponentRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewContainerRef} from '@angular/core';
import { faEdit, faQuestionCircle, faSignOutAlt, faUser } from '@fortawesome/free-solid-svg-icons'; import { faEdit, faQuestionCircle, faSignOutAlt, faUser } from '@fortawesome/free-solid-svg-icons';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AuthService } from 'src/app/shared/auth/auth.service'; import { AuthService } from 'src/app/shared/auth/auth.service';
import {User} from "../../shared/model/user/user.model"; import {User} from "../../shared/model/user/user.model";
import UserChecker from "../../shared/model/user/user.checker"; import UserChecker from "../../shared/model/user/user.checker";
import {HelpComponent} from "../header-popup/help/help.component";
import {MyProfileComponent} from "../header-popup/my-profile/my-profile.component";
@Component({ @Component({
selector: 'app-header-dropdown', selector: 'app-header-dropdown',
@@ -52,12 +54,17 @@ export class HeaderDropdownComponent implements OnInit, OnDestroy {
@Output() @Output()
signupPopupState: EventEmitter<boolean> = new EventEmitter(); signupPopupState: EventEmitter<boolean> = new EventEmitter();
constructor(private authService: AuthService) { } @Output()
helpPopupState: EventEmitter<boolean> = new EventEmitter();
@Output()
myProfilePopupState: EventEmitter<boolean> = new EventEmitter();
constructor(private viewContainerRef: ViewContainerRef, private authService: AuthService) { }
ngOnInit(): void { ngOnInit(): void {
this.userSubscription = this.authService.authSubject.subscribe( this.userSubscription = this.authService.authSubject.subscribe(
res => { res => {
console.log(UserChecker.test(res));
if (res && UserChecker.test(res)) { if (res && UserChecker.test(res)) {
this.user = <User>res; this.user = <User>res;
} else { } else {
@@ -87,8 +94,15 @@ export class HeaderDropdownComponent implements OnInit, OnDestroy {
this.signupPopupState.emit(true); this.signupPopupState.emit(true);
} }
onMyProfileClicked() {
this.myProfilePopupState.emit(true);
}
onHelpClicked() {
this.helpPopupState.emit(true);
}
onLogout() { onLogout() {
this.authService.logout(); this.authService.logout();
} }
} }

View File

@@ -0,0 +1,7 @@
.help-container {
max-width: 400px;
}
p {
color: #555555;
}

View File

@@ -0,0 +1,28 @@
<app-popup [state]="state"
(stateChange)="onStateChange($event)"
[ignoreClickOutside]="ignoreClickOutside">
<div class="help-container container m-0 overflow-hidden">
<p>
This is a simple example project to demonstrate
User Authentication and Authorization using
<a href="https://spring.io/projects/spring-security" target="_blank">Spring Security</a>
and
<a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/" target="_blank">OAuth2</a>.
<br/><br/>
The only data stored is your email address, username and name.
This data is stored in a database and is used to authenticate you
and will not be used for any other purpose.
<br/><br/>
All data can be deleted by clicking the "Delete Account" button
on the "My Profile" option.
</p>
</div>
</app-popup>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HelpComponent } from './help.component';
describe('HelpComponent', () => {
let component: HelpComponent;
let fixture: ComponentFixture<HelpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HelpComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(HelpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'app-help',
templateUrl: './help.component.html',
styleUrls: ['./help.component.css']
})
export class HelpComponent {
@Input()
state: boolean = false;
@Input()
ignoreClickOutside!: HTMLDivElement[];
@Output()
stateChange = new EventEmitter<boolean>();
constructor() { }
onStateChange(state: boolean) {
this.stateChange.emit(state);
}
}

View File

@@ -0,0 +1,98 @@
* {
margin: 0;
}
.profile-container .row {
display: flex;
flex-direction: column;
justify-content: center;
}
.profile-container {
justify-content: space-around;
display: flex !important;
flex-direction: column;
align-content: center;
align-items: center;
height: 200px;
max-width: 400px;
}
.separator-line {
justify-content: center;
align-content: center;
align-items: center;
margin: 30px 0;
display: flex;
width: 100%;
}
.line {
width: 100%;
border-bottom: 2px solid #80808076;
border-radius: 50px;
}
.profile-container button {
text-decoration: none;
border-radius: 8px;
color: #ffffff;
font-weight: 500;
font-size: 16px;
border: none;
height: 50px;
width: 150px;
}
.picture-btn {
background-color: rgba(118, 118, 118, 0.7) !important;
}
.delete-btn {
background-color: rgba(216, 41, 28, 0.7) !important;
}
@media (min-width:767px) {
.profile-container {
all: unset;
justify-content: space-around;
width: 600px;
align-items: center;
display: flex;
height: 200px;
}
.profile-container .row {
all: unset;
display: flex;
height: 200px;
}
.btn-container {
justify-content: center;
align-content: center;
align-items: center;
display: flex;
}
.separator-line {
all: unset;
justify-content: center;
align-content: center;
align-items: center;
margin: 0px 60px;
display: flex;
height: 100%;
}
.line {
all: unset;
border-right: 2px solid #80808076;
border-radius: 50px;
height: 100%;
}
}

View File

@@ -0,0 +1,42 @@
<app-popup [state]="state"
(stateChange)="onStateChange($event)"
[ignoreClickOutside]="ignoreClickOutside">
<div class="container m-0 overflow-hidden"
[@resizeContainerForErrorMessage]="hideErrorMessage()">
<app-error-box [errorMessage]="errorMessage"
[@showErrorMessage]="showErrorMessage()">
</app-error-box>
<div class="container profile-container"
[@hideAuthContainer]="hideErrorMessage()"
(@hideAuthContainer.done)="hideAuthContainer($event)">
<div class="row">
<div class="btn-container">
<button class="picture-btn"
(click)="onAddProfilePicture()">
Add Profile Picture
</button>
</div>
<div class="separator-line">
<div class="line"></div>
</div>
<div class="btn-container">
<button class="delete-btn"
(click)="onDeleteAccount()">
Delete Account
</button>
</div>
</div>
</div>
</div>
</app-popup>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileComponent } from './my-profile.component';
describe('MyProfileComponent', () => {
let component: MyProfileComponent;
let fixture: ComponentFixture<MyProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MyProfileComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,162 @@
import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {AuthService} from "../../../shared/auth/auth.service";
import {User} from "../../../shared/model/user/user.model";
import {animate, animateChild, group, query, state, style, transition, trigger} from "@angular/animations";
import {MatIconRegistry} from "@angular/material/icon";
import {DomSanitizer} from "@angular/platform-browser";
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {ValidateNotEmptyValidator} from "../../../shared/validators/validate-not-empty.validator";
import {ValidatePasswordValidator} from "../../../shared/validators/validate-password.validator";
import {first, take} from "rxjs";
import UserChecker from "../../../shared/model/user/user.checker";
import HttpErrorChecker from "../../../shared/model/httpError/httpErrorChecker";
import {HttpError} from "../../../shared/model/httpError/httpError.model";
@Component({
selector: 'app-my-profile',
templateUrl: './my-profile.component.html',
styleUrls: ['./my-profile.component.css'],
animations: [
trigger('resizeContainerForErrorMessage', [
state('hide',
style({
height: '100px',
width: '320px',
})
),
transition(
'show => hide',
group([
query(
"@*",
animateChild(),
{ optional: true }
),
animate('1s ease')
])
)
]),
trigger('showErrorMessage', [
state('show',
style({
opacity: 1,
height: '100px',
width: '320px',
})
),
state('hide',
style({
opacity: 0,
height: '0px',
width: '0px',
})
),
transition(
'* => show',
animate(
'500ms ease-in'
)
),
]),
trigger('hideAuthContainer', [
state('hide',
style({
opacity: 0,
})
),
transition(
'show => hide',
group([
query(
"@*",
animateChild(),
{ optional: true }
),
animate(
'250ms ease-out'
)
])
)
]),
]
})
export class MyProfileComponent implements OnInit {
@Input()
state: boolean = false;
@Input()
user!: User | null;
@Input()
ignoreClickOutside!: HTMLDivElement[];
@Output()
stateChange = new EventEmitter<boolean>();
alterForm!: FormGroup;
errorMessage!: string | null;
isShowErrorMessage = false;
constructor(private authService: AuthService) {
}
ngOnInit(): void {
this.alterForm = new FormGroup({
'username': new FormControl(null, [Validators.required, ValidateNotEmptyValidator]),
'password': new FormControl(null, [Validators.required, ValidatePasswordValidator])
});
this.errorMessage = null;
}
onStateChange(state: boolean) {
this.stateChange.emit(state);
}
public showErrorMessage(): string {
if (this.isShowErrorMessage) {
return "show";
}
return "hide";
}
public hideErrorMessage(): string {
if (!!this.errorMessage) {
return "hide";
}
return "show";
}
public onDeleteAccount() {
this.authService.deleteAccount().subscribe({
next: (res) => {
if (res && UserChecker.test(res)) {
this.closePopup()
} if (HttpErrorChecker.test(res)) {
this.errorMessage = (<HttpError>res).details;
}
}
})
// this.authService.logout()
// this.onStateChange(false);
}
public onAddProfilePicture() {
this.authService.addProfilePicture()
}
hideAuthContainer(event: any) {
if (event.toState === "hide") {
event.element.style.display = "none";
this.isShowErrorMessage = true;
}
}
private closePopup() {
this.onStateChange(false);
}
}

View File

@@ -36,7 +36,6 @@ export class NavSliderComponent extends SliderItemComponent implements OnInit, O
ngOnInit(): void { ngOnInit(): void {
this.userSubscription = this.authService.authSubject.subscribe( this.userSubscription = this.authService.authSubject.subscribe(
res => { res => {
console.log(UserChecker.test(res));
if (res && UserChecker.test(res)) { if (res && UserChecker.test(res)) {
this.loggedUser = <User>res; this.loggedUser = <User>res;
} else { } else {

View File

@@ -16,13 +16,13 @@ export class UserSliderComponent extends SliderItemComponent implements OnInit {
{ {
name: "Login", name: "Login",
onClick: () => { onClick: () => {
this.loginOptionClicked(); this.onLoginOptionClicked();
} }
}, },
{ {
name: "Signup", name: "Signup",
onClick: () => { onClick: () => {
this.signupOptionClicked(); this.onSignUpOptionClick();
} }
} }
] ]
@@ -30,11 +30,15 @@ export class UserSliderComponent extends SliderItemComponent implements OnInit {
userOptions = [ userOptions = [
{ {
name: "My Profile", name: "My Profile",
onClick: () => {} onClick: () => {
this.onMyProfileClicked()
}
}, },
{ {
name: "Help", name: "Help",
onClick: () => {} onClick: () => {
this.onHelpClicked();
}
}, },
{ {
name: "Logout", name: "Logout",
@@ -48,12 +52,17 @@ export class UserSliderComponent extends SliderItemComponent implements OnInit {
authSubscription!: Subscription; authSubscription!: Subscription;
@Output()
loginPopupState: EventEmitter<boolean> = new EventEmitter();
@Output() @Output()
loginPopupState = new EventEmitter<boolean>(); signupPopupState: EventEmitter<boolean> = new EventEmitter();
@Output() @Output()
signupPopupState = new EventEmitter<boolean>(); helpPopupState: EventEmitter<boolean> = new EventEmitter();
@Output()
myProfilePopupState: EventEmitter<boolean> = new EventEmitter();
constructor(private authService: AuthService) { constructor(private authService: AuthService) {
super(); super();
@@ -72,14 +81,22 @@ export class UserSliderComponent extends SliderItemComponent implements OnInit {
) )
} }
loginOptionClicked(): void { onLoginOptionClicked(): void {
this.loginPopupState.emit(true); this.loginPopupState.emit(true);
} }
signupOptionClicked(): void { onSignUpOptionClick(): void {
this.signupPopupState.emit(true); this.signupPopupState.emit(true);
} }
onMyProfileClicked(): void {
this.myProfilePopupState.emit(true);
}
onHelpClicked(): void {
this.helpPopupState.emit(true);
}
onLogout() { onLogout() {
this.authService.logout(); this.authService.logout();
} }

View File

@@ -31,7 +31,9 @@
[ignoreClickOutside]="[profileBtn]" [ignoreClickOutside]="[profileBtn]"
[state]="profileDropdownState" [state]="profileDropdownState"
(loginPopupState)="loginPopupStateChange($event)" (loginPopupState)="loginPopupStateChange($event)"
(signupPopupState)="signupPopupStateChange($event)"> (signupPopupState)="signupPopupStateChange($event)"
(myProfilePopupState)="myProfilePopupStateChange($event)"
(helpPopupState)="helpPopupStateChange($event)">
</app-header-dropdown> </app-header-dropdown>
</div> </div>
<div class="burger-container" (click)="toogleNavSlider()"> <div class="burger-container" (click)="toogleNavSlider()">
@@ -58,7 +60,9 @@
<app-user-slider <app-user-slider
[state]="userSliderStatus" [state]="userSliderStatus"
(loginPopupState)="loginPopupStateChange($event)" (loginPopupState)="loginPopupStateChange($event)"
(signupPopupState)="signupPopupStateChange($event)"> (signupPopupState)="signupPopupStateChange($event)"
(myProfilePopupState)="myProfilePopupStateChange($event)"
(helpPopupState)="helpPopupStateChange($event)">
</app-user-slider> </app-user-slider>
</app-header-slider> </app-header-slider>
</div> </div>

View File

@@ -6,6 +6,8 @@ import {AuthService} from "../shared/auth/auth.service";
import UserChecker from "../shared/model/user/user.checker"; import UserChecker from "../shared/model/user/user.checker";
import {User} from "../shared/model/user/user.model"; import {User} from "../shared/model/user/user.model";
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
import {HelpComponent} from "./header-popup/help/help.component";
import {MyProfileComponent} from "./header-popup/my-profile/my-profile.component";
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@@ -40,12 +42,15 @@ export class HeaderComponent implements OnInit, OnDestroy {
private signupComponent!: ComponentRef<SignupComponent>; private signupComponent!: ComponentRef<SignupComponent>;
private myProfileComponent!: ComponentRef<MyProfileComponent>;
private helpComponent!: ComponentRef<HelpComponent>;
constructor(private viewContainerRef: ViewContainerRef, private authService: AuthService) { } constructor(private viewContainerRef: ViewContainerRef, private authService: AuthService) { }
ngOnInit(): void { ngOnInit(): void {
this.userSubscription = this.authService.authSubject.subscribe( this.userSubscription = this.authService.authSubject.subscribe(
res => { res => {
console.log(UserChecker.test(res));
if (res && UserChecker.test(res)) { if (res && UserChecker.test(res)) {
this.loggedUser = <User>res; this.loggedUser = <User>res;
} else { } else {
@@ -83,23 +88,36 @@ export class HeaderComponent implements OnInit, OnDestroy {
public closeDropdown(): void { public closeDropdown(): void {
this.profileDropdownState = false; this.profileDropdownState = false;
} }
public closeNavSlider(): void {
if (this.userSliderStatus) {
this.userSliderStatus = false;
} else {
this.navSliderStatus = false;
}
}
public loginPopupStateChange(state: boolean): void { public loginPopupStateChange(state: boolean): void {
if (state) { if (state) {
this.createLoginPopup(); this.createLoginPopup();
} else { } else {
this.closeLoginPopup(); this.closeLoginPopup();
} }
}
public signupPopupStateChange(state: boolean): void {
if (state) {
this.createSignupPopup();
} else {
this.closeSignupPopup();
}
}
myProfilePopupStateChange(state: boolean): void {
if (state) {
this.createMyProfilePopup();
} else {
this.closeMyProfilePopup();
}
}
helpPopupStateChange(state: boolean): void {
if (state) {
this.createHelpPopup();
} else {
this.closeHelpPopup();
}
} }
private createLoginPopup(): void { private createLoginPopup(): void {
@@ -148,6 +166,41 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.profileDropdownState = false; this.profileDropdownState = false;
} }
private createMyProfilePopup() {
this.myProfileComponent = this.viewContainerRef.createComponent(MyProfileComponent);
this.myProfileComponent.instance.state = true;
this.myProfileComponent.instance.user = this.loggedUser;
this.myProfileComponent.instance.stateChange.subscribe(
state => {
if (!state) {
this.closeMyProfilePopup()
}
}
);
this.navSliderStatus = false;
this.userSliderStatus = false;
this.profileDropdownState = false;
}
private createHelpPopup() {
this.helpComponent = this.viewContainerRef.createComponent(HelpComponent);
this.helpComponent.instance.state = true;
this.helpComponent.instance.stateChange.subscribe(
state => {
if (!state) {
this.closeHelpPopup()
}
}
);
this.navSliderStatus = false;
this.userSliderStatus = false;
this.profileDropdownState = false;
}
private closeLoginPopup() { private closeLoginPopup() {
this.loginComponent.destroy(); this.loginComponent.destroy();
} }
@@ -156,15 +209,11 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.signupComponent.destroy(); this.signupComponent.destroy();
} }
public signupPopupStateChange(state: boolean): void { private closeMyProfilePopup() {
this.signupPopupState = state; this.myProfileComponent.destroy();
if (state) {
this.createSignupPopup();
} else {
this.closeSignupPopup();
}
} }
private closeHelpPopup() {
this.helpComponent.destroy();
}
} }

View File

@@ -16,6 +16,8 @@ import { SignupComponent } from './header-popup/signup/signup.component';
import { CallbackComponent } from './header-popup/callback/callback.component'; import { CallbackComponent } from './header-popup/callback/callback.component';
import {MatIconModule} from '@angular/material/icon'; import {MatIconModule} from '@angular/material/icon';
import { ErrorBoxComponent } from './header-popup/error-box/error-box.component'; import { ErrorBoxComponent } from './header-popup/error-box/error-box.component';
import { HelpComponent } from './header-popup/help/help.component';
import { MyProfileComponent } from './header-popup/my-profile/my-profile.component';
@@ -30,6 +32,8 @@ import { ErrorBoxComponent } from './header-popup/error-box/error-box.component'
SignupComponent, SignupComponent,
CallbackComponent, CallbackComponent,
ErrorBoxComponent, ErrorBoxComponent,
HelpComponent,
MyProfileComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -21,7 +21,6 @@ export class AuthService {
readonly BACKEND_OAUTH_PATH = environment.backendOAuthPath; readonly BACKEND_OAUTH_PATH = environment.backendOAuthPath;
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
login(userAuthAtempt: User): void { login(userAuthAtempt: User): void {
this.validateUser(this.loginUser(userAuthAtempt)); this.validateUser(this.loginUser(userAuthAtempt));
} }
@@ -56,16 +55,15 @@ export class AuthService {
this.destroySessions().subscribe() this.destroySessions().subscribe()
} }
async getUserAccessToken(): Promise<Token | undefined> { deleteAccount() {
if (this.userAuthenticated) { return this.deleteAccountRequest().pipe(
if ((!this.userAuthenticated.accessToken && this.refreshAccessToken) || first()
(this.userAuthenticated.accessToken && this.userAuthenticated.accessToken.expirationDate < Date.now())) { );
this.userAuthenticated = <User>(await this.refreshAccessToken());
}
return this.userAuthenticated.accessToken;
} else return
} }
addProfilePicture() {
}
private loginUser(userAuthAtempt: User): Observable<User|any> { private loginUser(userAuthAtempt: User): Observable<User|any> {
let loginParams = new URLSearchParams(); let loginParams = new URLSearchParams();
@@ -155,6 +153,13 @@ export class AuthService {
); );
} }
private deleteAccountRequest() {
return this.http.delete(
this.BACKEND_PATH + `/user/delete/${this.userAuthenticated.id}`,
{ withCredentials: true }
);
}
private validateUser(userAuthAtempt: Observable<User>) { private validateUser(userAuthAtempt: Observable<User>) {
userAuthAtempt.pipe( userAuthAtempt.pipe(
catchError(error => { catchError(error => {
@@ -177,5 +182,4 @@ export class AuthService {
} }
}); });
} }
} }

View File

@@ -21,7 +21,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewEncapsulation }
animateChild(), animateChild(),
{ optional: true } { optional: true }
), ),
animate('125ms ease-in') animate('250ms ease-in')
]) ])
), ),
transition( transition(

View File

@@ -18,6 +18,7 @@
<meta name="apple-mobile-web-app-title" content="Hideyoshi"> <meta name="apple-mobile-web-app-title" content="Hideyoshi">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">
<link rel="preconnect" href="https://hideyoshi-portfolio-dev.s3.amazonaws.com">
</head> </head>
<body> <body>