feat/FE/US-34/TASK-54-51-slicing-ui-client-side-validation-stock-adjustment

This commit is contained in:
randy-ar
2025-10-11 02:03:10 +07:00
parent 94a5ce5604
commit aa7b6581d9
12 changed files with 1094 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
'use client';
import { ChangeEventHandler, ReactNode } from 'react';
import { cn } from '@/lib/helper';
export interface RadioOption {
label: string;
value: string;
}
export interface RadioInputProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string;
options: RadioOption[];
variant?: string; // contoh: 'radio-primary', 'radio-secondary', dll
className?: {
wrapper?: string;
label?: string;
radioWrapper?: string;
radio?: string;
};
isError?: boolean;
isValid?: boolean;
errorMessage?: string;
required?: boolean;
disabled?: boolean;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
}
const RadioInput = ({
label,
bottomLabel,
name,
value,
options,
variant = 'radio-primary',
className,
isError,
isValid,
errorMessage,
required = false,
disabled = false,
onChange,
onBlur,
}: RadioInputProps) => {
return (
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
{/* Label atas */}
{label && (
<label
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<span className='text-error ml-1' title='required'>
*
</span>
)}
</label>
)}
{/* Daftar opsi radio */}
<div
className={cn(
'flex flex-row flex-wrap gap-4 items-center',
className?.radioWrapper
)}
>
{options.map((option) => (
<label
key={option.value}
className={cn(
'flex flex-row items-center gap-2 cursor-pointer',
disabled && 'opacity-60 cursor-not-allowed'
)}
>
<input
type='radio'
name={name}
value={option.value}
checked={value === option.value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('radio', variant, className?.radio)}
/>
<span className='text-sm'>{option.label}</span>
</label>
))}
</div>
{/* Label bawah */}
{!isError && bottomLabel && (
<p className='text-sm opacity-60'>{bottomLabel}</p>
)}
{/* Pesan error */}
{isError && errorMessage && (
<p className='text-sm text-error'>{errorMessage}</p>
)}
</div>
);
};
export default RadioInput;
+124
View File
@@ -0,0 +1,124 @@
'use client';
import {
ChangeEventHandler,
FocusEventHandler,
ReactNode,
} from 'react';
import { cn } from '@/lib/helper';
export interface TextAreaProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string | number;
placeholder?: string;
className?: {
wrapper?: string;
label?: string;
inputWrapper?: string;
input?: string;
};
isError?: boolean;
isValid?: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
isLoading?: boolean;
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
cols?: number;
}
const TextArea = ({
label,
bottomLabel,
name,
value,
placeholder,
className,
isError,
isValid,
errorMessage,
startAdornment,
endAdornment,
disabled = false,
required = false,
onChange,
onBlur,
readOnly = false,
isLoading = false,
cols = 3
}: TextAreaProps) => {
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
{
'text-error': isError,
},
className?.label
)}
>
{label}
{required && (
<>
{' '}
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'> *</span>
</span>
</>
)}
</label>
)}
{startAdornment && startAdornment}
<textarea
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
id={name}
name={name}
placeholder={placeholder}
value={value}
cols={cols}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
</div>
);
};
export default TextArea;