From 0084aefae445c2e450bae42aaab2fba72100c89b Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 5 Mar 2025 10:03:44 +0330 Subject: [PATCH] update service --- src/components/UploadBoxDraggble.tsx | 47 ++- src/config/Pages.ts | 2 + src/pages/ads/AdsList.tsx | 22 +- src/pages/ads/Update.tsx | 258 ++++++++++++++++ src/pages/ads/hooks/useAdsData.ts | 20 ++ src/pages/ads/service/AdsService.ts | 15 + src/pages/service/List.tsx | 2 +- src/pages/service/UpdateService.tsx | 284 ++++++++++++++++++ .../service/components/AddServiceSidebar.tsx | 3 +- .../components/UpdateServiceSidebar.tsx | 104 +++++++ src/pages/service/hooks/useServiceData.ts | 15 + src/pages/service/service/ServiceService.ts | 10 + src/router/Main.tsx | 4 + 13 files changed, 774 insertions(+), 12 deletions(-) create mode 100644 src/pages/ads/Update.tsx create mode 100644 src/pages/service/UpdateService.tsx create mode 100644 src/pages/service/components/UpdateServiceSidebar.tsx diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx index 53215a0..090fb41 100644 --- a/src/components/UploadBoxDraggble.tsx +++ b/src/components/UploadBoxDraggble.tsx @@ -1,5 +1,5 @@ import { CloseCircle, DocumentUpload, Gallery } from 'iconsax-react' -import { FC, useCallback, useState } from 'react' +import { FC, useCallback, useEffect, useState } from 'react' import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; @@ -8,12 +8,16 @@ type Props = { onChange: (file: File[]) => void; isMultiple?: boolean; isFile?: boolean, + preview?: string[], + onChangePreview?: (preview: string[]) => void } const UploadBoxDraggble: FC = (props: Props) => { - const [files, setFiles] = useState([]) const { t } = useTranslation('global') + const [files, setFiles] = useState([]) + const [perviews, setPerviews] = useState(props.preview ? props.preview : []) + const [isFill, setIsFill] = useState(false) const onDrop = useCallback((acceptedFiles: File[]) => { if (props.isMultiple) { @@ -36,6 +40,27 @@ const UploadBoxDraggble: FC = (props: Props) => { props.onChange(array) } + const handleDeletePreview = (index: number) => { + const array = [...perviews]; + array.splice(index, 1); + setPerviews(array); + if (props.onChangePreview) { + props.onChangePreview(array); + } + } + + useEffect(() => { + + if (props.preview && props.preview.length > 0 && !isFill) { + setPerviews(props.preview) + console.log('preview', props.preview); + + setIsFill(true) + } + + }, [props.preview, isFill]) + + return (
@@ -58,8 +83,22 @@ const UploadBoxDraggble: FC = (props: Props) => {
{ - files.length > 0 && ( + files.length > 0 || perviews ? (
+ { + perviews && perviews.map((item, index) => { + return ( +
+
+ +
handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'> + +
+
+
+ ) + }) + } { files.map((file, index) => { if (props.isFile) { @@ -88,7 +127,7 @@ const UploadBoxDraggble: FC = (props: Props) => { }) }
- ) + ) : null }
diff --git a/src/config/Pages.ts b/src/config/Pages.ts index f65f7fa..966c0e8 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -9,6 +9,7 @@ export const Pages = { mine: "/services", other: "/other-service", detail: "/services/detail/", + update: "/services/update/", add: "/services/add", list: "/services/list", category: "/services/category", @@ -72,6 +73,7 @@ export const Pages = { ads: { list: "/ads/list", create: "/ads/create", + update: "/ads/update/", }, blog: { list: "/blog/list", diff --git a/src/pages/ads/AdsList.tsx b/src/pages/ads/AdsList.tsx index 9d1bd2c..58bcdd2 100644 --- a/src/pages/ads/AdsList.tsx +++ b/src/pages/ads/AdsList.tsx @@ -1,14 +1,14 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '../../components/Button' -import { Add, Eye } from 'iconsax-react' +import { Add, Eye, Trash } from 'iconsax-react' import Td from '../../components/Td' import { Link } from 'react-router-dom' import { Pages } from '../../config/Pages' import DatePickerComponent from '../../components/DatePicker' import Select from '../../components/Select' import Input from '../../components/Input' -import { useGetAdsList } from './hooks/useAdsData' +import { useDeleteAds, useGetAdsList } from './hooks/useAdsData' import { AdsItemType } from './types/AdsTypes' import moment from 'moment-jalaali' import ToggleStatusAds from './components/ToggleStatus' @@ -22,6 +22,15 @@ const AddList: FC = () => { const [since, setSince] = useState('') const [page, setPage] = useState(1) const getAds = useGetAdsList(page, search, isActive, since) + const deleteAds = useDeleteAds() + + const handleDelete = (id: string) => { + deleteAds.mutate(id, { + onSuccess: () => { + getAds.refetch() + } + }) + } return (
@@ -120,9 +129,12 @@ const AddList: FC = () => { /> - - - +
+ + + + handleDelete(item.id)} size={20} color='#8C90A3' /> +
) diff --git a/src/pages/ads/Update.tsx b/src/pages/ads/Update.tsx new file mode 100644 index 0000000..8317f0a --- /dev/null +++ b/src/pages/ads/Update.tsx @@ -0,0 +1,258 @@ +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Input from '../../components/Input' +import Button from '../../components/Button' +import SwitchComponent from '../../components/Switch' +import { TickCircle, Link2 } from 'iconsax-react' +import DatePickerComponent from '../../components/DatePicker' +import { useFormik } from 'formik' +import { AdsDisplayLocation, CreateAdsType } from './types/AdsTypes' +import * as Yup from 'yup' +import UploadBoxDraggble from '../../components/UploadBoxDraggble' +import CheckBoxComponent from '../../components/CheckBoxComponent' +import { useSingleUpload } from '../service/hooks/useServiceData' +import { useGetDetailAds, useUpdateAds } from './hooks/useAdsData' +import { ErrorType } from '../../helpers/types' +import { toast } from 'react-toastify' +import { clx } from '../../helpers/utils' +import { useNavigate, useParams } from 'react-router-dom' +import { Pages } from '../../config/Pages' +import moment from 'moment-jalaali' + +const UpdateAds: FC = () => { + + const { id } = useParams() + const navigate = useNavigate() + const { t } = useTranslation('global') + const [file, setFile] = useState() + const singleUpload = useSingleUpload() + const updateAds = useUpdateAds(id ? id : '') + const getDetail = useGetDetailAds(id) + + const formik = useFormik({ + initialValues: { + displayLocation: '', + endDate: '', + imageUrl: '', + isActive: true, + link: '', + serviceId: undefined, + startDate: '', + title: '' + }, + validationSchema: Yup.object().shape({ + title: Yup.string().required(t('errors.required')), + displayLocation: Yup.string().required(t('errors.required')), + endDate: Yup.string().required(t('errors.required')), + link: Yup.string().required(t('errors.required')), + startDate: Yup.string().required(t('errors.required')) + }), + onSubmit: async (values) => { + if (file) { + const formData = new FormData() + formData.append('file', file) + await singleUpload.mutateAsync(formData, { + onSuccess: (data) => { + values.imageUrl = data?.data?.url + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + } + + updateAds.mutate(values, { + onSuccess: () => { + toast.success(t('success')) + navigate(Pages.ads.list) + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + + } + }) + + useEffect(() => { + + if (getDetail.data) { + console.log(getDetail.data); + + formik.setValues({ + displayLocation: getDetail.data?.data?.ads?.displayLocation, + endDate: moment(getDetail.data?.data?.ads?.endDate).format('jYYYY-jMM-jDD'), + imageUrl: getDetail.data?.data?.ads?.imageUrl, + isActive: getDetail.data?.data?.ads?.isActive, + link: getDetail.data?.data?.ads?.link, + serviceId: getDetail.data?.data?.ads?.serviceId, + startDate: moment(getDetail.data?.data?.ads?.startDate).format('jYYYY-jMM-jDD'), + title: getDetail.data?.data?.ads?.title + }) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getDetail.data]) + + + return ( +
+
+
+ {t('ads.new_ads')} +
+ +
+
+
+
+ +
+
+ formik.setFieldValue('startDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))} + placeholder='' + error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''} + defaulValue={formik.values.startDate} + /> + formik.setFieldValue('endDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))} + placeholder='' + error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''} + defaulValue={formik.values.endDate} + /> +
+
+ + +
+
+
+
+

+ وضعیت تبلیغ +

+
+
+ {t('ads.deactive')} +
+ formik.setFieldValue('isActive', value)} + /> +
+
+

{t('ads.location')}

+ +
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.HOMEPAGE_TOP)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.HOMEPAGE_TOP}`)} +
+
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.SINGLE_SERVICE_PAGE)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.SINGLE_SERVICE_PAGE}`)} +
+
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.MY_SERVICES_PAGE)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.MY_SERVICES_PAGE}`)} +
+
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_TOP)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.OTHER_SERVICES_TOP}`)} +
+
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_LEFT)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.OTHER_SERVICES_LEFT}`)} +
+
+
+
+ formik.setFieldValue('displayLocation', AdsDisplayLocation.SERVICE)} + /> +
+
+ {t(`ads.${AdsDisplayLocation.SERVICE}`)} +
+
+ +
+
+ setFile(value[0])} + /> +
+
+ ads +
+
+
+ +
+ ) +} + +export default UpdateAds \ No newline at end of file diff --git a/src/pages/ads/hooks/useAdsData.ts b/src/pages/ads/hooks/useAdsData.ts index 3b2d8e5..abefbab 100644 --- a/src/pages/ads/hooks/useAdsData.ts +++ b/src/pages/ads/hooks/useAdsData.ts @@ -25,3 +25,23 @@ export const useToggleStatusAds = () => { mutationFn: (variables: string) => api.toggleStatusAds(variables), }); }; + +export const useGetDetailAds = (id?: string) => { + return useQuery({ + queryKey: ["ads-detail", id], + queryFn: () => api.getDetailAds(id ? id : ""), + enabled: !!id, + }); +}; + +export const useUpdateAds = (id: string) => { + return useMutation({ + mutationFn: (variables: CreateAdsType) => api.updateAds(id, variables), + }); +}; + +export const useDeleteAds = () => { + return useMutation({ + mutationFn: (variables: string) => api.deleteAds(variables), + }); +}; diff --git a/src/pages/ads/service/AdsService.ts b/src/pages/ads/service/AdsService.ts index 2122d52..d946b58 100644 --- a/src/pages/ads/service/AdsService.ts +++ b/src/pages/ads/service/AdsService.ts @@ -31,3 +31,18 @@ export const toggleStatusAds = async (id: string) => { const { data } = await axios.patch(`/advertise/toggle-status/${id}`); return data; }; + +export const getDetailAds = async (id: string) => { + const { data } = await axios.get(`/advertise/${id}`); + return data; +}; + +export const updateAds = async (id: string, params: CreateAdsType) => { + const { data } = await axios.patch(`/advertise/${id}`, params); + return data; +}; + +export const deleteAds = async (id: string) => { + const { data } = await axios.delete(`/advertise/${id}`); + return data; +}; diff --git a/src/pages/service/List.tsx b/src/pages/service/List.tsx index 540ca97..dbe5b08 100644 --- a/src/pages/service/List.tsx +++ b/src/pages/service/List.tsx @@ -154,7 +154,7 @@ const ListService: FC = () => { /> - + diff --git a/src/pages/service/UpdateService.tsx b/src/pages/service/UpdateService.tsx new file mode 100644 index 0000000..4f7a5ec --- /dev/null +++ b/src/pages/service/UpdateService.tsx @@ -0,0 +1,284 @@ +import { FC, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Input from '../../components/Input' +import Select from '../../components/Select' +import Quill from 'quill'; +import Button from '../../components/Button'; +import { TickCircle } from 'iconsax-react'; +import { useFormik } from 'formik'; +import { CreateServiceType } from './types/ServiceTypes'; +import DatePickerComponent from '../../components/DatePicker'; +import * as Yup from 'yup' +import { useGetDetailService, useMultiUpload, useSingleUpload, useUpdateService } from './hooks/useServiceData'; +import moment from 'moment-jalaali'; +import { ErrorType } from '../../helpers/types'; +import { toast } from 'react-toastify'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Pages } from '../../config/Pages'; +import UpdateServiceSidebar from './components/UpdateServiceSidebar'; + +const UpdateService: FC = () => { + + const { id } = useParams() + const navigate = useNavigate() + const { t } = useTranslation('global') + const singleUpload = useSingleUpload() + const multiUpload = useMultiUpload() + const editorRef = useRef(null); + const createService = useUpdateService(id ? id : '') + const [iconFile, setIconFile] = useState() + const [imagesFile, setImagesFile] = useState() + const getDetailService = useGetDetailService(id) + + useEffect(() => { + const quill = new Quill('#editor', { + theme: 'snow', + modules: { + toolbar: [ + [{ header: '1' }, { header: '2' }, { font: [] }], + [{ list: 'ordered' }, { list: 'bullet' }], + ['bold', 'italic', 'underline'], + ['link', 'image'], + [{ align: [] }], + ['clean'] + ] + } + }); + quill.on('text-change', () => { + if (editorRef.current) { + const html = editorRef.current?.querySelector('.ql-editor')?.innerHTML || ''; + formik.setFieldValue('metaDescription', html); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const formik = useFormik({ + initialValues: { + author: '', + categoryId: '', + description: '', + icon: '', + images: [], + isDanakSuggest: false, + link: '', + metaDescription: '', + name: '', + title: '', + serviceLanguage: '', + softwareLanguage: '', + userCount: 0, + createDate: '' + }, + validationSchema: Yup.object({ + name: Yup.string().required(t('errors.required')), + title: Yup.string().required(t('errors.required')), + description: Yup.string().required(t('errors.required')), + author: Yup.string().required(t('errors.required')), + createDate: Yup.date().required(t('errors.required')), + userCount: Yup.number().required(t('errors.required')), + serviceLanguage: Yup.string().required(t('errors.required')), + metaDescription: Yup.string().required(t('errors.required')), + link: Yup.string().required(t('errors.required')), + }), + onSubmit: async (values) => { + + + if (iconFile) { + const formData = new FormData() + formData.append('file', iconFile) + await singleUpload.mutateAsync(formData, { + onSuccess: (data) => { + values.icon = data?.data?.url + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + } + + if (imagesFile && imagesFile.length > 0) { + const images = new FormData() + imagesFile.forEach(file => { + images.append('files', file) + }) + await multiUpload.mutateAsync(images, { + onSuccess: async (data) => { + console.log('values.images', values.images); + + values.images = [...(values.images || []), ...data.data.map((item: { url: string }) => item?.url)] + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + } + + const params = { + ...values, + userCount: +values.userCount, + createDate: moment(values.createDate).format('YYYY-MM-DD'), + } + createService.mutate(params, { + onSuccess: () => { + formik.resetForm() + toast.success(t('success')) + navigate(Pages.services.list) + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + + }, + }) + + useEffect(() => { + if (getDetailService.data) { + const data = getDetailService.data?.data?.danakService; + formik.setValues({ + author: data?.author, + categoryId: data?.category?.id, + description: data?.description, + icon: data?.icon, + images: data?.images?.map((image: { imageUrl: string }) => image.imageUrl), + isDanakSuggest: data?.isDanakSuggest, + link: data?.link, + metaDescription: data?.metaDescription, + name: data?.name, + title: data?.title, + serviceLanguage: data?.serviceLanguage, + softwareLanguage: data?.softwareLanguage, + userCount: data?.userCount || 0, + createDate: moment(data?.createDate).format('YYYY-MM-DD'), + }); + const editorElement = editorRef.current?.querySelector('.ql-editor'); + if (editorElement) { + editorElement.innerHTML = data?.metaDescription || ''; + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getDetailService.data]); + + return ( +
+ +
+
+ {t('service.add_service')} +
+
+ +
+
+
+
+ + +
+
+ + + +
+ +
+ + + + +
+ +
+ + + formik.setFieldValue('createDate', d)} + placeholder={t('select')} + error_text={formik.touched.createDate && formik.errors.createDate ? formik.errors.createDate : ''} + defaulValue={formik.values.createDate} + /> +
+ +
+ + +
diff --git a/src/pages/service/components/UpdateServiceSidebar.tsx b/src/pages/service/components/UpdateServiceSidebar.tsx new file mode 100644 index 0000000..0e7a12d --- /dev/null +++ b/src/pages/service/components/UpdateServiceSidebar.tsx @@ -0,0 +1,104 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import CheckBoxComponent from '../../../components/CheckBoxComponent' +import Input from '../../../components/Input' +import UploadBoxDraggble from '../../../components/UploadBoxDraggble' +import { FormikProps } from 'formik' +import { CreateServiceType, ServiceCategoryType } from '../types/ServiceTypes' +import { useGetCategoryParents, useGetDetailService } from '../hooks/useServiceData' +import { useParams } from 'react-router-dom' + +type Props = { + formik: FormikProps, + onChangeIconFile: (file: File) => void, + onChangeImagesFile: (file: File[]) => void +} + +const UpdateServiceSidebar: FC = (props: Props) => { + + const { t } = useTranslation('global') + const { id } = useParams() + const { formik } = props + + const getCategory = useGetCategoryParents() + const { data } = useGetDetailService(id) + + + return ( +
+
+
+ {t('service.service_status')} +
+ {/*
+ formik.setFieldValue('isDanakSuggest', e)} + /> +
*/} +
+ +
+ formik.setFieldValue('isDanakSuggest', e.target.checked)} + /> +
{t('service.special_danak')}
+
+ +
+ +
+ +
+
{t('service.category')}
+
+ { + getCategory.data?.data?.categories?.map((item: ServiceCategoryType) => { + return ( +
+ formik.setFieldValue('categoryId', e.target.checked ? item.id : '')} + /> +
{item.title}
+
+ ) + }) + } + +
+
+ +
+
{t('service.service_icon')}
+
+ props.onChangeIconFile(file[0])} + preview={[data?.data?.danakService?.icon]} + onChangePreview={(images: string[]) => formik.setFieldValue('icon', images[0])} + /> +
+
+
+
{t('service.service_images')}
+
+ image.imageUrl) || []} + onChangePreview={(images: string[]) => formik.setFieldValue('images', images)} + /> +
+
+
+ ) +} + +export default UpdateServiceSidebar \ No newline at end of file diff --git a/src/pages/service/hooks/useServiceData.ts b/src/pages/service/hooks/useServiceData.ts index 4ecf41e..21b830b 100644 --- a/src/pages/service/hooks/useServiceData.ts +++ b/src/pages/service/hooks/useServiceData.ts @@ -146,3 +146,18 @@ export const useChangeStatusReview = () => { api.changeStatusReview(variables), }); }; + +export const useGetDetailService = (id?: string) => { + return useQuery({ + queryKey: ["service-detail", id], + queryFn: () => api.getServiceDetail(id ? id : ""), + enabled: !!id, + }); +}; + +export const useUpdateService = (id: string) => { + return useMutation({ + mutationFn: (variables: CreateServiceType) => + api.updateService(id, variables), + }); +}; diff --git a/src/pages/service/service/ServiceService.ts b/src/pages/service/service/ServiceService.ts index e7fd359..7c2c57d 100644 --- a/src/pages/service/service/ServiceService.ts +++ b/src/pages/service/service/ServiceService.ts @@ -130,3 +130,13 @@ export const changeStatusReview = async (params: { ); return data; }; + +export const getServiceDetail = async (id: string) => { + const { data } = await axios.get(`/danak-services/${id}`); + return data; +}; + +export const updateService = async (id: string, params: CreateServiceType) => { + const { data } = await axios.patch(`/danak-services/${id}`, params); + return data; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 580ad54..0fb72cb 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -55,6 +55,8 @@ import Plans from '../pages/service/Plans' import GroupCreate from '../pages/users/GroupCreate' import GroupList from '../pages/users/GroupList' import Reviews from '../pages/service/Reviews' +import UpdateAds from '../pages/ads/Update' +import UpdateService from '../pages/service/UpdateService' const MainRouter: FC = () => { @@ -79,6 +81,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } /> } /> } /> @@ -88,6 +91,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } /> } /> } />