feat(phone-input): integrate phone number input component and validation
- Added `libphonenumber-js` dependency for improved phone number handling. - Refactored `CreateAdmin` form to utilize a new `PhoneNumberInput` component with validation rules. - Updated `useCreateAdminPresenter` to normalize phone number input before submission. - Enhanced error messages for phone validation in `Texts` constant. - Modified `User` schema to validate phone numbers using E.164 format.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { isValidE164Phone } from "@/lib/phone/e164";
|
||||
import { z } from "zod";
|
||||
import { createApiResponseSchema } from "./Factory";
|
||||
|
||||
@@ -25,7 +26,13 @@ export const UserSchema = z.object({
|
||||
export const CreateAdminSchema = z.object({
|
||||
full_name: z.string().min(1, "Full name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
|
||||
phone: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val?.trim() ? val.trim() : ""))
|
||||
.refine((val) => !val || isValidE164Phone(val), {
|
||||
message: "Invalid phone number",
|
||||
}),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters long")
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
parsePhoneNumberFromString,
|
||||
isValidPhoneNumber,
|
||||
type CountryCode,
|
||||
type PhoneNumber,
|
||||
} from "libphonenumber-js";
|
||||
|
||||
/** E.164: optional leading `+`, up to 15 digits (ITU-T). */
|
||||
export const E164_MAX_DIGITS = 15;
|
||||
export const E164_MAX_LENGTH = E164_MAX_DIGITS + 1;
|
||||
/** Comfortable formatted typing limit in the UI (not storage). */
|
||||
export const PHONE_UI_MAX_LENGTH = 25;
|
||||
|
||||
const regionDisplay = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
|
||||
export function getCountryLabel(code: CountryCode): string {
|
||||
try {
|
||||
return regionDisplay.of(code) ?? code;
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip decorative characters; keep leading `+` and digits only. */
|
||||
export function sanitizePhoneInput(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return "";
|
||||
const hasPlus = trimmed.startsWith("+");
|
||||
const digits = trimmed.replace(/\D/g, "");
|
||||
if (!digits) return hasPlus ? "+" : "";
|
||||
return hasPlus ? `+${digits}` : digits;
|
||||
}
|
||||
|
||||
export function parsePhone(
|
||||
value: string,
|
||||
defaultCountry?: CountryCode,
|
||||
): PhoneNumber | undefined {
|
||||
const sanitized = sanitizePhoneInput(value);
|
||||
if (!sanitized) return undefined;
|
||||
return parsePhoneNumberFromString(sanitized, defaultCountry);
|
||||
}
|
||||
|
||||
/** Returns `+{country}{national}` or `undefined` if not a valid number. */
|
||||
export function normalizeToE164(
|
||||
value: string,
|
||||
defaultCountry?: CountryCode,
|
||||
): string | undefined {
|
||||
const parsed = parsePhone(value, defaultCountry);
|
||||
if (!parsed?.isValid()) return undefined;
|
||||
return parsed.format("E.164");
|
||||
}
|
||||
|
||||
export function isValidE164Phone(value: string): boolean {
|
||||
const sanitized = sanitizePhoneInput(value);
|
||||
if (!sanitized) return false;
|
||||
return isValidPhoneNumber(sanitized);
|
||||
}
|
||||
|
||||
export function formatPhoneForDisplay(
|
||||
e164: string,
|
||||
defaultCountry?: CountryCode,
|
||||
): { country: CountryCode; national: string } | null {
|
||||
const parsed = parsePhone(e164, defaultCountry);
|
||||
if (!parsed) return null;
|
||||
return {
|
||||
country: parsed.country ?? defaultCountry ?? "US",
|
||||
national: parsed.formatNational(),
|
||||
};
|
||||
}
|
||||
|
||||
export function splitE164ForEditing(
|
||||
e164: string | undefined,
|
||||
fallbackCountry: CountryCode = "US",
|
||||
): { country: CountryCode; national: string } {
|
||||
if (!e164?.trim()) {
|
||||
return { country: fallbackCountry, national: "" };
|
||||
}
|
||||
const display = formatPhoneForDisplay(e164, fallbackCountry);
|
||||
if (display) return display;
|
||||
return { country: fallbackCountry, national: e164.replace(/^\+\d{1,3}\s*/, "") };
|
||||
}
|
||||
Reference in New Issue
Block a user