141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
import React, { useState, forwardRef } from "react";
|
|
import { Phone, Eye, EyeOff } from "lucide-react";
|
|
|
|
type InputType = "text" | "password" | "number" | "email" | "tel" | "date" | "month" | "datetime-local";
|
|
|
|
interface CustomInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
label?: string;
|
|
type?: InputType;
|
|
leftIcon?: React.ReactNode;
|
|
rightIcon?: React.ReactNode;
|
|
maxLength?: number;
|
|
phonePrefix?: string;
|
|
error?: string;
|
|
containerClassName?: string;
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
|
|
(
|
|
{
|
|
label,
|
|
type = "text",
|
|
leftIcon,
|
|
rightIcon,
|
|
maxLength,
|
|
phonePrefix,
|
|
disabled,
|
|
className = "",
|
|
containerClassName = "",
|
|
error,
|
|
onInput,
|
|
size = "md",
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const isPassword = type === "password";
|
|
const isPhone = phonePrefix !== undefined;
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
const inputType = isPassword && showPassword ? "text" : type;
|
|
|
|
const sizeClasses = {
|
|
sm: "py-1.5 text-sm h-[36px]",
|
|
md: "py-2.5 text-sm h-[42px]",
|
|
lg: "py-3 text-base h-[48px]",
|
|
};
|
|
|
|
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
|
|
if (maxLength && (type === "number" || type === "tel")) {
|
|
const target = e.target as HTMLInputElement;
|
|
if (target.value.length > maxLength) {
|
|
target.value = target.value.slice(0, maxLength);
|
|
}
|
|
}
|
|
onInput?.(e as any);
|
|
};
|
|
|
|
return (
|
|
<div className={`w-full flex flex-col gap-1.5 ${containerClassName}`}>
|
|
{label && (
|
|
<label htmlFor={props.id} className="block text-sm font-medium text-gray-900">
|
|
{label}
|
|
{props.required && <span className="ml-1 text-red-500">*</span>}
|
|
</label>
|
|
)}
|
|
|
|
<div
|
|
className={`relative w-full ${isPhone
|
|
? "flex overflow-hidden rounded-lg border border-gray-300 bg-white focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all duration-200"
|
|
: ""
|
|
}`}
|
|
>
|
|
{isPhone && (
|
|
<div className="flex items-center gap-2 border-r border-gray-300 bg-gray-50 px-3 text-sm font-semibold text-gray-900">
|
|
<Phone size={16} className="text-gray-500" />
|
|
{phonePrefix}
|
|
</div>
|
|
)}
|
|
|
|
{!isPhone && leftIcon && (
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
|
|
{leftIcon}
|
|
</span>
|
|
)}
|
|
|
|
<input
|
|
ref={ref}
|
|
type={inputType}
|
|
maxLength={
|
|
type === "number" || type === "tel" ? undefined : maxLength
|
|
}
|
|
onInput={handleInput}
|
|
disabled={disabled}
|
|
className={`
|
|
${isPhone
|
|
? `w-full px-3 ${sizeClasses[size]} bg-white outline-none placeholder:text-gray-500`
|
|
: `
|
|
w-full rounded-lg
|
|
bg-white text-gray-900
|
|
border ${error ? "border-red-500" : "border-gray-300"}
|
|
px-3 ${sizeClasses[size]}
|
|
outline-none
|
|
transition-all duration-200
|
|
hover:border-primary
|
|
focus:border-primary focus:ring-2 focus:ring-primary/20
|
|
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed disabled:border-gray-300
|
|
placeholder:text-gray-500
|
|
${leftIcon ? "pl-10" : ""}
|
|
${rightIcon || isPassword ? "pr-10" : ""}
|
|
`
|
|
}
|
|
${className}
|
|
`}
|
|
{...props}
|
|
/>
|
|
|
|
{!isPhone && isPassword ? (
|
|
<span
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-slate-400 select-none hover:text-primary"
|
|
>
|
|
{showPassword ? <Eye size={20} /> : <EyeOff size={20} />}
|
|
</span>
|
|
) : (
|
|
!isPhone &&
|
|
rightIcon && (
|
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400">
|
|
{rightIcon}
|
|
</span>
|
|
)
|
|
)}
|
|
</div>
|
|
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
export default CustomInput;
|