UPDATE AUTH
This commit is contained in:
+207
-266
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user