crud guide service
This commit is contained in:
@@ -15,6 +15,7 @@ export const Pages = {
|
||||
category: "/services/category",
|
||||
plan: "/services/plan/",
|
||||
review: "/services/reviews",
|
||||
guide: "/services/guide/",
|
||||
},
|
||||
sliders: {
|
||||
list: "/sliders/list",
|
||||
|
||||
+8
-1
@@ -105,7 +105,14 @@
|
||||
"reviews": "نظرات",
|
||||
"create_plan": "ایجاد پلن",
|
||||
"plan_users": "کاربران پلن",
|
||||
"plan_list": "لیست پلن ها"
|
||||
"plan_list": "لیست پلن ها",
|
||||
"guide": "راهنمای سرویس"
|
||||
},
|
||||
"guide": {
|
||||
"list_guide": "لیست راهنمای سرویس",
|
||||
"submit_guide": "افزودن راهنمای سرویس",
|
||||
"order": "ترتیب",
|
||||
"content": "محتوا"
|
||||
},
|
||||
"review": {
|
||||
"title": "عنوان",
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Button from '../../components/Button'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Switch from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateGuide } from './hooks/useGuideData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const Create: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [isActive, setIsActive] = useState<boolean>(true)
|
||||
|
||||
const createGuide = useCreateGuide()
|
||||
|
||||
type GuideFormValues = {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number | string;
|
||||
}
|
||||
|
||||
const formik = useFormik<GuideFormValues>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
content: '',
|
||||
order: 1,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
content: Yup.string().required(t('errors.required')),
|
||||
order: Yup.number().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!id) return
|
||||
createGuide.mutate({
|
||||
...values,
|
||||
order: Number(values.order),
|
||||
isActive,
|
||||
serviceId: id || '',
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.services.guide + id)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='w-full mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('guide.submit_guide')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createGuide.isPending}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<TickCircle
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('submit')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
|
||||
<div className='xl:col-span-2'>
|
||||
<div className='mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<Input
|
||||
label={t('title')}
|
||||
name='title'
|
||||
value={formik.values.title}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title && formik.errors.title ? String(formik.errors.title) : ''}
|
||||
/>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
label={t('description')}
|
||||
name='description'
|
||||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.description && formik.errors.description ? String(formik.errors.description) : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
label={t('guide.content')}
|
||||
name='content'
|
||||
value={formik.values.content}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.content && formik.errors.content ? String(formik.errors.content) : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white mt-10 w-full text-xs h-fit px-5 py-7 rounded-3xl'>
|
||||
<div>
|
||||
<Switch
|
||||
active={isActive}
|
||||
onChange={(v) => setIsActive(v)}
|
||||
label={t('status')}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
<Input
|
||||
label={t('guide.order')}
|
||||
name='order'
|
||||
type='number'
|
||||
value={formik.values.order}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.order && formik.errors.order ? String(formik.errors.order) : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Create
|
||||
@@ -0,0 +1,109 @@
|
||||
import { FC } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGetGuides } from './hooks/useGuideData'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Td from '../../components/Td'
|
||||
import StatusWithText from '../../components/StatusWithText'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
import { useDeleteGuide } from './hooks/useGuideData'
|
||||
|
||||
type GuideItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const List: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { id } = useParams()
|
||||
const getGuides = useGetGuides(id || '')
|
||||
const deleteGuide = useDeleteGuide(id || '')
|
||||
|
||||
const guides: GuideItem[] = getGuides.data?.data?.guides ?? []
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('guide.list_guide')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.services.guide + id + '/create'}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('guide.submit_guide')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getGuides.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('number')} />
|
||||
<Td text={t('title')} />
|
||||
<Td text={t('description')} />
|
||||
<Td text={t('guide.order')} />
|
||||
<Td text={t('status')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
guides.map((item: GuideItem, index: number) => (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={String(index + 1)} />
|
||||
<Td text={item.title} />
|
||||
<Td text={item.description} />
|
||||
<Td text={String(item.order)} />
|
||||
<Td text=''>
|
||||
<StatusWithText
|
||||
variant={item.isActive ? 'success' : 'error'}
|
||||
text={item.isActive ? t('active') : t('inactive')}
|
||||
/>
|
||||
</Td>
|
||||
<Td text=''>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Link to={Pages.services.guide + id + '/' + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => deleteGuide.mutate(item.id)}
|
||||
isLoading={deleteGuide.isPending}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default List
|
||||
@@ -0,0 +1,162 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Button from '../../components/Button'
|
||||
import Input from '../../components/Input'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import Switch from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useGetGuideDetail, useUpdateGuide } from './hooks/useGuideData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
type GuideFormValues = {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number | string;
|
||||
}
|
||||
|
||||
const Update: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { id, guideId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [isActive, setIsActive] = useState<boolean>(true)
|
||||
|
||||
const getDetail = useGetGuideDetail(guideId || '')
|
||||
const updateGuide = useUpdateGuide(id || '')
|
||||
|
||||
const formik = useFormik<GuideFormValues>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
content: '',
|
||||
order: 1,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required(t('errors.required')),
|
||||
description: Yup.string().required(t('errors.required')),
|
||||
content: Yup.string().required(t('errors.required')),
|
||||
order: Yup.number().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!guideId) return
|
||||
updateGuide.mutate({
|
||||
id: guideId,
|
||||
params: {
|
||||
...values,
|
||||
order: Number(values.order),
|
||||
isActive,
|
||||
}
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
navigate(Pages.services.guide + id)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (getDetail.data?.data?.guide) {
|
||||
const g = getDetail.data?.data?.guide
|
||||
formik.setValues({
|
||||
title: g.title,
|
||||
description: g.description,
|
||||
content: g.content,
|
||||
order: g.order,
|
||||
})
|
||||
setIsActive(Boolean(g.isActive))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getDetail.data])
|
||||
|
||||
return (
|
||||
<div className='w-full mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('edit')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateGuide.isPending}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<TickCircle
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('submit')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
|
||||
<div className='xl:col-span-2'>
|
||||
<div className='mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<Input
|
||||
label={t('title')}
|
||||
name='title'
|
||||
value={formik.values.title}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title && formik.errors.title ? String(formik.errors.title) : ''}
|
||||
/>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
label={t('description')}
|
||||
name='description'
|
||||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.description && formik.errors.description ? String(formik.errors.description) : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
label={t('guide.content')}
|
||||
name='content'
|
||||
value={formik.values.content}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.content && formik.errors.content ? String(formik.errors.content) : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white mt-10 w-full text-xs h-fit px-5 py-7 rounded-3xl'>
|
||||
<div>
|
||||
<Switch
|
||||
active={isActive}
|
||||
onChange={(v) => setIsActive(v)}
|
||||
label={t('status')}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
<Input
|
||||
label={t('guide.order')}
|
||||
name='order'
|
||||
type='number'
|
||||
value={formik.values.order}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.order && formik.errors.order ? String(formik.errors.order) : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Update
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createGuide,
|
||||
deleteGuide,
|
||||
getGuideDetail,
|
||||
getGuides,
|
||||
updateGuide,
|
||||
UpdateGuideType,
|
||||
} from "../service/GuuideService";
|
||||
import { CreateGuideType } from "../types/Types";
|
||||
|
||||
export const useGetGuides = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["guides", id],
|
||||
queryFn: () => getGuides(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateGuide = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateGuideType) => createGuide(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteGuide = (serviceId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (guideId: string) => deleteGuide(guideId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["guides", serviceId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetGuideDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["guide-detail", id],
|
||||
queryFn: () => getGuideDetail(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGuide = (serviceId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, params }: { id: string; params: UpdateGuideType }) =>
|
||||
updateGuide(id, params),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["guides", serviceId] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateGuideType } from "../types/Types";
|
||||
|
||||
export const getGuides = async (id: string) => {
|
||||
const { data } = await axios.get(`/danak-services/${id}/guides`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createGuide = async (params: CreateGuideType) => {
|
||||
const { data } = await axios.post(`/danak-services/guides`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteGuide = async (id: string) => {
|
||||
const { data } = await axios.delete(`/danak-services/guides/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGuideDetail = async (id: string) => {
|
||||
const { data } = await axios.get(`/danak-services/guides/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export type UpdateGuideType = {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export const updateGuide = async (id: string, params: UpdateGuideType) => {
|
||||
const { data } = await axios.patch(`/danak-services/guides/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export type CreateGuideType = {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
serviceId: string;
|
||||
};
|
||||
@@ -168,7 +168,11 @@ const ListService: FC = () => {
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={''} />
|
||||
<Td text={''}>
|
||||
<Link to={Pages.services.guide + item.id}>
|
||||
<Button label='راهنمای سرویس' className='px-4' />
|
||||
</Link>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -69,6 +69,9 @@ import CreatePlan from '../pages/support/Create'
|
||||
import SupportList from '../pages/support/List'
|
||||
import UpdateSupport from '../pages/support/Update'
|
||||
import PlanUsers from '../pages/support/PlanUsers'
|
||||
import List from '../pages/guide/List'
|
||||
import CreateGuide from '../pages/guide/Create'
|
||||
import UpdateGuide from '../pages/guide/Update.tsx'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -149,6 +152,9 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.support.list} element={<SupportList />} />
|
||||
<Route path={Pages.support.detail + ':id'} element={<UpdateSupport />} />
|
||||
<Route path={Pages.support.planUsers + ':id'} element={<PlanUsers />} />
|
||||
<Route path={Pages.services.guide + ':id'} element={<List />} />
|
||||
<Route path={Pages.services.guide + ':id/create'} element={<CreateGuide />} />
|
||||
<Route path={Pages.services.guide + ':id/:guideId'} element={<UpdateGuide />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user