Compare commits

...

15 Commits

Author SHA1 Message Date
hamid zarghami 0e429fbbc1 docker file to mirror
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-07 11:14:28 +03:30
hamid zarghami 470204c686 pull
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-07 10:58:32 +03:30
hamid zarghami 4bc6168246 conditioins in login 2026-07-07 10:55:46 +03:30
danak 7709c5a01f Update Dockerfile
deploy to danak / build_and_deploy (push) Has been cancelled
docker
2026-06-13 07:17:59 +00:00
hamid zarghami 1ce6f3387a dpage login
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-13 09:39:12 +03:30
hamid zarghami 6bc8a24683 mirror docker
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-01 11:32:08 +03:30
hamid zarghami 36f3415344 fix accebily link 2026-02-16 10:22:12 +03:30
hamid zarghami 8042bdc481 fix active sidebar 2026-02-16 10:09:16 +03:30
hamid zarghami 0569e5cac1 buy dkala 2026-02-15 14:04:13 +03:30
hamid zarghami aae0706e03 add is loading + not found 2026-02-07 16:59:18 +03:30
hamid zarghami 433bd65cb0 forgot password 2026-02-07 12:01:28 +03:30
hamid zarghami c9c42cf638 pixel perfect search 2026-02-07 10:33:34 +03:30
hamid zarghami 1885bca62a manage restaurant fix design bug 2026-02-07 10:16:51 +03:30
hamid zarghami 47b9b4b196 remove business phone 2026-02-07 10:10:58 +03:30
hamid zarghami 80d9c16073 search 2026-02-07 10:07:02 +03:30
24 changed files with 730 additions and 139 deletions
+24 -3
View File
@@ -1,19 +1,40 @@
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
# Change Alpine repo
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
# Configure npm registry mirror (Liara)
ENV NPM_CONFIG_REGISTRY=https://package-mirror.liara.ir/repository/npm/
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/
# Install tzdata to support timezone settings # Install tzdata to support timezone settings
RUN apk add --no-cache tzdata # RUN apk add --no-cache tzdata
# Set the timezone to Asia/Tehran # Set the timezone to Asia/Tehran
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone # RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
WORKDIR /build WORKDIR /build
# Optional build-args (CapRover / docker build --build-arg). When unset, Vite reads .env from COPY.
ARG VITE_BASE_URL
ARG VITE_TOKEN_NAME
ARG VITE_REFRESH_TOKEN_NAME
COPY package*.json ./ COPY package*.json ./
RUN npm ci --legacy-peer-deps RUN npm ci --legacy-peer-deps
COPY . ./ COPY . ./
RUN npm run build RUN set -e; \
env_args=""; \
[ -n "$VITE_BASE_URL" ] && env_args="$env_args VITE_BASE_URL=$VITE_BASE_URL"; \
[ -n "$VITE_TOKEN_NAME" ] && env_args="$env_args VITE_TOKEN_NAME=$VITE_TOKEN_NAME"; \
[ -n "$VITE_REFRESH_TOKEN_NAME" ] && env_args="$env_args VITE_REFRESH_TOKEN_NAME=$VITE_REFRESH_TOKEN_NAME"; \
eval "env$env_args npm run build"
FROM nginx:stable-alpine AS production-stage FROM nginx:stable-alpine AS production-stage
# Change Alpine repo
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
COPY --from=builder /build/dist /usr/share/nginx/html COPY --from=builder /build/dist /usr/share/nginx/html
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

+22 -12
View File
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import { ItemServiceType } from '../pages/service/types/ServiecTypes' import { ItemServiceType } from '../pages/service/types/ServiecTypes'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { NumberFormat } from '../config/func' import { NumberFormat } from '../config/func'
import { useDirectLogin, useGetDmenuSubscription } from '../pages/service/hooks/useServiceData' import { useDirectLogin, useDirectLoginDkala, useDirectLoginDpage, useGetDmenuSubscription, useGetDkalaSubscription } from '../pages/service/hooks/useServiceData'
import { toast } from './Toast' import { toast } from './Toast'
import { ErrorType } from '../helpers/types' import { ErrorType } from '../helpers/types'
@@ -31,8 +31,18 @@ const ServiceItem: FC<Props> = (props: Props) => {
const { item } = props const { item } = props
const isDmenu = item?.slug === 'Dmenu' const isDmenu = item?.slug === 'Dmenu'
const { data: dmenuSubscription, isPending, isError } = useGetDmenuSubscription(isChecked && isDmenu && props.isLinkPanel ? props.subscriptionId! : '',) const isDkala = item?.slug === 'Dkala'
const isDpage = item?.slug === 'Dpage'
const isDmenuOrDkala = isDmenu || isDkala
const needsDirectLogin = isDmenuOrDkala || isDpage
const { data: dmenuSubscription, isPending: isDmenuPending, isError: isDmenuError } = useGetDmenuSubscription(isChecked && isDmenu && props.isLinkPanel ? props.subscriptionId! : '')
const { data: dkalaSubscription, isPending: isDkalaPending, isError: isDkalaError } = useGetDkalaSubscription(isChecked && isDkala && props.isLinkPanel ? props.subscriptionId! : '')
const subscription = isDmenu ? dmenuSubscription : dkalaSubscription
const isPending = isDmenu ? isDmenuPending : isDkalaPending
const isError = isDmenu ? isDmenuError : isDkalaError
const { mutate: directLogin } = useDirectLogin() const { mutate: directLogin } = useDirectLogin()
const { mutate: directLoginDkala } = useDirectLoginDkala()
const { mutate: directLoginDpage } = useDirectLoginDpage()
const getRemainingDays = (endDate: string): number => { const getRemainingDays = (endDate: string): number => {
const now = moment() const now = moment()
@@ -63,14 +73,15 @@ const ServiceItem: FC<Props> = (props: Props) => {
} }
const handleDirectLogin = () => { const handleDirectLogin = () => {
if (!isDmenu) { if (!needsDirectLogin) {
window.open(item.link, '_blank') window.open(item.link, '_blank')
return return
} }
if (!props.subscriptionId) { if (!props.subscriptionId) {
return return
} }
directLogin(props.subscriptionId, { const doLogin = isDkala ? directLoginDkala : isDpage ? directLoginDpage : directLogin
doLogin(props.subscriptionId, {
onSuccess: (data) => { onSuccess: (data) => {
window.open(item.link + `?token=${data?.data?.tokens?.accessToken?.token}&refreshToken=${data?.data?.tokens?.refreshToken?.token}`, '_blank') window.open(item.link + `?token=${data?.data?.tokens?.accessToken?.token}&refreshToken=${data?.data?.tokens?.refreshToken?.token}`, '_blank')
}, },
@@ -78,12 +89,11 @@ const ServiceItem: FC<Props> = (props: Props) => {
toast(error.response?.data?.error?.message[0], 'error') toast(error.response?.data?.error?.message[0], 'error')
} }
}) })
} }
useEffect(() => { useEffect(() => {
if (isChecked && isDmenu && props.isLinkPanel) { if (isChecked && isDmenuOrDkala && props.isLinkPanel) {
if (dmenuSubscription && !isPending) { if (subscription && !isPending) {
if (item.link) { if (item.link) {
// window.open(item.link, '_blank') // window.open(item.link, '_blank')
handleDirectLogin() handleDirectLogin()
@@ -91,12 +101,12 @@ const ServiceItem: FC<Props> = (props: Props) => {
setIsChecked(false) setIsChecked(false)
} else if (isError && !isPending) { } else if (isError && !isPending) {
if (props.subscriptionId) { if (props.subscriptionId) {
navigate(Pages.services.manageRestaurant + props.subscriptionId) navigate(isDmenu ? Pages.services.manageRestaurant + props.subscriptionId : Pages.services.manageDkala + props.subscriptionId)
} }
setIsChecked(false) setIsChecked(false)
} }
} }
}, [dmenuSubscription, isError, isPending, isChecked, isDmenu, props.isLinkPanel, props.subscriptionId, item.link, navigate]) }, [subscription, isError, isPending, isChecked, isDmenuOrDkala, props.isLinkPanel, props.subscriptionId, item.link, navigate])
const handleCheckSubscription = () => { const handleCheckSubscription = () => {
if (props.isDisabled) { if (props.isDisabled) {
@@ -104,7 +114,7 @@ const ServiceItem: FC<Props> = (props: Props) => {
} }
if (props.isLinkPanel && item.link) { if (props.isLinkPanel && item.link) {
if (isDmenu && props.subscriptionId) { if (isDmenuOrDkala && props.subscriptionId) {
setIsChecked(true) setIsChecked(true)
} else { } else {
// window.open(item.link, '_blank') // window.open(item.link, '_blank')
@@ -159,7 +169,7 @@ const ServiceItem: FC<Props> = (props: Props) => {
<Button <Button
className='h-8 sm:w-fit w-full px-2 text-xs text-black bg-description bg-opacity-20 rounded-xl' className='h-8 sm:w-fit w-full px-2 text-xs text-black bg-description bg-opacity-20 rounded-xl'
onClick={handleCheckSubscription} onClick={handleCheckSubscription}
isLoading={isChecked && isDmenu && props.isLinkPanel ? isPending : false} isLoading={isChecked && isDmenuOrDkala && props.isLinkPanel ? isPending : false}
> >
<div className='flex gap-2 whitespace-nowrap'> <div className='flex gap-2 whitespace-nowrap'>
@@ -174,7 +184,7 @@ const ServiceItem: FC<Props> = (props: Props) => {
</Button> </Button>
{ {
props.subscriptionId && props.subscriptionId && !props.isQuikAccess &&
<Link className='sm:w-fit w-full' to={Pages.services.detail + item.slug + '?id=' + props.subscriptionId}> <Link className='sm:w-fit w-full' to={Pages.services.detail + item.slug + '?id=' + props.subscriptionId}>
<Button <Button
className='h-8 sm:w-fit px-2 text-xs text-black bg-description bg-opacity-20 rounded-xl' className='h-8 sm:w-fit px-2 text-xs text-black bg-description bg-opacity-20 rounded-xl'
+2 -1
View File
@@ -7,9 +7,10 @@ export const Pages = {
dashboard: "/dashboard", dashboard: "/dashboard",
services: { services: {
manageRestaurant: "/services/manage-restaurant/", manageRestaurant: "/services/manage-restaurant/",
manageDkala: "/services/manage-dkala/",
mine: "/services", mine: "/services",
other: "/other-service", other: "/other-service",
detail: "/services/detail/", detail: "/other-service/detail/",
buy: "/other-service/buy/", buy: "/other-service/buy/",
}, },
transactions: "/transactions", transactions: "/transactions",
+19 -2
View File
@@ -42,12 +42,18 @@
"enter_mobile_number": "شماره موبایل خود را وارد کنید", "enter_mobile_number": "شماره موبایل خود را وارد کنید",
"try_again": "ارسال مجدد", "try_again": "ارسال مجدد",
"toast_resend_code": "کد جدیدی برای شما ارسال شد", "toast_resend_code": "کد جدیدی برای شما ارسال شد",
"old_version": "نسخه قدیمی داشبورد" "old_version": "نسخه قدیمی داشبورد",
"enter_info_forgot": "ایمیل خود را وارد کنید",
"welcome_forgot": "فراموشی رمز عبور",
"email_code": "کد تایید",
"newPassword": "پسورد جدید"
}, },
"errors": { "errors": {
"required": "این فیلد اجباری می باشد", "required": "این فیلد اجباری می باشد",
"subscription_not_found": "شناسه اشتراک یافت نشد",
"password_not_match": "رمز عبور با تکرار آن مطابقت ندارد", "password_not_match": "رمز عبور با تکرار آن مطابقت ندارد",
"is_not_image": "فرمت فایل انتخاب شده اشتباه می باشد" "is_not_image": "فرمت فایل انتخاب شده اشتباه می باشد",
"format_email_error": "فرمت ایمیل اشتباه است"
}, },
"description": "توضیحات", "description": "توضیحات",
"sidebar": { "sidebar": {
@@ -535,5 +541,16 @@
"enter_slug": "اسلاگ را وارد کنید", "enter_slug": "اسلاگ را وارد کنید",
"established_year": "سال تأسیس", "established_year": "سال تأسیس",
"enter_year": "سال تأسیس را وارد کنید" "enter_year": "سال تأسیس را وارد کنید"
},
"dkala": {
"manage": "مدیریت دی‌کالا",
"shop_info": "اطلاعات فروشگاه",
"enter_shop_info": "لطفا اطلاعات فروشگاه خود را وارد کنید",
"slug": "اسلاگ",
"enter_slug": "اسلاگ را وارد کنید",
"slug_validation": "اسلاگ باید فقط شامل حروف انگلیسی، اعداد و خط تیره باشد",
"service_info_error": "خطا در دریافت اطلاعات سرویس",
"direct_login_error": "خطا در ورود مستقیم",
"create_shop_error": "خطا در ایجاد فروشگاه"
} }
} }
+42
View File
@@ -0,0 +1,42 @@
import { useState, type FC } from 'react'
import LogoImage from '../../assets/images/logo.svg'
import LogoSmallImage from '../../assets/images/logo-small.svg'
import ForgotStep1 from './components/ForgotStep1'
import ForgotStep2 from './components/ForgotStep2'
const Forgot: FC = () => {
const [step, setStep] = useState<number>(1)
const [email, setEmail] = useState<string>('')
return (
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
<div className='flex-1 h-full flex flex-col'>
<div className='flex relative flex-1 flex-col h-full justify-center'>
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
{
step === 1 ?
<ForgotStep1 changeEmail={setEmail} changeStep={setStep} />
:
<ForgotStep2 email={email} changeStep={setStep} />
}
</div>
</div>
</div>
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
<img src={LogoImage} className='h-20' />
</div>
</div>
</div>
)
}
export default Forgot
+69
View File
@@ -0,0 +1,69 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import { useFormik } from 'formik'
import { ForgotPasswordType } from '../types/AuthTypes'
import * as Yup from 'yup'
import Button from '../../../components/Button'
import { useForgotPassword } from '../hooks/useAuthData'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
type Props = {
changeStep: (value: number) => void,
changeEmail: (value: string) => void,
}
const ForgotStep1: FC<Props> = ({ changeStep, changeEmail }) => {
const { t } = useTranslation('global')
const { mutate, isPending } = useForgotPassword()
const formik = useFormik<ForgotPasswordType>({
initialValues: {
email: ''
},
validationSchema: Yup.object({
email: Yup.string().email(t('errors.format_email_error')).required(t('errors.required'))
}),
onSubmit: (values) => {
mutate(values, {
onSuccess: (data) => {
toast(data?.data?.message)
changeEmail(values.email)
changeStep(2)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0])
}
})
}
})
return (
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.welcome_forgot')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_info_forgot')}
</p>
<div className='mt-16'>
<Input
label={t('email')}
{...formik.getFieldProps('email')}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
</div>
<Button
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
/>
</div>
)
}
export default ForgotStep1
+89
View File
@@ -0,0 +1,89 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import { useFormik } from 'formik'
import { ResetPasswordType } from '../types/AuthTypes'
import * as Yup from 'yup'
import Button from '../../../components/Button'
import { useResetPassword } from '../hooks/useAuthData'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
import { Pages } from '../../../config/Pages'
type Props = {
changeStep: (value: number) => void,
email: string,
}
const ForgotStep2: FC<Props> = ({ email }) => {
const { t } = useTranslation('global')
const { mutate, isPending } = useResetPassword()
const formik = useFormik<ResetPasswordType>({
initialValues: {
email: email,
code: '',
newPassword: ''
},
validationSchema: Yup.object({
email: Yup.string().email(t('errors.format_email_error')).required(t('errors.required')),
code: Yup.string().required(t('errors.required')),
newPassword: Yup.string().required(t('errors.required')),
}),
onSubmit: (values) => {
mutate(values, {
onSuccess: (data) => {
toast(data?.data?.message)
window.location.href = Pages.auth.login
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0])
}
})
}
})
return (
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.welcome_forgot')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_info_forgot')}
</p>
<div className='mt-16'>
<Input
readOnly
label={t('email')}
{...formik.getFieldProps('email')}
value={email}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
<div className='mt-5'>
<Input
label={t('auth.email_code')}
{...formik.getFieldProps('code')}
error_text={formik.touched.code && formik.errors.code ? formik.errors.code : ''}
/>
</div>
<div className='mt-5'>
<Input
label={t('auth.newPassword')}
{...formik.getFieldProps('newPassword')}
error_text={formik.touched.newPassword && formik.errors.newPassword ? formik.errors.newPassword : ''}
/>
</div>
</div>
<Button
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
/>
</div>
)
}
export default ForgotStep2
+96 -96
View File
@@ -1,97 +1,99 @@
import { FC } from 'react' import { useFormik } from "formik";
import Input from '../../../components/Input' import { ArrowLeft } from "iconsax-react";
import { useTranslation } from 'react-i18next' import { FC } from "react";
import { LoginType } from '../types/AuthTypes' import { useTranslation } from "react-i18next";
import { useFormik } from 'formik' import { Link } from "react-router-dom";
import * as Yup from 'yup' import * as Yup from "yup";
import { useAuthStore } from '../store/AuthStore' import Button from "../../../components/Button";
import Error from '../../../components/Error' import Error from "../../../components/Error";
import Button from '../../../components/Button' import Input from "../../../components/Input";
import { isEmail } from '../../../config/func' import { toast } from "../../../components/Toast";
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData' import { isEmail } from "../../../config/func";
import { ErrorType } from '../../../helpers/types' import { Pages } from "../../../config/Pages";
import { toast } from '../../../components/Toast' import { ErrorType } from "../../../helpers/types";
import { useCheckHasAccount, useLoginWithOtp } from "../hooks/useAuthData";
import { useAuthStore } from "../store/AuthStore";
import { LoginType } from "../types/AuthTypes";
const LoginStep1: FC = () => { const LoginStep1: FC = () => {
const { t } = useTranslation("global");
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore();
const loginWithOtp = useLoginWithOtp();
const checkHasAccount = useCheckHasAccount();
const { t } = useTranslation('global') const formik = useFormik<LoginType>({
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore() initialValues: {
const loginWithOtp = useLoginWithOtp() phone_email: phone,
const checkHasAccount = useCheckHasAccount() },
validationSchema: Yup.object({
phone_email: Yup.string().required(t("errors.required")),
}),
onSubmit(values) {
if (isEmail(values.phone_email)) {
setEmail(values.phone_email);
checkHasAccount.mutate(
{ email: values.phone_email },
{
onSuccess() {
setStepLogin(2);
},
onError(error: ErrorType) {
toast(error.response?.data?.error.message?.[0], "error");
},
},
);
} else {
setPhone(values.phone_email);
loginWithOtp.mutate(
{ phone: values.phone_email },
{
onSuccess() {
setStepLogin(2);
toast(t("auth.otp_sent"), "success");
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], "error");
},
},
);
}
},
});
const formik = useFormik<LoginType>({ return (
initialValues: { <div>
phone_email: phone, <div className="mt-5">
}, <h2 className="text-2xl font-bold">{t("auth.welcome")}</h2>
validationSchema: Yup.object({ <p className="text-description text-sm mt-2">{t("auth.enter_info_login")}</p>
phone_email: Yup.string() </div>
.required(t('errors.required'))
}),
onSubmit(values) {
if (isEmail(values.phone_email)) {
setEmail(values.phone_email)
checkHasAccount.mutate({ email: values.phone_email }, {
onSuccess() {
setStepLogin(2)
},
onError(error: ErrorType) {
toast(error.response?.data?.error.message?.[0], 'error')
},
})
} else {
setPhone(values.phone_email)
loginWithOtp.mutate({ phone: values.phone_email }, {
onSuccess() {
setStepLogin(2)
toast(t('auth.otp_sent'), 'success')
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
},
})
}
},
})
return ( <div className="mt-16">
<div> <Input
<div className='mt-5'> label={t("auth.mobile_or_email")}
<h2 className='text-2xl font-bold'> placeholder={t("auth.enter_mobile_or_email")}
{t('auth.welcome')} type="text"
</h2> className="text-right"
<p className='text-description text-sm mt-2'> name="phone_email"
{t('auth.enter_info_login')} onChange={formik.handleChange}
</p> value={formik.values.phone_email}
</div> />
<div className='mt-16'> {formik.touched.phone_email && formik.errors.phone_email && <Error errorText={formik.errors.phone_email} />}
<Input </div>
label={t('auth.mobile_or_email')}
placeholder={t('auth.enter_mobile_or_email')}
type='text'
className='text-right'
name='phone_email'
onChange={formik.handleChange}
value={formik.values.phone_email}
/>
{ <label className="mt-4 flex items-center gap-2 text-sm text-description cursor-pointer">
formik.touched.phone_email && formik.errors.phone_email && <input type="checkbox" checked readOnly className="mt-1 h-4 w-4 accent-primary" />
<Error <span className="text-xs mt-1">
errorText={formik.errors.phone_email} شما با ثبتنام، تمامی{" "}
/> <a href="https://danakcorp.com/conditions" target="_blank" rel="noopener noreferrer" className="text-primary">
} قوانین و مقررات داناک
</a>{" "}
را میپذیرید.
</span>
</label>
</div> <Button label={t("auth.next")} className="mt-8" onClick={() => formik.handleSubmit()} isLoading={loginWithOtp.isPending || checkHasAccount.isPending} />
{/* <div className='mt-6 flex justify-center gap-2 items-center text-sm'>
<Button
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
/>
{/* <div className='mt-6 flex justify-center gap-2 items-center text-sm'>
<p className='text-description'> <p className='text-description'>
{t('auth.have_account')} {t('auth.have_account')}
</p> </p>
@@ -100,14 +102,12 @@ const LoginStep1: FC = () => {
</Link> </Link>
</div> */} </div> */}
{/* <a href='https://accounts.danakcorp.com' target='_blank' className='mt-6 flex gap-1 text-sm justify-center text-description'> <Link to={Pages.auth.forgotPassword} className="mt-6 flex gap-1 text-sm justify-center text-description">
<div> <div>{t("auth.forgot_password")}</div>
{t('auth.old_version')} <ArrowLeft size={20} color="black" />
</div> </Link>
<ArrowLeft size={20} color='black' /> </div>
</a> */} );
</div> };
)
}
export default LoginStep1 export default LoginStep1;
+12
View File
@@ -42,4 +42,16 @@ export const useLogout = () => {
return useMutation({ return useMutation({
mutationFn: (_) => api.logout(), mutationFn: (_) => api.logout(),
}); });
};
export const useForgotPassword = () => {
return useMutation({
mutationFn: api.forgotPassword,
});
};
export const useResetPassword = () => {
return useMutation({
mutationFn: api.resetPassword,
});
}; };
+13 -1
View File
@@ -2,11 +2,13 @@ import axios from "../../../config/axios";
import { import {
CheckHasAccountPhoneType, CheckHasAccountPhoneType,
CheckHasAccountType, CheckHasAccountType,
ForgotPasswordType,
LoginWithOtpType, LoginWithOtpType,
LoginWithPasswordType, LoginWithPasswordType,
OtpVerifyType, OtpVerifyType,
RefreshTokenType, RefreshTokenType,
RegisterType, RegisterType,
ResetPasswordType,
} from "../types/AuthTypes"; } from "../types/AuthTypes";
export const loginWithPassword = async (params: LoginWithPasswordType) => { export const loginWithPassword = async (params: LoginWithPasswordType) => {
@@ -29,7 +31,7 @@ export const checkHasAccount = async (params: CheckHasAccountType) => {
}; };
export const checkHasAccountRegister = async ( export const checkHasAccountRegister = async (
params: CheckHasAccountPhoneType params: CheckHasAccountPhoneType,
) => { ) => {
const { data } = await axios.post(`/auth/register/initiate`, params); const { data } = await axios.post(`/auth/register/initiate`, params);
return data; return data;
@@ -48,3 +50,13 @@ export const logout = async () => {
const { data } = await axios.post(`/auth/logout`); const { data } = await axios.post(`/auth/logout`);
return data; return data;
}; };
export const forgotPassword = async (params: ForgotPasswordType) => {
const { data } = await axios.post(`/auth/forgot-password`, params);
return data;
};
export const resetPassword = async (params: ResetPasswordType) => {
const { data } = await axios.post(`/auth/reset-password`, params);
return data;
};
+10
View File
@@ -48,3 +48,13 @@ export type RegisterType = {
export type RefreshTokenType = { export type RefreshTokenType = {
refreshToken: string; refreshToken: string;
}; };
export type ForgotPasswordType = {
email: string;
};
export type ResetPasswordType = {
newPassword: string;
email: string;
code: string;
};
+1
View File
@@ -134,6 +134,7 @@ const Home: FC = () => {
isQuikAccess isQuikAccess
businessName={item.userSubscription.businessName} businessName={item.userSubscription.businessName}
isLinkPanel isLinkPanel
subscriptionId={item.userSubscription.id}
onMoreClick={() => setShowModal(true)} onMoreClick={() => setShowModal(true)}
/> />
) )
+2 -3
View File
@@ -26,7 +26,6 @@ const BuyService: FC = () => {
const formik = useFormik<BuyServiceType>({ const formik = useFormik<BuyServiceType>({
initialValues: { initialValues: {
businessName: '', businessName: '',
businessPhone: '',
description: '', description: '',
planId: planId ? planId : '', planId: planId ? planId : '',
serviceId: serviceId ? serviceId : '' serviceId: serviceId ? serviceId : ''
@@ -82,13 +81,13 @@ const BuyService: FC = () => {
error_text={formik.touched.businessName && formik.errors.businessName ? formik.errors.businessName : ''} error_text={formik.touched.businessName && formik.errors.businessName ? formik.errors.businessName : ''}
/> />
</div> </div>
<div className='mt-8'> {/* <div className='mt-8'>
<Input <Input
label={t('service.phone_business')} label={t('service.phone_business')}
{...formik.getFieldProps('businessPhone')} {...formik.getFieldProps('businessPhone')}
error_text={formik.touched.businessPhone && formik.errors.businessPhone ? formik.errors.businessPhone : ''} error_text={formik.touched.businessPhone && formik.errors.businessPhone ? formik.errors.businessPhone : ''}
/> />
</div> </div> */}
<div className='mt-8'> <div className='mt-8'>
<Textarea <Textarea
label={t('service.short_description')} label={t('service.short_description')}
+144
View File
@@ -0,0 +1,144 @@
import { type FC } from 'react'
import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useCreateDkalaShop, useDirectLoginDkala, useGetSubscription } from './hooks/useServiceData'
import { useFormik } from 'formik'
import { DkalaCreateShopType } from './types/ServiecTypes'
import * as Yup from 'yup'
import { toast } from '../../components/Toast'
import { ErrorType } from '../../helpers/types'
import Button from '../../components/Button'
import Error from '../../components/Error'
import { TickCircle } from 'iconsax-react'
import { Helmet } from 'react-helmet-async'
import GuideSlugImage from '../../assets/images/dkala.png'
export const ManageDkala: FC = () => {
const { t } = useTranslation('global')
const { subscriptionId } = useParams()
const createDkalaShop = useCreateDkalaShop()
const getSubscription = useGetSubscription(subscriptionId || '')
const { mutate: directLoginDkala } = useDirectLoginDkala()
const handleDirectLogin = () => {
if (!subscriptionId) {
toast(t('errors.subscription_not_found'), 'error')
return
}
directLoginDkala(subscriptionId, {
onSuccess: (data) => {
const serviceLink = getSubscription?.data?.data?.userSubscription?.plan?.service?.link
if (serviceLink && data?.data?.tokens?.accessToken?.token && data?.data?.tokens?.refreshToken?.token) {
window.location.href = `${serviceLink}?token=${data.data.tokens.accessToken.token}&refreshToken=${data.data.tokens.refreshToken.token}`
} else {
toast(t('dkala.service_info_error'), 'error')
}
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0] || t('dkala.direct_login_error'), 'error')
}
})
}
const formik = useFormik<DkalaCreateShopType>({
initialValues: {
slug: '',
userSubscriptionId: subscriptionId || ''
},
enableReinitialize: true,
validationSchema: Yup.object({
slug: Yup.string()
.required(t('errors.required'))
.matches(/^[a-zA-Z0-9-]+$/, t('dkala.slug_validation')),
userSubscriptionId: Yup.string().required(t('errors.required'))
}),
onSubmit: (values) => {
createDkalaShop.mutate(values, {
onSuccess: () => {
toast(t('success'), 'success')
formik.resetForm()
handleDirectLogin()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0] || t('dkala.create_shop_error'), 'error')
}
})
}
})
return (
<>
<Helmet>
<title>{t('dkala.manage')}</title>
</Helmet>
<div className='mt-4'>
<div className='text-lg font-semibold mb-2'>
{t('dkala.manage')}
</div>
<div className='bg-white rounded-3xl xl:p-6 p-4 xl:pb-0 pb-0 mt-8 text-sm'>
<div className='mt-4'>
<div className='mb-6'>
<div className='text-base font-medium mb-2'>
{t('dkala.shop_info')}
</div>
<div className='text-description text-xs'>
{t('dkala.enter_shop_info')}
</div>
</div>
<div className='flex lg:max-w-[500px] mx-auto flex-col gap-6'>
<div className='w-full'>
<label className='text-sm font-medium mb-2 block'>
{t('dkala.slug')}
</label>
<div className='w-full relative mt-1'>
<div className='flex items-center border border-border rounded-xl overflow-hidden bg-white hover:border-gray-400 transition-colors focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20' dir='ltr'>
<div className='px-4 py-2.5 bg-gray-50 text-xs text-gray-600 border-r border-border whitespace-nowrap font-medium'>
dkala.danakcorp.com/
</div>
<input
type='text'
value={formik.values.slug}
onChange={(e) => {
formik.setFieldValue('slug', e.target.value)
}}
onBlur={formik.handleBlur}
placeholder={t('dkala.enter_slug')}
className='flex-1 h-10 text-black block px-4 text-xs border-0 focus:outline-none focus:ring-0 bg-transparent'
dir='ltr'
/>
</div>
{formik.touched.slug && formik.errors.slug && (
<Error errorText={formik.errors.slug} />
)}
</div>
</div>
<div className='mt-4 flex justify-center'>
<Button
className='xl:w-fit px-16'
onClick={() => formik.handleSubmit()}
isLoading={createDkalaShop.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle
size={24}
color='white'
/>
<div>{t('save')}</div>
</div>
</Button>
</div>
<div className='mt-10'>
<img src={GuideSlugImage} className='w-[300px] mx-auto' />
</div>
</div>
</div>
</div>
</div>
</>
)
}
+4 -4
View File
@@ -11,7 +11,7 @@ import Button from '../../components/Button'
import Error from '../../components/Error' import Error from '../../components/Error'
import { TickCircle } from 'iconsax-react' import { TickCircle } from 'iconsax-react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import GuideSlugImage from '../../assets/images/guide-slug.png' import GuideSlugImage from '../../assets/images/dmenu.png'
export const ManageRestaurant: FC = () => { export const ManageRestaurant: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
@@ -77,7 +77,7 @@ export const ManageRestaurant: FC = () => {
{t('restaurant.manage')} {t('restaurant.manage')}
</div> </div>
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'> <div className='bg-white rounded-3xl xl:p-6 p-4 xl:pb-0 pb-0 mt-8 text-sm'>
<div className='mt-4'> <div className='mt-4'>
<div className='mb-6'> <div className='mb-6'>
<div className='text-base font-medium mb-2'> <div className='text-base font-medium mb-2'>
@@ -116,9 +116,9 @@ export const ManageRestaurant: FC = () => {
</div> </div>
</div> </div>
<div className='mt-4 flex justify-end'> <div className='mt-4 flex justify-center'>
<Button <Button
className='xl:w-fit px-7' className='xl:w-fit px-16'
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
isLoading={createDmenuRestaurant.isPending} isLoading={createDmenuRestaurant.isPending}
> >
+48 -1
View File
@@ -1,6 +1,10 @@
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, CreateReviewType } from "../types/ServiecTypes"; import {
BuyServiceType,
CreateReviewType,
DkalaCreateShopType,
} from "../types/ServiecTypes";
export const useGetSuggestedServices = () => { export const useGetSuggestedServices = () => {
return useQuery({ return useQuery({
@@ -23,6 +27,14 @@ export const useGetServicesByCategory = (categoryId: string) => {
}); });
}; };
export const useGetServiceSearch = (search: string) => {
return useQuery({
queryKey: ["services-search", search],
queryFn: () => api.getServiceSearch(search),
enabled: !!search,
});
};
export const useGetDetailService = (serviceId: string) => { export const useGetDetailService = (serviceId: string) => {
return useQuery({ return useQuery({
queryKey: ["service", serviceId], queryKey: ["service", serviceId],
@@ -71,12 +83,40 @@ export const useGetDmenuSubscription = (subscriptionId: string) => {
}); });
}; };
export const useGetDkalaSubscription = (subscriptionId: string) => {
return useQuery({
queryKey: ["dkala-subscription", subscriptionId],
queryFn: () => api.getDkalaSubscription(subscriptionId),
enabled: !!subscriptionId,
});
};
export const useCreateDkalaShop = () => {
return useMutation({
mutationFn: (variables: DkalaCreateShopType) =>
api.createDkalaShop(variables),
});
};
export const useDirectLoginDkala = () => {
return useMutation({
mutationFn: api.directLoginDkala,
mutationKey: ["direct-login-dkala"],
});
};
export const useCreateDmenuRestaurant = () => { export const useCreateDmenuRestaurant = () => {
return useMutation({ return useMutation({
mutationFn: api.createDmenuRestaurant, mutationFn: api.createDmenuRestaurant,
}); });
}; };
export const useCreateDkalaRestaurant = () => {
return useMutation({
mutationFn: api.createDkalaShop,
});
};
export const useGetSubscription = (subscriptionId: string) => { export const useGetSubscription = (subscriptionId: string) => {
return useQuery({ return useQuery({
queryKey: ["subscription", subscriptionId], queryKey: ["subscription", subscriptionId],
@@ -91,3 +131,10 @@ export const useDirectLogin = () => {
mutationKey: ["direct-login"], mutationKey: ["direct-login"],
}); });
}; };
export const useDirectLoginDpage = () => {
return useMutation({
mutationFn: api.directLoginDpage,
mutationKey: ["direct-login-dpage"],
});
};
+49 -9
View File
@@ -5,6 +5,7 @@ import {
ExtentSubscriptionType, ExtentSubscriptionType,
ExtentSubscriptionResponseType, ExtentSubscriptionResponseType,
DmenuCreateRestaurantType, DmenuCreateRestaurantType,
DkalaCreateShopType,
GetSubscriptionResponseType, GetSubscriptionResponseType,
DirectLoginResponseType, DirectLoginResponseType,
} from "../types/ServiecTypes"; } from "../types/ServiecTypes";
@@ -26,6 +27,15 @@ export const getServicesByCategory = async (categoryId: string) => {
return data; return data;
}; };
export const getServiceSearch = async (search: string) => {
let query = ``;
if (search) {
query = `?q=${search}`;
}
const { data } = await axios.get(`/danak-services/search${query}`);
return data;
};
export const getDetailService = async (serviceId: string) => { export const getDetailService = async (serviceId: string) => {
const { data } = await axios.get(`/danak-services/${serviceId}`); const { data } = await axios.get(`/danak-services/${serviceId}`);
return data; return data;
@@ -34,7 +44,7 @@ export const getDetailService = async (serviceId: string) => {
export const buyService = async (params: BuyServiceType) => { export const buyService = async (params: BuyServiceType) => {
const { data } = await axios.post( const { data } = await axios.post(
`/subscriptions/${params.serviceId}/subscribe`, `/subscriptions/${params.serviceId}/subscribe`,
params params,
); );
return data; return data;
}; };
@@ -46,7 +56,7 @@ export const getMyServices = async (search: string) => {
export const createReview = async (params: CreateReviewType) => { export const createReview = async (params: CreateReviewType) => {
const { data } = await axios.post( const { data } = await axios.post(
`/danak-services/${params.id}/reviews`, `/danak-services/${params.id}/reviews`,
params params,
); );
return data; return data;
}; };
@@ -57,41 +67,71 @@ export const getServiceBySlug = async (slug: string) => {
}; };
export const extentSubscription = async ( export const extentSubscription = async (
params: ExtentSubscriptionType params: ExtentSubscriptionType,
): Promise<ExtentSubscriptionResponseType> => { ): Promise<ExtentSubscriptionResponseType> => {
const { data } = await axios.put( const { data } = await axios.put(
`/subscriptions/${params.subscriptionId}/subscribe`, `/subscriptions/${params.subscriptionId}/subscribe`,
params params,
); );
return data; return data;
}; };
export const getDmenuSubscription = async (subscriptionId: string) => { export const getDmenuSubscription = async (subscriptionId: string) => {
const { data } = await axios.get( const { data } = await axios.get(
`/dmenu/restaurants/subscription/${subscriptionId}` `/dmenu/restaurants/subscription/${subscriptionId}`,
);
return data;
};
export const getDkalaSubscription = async (subscriptionId: string) => {
const { data } = await axios.get(
`/dkala/shops/subscription/${subscriptionId}`,
); );
return data; return data;
}; };
export const createDmenuRestaurant = async ( export const createDmenuRestaurant = async (
params: DmenuCreateRestaurantType params: DmenuCreateRestaurantType,
) => { ) => {
const { data } = await axios.post(`/dmenu/restaurants`, params); const { data } = await axios.post(`/dmenu/restaurants`, params);
return data; return data;
}; };
export const createDkalaShop = async (params: DkalaCreateShopType) => {
const { data } = await axios.post(`/dkala/shops`, params);
return data;
};
export const directLoginDkala = async (
userSubscriptionId: string,
): Promise<DirectLoginResponseType> => {
const { data } = await axios.post(
`/dkala/auth/direct-login/${userSubscriptionId}`,
);
return data;
};
export const getSubscription = async ( export const getSubscription = async (
subscriptionId: string subscriptionId: string,
): Promise<GetSubscriptionResponseType> => { ): Promise<GetSubscriptionResponseType> => {
const { data } = await axios.get(`/subscriptions/user/${subscriptionId}`); const { data } = await axios.get(`/subscriptions/user/${subscriptionId}`);
return data; return data;
}; };
export const directLogin = async ( export const directLogin = async (
userSubscriptionId: string userSubscriptionId: string,
): Promise<DirectLoginResponseType> => { ): Promise<DirectLoginResponseType> => {
const { data } = await axios.post( const { data } = await axios.post(
`/dmenu/auth/direct-login/${userSubscriptionId}` `/dmenu/auth/direct-login/${userSubscriptionId}`,
); );
return data; return data;
}; };
export const directLoginDpage = async (
subscriptionId: string,
): Promise<DirectLoginResponseType> => {
const { data } = await axios.post(`/dpage/login`, {
userSubscriptionId: subscriptionId,
});
return data;
};
+5
View File
@@ -283,6 +283,11 @@ export type DmenuCreateRestaurantType = {
userSubscriptionId: string; userSubscriptionId: string;
}; };
export type DkalaCreateShopType = {
slug: string;
userSubscriptionId: string;
};
export type ServiceImageType = { export type ServiceImageType = {
id: string; id: string;
createdAt: string; createdAt: string;
+2
View File
@@ -2,6 +2,7 @@ import { FC } from 'react'
import { Route, Routes } from 'react-router-dom' import { Route, Routes } from 'react-router-dom'
import { Pages } from '../config/Pages' import { Pages } from '../config/Pages'
import Login from '../pages/auth/Login' import Login from '../pages/auth/Login'
import Forgot from '../pages/auth/Forgot'
// import Register from '../pages/auth/Register' // import Register from '../pages/auth/Register'
const AuthRouter: FC = () => { const AuthRouter: FC = () => {
@@ -9,6 +10,7 @@ const AuthRouter: FC = () => {
<Routes> <Routes>
<Route path={Pages.auth.login} element={<Login />} /> <Route path={Pages.auth.login} element={<Login />} />
<Route path={Pages.auth.register} element={<Login />} /> <Route path={Pages.auth.register} element={<Login />} />
<Route path={Pages.auth.forgotPassword} element={<Forgot />} />
</Routes> </Routes>
) )
} }
+2
View File
@@ -33,6 +33,7 @@ import Support from '../pages/support/Support'
import SupportInfo from '../pages/support/SupportInfo' import SupportInfo from '../pages/support/SupportInfo'
import Invoice from '../pages/receipts/Invoice' import Invoice from '../pages/receipts/Invoice'
import { ManageRestaurant } from '../pages/service/ManageRestaurant' import { ManageRestaurant } from '../pages/service/ManageRestaurant'
import { ManageDkala } from '../pages/service/ManageDkala'
const MainRouter: FC = () => { const MainRouter: FC = () => {
return ( return (
@@ -47,6 +48,7 @@ const MainRouter: FC = () => {
<Route path={Pages.support.index} element={<Support />} /> <Route path={Pages.support.index} element={<Support />} />
<Route path={Pages.support.info} element={<SupportInfo />} /> <Route path={Pages.support.info} element={<SupportInfo />} />
<Route path={Pages.services.manageRestaurant + ':subscriptionId'} element={<ManageRestaurant />} /> <Route path={Pages.services.manageRestaurant + ':subscriptionId'} element={<ManageRestaurant />} />
<Route path={Pages.services.manageDkala + ':subscriptionId'} element={<ManageDkala />} />
<Route path={Pages.services.mine} element={<MyServices />} /> <Route path={Pages.services.mine} element={<MyServices />} />
<Route path={Pages.services.other} element={<OtherServices />} /> <Route path={Pages.services.other} element={<OtherServices />} />
<Route path={Pages.services.detail + ':id'} element={<DetailService />} /> <Route path={Pages.services.detail + ':id'} element={<DetailService />} />
+2 -7
View File
@@ -1,5 +1,4 @@
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import Input from '../components/Input'
import { ArrowDown2, Card, CloseCircle, Element3, HambergerMenu, Logout, ProfileCircle, Receipt1, Setting2, TicketDiscount, Wallet } from 'iconsax-react' import { ArrowDown2, Card, CloseCircle, Element3, HambergerMenu, Logout, ProfileCircle, Receipt1, Setting2, TicketDiscount, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/avatar_image.png' import AvatarImage from '../assets/images/avatar_image.png'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -12,6 +11,7 @@ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import SideBarItem from './SideBarItem' import SideBarItem from './SideBarItem'
import { useGetWalletBalance } from '../pages/wallet/hooks/useWalletData' import { useGetWalletBalance } from '../pages/wallet/hooks/useWalletData'
import { NumberFormat } from '../config/func' import { NumberFormat } from '../config/func'
import Search from './components/Search'
const Header: FC = () => { const Header: FC = () => {
@@ -29,12 +29,7 @@ const Header: FC = () => {
return ( return (
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'> <div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
<div className='min-w-[270px] hidden xl:block'> <Search />
<Input
variant='search'
placeholder={t('header.search')}
/>
</div>
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'> <div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
<HambergerMenu size={24} color='black' /> <HambergerMenu size={24} color='black' />
</div> </div>
+73
View File
@@ -0,0 +1,73 @@
import { useEffect, useState, type FC } from 'react'
import Input from '../../components/Input'
import { useTranslation } from 'react-i18next'
import { useGetServiceSearch } from '../../pages/service/hooks/useServiceData'
import { ServiceType } from '../../pages/service/types/ServiecTypes'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useOutsideClick } from '../../hooks/useOutSideClick'
import { clx } from '../../helpers/utils'
import MoonLoader from 'react-spinners/MoonLoader'
const Search: FC = () => {
const { t } = useTranslation('global')
const [search, setSearch] = useState<string>('')
const [showSearch, setShowSearch] = useState<boolean>(false)
const { data, isPending } = useGetServiceSearch(search)
const ref = useOutsideClick(() => {
setShowSearch(false)
})
useEffect(() => {
if (search.length) {
setShowSearch(true)
} else {
setShowSearch(false)
}
}, [data])
return (
<div className='min-w-[270px] relative hidden xl:block'>
<Input
variant='search'
placeholder={t('header.search')}
onChangeSearchFinal={setSearch}
/>
{
showSearch &&
<div ref={ref} className='absolute shadow bg-white top-[100%] rounded-xl right-0 w-[270px] p-4 flex flex-col gap-4 overflow-y-auto max-h-screen'>
{
isPending ?
<div className='flex justify-center'>
<MoonLoader color='black' size={20} />
</div>
:
data?.data?.services?.length === 0 ?
<div className='flex justify-center text-sm text-gray-400'>
نتیجه ای یافت نشد.
</div>
:
data?.data?.services?.map((item: ServiceType, index: number) => {
return (
<Link onClick={() => setShowSearch(false)} to={Pages.services.detail + item.slug} key={item.id} className={clx(
'flex gap-3 items-center border-b pb-4',
data?.data?.services?.length === index + 1 && 'border-b-0 pb-0'
)}>
<img src={item.icon} className='size-8 rounded-lg' />
<div className='text-[13px]'>{item.name}</div>
</Link>
)
})
}
</div>
}
</div>
)
}
export default Search