From e5185675be4b622268d56d7d1ae71cabe34e0981 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 28 Oct 2025 11:50:14 +0330 Subject: [PATCH] blog category --- src/pages/blogs/Category.tsx | 202 +++++++++++++++++++ src/pages/blogs/components/CategoryModal.tsx | 155 ++++++++++++++ src/pages/blogs/components/index.ts | 1 + src/pages/blogs/hooks/useBlogsData.ts | 25 +++ src/pages/blogs/service/BlogsService.ts | 21 ++ src/pages/blogs/types/Types.ts | 9 +- src/router/Main.tsx | 4 + src/shared/components/BlogSubMenu.tsx | 2 +- 8 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 src/pages/blogs/Category.tsx create mode 100644 src/pages/blogs/components/CategoryModal.tsx diff --git a/src/pages/blogs/Category.tsx b/src/pages/blogs/Category.tsx new file mode 100644 index 0000000..740f379 --- /dev/null +++ b/src/pages/blogs/Category.tsx @@ -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(null) + const [expandedCategories, setExpandedCategories] = useState>(new Set()) + + const { data: categoriesData, isLoading, error } = useGetBlogCategories() + const deleteCategoryMutation = useDeleteBlogCategory() + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+ +
+ ) + } + + 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 = ( + + +
+ {hasChildren && ( + + )} + {!hasChildren &&
} + {category.title_fa} +
+ + + +
+ {category.description || '-'} +
+ + +
+ + + handleDeleteCategory(category._id)} + isLoading={deleteCategoryMutation.isPending} + /> +
+ + + ) + + 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 ( + + + هیچ دسته‌بندی یافت نشد + + + ) + } + + const allRows: ReactElement[] = [] + categories.forEach(category => { + allRows.push(...renderCategory(category)) + }) + return allRows + } + + return ( +
+ +
+
+ +
+ + + + + + + {renderAllCategories()} + +
+ + + +
+
+ + setIsCreateModalOpen(false)} + mode="create" + /> + + +
+ ) +} + +export default BlogCategoryList diff --git a/src/pages/blogs/components/CategoryModal.tsx b/src/pages/blogs/components/CategoryModal.tsx new file mode 100644 index 0000000..80ee469 --- /dev/null +++ b/src/pages/blogs/components/CategoryModal.tsx @@ -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 = ({ open, close, mode, category }) => { + const queryClient = useQueryClient() + const { data: categoriesData } = useGetBlogCategories() + const createCategoryMutation = useCreateBlogCategory() + const updateCategoryMutation = useUpdateBlogCategory() + + const formik = useFormik({ + 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 ( + +
+ + + + +