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";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Button, Stack } from "@mantine/core";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
@ -15,14 +15,23 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LoginForm: FC<Props> = ({ isCreatingId = false }) => {
|
const LoginForm: FC<Props> = ({ isCreatingId = false }) => {
|
||||||
|
const [phoneMask, setPhoneMask] = useState<string>("");
|
||||||
const form = useForm<LoginForm>({
|
const form = useForm<LoginForm>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
},
|
},
|
||||||
|
validate: {
|
||||||
|
phoneNumber: phoneNumber =>
|
||||||
|
phoneNumber.length !== phoneMask.length &&
|
||||||
|
"Введите корректный номер",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: LoginForm) => {
|
const handleSubmit = (values: LoginForm) => {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
|
console.log(phoneMask);
|
||||||
|
|
||||||
|
redirect("/verify-phone");
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToCreateId = () => redirect("/create-id");
|
const navigateToCreateId = () => redirect("/create-id");
|
||||||
@ -32,7 +41,10 @@ const LoginForm: FC<Props> = ({ isCreatingId = false }) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PhoneInput {...form.getInputProps("phoneNumber")} />
|
<PhoneInput
|
||||||
|
{...form.getInputProps("phoneNumber")}
|
||||||
|
setPhoneMask={setPhoneMask}
|
||||||
|
/>
|
||||||
{isCreatingId ? (
|
{isCreatingId ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -7,67 +7,63 @@ import {
|
|||||||
type InputBaseProps,
|
type InputBaseProps,
|
||||||
type PolymorphicComponentProps,
|
type PolymorphicComponentProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useUncontrolled } from "@mantine/hooks";
|
|
||||||
import CountrySelect from "@/components/PhoneInput/components/CountrySelect";
|
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 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;
|
initialCountryCode?: string;
|
||||||
defaultValue?: string;
|
};
|
||||||
} & Omit<
|
|
||||||
|
type InputProps = Omit<
|
||||||
PolymorphicComponentProps<typeof IMaskInput, InputBaseProps>,
|
PolymorphicComponentProps<typeof IMaskInput, InputBaseProps>,
|
||||||
"onChange" | "defaultValue"
|
"onChange" | "defaultValue"
|
||||||
> & { onChange: (value: string | null) => void };
|
>;
|
||||||
|
|
||||||
|
export type Props = AdditionalProps & InputProps;
|
||||||
|
|
||||||
const PhoneInput = ({
|
const PhoneInput = ({
|
||||||
initialCountryCode = "RU",
|
initialCountryCode = "RU",
|
||||||
value: _value,
|
value: _value,
|
||||||
onChange: _onChange,
|
onChange: _onChange,
|
||||||
defaultValue,
|
setPhoneMask: _setPhoneMask,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [value, onChange] = useUncontrolled({
|
const [mask, setMask] = useState<string>("");
|
||||||
value: _value,
|
const initialData = useRef(getInitialDataFromValue(initialCountryCode));
|
||||||
defaultValue,
|
const [country, setCountry] = useState<Country>(
|
||||||
onChange: _onChange,
|
initialData.current.country
|
||||||
});
|
|
||||||
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 [value, setValue] = useState<string>("");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dropdownWidth, setDropdownWidth] = useState<number>(300);
|
const [dropdownWidth, setDropdownWidth] = useState<number>(300);
|
||||||
|
|
||||||
const lastNotifiedValue = useRef<string | null>(value ?? "");
|
const lastNotifiedValue = useRef<string | null>(value ?? "");
|
||||||
|
|
||||||
useEffect(() => {
|
const onChange = (numberWithoutCode: string) => {
|
||||||
const value = localValue.trim();
|
setValue(numberWithoutCode);
|
||||||
if (value !== lastNotifiedValue.current) {
|
_onChange(`+${country.callingCode} ${numberWithoutCode}`);
|
||||||
lastNotifiedValue.current = value;
|
};
|
||||||
onChange(value);
|
|
||||||
}
|
const setPhoneMask = (phoneMask: string, country: Country) => {
|
||||||
}, [country.code, localValue]);
|
setMask(phoneMask);
|
||||||
|
_setPhoneMask(`+${country.callingCode} ${phoneMask}`);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
setPhoneMask(initialData.current.format, country);
|
||||||
typeof value !== "undefined" &&
|
}, [initialData.current.format]);
|
||||||
value !== lastNotifiedValue.current
|
|
||||||
) {
|
useEffect(() => {
|
||||||
const initialData = getInitialDataFromValue(
|
const localValue = value.trim();
|
||||||
value,
|
if (localValue !== lastNotifiedValue.current) {
|
||||||
initialCountryCode
|
lastNotifiedValue.current = localValue;
|
||||||
);
|
onChange(localValue);
|
||||||
lastNotifiedValue.current = value;
|
|
||||||
setCountry(initialData.country);
|
|
||||||
setFormat(initialData.format);
|
|
||||||
setLocalValue(initialData.localValue);
|
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [country.code, value]);
|
||||||
|
|
||||||
const { readOnly, disabled } = props;
|
const { readOnly, disabled } = props;
|
||||||
const leftSectionWidth = 90;
|
const leftSectionWidth = 90;
|
||||||
@ -88,8 +84,8 @@ const PhoneInput = ({
|
|||||||
country={country}
|
country={country}
|
||||||
setCountry={country => {
|
setCountry={country => {
|
||||||
setCountry(country);
|
setCountry(country);
|
||||||
setFormat(getFormat(country.code));
|
setPhoneMask(getPhoneMask(country.code), country);
|
||||||
setLocalValue("");
|
setValue("");
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
}
|
}
|
||||||
@ -110,9 +106,9 @@ const PhoneInput = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
inputMode={"numeric"}
|
inputMode={"numeric"}
|
||||||
mask={format.mask}
|
mask={mask}
|
||||||
value={localValue}
|
value={value}
|
||||||
onAccept={value => setLocalValue(value)}
|
onAccept={value => setValue(value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,33 +1,20 @@
|
|||||||
|
import { CountryCode } from "libphonenumber-js";
|
||||||
import { Country } from "@/components/PhoneInput/types";
|
import { Country } from "@/components/PhoneInput/types";
|
||||||
import getFormat from "@/components/PhoneInput/utils/getFormat";
|
|
||||||
import countryOptionsDataMap from "@/components/PhoneInput/utils/countryOptionsDataMap";
|
import countryOptionsDataMap from "@/components/PhoneInput/utils/countryOptionsDataMap";
|
||||||
import { CountryCode, parsePhoneNumberFromString } from "libphonenumber-js";
|
import getPhoneMask from "@/components/PhoneInput/utils/getPhoneMask";
|
||||||
|
|
||||||
type InitialDataFromValue = {
|
type InitialDataFromValue = {
|
||||||
country: Country;
|
country: Country;
|
||||||
format: ReturnType<typeof getFormat>;
|
format: ReturnType<typeof getPhoneMask>;
|
||||||
localValue: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialDataFromValue = (
|
const getInitialDataFromValue = (
|
||||||
value: string | undefined,
|
|
||||||
initialCountryCode: string
|
initialCountryCode: string
|
||||||
): InitialDataFromValue => {
|
): 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 {
|
return {
|
||||||
country: countryOptionsDataMap[phoneNumber.country],
|
country: countryOptionsDataMap[initialCountryCode],
|
||||||
localValue: phoneNumber.formatNational(),
|
format: getPhoneMask(initialCountryCode as CountryCode),
|
||||||
format: getFormat(phoneNumber.country),
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default getInitialDataFromValue;
|
export default getInitialDataFromValue;
|
||||||
|
|||||||
@ -2,14 +2,13 @@ import { CountryCode, getExampleNumber } from "libphonenumber-js";
|
|||||||
import examples from "libphonenumber-js/examples.mobile.json";
|
import examples from "libphonenumber-js/examples.mobile.json";
|
||||||
import { getCountryCallingCode } from "libphonenumber-js/max";
|
import { getCountryCallingCode } from "libphonenumber-js/max";
|
||||||
|
|
||||||
export default function getFormat(countryCode: CountryCode) {
|
export default function getPhoneMask(countryCode: CountryCode) {
|
||||||
let example = getExampleNumber(
|
let example = getExampleNumber(
|
||||||
countryCode,
|
countryCode,
|
||||||
examples
|
examples
|
||||||
)!.formatInternational();
|
)!.formatInternational();
|
||||||
const callingCode = getCountryCallingCode(countryCode);
|
const callingCode = getCountryCallingCode(countryCode);
|
||||||
const callingCodeLen = callingCode.length + 1;
|
const callingCodeLen = callingCode.length + 1;
|
||||||
example = example.slice(callingCodeLen);
|
example = example.slice(callingCodeLen).trim();
|
||||||
const mask = example.replace(/\d/g, "0");
|
return example.replace(/\d/g, "0");
|
||||||
return { example, mask };
|
|
||||||
}
|
}
|
||||||
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