faq crud
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { type FC } from 'react'
|
||||
import { useCreateFaq } from './hooks/useSettingData'
|
||||
import { useFormik } from 'formik'
|
||||
import { type CreateFaqType } from './types/Types'
|
||||
import { FaqPageEnum } from './enum/Enum'
|
||||
import * as Yup from 'yup'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Select from '../../components/Select'
|
||||
import Button from '../../components/Button'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { Pages } from '@/config/Pages'
|
||||
|
||||
const FaqCreate: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const createFaq = useCreateFaq()
|
||||
|
||||
const formik = useFormik<CreateFaqType>({
|
||||
initialValues: {
|
||||
question: '',
|
||||
answer: '',
|
||||
page: FaqPageEnum.Faq,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
question: Yup.string().required('سوال الزامی است'),
|
||||
answer: Yup.string().required('جواب الزامی است'),
|
||||
page: Yup.string().required('صفحه الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
createFaq.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success('سوال متداول با موفقیت ایجاد شد')
|
||||
navigate(Pages.pages.faq)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const pageOptions = [
|
||||
{ value: FaqPageEnum.Faq, label: 'سوالات متداول' },
|
||||
{ value: FaqPageEnum.Seller, label: 'فروشندگان' },
|
||||
{ value: FaqPageEnum.ProductTag, label: 'برچسب محصول' },
|
||||
{ value: FaqPageEnum.ProductDescription, label: 'توضیحات محصول' },
|
||||
{ value: FaqPageEnum.ProductBenefit, label: 'مزایای محصول' },
|
||||
{ value: FaqPageEnum.ShipmentProcess, label: 'فرآیند ارسال' },
|
||||
{ value: FaqPageEnum.Policy, label: 'سیاستها' },
|
||||
]
|
||||
|
||||
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={createFaq.isPending}
|
||||
disabled={!formik.isValid || createFaq.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>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<Input
|
||||
label='سوال'
|
||||
placeholder='سوال متداول را وارد کنید...'
|
||||
{...formik.getFieldProps('question')}
|
||||
error_text={formik.touched.question && formik.errors.question ? formik.errors.question : ''}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='جواب'
|
||||
placeholder='جواب سوال را وارد کنید...'
|
||||
{...formik.getFieldProps('answer')}
|
||||
error_text={formik.touched.answer && formik.errors.answer ? formik.errors.answer : ''}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='صفحه مربوطه'
|
||||
placeholder='انتخاب صفحه'
|
||||
items={pageOptions}
|
||||
{...formik.getFieldProps('page')}
|
||||
error_text={formik.touched.page && formik.errors.page ? formik.errors.page : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FaqCreate
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useState, type FC } from 'react'
|
||||
import { useGetFaq, useDeleteFaq } from './hooks/useSettingData'
|
||||
import { FaqPageEnum } from './enum/Enum'
|
||||
import { type FaqItem } from './types/Types'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Error from '../../components/Error'
|
||||
import Td from '../../components/Td'
|
||||
import Select from '../../components/Select'
|
||||
import { Calendar, DocumentText, MessageQuestion, Add, Edit } from 'iconsax-react'
|
||||
import Button from '../../components/Button'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||
|
||||
const FaqList: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [page, setPage] = useState<FaqPageEnum>(FaqPageEnum.Faq);
|
||||
const { data, isLoading, error, refetch } = useGetFaq(page);
|
||||
const deleteFaq = useDeleteFaq();
|
||||
|
||||
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 faqItems = data?.results?.faqs || [];
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleDateString('fa-IR');
|
||||
};
|
||||
|
||||
const truncateText = (text: string, maxLength: number = 50) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
const getPageName = (pageKey: string) => {
|
||||
const pageNames: Record<string, string> = {
|
||||
[FaqPageEnum.Faq]: 'سوالات متداول',
|
||||
[FaqPageEnum.Seller]: 'فروشندگان',
|
||||
[FaqPageEnum.ProductTag]: 'برچسب محصول',
|
||||
[FaqPageEnum.ProductDescription]: 'توضیحات محصول',
|
||||
[FaqPageEnum.ProductBenefit]: 'مزایای محصول',
|
||||
[FaqPageEnum.ShipmentProcess]: 'فرآیند ارسال',
|
||||
[FaqPageEnum.Policy]: 'سیاستها',
|
||||
};
|
||||
return pageNames[pageKey] || pageKey;
|
||||
};
|
||||
|
||||
const selectItems = Object.values(FaqPageEnum).map((pageValue) => ({
|
||||
value: pageValue,
|
||||
label: getPageName(pageValue),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center gap-4 mb-5">
|
||||
<div className='w-[200px]'>
|
||||
<Select
|
||||
className='w-full'
|
||||
value={page}
|
||||
onChange={(e) => setPage(e.target.value as FaqPageEnum)}
|
||||
items={selectItems}
|
||||
placeholder="انتخاب صفحه"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => navigate(Pages.pages.faqCreate)}
|
||||
className='w-fit'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={16} color='#fff' />
|
||||
<span>افزودن سوال متداول</span>
|
||||
</div>
|
||||
</Button>
|
||||
</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={'عملیات'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{faqItems.length === 0 ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={5} className="text-center py-8 text-gray-500">
|
||||
هیچ سوالی یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
faqItems.map((item: FaqItem) => (
|
||||
<tr key={item._id} className='tr'>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2 max-w-xs">
|
||||
<MessageQuestion size={16} color="#8C90A3" />
|
||||
<span className="font-medium">{item.question}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="max-w-xs">
|
||||
<span className="text-gray-600 text-sm">
|
||||
{truncateText(item.answer)}
|
||||
</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<DocumentText size={16} color="#8C90A3" />
|
||||
<span>{getPageName(item.page)}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={16} color="#8C90A3" />
|
||||
<span>{formatDate(item.createdAt)}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link to={`${Pages.pages.faqUpdate}${item._id}`}>
|
||||
<Edit size={16} color="#8C90A3" />
|
||||
</Link>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => {
|
||||
deleteFaq.mutate(item._id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FaqList
|
||||
@@ -0,0 +1,132 @@
|
||||
import { type FC } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGetFaqDetail, useUpdateFaq } from './hooks/useSettingData'
|
||||
import { useFormik } from 'formik'
|
||||
import { type CreateFaqType } from './types/Types'
|
||||
import { FaqPageEnum } from './enum/Enum'
|
||||
import * as Yup from 'yup'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Select from '../../components/Select'
|
||||
import Button from '../../components/Button'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Error from '../../components/Error'
|
||||
|
||||
const FaqUpdate: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const { data: faqDetail, isLoading: faqDetailLoading, error: faqDetailError } = useGetFaqDetail(id!)
|
||||
const updateFaq = useUpdateFaq()
|
||||
|
||||
const formik = useFormik<CreateFaqType>({
|
||||
initialValues: {
|
||||
question: faqDetail?.results?.faq?.question || '',
|
||||
answer: faqDetail?.results?.faq?.answer || '',
|
||||
page: faqDetail?.results?.faq?.page as FaqPageEnum || FaqPageEnum.Faq,
|
||||
},
|
||||
enableReinitialize: true,
|
||||
validationSchema: Yup.object({
|
||||
question: Yup.string().required('سوال الزامی است'),
|
||||
answer: Yup.string().required('جواب الزامی است'),
|
||||
page: Yup.string().required('صفحه الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
updateFaq.mutate({ id: id!, params: values }, {
|
||||
onSuccess: () => {
|
||||
toast.success('سوال متداول با موفقیت بهروزرسانی شد')
|
||||
navigate(Pages.pages.faq)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (faqDetailLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<PageLoading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (faqDetailError) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<Error errorText="خطا در بارگذاری اطلاعات سوال متداول" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const pageOptions = [
|
||||
{ value: FaqPageEnum.Faq, label: 'سوالات متداول' },
|
||||
{ value: FaqPageEnum.Seller, label: 'فروشندگان' },
|
||||
{ value: FaqPageEnum.ProductTag, label: 'برچسب محصول' },
|
||||
{ value: FaqPageEnum.ProductDescription, label: 'توضیحات محصول' },
|
||||
{ value: FaqPageEnum.ProductBenefit, label: 'مزایای محصول' },
|
||||
{ value: FaqPageEnum.ShipmentProcess, label: 'فرآیند ارسال' },
|
||||
{ value: FaqPageEnum.Policy, label: 'سیاستها' },
|
||||
]
|
||||
|
||||
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={updateFaq.isPending}
|
||||
disabled={!formik.isValid || updateFaq.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>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<Input
|
||||
label='سوال'
|
||||
placeholder='سوال متداول را وارد کنید...'
|
||||
{...formik.getFieldProps('question')}
|
||||
error_text={formik.touched.question && formik.errors.question ? formik.errors.question : ''}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='جواب'
|
||||
placeholder='جواب سوال را وارد کنید...'
|
||||
{...formik.getFieldProps('answer')}
|
||||
error_text={formik.touched.answer && formik.errors.answer ? formik.errors.answer : ''}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='صفحه مربوطه'
|
||||
placeholder='انتخاب صفحه'
|
||||
items={pageOptions}
|
||||
{...formik.getFieldProps('page')}
|
||||
error_text={formik.touched.page && formik.errors.page ? formik.errors.page : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FaqUpdate
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum FaqPageEnum {
|
||||
Faq = "Faq",
|
||||
Seller = "Seller",
|
||||
ProductTag = "ProductTag",
|
||||
ProductDescription = "ProductDescription",
|
||||
ProductBenefit = "ProductBenefit",
|
||||
ShipmentProcess = "ShipmentProcess",
|
||||
Policy = "Policy",
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as api from "../service/SettingService";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { FaqPageEnum } from "../enum/Enum";
|
||||
import { type CreateFaqType } from "../types/Types";
|
||||
|
||||
export const useGetAboutUs = () => {
|
||||
return useQuery({
|
||||
@@ -19,3 +21,40 @@ export const useDeleteAboutUs = () => {
|
||||
mutationFn: api.deleteAboutUs,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetFaq = (page: FaqPageEnum) => {
|
||||
return useQuery({
|
||||
queryKey: ["faq", page],
|
||||
queryFn: () => api.getFaq(page),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateFaq = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.createFaq,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteFaq = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.deleteFaq,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetFaqDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["faqDetail", id],
|
||||
queryFn: () => api.getFaqDetail(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateFaq = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({id, params}: {id: string, params: CreateFaqType}) => api.updateFaq(id, params),
|
||||
onSuccess: (_, {params}) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["faq", params.page] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,12 @@
|
||||
import axios from "../../../config/axios";
|
||||
import type { AboutUsResponse, CreateAboutUs } from "../types/Types";
|
||||
import type {
|
||||
AboutUsResponse,
|
||||
CreateAboutUs,
|
||||
CreateFaqType,
|
||||
FaqDetailResponse,
|
||||
FaqResponse,
|
||||
} from "../types/Types";
|
||||
import { FaqPageEnum } from "../enum/Enum";
|
||||
|
||||
export const getAboutUs = async (): Promise<AboutUsResponse> => {
|
||||
const { data } = await axios.get("/about-us");
|
||||
@@ -15,3 +22,31 @@ export const deleteAboutUs = async (id: string) => {
|
||||
const { data } = await axios.delete(`/admin/settings/about-us/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getFaq = async (page: FaqPageEnum): Promise<FaqResponse> => {
|
||||
const { data } = await axios.get(`/admin/settings/faqs?page=${page}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createFaq = async (params: CreateFaqType) => {
|
||||
const { data } = await axios.post("/admin/settings/faqs", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteFaq = async (id: string) => {
|
||||
const { data } = await axios.delete(`/admin/settings/faqs/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getFaqDetail = async (id: string): Promise<FaqDetailResponse> => {
|
||||
const { data } = await axios.get(`/admin/settings/faqs/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateFaq = async (id: string, params: CreateFaqType) => {
|
||||
const { data } = await axios.patch(`/admin/settings/faqs`, {
|
||||
...params,
|
||||
faqId: id,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FaqPageEnum } from "../enum/Enum";
|
||||
|
||||
export interface AboutUsItem {
|
||||
_id: string;
|
||||
title: string;
|
||||
@@ -22,3 +24,38 @@ export interface CreateAboutUs {
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface FaqItem {
|
||||
_id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
page: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface FaqResults {
|
||||
faqs: FaqItem[];
|
||||
}
|
||||
|
||||
export interface FaqResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: FaqResults;
|
||||
}
|
||||
|
||||
export interface CreateFaqType {
|
||||
question: string;
|
||||
answer: string;
|
||||
page: FaqPageEnum;
|
||||
}
|
||||
|
||||
export interface FaqDetailResults {
|
||||
faq: FaqItem;
|
||||
}
|
||||
|
||||
export interface FaqDetailResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: FaqDetailResults;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user