2025-08-05 12:35:40 +07:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
// React Imports
|
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
|
|
|
|
|
// Next Imports
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
|
|
|
|
|
|
|
|
|
// MUI Imports
|
2025-09-26 12:47:43 +07:00
|
|
|
import Card from '@mui/material/Card'
|
|
|
|
|
import CardContent from '@mui/material/CardContent'
|
2025-08-05 12:35:40 +07:00
|
|
|
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'
|
2025-09-26 12:47:43 +07:00
|
|
|
import { CircularProgress } from '@mui/material'
|
2025-08-05 12:35:40 +07:00
|
|
|
|
|
|
|
|
// Third-party Imports
|
|
|
|
|
import { Controller, useForm } from 'react-hook-form'
|
|
|
|
|
import { valibotResolver } from '@hookform/resolvers/valibot'
|
|
|
|
|
import { email, object, minLength, string, pipe, nonEmpty } from 'valibot'
|
|
|
|
|
import type { SubmitHandler } from 'react-hook-form'
|
|
|
|
|
import type { InferInput } from 'valibot'
|
2025-09-26 12:47:43 +07:00
|
|
|
import { toast } from 'react-toastify'
|
2025-08-05 12:35:40 +07:00
|
|
|
|
|
|
|
|
// Type Imports
|
2025-09-26 12:47:43 +07:00
|
|
|
import type { Locale } from '@configs/i18n'
|
2025-08-05 12:35:40 +07:00
|
|
|
|
|
|
|
|
// Component Imports
|
|
|
|
|
import Logo from '@components/layout/shared/Logo'
|
|
|
|
|
import CustomTextField from '@core/components/mui/TextField'
|
|
|
|
|
|
|
|
|
|
// Config Imports
|
|
|
|
|
import themeConfig from '@configs/themeConfig'
|
|
|
|
|
|
|
|
|
|
// Util Imports
|
|
|
|
|
import { getLocalizedUrl } from '@/utils/i18n'
|
|
|
|
|
import { useAuthMutation } from '../services/mutations/auth'
|
|
|
|
|
|
2025-09-26 12:47:43 +07:00
|
|
|
// Styled Component Imports
|
|
|
|
|
import AuthIllustrationWrapper from './AuthIllustrationWrapper'
|
2025-08-05 12:35:40 +07:00
|
|
|
|
|
|
|
|
type ErrorType = {
|
|
|
|
|
message: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FormData = InferInput<typeof schema>
|
|
|
|
|
|
|
|
|
|
const schema = object({
|
|
|
|
|
email: pipe(string(), minLength(1, 'This field is required'), email('Email is invalid')),
|
|
|
|
|
password: pipe(
|
|
|
|
|
string(),
|
|
|
|
|
nonEmpty('This field is required'),
|
|
|
|
|
minLength(5, 'Password must be at least 5 characters long')
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2025-09-26 12:47:43 +07:00
|
|
|
const Login = () => {
|
2025-08-05 12:35:40 +07:00
|
|
|
// States
|
|
|
|
|
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
|
|
|
|
const [errorState, setErrorState] = useState<ErrorType | null>(null)
|
|
|
|
|
|
2025-08-06 14:41:23 +07:00
|
|
|
const { login } = useAuthMutation()
|
2025-08-05 12:35:40 +07:00
|
|
|
|
|
|
|
|
// Hooks
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
const { lang: locale } = useParams()
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
control,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
formState: { errors }
|
|
|
|
|
} = useForm<FormData>({
|
|
|
|
|
resolver: valibotResolver(schema),
|
|
|
|
|
defaultValues: {
|
2025-08-14 00:29:19 +07:00
|
|
|
email: '',
|
|
|
|
|
password: ''
|
2025-08-05 12:35:40 +07:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleClickShowPassword = () => setIsPasswordShown(show => !show)
|
|
|
|
|
|
|
|
|
|
const onSubmit: SubmitHandler<FormData> = async (data: FormData) => {
|
2025-08-13 23:53:03 +07:00
|
|
|
login.mutate(data, {
|
|
|
|
|
onSuccess: (data: any) => {
|
|
|
|
|
if (data?.user?.role === 'admin') {
|
|
|
|
|
const redirectURL = searchParams.get('redirectTo') ?? '/dashboards/overview'
|
|
|
|
|
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
|
|
|
|
} else {
|
|
|
|
|
const redirectURL = searchParams.get('redirectTo') ?? '/sa/organizations/list'
|
|
|
|
|
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (error: any) => {
|
2025-08-14 00:29:19 +07:00
|
|
|
toast.error(error.response.data.message)
|
2025-08-13 23:53:03 +07:00
|
|
|
}
|
|
|
|
|
})
|
2025-08-05 12:35:40 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-26 12:47:43 +07:00
|
|
|
<AuthIllustrationWrapper>
|
|
|
|
|
<Card className='flex flex-col sm:is-[450px]'>
|
|
|
|
|
<CardContent className='sm:!p-12'>
|
|
|
|
|
<Link href={getLocalizedUrl('/', locale as Locale)} className='flex justify-center mbe-6'>
|
|
|
|
|
<Logo />
|
|
|
|
|
</Link>
|
|
|
|
|
<div className='flex flex-col gap-1 mbe-6'>
|
2025-08-05 12:35:40 +07:00
|
|
|
<Typography variant='h4'>{`Welcome to ${themeConfig.templateName}! 👋🏻`}</Typography>
|
|
|
|
|
<Typography>Please sign-in to your account and start the adventure</Typography>
|
|
|
|
|
</div>
|
2025-09-26 12:47:43 +07:00
|
|
|
<form noValidate autoComplete='off' onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-6'>
|
2025-08-05 12:35:40 +07:00
|
|
|
<Controller
|
|
|
|
|
name='email'
|
|
|
|
|
control={control}
|
|
|
|
|
rules={{ required: true }}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<CustomTextField
|
|
|
|
|
{...field}
|
|
|
|
|
autoFocus
|
|
|
|
|
fullWidth
|
|
|
|
|
type='email'
|
|
|
|
|
label='Email'
|
|
|
|
|
placeholder='Enter your email'
|
|
|
|
|
onChange={e => {
|
|
|
|
|
field.onChange(e.target.value)
|
|
|
|
|
errorState !== null && setErrorState(null)
|
|
|
|
|
}}
|
|
|
|
|
{...((errors.email || errorState !== null) && {
|
|
|
|
|
error: true,
|
|
|
|
|
helperText: errors?.email?.message || errorState?.message[0]
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name='password'
|
|
|
|
|
control={control}
|
|
|
|
|
rules={{ required: true }}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<CustomTextField
|
|
|
|
|
{...field}
|
|
|
|
|
fullWidth
|
|
|
|
|
label='Password'
|
|
|
|
|
placeholder='············'
|
2025-09-26 12:47:43 +07:00
|
|
|
id='outlined-adornment-password'
|
2025-08-05 12:35:40 +07:00
|
|
|
type={isPasswordShown ? 'text' : 'password'}
|
|
|
|
|
onChange={e => {
|
|
|
|
|
field.onChange(e.target.value)
|
|
|
|
|
errorState !== null && setErrorState(null)
|
|
|
|
|
}}
|
|
|
|
|
slotProps={{
|
|
|
|
|
input: {
|
|
|
|
|
endAdornment: (
|
|
|
|
|
<InputAdornment position='end'>
|
|
|
|
|
<IconButton
|
|
|
|
|
edge='end'
|
|
|
|
|
onClick={handleClickShowPassword}
|
|
|
|
|
onMouseDown={e => e.preventDefault()}
|
|
|
|
|
>
|
|
|
|
|
<i className={isPasswordShown ? 'tabler-eye' : 'tabler-eye-off'} />
|
|
|
|
|
</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 defaultChecked />} label='Remember me' />
|
|
|
|
|
<Typography
|
|
|
|
|
className='text-end'
|
|
|
|
|
color='primary.main'
|
|
|
|
|
component={Link}
|
|
|
|
|
href={getLocalizedUrl('/forgot-password', locale as Locale)}
|
|
|
|
|
>
|
|
|
|
|
Forgot password?
|
|
|
|
|
</Typography>
|
|
|
|
|
</div>
|
2025-08-07 23:48:31 +07:00
|
|
|
<Button fullWidth variant='contained' type='submit' disabled={login.isPending}>
|
2025-08-09 22:38:12 +07:00
|
|
|
{login.isPending ? <CircularProgress size={16} /> : 'Login'}
|
2025-08-05 12:35:40 +07:00
|
|
|
</Button>
|
|
|
|
|
<div className='flex justify-center items-center flex-wrap gap-2'>
|
|
|
|
|
<Typography>New on our platform?</Typography>
|
2025-08-13 23:53:03 +07:00
|
|
|
<Typography
|
|
|
|
|
component={Link}
|
|
|
|
|
href={getLocalizedUrl('/organization', locale as Locale)}
|
|
|
|
|
color='primary.main'
|
|
|
|
|
>
|
2025-08-05 12:35:40 +07:00
|
|
|
Create an account
|
|
|
|
|
</Typography>
|
|
|
|
|
</div>
|
2025-09-26 12:47:43 +07:00
|
|
|
<Divider className='gap-2 text-textPrimary'>or</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>
|
2025-08-05 12:35:40 +07:00
|
|
|
</form>
|
2025-09-26 12:47:43 +07:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AuthIllustrationWrapper>
|
2025-08-05 12:35:40 +07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Login
|