You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
8.1 KiB
301 lines
8.1 KiB
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
|
|
import type { QuestionField } from "@/data/question-data";
|
|
import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types";
|
|
import { useQuestionAnswer } from "./question-answer-storage";
|
|
import QuestionTitle from "./question-title";
|
|
|
|
type QuestionPhoneProps = {
|
|
question: QuestionField;
|
|
questionIndex: number;
|
|
countryCode?: string;
|
|
};
|
|
|
|
type PhoneValueParts = {
|
|
codeValue: string;
|
|
phoneValue: string;
|
|
};
|
|
|
|
const phoneUtil = PhoneNumberUtil.getInstance();
|
|
|
|
function isMarriagePhoneFieldValue(
|
|
value: unknown,
|
|
): value is MarriagePhoneFieldValue {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
|
|
const phoneValue = value as Partial<MarriagePhoneFieldValue>;
|
|
|
|
return (
|
|
typeof phoneValue.countryCode === "string" &&
|
|
typeof phoneValue.phoneNumber === "string"
|
|
);
|
|
}
|
|
|
|
function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts {
|
|
if (value === null) {
|
|
return {
|
|
codeValue: "",
|
|
phoneValue: "",
|
|
};
|
|
}
|
|
|
|
if (isMarriagePhoneFieldValue(value)) {
|
|
return {
|
|
codeValue: value.countryCode
|
|
? `+${value.countryCode.replace(/^\+/, "")}`
|
|
: fallbackCode,
|
|
phoneValue: value.phoneNumber,
|
|
};
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return {
|
|
codeValue: fallbackCode,
|
|
phoneValue: "",
|
|
};
|
|
}
|
|
|
|
if (value.length === 0) {
|
|
return {
|
|
codeValue: "",
|
|
phoneValue: "",
|
|
};
|
|
}
|
|
|
|
const separatorIndex = value.indexOf(" ");
|
|
|
|
if (separatorIndex >= 0) {
|
|
return {
|
|
codeValue: value.slice(0, separatorIndex),
|
|
phoneValue: value.slice(separatorIndex + 1),
|
|
};
|
|
}
|
|
|
|
if (value.startsWith("+")) {
|
|
try {
|
|
const parsedNumber = phoneUtil.parse(value);
|
|
const countryCode = parsedNumber.getCountryCode();
|
|
const nationalNumber = String(parsedNumber.getNationalNumber());
|
|
|
|
return {
|
|
codeValue: countryCode ? `+${countryCode}` : fallbackCode,
|
|
phoneValue: nationalNumber,
|
|
};
|
|
} catch {
|
|
return {
|
|
codeValue: fallbackCode,
|
|
phoneValue: value,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
codeValue: fallbackCode,
|
|
phoneValue: value,
|
|
};
|
|
}
|
|
|
|
function writePhoneValue(codeValue: string, phoneValue: string) {
|
|
if (!codeValue && !phoneValue) {
|
|
return null;
|
|
}
|
|
|
|
if (codeValue && phoneValue) {
|
|
return `${codeValue} ${phoneValue}`;
|
|
}
|
|
|
|
if (codeValue) {
|
|
return codeValue.startsWith("+") ? codeValue : `${codeValue} `;
|
|
}
|
|
|
|
return phoneValue;
|
|
}
|
|
|
|
function toStoredPhoneValue(
|
|
codeValue: string,
|
|
phoneValue: string,
|
|
): MarriagePhoneFieldValue | null {
|
|
const normalizedCountryCode = sanitizeCountryCode(codeValue).replace(/^\+/, "");
|
|
const normalizedPhoneNumber = phoneValue.trim().replace(/\s+/g, "");
|
|
|
|
if (!normalizedCountryCode && !normalizedPhoneNumber) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
countryCode: normalizedCountryCode,
|
|
phoneNumber: normalizedPhoneNumber,
|
|
};
|
|
}
|
|
|
|
function sanitizeCountryCode(value: string) {
|
|
const sanitized = value.replace(/[^\d+]/g, "");
|
|
|
|
if (sanitized.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
return sanitized.startsWith("+")
|
|
? `+${sanitized.slice(1).replace(/\+/g, "")}`
|
|
: `+${sanitized.replace(/\+/g, "")}`;
|
|
}
|
|
|
|
function sanitizePhoneNumber(value: string) {
|
|
return value.replace(/[^\d\s\-().]/g, "");
|
|
}
|
|
|
|
function getNormalizedPhoneValue(codeValue: string, phoneValue: string) {
|
|
const nextCodeValue = sanitizeCountryCode(codeValue);
|
|
const nextPhoneValue = phoneValue.trim();
|
|
|
|
if (nextCodeValue.length === 0 && nextPhoneValue.length === 0) {
|
|
return {
|
|
isValid: !nextPhoneValue.length,
|
|
normalizedValue: null,
|
|
};
|
|
}
|
|
|
|
if (nextCodeValue.length === 0 || nextPhoneValue.length === 0) {
|
|
return {
|
|
isValid: false,
|
|
normalizedValue: null,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const parsedNumber = phoneUtil.parse(`${nextCodeValue} ${nextPhoneValue}`);
|
|
|
|
if (!phoneUtil.isValidNumber(parsedNumber)) {
|
|
return {
|
|
isValid: false,
|
|
normalizedValue: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
isValid: true,
|
|
normalizedValue: phoneUtil.format(parsedNumber, PhoneNumberFormat.E164),
|
|
};
|
|
} catch {
|
|
return {
|
|
isValid: false,
|
|
normalizedValue: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
export function QuestionPhone({
|
|
question,
|
|
questionIndex,
|
|
countryCode = "+98",
|
|
}: QuestionPhoneProps) {
|
|
const { setValue, value } = useQuestionAnswer(question, questionIndex);
|
|
const defaultCodeValue = countryCode.trim() || "+98";
|
|
const initialValue = readPhoneValue(value, defaultCodeValue);
|
|
const [codeValue, setCodeValue] = useState(initialValue.codeValue);
|
|
const [phoneValue, setPhoneValue] = useState(initialValue.phoneValue);
|
|
const lastCommittedValueRef = useRef(value);
|
|
const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue);
|
|
const showInvalidState =
|
|
codeValue.trim().length > 0 &&
|
|
phoneValue.trim().length > 0 &&
|
|
!normalizedPhoneState.isValid;
|
|
const isAnswered =
|
|
question.required === false
|
|
? normalizedPhoneState.isValid || phoneValue.trim().length === 0
|
|
: normalizedPhoneState.isValid;
|
|
|
|
useEffect(() => {
|
|
if (value === lastCommittedValueRef.current) {
|
|
return;
|
|
}
|
|
|
|
const nextValue = readPhoneValue(value, defaultCodeValue);
|
|
|
|
setCodeValue(nextValue.codeValue);
|
|
setPhoneValue(nextValue.phoneValue);
|
|
lastCommittedValueRef.current = value;
|
|
}, [defaultCodeValue, value]);
|
|
|
|
const updateStoredValue = (nextCodeValue: string, nextPhoneValue: string) => {
|
|
const draftValue = writePhoneValue(nextCodeValue, nextPhoneValue);
|
|
const nextPhoneState = getNormalizedPhoneValue(
|
|
nextCodeValue,
|
|
nextPhoneValue,
|
|
);
|
|
const nextValue =
|
|
draftValue === null
|
|
? null
|
|
: nextPhoneState.isValid
|
|
? toStoredPhoneValue(nextCodeValue, nextPhoneValue)
|
|
: null;
|
|
|
|
lastCommittedValueRef.current = nextValue;
|
|
setValue(nextValue);
|
|
};
|
|
|
|
return (
|
|
<label
|
|
data-question-answered={isAnswered ? "true" : "false"}
|
|
data-question-type={question.type}
|
|
className="block space-y-3"
|
|
>
|
|
<QuestionTitle question={question} />
|
|
<div
|
|
dir="ltr"
|
|
className={[
|
|
"flex h-[54px] w-full items-center rounded-[15px] border bg-white text-[#181818] focus-within:border-[#6F6F6F]",
|
|
showInvalidState ? "border-[#F2465F]" : "border-[#E7D8D5]",
|
|
].join(" ")}
|
|
>
|
|
<div className="flex shrink-0 items-center pl-2.5 pr-2">
|
|
<input
|
|
type="tel"
|
|
inputMode="tel"
|
|
aria-label="Country code"
|
|
maxLength={4}
|
|
required={question.required}
|
|
placeholder={defaultCodeValue}
|
|
value={codeValue}
|
|
onChange={(event) => {
|
|
const nextCodeValue = sanitizeCountryCode(event.target.value);
|
|
|
|
setCodeValue(nextCodeValue);
|
|
updateStoredValue(nextCodeValue, phoneValue);
|
|
}}
|
|
className="w-[33px] border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
|
|
/>
|
|
<span aria-hidden="true" className="h-5 w-px bg-[#181818]/35" />
|
|
</div>
|
|
<span className="flex min-w-0 flex-1 items-center pr-4">
|
|
<input
|
|
type="tel"
|
|
inputMode="tel"
|
|
required={question.required}
|
|
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
|
|
value={phoneValue}
|
|
onChange={(event) => {
|
|
const nextPhoneValue = sanitizePhoneNumber(event.target.value);
|
|
|
|
setPhoneValue(nextPhoneValue);
|
|
updateStoredValue(codeValue, nextPhoneValue);
|
|
}}
|
|
dir="ltr"
|
|
className="h-full w-full border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
|
|
/>
|
|
</span>
|
|
</div>
|
|
{showInvalidState ? (
|
|
<span className="block text-[10px] font-semibold text-[#F2465F]">
|
|
Enter a valid phone number with country code.
|
|
</span>
|
|
) : null}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export default QuestionPhone;
|