From 87d862228c676ce71c91caac96cb70d67feb085e Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Thu, 2 May 2024 16:41:48 +0300 Subject: [PATCH] feat(RSS-ECOMM-2_09): add registration form (#116) * 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 --- package-lock.json | 6 + package.json | 1 + src/app/App/model/AppModel.ts | 2 +- .../InputField/model/InputFieldModel.ts | 15 +- .../InputField/view/InputFieldView.ts | 3 +- .../model/InputFieldValidatorModel.ts | 74 ++- src/pages/LoginPage/model/LoginPageModel.ts | 1 + src/pages/MainPage/model/MainPageModel.ts | 4 + .../model/RegistrationPageModel.ts | 27 +- .../view/RegistrationPageView.ts | 79 ++- .../view/registrationPageView.module.scss | 85 +++ src/shared/Store/Store.ts | 5 +- src/shared/Store/actions.ts | 8 + src/shared/Store/reducer.ts | 6 + src/shared/constants/enums.ts | 489 +++++++++++++++++- src/shared/img/svg/calendar.svg | 3 + src/shared/types/interfaces.ts | 11 +- src/shared/utils/getCountryIndex.ts | 6 + src/widgets/RegistrationForm/.gitkeep | 0 .../model/RegistrationFormModel.ts | 98 ++++ .../view/RegistrationFormView.ts | 160 ++++++ .../view/registrationForm.module.scss | 262 ++++++++++ 22 files changed, 1319 insertions(+), 26 deletions(-) create mode 100644 src/shared/img/svg/calendar.svg create mode 100644 src/shared/utils/getCountryIndex.ts delete mode 100644 src/widgets/RegistrationForm/.gitkeep create mode 100644 src/widgets/RegistrationForm/model/RegistrationFormModel.ts create mode 100644 src/widgets/RegistrationForm/view/RegistrationFormView.ts create mode 100644 src/widgets/RegistrationForm/view/registrationForm.module.scss diff --git a/package-lock.json b/package-lock.json index a50d00ed..a891eba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,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", @@ -7791,6 +7792,11 @@ "node": ">= 0.4" } }, + "node_modules/postcode-validator": { + "version": "3.8.20", + "resolved": "https://registry.npmjs.org/postcode-validator/-/postcode-validator-3.8.20.tgz", + "integrity": "sha512-cvxPfYdtfzTPtQuqnsGQe8rObb9CcCJou93OhSY038yVDdL5aU2QjDQqgNtD5sFpaic+BYrEHAGEEA+WB/nHXg==" + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", diff --git a/package.json b/package.json index 6212fe4b..db5f649b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index c10af0e7..d7664952 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -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 = new Map( Object.entries({ diff --git a/src/entities/InputField/model/InputFieldModel.ts b/src/entities/InputField/model/InputFieldModel.ts index 8321c177..294b48fd 100644 --- a/src/entities/InputField/model/InputFieldModel.ts +++ b/src/entities/InputField/model/InputFieldModel.ts @@ -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'; @@ -20,7 +20,7 @@ class InputFieldModel { this.setInputHandler(); } - this.setShowPasswordHandler(); + this.setSwitchPasswordVisibilityHandler(); } private inputHandler(): boolean { @@ -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; } diff --git a/src/entities/InputField/view/InputFieldView.ts b/src/entities/InputField/view/InputFieldView.ts index 069de0c4..f31df1b9 100644 --- a/src/entities/InputField/view/InputFieldView.ts +++ b/src/entities/InputField/view/InputFieldView.ts @@ -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, }); diff --git a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts index 0ab94d8e..fec768ad 100644 --- a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts +++ b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts @@ -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; @@ -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}`; @@ -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}`; @@ -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; @@ -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; } @@ -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[] = []; diff --git a/src/pages/LoginPage/model/LoginPageModel.ts b/src/pages/LoginPage/model/LoginPageModel.ts index cf277f45..2a7e4283 100644 --- a/src/pages/LoginPage/model/LoginPageModel.ts +++ b/src/pages/LoginPage/model/LoginPageModel.ts @@ -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; diff --git a/src/pages/MainPage/model/MainPageModel.ts b/src/pages/MainPage/model/MainPageModel.ts index 0f07f18d..a8019df3 100644 --- a/src/pages/MainPage/model/MainPageModel.ts +++ b/src/pages/MainPage/model/MainPageModel.ts @@ -12,6 +12,10 @@ class MainPageModel implements PageInterface { constructor(parent: HTMLDivElement) { this.view = new MainPageView(parent); + this.init(); + } + + private init(): void { this.subscribeToEventMediator(); } diff --git a/src/pages/RegistrationPage/model/RegistrationPageModel.ts b/src/pages/RegistrationPage/model/RegistrationPageModel.ts index d4393ff5..647664f1 100644 --- a/src/pages/RegistrationPage/model/RegistrationPageModel.ts +++ b/src/pages/RegistrationPage/model/RegistrationPageModel.ts @@ -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 { @@ -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; diff --git a/src/pages/RegistrationPage/view/RegistrationPageView.ts b/src/pages/RegistrationPage/view/RegistrationPageView.ts index c615678a..15a3b4ff 100644 --- a/src/pages/RegistrationPage/view/RegistrationPageView.ts +++ b/src/pages/RegistrationPage/view/RegistrationPageView.ts @@ -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; diff --git a/src/pages/RegistrationPage/view/registrationPageView.module.scss b/src/pages/RegistrationPage/view/registrationPageView.module.scss index 12508f19..ebf5346a 100644 --- a/src/pages/RegistrationPage/view/registrationPageView.module.scss +++ b/src/pages/RegistrationPage/view/registrationPageView.module.scss @@ -7,3 +7,88 @@ display: none; } } + +.authWrapper { + margin: 0 auto; + border-bottom: 10px solid var(--steam-green-800); + padding: calc(var(--large-offset) / 2) var(--small-offset); + max-width: 500px; + background-color: var(--white); +} + +.linksWrapper { + position: relative; + display: flex; + align-self: center; + justify-content: space-between; + margin: 0 auto; + margin-bottom: calc(var(--large-offset) / 2); + margin-left: 35%; + max-width: 160px; + + &::after { + content: ''; + position: absolute; + right: calc(50% - 3px); + top: 50%; + width: 3px; + height: 16px; + background-color: var(--noble-gray-800); + transform: translate(calc(-50% - 14px), -50%); + } + + @media (max-width: 768px) { + margin: 0 auto; + margin-bottom: calc(var(--large-offset) / 2); + } +} + +.loginLink, +.registerSpan { + font: var(--medium-font); + letter-spacing: 1px; +} + +.registerSpan { + color: var(--steam-green-800); +} + +.loginLink { + position: relative; + color: var(--noble-gray-800); + transition: color 0.2s; + + &::after { + content: ''; + position: absolute; + left: 0; + bottom: -4px; + width: 100%; + height: 2px; + background-color: currentcolor; + opacity: 0; + transform: scaleX(0); + transform-origin: center; + transition: + transform 0.2s, + opacity 0.2s; + } + + @media (hover: hover) { + &:hover { + color: var(--steam-green-800); + + &::after { + opacity: 1; + transform: scaleX(1); + } + } + } +} + +.authDescription { + margin-bottom: var(--extra-small-offset); + font: var(--regular-font); + letter-spacing: 1px; + text-align: center; +} diff --git a/src/shared/Store/Store.ts b/src/shared/Store/Store.ts index 91c82218..c8b04516 100644 --- a/src/shared/Store/Store.ts +++ b/src/shared/Store/Store.ts @@ -5,12 +5,9 @@ import type { Action, State } from './reducer.ts'; import type { Reducer, ReduxStore } from './types'; +import { initialState } from '../constants/enums.ts'; import { rootReducer } from './reducer.ts'; -const initialState: State = { - currentUser: null, -}; - export class Store implements ReduxStore { private listeners: VoidFunction[] = []; diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index abcc798b..0a7a1ff7 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -1,6 +1,7 @@ /* eslint-disable import/prefer-default-export */ const ACTION = { SET_CURRENT_USER: 'setCurrentUser', + SET_REGISTER_FORM_COUNTRY: 'setRegisterFormCountry', } as const; type ActionType = (typeof ACTION)[keyof typeof ACTION]; @@ -16,3 +17,10 @@ export const setCurrentUser = ( payload: value, type: ACTION.SET_CURRENT_USER, }); + +export const setRegisterFormCountry = ( + value: string, +): ActionWithPayload => ({ + payload: value, + type: ACTION.SET_REGISTER_FORM_COUNTRY, +}); diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index 5e30201d..1cf5b3f7 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -4,6 +4,7 @@ import type { Reducer } from './types.ts'; export interface State { currentUser: null | string; + registerFormCountry: string; } type InferValueTypes = T extends { [key: string]: infer U } ? U : never; @@ -16,6 +17,11 @@ export const rootReducer: Reducer = (state: State, action: Action ...state, currentUser: action.payload, }; + case 'setRegisterFormCountry': + return { + ...state, + registerFormCountry: action.payload, + }; default: return state; } diff --git a/src/shared/constants/enums.ts b/src/shared/constants/enums.ts index 12148274..c3aceabd 100644 --- a/src/shared/constants/enums.ts +++ b/src/shared/constants/enums.ts @@ -1,3 +1,10 @@ +import type { State } from '../Store/reducer'; + +export const initialState: State = { + currentUser: null, + registerFormCountry: '', +}; + export const INPUT_TYPES = { COLOR: 'color', DATE: 'date', @@ -117,12 +124,12 @@ export const MEDIATOR_EVENTS = { export const LOGIN_FORM_EMAIL_FIELD_PARAMS = { inputParams: { autocomplete: 'off', - id: 'email', + id: 'login_email', placeholder: 'user@example.com', type: 'text', }, labelParams: { - for: 'email', + for: 'login_email', text: '', }, } as const; @@ -130,12 +137,12 @@ export const LOGIN_FORM_EMAIL_FIELD_PARAMS = { export const LOGIN_FORM_PASSWORD_FIELD_PARAMS = { inputParams: { autocomplete: 'off', - id: 'password', + id: 'login_password', placeholder: '***********', type: 'password', }, labelParams: { - for: 'password', + for: 'login_password', text: '', }, } as const; @@ -143,7 +150,7 @@ export const LOGIN_FORM_PASSWORD_FIELD_PARAMS = { export const LOGIN_FORM_INPUT_FIELD_PARAMS = [LOGIN_FORM_EMAIL_FIELD_PARAMS, LOGIN_FORM_PASSWORD_FIELD_PARAMS]; const LOGIN_FORM_EMAIL_FIELD_VALIDATE_PARAMS = { - key: 'email', + key: 'login_email', notWhitespace: { message: 'Email must not contain whitespaces', pattern: /^\S+$/, @@ -156,7 +163,7 @@ const LOGIN_FORM_EMAIL_FIELD_VALIDATE_PARAMS = { } as const; const LOGIN_FORM_PASSWORD_FIELD_VALIDATE_PARAMS = { - key: 'password', + key: 'login_password', minLength: 8, notWhitespace: { message: 'Password must not contain whitespaces', @@ -192,5 +199,473 @@ export const PAGE_LINK_TEXT = { } as const; export const PAGE_DESCRIPTION = { - LOGIN: 'Enter your email and password to register.', + LOGIN: 'Enter your email and password to login.', + REGISTRATION: 'Enter your information to register.', +} as const; + +export const REGISTRATION_FORM_EMAIL_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'registration_email', + placeholder: 'user@example.com', + type: 'text', + }, + labelParams: { + for: 'registration_email', + text: 'Email', + }, +} as const; + +export const REGISTRATION_FORM_PASSWORD_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'registration_password', + placeholder: '***********', + type: 'password', + }, + labelParams: { + for: 'registration_password', + text: 'Password', + }, +} as const; + +export const REGISTRATION_FORM_FIRST_NAME_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'firstName', + placeholder: 'John', + type: 'text', + }, + labelParams: { + for: 'firstName', + text: 'First name', + }, +} as const; + +export const REGISTRATION_FORM_LAST_NAME_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'lastName', + placeholder: 'Doe', + type: 'text', + }, + labelParams: { + for: 'lastName', + text: 'Last name', + }, +} as const; + +export const REGISTRATION_FORM_BIRTHDAY_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'birthday', + lang: 'en', + placeholder: '01.01.2000', + type: 'date', + }, + labelParams: { + for: 'birthday', + text: 'Date of Birth', + }, +} as const; + +export const REGISTRATION_FORM_STREET_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'street', + placeholder: '595 Hornby St. 5th Floor', + type: 'text', + }, + labelParams: { + for: 'street', + text: 'Address', + }, +} as const; + +export const REGISTRATION_FORM_CITY_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'city', + placeholder: 'Vancouver', + type: 'text', + }, + labelParams: { + for: 'city', + text: 'City', + }, +} as const; + +export const REGISTRATION_FORM_COUNTRY_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'country', + placeholder: 'Canada', + type: 'text', + }, + labelParams: { + for: 'country', + text: 'Country', + }, +} as const; + +export const REGISTRATION_FORM_POSTAL_CODE_FIELD_PARAMS = { + inputParams: { + autocomplete: 'off', + id: 'postalCode', + placeholder: 'A1B 2C3', + type: 'text', + }, + labelParams: { + for: 'postalCode', + text: 'Postal code', + }, +} as const; + +export const REGISTRATION_FORM_INPUT_FIELD_PARAMS = [ + REGISTRATION_FORM_EMAIL_FIELD_PARAMS, + REGISTRATION_FORM_PASSWORD_FIELD_PARAMS, + REGISTRATION_FORM_FIRST_NAME_FIELD_PARAMS, + REGISTRATION_FORM_LAST_NAME_FIELD_PARAMS, + REGISTRATION_FORM_BIRTHDAY_FIELD_PARAMS, + REGISTRATION_FORM_STREET_FIELD_PARAMS, + REGISTRATION_FORM_CITY_FIELD_PARAMS, + REGISTRATION_FORM_COUNTRY_FIELD_PARAMS, + REGISTRATION_FORM_POSTAL_CODE_FIELD_PARAMS, +]; + +const REGISTRATION_FORM_EMAIL_FIELD_VALIDATE_PARAMS = { + key: 'registration_email', + notWhitespace: { + message: 'Email must not contain whitespaces', + pattern: /^\S+$/, + }, + required: true, + validMail: { + message: 'Enter correct email (user@example.com)', + pattern: /^([a-z0-9_-]+\.)*[a-z0-9_-]+@[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]{2,6}$/, + }, +} as const; + +const REGISTRATION_FORM_PASSWORD_FIELD_VALIDATE_PARAMS = { + key: 'registration_password', + minLength: 8, + notWhitespace: { + message: 'Password must not contain whitespaces', + pattern: /^\S+$/, + }, + required: true, + requiredSymbols: { + message: 'Password must contain English letters, at least 1 letter in upper and lower case and at least 1 number', + pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+/, + }, +} as const; + +const REGISTRATION_FORM_FIRST_NAME_FIELD_VALIDATE_PARAMS = { + key: 'firstName', + minLength: 1, + notSpecialSymbols: { + message: 'First name must contain only letters', + pattern: /^[a-zA-Z]*$/, + }, + notWhitespace: { + message: 'First name must not contain whitespaces', + pattern: /^\S+$/, + }, + required: true, +} as const; + +const REGISTRATION_FORM_LAST_NAME_FIELD_VALIDATE_PARAMS = { + key: 'lastName', + minLength: 1, + notSpecialSymbols: { + message: 'Last name must contain only letters', + pattern: /^[a-zA-Z]*$/, + }, + notWhitespace: { + message: 'Last name must not contain whitespaces', + pattern: /^\S+$/, + }, + required: true, +} as const; + +const REGISTRATION_FORM_BIRTHDAY_FIELD_VALIDATE_PARAMS = { + key: 'birthday', + required: true, + validBirthday: { + maxAge: 120, + message: 'Enter correct birthday (01.01.2000)', + minAge: 18, + pattern: /^\d{4}-\d{2}-\d{2}$/, + }, +} as const; + +export const REGISTRATION_FORM_STREET_FIELD_VALIDATE_PARAMS = { + key: 'street', + minLength: 1, + required: true, +}; + +export const REGISTRATION_FORM_CITY_FIELD_VALIDATE_PARAMS = { + key: 'city', + minLength: 1, + notSpecialSymbols: { + message: 'City must contain only letters', + pattern: /^[a-zA-Z]*$/, + }, + + required: true, +}; + +export const REGISTRATION_FORM_COUNTRY_FIELD_VALIDATE_PARAMS = { + key: 'country', + required: true, + validCountry: true, +}; + +export const REGISTRATION_FORM_POSTAL_CODE_FIELD_VALIDATE_PARAMS = { + key: 'postalCode', + required: true, + validPostalCode: true, +}; + +export const REGISTRATION_FORM_INPUT_FIELD_VALIDATION_PARAMS = [ + REGISTRATION_FORM_EMAIL_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_PASSWORD_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_FIRST_NAME_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_LAST_NAME_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_BIRTHDAY_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_STREET_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_CITY_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_COUNTRY_FIELD_VALIDATE_PARAMS, + REGISTRATION_FORM_POSTAL_CODE_FIELD_VALIDATE_PARAMS, +]; + +export const PASSWORD_TEXT = { + HIDDEN: '********', + SHOWN: 'Password123', +}; + +export const COUNTRIES: Record = { + 'Bonaire, Sint Eustatius and Saba': 'BQ', + 'Bosnia and Herzegovina': 'BA', + Botswana: 'BW', + 'Bouvet Island': 'BV', + Brazil: 'BR', + 'British Indian Ocean Territory': 'IO', + 'Brunei Darussalam': 'BN', + Bulgaria: 'BG', + 'Burkina Faso': 'BF', + Burundi: 'BI', + Cambodia: 'KH', + Cameroon: 'CM', + Canada: 'CA', + 'Cape Verde': 'CV', + 'Cayman Islands': 'KY', + 'Central African Republic': 'CF', + Chad: 'TD', + Chile: 'CL', + China: 'CN', + 'Christmas Island': 'CX', + 'Cocos (Keeling) Islands': 'CC', + Colombia: 'CO', + Comoros: 'KM', + Congo: 'CG', + 'Congo, the Democratic Republic of the': 'CD', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + Croatia: 'HR', + Cuba: 'CU', + Curaçao: 'CW', + Cyprus: 'CY', + 'Czech Republic': 'CZ', + "Côte d'Ivoire": 'CI', + Denmark: 'DK', + Djibouti: 'DJ', + Dominica: 'DM', + 'Dominican Republic': 'DO', + Ecuador: 'EC', + Egypt: 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + Eritrea: 'ER', + Estonia: 'EE', + Ethiopia: 'ET', + 'Falkland Islands (Malvinas)': 'FK', + 'Faroe Islands': 'FO', + Fiji: 'FJ', + Finland: 'FI', + France: 'FR', + 'French Guiana': 'GF', + 'French Polynesia': 'PF', + 'French Southern Territories': 'TF', + Gabon: 'GA', + Gambia: 'GM', + Georgia: 'GE', + Germany: 'DE', + Ghana: 'GH', + Gibraltar: 'GI', + Greece: 'GR', + Greenland: 'GL', + Grenada: 'GD', + Guadeloupe: 'GP', + Guam: 'GU', + Guatemala: 'GT', + Guernsey: 'GG', + Guinea: 'GN', + 'Guinea-Bissau': 'GW', + Guyana: 'GY', + Haiti: 'HT', + 'Heard Island and McDonald Islands': 'HM', + 'Holy See (Vatican City State)': 'VA', + Honduras: 'HN', + 'Hong Kong': 'HK', + Hungary: 'HU', + Iceland: 'IS', + India: 'IN', + Indonesia: 'ID', + 'Iran, Islamic Republic of': 'IR', + Iraq: 'IQ', + Ireland: 'IE', + 'Isle of Man': 'IM', + Israel: 'IL', + Italy: 'IT', + Jamaica: 'JM', + Japan: 'JP', + Jersey: 'JE', + Jordan: 'JO', + Kazakhstan: 'KZ', + Kenya: 'KE', + Kiribati: 'KI', + "Korea, Democratic People's Republic of": 'KP', + 'Korea, Republic of': 'KR', + Kuwait: 'KW', + Kyrgyzstan: 'KG', + "Lao People's Democratic Republic": 'LA', + Latvia: 'LV', + Lebanon: 'LB', + Lesotho: 'LS', + Liberia: 'LR', + Libya: 'LY', + Liechtenstein: 'LI', + Lithuania: 'LT', + Luxembourg: 'LU', + Macao: 'MO', + 'Macedonia, the Former Yugoslav Republic of': 'MK', + Madagascar: 'MG', + Malawi: 'MW', + Malaysia: 'MY', + Maldives: 'MV', + Mali: 'ML', + Malta: 'MT', + 'Marshall Islands': 'MH', + Martinique: 'MQ', + Mauritania: 'MR', + Mauritius: 'MU', + Mayotte: 'YT', + Mexico: 'MX', + 'Micronesia, Federated States of': 'FM', + 'Moldova, Republic of': 'MD', + Monaco: 'MC', + Mongolia: 'MN', + Montenegro: 'ME', + Montserrat: 'MS', + Morocco: 'MA', + Mozambique: 'MZ', + Myanmar: 'MM', + Namibia: 'NA', + Nauru: 'NR', + Nepal: 'NP', + Netherlands: 'NL', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + Nicaragua: 'NI', + Niger: 'NE', + Nigeria: 'NG', + Niue: 'NU', + 'Norfolk Island': 'NF', + 'Northern Mariana Islands': 'MP', + Norway: 'NO', + Oman: 'OM', + Pakistan: 'PK', + Palau: 'PW', + 'Palestine, State of': 'PS', + Panama: 'PA', + 'Papua New Guinea': 'PG', + Paraguay: 'PY', + Peru: 'PE', + Philippines: 'PH', + Pitcairn: 'PN', + Poland: 'PL', + Portugal: 'PT', + 'Puerto Rico': 'PR', + Qatar: 'QA', + Romania: 'RO', + 'Russian Federation': 'RU', + Rwanda: 'RW', + Réunion: 'RE', + 'Saint Barthélemy': 'BL', + 'Saint Helena, Ascension and Tristan da Cunha': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin (French part)': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + Samoa: 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + Senegal: 'SN', + Serbia: 'RS', + Seychelles: 'SC', + 'Sierra Leone': 'SL', + Singapore: 'SG', + 'Sint Maarten (Dutch part)': 'SX', + Slovakia: 'SK', + Slovenia: 'SI', + 'Solomon Islands': 'SB', + Somalia: 'SO', + 'South Africa': 'ZA', + 'South Georgia and the South Sandwich Islands': 'GS', + 'South Sudan': 'SS', + Spain: 'ES', + 'Sri Lanka': 'LK', + Sudan: 'SD', + Suriname: 'SR', + 'Svalbard and Jan Mayen': 'SJ', + Swaziland: 'SZ', + Sweden: 'SE', + Switzerland: 'CH', + 'Syrian Arab Republic': 'SY', + 'Taiwan, Province of China': 'TW', + Tajikistan: 'TJ', + 'Tanzania, United Republic of': 'TZ', + Thailand: 'TH', + 'Timor-Leste': 'TL', + Togo: 'TG', + Tokelau: 'TK', + Tonga: 'TO', + 'Trinidad and Tobago': 'TT', + Tunisia: 'TN', + Turkey: 'TR', + Turkmenistan: 'TM', + 'Turks and Caicos Islands': 'TC', + Tuvalu: 'TV', + Uganda: 'UG', + Ukraine: 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'United States Minor Outlying Islands': 'UM', + Uruguay: 'UY', + Uzbekistan: 'UZ', + Vanuatu: 'VU', + 'Venezuela, Bolivarian Republic of': 'VE', + 'Viet Nam': 'VN', + 'Virgin Islands, British': 'VG', + 'Virgin Islands, U.S.': 'VI', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + Yemen: 'YE', + Zambia: 'ZM', + Zimbabwe: 'ZW', } as const; diff --git a/src/shared/img/svg/calendar.svg b/src/shared/img/svg/calendar.svg new file mode 100644 index 00000000..c7a34389 --- /dev/null +++ b/src/shared/img/svg/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/types/interfaces.ts b/src/shared/types/interfaces.ts index bd7769bb..309c3f25 100644 --- a/src/shared/types/interfaces.ts +++ b/src/shared/types/interfaces.ts @@ -39,8 +39,9 @@ export interface UserInterface { export interface InputParams { autocomplete: 'off' | 'on'; id: string; + lang?: string; placeholder: null | string; - type: 'color' | 'date' | 'email' | 'number' | 'password' | 'range' | 'text'; + type: 'color' | 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text'; } export interface LabelParams { @@ -70,8 +71,16 @@ export interface InputFieldValidatorParams { message: string; pattern: RegExp; } | null; + validBirthday?: { + maxAge: number; + message: string; + minAge: number; + pattern: RegExp; + } | null; + validCountry?: boolean; validMail?: { message: string; pattern: RegExp; } | null; + validPostalCode?: boolean; } diff --git a/src/shared/utils/getCountryIndex.ts b/src/shared/utils/getCountryIndex.ts new file mode 100644 index 00000000..66d1558e --- /dev/null +++ b/src/shared/utils/getCountryIndex.ts @@ -0,0 +1,6 @@ +import { COUNTRIES } from '../constants/enums.ts'; + +export default function getCountryIndex(country: string): string { + const countryIndex: string = COUNTRIES[country]; + return countryIndex; +} diff --git a/src/widgets/RegistrationForm/.gitkeep b/src/widgets/RegistrationForm/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts new file mode 100644 index 00000000..d76d4dd8 --- /dev/null +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -0,0 +1,98 @@ +import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setRegisterFormCountry } from '@/shared/Store/actions.ts'; +import { EVENT_NAMES, REGISTRATION_FORM_COUNTRY_FIELD_VALIDATE_PARAMS } from '@/shared/constants/enums.ts'; +import getCountryIndex from '@/shared/utils/getCountryIndex.ts'; + +import RegistrationFormView from '../view/RegistrationFormView.ts'; +import REGISTRATION_FORM_STYLES from '../view/registrationForm.module.scss'; + +class RegisterFormModel { + private inputFields: InputFieldModel[] = []; + + private isValidInputFields: Record = {}; + + private view: RegistrationFormView = new RegistrationFormView(); + + constructor() { + this.init(); + } + + private init(): boolean { + this.inputFields = this.view.getInputFields(); + this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); + this.setPreventDefaultToForm(); + + return true; + } + + private setCountryItemsHandlers(input: HTMLInputElement): boolean { + const inputHTML = input; + this.view.getCountryItems().forEach((countryItem) => { + const currentItem = countryItem; + currentItem.addEventListener(EVENT_NAMES.CLICK, () => { + if (currentItem.textContent) { + inputHTML.value = currentItem.textContent; + const store = getStore(); + const currentCountryIndex = getCountryIndex(currentItem.textContent); + store.dispatch(setRegisterFormCountry(currentCountryIndex)); + const event = new Event(EVENT_NAMES.INPUT); + inputHTML.dispatchEvent(event); + this.view.hideCountryDropList(); + } + }); + }); + return true; + } + + private setInputFieldHandlers(inputField: InputFieldModel): boolean { + const inputHTML = inputField.getView().getInput().getHTML(); + this.isValidInputFields[inputHTML.id] = false; + inputHTML.addEventListener(EVENT_NAMES.INPUT, () => { + this.isValidInputFields[inputHTML.id] = inputField.getIsValid(); + this.switchSubmitFormButtonAccess(); + }); + + if (inputHTML.id === REGISTRATION_FORM_COUNTRY_FIELD_VALIDATE_PARAMS.key) { + this.setCountryItemsHandlers(inputHTML); + inputHTML.addEventListener(EVENT_NAMES.FOCUS, () => this.view.showCountryDropList()); + inputHTML.addEventListener(EVENT_NAMES.INPUT, () => this.view.switchVisibilityCountryItems(inputHTML)); + document.addEventListener(EVENT_NAMES.CLICK, (event) => { + const countryDropList = this.view.getCountryDropList(); + if (!countryDropList.classList.contains(REGISTRATION_FORM_STYLES.hidden) && event.target !== inputHTML) { + this.view.hideCountryDropList(); + } + }); + } + return true; + } + + private setPreventDefaultToForm(): boolean { + this.getHTML().addEventListener(EVENT_NAMES.SUBMIT, (event) => { + event.preventDefault(); + }); + + return true; + } + + private switchSubmitFormButtonAccess(): boolean { + if (Object.values(this.isValidInputFields).every((value) => value)) { + this.view.getSubmitFormButton().setEnabled(); + } else { + this.view.getSubmitFormButton().setDisabled(); + } + + return true; + } + + public getFirstInputField(): InputFieldModel { + return this.inputFields[0]; + } + + public getHTML(): HTMLFormElement { + return this.view.getHTML(); + } +} + +export default RegisterFormModel; diff --git a/src/widgets/RegistrationForm/view/RegistrationFormView.ts b/src/widgets/RegistrationForm/view/RegistrationFormView.ts new file mode 100644 index 00000000..70db316b --- /dev/null +++ b/src/widgets/RegistrationForm/view/RegistrationFormView.ts @@ -0,0 +1,160 @@ +import InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import { + BUTTON_TYPES, + COUNTRIES, + FORM_SUBMIT_BUTTON_TEXT, + REGISTRATION_FORM_INPUT_FIELD_PARAMS, + REGISTRATION_FORM_INPUT_FIELD_VALIDATION_PARAMS, + TAG_NAMES, +} from '@/shared/constants/enums.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import REGISTRATION_FORM_STYLES from './registrationForm.module.scss'; + +class RegistrationFormView { + private countryDropList: HTMLDivElement; + + private countryItems: HTMLDivElement[] = []; + + private countryWrapper: HTMLDivElement; + + private form: HTMLFormElement; + + private inputFields: InputFieldModel[] = []; + + private submitFormButton: ButtonModel; + + constructor() { + this.inputFields = this.createInputFields(); + this.submitFormButton = this.createSubmitFormButton(); + this.countryDropList = this.createCountryDropList(); + this.countryWrapper = this.createCountryWrapper(); + this.form = this.createHTML(); + } + + private createCountryDropList(): HTMLDivElement { + this.countryDropList = createBaseElement({ + cssClasses: [REGISTRATION_FORM_STYLES.countryDropList, REGISTRATION_FORM_STYLES.hidden], + tag: TAG_NAMES.DIV, + }); + + Object.entries(COUNTRIES).forEach(([countryCode]) => { + this.countryDropList.append(this.createCountryItem(countryCode)); + }); + + return this.countryDropList; + } + + private createCountryItem(countryCode: string): HTMLDivElement { + const countryItem = createBaseElement({ + cssClasses: [REGISTRATION_FORM_STYLES.countryItem], + innerContent: countryCode, + tag: TAG_NAMES.DIV, + }); + + this.countryItems.push(countryItem); + + return countryItem; + } + + private createCountryWrapper(): HTMLDivElement { + this.countryWrapper = createBaseElement({ + cssClasses: [REGISTRATION_FORM_STYLES.countryWrapper], + tag: TAG_NAMES.DIV, + }); + + return this.countryWrapper; + } + + private createHTML(): HTMLFormElement { + this.form = createBaseElement({ + cssClasses: [REGISTRATION_FORM_STYLES.registrationForm], + tag: TAG_NAMES.FORM, + }); + + this.inputFields.forEach((inputField) => { + const inputFieldElement = inputField.getView().getHTML(); + + if (inputFieldElement instanceof HTMLLabelElement) { + this.form.append(inputFieldElement); + } else { + this.form.append(inputFieldElement.getHTML()); + } + }); + + this.countryWrapper.append(this.countryDropList); + this.form.append(this.countryWrapper, this.submitFormButton.getHTML()); + return this.form; + } + + private createInputFields(): InputFieldModel[] { + REGISTRATION_FORM_INPUT_FIELD_PARAMS.forEach((inputFieldParams) => { + const currentValidateParams = REGISTRATION_FORM_INPUT_FIELD_VALIDATION_PARAMS.find( + (validParams) => validParams.key === inputFieldParams.inputParams.id, + ); + + if (currentValidateParams) { + const inputField = new InputFieldModel(inputFieldParams, currentValidateParams); + this.inputFields.push(inputField); + } else { + this.inputFields.push(new InputFieldModel(inputFieldParams, null)); + } + }); + + return this.inputFields; + } + + private createSubmitFormButton(): ButtonModel { + this.submitFormButton = new ButtonModel({ + attrs: { + type: BUTTON_TYPES.SUBMIT, + }, + text: FORM_SUBMIT_BUTTON_TEXT.REGISTRATION, + }); + + this.submitFormButton.setDisabled(); + + return this.submitFormButton; + } + + public getCountryDropList(): HTMLDivElement { + return this.countryDropList; + } + + public getCountryItems(): HTMLDivElement[] { + return this.countryItems; + } + + public getHTML(): HTMLFormElement { + return this.form; + } + + public getInputFields(): InputFieldModel[] { + return this.inputFields; + } + + public getSubmitFormButton(): ButtonModel { + return this.submitFormButton; + } + + public hideCountryDropList(): void { + this.countryDropList.classList.add(REGISTRATION_FORM_STYLES.hidden); + } + + public showCountryDropList(): void { + this.countryDropList.classList.remove(REGISTRATION_FORM_STYLES.hidden); + } + + public switchVisibilityCountryItems(inputHTML: HTMLInputElement): boolean { + const filterValue = inputHTML.value.toLowerCase(); + this.countryItems.forEach((countryItem) => { + const itemValue = countryItem.textContent?.toLowerCase(); + countryItem.classList.toggle(REGISTRATION_FORM_STYLES.hidden, !itemValue?.includes(filterValue)); + }); + + return true; + } +} + +export default RegistrationFormView; diff --git a/src/widgets/RegistrationForm/view/registrationForm.module.scss b/src/widgets/RegistrationForm/view/registrationForm.module.scss new file mode 100644 index 00000000..d5cb82d0 --- /dev/null +++ b/src/widgets/RegistrationForm/view/registrationForm.module.scss @@ -0,0 +1,262 @@ +.registrationForm { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat((6, max-content)); + margin: 0 auto; + gap: var(--extra-small-offset); + + label { + position: relative; + display: flex; + flex-direction: column; + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + gap: calc(var(--extra-small-offset) / 2); + + button { + position: absolute; + right: 0; + top: 34px; + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 8); + width: 24px; + height: 24px; + background-color: transparent; + transform: translate(-12%, 0); + + svg { + width: 20px; + height: 20px; + stroke: var(--noble-gray-600); + transition: stroke 0.2s; + } + + &:focus { + background-color: var(--noble-gray-200); + + svg { + stroke: var(--steam-green-800); + } + } + + @media (hover: hover) { + &:hover { + background-color: var(--noble-gray-200); + + svg { + stroke: var(--steam-green-800); + } + } + } + } + + @media (max-width: 768px) { + button { + top: 24px; + } + } + } + + input { + border: 1px solid var(--noble-gray-200); + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + transition: border 0.2s; + cursor: text; + + &::placeholder { + color: var(--noble-gray-600); + } + + &:focus { + border: 1px solid var(--steam-green-800); + } + + @media (hover: hover) { + &:hover { + border: 1px solid var(--steam-green-800); + } + } + + &[type='date'] { + -webkit-appearance: none; + appearance: none; + padding-right: 0; + } + + &[type='date']::-webkit-calendar-picker-indicator { + background-image: url('../../../shared/img/svg/calendar.svg'); + background-position: -3px 0; + cursor: pointer; + } + + &[type='date']::-webkit-inner-spin-button, + &[type='date']::-webkit-outer-spin-button { + display: none; + } + + &::-webkit-datetime-edit-day-field:focus, + &::-webkit-datetime-edit-month-field:focus, + &::-webkit-datetime-edit-year-field:focus { + outline: none; + border-radius: var(--small-br); + color: var(--steam-green-800); + background-color: var(--noble-gray-200); + } + } + + span { + font: var(--regular-font); + letter-spacing: 1px; + text-align: center; + color: var(--red-power-600); + } + + button { + grid-column: 2 span; + grid-row: 7; + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 2) var(--small-offset); + max-height: 40px; + font: var(--bold-font); + letter-spacing: 1px; + color: var(--white); + background-color: var(--steam-green-800); + transition: + color 0.2s, + background-color 0.2s; + + @media (max-width: 768px) { + grid-column: 1; + grid-row: 11; + } + + @media (hover: hover) { + &:hover { + background-color: var(--steam-green-700); + } + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } + } + + @media (max-width: 768px) { + grid-template-columns: repeat(1, 1fr); + } +} + +.countryWrapper { + position: sticky; + grid-column: 2 span; + grid-row: 5; + + @media (max-width: 768px) { + grid-column: 1 span; + grid-row: 8; + } +} + +.countryDropList { + position: absolute; + right: 0; + overflow-y: scroll; + border: 1px solid var(--noble-gray-300); + border-radius: var(--small-br); + width: 100%; + min-height: 200px; + max-height: 200px; + background-color: var(--white); + opacity: 1; + visibility: visible; + transition: + opacity 0.2s, + visibility 0.2s; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: var(--noble-gray-200); + } + + &::-webkit-scrollbar-thumb { + border-radius: var(--small-br); + background-color: var(--steam-green-800); + cursor: pointer; + } + + @media (max-width: 768px) { + bottom: 0; + } +} + +.countryItem { + padding: calc(var(--extra-small-offset) / 4) calc(var(--extra-small-offset) / 2); + font: var(--regular-font); + letter-spacing: 1px; + text-align: end; + color: var(--noble-gray-800); + transition: + color 0.2s, + background-color 0.2s; + animation: show 0.5s ease-in forwards; + cursor: pointer; + + @media (hover: hover) { + &:hover { + color: var(--steam-green-800); + background-color: var(--noble-gray-200); + } + } + + @media (max-width: 768px) { + text-align: start; + } + + &.hidden { + animation: hide 0.5s ease-in forwards; + } +} + +@keyframes hide { + 0% { + opacity: 1; + visibility: visible; + } + + 50% { + opacity: 0; + visibility: hidden; + } + + 100% { + display: none; + } +} + +@keyframes show { + 0% { + display: block; + opacity: 0; + visibility: hidden; + } + + 100% { + opacity: 1; + visibility: visible; + } +} + +.hidden { + opacity: 0; + visibility: hidden; + pointer-events: none; +}