Files
aeroresolve_frontend/src/components/custom/CustomInput.tsx
T

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;