Skip to content

Commit

Permalink
feat(RSS-ECOMM-2_14): set default address during registration (#131)
Browse files Browse the repository at this point in the history
* feat: add CountryChoice component

* fix: postalCode validation

* feat: update state

* refactor: separate CountryChoice component into a module

* feat: add settings address by default
  • Loading branch information
Kleostro authored May 3, 2024
1 parent 995c74e commit b3ba1b9
Show file tree
Hide file tree
Showing 18 changed files with 788 additions and 272 deletions.
11 changes: 1 addition & 10 deletions src/entities/InputField/view/InputFieldView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,7 @@ class InputFieldView {
}

private createInput(inputParams: InputParams): InputModel {
const { autocomplete, id, lang, placeholder, type } = inputParams;

this.input = new InputModel({
autocomplete,
id,
lang: lang || '',
placeholder: placeholder || '',
type,
});

this.input = new InputModel(inputParams);
return this.input;
}

Expand Down
76 changes: 76 additions & 0 deletions src/features/CountryChoice/model/CountryChoiceModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import getStore from '@/shared/Store/Store.ts';
import { setBillingCountry, setShippingCountry } from '@/shared/Store/actions.ts';
import observeStore, { selectBillingCountry, selectShippingCountry } from '@/shared/Store/observer.ts';
import { EVENT_NAMES, REGISTRATION_FORM_BILLING_ADDRESS_COUNTRY_FIELD_PARAMS } from '@/shared/constants/enums.ts';
import getCountryIndex from '@/shared/utils/getCountryIndex.ts';

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

class CountryChoiceModel {
private view: CountryChoiceView;

constructor(input: HTMLInputElement) {
this.view = new CountryChoiceView(input);
this.setCountryItemsHandlers(input);
this.setInputHandler(input);

const action =
input.id === REGISTRATION_FORM_BILLING_ADDRESS_COUNTRY_FIELD_PARAMS.inputParams.id
? selectBillingCountry
: selectShippingCountry;

observeStore(action, () => {
const event = new Event(EVENT_NAMES.INPUT);
input.dispatchEvent(event);
});
}

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;
this.setCountryToStore(currentItem, inputHTML.id);
const event = new Event(EVENT_NAMES.INPUT);
inputHTML.dispatchEvent(event);
this.view.hideCountryChoice();
}
});
});
return true;
}

private setCountryToStore(element: HTMLDivElement | HTMLInputElement, key: string): boolean {
const currentCountryIndex = getCountryIndex(
element instanceof HTMLDivElement ? element.textContent || '' : element.value,
);

const action =
key === REGISTRATION_FORM_BILLING_ADDRESS_COUNTRY_FIELD_PARAMS.inputParams.id
? setBillingCountry
: setShippingCountry;
getStore().dispatch(action(currentCountryIndex));
return true;
}

private setInputHandler(input: HTMLInputElement): boolean {
input.addEventListener(EVENT_NAMES.FOCUS, () => this.view.showCountryChoice());
input.addEventListener(EVENT_NAMES.INPUT, () => {
this.view.switchVisibilityCountryItems(input);
this.setCountryToStore(input, input.id);
});
return true;
}

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

public getView(): CountryChoiceView {
return this.view;
}
}

export default CountryChoiceModel;
Empty file.
94 changes: 94 additions & 0 deletions src/features/CountryChoice/view/CountryChoiceView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { COUNTRIES, EVENT_NAMES, TAG_NAMES } from '@/shared/constants/enums.ts';
import createBaseElement from '@/shared/utils/createBaseElement.ts';

import styles from './countryChoiceView.module.scss';

class CountryChoiceView {
private countryChoice: HTMLDivElement;

private countryDropList: HTMLDivElement;

private countryItems: HTMLDivElement[] = [];

constructor(input: HTMLInputElement) {
this.countryDropList = this.createCountryDropList();
this.countryChoice = this.createHTML();

// TBD replace node document with some element because listener works two times (twice)
document.addEventListener(EVENT_NAMES.CLICK, (event) => {
if (!this.countryDropList.classList.contains(styles.hidden) && event.target !== input) {
this.hideCountryChoice();
}
});
}

private createCountryDropList(): HTMLDivElement {
this.countryDropList = createBaseElement({
cssClasses: [styles.countryDropList],
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: [styles.countryItem],
innerContent: countryCode,
tag: TAG_NAMES.DIV,
});

this.countryItems.push(countryItem);

return countryItem;
}

private createHTML(): HTMLDivElement {
this.countryChoice = createBaseElement({
cssClasses: [styles.countryChoice, styles.hidden],
tag: TAG_NAMES.DIV,
});

this.countryChoice.append(this.countryDropList);

return this.countryChoice;
}

public getCountryDropList(): HTMLDivElement {
return this.countryDropList;
}

public getCountryItems(): HTMLDivElement[] {
return this.countryItems;
}

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

public hideCountryChoice(): void {
this.countryChoice.classList.add(styles.hidden);
document.body.classList.remove(styles.stopScroll);
}

public showCountryChoice(): void {
this.countryChoice.classList.remove(styles.hidden);
document.body.classList.add(styles.stopScroll);
}

public switchVisibilityCountryItems(inputHTML: HTMLInputElement): boolean {
const filterValue = inputHTML.value.toLowerCase();
this.countryItems.forEach((countryItem) => {
const itemValue = countryItem.textContent?.toLowerCase();
countryItem.classList.toggle(styles.hidden, !itemValue?.includes(filterValue));
});

return true;
}
}

export default CountryChoiceView;
109 changes: 109 additions & 0 deletions src/features/CountryChoice/view/countryChoiceView.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.countryChoice {
position: sticky;
z-index: 5;
grid-column: 1;
grid-row: 4;

@media (max-width: 768px) {
grid-column: 1;
grid-row: 5;
}
}

.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;
}

.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;
}
}

.stopScroll {
overflow-y: hidden;
}
19 changes: 8 additions & 11 deletions src/features/InputFieldValidator/model/InputFieldValidatorModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { InputFieldValidatorParams } from '@/shared/types/interfaces';

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

class InputFieldValidatorModel {
Expand Down Expand Up @@ -67,8 +67,7 @@ class InputFieldValidatorModel {

private checkRequired(value: string): boolean | string {
if (this.validParams.required && value.trim() === '') {
const errorMessage = 'Field is required';
return errorMessage;
return ERROR_MESSAGE.REQUIRED_FIELD;
}

return true;
Expand All @@ -95,8 +94,7 @@ class InputFieldValidatorModel {
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 ERROR_MESSAGE.INVALID_COUNTRY;
}
}

Expand All @@ -114,17 +112,16 @@ class InputFieldValidatorModel {

private checkValidPostalCode(value: string): boolean | string {
if (this.validParams.validPostalCode) {
const { registerFormCountry } = getStore().getState();
const { billingCountry, shippingCountry } = getStore().getState();
const currentCountry = this.validParams.key === USER_POSTAL_CODE.POSTAL_CODE ? shippingCountry : billingCountry;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
border-bottom: 10px solid var(--steam-green-800);
border-radius: var(--medium-br);
padding: calc(var(--large-offset) / 2) var(--small-offset);
max-width: 500px;
background-color: var(--white);
}

Expand All @@ -24,13 +23,12 @@
justify-content: space-between;
margin: 0 auto;
margin-bottom: calc(var(--large-offset) / 2);
margin-left: 35%;
max-width: 160px;
width: max-content;

&::after {
content: '';
position: absolute;
right: calc(50% - 3px);
right: calc(36% - 3px);
top: 50%;
width: 3px;
height: 16px;
Expand All @@ -51,11 +49,13 @@
}

.registerSpan {
margin-left: 10%;
color: var(--steam-green-800);
}

.loginLink {
position: relative;
margin-right: 20%;
color: var(--noble-gray-800);
transition: color 0.2s;

Expand Down
Loading

0 comments on commit b3ba1b9

Please sign in to comment.