352 lines
8.8 KiB
TypeScript
352 lines
8.8 KiB
TypeScript
import React from "react";
|
|
import { cn } from "~/lib/utils";
|
|
import { Eye, EyeOff, AlertCircle, CheckCircle2 } from "lucide-react";
|
|
import { Input } from "./input";
|
|
import { Label } from "./label";
|
|
import CustomCheckbox from "./CustomCheckBox";
|
|
|
|
interface BaseFieldProps {
|
|
label?: string;
|
|
error?: string;
|
|
helper?: string;
|
|
required?: boolean;
|
|
className?: string;
|
|
containerClassName?: string;
|
|
}
|
|
|
|
interface TextFieldProps extends BaseFieldProps {
|
|
id: string;
|
|
type?: "text" | "email" | "tel" | "url";
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
autoComplete?: string;
|
|
leftIcon?: React.ReactNode;
|
|
rightIcon?: React.ReactNode;
|
|
maxLength?: number;
|
|
minLength?: number;
|
|
}
|
|
|
|
export function TextField({
|
|
id,
|
|
label,
|
|
type = "text",
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
error,
|
|
helper,
|
|
required,
|
|
disabled,
|
|
autoComplete,
|
|
leftIcon,
|
|
rightIcon,
|
|
maxLength,
|
|
minLength,
|
|
className,
|
|
containerClassName,
|
|
}: TextFieldProps) {
|
|
const hasError = !!error;
|
|
const hasSuccess = !hasError && value.length > 0;
|
|
|
|
return (
|
|
<div className={cn("space-y-2", containerClassName)}>
|
|
{label && (
|
|
<Label
|
|
htmlFor={id}
|
|
className={cn(
|
|
"block text-sm font-medium font-persian",
|
|
hasError ? "text-destructive" : "text-foreground",
|
|
required && "after:content-['*'] after:ml-1 after:text-destructive",
|
|
)}
|
|
>
|
|
{label}
|
|
</Label>
|
|
)}
|
|
|
|
<div className="relative">
|
|
<Input
|
|
id={id}
|
|
type={type}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
autoComplete={autoComplete}
|
|
maxLength={maxLength}
|
|
minLength={minLength}
|
|
className={cn(
|
|
"w-full h-12 outline-none bg-white text-base text-[#5F6284] px-4 font-persian text-right transition-all duration-200",
|
|
className,
|
|
)}
|
|
style={{boxShadow : "none"}}
|
|
/>
|
|
|
|
</div>
|
|
|
|
{helper && !error && (
|
|
<p className="text-sm text-muted-foreground font-persian">{helper}</p>
|
|
)}
|
|
|
|
{maxLength && (
|
|
<div className="text-xs text-muted-foreground text-left font-persian">
|
|
{value.length}/{maxLength}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface PasswordFieldProps extends BaseFieldProps {
|
|
id: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
autoComplete?: string;
|
|
showStrength?: boolean;
|
|
minLength?: number;
|
|
}
|
|
|
|
export function PasswordField({
|
|
id,
|
|
label,
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
error,
|
|
helper,
|
|
required,
|
|
disabled,
|
|
autoComplete = "current-password",
|
|
showStrength = false,
|
|
minLength,
|
|
className,
|
|
containerClassName,
|
|
}: PasswordFieldProps) {
|
|
const [showPassword, setShowPassword] = React.useState(false);
|
|
const hasError = !!error;
|
|
|
|
const getPasswordStrength = (
|
|
password: string,
|
|
): {
|
|
score: number;
|
|
text: string;
|
|
color: string;
|
|
} => {
|
|
if (!password) return { score: 0, text: "", color: "" };
|
|
|
|
let score = 0;
|
|
if (password.length >= 8) score++;
|
|
if (/[a-z]/.test(password)) score++;
|
|
if (/[A-Z]/.test(password)) score++;
|
|
if (/[0-9]/.test(password)) score++;
|
|
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
|
|
|
const strength = [
|
|
{ text: "بسیار ضعیف", color: "text-red-500" },
|
|
{ text: "ضعیف", color: "text-orange-500" },
|
|
{ text: "متوسط", color: "text-yellow-500" },
|
|
{ text: "قوی", color: "text-blue-500" },
|
|
{ text: "بسیار قوی", color: "text-green-500" },
|
|
];
|
|
|
|
return {
|
|
score,
|
|
text: strength[Math.min(score, 4)].text,
|
|
color: strength[Math.min(score, 4)].color,
|
|
};
|
|
};
|
|
|
|
const strength = showStrength ? getPasswordStrength(value) : null;
|
|
|
|
return (
|
|
<div className={cn("space-y-2", containerClassName)}>
|
|
{label && (
|
|
<Label
|
|
htmlFor={id}
|
|
className={cn(
|
|
"block text-sm font-medium font-persian",
|
|
hasError ? "text-destructive" : "text-foreground",
|
|
required && "after:content-['*'] after:ml-1 after:text-destructive",
|
|
)}
|
|
>
|
|
{label}
|
|
</Label>
|
|
)}
|
|
|
|
<div className="relative">
|
|
<Input
|
|
id={id}
|
|
type={showPassword ? "text" : "password"}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
autoComplete={autoComplete}
|
|
minLength={minLength}
|
|
className={cn(
|
|
"w-full h-12 px-4 pl-10 bg-white text-base text-[#5F6284] font-persian text-right transition-all duration-200",
|
|
hasError &&
|
|
"border-destructive focus:border-destructive focus:ring-destructive/20",
|
|
className,
|
|
)}
|
|
style={{boxShadow : "none"}}
|
|
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-black transition-colors"
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{showStrength && value && (
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-xs font-persian text-muted-foreground">
|
|
قدرت رمز عبور:
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"text-xs font-persian font-medium",
|
|
strength?.color,
|
|
)}
|
|
>
|
|
{strength?.text}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{[1, 2, 3, 4, 5].map((step) => (
|
|
<div
|
|
key={step}
|
|
className={cn(
|
|
"h-1 flex-1 rounded-full transition-colors duration-200",
|
|
step <= (strength?.score || 0)
|
|
? step <= 2
|
|
? "bg-red-500"
|
|
: step <= 3
|
|
? "bg-yellow-500"
|
|
: step <= 4
|
|
? "bg-blue-500"
|
|
: "bg-green-500"
|
|
: "bg-muted",
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<p className="text-sm text-destructive font-persian flex items-center gap-1">
|
|
<AlertCircle className="h-3 w-3" />
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{helper && !error && (
|
|
<p className="text-sm text-muted-foreground font-persian">{helper}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CheckboxFieldProps extends BaseFieldProps {
|
|
id: string;
|
|
checked: boolean;
|
|
onChange: (checked: boolean) => void;
|
|
disabled?: boolean;
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
export function CheckboxField({
|
|
id,
|
|
label,
|
|
checked,
|
|
onChange,
|
|
error,
|
|
helper,
|
|
required,
|
|
disabled,
|
|
size = "md",
|
|
className,
|
|
containerClassName,
|
|
}: CheckboxFieldProps) {
|
|
const sizes = {
|
|
sm: "w-3 h-3",
|
|
md: "w-4 h-4",
|
|
lg: "w-5 h-5",
|
|
};
|
|
|
|
return (
|
|
<div className={cn("space-y-2", containerClassName)}>
|
|
<div className="flex flex-row-reverse items-center gap-2">
|
|
<CustomCheckbox
|
|
id={id}
|
|
checked={checked}
|
|
onChange={onChange}
|
|
/>
|
|
{label && (
|
|
<Label
|
|
htmlFor={id}
|
|
className={cn(
|
|
"text-sm font-persian font-light text-white cursor-pointer",
|
|
error ? "text-destructive" : "text-foreground",
|
|
disabled && "opacity-50 cursor-not-allowed",
|
|
required &&
|
|
"after:content-['*'] after:ml-1 after:text-destructive",
|
|
)}
|
|
>
|
|
{label}
|
|
</Label>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-destructive font-persian flex items-center gap-1">
|
|
<AlertCircle className="h-3 w-3" />
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{helper && !error && (
|
|
<p className="text-sm text-muted-foreground font-persian">{helper}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FieldGroupProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function FieldGroup({ children, className }: FieldGroupProps) {
|
|
return <div className={cn("space-y-4", className)}>{children}</div>;
|
|
}
|
|
|
|
interface FormActionsProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function FormActions({ children, className }: FormActionsProps) {
|
|
return (
|
|
<div
|
|
className={cn("flex flex-col-reverse sm:flex-row gap-3 pt-4", className)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|