- Add photographer finance page at /meus-pagamentos with payment history table - Remove university management page and related routes - Update Finance and UserApproval pages with consistent spacing and typography - Fix Dashboard background color to match other pages (bg-gray-50) - Standardize navbar logo sizing across all pages - Change institution field in course form from dropdown to text input - Add year and semester fields for university graduation dates - Improve header spacing on all pages to pt-20 sm:pt-24 md:pt-28 lg:pt-32 - Apply font-serif styling consistently across page headers
153 lines
4.4 KiB
TypeScript
153 lines
4.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-[10px] sm:text-xs font-medium text-gray-700 mb-1 tracking-wide uppercase">
|
|
{label}
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
className={`w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm 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-9 sm:pr-10" : ""}
|
|
${className}`}
|
|
type={inputType}
|
|
onChange={handleChange}
|
|
maxLength={getMaxLength()}
|
|
{...props}
|
|
/>
|
|
{isPassword && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-2 sm:right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
) : (
|
|
<Eye size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{error && (
|
|
<span className="text-[10px] sm: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>
|
|
);
|
|
};
|