create product
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
VITE_API_BASE_URL = 'http://10.241.118.88:4000'
|
VITE_API_BASE_URL = 'http://172.27.69.89:4000'
|
||||||
VITE_TOKEN_NAME = 'negareh_t'
|
VITE_TOKEN_NAME = 'negareh_t'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'negareh_rt'
|
VITE_REFRESH_TOKEN_NAME = 'negareh_rt'
|
||||||
@@ -9,6 +9,11 @@ export const Paths = {
|
|||||||
create: '/product/category/create',
|
create: '/product/category/create',
|
||||||
list: '/product/category/list',
|
list: '/product/category/list',
|
||||||
update: '/product/category/update/'
|
update: '/product/category/update/'
|
||||||
|
},
|
||||||
|
attribute: {
|
||||||
|
list: '/attribute/list',
|
||||||
|
create: '/attribute/create',
|
||||||
|
update: '/attribute/update'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
|
|||||||
+139
-21
@@ -1,13 +1,87 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import GridWrapper from '@/components/GridWrapper';
|
|
||||||
import Input from '@/components/Input';
|
import Input from '@/components/Input';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import UploadBox from '@/components/UploadBox';
|
|
||||||
import { clx } from '@/helpers/utils';
|
import { clx } from '@/helpers/utils';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
import { AddSquare } from 'iconsax-react'
|
import { AddSquare } from 'iconsax-react'
|
||||||
import { type FC } from 'react'
|
import { useState, type FC } from 'react'
|
||||||
|
import type { CreateProductType } from './types/Types';
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import Textarea from '@/components/Textarea';
|
||||||
|
import CategoriesSelect from './components/CategoriesSelect';
|
||||||
|
import SwitchComponent from '@/components/Switch';
|
||||||
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble';
|
||||||
|
import { useMultiUpload } from '../uploader/hooks/useUploader';
|
||||||
|
import { useCreateProduct } from './hooks/useProductData';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import type { ErrorType } from '@/helpers/types';
|
||||||
|
import { extractErrorMessage } from '@/config/func';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const CreateProduct: FC = () => {
|
const CreateProduct: FC = () => {
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const { mutate: upload, isPending: isUploading } = useMultiUpload()
|
||||||
|
const { mutate: createProduct, isPending } = useCreateProduct()
|
||||||
|
|
||||||
|
const handleSave = (values: CreateProductType) => {
|
||||||
|
values.categoryId = Number(values.categoryId)
|
||||||
|
createProduct(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('عملیات با موفقیت انجام شد')
|
||||||
|
formik.resetForm()
|
||||||
|
navigate(-1)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formik = useFormik<CreateProductType>({
|
||||||
|
initialValues: {
|
||||||
|
categoryId: undefined,
|
||||||
|
desc: '',
|
||||||
|
images: [],
|
||||||
|
isActive: true,
|
||||||
|
linkUrl: '',
|
||||||
|
order: 1,
|
||||||
|
quantities: [],
|
||||||
|
title: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
categoryId: Yup.string().required('این فیلد اجباری می باشد.'),
|
||||||
|
title: Yup.string().required('این فیلد اجباری می باشد.'),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
if (files.length > 0) {
|
||||||
|
upload(files, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
const imagesUrl: string[] = []
|
||||||
|
data.data?.map((item) => {
|
||||||
|
imagesUrl.push(item.url)
|
||||||
|
})
|
||||||
|
values.images = imagesUrl
|
||||||
|
handleSave(values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleSave(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleQuantities = (value: number) => {
|
||||||
|
const array = [...formik.values.quantities]
|
||||||
|
const index: number = array.findIndex(o => o === value)
|
||||||
|
if (index > -1) {
|
||||||
|
array.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
array.push(value)
|
||||||
|
}
|
||||||
|
formik.setFieldValue('quantities', array)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -15,6 +89,8 @@ const CreateProduct: FC = () => {
|
|||||||
<h1 className='text-lg font-light'>محصول جدید</h1>
|
<h1 className='text-lg font-light'>محصول جدید</h1>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={isUploading || isPending}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1.5'>
|
<div className='flex gap-1.5'>
|
||||||
<AddSquare size={18} color='black' />
|
<AddSquare size={18} color='black' />
|
||||||
@@ -23,7 +99,8 @@ const CreateProduct: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 bg-white p-6 rounded-3xl '>
|
<div className='flex gap-6'>
|
||||||
|
<div className='mt-8 bg-white p-6 rounded-3xl flex-1'>
|
||||||
<div className='font-light'>
|
<div className='font-light'>
|
||||||
اطلاعات محصول
|
اطلاعات محصول
|
||||||
</div>
|
</div>
|
||||||
@@ -31,38 +108,79 @@ const CreateProduct: FC = () => {
|
|||||||
<div className='mt-6 rowTwoInput'>
|
<div className='mt-6 rowTwoInput'>
|
||||||
<Input
|
<Input
|
||||||
label='عنوان محصول'
|
label='عنوان محصول'
|
||||||
name='title'
|
{...formik.getFieldProps('title')}
|
||||||
onChange={() => { }}
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='لینک در وبسایت'
|
label='لینک در وبسایت'
|
||||||
name='title'
|
{...formik.getFieldProps('linkUrl')}
|
||||||
onChange={() => { }}
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 rowTwoInput'>
|
||||||
|
<CategoriesSelect
|
||||||
|
{...formik.getFieldProps('categoryId')}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='ترتیب'
|
||||||
|
{...formik.getFieldProps('order')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<UploadBox
|
<Textarea
|
||||||
label='تصویر محصول'
|
label='توضیحات'
|
||||||
onChange={() => { }}
|
{...formik.getFieldProps('desc')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<div>بسته تعداد</div>
|
<div>بسته تعداد</div>
|
||||||
<div className='mt-2.5 flex gap-4'>
|
<div className='mt-2.5 flex gap-4'>
|
||||||
<div className='h-8 w-[72px] text-sm rounded-lg border border-[#C3C7DD] flex items-center justify-center'>100</div>
|
{
|
||||||
<div className='h-8 w-[72px] text-sm rounded-lg border border-[#C3C7DD] flex items-center justify-center'>100</div>
|
[100, 150, 200, 250, 300, 350, 500, 1000].map((item) => {
|
||||||
<div className='h-8 w-[72px] text-sm rounded-lg border border-[#C3C7DD] flex items-center justify-center'>100</div>
|
return (
|
||||||
<div className={clx(
|
<div key={item} className={clx(
|
||||||
'h-8 w-[72px] text-sm rounded-lg border border-[#C3C7DD] flex items-center justify-center',
|
'h-8 w-[72px] cursor-pointer text-sm rounded-lg border border-[#C3C7DD] flex items-center justify-center',
|
||||||
'border-primary '
|
formik.values.quantities.includes(item) && 'border-primary'
|
||||||
)}>100</div>
|
)}
|
||||||
|
onClick={() => handleQuantities(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 bg-white p-6 rounded-3xl '>
|
<div className='bg-white p-6 mt-8 rounded-3xl w-[250px]'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>وضعیت</div>
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.isActive}
|
||||||
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='text-sm mb-4'>
|
||||||
|
تصاویر محصول
|
||||||
|
</div>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label='آپلود تصاویر'
|
||||||
|
onChange={setFiles}
|
||||||
|
isMultiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className='mt-8 bg-white p-6 rounded-3xl '>
|
||||||
<h1 className='font-light text-lg'>
|
<h1 className='font-light text-lg'>
|
||||||
ویژگی های محصول
|
ویژگی های محصول
|
||||||
</h1>
|
</h1>
|
||||||
@@ -110,7 +228,7 @@ const CreateProduct: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</GridWrapper>
|
</GridWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const CreateAttribute = () => {
|
||||||
|
return (
|
||||||
|
<div>CreateAttribute</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateAttribute
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { type FC, type SelectHTMLAttributes } from 'react'
|
||||||
|
import { useGetCategory } from '../hooks/useProductData'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error_text?: string,
|
||||||
|
|
||||||
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
|
const CategoriesSelect: FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { data: categories } = useGetCategory()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label='دسته'
|
||||||
|
placeholder='انتخاب'
|
||||||
|
items={categories?.data?.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
}
|
||||||
|
}) || []}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoriesSelect
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/ProductService";
|
import * as api from "../service/ProductService";
|
||||||
import type { CreateCategoryType } from "../types/Types";
|
import type { CreateCategoryType } from "../types/Types";
|
||||||
|
|
||||||
@@ -35,3 +35,22 @@ export const useGetCategoryDetail = (id: string) => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetProducts = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["products"],
|
||||||
|
queryFn: api.getCategory,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateProduct = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createProduct,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["products"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import { type CategoriesResponse, type CategoryResponse, type CreateCategoryType } from "../types/Types";
|
import { type CategoriesResponse, type CategoryResponse, type CreateCategoryType, type CreateProductType } from "../types/Types";
|
||||||
|
|
||||||
export const getCategory = async () => {
|
export const getCategory = async () => {
|
||||||
const { data } = await axios.get<CategoriesResponse>("/admin/category");
|
const { data } = await axios.get<CategoriesResponse>("/admin/category");
|
||||||
@@ -25,3 +25,8 @@ export const getCategoryDetail = async(id:string) => {
|
|||||||
const { data } = await axios.get<CategoryResponse>(`/admin/category/${id}`)
|
const { data } = await axios.get<CategoryResponse>(`/admin/category/${id}`)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createProduct = async (params:CreateProductType) => {
|
||||||
|
const { data } = await axios.post("/admin/product", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -23,3 +23,14 @@ export type CreateCategoryType = {
|
|||||||
avatarUrl?: string,
|
avatarUrl?: string,
|
||||||
order: number,
|
order: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateProductType = {
|
||||||
|
categoryId?: number,
|
||||||
|
title: string,
|
||||||
|
desc: string,
|
||||||
|
linkUrl: string,
|
||||||
|
quantities: number[],
|
||||||
|
isActive: boolean,
|
||||||
|
images: string[],
|
||||||
|
order: number
|
||||||
|
}
|
||||||
@@ -6,3 +6,9 @@ export const useSingleUpload = () => {
|
|||||||
mutationFn: api.singleUpload,
|
mutationFn: api.singleUpload,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useMultiUpload = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.multiUpload,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import type { SingleUploadResponseType } from "../types/Types";
|
import type {
|
||||||
|
MultiUploadResponseType,
|
||||||
|
SingleUploadResponseType,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
export const singleUpload = async (file: File) => {
|
export const singleUpload = async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -10,3 +13,15 @@ export const singleUpload = async (file: File) => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const multiUpload = async (files: File[]) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
files?.map((file) => {
|
||||||
|
formData.append("files", file);
|
||||||
|
});
|
||||||
|
const { data } = await axios.post<MultiUploadResponseType>(
|
||||||
|
"/admin/multi-file",
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export type SingleUploadType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SingleUploadResponseType = BaseResponse<SingleUploadType>;
|
export type SingleUploadResponseType = BaseResponse<SingleUploadType>;
|
||||||
|
export type MultiUploadResponseType = BaseResponse<SingleUploadType[]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user