Skip to content

Commit

Permalink
feat(RSS-ECOMM-2_09): add registration form (#116)
Browse files Browse the repository at this point in the history
* feat: add RegistrationForm component

* feat: add validation RegistrationForm

* feat: add new validators for RegistrationForm

* feat: add logic for selecting a country from a predefined list

* feat: add saving selected country in store

* feat: add validate postal code and country input fields

* fix: change description registration form
  • Loading branch information
Kleostro authored May 2, 2024
1 parent 0471530 commit 87d8622
Show file tree
Hide file tree
Showing 22 changed files with 1,319 additions and 26 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"autoprefixer": "^10.4.19",
"isomorphic-fetch": "^3.0.0",
"modern-normalize": "^2.0.0",
"postcode-validator": "^3.8.20",
"vite-plugin-checker": "^0.6.4",
"vite-plugin-image-optimizer": "^1.1.7",
"vite-plugin-svg-spriter": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/App/model/AppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AppModel {
const root = this.getHTML();
const loginPage = new LoginPageModel(root, this.router);
const mainPage = new MainPageModel(root);
const registrationPage = new RegistrationPageModel(root);
const registrationPage = new RegistrationPageModel(root, this.router);
const notFoundPage = new NotFoundPageModel(root);
const pages: Map<string, PageInterface> = new Map(
Object.entries({
Expand Down
15 changes: 7 additions & 8 deletions src/entities/InputField/model/InputFieldModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { InputFieldParams, InputFieldValidatorParams } from '@/shared/types/interfaces.ts';

import InputFieldValidatorModel from '@/features/InputFieldValidator/model/InputFieldValidatorModel.ts';
import { EVENT_NAMES, INPUT_TYPES } from '@/shared/constants/enums.ts';
import { EVENT_NAMES, INPUT_TYPES, PASSWORD_TEXT } from '@/shared/constants/enums.ts';

import InputFieldView from '../view/InputFieldView.ts';

Expand All @@ -20,7 +20,7 @@ class InputFieldModel {
this.setInputHandler();
}

this.setShowPasswordHandler();
this.setSwitchPasswordVisibilityHandler();
}

private inputHandler(): boolean {
Expand All @@ -44,22 +44,21 @@ class InputFieldModel {

private setInputHandler(): boolean {
const input = this.view.getInput().getHTML();
input.addEventListener(EVENT_NAMES.INPUT, () => {
this.inputHandler();
});
input.addEventListener(EVENT_NAMES.INPUT, () => this.inputHandler());

return true;
}

private setShowPasswordHandler(): boolean {
private setSwitchPasswordVisibilityHandler(): boolean {
const button = this.view.getShowPasswordButton().getHTML();
button.addEventListener(EVENT_NAMES.CLICK, () => this.showPasswordHandler());
button.addEventListener(EVENT_NAMES.CLICK, () => this.switchPasswordVisibilityHandler());
return true;
}

private showPasswordHandler(): boolean {
private switchPasswordVisibilityHandler(): boolean {
const input = this.view.getInput().getHTML();
input.type = input.type === INPUT_TYPES.PASSWORD ? INPUT_TYPES.TEXT : INPUT_TYPES.PASSWORD;
input.placeholder = input.type === INPUT_TYPES.PASSWORD ? PASSWORD_TEXT.HIDDEN : PASSWORD_TEXT.SHOWN;
this.view.switchPasswordButtonSVG(input.type);
return true;
}
Expand Down
3 changes: 2 additions & 1 deletion src/entities/InputField/view/InputFieldView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ class InputFieldView {
}

private createInput(inputParams: InputParams): InputModel {
const { autocomplete, id, placeholder, type } = inputParams;
const { autocomplete, id, lang, placeholder, type } = inputParams;
this.input = new InputModel({
autocomplete,
id,
lang: lang || '',
placeholder: placeholder || '',
type,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { InputFieldValidatorParams } from '@/shared/types/interfaces';

import getStore from '@/shared/Store/Store.ts';
import { COUNTRIES } from '@/shared/constants/enums.ts';
import { postcodeValidator } from 'postcode-validator';

class InputFieldValidatorModel {
private isValid: boolean;

Expand All @@ -10,6 +14,18 @@ class InputFieldValidatorModel {
this.isValid = isValid;
}

private checkMaxAge(value: string): boolean | string {
const today = new Date();
const birthDate = new Date(value);
const age = today.getFullYear() - birthDate.getFullYear();
if (this.validParams.validBirthday && age > this.validParams.validBirthday.maxAge) {
const errorMessage = `You must be at most ${this.validParams.validBirthday.maxAge} years old`;
return errorMessage;
}

return true;
}

private checkMaxLength(value: string): boolean | string {
if (this.validParams.maxLength && value.length > this.validParams.maxLength) {
const errorMessage = `Max length should not exceed ${this.validParams.maxLength}`;
Expand All @@ -19,6 +35,18 @@ class InputFieldValidatorModel {
return true;
}

private checkMinAge(value: string): boolean | string {
const today = new Date();
const birthDate = new Date(value);
const age = today.getFullYear() - birthDate.getFullYear();
if (this.validParams.validBirthday && age < this.validParams.validBirthday.minAge) {
const errorMessage = `You must be at least ${this.validParams.validBirthday.minAge} years old`;
return errorMessage;
}

return true;
}

private checkMinLength(value: string): boolean | string {
if (this.validParams.minLength && value.length < this.validParams.minLength) {
const errorMessage = `Min length should be at least ${this.validParams.minLength}`;
Expand Down Expand Up @@ -55,6 +83,26 @@ class InputFieldValidatorModel {
return true;
}

private checkValidAge(value: string): boolean | string {
if (this.validParams.validBirthday && !this.validParams.validBirthday.pattern.test(value)) {
const errorMessage = this.validParams.validBirthday.message;
return errorMessage;
}

return true;
}

private checkValidCountry(value: string): boolean | string {
if (this.validParams.validCountry) {
if (!Object.keys(COUNTRIES).find((countryCode) => countryCode === value)) {
const errorMessage = 'Invalid country';
return errorMessage;
}
}

return true;
}

private checkValidMail(value: string): boolean | string {
if (this.validParams.validMail && !this.validParams.validMail.pattern.test(value)) {
const errorMessage = this.validParams.validMail.message;
Expand All @@ -64,8 +112,27 @@ class InputFieldValidatorModel {
return true;
}

private checkValidPostalCode(value: string): boolean | string {
if (this.validParams.validPostalCode) {
const { registerFormCountry } = getStore().getState();

try {
const result = postcodeValidator(value, registerFormCountry);
if (!result) {
const errorMessage = 'Invalid postal code';
return errorMessage;
}
} catch (error) {
const errorMessage = "Sorry, we don't deliver to your region yet";
return errorMessage;
}
}

return true;
}

private checkWhitespace(value: string): boolean | string {
if (this.validParams.notWhitespace && !this.validParams.notWhitespace.pattern.test(value) && value.trim() !== '') {
if (this.validParams.notWhitespace && !this.validParams.notWhitespace.pattern.test(value)) {
const errorMessage = this.validParams.notWhitespace.message;
return errorMessage;
}
Expand All @@ -82,6 +149,11 @@ class InputFieldValidatorModel {
this.checkMaxLength(value),
this.checkRequiredSymbols(value),
this.checkValidMail(value),
this.checkValidAge(value),
this.checkMinAge(value),
this.checkMaxAge(value),
this.checkValidCountry(value),
this.checkValidPostalCode(value),
];

const errorMessages: string[] = [];
Expand Down
1 change: 1 addition & 0 deletions src/pages/LoginPage/model/LoginPageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class LoginPageModel implements PageInterface {
private switchPageVisibility(route: unknown): boolean {
if (route === PAGES_IDS.LOGIN_PAGE) {
this.view.show();
this.loginForm.getFirstInputField().getView().getInput().getHTML().focus();
} else {
this.view.hide();
return false;
Expand Down
4 changes: 4 additions & 0 deletions src/pages/MainPage/model/MainPageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class MainPageModel implements PageInterface {

constructor(parent: HTMLDivElement) {
this.view = new MainPageView(parent);
this.init();
}

private init(): void {
this.subscribeToEventMediator();
}

Expand Down
27 changes: 25 additions & 2 deletions src/pages/RegistrationPage/model/RegistrationPageModel.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import type RouterModel from '@/app/Router/model/RouterModel.ts';
import type { PageInterface } from '@/shared/types/interfaces.ts';

import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts';
import { MEDIATOR_EVENTS, PAGES_IDS } from '@/shared/constants/enums.ts';
import { EVENT_NAMES, MEDIATOR_EVENTS, PAGES_IDS } from '@/shared/constants/enums.ts';
import RegisterFormModel from '@/widgets/RegistrationForm/model/RegistrationFormModel.ts';

import RegistrationPageView from '../view/RegistrationPageView.ts';

class RegistrationPageModel implements PageInterface {
private eventMediator = EventMediatorModel.getInstance();

private registerForm = new RegisterFormModel();

private router: RouterModel;

private view: RegistrationPageView;

constructor(parent: HTMLDivElement) {
constructor(parent: HTMLDivElement, router: RouterModel) {
this.view = new RegistrationPageView(parent);
this.router = router;
this.init();
}

private init(): void {
this.view.getAuthWrapper().append(this.registerForm.getHTML());
this.subscribeToEventMediator();
this.loginLinkHandler();
}

private loginLinkHandler(): void {
const loginLink = this.view.getLoginLink();

loginLink.addEventListener(EVENT_NAMES.CLICK, (event) => {
event.preventDefault();
this.router.navigateTo(PAGES_IDS.LOGIN_PAGE);
});
}

private subscribeToEventMediator(): void {
Expand All @@ -22,6 +44,7 @@ class RegistrationPageModel implements PageInterface {
private switchPageVisibility(route: unknown): boolean {
if (route === PAGES_IDS.REGISTRATION_PAGE) {
this.view.show();
this.registerForm.getFirstInputField().getView().getInput().getHTML().focus();
} else {
this.view.hide();
return false;
Expand Down
79 changes: 78 additions & 1 deletion src/pages/RegistrationPage/view/RegistrationPageView.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,110 @@
import { TAG_NAMES } from '@/shared/constants/enums.ts';
import { PAGE_DESCRIPTION, PAGE_LINK_TEXT, PAGES_IDS, TAG_NAMES } from '@/shared/constants/enums.ts';
import createBaseElement from '@/shared/utils/createBaseElement.ts';

import REGISTRATION_PAGE_STYLES from './registrationPageView.module.scss';

class RegistrationPageView {
private authDescription: HTMLHeadingElement;

private authWrapper: HTMLDivElement;

private linksWrapper: HTMLDivElement;

private loginLink: HTMLAnchorElement;

private page: HTMLDivElement;

private parent: HTMLDivElement;

private registerSpan: HTMLSpanElement;

constructor(parent: HTMLDivElement) {
this.parent = parent;
this.registerSpan = this.createRegisterSpan();
this.loginLink = this.createLoginLink();
this.authDescription = this.createAuthDescription();
this.linksWrapper = this.createLinksWrapper();
this.authWrapper = this.createAuthWrapper();
this.page = this.createHTML();
}

private createAuthDescription(): HTMLHeadingElement {
this.authDescription = createBaseElement({
cssClasses: [REGISTRATION_PAGE_STYLES.authDescription],
innerContent: PAGE_DESCRIPTION.REGISTRATION,
tag: TAG_NAMES.H3,
});

return this.authDescription;
}

private createAuthWrapper(): HTMLDivElement {
this.authWrapper = createBaseElement({
cssClasses: [REGISTRATION_PAGE_STYLES.authWrapper],
tag: TAG_NAMES.DIV,
});

this.authWrapper.append(this.linksWrapper, this.authDescription);
return this.authWrapper;
}

private createHTML(): HTMLDivElement {
this.page = createBaseElement({
cssClasses: [REGISTRATION_PAGE_STYLES.registrationPage],
tag: TAG_NAMES.DIV,
});

this.page.append(this.authWrapper);
this.parent.append(this.page);

return this.page;
}

private createLinksWrapper(): HTMLDivElement {
this.linksWrapper = createBaseElement({
cssClasses: [REGISTRATION_PAGE_STYLES.linksWrapper],
tag: TAG_NAMES.DIV,
});

this.linksWrapper.append(this.loginLink, this.registerSpan);
return this.linksWrapper;
}

private createLoginLink(): HTMLAnchorElement {
this.loginLink = createBaseElement({
attributes: {
href: PAGES_IDS.LOGIN_PAGE,
},
cssClasses: [REGISTRATION_PAGE_STYLES.loginLink],
innerContent: PAGE_LINK_TEXT.LOGIN,
tag: TAG_NAMES.A,
});

return this.loginLink;
}

private createRegisterSpan(): HTMLSpanElement {
this.registerSpan = createBaseElement({
cssClasses: [REGISTRATION_PAGE_STYLES.registerSpan],
innerContent: PAGE_LINK_TEXT.REGISTRATION,
tag: TAG_NAMES.SPAN,
});

return this.registerSpan;
}

public getAuthWrapper(): HTMLDivElement {
return this.authWrapper;
}

public getHTML(): HTMLDivElement {
return this.page;
}

public getLoginLink(): HTMLAnchorElement {
return this.loginLink;
}

public hide(): boolean {
this.page.classList.add(REGISTRATION_PAGE_STYLES.registrationPage_hidden);
return true;
Expand Down
Loading

0 comments on commit 87d8622

Please sign in to comment.