141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { validatePhone } from '@/lib/validation';
|
||
|
||
interface PhoneInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||
value?: string;
|
||
onChange: (val: string) => void;
|
||
label?: string;
|
||
error?: string;
|
||
required?: boolean;
|
||
className?: string;
|
||
placeholder?: string;
|
||
}
|
||
|
||
export const PhoneInput: React.FC<PhoneInputProps> = ({
|
||
value,
|
||
onChange,
|
||
label,
|
||
error: customError,
|
||
required = false,
|
||
className = '',
|
||
placeholder = '98765 43210',
|
||
disabled,
|
||
...props
|
||
}) => {
|
||
const [touched, setTouched] = useState(false);
|
||
|
||
// Normalize current value to bare 10 digits for input display
|
||
let displayValue = value || '';
|
||
if (displayValue.startsWith('+91')) displayValue = displayValue.slice(3);
|
||
else if (displayValue.startsWith('91') && displayValue.length === 12) displayValue = displayValue.slice(2);
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
let raw = e.target.value.replace(/\D/g, ''); // keep only numbers
|
||
if (raw.length > 10) raw = raw.slice(0, 10);
|
||
onChange(raw);
|
||
};
|
||
|
||
const validationError = touched ? validatePhone(displayValue, required) : null;
|
||
const displayError = customError || validationError;
|
||
|
||
return (
|
||
<div className={`space-y-1 ${className}`}>
|
||
{label && (
|
||
<label className="block text-[13px] font-medium text-gray-700">
|
||
{label} {required && <span className="text-red-500">*</span>}
|
||
</label>
|
||
)}
|
||
<div className="relative flex rounded-lg shadow-sm">
|
||
{/* Fixed +91 Prefix Badge */}
|
||
<span className="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-gray-700 font-semibold text-[13px] select-none shrink-0">
|
||
🇮🇳 +91
|
||
</span>
|
||
<input
|
||
{...props}
|
||
type="tel"
|
||
inputMode="numeric"
|
||
pattern="[0-9]*"
|
||
maxLength={10}
|
||
value={displayValue}
|
||
onChange={handleChange}
|
||
onBlur={() => setTouched(true)}
|
||
disabled={disabled}
|
||
placeholder={placeholder}
|
||
className={`block w-full min-w-0 flex-1 rounded-none rounded-r-lg border text-[13px] px-3 py-2 transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20 ${
|
||
displayError
|
||
? 'border-red-500 text-red-900 focus:border-red-500 focus:ring-red-200'
|
||
: 'border-gray-300 focus:border-primary text-gray-900'
|
||
} ${disabled ? 'bg-gray-100 cursor-not-allowed text-gray-500' : 'bg-white'}`}
|
||
/>
|
||
</div>
|
||
{displayError && (
|
||
<p className="text-[11px] font-medium text-red-500 mt-1 flex items-center gap-1">
|
||
<span>⚠️</span> {displayError}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
interface PincodeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||
value?: string;
|
||
onChange: (val: string) => void;
|
||
label?: string;
|
||
error?: string;
|
||
required?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
export const PincodeInput: React.FC<PincodeInputProps> = ({
|
||
value,
|
||
onChange,
|
||
label,
|
||
error: customError,
|
||
required = false,
|
||
className = '',
|
||
placeholder = '6-digit PIN code (e.g. 560001)',
|
||
disabled,
|
||
...props
|
||
}) => {
|
||
const [touched, setTouched] = useState(false);
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
let raw = e.target.value.replace(/\D/g, '');
|
||
if (raw.length > 6) raw = raw.slice(0, 6);
|
||
onChange(raw);
|
||
};
|
||
|
||
const validationError = touched && required && !value ? 'PIN code is required' : (touched && value && value.length !== 6 ? 'PIN code must be exactly 6 digits' : null);
|
||
const displayError = customError || validationError;
|
||
|
||
return (
|
||
<div className={`space-y-1 ${className}`}>
|
||
{label && (
|
||
<label className="block text-[13px] font-medium text-gray-700">
|
||
{label} {required && <span className="text-red-500">*</span>}
|
||
</label>
|
||
)}
|
||
<input
|
||
{...props}
|
||
type="text"
|
||
inputMode="numeric"
|
||
maxLength={6}
|
||
value={value || ''}
|
||
onChange={handleChange}
|
||
onBlur={() => setTouched(true)}
|
||
disabled={disabled}
|
||
placeholder={placeholder}
|
||
className={`block w-full rounded-lg border text-[13px] px-3 py-2 transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20 ${
|
||
displayError
|
||
? 'border-red-500 text-red-900 focus:border-red-500 focus:ring-red-200'
|
||
: 'border-gray-300 focus:border-primary text-gray-900'
|
||
} ${disabled ? 'bg-gray-100 cursor-not-allowed text-gray-500' : 'bg-white'}`}
|
||
/>
|
||
{displayError && (
|
||
<p className="text-[11px] font-medium text-red-500 mt-1 flex items-center gap-1">
|
||
<span>⚠️</span> {displayError}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|