add new componenet for refactors

This commit is contained in:
Saeed AB 2025-09-16 22:06:39 +03:30
parent 1f68770efc
commit f1dd7c38ad
6 changed files with 137 additions and 90 deletions

View File

@ -0,0 +1,33 @@
import { useId } from "react";
interface CheckboxProps {
checked: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
className?: string;
id ?:string;
}
export default function CustomCheckBox({
checked,
disabled = false,
onChange,
className = "",
id
}: CheckboxProps) {
const handleChange = (e: any) => {
onChange?.(e.target.checked);
};
return (
<input
id={id}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={handleChange}
className={`form-checkbox ${className}`}
/>
);
}

View File

@ -0,0 +1,42 @@
import { cn } from "~/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "./card";
interface BaseCardProps {
title?: string;
className?: string;
headerClassName?: string;
contentClassName?: string;
children: React.ReactNode;
withHeader?: boolean;
}
export function BaseCard({
title,
className,
headerClassName,
contentClassName,
children,
withHeader = false,
}: BaseCardProps) {
return (
<Card
className={cn(
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center",
className
)}
>
{withHeader && title ? (
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
</CardHeader>
) : title ? (
<div className="border-b-2 border-gray-500/20 pb-2">
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
</div>
) : null}
<CardContent className={cn("py-2 px-4", contentClassName)}>
{children}
</CardContent>
</Card>
);
}

View File

@ -44,8 +44,8 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4 cursor-pointer" /> <X className="h-6 w-6 cursor-pointer" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View File

@ -3,6 +3,7 @@ import { cn } from "~/lib/utils";
import { Eye, EyeOff, AlertCircle, CheckCircle2 } from "lucide-react"; import { Eye, EyeOff, AlertCircle, CheckCircle2 } from "lucide-react";
import { Input } from "./input"; import { Input } from "./input";
import { Label } from "./label"; import { Label } from "./label";
import CustomCheckbox from "./CustomCheckBox";
interface BaseFieldProps { interface BaseFieldProps {
label?: string; label?: string;
@ -65,12 +66,6 @@ export function TextField({
)} )}
<div className="relative"> <div className="relative">
{leftIcon && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground">
{leftIcon}
</div>
)}
<Input <Input
id={id} id={id}
type={type} type={type}
@ -82,39 +77,14 @@ export function TextField({
maxLength={maxLength} maxLength={maxLength}
minLength={minLength} minLength={minLength}
className={cn( className={cn(
"w-full h-12 px-4 font-persian text-right transition-all duration-200", "w-full h-12 outline-none bg-white text-base text-[#5F6284] px-4 font-persian text-right transition-all duration-200",
leftIcon && "pr-10",
(rightIcon || hasError || hasSuccess) && "pl-10",
hasError &&
"border-destructive focus:border-destructive focus:ring-destructive/20",
hasSuccess &&
"border-green-500 focus:border-green-500 focus:ring-green-500/20",
className, className,
)} )}
style={{boxShadow : "none"}}
/> />
{(rightIcon || hasError || hasSuccess) && (
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
{hasError ? (
<AlertCircle className="h-4 w-4 text-destructive" />
) : hasSuccess ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
rightIcon && (
<span className="text-muted-foreground">{rightIcon}</span>
)
)}
</div>
)}
</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 && ( {helper && !error && (
<p className="text-sm text-muted-foreground font-persian">{helper}</p> <p className="text-sm text-muted-foreground font-persian">{helper}</p>
)} )}
@ -217,17 +187,19 @@ export function PasswordField({
autoComplete={autoComplete} autoComplete={autoComplete}
minLength={minLength} minLength={minLength}
className={cn( className={cn(
"w-full h-12 px-4 pl-10 font-persian text-right transition-all duration-200", "w-full h-12 px-4 pl-10 bg-white text-base text-[#5F6284] font-persian text-right transition-all duration-200",
hasError && hasError &&
"border-destructive focus:border-destructive focus:ring-destructive/20", "border-destructive focus:border-destructive focus:ring-destructive/20",
className, className,
)} )}
style={{boxShadow : "none"}}
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-black transition-colors"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? ( {showPassword ? (
@ -318,26 +290,17 @@ export function CheckboxField({
return ( return (
<div className={cn("space-y-2", containerClassName)}> <div className={cn("space-y-2", containerClassName)}>
<div className="flex items-center gap-2"> <div className="flex flex-row-reverse items-center gap-2">
<input <CustomCheckbox
id={id} id={id}
type="checkbox"
checked={checked} checked={checked}
onChange={(e) => onChange(e.target.checked)} onChange={onChange}
disabled={disabled} />
className={cn(
sizes[size],
"text-[var(--color-login-primary)] bg-background border-input rounded focus:ring-[var(--color-login-primary)] focus:ring-2 accent-[var(--color-login-primary)] transition-all duration-200",
disabled && "opacity-50 cursor-not-allowed",
error && "border-destructive focus:ring-destructive",
className,
)}
/>
{label && ( {label && (
<Label <Label
htmlFor={id} htmlFor={id}
className={cn( className={cn(
"text-sm font-persian cursor-pointer", "text-sm font-persian font-light text-white cursor-pointer",
error ? "text-destructive" : "text-foreground", error ? "text-destructive" : "text-foreground",
disabled && "opacity-50 cursor-not-allowed", disabled && "opacity-50 cursor-not-allowed",
required && required &&

View File

@ -1,39 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FunnelChart } from './funnel-chart';
const mockData = [
{ name: "تعداد کل", value: 250, label: "تعداد کل" },
{ name: "نمونه موفق", value: 130, label: "نمونه موفق" },
{ name: "محصولات موفق", value: 70, label: "محصولات موفق" },
{ name: "بهبود یا تغییر موفق", value: 80, label: "بهبود یا تغییر موفق" },
{ name: "محصول جدید", value: 50, label: "محصول جدید" },
];
describe('FunnelChart', () => {
it('renders funnel chart with correct data', () => {
render(<FunnelChart data={mockData} title="قيف فرآیند پروژه ها" />);
expect(screen.getByText('قيف فرآیند پروژه ها')).toBeInTheDocument();
expect(screen.getByText('۱۰۰%')).toBeInTheDocument();
expect(screen.getByText('۲۵%')).toBeInTheDocument();
expect(screen.getByText('ابتدا فرآیند')).toBeInTheDocument();
expect(screen.getByText('انتها فرآیند')).toBeInTheDocument();
});
it('displays funnel data values correctly', () => {
render(<FunnelChart data={mockData} />);
expect(screen.getByText('۲۵۰')).toBeInTheDocument();
expect(screen.getByText('۱۳۰')).toBeInTheDocument();
expect(screen.getByText('۷۰')).toBeInTheDocument();
expect(screen.getByText('۸۰')).toBeInTheDocument();
expect(screen.getByText('۵۰')).toBeInTheDocument();
});
it('renders without title when not provided', () => {
render(<FunnelChart data={mockData} />);
expect(screen.queryByText('قيف فرآیند پروژه ها')).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,48 @@
import { formatNumber } from "~/lib/utils";
import { BaseCard } from "./base-card";
interface MetricCardProps {
title: string;
value: string | number;
percentValue?: string | number;
valueLabel?: string;
percentLabel?: string;
}
export function MetricCard({
title,
value,
percentValue,
valueLabel = "میلیون ریال",
percentLabel = "درصد به کل",
}: MetricCardProps) {
return (
<BaseCard title={title}>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl font-bold text-green-400">
{formatNumber(value)}
</p>
<div className="text-xs text-gray-400 font-persian">
{valueLabel}
</div>
</div>
{percentValue !== undefined && (
<>
<span className="text-5xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-3xl font-bold text-green-400">
{formatNumber(percentValue)}%
</p>
<div className="text-xs text-gray-400 font-persian">
{percentLabel}
</div>
</div>
</>
)}
</div>
</div>
</BaseCard>
);
}