diff --git a/roadmap.md b/roadmap.md index 3aa832c..f40c9f0 100644 --- a/roadmap.md +++ b/roadmap.md @@ -5,8 +5,10 @@ - Improve skier sprites when turning - Define new default tracks, records and ghosts - Improve online data list to match big items list -- Use mailing server - Define premium url & link +- Add verification step +- ✔️ ~~Add password management step~~ +- ✔️ ~~Use mailing server~~ - ✔️ ~~Implement async multiplayer~~ - ✔️ ~~Improve spectators dynamic and animation~~ - ✔️ ~~Rework skier dynamic~~ diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 26a2b65..6bbdeea 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -88,5 +88,17 @@ export const routes: Routes = [ data: { type: 'online' }, canActivate: [AuthGuard, AvailableGuard] }, + { + path: 'reset-password', + loadComponent: () => + import('./pages/reset-password/reset-password.component').then(m => m.ResetPasswordComponent), + canActivate: [AvailableGuard] + }, + { + path: 'reset-password/:token', + loadComponent: () => + import('./pages/reset-password/reset-password.component').then(m => m.ResetPasswordComponent), + canActivate: [AvailableGuard] + }, { path: '**', pathMatch: 'full', redirectTo: '' } ]; diff --git a/src/app/common/scss/components.scss b/src/app/common/scss/components.scss index 123b31a..e692125 100644 --- a/src/app/common/scss/components.scss +++ b/src/app/common/scss/components.scss @@ -17,6 +17,10 @@ border: 1px solid var(--color-primary); color: var(--color-primary); + &.placeholder { + margin-top: 1rem; + } + &.tertiary { border: 1px solid var(--color-tertiary); color: var(--color-tertiary); @@ -73,6 +77,11 @@ position: sticky; top: 0; background-color: var(--color-surface); + + &.placeholder { + color: var(--color-tertiary); + text-align: center; + } } .retro-subtitle { @@ -92,6 +101,10 @@ color: var(--color-primary); font-weight: 400; + &.placeholder { + text-align: center; + } + &.tertiary { color: var(--color-tertiary); } @@ -101,11 +114,28 @@ } } +.retro-hint { + font-size: var(--text-small); + color: var(--color-primary-light); + text-align: right; +} + .retro-link { color: var(--color-tertiary); font-weight: 500; } +.retro-placeholder-image { + display: flex; + padding: 2rem; + justify-content: center; + align-items: center; + img { + fill: var(--color-tertiary); + color: var(--color-tertiary); + } +} + .retro-placeholder { font-size: var(--text-small); padding: 1rem 0.5rem; diff --git a/src/app/common/services/auth.service.ts b/src/app/common/services/auth.service.ts index b829c2e..e74b5f8 100644 --- a/src/app/common/services/auth.service.ts +++ b/src/app/common/services/auth.service.ts @@ -58,4 +58,12 @@ export class AuthService { ? User.buildFromRecord(environment.pb.authStore.record as RecordModel) : null; } + + public sendResetPasswordMail(email: string): Promise { + return environment.pb.collection('users').requestPasswordReset(email); + } + + public changePassword(password: string, passwordConfirm: string, token: string): Promise { + return environment.pb.collection('users').confirmPasswordReset(token, password, passwordConfirm); + } } diff --git a/src/app/pages/login/login.component.html b/src/app/pages/login/login.component.html index 17d8972..7efe154 100644 --- a/src/app/pages/login/login.component.html +++ b/src/app/pages/login/login.component.html @@ -5,7 +5,9 @@
@if(!serverAvailable()) { -
Unfortunately online services are not available, please try again later.
+
+ Unfortunately online services are not available, please try again later. +
} @else {
Ride online
@@ -33,6 +35,11 @@ placeholder="6 to 16 char." />
+ @if(loginError()) {
{{ loginError() }}
} diff --git a/src/app/pages/login/login.component.ts b/src/app/pages/login/login.component.ts index de42c2f..fdd3077 100644 --- a/src/app/pages/login/login.component.ts +++ b/src/app/pages/login/login.component.ts @@ -25,7 +25,7 @@ interface RegisterForm extends LoginForm { name: FormControl; } -const emailValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { +export const emailValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { let email: string = control?.value; if (email?.length) { const regex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; diff --git a/src/app/pages/reset-password/reset-password.component.html b/src/app/pages/reset-password/reset-password.component.html new file mode 100644 index 0000000..dd55e03 --- /dev/null +++ b/src/app/pages/reset-password/reset-password.component.html @@ -0,0 +1,101 @@ + + + + + +
+ @if(token) { @if(passwordChanged && passwordChanged()) { +
+ +
+
Password updated
+
+ Password updated successfully! You're all set to get back on the + mountain and enjoy the ride. +
+ + } @else { +
Set your new password
+
+
+
Password
+ +
+
+
Confirm password
+ +
+ + @if(password.valid && passwordConfirm.valid && password.value !== + passwordConfirm.value ) { +
Passwords don’t match. Try again.
+ } @if(passwordChanged && !passwordChanged()) { +
+ Something went wrong while updating your password. Please try again. +
+ } +
+ } } @else { @if(mailSent()) { +
+ +
+
Mail sent
+
+ Check your inbox for a message from us. Follow the instructions in the + email to reset your password and get back on track! +
+ } @else { +
Reset your password
+
+ Forgot your password? No worries, we’ve got you covered! +
+
+ Simply enter your email below and click the button. You’ll receive an + email shortly with instructions to reset your password and create a new + one. +
+
+
+
Email
+ +
+ +
+ } } +
diff --git a/src/app/pages/reset-password/reset-password.component.scss b/src/app/pages/reset-password/reset-password.component.scss new file mode 100644 index 0000000..5865101 --- /dev/null +++ b/src/app/pages/reset-password/reset-password.component.scss @@ -0,0 +1,13 @@ +:host { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: 1rem; + + .form-container { + padding: 1rem 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/src/app/pages/reset-password/reset-password.component.ts b/src/app/pages/reset-password/reset-password.component.ts new file mode 100644 index 0000000..280e163 --- /dev/null +++ b/src/app/pages/reset-password/reset-password.component.ts @@ -0,0 +1,56 @@ +import { ChangeDetectionStrategy, Component, inject, signal, type Signal } from '@angular/core'; +import { ToolbarComponent } from '../../common/components/toolbar/toolbar.component'; +import { ButtonIconComponent } from '../../common/components/button-icon/button-icon.component'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { emailValidator } from '../login/login.component'; +import { AuthService } from '../../common/services/auth.service'; + +@Component({ + selector: 'app-reset-password', + imports: [ButtonIconComponent, ReactiveFormsModule, RouterLink, ToolbarComponent], + templateUrl: './reset-password.component.html', + styleUrl: './reset-password.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ResetPasswordComponent { + private authService = inject(AuthService); + private route = inject(ActivatedRoute); + + protected mailSent = signal(false); + protected passwordChanged?: Signal; + protected email = new FormControl('', [Validators.required, emailValidator]); + protected password = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(16)]); + protected passwordConfirm = new FormControl('', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(16) + ]); + protected readonly token = (this.route.snapshot.params as { token: string }).token; + + protected sendMail(): void { + if (this.email.valid) { + this.authService.sendResetPasswordMail(this.email.value!).then(success => { + if (success) { + this.mailSent.set(true); + this.email.reset(); + } + }); + } + } + + protected changePassword(): void { + if ( + this.token && + this.password.valid && + this.passwordConfirm.valid && + this.password.value === this.passwordConfirm.value + ) { + this.authService + .changePassword(this.password.value!, this.passwordConfirm.value!, this.token) + .then(result => { + this.passwordChanged = signal(result); + }); + } + } +} diff --git a/src/assets/icons/mailSent.svg b/src/assets/icons/mailSent.svg new file mode 100644 index 0000000..e94da05 --- /dev/null +++ b/src/assets/icons/mailSent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/password.svg b/src/assets/icons/password.svg new file mode 100644 index 0000000..700c85c --- /dev/null +++ b/src/assets/icons/password.svg @@ -0,0 +1 @@ + \ No newline at end of file