93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React from "react";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { Eye, EyeOff } from "lucide-react";
|
||
|
|
|
||
|
|
interface FormFieldProps {
|
||
|
|
label: string;
|
||
|
|
name: string;
|
||
|
|
type?: "text" | "email" | "password" | "tel" | "number";
|
||
|
|
placeholder?: string;
|
||
|
|
error?: string;
|
||
|
|
disabled?: boolean;
|
||
|
|
required?: boolean;
|
||
|
|
className?: string;
|
||
|
|
inputProps?: React.ComponentProps<typeof Input>;
|
||
|
|
showPasswordToggle?: boolean;
|
||
|
|
onPasswordToggle?: () => void;
|
||
|
|
showPassword?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const FormField: React.FC<FormFieldProps> = ({
|
||
|
|
label,
|
||
|
|
name,
|
||
|
|
type = "text",
|
||
|
|
placeholder,
|
||
|
|
error,
|
||
|
|
disabled = false,
|
||
|
|
required = false,
|
||
|
|
className,
|
||
|
|
inputProps,
|
||
|
|
showPasswordToggle = false,
|
||
|
|
onPasswordToggle,
|
||
|
|
showPassword = false,
|
||
|
|
}) => {
|
||
|
|
const inputType = showPasswordToggle && type === "password"
|
||
|
|
? (showPassword ? "text" : "password")
|
||
|
|
: type;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn("space-y-2", className)}>
|
||
|
|
<Label
|
||
|
|
htmlFor={name}
|
||
|
|
className="font-medium text-default-600"
|
||
|
|
>
|
||
|
|
{label}
|
||
|
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||
|
|
</Label>
|
||
|
|
<div className="relative">
|
||
|
|
<Input
|
||
|
|
id={name}
|
||
|
|
name={name}
|
||
|
|
type={inputType}
|
||
|
|
placeholder={placeholder}
|
||
|
|
disabled={disabled}
|
||
|
|
className={cn(
|
||
|
|
"peer",
|
||
|
|
{
|
||
|
|
"border-destructive": error,
|
||
|
|
"pr-10": showPasswordToggle,
|
||
|
|
},
|
||
|
|
inputProps?.className
|
||
|
|
)}
|
||
|
|
aria-invalid={!!error}
|
||
|
|
aria-describedby={error ? `${name}-error` : undefined}
|
||
|
|
{...inputProps}
|
||
|
|
/>
|
||
|
|
{showPasswordToggle && (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onPasswordToggle}
|
||
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded"
|
||
|
|
tabIndex={-1}
|
||
|
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
||
|
|
>
|
||
|
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{error && (
|
||
|
|
<div
|
||
|
|
id={`${name}-error`}
|
||
|
|
className="text-destructive mt-2 text-sm"
|
||
|
|
role="alert"
|
||
|
|
>
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|