feat: phone number validation
This commit is contained in:
15
app/verify-phone/page.tsx
Normal file
15
app/verify-phone/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import Logo from "@/components/Logo/Logo";
|
||||
import PageItem from "@/components/PageBlock/PageItem";
|
||||
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||
import VerifyPhoneForm from "@/components/VerifyPhoneForm/VerifyPhoneForm";
|
||||
|
||||
export default function CreateIdPage() {
|
||||
return (
|
||||
<PageContainer center>
|
||||
<PageItem fullScreenMobile>
|
||||
<Logo title={"Введите код, отправленный на номер"} />
|
||||
<VerifyPhoneForm />
|
||||
</PageItem>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
@ -15,14 +15,23 @@ type Props = {
|
||||
};
|
||||
|
||||
const LoginForm: FC<Props> = ({ isCreatingId = false }) => {
|
||||
const [phoneMask, setPhoneMask] = useState<string>("");
|
||||
const form = useForm<LoginForm>({
|
||||
initialValues: {
|
||||
phoneNumber: "",
|
||||
},
|
||||
validate: {
|
||||
phoneNumber: phoneNumber =>
|
||||
phoneNumber.length !== phoneMask.length &&
|
||||
"Введите корректный номер",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: LoginForm) => {
|
||||
console.log(values);
|
||||
console.log(phoneMask);
|
||||
|
||||
redirect("/verify-phone");
|
||||
};
|
||||
|
||||
const navigateToCreateId = () => redirect("/create-id");
|
||||
@ -32,7 +41,10 @@ const LoginForm: FC<Props> = ({ isCreatingId = false }) => {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<PhoneInput {...form.getInputProps("phoneNumber")} />
|
||||
<PhoneInput
|
||||
{...form.getInputProps("phoneNumber")}
|
||||
setPhoneMask={setPhoneMask}
|
||||
/>
|
||||
{isCreatingId ? (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@ -7,67 +7,63 @@ import {
|
||||
type InputBaseProps,
|
||||
type PolymorphicComponentProps,
|
||||
} from "@mantine/core";
|
||||
import { useUncontrolled } from "@mantine/hooks";
|
||||
import CountrySelect from "@/components/PhoneInput/components/CountrySelect";
|
||||
import getFormat from "@/components/PhoneInput/utils/getFormat";
|
||||
import { Country } from "@/components/PhoneInput/types";
|
||||
import getInitialDataFromValue from "@/components/PhoneInput/utils/getInitialDataFromValue";
|
||||
import getPhoneMask from "@/components/PhoneInput/utils/getPhoneMask";
|
||||
|
||||
export type Props = {
|
||||
type AdditionalProps = {
|
||||
onChange: (value: string | null) => void;
|
||||
setPhoneMask: (mask: string) => void;
|
||||
initialCountryCode?: string;
|
||||
defaultValue?: string;
|
||||
} & Omit<
|
||||
};
|
||||
|
||||
type InputProps = Omit<
|
||||
PolymorphicComponentProps<typeof IMaskInput, InputBaseProps>,
|
||||
"onChange" | "defaultValue"
|
||||
> & { onChange: (value: string | null) => void };
|
||||
>;
|
||||
|
||||
export type Props = AdditionalProps & InputProps;
|
||||
|
||||
const PhoneInput = ({
|
||||
initialCountryCode = "RU",
|
||||
value: _value,
|
||||
onChange: _onChange,
|
||||
defaultValue,
|
||||
setPhoneMask: _setPhoneMask,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [value, onChange] = useUncontrolled({
|
||||
value: _value,
|
||||
defaultValue,
|
||||
onChange: _onChange,
|
||||
});
|
||||
const initialData = useRef(
|
||||
getInitialDataFromValue(value, initialCountryCode)
|
||||
);
|
||||
const [country, setCountry] = useState(initialData.current.country);
|
||||
const [format, setFormat] = useState(initialData.current.format);
|
||||
const [localValue, setLocalValue] = useState(
|
||||
initialData.current.localValue
|
||||
const [mask, setMask] = useState<string>("");
|
||||
const initialData = useRef(getInitialDataFromValue(initialCountryCode));
|
||||
const [country, setCountry] = useState<Country>(
|
||||
initialData.current.country
|
||||
);
|
||||
const [value, setValue] = useState<string>("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [dropdownWidth, setDropdownWidth] = useState<number>(300);
|
||||
|
||||
const lastNotifiedValue = useRef<string | null>(value ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
const value = localValue.trim();
|
||||
if (value !== lastNotifiedValue.current) {
|
||||
lastNotifiedValue.current = value;
|
||||
onChange(value);
|
||||
}
|
||||
}, [country.code, localValue]);
|
||||
const onChange = (numberWithoutCode: string) => {
|
||||
setValue(numberWithoutCode);
|
||||
_onChange(`+${country.callingCode} ${numberWithoutCode}`);
|
||||
};
|
||||
|
||||
const setPhoneMask = (phoneMask: string, country: Country) => {
|
||||
setMask(phoneMask);
|
||||
_setPhoneMask(`+${country.callingCode} ${phoneMask}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof value !== "undefined" &&
|
||||
value !== lastNotifiedValue.current
|
||||
) {
|
||||
const initialData = getInitialDataFromValue(
|
||||
value,
|
||||
initialCountryCode
|
||||
);
|
||||
lastNotifiedValue.current = value;
|
||||
setCountry(initialData.country);
|
||||
setFormat(initialData.format);
|
||||
setLocalValue(initialData.localValue);
|
||||
setPhoneMask(initialData.current.format, country);
|
||||
}, [initialData.current.format]);
|
||||
|
||||
useEffect(() => {
|
||||
const localValue = value.trim();
|
||||
if (localValue !== lastNotifiedValue.current) {
|
||||
lastNotifiedValue.current = localValue;
|
||||
onChange(localValue);
|
||||
}
|
||||
}, [value]);
|
||||
}, [country.code, value]);
|
||||
|
||||
const { readOnly, disabled } = props;
|
||||
const leftSectionWidth = 90;
|
||||
@ -88,8 +84,8 @@ const PhoneInput = ({
|
||||
country={country}
|
||||
setCountry={country => {
|
||||
setCountry(country);
|
||||
setFormat(getFormat(country.code));
|
||||
setLocalValue("");
|
||||
setPhoneMask(getPhoneMask(country.code), country);
|
||||
setValue("");
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
@ -110,9 +106,9 @@ const PhoneInput = ({
|
||||
},
|
||||
}}
|
||||
inputMode={"numeric"}
|
||||
mask={format.mask}
|
||||
value={localValue}
|
||||
onAccept={value => setLocalValue(value)}
|
||||
mask={mask}
|
||||
value={value}
|
||||
onAccept={value => setValue(value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,33 +1,20 @@
|
||||
import { CountryCode } from "libphonenumber-js";
|
||||
import { Country } from "@/components/PhoneInput/types";
|
||||
import getFormat from "@/components/PhoneInput/utils/getFormat";
|
||||
import countryOptionsDataMap from "@/components/PhoneInput/utils/countryOptionsDataMap";
|
||||
import { CountryCode, parsePhoneNumberFromString } from "libphonenumber-js";
|
||||
import getPhoneMask from "@/components/PhoneInput/utils/getPhoneMask";
|
||||
|
||||
type InitialDataFromValue = {
|
||||
country: Country;
|
||||
format: ReturnType<typeof getFormat>;
|
||||
localValue: string;
|
||||
format: ReturnType<typeof getPhoneMask>;
|
||||
};
|
||||
|
||||
const getInitialDataFromValue = (
|
||||
value: string | undefined,
|
||||
initialCountryCode: string
|
||||
): InitialDataFromValue => {
|
||||
const defaultValue = {
|
||||
country: countryOptionsDataMap[initialCountryCode],
|
||||
format: getFormat(initialCountryCode as CountryCode),
|
||||
localValue: "",
|
||||
};
|
||||
if (!value) return defaultValue;
|
||||
const phoneNumber = parsePhoneNumberFromString(value);
|
||||
if (!phoneNumber) return defaultValue;
|
||||
if (!phoneNumber.country) return defaultValue;
|
||||
|
||||
return {
|
||||
country: countryOptionsDataMap[phoneNumber.country],
|
||||
localValue: phoneNumber.formatNational(),
|
||||
format: getFormat(phoneNumber.country),
|
||||
country: countryOptionsDataMap[initialCountryCode],
|
||||
format: getPhoneMask(initialCountryCode as CountryCode),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default getInitialDataFromValue;
|
||||
|
||||
@ -2,14 +2,13 @@ import { CountryCode, getExampleNumber } from "libphonenumber-js";
|
||||
import examples from "libphonenumber-js/examples.mobile.json";
|
||||
import { getCountryCallingCode } from "libphonenumber-js/max";
|
||||
|
||||
export default function getFormat(countryCode: CountryCode) {
|
||||
export default function getPhoneMask(countryCode: CountryCode) {
|
||||
let example = getExampleNumber(
|
||||
countryCode,
|
||||
examples
|
||||
)!.formatInternational();
|
||||
const callingCode = getCountryCallingCode(countryCode);
|
||||
const callingCodeLen = callingCode.length + 1;
|
||||
example = example.slice(callingCodeLen);
|
||||
const mask = example.replace(/\d/g, "0");
|
||||
return { example, mask };
|
||||
example = example.slice(callingCodeLen).trim();
|
||||
return example.replace(/\d/g, "0");
|
||||
}
|
||||
11
components/VerifyPhoneForm/VerifyPhone.module.css
Normal file
11
components/VerifyPhoneForm/VerifyPhone.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.pin-input-root {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pin-input {
|
||||
font-size: 17px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
59
components/VerifyPhoneForm/VerifyPhoneForm.tsx
Normal file
59
components/VerifyPhoneForm/VerifyPhoneForm.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Button, PinInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import style from "@/components/VerifyPhoneForm/VerifyPhone.module.css";
|
||||
|
||||
type VerifyNumberForm = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
const VerifyPhoneForm: FC = () => {
|
||||
const form = useForm<VerifyNumberForm>({
|
||||
initialValues: {
|
||||
code: "",
|
||||
},
|
||||
validate: {
|
||||
code: code => code.length !== 6 && "Введите весь код",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: VerifyNumberForm) => {
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
const navigateToLogin = () => redirect("/");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<PinInput
|
||||
length={6}
|
||||
placeholder="_"
|
||||
oneTimeCode
|
||||
size={"md"}
|
||||
classNames={{
|
||||
root: style["pin-input-root"],
|
||||
input: style["pin-input"],
|
||||
}}
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
<Button
|
||||
variant={"filled"}
|
||||
type={"submit"}
|
||||
disabled={form.values.code.length !== 6}>
|
||||
Подтвердить
|
||||
</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={navigateToLogin}>
|
||||
Назад
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyPhoneForm;
|
||||
Reference in New Issue
Block a user