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_TOKEN_NAME = 'sh_admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
||||||
@@ -116,4 +116,10 @@ export const Pages = {
|
|||||||
detail: "/support/detail/",
|
detail: "/support/detail/",
|
||||||
planUsers: "/support/plan-users/",
|
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 Input from '../../components/Input'
|
||||||
import Textarea from '../../components/Textarea'
|
import Textarea from '../../components/Textarea'
|
||||||
import UploadBox from '../../components/UploadBox'
|
import UploadBox from '../../components/UploadBox'
|
||||||
|
import Select from '../../components/Select'
|
||||||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData'
|
import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
|
||||||
const CreateCategory: FC = () => {
|
const CreateCategory: FC = () => {
|
||||||
|
|
||||||
@@ -33,12 +35,14 @@ const CreateCategory: FC = () => {
|
|||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
description: '',
|
description: '',
|
||||||
parent: '',
|
parent: '',
|
||||||
|
theme: '',
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||||||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||||||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||||||
parent: Yup.string().optional(),
|
parent: Yup.string().optional(),
|
||||||
|
theme: Yup.string().optional(),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ const CreateCategory: FC = () => {
|
|||||||
|
|
||||||
createCategoryMutation.mutate(values, {
|
createCategoryMutation.mutate(values, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
navigate('/admin/categories')
|
navigate(Pages.category.list)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -140,6 +144,20 @@ const CreateCategory: FC = () => {
|
|||||||
{...formik.getFieldProps('description')}
|
{...formik.getFieldProps('description')}
|
||||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.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>
|
</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 Td from '../../components/Td'
|
||||||
import { Eye } from 'iconsax-react'
|
import { ArrowDown2, ArrowLeft2, Edit } from 'iconsax-react'
|
||||||
import { Pages } from '../../config/Pages'
|
|
||||||
import { Link } from 'react-router-dom'
|
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 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
@@ -21,19 +115,7 @@ const CategoryList: FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className='tr'>
|
{data?.results?.data ? renderCategories(data.results.data, 0) : null}
|
||||||
<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>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 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) => {
|
export const createCategory = async (params: CategoryType) => {
|
||||||
const { data } = await axios.post(`/admin/category`, params);
|
const { data } = await axios.post(`/admin/category`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategories = async () => {
|
export const getCategories = async (): Promise<CategoriesResponseType> => {
|
||||||
const { data } = await axios.get(`/category`);
|
const { data } = await axios.get(`/category`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoryById = async (id: string) => {
|
export const getCategoryById = async (id: string): Promise<CategoryByIdResponseType> => {
|
||||||
const { data } = await axios.get(`/admin/category/${id}`);
|
const { data } = await axios.get(`/category/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCategory = async (id: string, params: Partial<CategoryType>) => {
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export type CategoryType = {
|
|||||||
icon: string;
|
icon: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
description: string;
|
description: string;
|
||||||
parent: string;
|
parent: string | null;
|
||||||
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryResponseType = {
|
export type CategoryResponseType = {
|
||||||
@@ -66,3 +67,11 @@ export type CategoriesResponseType = {
|
|||||||
data: CategoryTreeNode[];
|
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 { useSharedStore } from '../shared/store/sharedStore'
|
||||||
import Create from '../pages/category/Create'
|
import Create from '../pages/category/Create'
|
||||||
import List from '../pages/category/List'
|
import List from '../pages/category/List'
|
||||||
|
import Update from '../pages/category/Update'
|
||||||
|
import { Pages } from '../config/Pages'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -25,8 +27,9 @@ const MainRouter: FC = () => {
|
|||||||
)}>
|
)}>
|
||||||
<div className='pb-20'>
|
<div className='pb-20'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/category/create' element={<Create />} />
|
<Route path={Pages.category.create} element={<Create />} />
|
||||||
<Route path='/category/list' element={<List />} />
|
<Route path={Pages.category.list} element={<List />} />
|
||||||
|
<Route path={`${Pages.category.update}:id`} element={<Update />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+41
-4
@@ -27,6 +27,7 @@ import { useSharedStore } from './store/sharedStore'
|
|||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
import BlogSubMenu from './components/BlogSubMenu'
|
import BlogSubMenu from './components/BlogSubMenu'
|
||||||
import SubMenuItem from './components/SubMenuItem'
|
import SubMenuItem from './components/SubMenuItem'
|
||||||
|
import { Pages } from '../config/Pages'
|
||||||
|
|
||||||
|
|
||||||
const SideBar: FC = () => {
|
const SideBar: FC = () => {
|
||||||
@@ -47,7 +48,7 @@ const SideBar: FC = () => {
|
|||||||
split[1] === 'sellers' || split[1] === 'buyers' || split[1] === 'users' ||
|
split[1] === 'sellers' || split[1] === 'buyers' || split[1] === 'users' ||
|
||||||
split[1] === 'tickets' || split[1] === 'reports' || split[1] === 'chat' ||
|
split[1] === 'tickets' || split[1] === 'reports' || split[1] === 'chat' ||
|
||||||
split[1] === 'contact' || split[1] === 'settings' || split[1] === 'pages' ||
|
split[1] === 'contact' || split[1] === 'settings' || split[1] === 'pages' ||
|
||||||
split[1] === 'jobs') {
|
split[1] === 'jobs' || split[1] === 'category') {
|
||||||
setSubMenuName(split[1])
|
setSubMenuName(split[1])
|
||||||
setSubtMenu(true)
|
setSubtMenu(true)
|
||||||
} else {
|
} else {
|
||||||
@@ -278,7 +279,8 @@ const SideBar: FC = () => {
|
|||||||
: subMenuName === 'settings' ? <SettingsSubMenu />
|
: subMenuName === 'settings' ? <SettingsSubMenu />
|
||||||
: subMenuName === 'pages' ? <PagesSubMenu />
|
: subMenuName === 'pages' ? <PagesSubMenu />
|
||||||
: subMenuName === 'jobs' ? <JobsSubMenu />
|
: subMenuName === 'jobs' ? <JobsSubMenu />
|
||||||
: null
|
: subMenuName === 'category' ? <CategorySubMenu />
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -357,8 +359,8 @@ const ProductsSubMenu: FC = () => {
|
|||||||
<div className='flex flex-col mt-10 gap-4'>
|
<div className='flex flex-col mt-10 gap-4'>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={'دسته بندی ها'}
|
title={'دسته بندی ها'}
|
||||||
isActive={isActive('categories')}
|
isActive={isActive('category')}
|
||||||
link="/products/categories"
|
link={Pages.category.list}
|
||||||
/>
|
/>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={'برند ها'}
|
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
|
export default SideBar
|
||||||
Reference in New Issue
Block a user