blogs crud
This commit is contained in:
@@ -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' },
|
||||
cancelled_system: { label: 'کنسل شده توسط سیستم', color: 'bg-red-100 text-red-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
|
||||
|
||||
@@ -92,7 +92,7 @@ const Native: FC = () => {
|
||||
<Td text={order.createdAt || 'نامشخص'} />
|
||||
<Td text="">
|
||||
<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" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -22,3 +22,11 @@ export const useGetSellerOrders = (
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
export const getOrderDetailUser = async (id: string) => {
|
||||
const { data } = await axios.get(`/order/${id}/user`);
|
||||
return data;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user