add new componenet for refactors
This commit is contained in:
parent
1f68770efc
commit
f1dd7c38ad
33
app/components/ui/CustomCheckBox.tsx
Normal file
33
app/components/ui/CustomCheckBox.tsx
Normal 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}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
app/components/ui/base-card.tsx
Normal file
42
app/components/ui/base-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
48
app/components/ui/metric-card.tsx
Normal file
48
app/components/ui/metric-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user