120 lines
No EOL
4 KiB
TypeScript
120 lines
No EOL
4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
|
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label: string;
|
|
error?: string;
|
|
mask?: 'phone' | 'cnpj' | 'cep';
|
|
}
|
|
|
|
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, mask, onChange, ...props }) => {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const isPassword = type === 'password';
|
|
const inputType = isPassword && showPassword ? 'text' : type;
|
|
|
|
const applyMask = (value: string, maskType?: 'phone' | 'cnpj' | 'cep') => {
|
|
if (!maskType) return value;
|
|
|
|
const numbers = value.replace(/\D/g, '');
|
|
|
|
switch (maskType) {
|
|
case 'phone':
|
|
// Limita a 11 dígitos (celular)
|
|
const phoneNumbers = numbers.slice(0, 11);
|
|
if (phoneNumbers.length <= 10) {
|
|
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
|
|
}
|
|
return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
|
|
|
|
case 'cnpj':
|
|
// Limita a 14 dígitos
|
|
const cnpjNumbers = numbers.slice(0, 14);
|
|
return cnpjNumbers.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/, '$1.$2.$3/$4-$5');
|
|
|
|
case 'cep':
|
|
// Limita a 8 dígitos
|
|
const cepNumbers = numbers.slice(0, 8);
|
|
return cepNumbers.replace(/(\d{5})(\d{0,3})/, '$1-$2');
|
|
|
|
default:
|
|
return value;
|
|
}
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (mask) {
|
|
const maskedValue = applyMask(e.target.value, mask);
|
|
e.target.value = maskedValue;
|
|
}
|
|
onChange?.(e);
|
|
};
|
|
|
|
// Define maxLength baseado na máscara
|
|
const getMaxLength = () => {
|
|
if (!mask) return undefined;
|
|
switch (mask) {
|
|
case 'phone': return 15; // (00) 00000-0000
|
|
case 'cnpj': return 18; // 00.000.000/0000-00
|
|
case 'cep': return 9; // 00000-000
|
|
default: return undefined;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
|
{label}
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
|
|
${error ? 'border-red-500' : 'border-gray-300'}
|
|
${isPassword ? 'pr-10' : ''}
|
|
${className}`}
|
|
type={inputType}
|
|
onChange={handleChange}
|
|
maxLength={getMaxLength()}
|
|
{...props}
|
|
/>
|
|
{isPassword && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
label: string;
|
|
options: { value: string; label: string }[];
|
|
error?: string;
|
|
}
|
|
|
|
export const Select: React.FC<SelectProps> = ({ label, options, error, className = '', ...props }) => {
|
|
return (
|
|
<div className="w-full">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
|
{label}
|
|
</label>
|
|
<select
|
|
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors appearance-none bg-white
|
|
${error ? 'border-red-500' : 'border-gray-300'}
|
|
${className}`}
|
|
{...props}
|
|
>
|
|
<option value="" disabled>Selecione uma opção</option>
|
|
{options.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
|
</div>
|
|
);
|
|
}; |