base structure

This commit is contained in:
hamid zarghami
2025-11-12 11:45:13 +03:30
parent d9560b7d32
commit d36b8e40f0
26 changed files with 1083 additions and 8 deletions
+46
View File
@@ -0,0 +1,46 @@
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
import { clx } from '@/helpers/utils'
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'outline'
leftIcon?: ReactNode
rightIcon?: ReactNode
} & ButtonHTMLAttributes<HTMLButtonElement>
const variantClasses: Record<NonNullable<ButtonProps['variant']>, string> = {
primary: 'bg-black text-white hover:bg-black/90',
secondary: 'bg-white text-black border border-black hover:bg-black/5',
outline: 'border border-black text-black hover:bg-black/5'
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const {
children,
className,
variant = 'primary',
leftIcon,
rightIcon,
...rest
} = props
return (
<button
ref={ref}
className={clx(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-black/10 disabled:pointer-events-none disabled:opacity-50',
variantClasses[variant],
className
)}
{...rest}
>
{leftIcon ? <span className='flex items-center'>{leftIcon}</span> : null}
{children}
{rightIcon ? <span className='flex items-center'>{rightIcon}</span> : null}
</button>
)
})
Button.displayName = 'Button'
export default Button
+27
View File
@@ -0,0 +1,27 @@
import { type FC } from 'react'
import { clx } from '@/helpers/utils'
type DividerProps = {
label?: string
className?: string
lineClassName?: string
labelClassName?: string
}
const Divider: FC<DividerProps> = ({ label, className, lineClassName, labelClassName }) => {
return (
<div className={clx('flex items-center gap-4', className)}>
<span className={clx('flex-1 border-t border-dashed border-neutral-200', lineClassName)} />
{label ? (
<span className={clx('text-xs text-neutral-500', labelClassName)}>
{label}
</span>
) : null}
<span className={clx('flex-1 border-t border-dashed border-neutral-200', lineClassName)} />
</div>
)
}
export default Divider
+15
View File
@@ -0,0 +1,15 @@
import { type FC } from 'react'
type Props = {
errorText: string
}
const Error: FC<Props> = (props: Props) => {
return (
<div className='mt-1.5 font-normal text-red-500 text-[11px]'>
{props.errorText}
</div>
)
}
export default Error
+124
View File
@@ -0,0 +1,124 @@
import { type FC, type InputHTMLAttributes, useEffect, useState } from 'react'
import { clx } from '../helpers/utils';
import { Eye, EyeSlash, SearchNormal } from 'iconsax-react';
import Error from './Error';
type Variant = "floating_outlined" | "primary" | "search";
type Props = {
label?: string;
className?: string;
variant?: Variant;
error_text?: string;
onEnter?: () => void;
unit?: string;
seprator?: boolean;
isNotRequired?: boolean;
onChangeSearchFinal?: (value: string) => void;
} & InputHTMLAttributes<HTMLInputElement>
const formatNumber = (value: string | number): string => {
if (!value) return '';
const inputValue = String(value).replace(/,/g, '');
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const Input: FC<Props> = (props: Props) => {
const [formattedValue, setFormattedValue] = useState<string>(
props.value ? formatNumber(props.value as string) : ''
);
const [showPassword, setShowPassword] = useState<boolean>(false)
const [search, setSearch] = useState<string>('')
const inputClass = clx(
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
props.readOnly && 'bg-gray-100 border-0 text-description',
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
props.className
);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (props.seprator) {
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
const formatted = formatNumber(inputValue);
// به‌روزرسانی مقدار قالب‌بندی‌شده
setFormattedValue(formatted);
// ارسال مقدار خام به `onChange` والد
props.onChange?.({
...event,
target: {
...event.target,
value: inputValue,
},
});
} else {
props.onChange?.(event);
}
};
useEffect(() => {
if (props.value) {
setFormattedValue(formatNumber(props.value as string));
} else {
setFormattedValue('');
}
}, [props.value])
useEffect(() => {
if (props.variant === 'search' && props.onChangeSearchFinal) {
const timeout = setTimeout(() => {
props.onChangeSearchFinal?.(search)
}, 1000)
return () => clearTimeout(timeout)
}
}, [search, props])
return (
<div className='w-full'>
<label className='text-sm'>
{props.label}
</label>
<div className='w-full relative mt-1'>
<input {...props} onChange={(e) => {
setSearch(e.target.value)
handleInputChange(e)
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
{
props.type === 'password' &&
<button
type='button'
onClick={() => setShowPassword((oldValue) => !oldValue)}
className='absolute top-0 bottom-0 left-3 my-auto flex h-5 w-5 cursor-pointer items-center justify-center text-black'
>
{
showPassword
? <EyeSlash color='#8C90A3' size={20} />
: <Eye color='#8C90A3' size={20} />
}
</button>
}
{
props.variant === 'search' &&
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
}
{
props.error_text &&
<Error
errorText={props.error_text}
/>
}
</div>
</div>
)
}
export default Input