blog category
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
import { type FC, useState, type ReactElement } from 'react'
|
||||||
|
import PageTitle from '@/components/PageTitle'
|
||||||
|
import { useGetBlogCategories, useDeleteBlogCategory } from './hooks/useBlogsData'
|
||||||
|
import { type BlogCategory } from './types/Types'
|
||||||
|
import PageLoading from '@/components/PageLoading'
|
||||||
|
import Error from '@/components/Error'
|
||||||
|
import Td from '@/components/Td'
|
||||||
|
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||||
|
import { Edit, ArrowDown2, ArrowLeft2 } from 'iconsax-react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import { CategoryModal } from './components'
|
||||||
|
|
||||||
|
const BlogCategoryList: FC = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||||
|
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false)
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<BlogCategory | null>(null)
|
||||||
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const { data: categoriesData, isLoading, error } = useGetBlogCategories()
|
||||||
|
const deleteCategoryMutation = useDeleteBlogCategory()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<Error errorText="خطا در بارگذاری دستهبندیها" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteCategory = (categoryId: string) => {
|
||||||
|
deleteCategoryMutation.mutate(categoryId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('دستهبندی با موفقیت حذف شد')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['blog-categories'] })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditCategory = (category: BlogCategory) => {
|
||||||
|
setSelectedCategory(category)
|
||||||
|
setIsUpdateModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseUpdateModal = () => {
|
||||||
|
setIsUpdateModalOpen(false)
|
||||||
|
setSelectedCategory(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleExpand = (categoryId: string) => {
|
||||||
|
setExpandedCategories(prev => {
|
||||||
|
const newSet = new Set(prev)
|
||||||
|
if (newSet.has(categoryId)) {
|
||||||
|
newSet.delete(categoryId)
|
||||||
|
} else {
|
||||||
|
newSet.add(categoryId)
|
||||||
|
}
|
||||||
|
return newSet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = categoriesData?.results?.categories || []
|
||||||
|
|
||||||
|
// تابع بازگشتی برای رندر کردن دستهبندیها به صورت درختی
|
||||||
|
const renderCategory = (category: BlogCategory, level: number = 0): ReactElement[] => {
|
||||||
|
const hasChildren = category.children && category.children.length > 0
|
||||||
|
const isExpanded = expandedCategories.has(category._id)
|
||||||
|
|
||||||
|
const categoryRow = (
|
||||||
|
<tr key={category._id} className='tr'>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2" style={{ paddingRight: `${level * 24}px` }}>
|
||||||
|
{hasChildren && (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleExpand(category._id)}
|
||||||
|
className="hover:bg-gray-100 rounded p-1"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ArrowDown2 size={16} color='#8C90A3' />
|
||||||
|
) : (
|
||||||
|
<ArrowLeft2 size={16} color='#8C90A3' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!hasChildren && <div className="w-6"></div>}
|
||||||
|
<span>{category.title_fa}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={category.slug} />
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs truncate">
|
||||||
|
{category.description || '-'}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={() => handleEditCategory(category)}>
|
||||||
|
<Edit
|
||||||
|
color='#8C90A3'
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer hover:text-blue-500"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDeleteCategory(category._id)}
|
||||||
|
isLoading={deleteCategoryMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
|
||||||
|
const rows = [categoryRow]
|
||||||
|
|
||||||
|
// اگر دستهبندی فرزند دارد و باز شده است، آنها را هم نمایش بده
|
||||||
|
if (hasChildren && isExpanded) {
|
||||||
|
category.children.forEach(child => {
|
||||||
|
rows.push(...renderCategory(child, level + 1))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// رندر همه دستهبندیهای اصلی
|
||||||
|
const renderAllCategories = () => {
|
||||||
|
if (categories.length === 0) {
|
||||||
|
return (
|
||||||
|
<tr className='tr'>
|
||||||
|
<td colSpan={5} className="text-center py-8 text-gray-500">
|
||||||
|
هیچ دستهبندی یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRows: ReactElement[] = []
|
||||||
|
categories.forEach(category => {
|
||||||
|
allRows.push(...renderCategory(category))
|
||||||
|
})
|
||||||
|
return allRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageTitle />
|
||||||
|
<div className='flex justify-end mt-5'>
|
||||||
|
<Button
|
||||||
|
label='افزودن دستهبندی'
|
||||||
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
|
className='w-fit'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||||
|
<table className='w-full text-sm'>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={'عنوان'} />
|
||||||
|
<Td text={'اسلاگ'} />
|
||||||
|
<Td text={'توضیحات'} />
|
||||||
|
<Td text={'عملیات'} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{renderAllCategories()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CategoryModal
|
||||||
|
open={isCreateModalOpen}
|
||||||
|
close={() => setIsCreateModalOpen(false)}
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CategoryModal
|
||||||
|
open={isUpdateModalOpen}
|
||||||
|
close={handleCloseUpdateModal}
|
||||||
|
mode="update"
|
||||||
|
category={selectedCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogCategoryList
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { type FC, useEffect } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useCreateBlogCategory, useUpdateBlogCategory, useGetBlogCategories } from '../hooks/useBlogsData'
|
||||||
|
import { type CreateCategoryBlogType, type BlogCategory } from '../types/Types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
interface CategoryModalProps {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
mode: 'create' | 'update'
|
||||||
|
category?: BlogCategory | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryModal: FC<CategoryModalProps> = ({ open, close, mode, category }) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { data: categoriesData } = useGetBlogCategories()
|
||||||
|
const createCategoryMutation = useCreateBlogCategory()
|
||||||
|
const updateCategoryMutation = useUpdateBlogCategory()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateCategoryBlogType>({
|
||||||
|
initialValues: {
|
||||||
|
title_fa: '',
|
||||||
|
slug: '',
|
||||||
|
description: '',
|
||||||
|
parent: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
title_fa: Yup.string().required('عنوان دستهبندی الزامی است'),
|
||||||
|
slug: Yup.string().required('اسلاگ الزامی است'),
|
||||||
|
description: Yup.string().required('توضیحات الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
parent: values.parent || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'create') {
|
||||||
|
createCategoryMutation.mutate(submitData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('دستهبندی با موفقیت ایجاد شد')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['blog-categories'] })
|
||||||
|
formik.resetForm()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (mode === 'update' && category) {
|
||||||
|
updateCategoryMutation.mutate(
|
||||||
|
{ id: category._id, params: submitData },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('دستهبندی با موفقیت ویرایش شد')
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['blog-categories'] })
|
||||||
|
formik.resetForm()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'update' && category) {
|
||||||
|
formik.setValues({
|
||||||
|
title_fa: category.title_fa,
|
||||||
|
slug: category.slug,
|
||||||
|
description: category.description,
|
||||||
|
parent: category.parent || '',
|
||||||
|
})
|
||||||
|
} else if (mode === 'create') {
|
||||||
|
formik.resetForm()
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode, category, open])
|
||||||
|
|
||||||
|
const parentOptions = categoriesData?.results?.categories
|
||||||
|
?.filter((c: BlogCategory) => c._id !== category?._id)
|
||||||
|
?.map((c: BlogCategory) => ({
|
||||||
|
value: c._id,
|
||||||
|
label: c.title_fa
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={close}
|
||||||
|
isHeader={true}
|
||||||
|
title_header={mode === 'create' ? 'افزودن دستهبندی جدید' : 'ویرایش دستهبندی'}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<div className='mt-6 space-y-4 w-[450px]'>
|
||||||
|
<Input
|
||||||
|
label='عنوان دستهبندی'
|
||||||
|
placeholder='مثال: تکنولوژی'
|
||||||
|
{...formik.getFieldProps('title_fa')}
|
||||||
|
error_text={formik.touched.title_fa && formik.errors.title_fa ? formik.errors.title_fa : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='اسلاگ'
|
||||||
|
placeholder='مثال: technology'
|
||||||
|
{...formik.getFieldProps('slug')}
|
||||||
|
error_text={formik.touched.slug && formik.errors.slug ? formik.errors.slug : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label='توضیحات'
|
||||||
|
placeholder='توضیحات دستهبندی را وارد کنید...'
|
||||||
|
{...formik.getFieldProps('description')}
|
||||||
|
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label='دسته والد (اختیاری)'
|
||||||
|
placeholder='انتخاب دسته والد'
|
||||||
|
items={parentOptions}
|
||||||
|
{...formik.getFieldProps('parent')}
|
||||||
|
error_text={formik.touched.parent && formik.errors.parent ? formik.errors.parent : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex gap-3 justify-end mt-6'>
|
||||||
|
<Button
|
||||||
|
label='لغو'
|
||||||
|
onClick={close}
|
||||||
|
className='bg-gray-200 text-gray-700'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={mode === 'create' ? 'ایجاد' : 'بروزرسانی'}
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createCategoryMutation.isPending || updateCategoryMutation.isPending}
|
||||||
|
disabled={!formik.isValid || createCategoryMutation.isPending || updateCategoryMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryModal
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { BlogFormFields } from "./BlogFormFields";
|
export { BlogFormFields } from "./BlogFormFields";
|
||||||
export { FileUploadSection } from "./FileUploadSection";
|
export { FileUploadSection } from "./FileUploadSection";
|
||||||
export { TagManager } from "./TagManager";
|
export { TagManager } from "./TagManager";
|
||||||
|
export { default as CategoryModal } from "./CategoryModal";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
CreateBlogType,
|
CreateBlogType,
|
||||||
UpdateBlogType,
|
UpdateBlogType,
|
||||||
BlogCategoriesResponse,
|
BlogCategoriesResponse,
|
||||||
|
CreateCategoryBlogType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
import { useSharedStore } from "@/shared/store/sharedStore";
|
import { useSharedStore } from "@/shared/store/sharedStore";
|
||||||
export const useGetBlogs = (page: number = 1) => {
|
export const useGetBlogs = (page: number = 1) => {
|
||||||
@@ -48,3 +49,27 @@ export const useDeleteBlog = () => {
|
|||||||
mutationFn: (id: string) => api.deleteBlog(id),
|
mutationFn: (id: string) => api.deleteBlog(id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateBlogCategory = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createBlogCategory,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBlogCategory = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
params: CreateCategoryBlogType;
|
||||||
|
}) => api.updateBlogCategory(id, params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteBlogCategory = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteBlogCategory,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
UpdateBlogType,
|
UpdateBlogType,
|
||||||
BlogCategoriesResponse,
|
BlogCategoriesResponse,
|
||||||
BlogDetailResponse,
|
BlogDetailResponse,
|
||||||
|
CreateCategoryBlogType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getBlogs = async (
|
export const getBlogs = async (
|
||||||
@@ -44,3 +45,23 @@ export const deleteBlog = async (id: string) => {
|
|||||||
const { data } = await axios.delete(`/admin/blog/${id}`);
|
const { data } = await axios.delete(`/admin/blog/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// category
|
||||||
|
|
||||||
|
export const createBlogCategory = async (params: CreateCategoryBlogType) => {
|
||||||
|
const { data } = await axios.post(`/admin/blog/category`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBlogCategory = async (
|
||||||
|
id: string,
|
||||||
|
params: CreateCategoryBlogType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(`/admin/blog/category/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteBlogCategory = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/blog/category/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface BlogCategory {
|
|||||||
description: string;
|
description: string;
|
||||||
parent: string | null;
|
parent: string | null;
|
||||||
leaf: boolean;
|
leaf: boolean;
|
||||||
createdAt: string;
|
createdAt?: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
children: BlogCategory[];
|
children: BlogCategory[];
|
||||||
}
|
}
|
||||||
@@ -99,3 +99,10 @@ export interface BlogDetailResponse {
|
|||||||
blog: Blog;
|
blog: Blog;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateCategoryBlogType = {
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
parent?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import OrderDetail from '@/pages/orders/Detail'
|
|||||||
import BlogsList from '@/pages/blogs/List'
|
import BlogsList from '@/pages/blogs/List'
|
||||||
import CreateBlog from '@/pages/blogs/Create'
|
import CreateBlog from '@/pages/blogs/Create'
|
||||||
import UpdateBlog from '@/pages/blogs/Update'
|
import UpdateBlog from '@/pages/blogs/Update'
|
||||||
|
import BlogCategory from '@/pages/blogs/Category'
|
||||||
import ShippingList from '@/pages/shipment/List'
|
import ShippingList from '@/pages/shipment/List'
|
||||||
import CreateShipment from '@/pages/shipment/Create'
|
import CreateShipment from '@/pages/shipment/Create'
|
||||||
import CouponList from '@/pages/Coupon/List'
|
import CouponList from '@/pages/Coupon/List'
|
||||||
@@ -124,6 +125,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.blog.list} element={<BlogsList />} />
|
<Route path={Pages.blog.list} element={<BlogsList />} />
|
||||||
<Route path={Pages.blog.create} element={<CreateBlog />} />
|
<Route path={Pages.blog.create} element={<CreateBlog />} />
|
||||||
<Route path={`${Pages.blog.detail}:id`} element={<UpdateBlog />} />
|
<Route path={`${Pages.blog.detail}:id`} element={<UpdateBlog />} />
|
||||||
|
<Route path={Pages.blog.category} element={<BlogCategory />} />
|
||||||
|
|
||||||
<Route path={Pages.shipment.list} element={<ShippingList />} />
|
<Route path={Pages.shipment.list} element={<ShippingList />} />
|
||||||
<Route path={Pages.shipment.create} element={<CreateShipment />} />
|
<Route path={Pages.shipment.create} element={<CreateShipment />} />
|
||||||
@@ -192,6 +194,8 @@ const MainRouter: FC = () => {
|
|||||||
|
|
||||||
<Route path={Pages.products.reviews.list} element={<Reviews />} />
|
<Route path={Pages.products.reviews.list} element={<Reviews />} />
|
||||||
<Route path={Pages.products.questions.list} element={<Questions />} />
|
<Route path={Pages.products.questions.list} element={<Questions />} />
|
||||||
|
|
||||||
|
<Route path={Pages.blog.category} element={<BlogCategory />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const BlogSubMenu: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={'دسته بندی'}
|
title={'دسته بندی'}
|
||||||
isActive={isActive('create')}
|
isActive={isActive('category')}
|
||||||
link={Pages.blog.category}
|
link={Pages.blog.category}
|
||||||
/>
|
/>
|
||||||
{/* <SubMenuItem
|
{/* <SubMenuItem
|
||||||
|
|||||||
Reference in New Issue
Block a user