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_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
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 useConvertNumbers from './hooks/useConvertNumbers';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import moment from 'moment-jalaali';
i18next.init({
interpolation: { escapeValue: false },
@@ -47,6 +48,7 @@ const App: FC = () => {
// useNumberFont()
useConvertNumbers()
moment.loadPersian({ usePersianDigits: true, dialect: "persian-modern" });
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
useEffect(() => {
+54 -17
View File
@@ -18,11 +18,57 @@ type Props = {
onChangeSearchFinal?: (value: string) => void;
} & 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 [formattedValue, setFormattedValue] = useState<string>(
props.value ? formatNumber(props.value as string) : ''
);
const [showPassword, setShowPassword] = useState<boolean>(false)
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(() => {
if (props.variant === 'search' && props.onChangeSearchFinal) {
const timeout = setTimeout(() => {
@@ -32,13 +78,6 @@ const Input: FC<Props> = (props: 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 (
<div className='w-full'>
@@ -49,8 +88,8 @@ const Input: FC<Props> = (props: Props) => {
<div className='w-full relative mt-1'>
<input {...props} onChange={(e) => {
setSearch(e.target.value)
props.onChange?.(e)
}} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
handleInputChange(e)
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
{
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' />
}
{
props.error_text &&
<Error
errorText={props.error_text}
/>
}
</div>
{
props.error_text &&
<Error
errorText={props.error_text}
/>
}
</div>
)
}
+2 -1
View File
@@ -443,7 +443,8 @@
"failed": "پرداخت ناموفق!",
"desc_failed": "متأسفیم! پرداخت شما انجام نشد. لطفاً دوباره تلاش کنید یا روش پرداخت دیگری انتخاب کنید.",
"pending": "وضعیت پرداخت نامشخص است!",
"desc_pending": "اگر مبلغی از حساب شما کسر شده ولی سفارش شما بعد از ۷۲ ساعت تأیید نشد، لطفاً با پشتیبانی تماس بگیرید."
"desc_pending": "اگر مبلغی از حساب شما کسر شده ولی سفارش شما بعد از ۷۲ ساعت تأیید نشد، لطفاً با پشتیبانی تماس بگیرید.",
"home": "صفحه اصلی"
},
"discount": {
"number": "شماره",
+1 -1
View File
@@ -38,7 +38,7 @@ const Notifications: FC = () => {
<div ref={ref}>
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
<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}
</div>
</div>
+12 -5
View File
@@ -93,11 +93,18 @@ const CallBack: FC = () => {
</div>
</div>
<Button
label={t('callback.back')}
className='xl:max-w-[560px] mt-8'
onClick={() => navigate(Pages.transactions)}
/>
<div className='flex xl:max-w-[560px] w-full gap-2.5'>
<Button
label={t('callback.home')}
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>
)
}
+42 -102
View File
@@ -1,11 +1,8 @@
import { FC, Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import Rate from 'rc-rate'
import ServiceImages from './components/ServiceImages'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import AvatarImage from '../../assets/images/Avatar.png'
import AvatarImage from '../../assets/images/avatar_image.png'
import ServiceHeader from './components/ServiceHeader'
import { useGetDetailService } from './hooks/useServiceData'
import { useParams } from 'react-router-dom'
@@ -13,6 +10,8 @@ import PageLoading from '../../components/PageLoading'
import moment from 'moment-jalaali'
import { useGetAds } from '../ads/hooks/useAdsData'
import { AdsDisplayLocation } from '../ads/types/AdsTypes'
import CreateReview from './components/CreateReview'
import { ReviewItemType } from './types/ServiecTypes'
const DetailService: FC = () => {
@@ -74,8 +73,9 @@ const DetailService: FC = () => {
<Rate
count={5}
style={{ fontSize: 15 }}
value={4}
value={getDetailService?.data?.data?.averageRating}
disabled
allowHalf
/>
</div>
</div>
@@ -92,111 +92,51 @@ const DetailService: FC = () => {
</div>
<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>
{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 }}
/>
</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>
{
getDetailService.data?.data?.purchased &&
<CreateReview refetch={() => getDetailService.refetch()} />
}
<div className='flex-1 max-w-full bg-white p-6 rounded-3xl'>
<div className='text-sm'>
{t('service.users_comments')}
</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>
{
getDetailService.data?.data?.danakService?.reviews?.map((item: ReviewItemType) => {
return (
<div className='mt-5 border border-border rounded-3xl p-6'>
<div className='flex gap-2 items-center'>
<img src={item.user.profilePic || AvatarImage} className='size-6 rounded-full object-cover' />
<div className='text-description text-xs'>
{item.user?.firstName + ' ' + item.user?.lastName}
</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-4 flex justify-between'>
<div className='text-sm max-w-[70%] truncate'>
{item.title}
</div>
<div className='text-xs text-description'>
{moment(item.createdAt).fromNow()}
</div>
</div>
<div className='mt-2'>
<Rate
disabled
value={5}
count={5}
style={{ fontSize: 14 }}
/>
</div>
<div className='mt-2'>
<Rate
disabled
value={item.rating}
count={5}
style={{ fontSize: 14 }}
/>
</div>
<div className='mt-4 text-[11px] leading-6'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را ...
</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 className='mt-4 text-[11px] leading-6'>
{item.comment}
</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='text-xs whitespace-nowrap flex gap-1 items-center'>
<div className='flex gap-2'>
{
{/* {
finalPrice !== price &&
<div className='line-through text-xs text-description'>
{NumberFormat(Number(price))}
</div>
}
<div>{NumberFormat(Number(finalPrice))}</div>
} */}
<div>{NumberFormat(Number(price))}</div>
</div>
<span>{planText}</span>
</div>
+7 -1
View File
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/ServiceService";
import { BuyServiceType } from "../types/ServiecTypes";
import { BuyServiceType, CreateReviewType } from "../types/ServiecTypes";
export const useGetSuggestedServices = () => {
return useQuery({
@@ -44,3 +44,9 @@ export const useGetMyServices = (search: string) => {
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 { BuyServiceType } from "../types/ServiecTypes";
import { BuyServiceType, CreateReviewType } from "../types/ServiecTypes";
export const getServiceSuggestedDanak = async () => {
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}`);
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;
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'>
<Button
label={t('setting.save_change')}
className='w-32'
className='w-fit px-4'
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>
@@ -110,10 +116,16 @@ const ChangePassword: FC = () => {
<div className=' xl:hidden block'>
<Button
label={t('setting.save_change')}
className='w-32'
className='w-32 hidden xl:block'
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 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 UploadBox from '../../components/UploadBox'
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 { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
import PageLoading from '../../components/PageLoading'
@@ -212,11 +212,17 @@ const TicketDetail: FC = () => {
<div className='flex justify-end mt-6'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
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>
</Fragment>
}
@@ -87,6 +87,7 @@ const CardtoCard: FC = () => {
name='amount'
onChange={formik.handleChange}
error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''}
seprator
/>
</div>
+1
View File
@@ -77,6 +77,7 @@ const Online: FC = () => {
placeholder={t('wallet.enter_your_price')}
onChange={(e) => setDesiredAmount(Number(e.target.value))}
value={desiredAmount}
seprator
/>
</div>
+1
View File
@@ -88,6 +88,7 @@ const CardtoCard: FC = () => {
name='amount'
onChange={formik.handleChange}
error_text={formik.touched.amount && formik.errors.amount ? formik.errors.amount : ''}
seprator
/>
</div>