category list and update
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
VITE_BASE_URL = 'https://api.shinan.ir'
|
||||
VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com'
|
||||
VITE_TOKEN_NAME = 'sh_admin_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
||||
@@ -116,4 +116,10 @@ export const Pages = {
|
||||
detail: "/support/detail/",
|
||||
planUsers: "/support/plan-users/",
|
||||
},
|
||||
category: {
|
||||
list: "/category/list",
|
||||
create: "/category/create",
|
||||
update: "/category/update/",
|
||||
detail: "/category/",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,12 +6,14 @@ import SwitchComponent from '../../components/Switch'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import Select from '../../components/Select'
|
||||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const CreateCategory: FC = () => {
|
||||
|
||||
@@ -33,12 +35,14 @@ const CreateCategory: FC = () => {
|
||||
imageUrl: '',
|
||||
description: '',
|
||||
parent: '',
|
||||
theme: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||||
parent: Yup.string().optional(),
|
||||
theme: Yup.string().optional(),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
|
||||
@@ -59,7 +63,7 @@ const CreateCategory: FC = () => {
|
||||
|
||||
createCategoryMutation.mutate(values, {
|
||||
onSuccess: () => {
|
||||
navigate('/admin/categories')
|
||||
navigate(Pages.category.list)
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -140,6 +144,20 @@ const CreateCategory: FC = () => {
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='تم دسته'
|
||||
placeholder='انتخاب تم دسته'
|
||||
items={[
|
||||
{ value: 'Color', label: 'رنگی' },
|
||||
{ value: 'Monochrome', label: 'تکرنگ' },
|
||||
{ value: 'Gradient', label: 'گرادیانت' },
|
||||
{ value: 'Minimal', label: 'مینیمال' },
|
||||
]}
|
||||
value={formik.values.theme}
|
||||
onChange={(e) => formik.setFieldValue('theme', e.target.value)}
|
||||
error_text={formik.touched.theme && formik.errors.theme ? formik.errors.theme : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+98
-16
@@ -1,10 +1,104 @@
|
||||
import { type FC } from 'react'
|
||||
import { type FC, type JSX, useState } from 'react'
|
||||
import Td from '../../components/Td'
|
||||
import { Eye } from 'iconsax-react'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ArrowDown2, ArrowLeft2, Edit } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useGetCategories } from './hooks/useCategoryData'
|
||||
import { type CategoryTreeNode } from './types/Types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const CategoryList: FC = () => {
|
||||
|
||||
const { data, isLoading } = useGetCategories();
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleExpanded = (categoryId: string) => {
|
||||
setExpandedCategories(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(categoryId)) {
|
||||
newSet.delete(categoryId);
|
||||
} else {
|
||||
newSet.add(categoryId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const renderCategoryRow = (category: CategoryTreeNode, level: number = 0) => {
|
||||
return (
|
||||
<tr key={category._id} className='tr'>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden">
|
||||
<img
|
||||
src={category.icon}
|
||||
alt={category.title_fa}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ marginLeft: `${level * 20}px` }}>
|
||||
{category.title_fa}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={category.title_en} />
|
||||
<Td text="">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={category.imageUrl}
|
||||
alt={category.title_fa}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={category.description} />
|
||||
<Td text={category.parent || 'اصلی'} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link to={`${Pages.category.update}${category._id}`} state={{ from: 'list' }}>
|
||||
<Edit size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
{category.children && category.children.length > 0 && (
|
||||
<button
|
||||
onClick={() => toggleExpanded(category._id)}
|
||||
className="flex items-center justify-center w-6 h-6 hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
{expandedCategories.has(category._id) ? (
|
||||
<ArrowDown2 size={16} color="#6B7280" />
|
||||
) : (
|
||||
<ArrowLeft2 size={16} color="#6B7280" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCategories = (categories: CategoryTreeNode[], level: number = 0): JSX.Element[] => {
|
||||
const result: JSX.Element[] = [];
|
||||
|
||||
categories.forEach(category => {
|
||||
// اضافه کردن خود دستهبندی
|
||||
result.push(renderCategoryRow(category, level));
|
||||
|
||||
// اگر دستهبندی expand شده باشد، فرزندانش را اضافه کن
|
||||
if (expandedCategories.has(category._id) && category.children && category.children.length > 0) {
|
||||
result.push(...renderCategories(category.children, level + 1));
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
@@ -21,19 +115,7 @@ const CategoryList: FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className='tr'>
|
||||
<Td text={''} />
|
||||
<Td text={'item.title_fa'} />
|
||||
<Td text={'item.title_fa'} />
|
||||
<Td text={'item.title_fa'} />
|
||||
<Td text={'s'} />
|
||||
<Td text={'asd'} />
|
||||
<Td text={'asd'}>
|
||||
<Link to={Pages.cardBank.detail}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
</Td>
|
||||
</tr>
|
||||
{data?.results?.data ? renderCategories(data.results.data, 0) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import Button from '../../components/Button'
|
||||
import { TickCircle, ArrowLeft } from 'iconsax-react'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import Select from '../../components/Select'
|
||||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useUpdateCategory, useGetCategories, useGetCategoryById, useSingleUpload } from './hooks/useCategoryData'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const UpdateCategory: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [iconFile, setIconFile] = useState<File>()
|
||||
const [imageFile, setImageFile] = useState<File>()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [isActive, setIsActive] = useState(true)
|
||||
const [hasIconChanged, setHasIconChanged] = useState(false)
|
||||
const [hasImageChanged, setHasImageChanged] = useState(false)
|
||||
|
||||
const singleUpload = useSingleUpload()
|
||||
const updateCategoryMutation = useUpdateCategory()
|
||||
const getCategories = useGetCategories()
|
||||
const getCategoryById = useGetCategoryById(id!)
|
||||
|
||||
const formik = useFormik<CategoryType>({
|
||||
initialValues: {
|
||||
title_fa: '',
|
||||
title_en: '',
|
||||
icon: '',
|
||||
imageUrl: '',
|
||||
description: '',
|
||||
parent: null,
|
||||
theme: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||||
parent: Yup.string().nullable().optional(),
|
||||
theme: Yup.string().optional(),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
const updatedValues = { ...values }
|
||||
|
||||
// آپلود آیکون جدید اگر تغییر کرده باشد
|
||||
if (hasIconChanged && iconFile) {
|
||||
const iconRes = await singleUpload.mutateAsync(iconFile)
|
||||
updatedValues.icon = iconRes.results?.url?.url
|
||||
}
|
||||
|
||||
// آپلود تصویر جدید اگر تغییر کرده باشد
|
||||
if (hasImageChanged && imageFile) {
|
||||
const imageRes = await singleUpload.mutateAsync(imageFile)
|
||||
updatedValues.imageUrl = imageRes.results?.url?.url
|
||||
}
|
||||
|
||||
updateCategoryMutation.mutate(
|
||||
{ id: id!, data: updatedValues },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('دسته با موفقیت ویرایش شد')
|
||||
navigate(Pages.category.list)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('خطا در ویرایش دسته')
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// بارگذاری دادههای دسته برای ویرایش
|
||||
useEffect(() => {
|
||||
if (getCategoryById.data?.results?.data) {
|
||||
const categoryData = getCategoryById.data.results.data
|
||||
formik.setValues({
|
||||
title_fa: categoryData.title_fa || '',
|
||||
title_en: categoryData.title_en || '',
|
||||
icon: categoryData.icon || '',
|
||||
imageUrl: categoryData.imageUrl || '',
|
||||
description: categoryData.description || '',
|
||||
parent: categoryData.parent,
|
||||
theme: categoryData.theme || '',
|
||||
})
|
||||
setIsActive(!categoryData.deleted)
|
||||
}
|
||||
}, [getCategoryById.data, formik])
|
||||
|
||||
const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || []
|
||||
|
||||
|
||||
const filteredCategories = categoriesData.filter(cat =>
|
||||
cat.title_fa.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
cat.title_en.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const handleParentSelect = (category: CategoryTreeNode) => {
|
||||
formik.setFieldValue('parent', category._id)
|
||||
}
|
||||
|
||||
const handleIconChange = (files: File[]) => {
|
||||
setIconFile(files[0])
|
||||
setHasIconChanged(true)
|
||||
}
|
||||
|
||||
const handleImageChange = (files: File[]) => {
|
||||
setImageFile(files[0])
|
||||
setHasImageChanged(true)
|
||||
}
|
||||
|
||||
if (getCategoryById.isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-500">در حال بارگذاری...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (getCategoryById.isError) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-red-500">خطا در بارگذاری اطلاعات دسته</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className=''>ویرایش دسته</h1>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-6'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateCategoryMutation.isPending}
|
||||
disabled={!formik.isValid || updateCategoryMutation.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={16} color='#fff' />
|
||||
<div>ذخیره تغییرات</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 xl:mt-8 mt-6'>
|
||||
{/* اطلاعات اصلی */}
|
||||
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||||
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-gray-700'>وضعیت دسته</span>
|
||||
<SwitchComponent
|
||||
active={isActive}
|
||||
onChange={setIsActive}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||
<Input
|
||||
label='عنوان فارسی دسته'
|
||||
placeholder='مثال: الکترونیک'
|
||||
{...formik.getFieldProps('title_fa')}
|
||||
error_text={formik.touched.title_fa && formik.errors.title_fa ? formik.errors.title_fa : ''}
|
||||
/>
|
||||
<Input
|
||||
label='عنوان انگلیسی دسته'
|
||||
placeholder='مثال: Electronics'
|
||||
{...formik.getFieldProps('title_en')}
|
||||
error_text={formik.touched.title_en && formik.errors.title_en ? formik.errors.title_en : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
label='توضیحات دسته'
|
||||
placeholder='توضیحات کامل دسته را وارد کنید...'
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='تم دسته'
|
||||
placeholder='انتخاب تم دسته'
|
||||
items={[
|
||||
{ value: 'Color', label: 'رنگی' },
|
||||
{ value: 'Monochrome', label: 'تکرنگ' },
|
||||
{ value: 'Gradient', label: 'گرادیانت' },
|
||||
{ value: 'Minimal', label: 'مینیمال' },
|
||||
]}
|
||||
value={formik.values.theme}
|
||||
onChange={(e) => formik.setFieldValue('theme', e.target.value)}
|
||||
error_text={formik.touched.theme && formik.errors.theme ? formik.errors.theme : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ستون سمت راست */}
|
||||
<div className='w-80 space-y-6'>
|
||||
{/* آپلود فایلها */}
|
||||
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
|
||||
<h2 className='text-lg font-medium mb-6'>فایلها</h2>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<div>
|
||||
<UploadBox
|
||||
label='آیکون دسته'
|
||||
onChange={handleIconChange}
|
||||
/>
|
||||
{formik.values.icon && !hasIconChanged && (
|
||||
<div className='mt-2 p-2 bg-gray-50 rounded-lg'>
|
||||
<div className='text-xs text-gray-600 mb-2'>آیکون فعلی:</div>
|
||||
<div className='flex justify-center'>
|
||||
<img
|
||||
src={formik.values.icon}
|
||||
alt='آیکون فعلی'
|
||||
className='w-12 h-12 rounded-full object-cover border border-gray-200'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{formik.errors.icon && formik.touched.icon && (
|
||||
<p className='text-red-500 text-xs mt-1'>{formik.errors.icon}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UploadBox
|
||||
label='تصویر دسته'
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
{formik.values.imageUrl && !hasImageChanged && (
|
||||
<div className='mt-2 p-2 bg-gray-50 rounded-lg'>
|
||||
<div className='text-xs text-gray-600 mb-2'>تصویر فعلی:</div>
|
||||
<div className='flex justify-center'>
|
||||
<img
|
||||
src={formik.values.imageUrl}
|
||||
alt='تصویر فعلی'
|
||||
className='w-16 h-16 rounded-lg object-cover border border-gray-200'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{formik.errors.imageUrl && formik.touched.imageUrl && (
|
||||
<p className='text-red-500 text-xs mt-1'>{formik.errors.imageUrl}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* انتخاب دسته والد */}
|
||||
<div className='text-sm bg-white py-6 px-4 rounded-2xl'>
|
||||
<h2 className='text-lg font-medium mb-4'>انتخاب دسته والد</h2>
|
||||
|
||||
{/* جستجو */}
|
||||
<div className='mb-4'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='جستجو در دستهها...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className='w-full px-3 py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white'
|
||||
/>
|
||||
<div className='absolute left-3 top-1/2 transform -translate-y-1/2'>
|
||||
<svg className='w-4 h-4 text-gray-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* درخت دستهبندی */}
|
||||
<div className='bg-gray-50 border border-gray-200 rounded-lg p-3 max-h-72 overflow-y-auto'>
|
||||
{filteredCategories.length > 0 ? (
|
||||
<div className='space-y-2'>
|
||||
{filteredCategories.map((category: CategoryTreeNode) => (
|
||||
<div key={category._id} className='space-y-1'>
|
||||
{/* دسته اصلی */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-3 p-2.5 rounded-lg cursor-pointer transition-colors
|
||||
${category._id === formik.values.parent
|
||||
? 'bg-blue-100 border border-blue-300'
|
||||
: 'hover:bg-gray-100'
|
||||
}
|
||||
`}
|
||||
onClick={() => handleParentSelect(category)}
|
||||
>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
<span className={`text-sm font-medium ${category._id === formik.values.parent ? 'text-blue-700' : 'text-gray-900'}`}>
|
||||
{category.title_fa}
|
||||
</span>
|
||||
{category.children.length > 0 && (
|
||||
<span className='bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs'>
|
||||
{category.children.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{category.title_en}</div>
|
||||
</div>
|
||||
{category._id === formik.values.parent && (
|
||||
<div className='flex-shrink-0'>
|
||||
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* زیردستهها - همیشه باز */}
|
||||
{category.children.length > 0 && (
|
||||
<div className='mr-4 space-y-1'>
|
||||
{category.children.map((child: CategoryTreeNode) => (
|
||||
<div
|
||||
key={child._id}
|
||||
className={`
|
||||
flex items-center gap-3 p-2.5 rounded-lg cursor-pointer transition-colors
|
||||
${child._id === formik.values.parent
|
||||
? 'bg-blue-100 border border-blue-300'
|
||||
: 'hover:bg-gray-100'
|
||||
}
|
||||
`}
|
||||
onClick={() => handleParentSelect(child)}
|
||||
>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
<span className={`text-sm font-medium ${child._id === formik.values.parent ? 'text-blue-700' : 'text-gray-800'}`}>
|
||||
{child.title_fa}
|
||||
</span>
|
||||
{child.children.length > 0 && (
|
||||
<span className='bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full text-xs'>
|
||||
{child.children.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{child.title_en}</div>
|
||||
</div>
|
||||
{child._id === formik.values.parent && (
|
||||
<div className='flex-shrink-0'>
|
||||
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
<svg className='w-12 h-12 mx-auto mb-3 text-gray-300' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1} d='M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z' />
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1} d='M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z' />
|
||||
</svg>
|
||||
<p className='text-sm font-medium text-gray-400 mb-1'>
|
||||
{searchTerm ? 'نتیجهای یافت نشد' : 'دستهای موجود نیست'}
|
||||
</p>
|
||||
<p className='text-xs text-gray-400'>
|
||||
{searchTerm ? 'کلمه جستجو را تغییر دهید' : 'برای شروع، دستهای جدید ایجاد کنید'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateCategory
|
||||
@@ -1,23 +1,23 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { type CategoryType, type SingleUploadResponseType } from "../types/Types";
|
||||
import { type CategoryType, type SingleUploadResponseType, type CategoriesResponseType, type CategoryByIdResponseType } from "../types/Types";
|
||||
|
||||
export const createCategory = async (params: CategoryType) => {
|
||||
const { data } = await axios.post(`/admin/category`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCategories = async () => {
|
||||
export const getCategories = async (): Promise<CategoriesResponseType> => {
|
||||
const { data } = await axios.get(`/category`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCategoryById = async (id: string) => {
|
||||
const { data } = await axios.get(`/admin/category/${id}`);
|
||||
export const getCategoryById = async (id: string): Promise<CategoryByIdResponseType> => {
|
||||
const { data } = await axios.get(`/category/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateCategory = async (id: string, params: Partial<CategoryType>) => {
|
||||
const { data } = await axios.put(`/admin/category/${id}`, params);
|
||||
const { data } = await axios.patch(`/admin/category`, { catId: id, ...params });
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ export type CategoryType = {
|
||||
icon: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
parent: string;
|
||||
parent: string | null;
|
||||
theme?: string;
|
||||
};
|
||||
|
||||
export type CategoryResponseType = {
|
||||
@@ -66,3 +67,11 @@ export type CategoriesResponseType = {
|
||||
data: CategoryTreeNode[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CategoryByIdResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
data: CategoryTreeNode;
|
||||
};
|
||||
};
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ import { clx } from '../helpers/utils'
|
||||
import { useSharedStore } from '../shared/store/sharedStore'
|
||||
import Create from '../pages/category/Create'
|
||||
import List from '../pages/category/List'
|
||||
import Update from '../pages/category/Update'
|
||||
import { Pages } from '../config/Pages'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
@@ -25,8 +27,9 @@ const MainRouter: FC = () => {
|
||||
)}>
|
||||
<div className='pb-20'>
|
||||
<Routes>
|
||||
<Route path='/category/create' element={<Create />} />
|
||||
<Route path='/category/list' element={<List />} />
|
||||
<Route path={Pages.category.create} element={<Create />} />
|
||||
<Route path={Pages.category.list} element={<List />} />
|
||||
<Route path={`${Pages.category.update}:id`} element={<Update />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+41
-4
@@ -27,6 +27,7 @@ import { useSharedStore } from './store/sharedStore'
|
||||
import { clx } from '../helpers/utils'
|
||||
import BlogSubMenu from './components/BlogSubMenu'
|
||||
import SubMenuItem from './components/SubMenuItem'
|
||||
import { Pages } from '../config/Pages'
|
||||
|
||||
|
||||
const SideBar: FC = () => {
|
||||
@@ -47,7 +48,7 @@ const SideBar: FC = () => {
|
||||
split[1] === 'sellers' || split[1] === 'buyers' || split[1] === 'users' ||
|
||||
split[1] === 'tickets' || split[1] === 'reports' || split[1] === 'chat' ||
|
||||
split[1] === 'contact' || split[1] === 'settings' || split[1] === 'pages' ||
|
||||
split[1] === 'jobs') {
|
||||
split[1] === 'jobs' || split[1] === 'category') {
|
||||
setSubMenuName(split[1])
|
||||
setSubtMenu(true)
|
||||
} else {
|
||||
@@ -278,7 +279,8 @@ const SideBar: FC = () => {
|
||||
: subMenuName === 'settings' ? <SettingsSubMenu />
|
||||
: subMenuName === 'pages' ? <PagesSubMenu />
|
||||
: subMenuName === 'jobs' ? <JobsSubMenu />
|
||||
: null
|
||||
: subMenuName === 'category' ? <CategorySubMenu />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -357,8 +359,8 @@ const ProductsSubMenu: FC = () => {
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={'دسته بندی ها'}
|
||||
isActive={isActive('categories')}
|
||||
link="/products/categories"
|
||||
isActive={isActive('category')}
|
||||
link={Pages.category.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'برند ها'}
|
||||
@@ -897,4 +899,39 @@ const JobsSubMenu: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const CategorySubMenu: FC = () => {
|
||||
const location = useLocation()
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b border-border pb-10 px-6'>
|
||||
<Element3
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
دسته بندی ها
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={'لیست دسته ها'}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.category.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'افزودن دسته جدید'}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.category.create}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
Reference in New Issue
Block a user