This commit is contained in:
hamid zarghami
2025-02-16 11:20:25 +03:30
parent 8f39afeb48
commit df8c000aa6
7 changed files with 349 additions and 80 deletions
+8 -1
View File
@@ -264,7 +264,14 @@
"status": "وضعیت", "status": "وضعیت",
"enter_your_title": "عنوان تبلیغ را وارد کنید", "enter_your_title": "عنوان تبلیغ را وارد کنید",
"link": "لینک", "link": "لینک",
"enter_your_link": "لینک تبلیغ را وارد کنید" "enter_your_link": "لینک تبلیغ را وارد کنید",
"HOMEPAGE_TOP": "بالای صفحه اصلی",
"SINGLE_SERVICE_PAGE": "صفحه سرویس تکی",
"MY_SERVICES_PAGE": "صفحه سرویس های من",
"OTHER_SERVICES_TOP": "بالای سایر سرویس ها",
"OTHER_SERVICES_LEFT": "سمت چپ سایر سرویس ها",
"SERVICE": "سرویس",
"file_error": "فایل مورد نظر باید انتخاب شود"
}, },
"discount": { "discount": {
"list": "لیست تخفیفات", "list": "لیست تخفیفات",
+77 -59
View File
@@ -1,16 +1,28 @@
import { FC } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '../../components/Button' import Button from '../../components/Button'
import { Add, Eye } from 'iconsax-react' import { Add, Eye } from 'iconsax-react'
import Td from '../../components/Td' import Td from '../../components/Td'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages' import { Pages } from '../../config/Pages'
import SwitchComponent from '../../components/Switch'
import DatePickerComponent from '../../components/DatePicker' import DatePickerComponent from '../../components/DatePicker'
import Select from '../../components/Select' import Select from '../../components/Select'
import Input from '../../components/Input' import Input from '../../components/Input'
import { useGetAdsList } from './hooks/useAdsData'
import { AdsItemType } from './types/AdsTypes'
import moment from 'moment-jalaali'
import ToggleStatusAds from './components/ToggleStatus'
import PageLoading from '../../components/PageLoading'
import Pagination from '../../components/Pagination'
const AddList: FC = () => { const AddList: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [search, setSearch] = useState<string>('')
const [isActive, setIsActive] = useState<boolean>(false)
const [since, setSince] = useState<string>('')
const [page, setPage] = useState<number>(1)
const getAds = useGetAdsList(page, search, isActive, since)
return ( return (
<div className='mt-4 min-h-[500px]'> <div className='mt-4 min-h-[500px]'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
@@ -37,9 +49,8 @@ const AddList: FC = () => {
<div className='w-[200px]'> <div className='w-[200px]'>
<DatePickerComponent <DatePickerComponent
label={t('ads.date')} label={t('ads.date')}
onChange={() => { }} onChange={(date) => setSince(moment(date, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
placeholder='' placeholder=''
defaulValue='1400-01-01'
/> />
</div> </div>
<div className='w-[200px]'> <div className='w-[200px]'>
@@ -48,10 +59,15 @@ const AddList: FC = () => {
items={[ items={[
{ {
label: 'فعال', label: 'فعال',
value: 'active' value: 'true'
} },
{
label: 'غیر فعال',
value: 'false'
},
]} ]}
className='bg-white border' className='bg-white border'
onChange={(e) => setIsActive(e.target.value === 'true' ? true : false)}
/> />
</div> </div>
</div> </div>
@@ -61,65 +77,67 @@ const AddList: FC = () => {
variant='search' variant='search'
placeholder={t('ads.search')} placeholder={t('ads.search')}
className='bg-white w-full' className='bg-white w-full'
onChangeSearchFinal={(value) => setSearch(value)}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'> {
<table className='w-full text-sm '> getAds.isPending ?
<thead className='thead'> <div className='mt-5'>
<tr> <PageLoading />
<Td text={t('ads.number')} /> </div>
<Td text={t('ads.title')} /> :
<Td text={t('ads.location')} /> <div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<Td text={t('ads.startDate')} /> <table className='w-full text-sm '>
<Td text={t('ads.endDate')} /> <thead className='thead'>
<Td text={t('ads.status')} /> <tr>
<Td text={''} /> <Td text={t('ads.number')} />
</tr> <Td text={t('ads.title')} />
</thead> <Td text={t('ads.location')} />
<tbody> <Td text={t('ads.startDate')} />
<tr className='tr'> <Td text={t('ads.endDate')} />
<Td text={t('ads.number')} /> <Td text={t('ads.status')} />
<Td text={t('ads.title')} /> <Td text={''} />
<Td text={t('ads.location')} /> </tr>
<Td text={t('ads.startDate')} /> </thead>
<Td text={t('ads.endDate')} /> <tbody>
<Td text={t('')}> {
<SwitchComponent getAds.data?.data?.ads?.map((item: AdsItemType, index: number) => {
active={true} return (
onChange={() => { }} <tr key={item.id} className='tr'>
/> <Td text={String(index + 1)} />
</Td> <Td text={item.title} />
<Td text={''}> <Td text={t(`ads.${item.displayLocation}`)} />
<Link to={Pages.ticket.detail + '1'}> <Td text={moment(item.startDate).format('jYYYY-jMM-jDD')} />
<Eye size={20} color='#8C90A3' /> <Td text={moment(item.endDate).format('jYYYY-jMM-jDD')} />
</Link> <Td text={t('')}>
</Td> <ToggleStatusAds
</tr> defaultActive={item.isActive}
<tr className='tr'> id={item.id}
<Td text={t('ads.number')} /> />
<Td text={t('ads.title')} /> </Td>
<Td text={t('ads.location')} /> <Td text={''}>
<Td text={t('ads.startDate')} /> <Link to={Pages.ticket.detail + '1'}>
<Td text={t('ads.endDate')} /> <Eye size={20} color='#8C90A3' />
<Td text={t('')}> </Link>
<SwitchComponent </Td>
active={true} </tr>
onChange={() => { }} )
/> })
</Td> }
<Td text={''}> </tbody>
<Link to={Pages.discount + '1'}> </table>
<Eye size={20} color='#8C90A3' /> </div>
</Link> }
</Td>
</tr> <Pagination
</tbody> currentPage={page}
</table> onPageChange={(page) => setPage(page)}
</div> totalPages={getAds.data?.data?.pager?.totalPages}
/>
</div> </div>
) )
} }
+143 -20
View File
@@ -1,13 +1,78 @@
import { FC } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '../../components/Input' import Input from '../../components/Input'
import Button from '../../components/Button' import Button from '../../components/Button'
import SwitchComponent from '../../components/Switch' import SwitchComponent from '../../components/Switch'
import { TickCircle, TickSquare, Link2 } from 'iconsax-react' import { TickCircle, Link2 } from 'iconsax-react'
import DatePickerComponent from '../../components/DatePicker' import DatePickerComponent from '../../components/DatePicker'
import ImageUploader from './components/ImageUploader' 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 { useCreateAds } from './hooks/useAdsData'
import { ErrorType } from '../../helpers/types'
import { toast } from 'react-toastify'
import { clx } from '../../helpers/utils'
import { useNavigate } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import moment from 'moment-jalaali'
const CreateAd: FC = () => { const CreateAd: FC = () => {
const navigate = useNavigate()
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [file, setFile] = useState<File>()
const singleUpload = useSingleUpload()
const createAds = useCreateAds()
const formik = useFormik<CreateAdsType>({
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])
}
})
createAds.mutate(values, {
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.ads.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
} else {
toast.error(t('ads.file_error'))
}
}
})
return ( return (
<div className='mt-4'> <div className='mt-4'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
@@ -16,6 +81,8 @@ const CreateAd: FC = () => {
</div> </div>
<Button <Button
className='w-[172px]' className='w-[172px]'
onClick={() => formik.handleSubmit()}
isLoading={createAds.isPending || singleUpload.isPending}
> >
<div className='flex gap-2 items-center'> <div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' /> <TickCircle size={20} color='white' />
@@ -31,28 +98,35 @@ const CreateAd: FC = () => {
<Input <Input
label={t('ads.title')} label={t('ads.title')}
placeholder={t('ads.enter_your_title')} placeholder={t('ads.enter_your_title')}
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/> />
</div> </div>
<div className='mt-6 rowTwoInput'> <div className='mt-6 rowTwoInput'>
<DatePickerComponent <DatePickerComponent
label={t('ads.startDate')} label={t('ads.startDate')}
onChange={() => { }} onChange={(value) => formik.setFieldValue('startDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
placeholder='' placeholder=''
defaulValue='1400-01-01' error_text={formik.touched.startDate && formik.errors.startDate ? formik.errors.startDate : ''}
/> />
<DatePickerComponent <DatePickerComponent
label={t('ads.endDate')} label={t('ads.endDate')}
onChange={() => { }} onChange={(value) => formik.setFieldValue('endDate', moment(value, 'jYYYY-jMM-jDD').format('YYYY-MM-DD'))}
placeholder='' placeholder=''
defaulValue='1400-01-01' error_text={formik.touched.endDate && formik.errors.endDate ? formik.errors.endDate : ''}
/> />
</div> </div>
<div className='mt-6 relative'> <div className='mt-6 relative'>
<Input <Input
label={t('ads.link')} label={t('ads.link')}
placeholder={t('ads.enter_your_link')} placeholder={t('ads.enter_your_link')}
{...formik.getFieldProps('link')}
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
/> />
<Link2 size={20} color='blue' className='absolute left-4 bottom-[10px]' /> <Link2 size={20} color='blue' className={clx(
'absolute left-4 bottom-[10px] cursor-pointer',
formik.touched.link && formik.errors.link ? 'bottom-[31px]' : ''
)} />
</div> </div>
</div> </div>
<div className='min-h-[calc(100vh-201px)] bg-white w-full xl:w-sidebar py-10 px-5 h-fit rounded-3xl'> <div className='min-h-[calc(100vh-201px)] bg-white w-full xl:w-sidebar py-10 px-5 h-fit rounded-3xl'>
@@ -65,39 +139,88 @@ const CreateAd: FC = () => {
{t('ads.deactive')} {t('ads.deactive')}
</div> </div>
<SwitchComponent <SwitchComponent
active={false} active={formik.values.isActive}
onChange={() => { }} onChange={(value) => formik.setFieldValue('isActive', value)}
/> />
</div> </div>
</div> </div>
<p className='mt-6 text-sm'>{t('ads.location')}</p> <p className='mt-6 text-sm'>{t('ads.location')}</p>
<div className='p-2 border border-[##D0D0D0] rounded-lg mt-1'> <div className='p-2 border border-[##D0D0D0] rounded-lg mt-1'>
<div className='flex items-start gap-2 py-2'> <div className='flex items-center py-2'>
<div> <div>
<TickSquare size={20} color='black' variant='Bold' /> <CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.HOMEPAGE_TOP}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.HOMEPAGE_TOP)}
/>
</div> </div>
<div className='text-description text-xs leading-5'> <div className='text-description text-xs leading-5'>
لورم ایپسوم {t(`ads.${AdsDisplayLocation.HOMEPAGE_TOP}`)}
</div> </div>
</div> </div>
<div className='flex items-start gap-2 py-2'> <div className='flex items-center py-2'>
<div> <div>
<TickSquare size={20} color='black' variant='Bold' /> <CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.SINGLE_SERVICE_PAGE}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.SINGLE_SERVICE_PAGE)}
/>
</div> </div>
<div className='text-description text-xs leading-5'> <div className='text-description text-xs leading-5'>
لورم ایپسوم {t(`ads.${AdsDisplayLocation.SINGLE_SERVICE_PAGE}`)}
</div> </div>
</div> </div>
<div className='flex items-start gap-2 py-2'> <div className='flex items-center py-2'>
<div> <div>
<TickSquare size={20} color='black' variant='Bold' /> <CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.MY_SERVICES_PAGE}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.MY_SERVICES_PAGE)}
/>
</div> </div>
<div className='text-description text-xs leading-5'> <div className='text-description text-xs leading-5'>
لورم ایپسوم {t(`ads.${AdsDisplayLocation.MY_SERVICES_PAGE}`)}
</div> </div>
</div> </div>
<div className='flex items-center py-2'>
<div>
<CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.OTHER_SERVICES_TOP}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_TOP)}
/>
</div>
<div className='text-description text-xs leading-5'>
{t(`ads.${AdsDisplayLocation.OTHER_SERVICES_TOP}`)}
</div>
</div>
<div className='flex items-center py-2'>
<div>
<CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.OTHER_SERVICES_LEFT}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.OTHER_SERVICES_LEFT)}
/>
</div>
<div className='text-description text-xs leading-5'>
{t(`ads.${AdsDisplayLocation.OTHER_SERVICES_LEFT}`)}
</div>
</div>
<div className='flex items-center py-2'>
<div>
<CheckBoxComponent
checked={formik.values.displayLocation === AdsDisplayLocation.SERVICE}
onChange={() => formik.setFieldValue('displayLocation', AdsDisplayLocation.SERVICE)}
/>
</div>
<div className='text-description text-xs leading-5'>
{t(`ads.${AdsDisplayLocation.SERVICE}`)}
</div>
</div>
</div>
<div className='mt-8'>
<UploadBoxDraggble
label={t('ads.upload_picture')}
onChange={(value) => setFile(value[0])}
/>
</div> </div>
<ImageUploader />
</div> </div>
</div> </div>
+28
View File
@@ -0,0 +1,28 @@
import { FC, useState } from 'react'
import SwitchComponent from '../../../components/Switch'
import { useToggleStatusAds } from '../hooks/useAdsData'
type Props = {
defaultActive: boolean,
id: string
}
const ToggleStatusAds: FC<Props> = (props: Props) => {
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
const toggleStatus = useToggleStatusAds()
const handleChange = (value: boolean) => {
setIsActive(value)
toggleStatus.mutate(props.id)
}
return (
<SwitchComponent
active={isActive}
onChange={handleChange}
/>
)
}
export default ToggleStatusAds
+27
View File
@@ -0,0 +1,27 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/AdsService";
import { CreateAdsType } from "../types/AdsTypes";
export const useGetAdsList = (
page: number,
search: string,
isActive: boolean,
since: string
) => {
return useQuery({
queryKey: ["ads", page, search, isActive, since],
queryFn: () => api.getAdsList({ page, search, isActive, since }),
});
};
export const useCreateAds = () => {
return useMutation({
mutationFn: (variables: CreateAdsType) => api.createAds(variables),
});
};
export const useToggleStatusAds = () => {
return useMutation({
mutationFn: (variables: string) => api.toggleStatusAds(variables),
});
};
+33
View File
@@ -0,0 +1,33 @@
import axios from "../../../config/axios";
import { CreateAdsType } from "../types/AdsTypes";
interface GetAdsListParams {
page: number;
search?: string;
isActive?: boolean;
since?: string;
}
export const getAdsList = async ({
page,
search,
isActive,
since,
}: GetAdsListParams) => {
let query = `?page=${page}&q=${search}&isActive=${isActive}`;
if (since) {
query += `&since=${since}`;
}
const { data } = await axios.get(`/advertise/list${query}`);
return data;
};
export const createAds = async (params: CreateAdsType) => {
const { data } = await axios.post(`/advertise`, params);
return data;
};
export const toggleStatusAds = async (id: string) => {
const { data } = await axios.patch(`/advertise/toggle-status/${id}`);
return data;
};
+33
View File
@@ -0,0 +1,33 @@
export type CreateAdsType = {
title: string;
displayLocation: string;
startDate: string;
endDate: string;
isActive: boolean;
imageUrl: string;
link: string;
serviceId?: string;
};
export enum AdsDisplayLocation {
HOMEPAGE_TOP = "HOMEPAGE_TOP",
SINGLE_SERVICE_PAGE = "SINGLE_SERVICE_PAGE",
MY_SERVICES_PAGE = "MY_SERVICES_PAGE",
OTHER_SERVICES_TOP = "OTHER_SERVICES_TOP",
OTHER_SERVICES_LEFT = "OTHER_SERVICES_LEFT",
SERVICE = "SERVICE",
}
export type AdsItemType = {
id: string;
title: string;
displayLocation: AdsDisplayLocation;
startDate: string;
endDate: string;
isActive: boolean;
imageUrl: string;
link: string;
service?: string | null;
createdAt: string;
updatedAt: string;
};