blogs crud
This commit is contained in:
@@ -150,5 +150,7 @@ export const Pages = {
|
|||||||
detail: "/orders/native/",
|
detail: "/orders/native/",
|
||||||
cancellationCategories: "/orders/cancellation-categories",
|
cancellationCategories: "/orders/cancellation-categories",
|
||||||
returnCategories: "/orders/return-categories",
|
returnCategories: "/orders/return-categories",
|
||||||
|
detailUser: "/orders/detail-user/",
|
||||||
|
detailSeller: "/orders/detail-seller/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { type BlogFormType, type BlogCategory } from './types/Types'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useCreateBlog, useGetBlogCategories } from './hooks/useBlogsData'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useUploadSingle } from '../uploader/hooks/useUploaderData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import { BlogFormFields, FileUploadSection } from './components'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
|
||||||
|
const CreateBlog: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { data: categoriesData } = useGetBlogCategories()
|
||||||
|
const uploadSingle = useUploadSingle()
|
||||||
|
const createBlogMutation = useCreateBlog()
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
|
||||||
|
const formik = useFormik<BlogFormType>({
|
||||||
|
initialValues: {
|
||||||
|
title_fa: '',
|
||||||
|
slug: '',
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
content: '',
|
||||||
|
tags: [],
|
||||||
|
imageUrl: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
title_fa: Yup.string().required('عنوان فارسی بلاگ الزامی است'),
|
||||||
|
slug: Yup.string().required('اسلاگ بلاگ الزامی است'),
|
||||||
|
category: Yup.string().required('دسته بندی بلاگ الزامی است'),
|
||||||
|
description: Yup.string().required('توضیحات بلاگ الزامی است'),
|
||||||
|
content: Yup.string().required('محتوای بلاگ الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
let imageUrl = values.imageUrl
|
||||||
|
|
||||||
|
if (imageFile.length > 0) {
|
||||||
|
const imageResponse = await uploadSingle.mutateAsync(imageFile[0])
|
||||||
|
imageUrl = imageResponse.results?.url?.url || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
imageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
createBlogMutation.mutate(submitData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('بلاگ با موفقیت ایجاد شد')
|
||||||
|
navigate(Pages.blog.list)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoryOptions = categoriesData?.results?.categories?.map((category: BlogCategory) => ({
|
||||||
|
value: category._id,
|
||||||
|
label: category.title_fa
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<h1>ساخت بلاگ جدید</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className='px-6'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createBlogMutation.isPending || uploadSingle.isPending}
|
||||||
|
disabled={!formik.isValid || createBlogMutation.isPending || uploadSingle.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>
|
||||||
|
<BlogFormFields formik={formik} categoryOptions={categoryOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* فایلها */}
|
||||||
|
<FileUploadSection onFileChange={setImageFile} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateBlog
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useDeleteBlog, useGetBlogs } from './hooks/useBlogsData'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { type Blog } from './types/Types'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import Error from '../../components/Error'
|
||||||
|
import PaginationUi from '../../components/PaginationUi'
|
||||||
|
import Td from '../../components/Td'
|
||||||
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
|
import { Edit, Eye } from 'iconsax-react'
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
|
||||||
|
const BlogsList: FC = () => {
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const deleteBlog = useDeleteBlog()
|
||||||
|
const { data: blogsData, isLoading, error } = useGetBlogs(currentPage)
|
||||||
|
|
||||||
|
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 blogs = blogsData?.blogs || []
|
||||||
|
const pager = blogsData?.pager
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='flex justify-end mt-5'>
|
||||||
|
<Button
|
||||||
|
label='افزودن بلاگ'
|
||||||
|
onClick={() => navigate(`${Pages.blog.create}`)}
|
||||||
|
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={'نویسنده'} />
|
||||||
|
<Td text={'بازدید'} />
|
||||||
|
<Td text={'تاریخ ایجاد'} />
|
||||||
|
<Td text={'عملیات'} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{blogs.length === 0 ? (
|
||||||
|
<tr className='tr'>
|
||||||
|
<td colSpan={7} className="text-center py-8 text-gray-500">
|
||||||
|
هیچ بلاگی یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
blogs.map((blog: Blog) => (
|
||||||
|
<tr key={blog._id} className='tr'>
|
||||||
|
<Td text="">
|
||||||
|
<div className="w-12 h-12 rounded-lg overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={blog.imageUrl || '/placeholder-image.png'}
|
||||||
|
alt={blog.title_fa}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{blog.title_fa}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 text-xs">
|
||||||
|
{blog.slug}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={blog.category.title_fa} />
|
||||||
|
<Td text={blog.author.fullName} />
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Eye size={14} className="text-gray-400" />
|
||||||
|
<span>{blog.view.toLocaleString('fa-IR')}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text={blog.createdAt} />
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link to={`${Pages.blog.detail}${blog._id}`}>
|
||||||
|
<Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => {
|
||||||
|
deleteBlog.mutate(blog._id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['blogs'] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PaginationUi
|
||||||
|
pager={pager}
|
||||||
|
onPageChange={(page) => {
|
||||||
|
setCurrentPage(page)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogsList
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { type FC, useState, useEffect } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { type BlogFormType } from './types/Types'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useGetBlogDetail, useGetBlogCategories, useUpdateBlog } from './hooks/useBlogsData'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useUploadSingle } from '../uploader/hooks/useUploaderData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import { BlogFormFields, FileUploadSection } from './components'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
|
||||||
|
const UpdateBlog: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const { data: blogDetail, isLoading: blogDetailLoading, error: blogDetailError } = useGetBlogDetail(id!)
|
||||||
|
const { data: categoriesData } = useGetBlogCategories()
|
||||||
|
const uploadSingle = useUploadSingle()
|
||||||
|
const updateBlogMutation = useUpdateBlog()
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
const [blogId, setBlogId] = useState<string>('')
|
||||||
|
|
||||||
|
const formik = useFormik<BlogFormType>({
|
||||||
|
initialValues: {
|
||||||
|
title_fa: '',
|
||||||
|
slug: '',
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
content: '',
|
||||||
|
tags: [],
|
||||||
|
imageUrl: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
title_fa: Yup.string().required('عنوان فارسی بلاگ الزامی است'),
|
||||||
|
slug: Yup.string().required('اسلاگ بلاگ الزامی است'),
|
||||||
|
category: Yup.string().required('دسته بندی بلاگ الزامی است'),
|
||||||
|
description: Yup.string().required('توضیحات بلاگ الزامی است'),
|
||||||
|
content: Yup.string().required('محتوای بلاگ الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
let imageUrl = values.imageUrl
|
||||||
|
|
||||||
|
if (imageFile.length > 0) {
|
||||||
|
const imageResponse = await uploadSingle.mutateAsync(imageFile[0])
|
||||||
|
imageUrl = imageResponse.results?.url?.url || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { slug: _slug, ...valuesWithoutSlug } = values
|
||||||
|
const submitData = {
|
||||||
|
...valuesWithoutSlug,
|
||||||
|
imageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBlogMutation.mutate({ _id: blogId, ...submitData }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('بلاگ با موفقیت ویرایش شد')
|
||||||
|
navigate(Pages.blog.list)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blogDetail) {
|
||||||
|
setBlogId(blogDetail._id)
|
||||||
|
formik.setValues({
|
||||||
|
title_fa: blogDetail.title_fa,
|
||||||
|
slug: blogDetail.slug,
|
||||||
|
category: blogDetail.category._id,
|
||||||
|
description: blogDetail.description,
|
||||||
|
content: blogDetail.content,
|
||||||
|
tags: blogDetail.tags,
|
||||||
|
imageUrl: blogDetail.imageUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [blogDetail, formik])
|
||||||
|
|
||||||
|
const categoryOptions = categoriesData?.results?.categories?.map((category) => ({
|
||||||
|
value: category._id,
|
||||||
|
label: category.title_fa
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
if (blogDetailLoading) {
|
||||||
|
return <div>در حال بارگذاری...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blogDetailError) {
|
||||||
|
return <div>خطا در بارگذاری دادهها</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<h1>ویرایش بلاگ</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className='px-6'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateBlogMutation.isPending || uploadSingle.isPending}
|
||||||
|
disabled={!formik.isValid || updateBlogMutation.isPending || uploadSingle.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>
|
||||||
|
<BlogFormFields formik={formik} categoryOptions={categoryOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* فایلها */}
|
||||||
|
<FileUploadSection onFileChange={setImageFile} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateBlog
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { type FormikProps } from 'formik'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Select from '../../../components/Select'
|
||||||
|
import Textarea from '../../../components/Textarea'
|
||||||
|
import { type BlogFormType } from '../types/Types'
|
||||||
|
import { TagManager } from './TagManager'
|
||||||
|
|
||||||
|
interface BlogFormFieldsProps {
|
||||||
|
formik: FormikProps<BlogFormType>
|
||||||
|
categoryOptions: { value: string; label: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlogFormFields: FC<BlogFormFieldsProps> = ({ formik, categoryOptions }) => {
|
||||||
|
const getFieldError = (field: keyof BlogFormType) => {
|
||||||
|
const error = formik.touched[field] && formik.errors[field]
|
||||||
|
return error ? (Array.isArray(error) ? error[0] : error) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||||
|
<Input
|
||||||
|
label='عنوان فارسی بلاگ'
|
||||||
|
placeholder='مثال: راهنمای خرید گوشی'
|
||||||
|
{...formik.getFieldProps('title_fa')}
|
||||||
|
error_text={getFieldError('title_fa')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label='اسلاگ بلاگ'
|
||||||
|
placeholder='مثال: rahnamad-kharid-goshi'
|
||||||
|
{...formik.getFieldProps('slug')}
|
||||||
|
error_text={getFieldError('slug')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label='دستهبندی'
|
||||||
|
placeholder='انتخاب دستهبندی بلاگ'
|
||||||
|
items={categoryOptions}
|
||||||
|
{...formik.getFieldProps('category')}
|
||||||
|
error_text={getFieldError('category')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label='توضیحات بلاگ'
|
||||||
|
placeholder='توضیحات کوتاه بلاگ را وارد کنید...'
|
||||||
|
{...formik.getFieldProps('description')}
|
||||||
|
error_text={getFieldError('description')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label='محتوای بلاگ'
|
||||||
|
placeholder='محتوای کامل بلاگ را وارد کنید...'
|
||||||
|
rows={10}
|
||||||
|
{...formik.getFieldProps('content')}
|
||||||
|
error_text={getFieldError('content')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagManager
|
||||||
|
tags={formik.values.tags}
|
||||||
|
onTagsChange={(tags) => formik.setFieldValue('tags', tags)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
|
|
||||||
|
interface FileUploadSectionProps {
|
||||||
|
onFileChange: (files: File[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadSection: FC<FileUploadSectionProps> = ({ onFileChange }) => {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label='تصویر شاخص بلاگ'
|
||||||
|
isMultiple={false}
|
||||||
|
onChange={onFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTagManager } from '../hooks/useTagManager'
|
||||||
|
|
||||||
|
interface TagManagerProps {
|
||||||
|
tags: string[]
|
||||||
|
onTagsChange: (tags: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagManager: FC<TagManagerProps> = ({ tags, onTagsChange }) => {
|
||||||
|
const { tagInput, setTagInput, removeTag } = useTagManager(tags)
|
||||||
|
|
||||||
|
const handleAddTag = () => {
|
||||||
|
const trimmedTag = tagInput.trim()
|
||||||
|
if (trimmedTag && !tags.includes(trimmedTag)) {
|
||||||
|
onTagsChange([...tags, trimmedTag])
|
||||||
|
setTagInput('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveTag = (tagToRemove: string) => {
|
||||||
|
removeTag(tagToRemove)
|
||||||
|
onTagsChange(tags.filter(tag => tag !== tagToRemove))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDownWithUpdate = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleAddTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">برچسبها</label>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveTag(tag)}
|
||||||
|
className="text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<CloseCircle color='red' size={16} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder='برچسب را تایپ کنید و Enter بزنید...'
|
||||||
|
value={tagInput}
|
||||||
|
onChange={(e) => setTagInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDownWithUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { BlogFormFields } from "./BlogFormFields";
|
||||||
|
export { FileUploadSection } from "./FileUploadSection";
|
||||||
|
export { TagManager } from "./TagManager";
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/BlogsService";
|
||||||
|
import type {
|
||||||
|
GetBlogsResult,
|
||||||
|
Blog,
|
||||||
|
CreateBlogType,
|
||||||
|
UpdateBlogType,
|
||||||
|
BlogCategoriesResponse,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const useGetBlogs = (page: number = 1) => {
|
||||||
|
return useQuery<GetBlogsResult>({
|
||||||
|
queryKey: ["blogs", page],
|
||||||
|
queryFn: () => api.getBlogs(page),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetBlogCategories = () => {
|
||||||
|
return useQuery<BlogCategoriesResponse>({
|
||||||
|
queryKey: ["blog-categories"],
|
||||||
|
queryFn: api.getBlogCategories,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetBlogDetail = (id: string) => {
|
||||||
|
return useQuery<Blog>({
|
||||||
|
queryKey: ["blog-detail", id],
|
||||||
|
queryFn: () => api.getBlogDetail(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateBlog = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: CreateBlogType) => api.createBlog(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBlog = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: UpdateBlogType) => api.updateBlog(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteBlog = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.deleteBlog(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const useTagManager = (initialTags: string[] = []) => {
|
||||||
|
const [tagInput, setTagInput] = useState("");
|
||||||
|
const [tags, setTags] = useState<string[]>(initialTags);
|
||||||
|
|
||||||
|
const addTag = () => {
|
||||||
|
const trimmedTag = tagInput.trim();
|
||||||
|
if (trimmedTag && !tags.includes(trimmedTag)) {
|
||||||
|
setTags((prev) => [...prev, trimmedTag]);
|
||||||
|
setTagInput("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = (tagToRemove: string) => {
|
||||||
|
setTags((prev) => prev.filter((tag) => tag !== tagToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addTag();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTagsValue = (newTags: string[]) => {
|
||||||
|
setTags(newTags);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagInput,
|
||||||
|
setTagInput,
|
||||||
|
tags,
|
||||||
|
addTag,
|
||||||
|
removeTag,
|
||||||
|
handleKeyDown,
|
||||||
|
setTags: setTagsValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import type {
|
||||||
|
BlogsResponse,
|
||||||
|
GetBlogsResult,
|
||||||
|
Blog,
|
||||||
|
CreateBlogType,
|
||||||
|
UpdateBlogType,
|
||||||
|
BlogCategoriesResponse,
|
||||||
|
BlogDetailResponse,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const getBlogs = async (page: number = 1): Promise<GetBlogsResult> => {
|
||||||
|
const { data } = await axios.get<BlogsResponse>(`/blogs?page=${page}`);
|
||||||
|
return data.results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlogCategories = async (): Promise<BlogCategoriesResponse> => {
|
||||||
|
const { data } = await axios.get<BlogCategoriesResponse>("/blogs/categories");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlogDetail = async (id: string): Promise<Blog> => {
|
||||||
|
const { data } = await axios.get<BlogDetailResponse>(`/blogs/${id}`);
|
||||||
|
return data.results.blog;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createBlog = async (params: CreateBlogType) => {
|
||||||
|
const { data } = await axios.post(`/admin/blog`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBlog = async (params: UpdateBlogType) => {
|
||||||
|
const { _id, ...updateData } = params;
|
||||||
|
const { data } = await axios.post(`/admin/blog/${_id}`, updateData);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteBlog = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/blog/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
export interface BlogCategory {
|
||||||
|
_id: string;
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
parent: string | null;
|
||||||
|
leaf: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
children: BlogCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Blog {
|
||||||
|
_id: string;
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
category: {
|
||||||
|
_id: string;
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
author: {
|
||||||
|
fullName: string;
|
||||||
|
};
|
||||||
|
view: number;
|
||||||
|
tags: string[];
|
||||||
|
imageUrl: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pager {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
prevPage: boolean | null;
|
||||||
|
nextPage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogsResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
pager: Pager;
|
||||||
|
blogs: Blog[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetBlogsResult {
|
||||||
|
pager: Pager;
|
||||||
|
blogs: Blog[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateBlogType = {
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateBlogType = {
|
||||||
|
_id: string;
|
||||||
|
title_fa: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlogFormType = {
|
||||||
|
title_fa: string;
|
||||||
|
slug: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BlogCategoriesResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
categories: BlogCategory[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogDetailResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
blog: Blog;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
// import { useParams } from 'react-router-dom'
|
||||||
|
// import { useGetOrderDetailUser } from './hooks/useOrderData'
|
||||||
|
|
||||||
|
const OrderDetail: FC = () => {
|
||||||
|
|
||||||
|
// const { id } = useParams()
|
||||||
|
// const { data, isLoading, error } = useGetOrderDetailUser(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
asd
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderDetail
|
||||||
@@ -16,7 +16,7 @@ const STATUS_CONFIG = {
|
|||||||
process_by_sellers: { label: 'در حال پردازش توسط فروشنده', color: 'bg-blue-100 text-blue-800' },
|
process_by_sellers: { label: 'در حال پردازش توسط فروشنده', color: 'bg-blue-100 text-blue-800' },
|
||||||
cancelled_system: { label: 'کنسل شده توسط سیستم', color: 'bg-red-100 text-red-800' },
|
cancelled_system: { label: 'کنسل شده توسط سیستم', color: 'bg-red-100 text-red-800' },
|
||||||
Delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-800' },
|
Delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-800' },
|
||||||
} as const
|
}
|
||||||
|
|
||||||
const getStatusLabel = (status: string): string => STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status
|
const getStatusLabel = (status: string): string => STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const Native: FC = () => {
|
|||||||
<Td text={order.createdAt || 'نامشخص'} />
|
<Td text={order.createdAt || 'نامشخص'} />
|
||||||
<Td text="">
|
<Td text="">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link to={`${Pages.orders.detail}${order._id}?status=${status}`}>
|
<Link to={`${Pages.orders.detailUser}${order._id}`}>
|
||||||
<Eye color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500 transition-colors" />
|
<Eye color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500 transition-colors" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,3 +22,11 @@ export const useGetSellerOrders = (
|
|||||||
});
|
});
|
||||||
return { data, isLoading, error };
|
return { data, isLoading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetOrderDetailUser = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["order-detail-user", id],
|
||||||
|
queryFn: () => api.getOrderDetailUser(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,3 +20,8 @@ export const getSellerOrders = async (
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOrderDetailUser = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/order/${id}/user`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import Native from '@/pages/orders/Native'
|
|||||||
import OrdersSeller from '@/pages/orders/Seller'
|
import OrdersSeller from '@/pages/orders/Seller'
|
||||||
import CancellationCategories from '@/pages/orders/CancellationCategories'
|
import CancellationCategories from '@/pages/orders/CancellationCategories'
|
||||||
import ReturnCategories from '@/pages/orders/ReturnCategories'
|
import ReturnCategories from '@/pages/orders/ReturnCategories'
|
||||||
|
import OrderDetail from '@/pages/orders/Detail'
|
||||||
|
import BlogsList from '@/pages/blogs/List'
|
||||||
|
import CreateBlog from '@/pages/blogs/Create'
|
||||||
|
import UpdateBlog from '@/pages/blogs/Update'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -54,13 +58,20 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.products.brands.create} element={<BrandCreate />} />
|
<Route path={Pages.products.brands.create} element={<BrandCreate />} />
|
||||||
<Route path={Pages.products.warranty.list} element={<WarrantyList />} />
|
<Route path={Pages.products.warranty.list} element={<WarrantyList />} />
|
||||||
<Route path={Pages.products.warranty.create} element={<CreateWarranty />} />
|
<Route path={Pages.products.warranty.create} element={<CreateWarranty />} />
|
||||||
|
|
||||||
<Route path={Pages.orders.native} element={<Native />} />
|
<Route path={Pages.orders.native} element={<Native />} />
|
||||||
<Route path={Pages.orders.sellers} element={<OrdersSeller />} />
|
<Route path={Pages.orders.sellers} element={<OrdersSeller />} />
|
||||||
<Route path={Pages.orders.completed} element={<Native />} />
|
<Route path={Pages.orders.completed} element={<Native />} />
|
||||||
<Route path={Pages.orders.cancelled} element={<Native />} />
|
<Route path={Pages.orders.cancelled} element={<Native />} />
|
||||||
<Route path={Pages.orders.pending} element={<Native />} />
|
<Route path={Pages.orders.pending} element={<Native />} />
|
||||||
|
<Route path={`${Pages.orders.detailUser}:id`} element={<OrderDetail />} />
|
||||||
<Route path={Pages.orders.cancellationCategories} element={<CancellationCategories />} />
|
<Route path={Pages.orders.cancellationCategories} element={<CancellationCategories />} />
|
||||||
<Route path={Pages.orders.returnCategories} element={<ReturnCategories />} />
|
<Route path={Pages.orders.returnCategories} element={<ReturnCategories />} />
|
||||||
|
|
||||||
|
<Route path={Pages.blog.list} element={<BlogsList />} />
|
||||||
|
<Route path={Pages.blog.create} element={<CreateBlog />} />
|
||||||
|
<Route path={`${Pages.blog.detail}:id`} element={<UpdateBlog />} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user