blog category crud
This commit is contained in:
@@ -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'
|
||||
@@ -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>
|
||||
|
||||
@@ -5,4 +5,5 @@ export const SideBarItemHasSubMenu = [
|
||||
"users",
|
||||
"tickets",
|
||||
"learning",
|
||||
"blog",
|
||||
];
|
||||
|
||||
+102
-66
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user