@@ -12,27 +12,36 @@ type Props = {
|
|||||||
XOR<{ children: ReactNode }, { label: string }>;
|
XOR<{ children: ReactNode }, { label: string }>;
|
||||||
|
|
||||||
const Button: FC<Props> = memo((props: Props) => {
|
const Button: FC<Props> = memo((props: Props) => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
percentage,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
...buttonProps
|
||||||
|
} = props
|
||||||
|
|
||||||
const buttonClass = clx(
|
const buttonClass = clx(
|
||||||
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary w-full',
|
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary w-full',
|
||||||
props.disabled && 'cursor-not-allowed opacity-60',
|
disabled && 'cursor-not-allowed opacity-60',
|
||||||
props.className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button {...props} className={`${buttonClass} ${props.className}`} disabled={props.disabled || props.isLoading}>
|
<button {...buttonProps} className={`${buttonClass} ${className}`} disabled={disabled || isLoading}>
|
||||||
{
|
{
|
||||||
props.isLoading ?
|
isLoading ?
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
{
|
{
|
||||||
!props.percentage ?
|
!percentage ?
|
||||||
<MoonLoader size={20} color='#000' />
|
<MoonLoader size={20} color='#000' />
|
||||||
:
|
:
|
||||||
<span>{props.percentage}%</span>
|
<span>{percentage}%</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
props.label || props.children
|
label || children
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
+21
-11
@@ -17,27 +17,37 @@ type Props = {
|
|||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const Select: FC<Props> = (props: Props) => {
|
const Select: FC<Props> = (props: Props) => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
error_text,
|
||||||
|
placeholder,
|
||||||
|
label,
|
||||||
|
readOnly,
|
||||||
|
...selectProps
|
||||||
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{
|
{
|
||||||
props.label &&
|
label &&
|
||||||
<label className='text-sm text-primary-content'>
|
<label className='text-sm text-primary-content'>
|
||||||
{props.label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<select {...props} className={clx(
|
<select {...selectProps} className={clx(
|
||||||
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
||||||
props.readOnly && 'bg-muted border-0 text-muted-foreground',
|
readOnly && 'bg-muted border-0 text-muted-foreground',
|
||||||
props.className,
|
className,
|
||||||
props.label && 'mt-1'
|
label && 'mt-1'
|
||||||
)}>
|
)}>
|
||||||
{
|
{
|
||||||
props.placeholder &&
|
placeholder &&
|
||||||
<option value="" disabled selected>{props.placeholder}</option>
|
<option value="" disabled>{placeholder}</option>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
props.items?.map((item) => {
|
items?.map((item) => {
|
||||||
return (
|
return (
|
||||||
<option key={item.value} value={item.value} disabled={item.disabled}>
|
<option key={item.value} value={item.value} disabled={item.disabled}>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -51,9 +61,9 @@ const Select: FC<Props> = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
props.error_text && props.error_text !== '' ?
|
error_text && error_text !== '' ?
|
||||||
<div className='text-xs text-right text-red-600 mt-2 mr-2 font-medium'>
|
<div className='text-xs text-right text-red-600 mt-2 mr-2 font-medium'>
|
||||||
{props.error_text}
|
{error_text}
|
||||||
</div>
|
</div>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,12 +54,24 @@ const getCategoriesAtLevel = (
|
|||||||
return parent?.children ?? []
|
return parent?.children ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractList = <T,>(payload: unknown): T[] => {
|
||||||
|
if (Array.isArray(payload)) return payload
|
||||||
|
if (payload && typeof payload === 'object' && 'data' in payload) {
|
||||||
|
const maybeData = (payload as { data?: unknown }).data
|
||||||
|
return Array.isArray(maybeData) ? (maybeData as T[]) : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const ProductsSelect: FC<Props> = (props) => {
|
const ProductsSelect: FC<Props> = (props) => {
|
||||||
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
||||||
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
||||||
|
|
||||||
const { data: categoriesData } = useGetCategories()
|
const { data: categoriesData } = useGetCategories()
|
||||||
const categories = categoriesData?.data ?? []
|
const categories = useMemo(
|
||||||
|
() => extractList<CategoryType>(categoriesData),
|
||||||
|
[categoriesData],
|
||||||
|
)
|
||||||
|
|
||||||
const levelsToShow = useMemo(() => {
|
const levelsToShow = useMemo(() => {
|
||||||
const levels = [0]
|
const levels = [0]
|
||||||
@@ -75,11 +87,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
}, [categories, selectedPath])
|
}, [categories, selectedPath])
|
||||||
|
|
||||||
const activeCategoryId = selectedPath.at(-1) ?? ''
|
const activeCategoryId = selectedPath.at(-1) ?? ''
|
||||||
const activeCategory = activeCategoryId
|
const canLoadProducts = !!activeCategoryId
|
||||||
? findCategoryById(categories, activeCategoryId)
|
|
||||||
: null
|
|
||||||
const isLeafCategory = !!activeCategory && !(activeCategory.children?.length)
|
|
||||||
const canLoadProducts = isLeafCategory
|
|
||||||
|
|
||||||
const { data: productsData } = useGetProducts(activeCategoryId || undefined, {
|
const { data: productsData } = useGetProducts(activeCategoryId || undefined, {
|
||||||
enabled: canLoadProducts,
|
enabled: canLoadProducts,
|
||||||
@@ -90,23 +98,28 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
enabled: needsResolve,
|
enabled: needsResolve,
|
||||||
})
|
})
|
||||||
|
|
||||||
const productItems = useMemo(
|
const products = useMemo(
|
||||||
() => (productsData?.data ?? []).map((product) => ({
|
() => extractList<ProductType>(productsData),
|
||||||
label: product.title,
|
|
||||||
value: product.id,
|
|
||||||
})),
|
|
||||||
[productsData],
|
[productsData],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resolveProducts = useMemo(
|
||||||
|
() => extractList<ProductType>(resolveProductsData),
|
||||||
|
[resolveProductsData],
|
||||||
|
)
|
||||||
|
|
||||||
|
const productItems = useMemo(
|
||||||
|
() => products.map((product) => ({
|
||||||
|
label: product.title,
|
||||||
|
value: product.id,
|
||||||
|
})),
|
||||||
|
[products],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value) {
|
if (!value || selectedPath.length || !categories.length) return
|
||||||
setSelectedPath([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPath.length || !categories.length) return
|
const product = resolveProducts.find((item) => item.id === value)
|
||||||
|
|
||||||
const product = resolveProductsData?.data?.find((item) => item.id === value)
|
|
||||||
if (!product?.category) return
|
if (!product?.category) return
|
||||||
|
|
||||||
const categoryId = typeof product.category === 'string'
|
const categoryId = typeof product.category === 'string'
|
||||||
@@ -117,7 +130,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
if (!path) return
|
if (!path) return
|
||||||
|
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
}, [value, resolveProductsData, categories, selectedPath.length])
|
}, [value, resolveProducts, categories, selectedPath.length])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -125,11 +138,11 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = productsData?.data?.find((item) => item.id === value)
|
const product = products.find((item) => item.id === value)
|
||||||
?? resolveProductsData?.data?.find((item) => item.id === value)
|
?? resolveProducts.find((item) => item.id === value)
|
||||||
|
|
||||||
onProductSelect?.(product)
|
onProductSelect?.(product)
|
||||||
}, [value, productsData, resolveProductsData, onProductSelect])
|
}, [value, products, resolveProducts, onProductSelect])
|
||||||
|
|
||||||
const emitProductChange = (nextValue: string) => {
|
const emitProductChange = (nextValue: string) => {
|
||||||
onChange?.({
|
onChange?.({
|
||||||
@@ -145,7 +158,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
onChange?.(e)
|
onChange?.(e)
|
||||||
|
|
||||||
const product = productsData?.data?.find((item) => item.id === e.target.value)
|
const product = products.find((item) => item.id === e.target.value)
|
||||||
onProductSelect?.(product)
|
onProductSelect?.(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +188,7 @@ const ProductsSelect: FC<Props> = (props) => {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleProductChange}
|
onChange={handleProductChange}
|
||||||
error_text={error_text}
|
error_text={error_text}
|
||||||
disabled={!canLoadProducts}
|
disabled={!activeCategoryId}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
AddTicketType,
|
AddTicketType,
|
||||||
AttributeResponseType,
|
AttributeResponseType,
|
||||||
CategoriesResponseType,
|
CategoryType,
|
||||||
MyRequestsResponseType,
|
MyRequestsResponseType,
|
||||||
RequestDetailResponseType,
|
RequestDetailResponseType,
|
||||||
RequestType,
|
RequestType,
|
||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
} from "../type/Types";
|
} from "../type/Types";
|
||||||
|
|
||||||
export const getCategories = async () => {
|
export const getCategories = async () => {
|
||||||
const { data } = await axios.get<CategoriesResponseType>(`/public/categories`);
|
const { data } = await axios.get<CategoryType[]>(`/public/category/tree?isActive=true`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user