This commit is contained in:
hamid zarghami
2025-09-30 11:22:15 +03:30
parent 2395890e6c
commit 3a5d38fe33
10 changed files with 538 additions and 4 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ type Props = {
error_text?: string,
placeholder?: string,
label?: string,
value?: any,
value?: string,
} & SelectHTMLAttributes<HTMLSelectElement>
const Select: FC<Props> = (props: Props) => {
@@ -26,7 +26,7 @@ const Select: FC<Props> = (props: Props) => {
</label>
}
<select {...props} className={clx(
'w-full text-black block border appearance-none border-border px-2.5 h-10 text-sm rounded-2.5 bg-gray',
'w-full text-black block border appearance-none border-border px-[10px] h-10 text-sm rounded-[10px] bg-white',
props.className,
props.label && 'mt-1'
)}>
+3
View File
@@ -55,6 +55,9 @@ export const Pages = {
pages: {
aboutUs: "/pages/about-us",
aboutUsCreate: "/pages/about-us/create",
faq: "/pages/faq",
faqCreate: "/pages/faq/create",
faqUpdate: "/pages/faq/update/",
},
wallet: "/wallet",
profile: "/profile",
+110
View File
@@ -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
+162
View File
@@ -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
+132
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
export enum FaqPageEnum {
Faq = "Faq",
Seller = "Seller",
ProductTag = "ProductTag",
ProductDescription = "ProductDescription",
ProductBenefit = "ProductBenefit",
ShipmentProcess = "ShipmentProcess",
Policy = "Policy",
}
+40 -1
View File
@@ -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] });
},
});
};
+36 -1
View File
@@ -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;
};
+37
View File
@@ -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;
}
+7
View File
@@ -46,6 +46,10 @@ import ProductReport from '@/pages/report/ProductReport'
import CategoryReport from '@/pages/report/Category'
import AboutUsList from '@/pages/setting/AboutUsList'
import AboutUsCreate from '@/pages/setting/AboutUsCreate'
import FaqList from '@/pages/setting/FaqList'
import FaqCreate from '@/pages/setting/FaqCreate'
import FaqUpdate from '@/pages/setting/FaqUpdate'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -116,6 +120,9 @@ const MainRouter: FC = () => {
<Route path={Pages.pages.aboutUs} element={<AboutUsList />} />
<Route path={Pages.pages.aboutUsCreate} element={<AboutUsCreate />} />
<Route path={Pages.pages.faq} element={<FaqList />} />
<Route path={Pages.pages.faqCreate} element={<FaqCreate />} />
<Route path={`${Pages.pages.faqUpdate}:id`} element={<FaqUpdate />} />
</Routes>
</div>
</div>