This commit is contained in:
hamid zarghami
2025-03-02 12:12:20 +03:30
parent 1ab4f29d65
commit 19b65baefe
17 changed files with 292 additions and 142 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
VITE_TOKEN_NAME = 'dsc_token' VITE_TOKEN_NAME = 'dsc_token'
VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token' VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
VITE_BASE_URL = 'https://api.danakcorp.com' VITE_BASE_URL = 'https://api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.0.216:4000' # VITE_BASE_URL = 'http://192.168.1.124:4000'
+2
View File
@@ -15,6 +15,7 @@ import AuthRouter from './router/Auth'
import { Pages } from './config/Pages' import { Pages } from './config/Pages'
import useConvertNumbers from './hooks/useConvertNumbers'; import useConvertNumbers from './hooks/useConvertNumbers';
import { Helmet, HelmetProvider } from 'react-helmet-async'; import { Helmet, HelmetProvider } from 'react-helmet-async';
import moment from 'moment-jalaali';
i18next.init({ i18next.init({
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
@@ -47,6 +48,7 @@ const App: FC = () => {
// useNumberFont() // useNumberFont()
useConvertNumbers() useConvertNumbers()
moment.loadPersian({ usePersianDigits: true, dialect: "persian-modern" });
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking') const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
useEffect(() => { useEffect(() => {
+54 -17
View File
@@ -18,11 +18,57 @@ type Props = {
onChangeSearchFinal?: (value: string) => void; onChangeSearchFinal?: (value: string) => void;
} & InputHTMLAttributes<HTMLInputElement> } & InputHTMLAttributes<HTMLInputElement>
const formatNumber = (value: string | number): string => {
if (!value) return '';
const inputValue = String(value).replace(/,/g, '');
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const Input: FC<Props> = (props: Props) => { const Input: FC<Props> = (props: Props) => {
const [formattedValue, setFormattedValue] = useState<string>(
props.value ? formatNumber(props.value as string) : ''
);
const [showPassword, setShowPassword] = useState<boolean>(false) const [showPassword, setShowPassword] = useState<boolean>(false)
const [search, setSearch] = useState<string>('') const [search, setSearch] = useState<string>('')
const inputClass = clx(
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
props.readOnly && 'bg-gray-100 border-0 text-description',
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
props.className
);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (props.seprator) {
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
const formatted = formatNumber(inputValue);
// به‌روزرسانی مقدار قالب‌بندی‌شده
setFormattedValue(formatted);
// ارسال مقدار خام به `onChange` والد
props.onChange?.({
...event,
target: {
...event.target,
value: inputValue,
},
});
} else {
props.onChange?.(event);
}
};
useEffect(() => {
if (props.value) {
setFormattedValue(formatNumber(props.value as string));
} else {
setFormattedValue('');
}
}, [props.value])
useEffect(() => { useEffect(() => {
if (props.variant === 'search' && props.onChangeSearchFinal) { if (props.variant === 'search' && props.onChangeSearchFinal) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@@ -32,13 +78,6 @@ const Input: FC<Props> = (props: Props) => {
} }
}, [search, props]) }, [search, props])
const inputClass = clx(
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
props.readOnly && 'bg-gray-100 border-0 text-description',
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
props.className
);
return ( return (
<div className='w-full'> <div className='w-full'>
@@ -49,8 +88,8 @@ const Input: FC<Props> = (props: Props) => {
<div className='w-full relative mt-1'> <div className='w-full relative mt-1'>
<input {...props} onChange={(e) => { <input {...props} onChange={(e) => {
setSearch(e.target.value) setSearch(e.target.value)
props.onChange?.(e) handleInputChange(e)
}} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} /> }} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
{ {
props.type === 'password' && props.type === 'password' &&
@@ -62,15 +101,13 @@ const Input: FC<Props> = (props: Props) => {
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' /> <SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
} }
{
props.error_text &&
<Error
errorText={props.error_text}
/>
}
</div> </div>
{
props.error_text &&
<Error
errorText={props.error_text}
/>
}
</div> </div>
) )
} }
+2 -1
View File
@@ -443,7 +443,8 @@
"failed": "پرداخت ناموفق!", "failed": "پرداخت ناموفق!",
"desc_failed": "متأسفیم! پرداخت شما انجام نشد. لطفاً دوباره تلاش کنید یا روش پرداخت دیگری انتخاب کنید.", "desc_failed": "متأسفیم! پرداخت شما انجام نشد. لطفاً دوباره تلاش کنید یا روش پرداخت دیگری انتخاب کنید.",
"pending": "وضعیت پرداخت نامشخص است!", "pending": "وضعیت پرداخت نامشخص است!",
"desc_pending": "اگر مبلغی از حساب شما کسر شده ولی سفارش شما بعد از ۷۲ ساعت تأیید نشد، لطفاً با پشتیبانی تماس بگیرید." "desc_pending": "اگر مبلغی از حساب شما کسر شده ولی سفارش شما بعد از ۷۲ ساعت تأیید نشد، لطفاً با پشتیبانی تماس بگیرید.",
"home": "صفحه اصلی"
}, },
"discount": { "discount": {
"number": "شماره", "number": "شماره",
+1 -1
View File
@@ -38,7 +38,7 @@ const Notifications: FC = () => {
<div ref={ref}> <div ref={ref}>
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'> <div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
<Notification size={18} color="black" /> <Notification size={18} color="black" />
<div className="absolute top-0 right-0 -mt-1 -mr-1 rounded-full bg-red-500 text-white text-[8px] size-3 flex justify-center items-center"> <div className="absolute top-0 right-0 -mt-1 -mr-1 rounded-full bg-red-500 text-white text-[8px] size-3 flex pt-0.5 pl-[1px] justify-center items-center">
{getDashboard.data?.data?.notificationCount} {getDashboard.data?.data?.notificationCount}
</div> </div>
</div> </div>
+12 -5
View File
@@ -93,11 +93,18 @@ const CallBack: FC = () => {
</div> </div>
</div> </div>
<Button <div className='flex xl:max-w-[560px] w-full gap-2.5'>
label={t('callback.back')} <Button
className='xl:max-w-[560px] mt-8' label={t('callback.home')}
onClick={() => navigate(Pages.transactions)} className='w-full mt-8 bg-transparent border border-black text-black'
/> onClick={() => navigate(Pages.dashboard)}
/>
<Button
label={t('callback.back')}
className='w-full mt-8'
onClick={() => navigate(Pages.transactions)}
/>
</div>
</div> </div>
) )
} }
+42 -102
View File
@@ -1,11 +1,8 @@
import { FC, Fragment } from 'react' import { FC, Fragment } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import Rate from 'rc-rate' import Rate from 'rc-rate'
import ServiceImages from './components/ServiceImages' import ServiceImages from './components/ServiceImages'
import Input from '../../components/Input' import AvatarImage from '../../assets/images/avatar_image.png'
import Textarea from '../../components/Textarea'
import AvatarImage from '../../assets/images/Avatar.png'
import ServiceHeader from './components/ServiceHeader' import ServiceHeader from './components/ServiceHeader'
import { useGetDetailService } from './hooks/useServiceData' import { useGetDetailService } from './hooks/useServiceData'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
@@ -13,6 +10,8 @@ import PageLoading from '../../components/PageLoading'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { useGetAds } from '../ads/hooks/useAdsData' import { useGetAds } from '../ads/hooks/useAdsData'
import { AdsDisplayLocation } from '../ads/types/AdsTypes' import { AdsDisplayLocation } from '../ads/types/AdsTypes'
import CreateReview from './components/CreateReview'
import { ReviewItemType } from './types/ServiecTypes'
const DetailService: FC = () => { const DetailService: FC = () => {
@@ -74,8 +73,9 @@ const DetailService: FC = () => {
<Rate <Rate
count={5} count={5}
style={{ fontSize: 15 }} style={{ fontSize: 15 }}
value={4} value={getDetailService?.data?.data?.averageRating}
disabled disabled
allowHalf
/> />
</div> </div>
</div> </div>
@@ -92,111 +92,51 @@ const DetailService: FC = () => {
</div> </div>
<div className='mt-8 flex xl:flex-row flex-col gap-8 items-start'> <div className='mt-8 flex xl:flex-row flex-col gap-8 items-start'>
<div className='bg-white p-6 rounded-3xl xl:w-[360px] w-full'> {
<div> getDetailService.data?.data?.purchased &&
{t('service.submit_comment')} <CreateReview refetch={() => getDetailService.refetch()} />
</div> }
<div className='text-xs mt-4'>
{t('service.hint_comment')}
</div>
<div className='mt-6 text-description text-sm'>
<div>
{t('service.score')}
</div>
<div className='mt-2 dltr flex justify-end'>
<Rate
count={5}
style={{ fontSize: 22 }}
/>
</div>
<div className='mt-6'>
<Input
label={t('service.title')}
placeholder={t('service.enter_title_comment')}
/>
</div>
<div className='mt-6'>
<Textarea
label={t('service.your_comment')}
placeholder={t('service.write_your_comment')}
/>
</div>
<div className='mt-6'>
<Button
label={t('service.submit_comment')}
/>
</div>
</div>
</div>
<div className='flex-1 max-w-full bg-white p-6 rounded-3xl'> <div className='flex-1 max-w-full bg-white p-6 rounded-3xl'>
<div className='text-sm'> <div className='text-sm'>
{t('service.users_comments')} {t('service.users_comments')}
</div> </div>
<div className='mt-5 border border-border rounded-3xl p-6'> {
<div className='flex gap-2 items-center'> getDetailService.data?.data?.danakService?.reviews?.map((item: ReviewItemType) => {
<img src={AvatarImage} className='size-6 rounded-full object-cover' /> return (
<div className='text-description text-xs'> <div className='mt-5 border border-border rounded-3xl p-6'>
فروغ براتی <div className='flex gap-2 items-center'>
</div> <img src={item.user.profilePic || AvatarImage} className='size-6 rounded-full object-cover' />
</div> <div className='text-description text-xs'>
{item.user?.firstName + ' ' + item.user?.lastName}
</div>
</div>
<div className='mt-4 flex justify-between'> <div className='mt-4 flex justify-between'>
<div className='text-sm max-w-[70%] truncate'> <div className='text-sm max-w-[70%] truncate'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ {item.title}
</div> </div>
<div className='text-xs text-description'> <div className='text-xs text-description'>
۳ روز پیش {moment(item.createdAt).fromNow()}
</div> </div>
</div> </div>
<div className='mt-2'> <div className='mt-2'>
<Rate <Rate
disabled disabled
value={5} value={item.rating}
count={5} count={5}
style={{ fontSize: 14 }} style={{ fontSize: 14 }}
/> />
</div> </div>
<div className='mt-4 text-[11px] leading-6'> <div className='mt-4 text-[11px] leading-6'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را ... {item.comment}
</div> </div>
</div> </div>
<div className='mt-5 border border-border rounded-3xl p-6'> )
<div className='flex gap-2 items-center'> })
<img src={AvatarImage} className='size-6 rounded-full object-cover' /> }
<div className='text-description text-xs'>
فروغ براتی
</div>
</div>
<div className='mt-4 flex justify-between'>
<div className='text-sm max-w-[70%] truncate'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
</div>
<div className='text-xs text-description'>
۳ روز پیش
</div>
</div>
<div className='mt-2'>
<Rate
disabled
value={5}
count={5}
style={{ fontSize: 14 }}
/>
</div>
<div className='mt-4 text-[11px] leading-6'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را ...
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,106 @@
import Rate from 'rc-rate'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import Textarea from '../../../components/Textarea'
import Button from '../../../components/Button'
import { useFormik } from 'formik'
import { CreateReviewType } from '../types/ServiecTypes'
import * as Yup from 'yup'
import { useParams } from 'react-router-dom'
import { useCreateReview } from '../hooks/useServiceData'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
import Error from '../../../components/Error'
type Props = {
refetch: () => void
}
const CreateReview: FC<Props> = ({ refetch }) => {
const { id } = useParams()
const { t } = useTranslation('global')
const createReview = useCreateReview()
const formik = useFormik<CreateReviewType>({
initialValues: {
title: '',
comment: '',
rating: undefined,
id: id ? id : ''
},
validationSchema: Yup.object({
title: Yup.string().required(t('errors.required')),
comment: Yup.string().required(t('errors.required')),
rating: Yup.string().required(t('errors.required')),
}),
onSubmit: (values) => {
createReview.mutate(values, {
onSuccess: () => {
formik.resetForm()
refetch()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
},
})
return (
<div className='bg-white p-6 rounded-3xl xl:w-[360px] w-full'>
<div>
{t('service.submit_comment')}
</div>
<div className='text-xs mt-4'>
{t('service.hint_comment')}
</div>
<div className='mt-6 text-description text-sm'>
<div>
{t('service.score')}
</div>
<div className='mt-2 dltr flex justify-end'>
<Rate
count={5}
style={{ fontSize: 22 }}
value={formik.values.rating}
onChange={(value) => formik.setFieldValue('rating', value)}
/>
</div>
{formik.errors.rating && formik.touched.rating && <Error errorText={formik.errors.rating} />}
<div className='mt-6'>
<Input
label={t('service.title')}
placeholder={t('service.enter_title_comment')}
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className='mt-6'>
<Textarea
label={t('service.your_comment')}
placeholder={t('service.write_your_comment')}
{...formik.getFieldProps('comment')}
error_text={formik.touched.comment && formik.errors.comment ? formik.errors.comment : ''}
/>
</div>
<div className='mt-6'>
<Button
label={t('service.submit_comment')}
onClick={() => formik.handleSubmit()}
isLoading={createReview.isPending}
/>
</div>
</div>
</div>
)
}
export default CreateReview
@@ -82,13 +82,13 @@ const ServiceHeader: FC<Props> = (props: Props) => {
<div className='flex gap-4 mt-5 xl:mt-0 items-center justify-between xl:justify-normal'> <div className='flex gap-4 mt-5 xl:mt-0 items-center justify-between xl:justify-normal'>
<div className='text-xs whitespace-nowrap flex gap-1 items-center'> <div className='text-xs whitespace-nowrap flex gap-1 items-center'>
<div className='flex gap-2'> <div className='flex gap-2'>
{ {/* {
finalPrice !== price && finalPrice !== price &&
<div className='line-through text-xs text-description'> <div className='line-through text-xs text-description'>
{NumberFormat(Number(price))} {NumberFormat(Number(price))}
</div> </div>
} } */}
<div>{NumberFormat(Number(finalPrice))}</div> <div>{NumberFormat(Number(price))}</div>
</div> </div>
<span>{planText}</span> <span>{planText}</span>
</div> </div>
+7 -1
View File
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/ServiceService"; import * as api from "../service/ServiceService";
import { BuyServiceType } from "../types/ServiecTypes"; import { BuyServiceType, CreateReviewType } from "../types/ServiecTypes";
export const useGetSuggestedServices = () => { export const useGetSuggestedServices = () => {
return useQuery({ return useQuery({
@@ -44,3 +44,9 @@ export const useGetMyServices = (search: string) => {
queryFn: () => api.getMyServices(search), queryFn: () => api.getMyServices(search),
}); });
}; };
export const useCreateReview = () => {
return useMutation({
mutationFn: (variables: CreateReviewType) => api.createReview(variables),
});
};
+9 -1
View File
@@ -1,5 +1,5 @@
import axios from "../../../config/axios"; import axios from "../../../config/axios";
import { BuyServiceType } from "../types/ServiecTypes"; import { BuyServiceType, CreateReviewType } from "../types/ServiecTypes";
export const getServiceSuggestedDanak = async () => { export const getServiceSuggestedDanak = async () => {
const { data } = await axios.get(`/danak-services/suggested`); const { data } = await axios.get(`/danak-services/suggested`);
@@ -32,3 +32,11 @@ export const getMyServices = async (search: string) => {
const { data } = await axios.get(`/subscriptions/user?q=${search}`); const { data } = await axios.get(`/subscriptions/user?q=${search}`);
return data; return data;
}; };
export const createReview = async (params: CreateReviewType) => {
const { data } = await axios.post(
`/danak-services/${params.id}/reviews`,
params
);
return data;
};
+22
View File
@@ -119,3 +119,25 @@ export type BuyServiceType = {
description: string; description: string;
serviceId: string; serviceId: string;
}; };
export type CreateReviewType = {
id: string;
comment: string;
rating?: number;
title: string;
};
export type ReviewItemType = {
comment: string;
createdAt: string;
id: string;
rating: number;
status: "APPROVED" | "PENDING" | "REJECTED";
title: string;
user: {
id: string;
firstName: string;
lastName: string;
profilePic: string | null;
};
};
@@ -81,10 +81,16 @@ const ChangePassword: FC = () => {
<div className='mt-9 xl:block hidden'> <div className='mt-9 xl:block hidden'>
<Button <Button
label={t('setting.save_change')} className='w-fit px-4'
className='w-32'
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
/> >
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('setting.save_change')}
</div>
</div>
</Button>
</div> </div>
</div> </div>
@@ -110,10 +116,16 @@ const ChangePassword: FC = () => {
<div className=' xl:hidden block'> <div className=' xl:hidden block'>
<Button <Button
label={t('setting.save_change')} className='w-32 hidden xl:block'
className='w-32'
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
/> >
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('setting.save_change')}
</div>
</div>
</Button>
</div> </div>
<div className='justify-end xl:hidden flex'> <div className='justify-end xl:hidden flex'>
+10 -4
View File
@@ -4,7 +4,7 @@ import Input from '../../components/Input'
import Textarea from '../../components/Textarea' import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox' import UploadBox from '../../components/UploadBox'
import Button from '../../components/Button' import Button from '../../components/Button'
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react' import { CloseCircle, InfoCircle, Paperclip2, Send2 } from 'iconsax-react'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData' import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
import PageLoading from '../../components/PageLoading' import PageLoading from '../../components/PageLoading'
@@ -212,11 +212,17 @@ const TicketDetail: FC = () => {
<div className='flex justify-end mt-6'> <div className='flex justify-end mt-6'>
<Button <Button
label={t('ticket.send')} className='xl:max-w-[100px]'
className='max-w-[100px]'
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
isLoading={addMessage.isPending || multiUpload.isPending} isLoading={addMessage.isPending || multiUpload.isPending}
/> >
<div className='flex gap-2 items-center'>
<Send2 size={20} color='white' />
<div>
{t('ticket.send')}
</div>
</div>
</Button>
</div> </div>
</Fragment> </Fragment>
} }
@@ -87,6 +87,7 @@ const CardtoCard: FC = () => {
name='amount' name='amount'
onChange={formik.handleChange} onChange={formik.handleChange}
error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''} error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''}
seprator
/> />
</div> </div>
+1
View File
@@ -77,6 +77,7 @@ const Online: FC = () => {
placeholder={t('wallet.enter_your_price')} placeholder={t('wallet.enter_your_price')}
onChange={(e) => setDesiredAmount(Number(e.target.value))} onChange={(e) => setDesiredAmount(Number(e.target.value))}
value={desiredAmount} value={desiredAmount}
seprator
/> />
</div> </div>
+1
View File
@@ -88,6 +88,7 @@ const CardtoCard: FC = () => {
name='amount' name='amount'
onChange={formik.handleChange} onChange={formik.handleChange}
error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''} error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''}
seprator
/> />
</div> </div>