diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index be190812d..a02808cd3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -15,6 +15,20 @@ export const routes: Routes = [ (mod) => mod.SignUpComponent, ), }, + { + path: 'forgot-password', + loadComponent: () => + import( + './features/auth/forgot-password/forgot-password.component' + ).then((mod) => mod.ForgotPasswordComponent), + }, + { + path: 'reset-password', + loadComponent: () => + import( + './features/auth/reset-password/reset-password.component' + ).then((mod) => mod.ResetPasswordComponent), + }, { path: 'home', loadComponent: () => diff --git a/src/app/core/components/header/header.component.html b/src/app/core/components/header/header.component.html index 89a623ba6..0ba10ea93 100644 --- a/src/app/core/components/header/header.component.html +++ b/src/app/core/components/header/header.component.html @@ -1,3 +1 @@ {{ authButtonText() }} - - diff --git a/src/app/core/components/sidenav/sidenav.component.html b/src/app/core/components/sidenav/sidenav.component.html index a02272279..b0ea7763b 100644 --- a/src/app/core/components/sidenav/sidenav.component.html +++ b/src/app/core/components/sidenav/sidenav.component.html @@ -10,12 +10,6 @@ [routerLinkActiveOptions]="{ exact: true }" > @if (item.icon) { - - - - - - } {{ item.label }} diff --git a/src/app/core/components/sidenav/sidenav.component.ts b/src/app/core/components/sidenav/sidenav.component.ts index fefc1756d..b9af5eee2 100644 --- a/src/app/core/components/sidenav/sidenav.component.ts +++ b/src/app/core/components/sidenav/sidenav.component.ts @@ -20,5 +20,10 @@ export class SidenavComponent { label: 'Support', icon: 'support', }, + { + path: '/donate', + label: 'Donate', + icon: 'donate', + }, ]; } diff --git a/src/app/core/helpers/types.helper.ts b/src/app/core/helpers/types.helper.ts new file mode 100644 index 000000000..9f24c487b --- /dev/null +++ b/src/app/core/helpers/types.helper.ts @@ -0,0 +1,20 @@ +export type StringOrNull = string | null; +export type StringOrNullOrUndefined = string | null | undefined; + +export type BooleanOrNull = boolean | null; +export type BooleanOrNullOrUndefined = boolean | null | undefined; + +export type NumberOrNull = number | null; +export type NumberOrNullOrUndefined = number | null | undefined; + +export type DateOrNull = Date | null; +export type DateOrNullOrUndefined = Date | null | undefined; + +export type ArrayOrNull = T[] | null; +export type ArrayOrNullOrUndefined = T[] | null | undefined; + +export type ObjectOrNull = T | null; +export type ObjectOrNullOrUndefined = T | null | undefined; + +export type Primitive = string | number | boolean | null | undefined; +export type NonNullablePrimitive = string | number | boolean; diff --git a/src/app/features/auth/forgot-password/forgot-password-form-group.type.ts b/src/app/features/auth/forgot-password/forgot-password-form-group.type.ts new file mode 100644 index 000000000..8c3625e0a --- /dev/null +++ b/src/app/features/auth/forgot-password/forgot-password-form-group.type.ts @@ -0,0 +1,3 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +export type ForgotPasswordFormGroupType = FormGroup<{ email: FormControl }>; diff --git a/src/app/features/auth/forgot-password/forgot-password.component.html b/src/app/features/auth/forgot-password/forgot-password.component.html new file mode 100644 index 000000000..7bcd028c1 --- /dev/null +++ b/src/app/features/auth/forgot-password/forgot-password.component.html @@ -0,0 +1,35 @@ + + Forgot Your Password? + Enter your email address and we'll send a link to reset your password + + + Email + + + + + + + @if (message()) { + + } + + diff --git a/src/app/features/auth/forgot-password/forgot-password.component.scss b/src/app/features/auth/forgot-password/forgot-password.component.scss new file mode 100644 index 000000000..ea6dc4f14 --- /dev/null +++ b/src/app/features/auth/forgot-password/forgot-password.component.scss @@ -0,0 +1,37 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +:host { + @include mix.flex-center; + flex: 1; + background: url("/assets/images/auth-background.png") center no-repeat; + background-size: cover; + + .forgot-password-container { + position: relative; + @include mix.flex-column; + color: var.$dark-blue-1; + max-width: 32rem; + flex: 1; + gap: 1.2rem; + padding: 2rem; + background: white; + border-radius: 0.6rem; + box-shadow: 0 2px 4px var.$grey-outline; + + h2 { + text-align: center; + } + + .email-input { + margin-bottom: 2.5rem; + } + + .message-container { + width: 100%; + position: absolute; + bottom: -4.5rem; + left: 0; + } + } +} diff --git a/src/app/features/auth/forgot-password/forgot-password.component.spec.ts b/src/app/features/auth/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 000000000..a9db5e20a --- /dev/null +++ b/src/app/features/auth/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ForgotPasswordComponent } from './forgot-password.component'; + +describe('ForgotPasswordComponent', () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForgotPasswordComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/auth/forgot-password/forgot-password.component.ts b/src/app/features/auth/forgot-password/forgot-password.component.ts new file mode 100644 index 000000000..e5937230b --- /dev/null +++ b/src/app/features/auth/forgot-password/forgot-password.component.ts @@ -0,0 +1,41 @@ +import { Component, inject, signal } from '@angular/core'; +import { InputText } from 'primeng/inputtext'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { Button } from 'primeng/button'; +import { MessageInfo } from './message-info.model'; +import { Message } from 'primeng/message'; +import { ForgotPasswordFormGroupType } from '@osf/features/auth/forgot-password/forgot-password-form-group.type'; + +@Component({ + selector: 'osf-forgot-password', + standalone: true, + imports: [InputText, ReactiveFormsModule, Button, Message], + templateUrl: './forgot-password.component.html', + styleUrl: './forgot-password.component.scss', +}) +export class ForgotPasswordComponent { + #fb = inject(FormBuilder); + forgotPasswordForm: ForgotPasswordFormGroupType = this.#fb.group({ + email: ['', [Validators.required, Validators.email]], + }); + message = signal(null); + + onSubmit(): void { + // TODO: Implement password reset logic + if (this.forgotPasswordForm.valid) { + this.message.set({ + severity: 'success', + content: 'Thanks. Check your email to reset your password.', + }); + + // this.message.set({ + // severity: 'error', + // content: 'Email not found.', + // }); + } + } + + onCloseMessage(): void { + this.message.set(null); + } +} diff --git a/src/app/features/auth/forgot-password/message-info.model.ts b/src/app/features/auth/forgot-password/message-info.model.ts new file mode 100644 index 000000000..b3de7a6f7 --- /dev/null +++ b/src/app/features/auth/forgot-password/message-info.model.ts @@ -0,0 +1,4 @@ +export interface MessageInfo { + severity: 'error' | 'warn' | 'success'; + content: string; +} diff --git a/src/app/features/auth/reset-password/reset-password-form-group.type.ts b/src/app/features/auth/reset-password/reset-password-form-group.type.ts new file mode 100644 index 000000000..4ecd42166 --- /dev/null +++ b/src/app/features/auth/reset-password/reset-password-form-group.type.ts @@ -0,0 +1,6 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +export type ResetPasswordFormGroupType = FormGroup<{ + newPassword: FormControl; + confirmNewPassword: FormControl; +}>; diff --git a/src/app/features/auth/reset-password/reset-password.component.html b/src/app/features/auth/reset-password/reset-password.component.html new file mode 100644 index 000000000..8ab27dd77 --- /dev/null +++ b/src/app/features/auth/reset-password/reset-password.component.html @@ -0,0 +1,60 @@ +@if (!isFormSubmitted()) { + + Reset Password + + + + New Password + + + + + + + Confirm New Password + + @if ( + resetPasswordForm.get("confirmNewPassword")?.errors?.[ + "passwordMismatch" + ] && resetPasswordForm.get("confirmNewPassword")?.touched + ) { + Passwords must match + } + + + + + +} @else { + + Thank You! + You have successfully reset your password + + +} diff --git a/src/app/features/auth/reset-password/reset-password.component.scss b/src/app/features/auth/reset-password/reset-password.component.scss new file mode 100644 index 000000000..da46608a0 --- /dev/null +++ b/src/app/features/auth/reset-password/reset-password.component.scss @@ -0,0 +1,54 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +:host { + @include mix.flex-center; + flex: 1; + background: url("/assets/images/auth-background.png") center no-repeat; + background-size: cover; + + .reset-password-container, + .message-container { + @include mix.flex-column; + color: var.$dark-blue-1; + max-width: 32rem; + flex: 1; + gap: 1.2rem; + padding: 2rem; + background: white; + border-radius: 0.6rem; + box-shadow: 0 2px 4px var.$grey-outline; + + h2, + .message-text { + text-align: center; + } + + .message-text { + margin-bottom: 1.5rem; + } + + form { + @include mix.flex-column; + flex: 1; + + .field { + margin-bottom: 1rem; + + .text-danger { + color: var.$red-1; + font-weight: 600; + } + } + + small { + color: var.$pr-blue-1; + font-weight: 600; + } + + .btn-full-width { + margin-top: 1.5rem; + } + } + } +} diff --git a/src/app/features/auth/reset-password/reset-password.component.spec.ts b/src/app/features/auth/reset-password/reset-password.component.spec.ts new file mode 100644 index 000000000..4df6bb807 --- /dev/null +++ b/src/app/features/auth/reset-password/reset-password.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResetPasswordComponent } from './reset-password.component'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResetPasswordComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/auth/reset-password/reset-password.component.ts b/src/app/features/auth/reset-password/reset-password.component.ts new file mode 100644 index 000000000..74b56119d --- /dev/null +++ b/src/app/features/auth/reset-password/reset-password.component.ts @@ -0,0 +1,49 @@ +import { Component, inject, signal } from '@angular/core'; +import { Button } from 'primeng/button'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Password } from 'primeng/password'; +import { RouterLink } from '@angular/router'; +import { + PASSWORD_REGEX, + passwordMatchValidator, +} from '../sign-up/sign-up.helper'; +import { PasswordInputHintComponent } from '@shared/components/password-input-hint/password-input-hint.component'; +import { ResetPasswordFormGroupType } from './reset-password-form-group.type'; + +@Component({ + selector: 'osf-reset-password', + standalone: true, + imports: [ + Button, + Password, + ReactiveFormsModule, + RouterLink, + PasswordInputHintComponent, + ], + templateUrl: './reset-password.component.html', + styleUrl: './reset-password.component.scss', +}) +export class ResetPasswordComponent { + #fb = inject(FormBuilder); + passwordRegex = PASSWORD_REGEX; + resetPasswordForm: ResetPasswordFormGroupType = this.#fb.group( + { + newPassword: [ + '', + [Validators.required, Validators.pattern(this.passwordRegex)], + ], + confirmNewPassword: ['', Validators.required], + }, + { + validators: passwordMatchValidator(), + }, + ); + isFormSubmitted = signal(false); + + onSubmit(): void { + if (this.resetPasswordForm.valid) { + // TODO: Implement password reset logic + this.isFormSubmitted.set(true); + } + } +} diff --git a/src/app/features/auth/sign-up/sign-up.component.html b/src/app/features/auth/sign-up/sign-up.component.html index 0e3c33912..270105c9e 100644 --- a/src/app/features/auth/sign-up/sign-up.component.html +++ b/src/app/features/auth/sign-up/sign-up.component.html @@ -1,113 +1,116 @@ - - Create A Free Account +@if (!isFormSubmitted()) { + + Create A Free Account - - - - + + + + - - - - - - - + + + + + + + - - or - + + or + - - - Full Name - - + + + Full Name + + - - Email - - + + Email + + - - Password - - @if ( - signUpForm.controls["password"].errors && - signUpForm.get("password")?.touched - ) { - - Your password needs to be at least 8 characters long, include both - lower- and upper-case characters, and have at least one number or - special character - - } - + + Password + + + - - Confirm Password - - @if ( - signUpForm.get("confirmPassword")?.errors?.["passwordMismatch"] && - signUpForm.get("confirmPassword")?.touched - ) { - Passwords must match - } - + + Confirm Password + + @if ( + signUpForm.get("confirmPassword")?.errors?.["passwordMismatch"] && + signUpForm.get("confirmPassword")?.touched + ) { + Passwords must match + } + - - - - I agree to the - Terms of Use and - Privacy Policy - + + + + I agree to the + Terms of Use and + Privacy Policy + + - - - - + + + +} @else { + + Registration successful + Check your email to confirm your account + +} diff --git a/src/app/features/auth/sign-up/sign-up.component.scss b/src/app/features/auth/sign-up/sign-up.component.scss index e2df4dd90..74c2c1bd8 100644 --- a/src/app/features/auth/sign-up/sign-up.component.scss +++ b/src/app/features/auth/sign-up/sign-up.component.scss @@ -4,20 +4,25 @@ :host { @include mix.flex-column-center; flex: 1; - background: url("/assets/images/sign-up-background.png") center no-repeat; + background: url("/assets/images/auth-background.png") center no-repeat; background-size: cover; - .sign-up-container { + .sign-up-container, + .message-container { @include mix.flex-column; - flex: 1; color: var.$dark-blue-1; width: 32rem; - margin: 6.5rem 0 2rem 0; + margin: 4.5rem 0 2rem 0; padding: 2rem; background: white; border-radius: 0.6rem; box-shadow: 0 2px 4px var.$grey-outline; + p { + text-align: center; + margin-top: 1rem; + } + h2 { text-align: center; } @@ -78,4 +83,12 @@ } } } + + .sign-up-container { + flex: 1; + } + + .message-container { + margin-top: 40vh; + } } diff --git a/src/app/features/auth/sign-up/sign-up.component.ts b/src/app/features/auth/sign-up/sign-up.component.ts index f17026a88..0b688a025 100644 --- a/src/app/features/auth/sign-up/sign-up.component.ts +++ b/src/app/features/auth/sign-up/sign-up.component.ts @@ -13,6 +13,7 @@ import { DividerModule } from 'primeng/divider'; import { CommonModule, NgOptimizedImage } from '@angular/common'; import { PASSWORD_REGEX, passwordMatchValidator } from './sign-up.helper'; import { Router, RouterLink } from '@angular/router'; +import { PasswordInputHintComponent } from '@shared/components/password-input-hint/password-input-hint.component'; @Component({ selector: 'osf-sign-up', @@ -27,6 +28,7 @@ import { Router, RouterLink } from '@angular/router'; DividerModule, NgOptimizedImage, RouterLink, + PasswordInputHintComponent, ], templateUrl: './sign-up.component.html', styleUrl: './sign-up.component.scss', diff --git a/src/app/shared/components/password-input-hint/password-input-hint.component.html b/src/app/shared/components/password-input-hint/password-input-hint.component.html new file mode 100644 index 000000000..90ca200bf --- /dev/null +++ b/src/app/shared/components/password-input-hint/password-input-hint.component.html @@ -0,0 +1,4 @@ + + Your password needs to be at least 8 characters long, include both lower- and + upper-case characters, and have at least one number or special character + diff --git a/src/app/shared/components/password-input-hint/password-input-hint.component.scss b/src/app/shared/components/password-input-hint/password-input-hint.component.scss new file mode 100644 index 000000000..39620e29f --- /dev/null +++ b/src/app/shared/components/password-input-hint/password-input-hint.component.scss @@ -0,0 +1,11 @@ +@use "assets/styles/variables" as var; + +:host { + color: var.$pr-blue-1; + font-weight: 600; + font-size: 0.85rem; + + .text-danger { + color: var.$red-1; + } +} diff --git a/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts b/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts new file mode 100644 index 000000000..0d9ae043f --- /dev/null +++ b/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PasswordInputHintComponent } from './password-input-hint.component'; + +describe('PasswordInputHintComponent', () => { + let component: PasswordInputHintComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordInputHintComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PasswordInputHintComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/password-input-hint/password-input-hint.component.ts b/src/app/shared/components/password-input-hint/password-input-hint.component.ts new file mode 100644 index 000000000..3d4310988 --- /dev/null +++ b/src/app/shared/components/password-input-hint/password-input-hint.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; +import { BooleanOrNullOrUndefined } from '@core/helpers/types.helper'; + +@Component({ + selector: 'osf-password-input-hint', + imports: [], + templateUrl: './password-input-hint.component.html', + styleUrl: './password-input-hint.component.scss', +}) +export class PasswordInputHintComponent { + isError = input(false); +} diff --git a/src/assets/images/sign-up-background.png b/src/assets/images/auth-background.png similarity index 100% rename from src/assets/images/sign-up-background.png rename to src/assets/images/auth-background.png diff --git a/src/assets/styles/_mixins.scss b/src/assets/styles/_mixins.scss index 87a4c6b78..2e409f7de 100644 --- a/src/assets/styles/_mixins.scss +++ b/src/assets/styles/_mixins.scss @@ -15,6 +15,11 @@ align-items: center; } +@mixin flex-justify-center { + display: flex; + justify-content: center; +} + @mixin flex-center-right { display: flex; justify-content: flex-end; diff --git a/src/assets/styles/overrides/checkbox.scss b/src/assets/styles/overrides/checkbox.scss index 23fc09a47..310bb0fa4 100644 --- a/src/assets/styles/overrides/checkbox.scss +++ b/src/assets/styles/overrides/checkbox.scss @@ -21,7 +21,7 @@ .p-checkbox-icon { color: white; - font-size: 20px; + font-size: 1.4rem; width: 0.7rem; } } diff --git a/src/assets/styles/overrides/input.scss b/src/assets/styles/overrides/input.scss index 01d1b1b0f..36dc4e274 100644 --- a/src/assets/styles/overrides/input.scss +++ b/src/assets/styles/overrides/input.scss @@ -5,10 +5,12 @@ width: 100%; background: white; border-color: var.$grey-2; + font-size: 16px; } .p-password { width: 100%; + font-size: 16px; } label { diff --git a/src/assets/styles/overrides/message.scss b/src/assets/styles/overrides/message.scss new file mode 100644 index 000000000..cdaad5d21 --- /dev/null +++ b/src/assets/styles/overrides/message.scss @@ -0,0 +1,19 @@ +@use "../variables" as var; + +.p-message { + border: none; + color: white; + height: 3rem; +} + +.p-message-success { + background-color: var.$green-1; +} + +.p-message-warn { + background-color: var.$yellow-1; +} + +.p-message-error { + background-color: var.$red-1; +} diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index ceeb4e83e..a1cbadb99 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -6,6 +6,7 @@ @use "./overrides/checkbox"; @use "./overrides/divider"; @use "./overrides/table"; +@use "./overrides/message"; @layer base, primeng, reset;
Enter your email address and we'll send a link to reset your password
You have successfully reset your password
Check your email to confirm your account