blog category crud

This commit is contained in:
hamid zarghami
2025-04-15 14:58:17 +03:30
parent 63aecce426
commit d3031e5557
11 changed files with 350 additions and 87 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
# VITE_BASE_URL = 'https://api.danakcorp.com'
VITE_BASE_URL = 'http://192.168.1.108:4000'
VITE_BASE_URL = 'https://api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.1.108:4000'
+5 -1
View File
@@ -1,5 +1,6 @@
import { FC, InputHTMLAttributes } from 'react'
import Error from './Error'
import { clx } from '../helpers/utils'
type Props = {
label?: string,
@@ -17,8 +18,11 @@ const Textarea: FC<Props> = (props: Props) => {
}
<textarea
className='border border-border leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs'
{...props}
className={clx(
'border border-border leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs',
props.className
)}
>
</textarea>
+1
View File
@@ -5,4 +5,5 @@ export const SideBarItemHasSubMenu = [
"users",
"tickets",
"learning",
"blog",
];
+102 -66
View File
@@ -1,15 +1,64 @@
import { FC } from 'react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Select from '../../components/Select'
import Input from '../../components/Input'
import Td from '../../components/Td'
import SwitchComponent from '../../components/Switch'
import { TickSquare, More, TickCircle } from 'iconsax-react'
import { TickCircle, Trash } from 'iconsax-react'
import "quill/dist/quill.snow.css";
import Button from '../../components/Button'
import { useCreateBlogCategory, useDeleteBlogCategory, useGetBlogCategories } from './hooks/useBlogData'
import { BlogCategoryType, CreateBlogCategoryType } from './types/BlogTypes'
import moment from 'moment-jalaali'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import Textarea from '../../components/Textarea'
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
import { useSingleUpload } from '../service/hooks/useServiceData'
import { toast } from 'react-toastify'
import UpdateCategory from './components/UpdateCategory'
const BlogCategory: FC = () => {
const { t } = useTranslation('global')
const getBlogCategories = useGetBlogCategories()
const [file, setFile] = useState<File>()
const singleUpload = useSingleUpload()
const createBlogCategory = useCreateBlogCategory()
const deleteBlogCategory = useDeleteBlogCategory()
const formik = useFormik<CreateBlogCategoryType>({
initialValues: {
title: '',
description: '',
iconUrl: '',
isActive: true
},
validationSchema: Yup.object({
title: Yup.string().required(t('errors.required')),
description: Yup.string().required(t('errors.required')),
}),
onSubmit: async (values) => {
if (!file) {
toast.error('فایل را انتخاب کنید')
return
}
const formData = new FormData()
formData.append('file', file)
await singleUpload.mutateAsync(formData, {
onSuccess: (data) => {
values.iconUrl = data.data.url
}
})
createBlogCategory.mutateAsync(values, {
onSuccess: () => {
toast.success('دسته بندی با موفقیت ثبت شد')
setFile(undefined)
formik.resetForm()
getBlogCategories.refetch()
}
})
}
})
return (
<div className='mt-4'>
<div className='flex justify-start items-center'>
@@ -19,86 +68,56 @@ const BlogCategory: FC = () => {
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1'>
<div className='flex items-end w-full justify-between'>
<div className='w-[200px]'>
<Select
items={[
{
label: 'تست',
value: 'active'
}
]}
className='bg-white border'
/>
</div>
<div className='xl:w-[300px] w-full'>
<Input
variant='search'
placeholder={t('ads.search')}
className='bg-white w-full'
/>
</div>
</div>
<div className='bg-white min-h-[calc(100vh-245px)] mt-4 rounded-3xl'>
<div className='relative overflow-x-auto rounded-3xl w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
<tr>
<td className='w-[100px]'>
</td>
<Td text={t('blog.number')} />
<Td text={''} />
<Td text={t('blog.blog_title')} />
<Td text={t('blog.blog_Summary')} />
<Td text={'توضیحات'} />
<Td text={t('blog.category')} />
<Td text={t('blog.shareDate')} />
<Td text={t('blog.status')} />
<Td text={''} />
<Td text={t('')} />
</tr>
</thead>
<tbody>
<tr className='tr'>
{
getBlogCategories.data?.data?.categories?.map((item: BlogCategoryType) => {
return (
<tr key={item.id} className='tr'>
<td>
<div className='flex justify-center'>
<TickSquare size={20} color='black' variant='Bold' />
<img src={item.iconUrl} className='w-10' />
</div>
</td>
<Td text={t('blog.number')} />
<Td text={t('blog.blog_title')} />
<Td text={t('blog.blog_Summary')} />
<Td text={t('blog.category')} />
<Td text={t('blog.shareDate')} />
<Td text={item.title} />
<Td text={item.description} />
<Td text={t('')}>
<SwitchComponent
active={true}
onChange={() => { }}
/>
</Td>
<Td text={''}>
<More size={20} color='black' className='rotate-90' />
</Td>
</tr>
<tr className='tr'>
<td>
<div className='flex justify-center'>
<TickSquare size={20} color='black' variant='Bold' />
</div>
</td>
<Td text={t('blog.number')} />
<Td text={t('blog.blog_title')} />
<Td text={t('blog.blog_Summary')} />
<Td text={t('blog.category')} />
<Td text={t('blog.shareDate')} />
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
<Td text={t('')}>
<SwitchComponent
active={true}
onChange={() => { }}
/>
</Td>
<Td text={''}>
<More size={20} color='black' className='rotate-90' />
<div className='flex gap-2 items-center'>
<UpdateCategory id={item.id} />
<Trash size={20} color='red' onClick={() => {
deleteBlogCategory.mutateAsync(item.id, {
onSuccess: () => {
toast.success('دسته بندی با موفقیت حذف شد')
getBlogCategories.refetch()
}
})
}} />
</div>
</Td>
</tr>
)
})
}
</tbody>
</table>
</div>
@@ -115,27 +134,44 @@ const BlogCategory: FC = () => {
{t('ads.deactive')}
</div>
<SwitchComponent
active={false}
onChange={() => { }}
active={formik.values.isActive}
onChange={(value) => formik.setFieldValue('isActive', value)}
/>
</div>
</div>
<p className='mt-6 text-sm'>{t('blog.category_title')}</p>
<div className='mt-6'>
<Input className='-mt-4' />
<Input
label='عنوان'
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : undefined}
/>
</div>
<p className='mt-6 text-sm'>{t('blog.parent_category')}</p>
<div className='mt-6'>
<Input className='-mt-4' />
<Textarea
label='توضیحات'
{...formik.getFieldProps('description')}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
/>
</div>
<div className='mt-6'>
<UploadBoxDraggble
label='آپلود آیکون'
onChange={(file) => setFile(file[0])}
/>
</div>
<Button
className='w-[172px] absolute bottom-4 left-4'
onClick={() => formik.handleSubmit()}
isLoading={createBlogCategory.isPending || singleUpload.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('blog.submit')}
ذخیره
</div>
</div>
</Button>
+1 -1
View File
@@ -120,7 +120,7 @@ const BlogList: FC = () => {
<Td text={''}>
<p dangerouslySetInnerHTML={{ __html: item.previewContent }}></p>
</Td>
<Td text={item.category.title} />
<Td text={item.category?.title} />
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
{/* <Td text={t('')}>
<SwitchComponent
@@ -0,0 +1,113 @@
import { Edit } from 'iconsax-react'
import { FC, useEffect, useState } from 'react'
import { useGetBlogCategoryDetail, useUpdateBlogCategory } from '../hooks/useBlogData'
import DefaulModal from '../../../components/DefaulModal'
import { useFormik } from 'formik'
import { CreateBlogCategoryType } from '../types/BlogTypes'
import Input from '../../../components/Input'
import Textarea from '../../../components/Textarea'
import UploadBox from '../../../components/UploadBox'
import { useSingleUpload } from '../../service/hooks/useServiceData'
import SwitchComponent from '../../../components/Switch'
import Button from '../../../components/Button'
import { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
interface Props {
id: string
}
const UpdateCategory: FC<Props> = ({ id }) => {
const [isOpen, setIsOpen] = useState(false)
const [file, setFile] = useState<File>()
const { data } = useGetBlogCategoryDetail(id, isOpen)
const singleUpload = useSingleUpload()
const updateBlogCategory = useUpdateBlogCategory()
const formik = useFormik<CreateBlogCategoryType>({
initialValues: {
title: '',
description: '',
iconUrl: '',
isActive: true
},
onSubmit: async (values) => {
if (file) {
const formData = new FormData()
formData.append('file', file)
await singleUpload.mutateAsync(formData, {
onSuccess: (data) => {
values.iconUrl = data.data.url
}
})
}
updateBlogCategory.mutateAsync({ id: id, params: values }, {
onSuccess: () => {
toast.success('دسته بندی با موفقیت ویرایش شد')
setIsOpen(false)
window.location.reload()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
})
useEffect(() => {
if (data?.data?.category) {
formik.setValues(data?.data?.category)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
return (
<div>
<Edit onClick={() => setIsOpen(true)} size={20} color='#888' />
<DefaulModal
open={isOpen}
close={() => setIsOpen(false)}
isHeader
title_header='ویرایش دسته بندی'
>
<div className='mt-8'>
<UploadBox
label='آپلود آیکون'
onChange={(file) => setFile(file[0])}
/>
<div className='mt-6'>
<Input
label='عنوان'
{...formik.getFieldProps('title')}
className='bg-white bg-opacity-25 w-full'
/>
</div>
<div className='mt-6'>
<Textarea
label='توضیحات'
{...formik.getFieldProps('description')}
className='bg-white bg-opacity-25 w-full'
/>
</div>
<div className='mt-6'>
<SwitchComponent
active={formik.values.isActive}
onChange={(value) => formik.setFieldValue('isActive', value)}
/>
</div>
<div className='mt-10 flex justify-end'>
<Button
onClick={() => formik.handleSubmit()}
className='w-fit px-7'
label='ذخیره'
isLoading={updateBlogCategory.isPending}
/>
</div>
</div>
</DefaulModal>
</div >
)
}
export default UpdateCategory
+31 -1
View File
@@ -1,6 +1,6 @@
import * as api from "../service/BlogService";
import { useQuery, useMutation } from "@tanstack/react-query";
import { CreateBlogType } from "../types/BlogTypes";
import { CreateBlogCategoryType, CreateBlogType } from "../types/BlogTypes";
export const useGetBlogCategories = () => {
return useQuery({
@@ -9,6 +9,36 @@ export const useGetBlogCategories = () => {
});
};
export const useCreateBlogCategory = () => {
return useMutation({
mutationFn: api.createBlogCategory,
});
};
export const useUpdateBlogCategory = () => {
return useMutation({
mutationFn: (data: { id: string; params: CreateBlogCategoryType }) =>
api.updateBlogCategory(data.id, data.params),
});
};
export const useDeleteBlogCategory = () => {
return useMutation({
mutationFn: api.deleteBlogCategory,
});
};
export const useGetBlogCategoryDetail = (
id: string,
enabled: boolean = true
) => {
return useQuery({
queryKey: ["blog-category-detail", id],
queryFn: () => api.getBlogCategoryDetail(id),
enabled,
});
};
export const useGetBlogs = (search: string) => {
return useQuery({
queryKey: ["blogs", search],
+24 -1
View File
@@ -1,11 +1,34 @@
import axios from "../../../config/axios";
import { CreateBlogType } from "../types/BlogTypes";
import { CreateBlogCategoryType, CreateBlogType } from "../types/BlogTypes";
export const getBlogCategories = async () => {
const { data } = await axios.get("/blogs/categories/list");
return data;
};
export const createBlogCategory = async (params: CreateBlogCategoryType) => {
const { data } = await axios.post("/blogs/categories", params);
return data;
};
export const updateBlogCategory = async (
id: string,
params: CreateBlogCategoryType
) => {
const { data } = await axios.patch(`/blogs/categories/${id}`, params);
return data;
};
export const deleteBlogCategory = async (id: string) => {
const { data } = await axios.delete(`/blogs/categories/${id}`);
return data;
};
export const getBlogCategoryDetail = async (id: string) => {
const { data } = await axios.get(`/blogs/categories/${id}`);
return data;
};
export const getBlogs = async (search: string) => {
const { data } = await axios.get(`/blogs/list?q=${search}`);
return data;
+9
View File
@@ -39,4 +39,13 @@ export type BlogCategoryType = {
id: string;
title: string;
iconUrl: string;
createdAt: string;
description: string;
};
export type CreateBlogCategoryType = {
title: string;
description: string;
iconUrl: string;
isActive: boolean;
};
+3 -1
View File
@@ -15,7 +15,7 @@ import CustomerSubMenu from './components/CustomerSubMenu'
import TicketSubMenu from './components/TicketSubMenu'
import UsersSubMenu from './components/UsersSubMenu'
import LearningSubMenu from './components/LearningSubMenu'
import BlogSubMenu from './components/BlogSubMenu'
const SideBar: FC = () => {
const { t } = useTranslation('global')
@@ -275,6 +275,8 @@ const SideBar: FC = () => {
<UsersSubMenu />
: subMenuName === 'learning' ?
<LearningSubMenu />
: subMenuName === 'blog' ?
<BlogSubMenu />
: null
}
</div>
+45
View File
@@ -0,0 +1,45 @@
import { DocumentText } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { Pages } from '../../config/Pages'
const BlogSubMenu: FC = () => {
const { t } = useTranslation('global')
const isActive = (name: string) => {
return location.pathname.includes(name)
}
return (
<div className='py-12'>
<div className='flex gap-3 items-center border-b pb-10 px-6'>
<DocumentText
size={24}
color='#000'
variant='Bold'
/>
<div className='text-xs'>
{t('sidebar.blog')}
</div>
</div>
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={'لیست'}
isActive={isActive('list')}
link={Pages.blog.list}
/>
<SubMenuItem
title={'دسته بندی'}
isActive={isActive('create')}
link={Pages.blog.category}
/>
</div>
</div>
)
}
export default BlogSubMenu