Enhance lead form and localization with new phone input components. Added international phone input handling and validation in LeadForm. Updated localization files for phone-related messages in both English and Russian. Introduced new utility functions for phone number normalization and formatting.
This commit is contained in:
@@ -6,13 +6,16 @@
|
||||
"name": "ps2-react-client",
|
||||
"dependencies": {
|
||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.8",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"ahooks": "^3.7.10",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"caniuse-lite": "^1.0.30001764",
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^11.17.0",
|
||||
"i18next": "^23.8.2",
|
||||
"ky": "^1.1.3",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
"peerjs": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^4.3.0",
|
||||
@@ -21,10 +24,12 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-full-screen": "^1.1.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^14.0.3",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-timeit": "^1.2.12",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"react-toastify": "^10.0.5",
|
||||
@@ -182,6 +187,10 @@
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.100.6", "", {}, "sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.100.6", "", { "dependencies": { "@tanstack/query-core": "5.100.6" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.8", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-HzbgCY53T6bfu4tT7Aq3TvViJyHjLjPNaAS3HOuMc9pw97KHsUtXNX4L+wu59g1WnjsZSko35MbEqnO58rihhw=="],
|
||||
@@ -376,6 +385,8 @@
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
||||
"framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fscreen": ["fscreen@1.2.0", "", {}, "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg=="],
|
||||
@@ -490,6 +501,8 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"libphonenumber-js": ["libphonenumber-js@1.12.42", "", {}, "sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
@@ -520,6 +533,10 @@
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="],
|
||||
|
||||
"motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
@@ -620,6 +637,8 @@
|
||||
|
||||
"react-full-screen": ["react-full-screen@1.1.1", "", { "dependencies": { "fscreen": "^1.0.2" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.74.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g=="],
|
||||
|
||||
"react-i18next": ["react-i18next@14.1.3", "", { "dependencies": { "@babel/runtime": "^7.23.9", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw=="],
|
||||
|
||||
"react-input-mask": ["react-input-mask@2.0.4", "", { "dependencies": { "invariant": "^2.2.4", "warning": "^4.0.2" }, "peerDependencies": { "react": ">=0.14.0", "react-dom": ">=0.14.0" } }, "sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ=="],
|
||||
@@ -634,6 +653,8 @@
|
||||
|
||||
"react-router-dom": ["react-router-dom@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw=="],
|
||||
|
||||
"react-swipeable": ["react-swipeable@7.0.2", "", { "peerDependencies": { "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w=="],
|
||||
|
||||
"react-timeit": ["react-timeit@1.2.12", "", { "dependencies": { "react-jss": "^10.9.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0" } }, "sha512-/c+NzN32ju98+RJ30TzFvp8eiJCSMtT5jheu9kUHIbDC7grZX5l2iK6i+AUGf/WAF8R++sjBNr9VVpY4VXQVSw=="],
|
||||
|
||||
"react-timer-hook": ["react-timer-hook@3.0.8", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-bi2e7DhPBU1MRPU4ZHaVqBmgM9e2HK0ae8O2AIqwqjcPo4/qR7lVGQonOQLAKOZPQCJSYfV8F5aBWzOLXElzqQ=="],
|
||||
|
||||
+12
-4
@@ -124,9 +124,12 @@ const resources = {
|
||||
consentLinkData: "согласие на обработку персональных данных",
|
||||
consentMiddle: "и принимаете",
|
||||
consentLinkPolicy: "условия политики",
|
||||
phonePlaceholder: "+7 (XXX) XXX-XX-XX",
|
||||
phoneRequired: "Укажите номер телефона",
|
||||
phoneInvalid: "Введите корректный номер телефона",
|
||||
},
|
||||
questionModal: {
|
||||
phonePlaceholder: "+X (XXX) XXX - XX - XX",
|
||||
phonePlaceholder: "+7 (XXX) XXX-XX-XX",
|
||||
},
|
||||
feedbackModal: {
|
||||
successRich:
|
||||
@@ -247,7 +250,8 @@ const resources = {
|
||||
// Error codes from server
|
||||
INTERNAL_ERROR: "Внутренняя ошибка сервера",
|
||||
INVALID_OBJECT_ID: "Некорректный идентификатор объекта",
|
||||
SESSION_NOT_FOUND: "Сессия не найдена. Возвращаемся на главную страницу...",
|
||||
SESSION_NOT_FOUND:
|
||||
"Сессия не найдена. Возвращаемся на главную страницу...",
|
||||
SESSION_FETCH_ERROR: "Ошибка при получении данных сессии",
|
||||
IP_ADDRESS_ERROR: "Не удалось определить IP-адрес",
|
||||
COUNTRY_CODE_FETCH_ERROR: "Ошибка при определении страны",
|
||||
@@ -261,7 +265,8 @@ const resources = {
|
||||
},
|
||||
speedtest: {
|
||||
pleaseWait: "Пожалуйста, подождите",
|
||||
checkingConnection: "Проверяем качество вашего<br />интернет-соединения",
|
||||
checkingConnection:
|
||||
"Проверяем качество вашего<br />интернет-соединения",
|
||||
checking: "Проверка",
|
||||
secondsLeft: "Осталось {{count}} секунд",
|
||||
},
|
||||
@@ -437,9 +442,12 @@ const resources = {
|
||||
consentLinkData: "consent to personal data processing",
|
||||
consentMiddle: "and accept the",
|
||||
consentLinkPolicy: "policy terms",
|
||||
phonePlaceholder: "+XXXXXXXXXXXXXXX",
|
||||
phoneRequired: "Enter your phone number",
|
||||
phoneInvalid: "Enter a valid phone number",
|
||||
},
|
||||
questionModal: {
|
||||
phonePlaceholder: "+X (XXX) XXX - XX - XX",
|
||||
phonePlaceholder: "+XXXXXXXXXXXXXXX",
|
||||
},
|
||||
feedbackModal: {
|
||||
successRich:
|
||||
|
||||
@@ -6,6 +6,9 @@ import { productOptionsFromT } from "@/landing/lib/productLabels";
|
||||
import type { Product } from "@/landing/types";
|
||||
import { Button } from "@/landing/ui/Button";
|
||||
import { CheckboxesGroup } from "@/landing/ui/CheckboxesGroup";
|
||||
import { ruPhoneDigits } from "@/landing/lib/phoneRu";
|
||||
import { INTL_PHONE_MAX_DIGITS } from "@/landing/lib/phoneIntl";
|
||||
import { PhoneInputIntl } from "@/landing/ui/PhoneInputIntl";
|
||||
import { PhoneInputRu } from "@/landing/ui/PhoneInputRu";
|
||||
import { useRefererStore } from "@/landing/stores/useRefererStore";
|
||||
import {
|
||||
@@ -32,7 +35,8 @@ export function LeadForm({
|
||||
phonePlaceholder?: string;
|
||||
formClassName?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const isRuLocale = i18n.language.startsWith("ru");
|
||||
const { referer } = useRefererStore();
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const projectOptions = useMemo(() => productOptionsFromT(t), [t]);
|
||||
@@ -46,9 +50,15 @@ export function LeadForm({
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, formState, control } = form;
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
control,
|
||||
} = form;
|
||||
const nameId = idPrefix ? `${idPrefix}name` : "name";
|
||||
const emailId = idPrefix ? `${idPrefix}email` : "email";
|
||||
const phoneId = idPrefix ? `${idPrefix}tel` : "tel";
|
||||
|
||||
const onSubmit: SubmitHandler<LeadFormValues> = async (data) => {
|
||||
setSubmitError(null);
|
||||
@@ -105,29 +115,68 @@ export function LeadForm({
|
||||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<PhoneInputRu
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
inputRef={field.ref}
|
||||
placeholder={phonePlaceholder}
|
||||
/>
|
||||
)}
|
||||
rules={{
|
||||
validate: (value: string) => {
|
||||
if (!value?.trim()) return t("leadForm.phoneRequired");
|
||||
if (isRuLocale) {
|
||||
return (
|
||||
ruPhoneDigits(value).length === 11 ||
|
||||
t("leadForm.phoneInvalid")
|
||||
);
|
||||
}
|
||||
const digits = value.replace(/\D/g, "");
|
||||
return (
|
||||
(digits.length >= 8 &&
|
||||
digits.length <= INTL_PHONE_MAX_DIGITS) ||
|
||||
t("leadForm.phoneInvalid")
|
||||
);
|
||||
},
|
||||
}}
|
||||
render={({ field }) =>
|
||||
isRuLocale ? (
|
||||
<PhoneInputRu
|
||||
id={phoneId}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
inputRef={field.ref}
|
||||
placeholder={
|
||||
phonePlaceholder ?? t("leadForm.phonePlaceholder")
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<PhoneInputIntl
|
||||
id={phoneId}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
inputRef={field.ref}
|
||||
placeholder={
|
||||
phonePlaceholder ?? t("leadForm.phonePlaceholder")
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2" />
|
||||
</div>
|
||||
{errors.phone?.message ? (
|
||||
<p className="text-sm text-red-400 -mt-1" role="alert">
|
||||
{String(errors.phone.message)}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="md:flex items-stretch lg:gap-[0.833vw] gap-3 max-md:translate-y-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
className="btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl disabled:opacity-60"
|
||||
>
|
||||
{t("leadForm.submit")}
|
||||
</Button>
|
||||
<div className="text2 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1">
|
||||
<span className="text-[#7A7A7A]">{t("leadForm.consentBefore")}</span>{" "}
|
||||
<span className="text-[#7A7A7A]">
|
||||
{t("leadForm.consentBefore")}
|
||||
</span>{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -136,7 +185,9 @@ export function LeadForm({
|
||||
>
|
||||
{t("leadForm.consentLinkData")}
|
||||
</a>{" "}
|
||||
<span className="text-[#7A7A7A]">{t("leadForm.consentMiddle")} </span>
|
||||
<span className="text-[#7A7A7A]">
|
||||
{t("leadForm.consentMiddle")}{" "}
|
||||
</span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -28,8 +28,10 @@ export function StreamingProject({
|
||||
const streamHref = build ? streamDemoUrlFromBuild(build) : href;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={streamHref}
|
||||
<div
|
||||
onClick={() => {
|
||||
window.location.href = streamHref;
|
||||
}}
|
||||
className={`lg:aspect-[344/396] max-md:aspect-none flex-1 md:max-lg:min-w-[300px] transition-transform will-change-transform lg:rounded-[1.111vw] rounded-2xl lg:p-[1.111vw] p-4 flex duration-500 relative overflow-hidden group max-md:absolute max-md:inset-x-5 max-md:w-auto select-none h-full ${
|
||||
index === current
|
||||
? "max-md:[transform:translateZ(40px)]"
|
||||
@@ -40,13 +42,13 @@ export function StreamingProject({
|
||||
: "max-md:[transform:scale(0.85)]"
|
||||
} ${className ?? ""}`}
|
||||
>
|
||||
<div className="z-0 rounded-2xl absolute inset-0 overflow-hidden transition-transform duration-500 group-hover:scale-110">
|
||||
<div className="group-hover:scale-110 overflow-hidden absolute inset-0 z-0 rounded-2xl transition-transform duration-500">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-0 size-full object-cover object-bottom"
|
||||
className="size-full object-cover object-bottom absolute inset-0 z-0"
|
||||
draggable={false}
|
||||
/>
|
||||
<div
|
||||
@@ -93,6 +95,6 @@ export function StreamingProject({
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,10 +63,13 @@
|
||||
"consentBefore": "*By submitting, you give",
|
||||
"consentLinkData": "consent to personal data processing",
|
||||
"consentMiddle": "and accept the",
|
||||
"consentLinkPolicy": "policy terms"
|
||||
"consentLinkPolicy": "policy terms",
|
||||
"phonePlaceholder": "+XXXXXXXXXXXXXXX",
|
||||
"phoneRequired": "Enter your phone number",
|
||||
"phoneInvalid": "Enter a valid phone number"
|
||||
},
|
||||
"questionModal": {
|
||||
"phonePlaceholder": "+X (XXX) XXX - XX - XX"
|
||||
"phonePlaceholder": "+XXXXXXXXXXXXXXX"
|
||||
},
|
||||
"feedbackModal": {
|
||||
"successRich": "We’ve received your request <brMobile /> and we’ll contact you <brDesktop /> soon!",
|
||||
|
||||
@@ -63,10 +63,13 @@
|
||||
"consentBefore": "*Нажимая кнопку отправить, вы даете",
|
||||
"consentLinkData": "согласие на обработку персональных данных",
|
||||
"consentMiddle": "и принимаете",
|
||||
"consentLinkPolicy": "условия политики"
|
||||
"consentLinkPolicy": "условия политики",
|
||||
"phonePlaceholder": "+7 (XXX) XXX-XX-XX",
|
||||
"phoneRequired": "Укажите номер телефона",
|
||||
"phoneInvalid": "Введите корректный номер телефона"
|
||||
},
|
||||
"questionModal": {
|
||||
"phonePlaceholder": "+X (XXX) XXX - XX - XX"
|
||||
"phonePlaceholder": "+7 (XXX) XXX-XX-XX"
|
||||
},
|
||||
"feedbackModal": {
|
||||
"successRich": "Мы получили заявку <brMobile /> и скоро свяжемся <brDesktop /> с вами!",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Максимум значащих цифр в международном номере (E.164). */
|
||||
export const INTL_PHONE_MAX_DIGITS = 15;
|
||||
|
||||
/** Нормализация: «+» и только цифры после него (до 15), без пробелов и прочего. */
|
||||
export function normalizeIntlPhoneFromInput(rawNoSpaces: string): string {
|
||||
if (!rawNoSpaces) return "";
|
||||
const v = rawNoSpaces.trim();
|
||||
const plusIdx = v.indexOf("+");
|
||||
if (plusIdx === -1) {
|
||||
const digitsOnly = v.replace(/\D/g, "").slice(0, INTL_PHONE_MAX_DIGITS);
|
||||
return digitsOnly ? `+${digitsOnly}` : "";
|
||||
}
|
||||
const afterPlus = v
|
||||
.slice(plusIdx + 1)
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, INTL_PHONE_MAX_DIGITS);
|
||||
if (!afterPlus) return "+";
|
||||
return `+${afterPlus}`;
|
||||
}
|
||||
|
||||
/** Отображение: «+» и цифры подряд, без пробелов. */
|
||||
export function formatIntlPhoneDisplay(stored: string): string {
|
||||
return normalizeIntlPhoneFromInput(stored.replace(/\s/g, ""));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type Ref } from "react";
|
||||
import {
|
||||
formatIntlPhoneDisplay,
|
||||
normalizeIntlPhoneFromInput,
|
||||
} from "@/landing/lib/phoneIntl";
|
||||
|
||||
const inputClassName =
|
||||
"placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full bg-transparent rounded-none transition-all outline-none";
|
||||
|
||||
interface PhoneInputIntlProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
id?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function PhoneInputIntl({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
id = "tel",
|
||||
placeholder = "+123456789012345",
|
||||
}: PhoneInputIntlProps) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="tel"
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
value={formatIntlPhoneDisplay(value)}
|
||||
onBlur={onBlur}
|
||||
onChange={(e) => {
|
||||
if (!e.nativeEvent.type.startsWith("input")) return;
|
||||
const cleanValue = e.target.value.replace(/\s/g, "");
|
||||
onChange(normalizeIntlPhoneFromInput(cleanValue));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user