UPDATE AUTH

This commit is contained in:
2026-03-24 13:53:21 +03:30
parent 52e6a90319
commit 0f2b67ea4d
13 changed files with 1361 additions and 721 deletions
+207 -266
View File
@@ -1,311 +1,252 @@
'use client'
"use client";
// React Imports
import { useMemo, useState } from 'react'
import { useMemo, useState } from "react";
// Next Imports
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
// MUI Imports
import useMediaQuery from '@mui/material/useMediaQuery'
import { styled, useTheme } from '@mui/material/styles'
import Typography from '@mui/material/Typography'
import Button from '@mui/material/Button'
import Alert from '@mui/material/Alert'
import CircularProgress from '@mui/material/CircularProgress'
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import Checkbox from "@mui/material/Checkbox";
import Button from "@mui/material/Button";
import FormControlLabel from "@mui/material/FormControlLabel";
import Alert from "@mui/material/Alert";
import CircularProgress from "@mui/material/CircularProgress";
// Third-party Imports
import { Controller, useForm } from 'react-hook-form'
import { valibotResolver } from '@hookform/resolvers/valibot'
import { object, minLength, maxLength, string, pipe, nonEmpty, custom } from 'valibot'
import type { SubmitHandler } from 'react-hook-form'
import classnames from 'classnames'
import { OTPInput } from 'input-otp'
// Type Imports
import type { SystemMode } from '@core/types'
import { Controller, useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
import { object, minLength, string, pipe, nonEmpty } from "valibot";
import type { SubmitHandler } from "react-hook-form";
// Component Imports
import Logo from '@components/layout/shared/Logo'
import CustomTextField from '@core/components/mui/TextField'
import Logo from "@components/layout/shared/Logo";
import CustomTextField from "@core/components/mui/TextField";
import AuthIllustrationWrapper from "@views/pages/auth/AuthIllustrationWrapper";
// Context Imports
import { useAuth } from '@/contexts/authContext'
import { useAuth } from "@/contexts/authContext";
// Config Imports
import themeConfig from '@configs/themeConfig'
import themeConfig from "@configs/themeConfig";
// Hook Imports
import { useImageVariant } from '@core/hooks/useImageVariant'
import { useSettings } from '@core/hooks/useSettings'
type LoginFormData = {
identifier: string;
password: string;
};
// Styled Custom Components
const LoginIllustration = styled('img')(({ theme }) => ({
zIndex: 2,
blockSize: 'auto',
maxBlockSize: 680,
maxInlineSize: '100%',
margin: theme.spacing(12),
[theme.breakpoints.down(1536)]: {
maxBlockSize: 550
},
[theme.breakpoints.down('lg')]: {
maxBlockSize: 450
const getErrorMessage = (error: unknown, fallbackMessage: string) => {
if (error instanceof Error) return error.message;
if (typeof error === "object" && error !== null && "message" in error) {
return String(error.message);
}
}))
const MaskImg = styled('img')({
blockSize: 'auto',
maxBlockSize: 355,
inlineSize: '100%',
position: 'absolute',
insetBlockEnd: 0,
zIndex: -1
})
return fallbackMessage;
};
const OtpContainer = styled('div')({
display: 'flex',
justifyContent: 'center',
gap: '8px',
marginTop: '16px'
})
const Login = () => {
const t = useTranslations("login");
const router = useRouter();
const searchParams = useSearchParams();
const { login } = useAuth();
type ErrorType = {
message: string
}
const [isPasswordShown, setIsPasswordShown] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
type PhoneFormData = {
phone_number: string
}
const Login = ({ mode }: { mode: SystemMode }) => {
const t = useTranslations('login')
const phoneSchema = useMemo(
const loginSchema = useMemo(
() =>
object({
phone_number: pipe(
identifier: pipe(
string(),
nonEmpty(String(t('validation.phoneRequired'))),
minLength(10, String(t('validation.phoneMinLength'))),
maxLength(15, String(t('validation.phoneMaxLength'))),
custom((input) => /^[0-9]+$/.test(String(input)), String(t('validation.phoneDigitsOnly')))
)
nonEmpty(String(t("validation.identifierRequired"))),
),
password: pipe(
string(),
nonEmpty(String(t("validation.passwordRequired"))),
minLength(8, String(t("validation.passwordMinLength"))),
),
}),
[t]
)
// States
const [step, setStep] = useState<'phone' | 'otp'>('phone')
const [otp, setOtp] = useState('')
const [tempToken, setTempToken] = useState<string>('')
const [errorState, setErrorState] = useState<ErrorType | null>(null)
const [isLoading, setIsLoading] = useState(false)
// Vars
const darkImg = '/images/pages/auth-mask-dark.png'
const lightImg = '/images/pages/auth-mask-light.png'
const darkIllustration = '/images/illustrations/auth/v2-login-dark.png'
const lightIllustration = '/images/illustrations/auth/v2-login-light.png'
const borderedDarkIllustration = '/images/illustrations/auth/v2-login-dark-border.png'
const borderedLightIllustration = '/images/illustrations/auth/v2-login-light-border.png'
// Hooks
const router = useRouter()
const searchParams = useSearchParams()
const { settings } = useSettings()
const theme = useTheme()
const hidden = useMediaQuery(theme.breakpoints.down('md'))
const authBackground = useImageVariant(mode, lightImg, darkImg)
const { requestOTP, login } = useAuth()
[t],
);
const {
control,
handleSubmit,
getValues,
formState: { errors }
} = useForm<PhoneFormData>({
resolver: valibotResolver(phoneSchema) as any,
formState: { errors },
} = useForm<LoginFormData>({
resolver: valibotResolver(loginSchema) as never,
defaultValues: {
phone_number: ''
}
})
identifier: "",
password: "",
},
});
const characterIllustration = useImageVariant(
mode,
lightIllustration,
darkIllustration,
borderedLightIllustration,
borderedDarkIllustration
)
const handleClickShowPassword = () => setIsPasswordShown((show) => !show);
const onPhoneSubmit: SubmitHandler<PhoneFormData> = async (data: PhoneFormData) => {
setIsLoading(true)
setErrorState(null)
const onSubmit: SubmitHandler<LoginFormData> = async (data) => {
setIsSubmitting(true);
setErrorMessage(null);
try {
const token = await requestOTP(data.phone_number)
setTempToken(token)
setStep('otp')
} catch (error: any) {
setErrorState({ message: error.message || t('errors.sendOtpFailed') })
await login(data.identifier.trim(), data.password);
const redirectURL = searchParams.get("redirectTo") ?? "/";
router.replace(redirectURL);
} catch (error) {
setErrorMessage(getErrorMessage(error, String(t("errors.loginFailed"))));
} finally {
setIsLoading(false)
setIsSubmitting(false);
}
}
const onOtpSubmit = async (otpValue?: string) => {
const code = otpValue ?? otp
if (code.length !== 6) {
setErrorState({ message: t('errors.incompleteOtp') })
return
}
setIsLoading(true)
setErrorState(null)
try {
const phoneNumber = getValues('phone_number')
await login(phoneNumber, code, tempToken)
// Redirect on successful login
const redirectURL = searchParams.get('redirectTo') ?? '/'
router.replace(redirectURL)
} catch (error: any) {
setErrorState({ message: error.message || t('errors.otpVerificationFailed') })
} finally {
setIsLoading(false)
}
}
const handleBackToPhone = () => {
setStep('phone')
setOtp('')
setErrorState(null)
}
};
return (
<div className='flex bs-full justify-center'>
<div
className={classnames(
'flex bs-full items-center justify-center flex-1 min-bs-[100dvh] relative p-6 max-md:hidden',
{
'border-ie': settings.skin === 'bordered'
}
)}
>
<LoginIllustration src={characterIllustration} alt='character-illustration' />
{!hidden && <MaskImg alt='mask' src={authBackground} />}
</div>
<div className='flex justify-center items-center bs-full bg-backgroundPaper !min-is-full p-6 md:!min-is-[unset] md:p-12 md:is-[480px]'>
<div className='absolute block-start-5 sm:block-start-[33px] inline-start-6 sm:inline-start-[38px]'>
<Logo />
</div>
<div className='flex flex-col gap-6 is-full sm:is-auto md:is-full sm:max-is-[400px] md:max-is-[unset] mbs-8 sm:mbs-11 md:mbs-0'>
<div className='flex flex-col gap-1'>
<Typography variant='h4'>{t('welcome', { templateName: themeConfig.templateName })}</Typography>
<Typography>
{step === 'phone' ? t('phoneStep') : t('otpStep')}
<AuthIllustrationWrapper>
<Card className="flex flex-col sm:is-[450px]">
<CardContent className="sm:!p-12">
<Link href="/" className="flex justify-center mbe-6">
<Logo />
</Link>
<div className="flex flex-col gap-1 mbe-6">
<Typography variant="h4">
{t("welcome", { templateName: themeConfig.templateName })}
</Typography>
<Typography>{t("subtitle")}</Typography>
</div>
{errorState && (
<Alert severity='error' onClose={() => setErrorState(null)}>
{errorState.message}
{errorMessage && (
<Alert
severity="error"
onClose={() => setErrorMessage(null)}
className="mbe-6"
>
{errorMessage}
</Alert>
)}
{step === 'phone' ? (
<form
noValidate
autoComplete='off'
action={() => {}}
onSubmit={handleSubmit(onPhoneSubmit)}
className='flex flex-col gap-6'
>
<Controller
name='phone_number'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField
{...field}
autoFocus
fullWidth
type='tel'
label={t('phoneNumber')}
placeholder={t('placeholderPhone')}
onChange={e => {
field.onChange(e.target.value.replace(/\D/g, '')) // Only allow digits
errorState !== null && setErrorState(null)
}}
{...(errors.phone_number && {
error: true,
helperText: errors.phone_number.message
})}
/>
)}
/>
<Button fullWidth variant='contained' type='submit' disabled={isLoading}>
{isLoading ? <CircularProgress size={24} /> : t('sendOtp')}
</Button>
<div className='flex justify-center items-center flex-wrap gap-2'>
<Typography>{t('newUser')}</Typography>
<Typography component={Link} href='/register' color='primary.main'>
{t('createAccount')}
</Typography>
</div>
</form>
) : (
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-4'>
<Typography variant='body2' color='text.secondary'>
{t('otpSent', { phone: getValues('phone_number') })}
</Typography>
<OtpContainer>
<OTPInput
value={otp}
onChange={(value) => {
setOtp(value)
if (value.length === 6) onOtpSubmit(value)
}}
maxLength={6}
containerClassName='flex gap-2 ltr:flex-row rtl:flex-row-reverse'
render={({ slots }) => (
<>
{slots.map((slot, idx) => (
<div
key={idx}
className={classnames(
'flex items-center justify-center w-12 h-12 text-2xl font-semibold border rounded',
{
'border-primary': slot.isActive,
'border-defaultBorder': !slot.isActive && slot.char !== null,
'border-error': errorState !== null
}
)}
>
{slot.char}
</div>
))}
</>
)}
/>
</OtpContainer>
</div>
<Button fullWidth variant='contained' onClick={() => onOtpSubmit()} disabled={isLoading || otp.length !== 6}>
{isLoading ? <CircularProgress size={24} /> : t('verifyOtp')}
</Button>
<Button fullWidth variant='text' onClick={handleBackToPhone} disabled={isLoading}>
{t('backToPhone')}
</Button>
</div>
)}
</div>
</div>
</div>
)
}
<form
noValidate
autoComplete="off"
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-6"
>
<Controller
name="identifier"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
autoFocus
fullWidth
label={t("identifier")}
placeholder={t("placeholderIdentifier")}
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
{...(errors.identifier && {
error: true,
helperText: errors.identifier.message,
})}
/>
)}
/>
export default Login
<Controller
name="password"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label={t("password")}
placeholder="············"
type={isPasswordShown ? "text" : "password"}
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton
edge="end"
onClick={handleClickShowPassword}
onMouseDown={(event) => event.preventDefault()}
>
<i
className={
isPasswordShown
? "tabler-eye-off"
: "tabler-eye"
}
/>
</IconButton>
</InputAdornment>
),
},
}}
{...(errors.password && {
error: true,
helperText: errors.password.message,
})}
/>
)}
/>
<div className="flex justify-between items-center gap-x-3 gap-y-1 flex-wrap">
<FormControlLabel
control={<Checkbox />}
label={t("rememberMe")}
/>
<Typography
className="text-end"
color="primary.main"
component={Link}
href="/forgot-password"
>
{t("forgotPassword")}
</Typography>
</div>
<Button
fullWidth
variant="contained"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? (
<CircularProgress size={24} color="inherit" />
) : (
t("submit")
)}
</Button>
<div className="flex justify-center items-center flex-wrap gap-2">
<Typography>{t("newUser")}</Typography>
<Typography
component={Link}
href="/register"
color="primary.main"
>
{t("createAccount")}
</Typography>
</div>
</form>
</CardContent>
</Card>
</AuthIllustrationWrapper>
);
};
export default Login;
+312 -143
View File
@@ -1,173 +1,342 @@
'use client'
"use client";
// React Imports
import { useState } from 'react'
import { useMemo, useState } from "react";
// Next Imports
import Link from 'next/link'
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
// MUI Imports
import useMediaQuery from '@mui/material/useMediaQuery'
import { styled, useTheme } from '@mui/material/styles'
import Typography from '@mui/material/Typography'
import IconButton from '@mui/material/IconButton'
import InputAdornment from '@mui/material/InputAdornment'
import Checkbox from '@mui/material/Checkbox'
import Button from '@mui/material/Button'
import FormControlLabel from '@mui/material/FormControlLabel'
import Divider from '@mui/material/Divider'
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import Button from "@mui/material/Button";
import Alert from "@mui/material/Alert";
import CircularProgress from "@mui/material/CircularProgress";
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { SystemMode } from '@core/types'
import { Controller, useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
import {
object,
string,
pipe,
nonEmpty,
minLength,
maxLength,
email,
custom,
} from "valibot";
import type { SubmitHandler } from "react-hook-form";
// Component Imports
import Logo from '@components/layout/shared/Logo'
import CustomTextField from '@core/components/mui/TextField'
import Logo from "@components/layout/shared/Logo";
import CustomTextField from "@core/components/mui/TextField";
import AuthIllustrationWrapper from "@views/pages/auth/AuthIllustrationWrapper";
// Hook Imports
import { useImageVariant } from '@core/hooks/useImageVariant'
import { useSettings } from '@core/hooks/useSettings'
// Context Imports
import { useAuth } from "@/contexts/authContext";
// Styled Custom Components
const RegisterIllustration = styled('img')(({ theme }) => ({
zIndex: 2,
blockSize: 'auto',
maxBlockSize: 600,
maxInlineSize: '100%',
margin: theme.spacing(12),
[theme.breakpoints.down(1536)]: {
maxBlockSize: 550
},
[theme.breakpoints.down('lg')]: {
maxBlockSize: 450
type RegisterFormData = {
username: string;
email: string;
phone_number: string;
password: string;
first_name: string;
last_name: string;
};
const getErrorMessage = (error: unknown, fallbackMessage: string) => {
if (error instanceof Error) return error.message;
if (typeof error === "object" && error !== null && "message" in error) {
return String(error.message);
}
}))
const MaskImg = styled('img')({
blockSize: 'auto',
maxBlockSize: 345,
inlineSize: '100%',
position: 'absolute',
insetBlockEnd: 0,
zIndex: -1
})
return fallbackMessage;
};
const Register = ({ mode }: { mode: SystemMode }) => {
// States
const [isPasswordShown, setIsPasswordShown] = useState(false)
const Register = () => {
const router = useRouter();
const searchParams = useSearchParams();
const { register } = useAuth();
// Vars
const darkImg = '/images/pages/auth-mask-dark.png'
const lightImg = '/images/pages/auth-mask-light.png'
const darkIllustration = '/images/illustrations/auth/v2-register-dark.png'
const lightIllustration = '/images/illustrations/auth/v2-register-light.png'
const borderedDarkIllustration = '/images/illustrations/auth/v2-register-dark-border.png'
const borderedLightIllustration = '/images/illustrations/auth/v2-register-light-border.png'
const [isPasswordShown, setIsPasswordShown] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Hooks
const { settings } = useSettings()
const theme = useTheme()
const hidden = useMediaQuery(theme.breakpoints.down('md'))
const authBackground = useImageVariant(mode, lightImg, darkImg)
const registerSchema = useMemo(
() =>
object({
first_name: string(),
last_name: string(),
username: pipe(string(), nonEmpty("نام کاربری الزامی است")),
email: pipe(
string(),
nonEmpty("ایمیل الزامی است"),
email("ایمیل معتبر وارد کنید"),
),
phone_number: pipe(
string(),
nonEmpty("شماره موبایل الزامی است"),
minLength(10, "شماره موبایل باید حداقل ۱۰ رقم باشد"),
maxLength(15, "شماره موبایل باید حداکثر ۱۵ رقم باشد"),
custom(
(input) => /^[0-9]+$/.test(String(input)),
"شماره موبایل باید فقط شامل عدد باشد",
),
),
password: pipe(
string(),
nonEmpty("رمز عبور الزامی است"),
minLength(8, "رمز عبور باید حداقل ۸ کاراکتر باشد"),
),
}),
[],
);
const characterIllustration = useImageVariant(
mode,
lightIllustration,
darkIllustration,
borderedLightIllustration,
borderedDarkIllustration
)
const {
control,
handleSubmit,
formState: { errors },
} = useForm<RegisterFormData>({
resolver: valibotResolver(registerSchema) as never,
defaultValues: {
first_name: "",
last_name: "",
username: "",
email: "",
phone_number: "",
password: "",
},
});
const handleClickShowPassword = () => setIsPasswordShown(show => !show)
const handleClickShowPassword = () => setIsPasswordShown((show) => !show);
const onSubmit: SubmitHandler<RegisterFormData> = async (data) => {
setIsSubmitting(true);
setErrorMessage(null);
try {
await register({
first_name: data.first_name.trim(),
last_name: data.last_name.trim(),
username: data.username.trim(),
email: data.email.trim(),
phone_number: data.phone_number.trim(),
password: data.password,
});
const redirectURL = searchParams.get("redirectTo") ?? "/";
router.replace(redirectURL);
} catch (error) {
setErrorMessage(getErrorMessage(error, "ثبت نام ناموفق بود"));
} finally {
setIsSubmitting(false);
}
};
return (
<div className='flex bs-full justify-center'>
<div
className={classnames(
'flex bs-full items-center justify-center flex-1 min-bs-[100dvh] relative p-6 max-md:hidden',
{
'border-ie': settings.skin === 'bordered'
}
)}
>
<RegisterIllustration src={characterIllustration} alt='character-illustration' />
{!hidden && <MaskImg alt='mask' src={authBackground} />}
</div>
<div className='flex justify-center items-center bs-full bg-backgroundPaper !min-is-full p-6 md:!min-is-[unset] md:p-12 md:is-[480px]'>
<Link
href='/login'
className='absolute block-start-5 sm:block-start-[33px] inline-start-6 sm:inline-start-[38px]'
>
<Logo />
</Link>
<div className='flex flex-col gap-6 is-full sm:is-auto md:is-full sm:max-is-[400px] md:max-is-[unset] mbs-8 sm:mbs-11 md:mbs-0'>
<div className='flex flex-col gap-1'>
<Typography variant='h4'>ماجراجویی از اینجا شروع میشود 🚀</Typography>
<Typography>مدیریت اپلیکیشن خود را آسان و لذتبخش کنید!</Typography>
<AuthIllustrationWrapper>
<Card className="flex flex-col sm:is-[480px]">
<CardContent className="sm:!p-12">
<Link href="/" className="flex justify-center mbe-6">
<Logo />
</Link>
<div className="flex flex-col gap-1 mbe-6">
<Typography variant="h4">ایجاد حساب کاربری</Typography>
<Typography>برای شروع، اطلاعات حساب خود را وارد کنید.</Typography>
</div>
<form noValidate autoComplete='off' onSubmit={e => e.preventDefault()} className='flex flex-col gap-6'>
<CustomTextField autoFocus fullWidth label='نام کاربری' placeholder='نام کاربری خود را وارد کنید' />
<CustomTextField fullWidth label='ایمیل' placeholder='ایمیل خود را وارد کنید' />
<CustomTextField
{errorMessage && (
<Alert
severity="error"
onClose={() => setErrorMessage(null)}
className="mbe-6"
>
{errorMessage}
</Alert>
)}
<form
noValidate
autoComplete="off"
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-6"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Controller
name="first_name"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
autoFocus
fullWidth
label="نام"
placeholder="نام خود را وارد کنید"
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
/>
)}
/>
<Controller
name="last_name"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label="نام خانوادگی"
placeholder="نام خانوادگی خود را وارد کنید"
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
/>
)}
/>
</div>
<Controller
name="username"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label="نام کاربری"
placeholder="نام کاربری خود را وارد کنید"
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
{...(errors.username && {
error: true,
helperText: errors.username.message,
})}
/>
)}
/>
<Controller
name="email"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
type="email"
label="ایمیل"
placeholder="ایمیل خود را وارد کنید"
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
{...(errors.email && {
error: true,
helperText: errors.email.message,
})}
/>
)}
/>
<Controller
name="phone_number"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
type="tel"
label="شماره موبایل"
placeholder="شماره موبایل خود را وارد کنید"
onChange={(event) => {
field.onChange(event.target.value.replace(/\D/g, ""));
if (errorMessage) setErrorMessage(null);
}}
{...(errors.phone_number && {
error: true,
helperText: errors.phone_number.message,
})}
/>
)}
/>
<Controller
name="password"
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label="رمز عبور"
placeholder="············"
type={isPasswordShown ? "text" : "password"}
onChange={(event) => {
field.onChange(event.target.value);
if (errorMessage) setErrorMessage(null);
}}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton
edge="end"
onClick={handleClickShowPassword}
onMouseDown={(event) => event.preventDefault()}
>
<i
className={
isPasswordShown
? "tabler-eye-off"
: "tabler-eye"
}
/>
</IconButton>
</InputAdornment>
),
},
}}
{...(errors.password && {
error: true,
helperText: errors.password.message,
})}
/>
)}
/>
<Button
fullWidth
label='رمز عبور'
placeholder='············'
type={isPasswordShown ? 'text' : 'password'}
slotProps={{
input: {
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={handleClickShowPassword} onMouseDown={e => e.preventDefault()}>
<i className={isPasswordShown ? 'tabler-eye-off' : 'tabler-eye'} />
</IconButton>
</InputAdornment>
)
}
}}
/>
<FormControlLabel
control={<Checkbox />}
label={
<>
<span>موافقم با </span>
<Link className='text-primary' href='/' onClick={e => e.preventDefault()}>
حریم خصوصی و شرایط استفاده
</Link>
</>
}
/>
<Button fullWidth variant='contained' type='submit'>
ثبت نام
variant="contained"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? (
<CircularProgress size={24} color="inherit" />
) : (
"ثبت نام"
)}
</Button>
<div className='flex justify-center items-center flex-wrap gap-2'>
<div className="flex justify-center items-center flex-wrap gap-2">
<Typography>قبلاً حساب کاربری دارید؟</Typography>
<Typography component={Link} href='/login' color='primary.main'>
<Typography component={Link} href="/login" color="primary.main">
وارد شوید
</Typography>
</div>
<Divider className='gap-2'>یا</Divider>
<div className='flex justify-center items-center gap-1.5'>
<IconButton className='text-facebook' size='small'>
<i className='tabler-brand-facebook-filled' />
</IconButton>
<IconButton className='text-twitter' size='small'>
<i className='tabler-brand-twitter-filled' />
</IconButton>
<IconButton className='text-textPrimary' size='small'>
<i className='tabler-brand-github-filled' />
</IconButton>
<IconButton className='text-error' size='small'>
<i className='tabler-brand-google-filled' />
</IconButton>
</div>
</form>
</div>
</div>
</div>
)
}
</CardContent>
</Card>
</AuthIllustrationWrapper>
);
};
export default Register
export default Register;