move to Donetick Org, First commit frontend
This commit is contained in:
commit
2657469964
105 changed files with 21572 additions and 0 deletions
45
src/views/Authorization/AuthorizationContainer.jsx
Normal file
45
src/views/Authorization/AuthorizationContainer.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
// import Logo from 'Components/Logo'
|
||||
import { Box, Paper } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
const Container = styled('div')(({ theme }) => ({
|
||||
minHeight: '100vh',
|
||||
padding: '24px',
|
||||
display: 'grid',
|
||||
placeItems: 'start center',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
// center children
|
||||
placeItems: 'center',
|
||||
},
|
||||
}))
|
||||
|
||||
const AuthCard = styled(Paper)(({ theme }) => ({
|
||||
// border: "1px solid #c4c4c4",
|
||||
padding: 24,
|
||||
paddingTop: 32,
|
||||
borderRadius: 24,
|
||||
width: '100%',
|
||||
maxWidth: '400px',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
maxWidth: 'unset',
|
||||
},
|
||||
}))
|
||||
|
||||
export default function AuthCardContainer({ children, ...props }) {
|
||||
return (
|
||||
<Container>
|
||||
<AuthCard elevation={0}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
{/* <Logo size='96px' /> */}
|
||||
</Box>
|
||||
{children}
|
||||
</AuthCard>
|
||||
</Container>
|
||||
)
|
||||
}
|
227
src/views/Authorization/ForgotPasswordView.jsx
Normal file
227
src/views/Authorization/ForgotPasswordView.jsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
// create boilerplate for ResetPasswordView:
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Sheet,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { API_URL } from './../../Config'
|
||||
|
||||
const ForgotPasswordView = () => {
|
||||
const navigate = useNavigate()
|
||||
// const [showLoginSnackbar, setShowLoginSnackbar] = useState(false)
|
||||
// const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [resetStatusOk, setResetStatusOk] = useState(null)
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailError, setEmailError] = useState(null)
|
||||
|
||||
const validateEmail = email => {
|
||||
return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!email) {
|
||||
return setEmailError('Email is required')
|
||||
}
|
||||
|
||||
// validate email:
|
||||
if (validateEmail(email)) {
|
||||
setEmailError('Please enter a valid email address')
|
||||
return
|
||||
}
|
||||
|
||||
if (emailError) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email: email }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setResetStatusOk(true)
|
||||
// wait 3 seconds and then redirect to login:
|
||||
} else {
|
||||
setResetStatusOk(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setResetStatusOk(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailChange = e => {
|
||||
setEmail(e.target.value)
|
||||
if (validateEmail(e.target.value)) {
|
||||
setEmailError('Please enter a valid email address')
|
||||
} else {
|
||||
setEmailError(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
component='main'
|
||||
maxWidth='xs'
|
||||
|
||||
// make content center in the middle of the page:
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Sheet
|
||||
component='form'
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 2,
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'md',
|
||||
minHeight: '70vh',
|
||||
justifyContent: 'space-between',
|
||||
justifyItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<img
|
||||
src='/src/assets/logo.svg'
|
||||
alt='logo'
|
||||
width='128px'
|
||||
height='128px'
|
||||
/>
|
||||
{/* <Logo /> */}
|
||||
<Typography level='h2'>
|
||||
Done
|
||||
<span
|
||||
style={{
|
||||
color: '#06b6d4',
|
||||
}}
|
||||
>
|
||||
tick
|
||||
</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* HERE */}
|
||||
<Box sx={{ textAlign: 'center' }}></Box>
|
||||
{resetStatusOk === null && (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='grid gap-6'>
|
||||
<Typography level='body2' gutterBottom>
|
||||
Enter your email, and we'll send you a link to get into your
|
||||
account.
|
||||
</Typography>
|
||||
<FormControl error={emailError !== null}>
|
||||
<Input
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
variant='soft'
|
||||
fullWidth
|
||||
size='lg'
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
error={emailError !== null}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>{emailError}</FormHelperText>
|
||||
</FormControl>
|
||||
<Box>
|
||||
<Button
|
||||
variant='solid'
|
||||
size='lg'
|
||||
fullWidth
|
||||
sx={{
|
||||
mb: 1,
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='soft'
|
||||
sx={{
|
||||
width: '100%',
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate('/login')
|
||||
}}
|
||||
color='neutral'
|
||||
>
|
||||
Back to Login
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{resetStatusOk != null && (
|
||||
<>
|
||||
<Box mt={-30}>
|
||||
<Typography level='body-md'>
|
||||
if there is an account associated with the email you entered,
|
||||
you will receive an email with instructions on how to reset
|
||||
your
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant='soft'
|
||||
size='lg'
|
||||
sx={{ position: 'relative', bottom: '0' }}
|
||||
onClick={() => {
|
||||
navigate('/login')
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Snackbar
|
||||
open={resetStatusOk ? resetStatusOk : resetStatusOk === false}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => {
|
||||
if (resetStatusOk) {
|
||||
navigate('/login')
|
||||
}
|
||||
}}
|
||||
>
|
||||
{resetStatusOk
|
||||
? 'Reset email sent, check your email'
|
||||
: 'Reset email failed, try again later'}
|
||||
</Snackbar>
|
||||
</Sheet>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordView
|
345
src/views/Authorization/LoginView.jsx
Normal file
345
src/views/Authorization/LoginView.jsx
Normal file
|
@ -0,0 +1,345 @@
|
|||
import GoogleIcon from '@mui/icons-material/Google'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Input,
|
||||
Sheet,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import Cookies from 'js-cookie'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { LoginSocialGoogle } from 'reactjs-social-login'
|
||||
import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import Logo from '../../Logo'
|
||||
import { GetUserProfile } from '../../utils/Fetcher'
|
||||
const LoginView = () => {
|
||||
const { userProfile, setUserProfile } = React.useContext(UserContext)
|
||||
const [username, setUsername] = React.useState('')
|
||||
const [password, setPassword] = React.useState('')
|
||||
const [error, setError] = React.useState(null)
|
||||
const Navigate = useNavigate()
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
fetch(`${API_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json().then(data => {
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
// console.log('redirectUrl', redirectUrl)
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
})
|
||||
} else if (response.status === 401) {
|
||||
setError('Wrong username or password')
|
||||
} else {
|
||||
setError('An error occurred, please try again')
|
||||
console.log('Login failed')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
setError('Unable to communicate with server, please try again')
|
||||
console.log('Login failed', err)
|
||||
})
|
||||
}
|
||||
|
||||
const loggedWithProvider = function (provider, data) {
|
||||
console.log(provider, data)
|
||||
return fetch(API_URL + `/auth/${provider}/callback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: provider,
|
||||
token:
|
||||
data['access_token'] || // data["access_token"] is for Google
|
||||
data['accessToken'], // data["accessToken"] is for Facebook
|
||||
data: data,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json().then(data => {
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
// setIsLoggedIn(true);
|
||||
getUserProfileAndNavigateToHome()
|
||||
})
|
||||
}
|
||||
return response.json().then(error => {
|
||||
setError("Couldn't log in with Google, please try again")
|
||||
})
|
||||
})
|
||||
}
|
||||
const getUserProfileAndNavigateToHome = () => {
|
||||
GetUserProfile().then(data => {
|
||||
data.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
// check if redirect url is set in cookie:
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl) {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/my/chores')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleForgotPassword = () => {
|
||||
Navigate('/forgot-password')
|
||||
}
|
||||
return (
|
||||
<Container
|
||||
component='main'
|
||||
maxWidth='xs'
|
||||
|
||||
// make content center in the middle of the page:
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Sheet
|
||||
component='form'
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 2,
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
{/* <img
|
||||
src='/src/assets/logo.svg'
|
||||
alt='logo'
|
||||
width='128px'
|
||||
height='128px'
|
||||
/> */}
|
||||
<Logo />
|
||||
|
||||
<Typography level='h2'>
|
||||
Done
|
||||
<span
|
||||
style={{
|
||||
color: '#06b6d4',
|
||||
}}
|
||||
>
|
||||
tick
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
{userProfile && (
|
||||
<>
|
||||
<Avatar
|
||||
src={userProfile?.image}
|
||||
alt={userProfile?.username}
|
||||
size='lg'
|
||||
sx={{
|
||||
mt: 2,
|
||||
width: '96px',
|
||||
height: '96px',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography level='body-md' alignSelf={'center'}>
|
||||
Welcome back,{' '}
|
||||
{userProfile?.displayName || userProfile?.username}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
onClick={() => {
|
||||
getUserProfileAndNavigateToHome()
|
||||
}}
|
||||
>
|
||||
Continue as {userProfile.displayName || userProfile.username}
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
q
|
||||
variant='plain'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
setUserProfile(null)
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
// go to login page:
|
||||
window.location.href = '/login'
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!userProfile && (
|
||||
<>
|
||||
<Typography level='body2'>
|
||||
Sign in to your account to continue
|
||||
</Typography>
|
||||
<Typography level='body2' alignSelf={'start'} mt={4}>
|
||||
Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='email'
|
||||
label='Email Address'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={e => {
|
||||
setUsername(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
Password:
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='password'
|
||||
label='Password'
|
||||
type='password'
|
||||
id='password'
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPassword(e.target.value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='solid'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mt: 3,
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
q
|
||||
variant='plain'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={handleForgotPassword}
|
||||
>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Divider> or </Divider>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<LoginSocialGoogle
|
||||
client_id={GOOGLE_CLIENT_ID}
|
||||
redirect_uri={REDIRECT_URL}
|
||||
scope='openid profile email'
|
||||
discoveryDocs='claims_supported'
|
||||
access_type='online'
|
||||
isOnlyGetToken={true}
|
||||
onResolve={({ provider, data }) => {
|
||||
loggedWithProvider(provider, data)
|
||||
}}
|
||||
onReject={err => {
|
||||
setError("Couldn't log in with Google, please try again")
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='lg'
|
||||
fullWidth
|
||||
sx={{
|
||||
width: '100%',
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<GoogleIcon />
|
||||
Continue with Google
|
||||
</div>
|
||||
</Button>
|
||||
</LoginSocialGoogle>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
Navigate('/signup')
|
||||
}}
|
||||
fullWidth
|
||||
variant='soft'
|
||||
size='lg'
|
||||
// sx={{ mt: 3, mb: 2 }}
|
||||
>
|
||||
Create new account
|
||||
</Button>
|
||||
</Sheet>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={error !== null}
|
||||
onClose={() => setError(null)}
|
||||
autoHideDuration={3000}
|
||||
message={error}
|
||||
>
|
||||
{error}
|
||||
</Snackbar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginView
|
243
src/views/Authorization/Signup.jsx
Normal file
243
src/views/Authorization/Signup.jsx
Normal file
|
@ -0,0 +1,243 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Sheet,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Logo from '../../Logo'
|
||||
import { login, signUp } from '../../utils/Fetcher'
|
||||
|
||||
const SignupView = () => {
|
||||
const [username, setUsername] = React.useState('')
|
||||
const [password, setPassword] = React.useState('')
|
||||
const Navigate = useNavigate()
|
||||
const [displayName, setDisplayName] = React.useState('')
|
||||
const [email, setEmail] = React.useState('')
|
||||
const [usernameError, setUsernameError] = React.useState('')
|
||||
const [passwordError, setPasswordError] = React.useState('')
|
||||
const [emailError, setEmailError] = React.useState('')
|
||||
const [displayNameError, setDisplayNameError] = React.useState('')
|
||||
const [error, setError] = React.useState(null)
|
||||
const handleLogin = (username, password) => {
|
||||
login(username, password).then(response => {
|
||||
if (response.status === 200) {
|
||||
response.json().then(res => {
|
||||
localStorage.setItem('ca_token', res.token)
|
||||
localStorage.setItem('ca_expiration', res.expire)
|
||||
setTimeout(() => {
|
||||
// TODO: not sure if there is a race condition here
|
||||
// but on first sign up it renavigates to login.
|
||||
Navigate('/my/chores')
|
||||
}, 500)
|
||||
})
|
||||
} else {
|
||||
console.log('Login failed', response)
|
||||
// Navigate('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleSignUpValidation = () => {
|
||||
// Reset errors before validation
|
||||
setUsernameError(null)
|
||||
setPasswordError(null)
|
||||
setDisplayNameError(null)
|
||||
setEmailError(null)
|
||||
|
||||
let isValid = true
|
||||
|
||||
if (!username.trim()) {
|
||||
setUsernameError('Username is required')
|
||||
isValid = false
|
||||
}
|
||||
if (username.length < 4) {
|
||||
setUsernameError('Username must be at least 4 characters')
|
||||
isValid = false
|
||||
}
|
||||
// if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
// setEmailError('Invalid email address')
|
||||
// isValid = false
|
||||
// }
|
||||
|
||||
if (password.length < 8) {
|
||||
setPasswordError('Password must be at least 8 characters')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!displayName.trim()) {
|
||||
setDisplayNameError('Display name is required')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// display name should only contain letters and spaces and numbers:
|
||||
if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) {
|
||||
setDisplayNameError('Display name can only contain letters and numbers')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// username should only contain letters , numbers , dot and dash:
|
||||
if (!/^[a-zA-Z0-9.-]+$/.test(username)) {
|
||||
setUsernameError(
|
||||
'Username can only contain letters, numbers, dot and dash',
|
||||
)
|
||||
isValid = false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
if (!handleSignUpValidation()) {
|
||||
return
|
||||
}
|
||||
signUp(username, password, displayName, email).then(response => {
|
||||
if (response.status === 201) {
|
||||
handleLogin(username, password)
|
||||
} else {
|
||||
console.log('Signup failed')
|
||||
setError('Signup failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container component='main' maxWidth='xs'>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Sheet
|
||||
component='form'
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
padding: 2,
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<Typography level='h2'>
|
||||
Done
|
||||
<span
|
||||
style={{
|
||||
color: '#06b6d4',
|
||||
}}
|
||||
>
|
||||
tick
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography level='body2'>
|
||||
Create an account to get started!
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='body2' alignSelf={'start'} mt={4}>
|
||||
Username
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
id='email'
|
||||
label='Email Address'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={e => {
|
||||
setUsernameError(null)
|
||||
setUsername(e.target.value.trim())
|
||||
}}
|
||||
/>
|
||||
<FormControl error={usernameError}>
|
||||
<FormHelperText c>{usernameError}</FormHelperText>
|
||||
</FormControl>
|
||||
{/* Error message display */}
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
Password:
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='password'
|
||||
label='Password'
|
||||
type='password'
|
||||
id='password'
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPasswordError(null)
|
||||
setPassword(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<FormControl error={passwordError}>
|
||||
<FormHelperText>{passwordError}</FormHelperText>
|
||||
</FormControl>
|
||||
<Typography level='body2' alignSelf={'start'}>
|
||||
Display Name:
|
||||
</Typography>
|
||||
<Input
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
name='displayName'
|
||||
label='Display Name'
|
||||
id='displayName'
|
||||
value={displayName}
|
||||
onChange={e => {
|
||||
setDisplayNameError(null)
|
||||
setDisplayName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<FormControl error={displayNameError}>
|
||||
<FormHelperText>{displayNameError}</FormHelperText>
|
||||
</FormControl>
|
||||
<Button
|
||||
// type='submit'
|
||||
size='lg'
|
||||
fullWidth
|
||||
variant='solid'
|
||||
sx={{ mt: 3, mb: 1 }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
<Divider> or </Divider>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
Navigate('/login')
|
||||
}}
|
||||
fullWidth
|
||||
variant='soft'
|
||||
// sx={{ mt: 3, mb: 2 }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Sheet>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupView
|
194
src/views/Authorization/UpdatePasswordView.jsx
Normal file
194
src/views/Authorization/UpdatePasswordView.jsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
// create boilerplate for ResetPasswordView:
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Sheet,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { API_URL } from '../../Config'
|
||||
import Logo from '../../Logo'
|
||||
|
||||
const UpdatePasswordView = () => {
|
||||
const navigate = useNavigate()
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('')
|
||||
const [passwordError, setPasswordError] = useState(null)
|
||||
const [passworConfirmationError, setPasswordConfirmationError] =
|
||||
useState(null)
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const [updateStatusOk, setUpdateStatusOk] = useState(null)
|
||||
|
||||
const verifiticationCode = searchParams.get('c')
|
||||
|
||||
const handlePasswordChange = e => {
|
||||
const password = e.target.value
|
||||
setPassword(password)
|
||||
if (password.length < 8) {
|
||||
setPasswordError('Password must be at least 8 characters')
|
||||
} else {
|
||||
setPasswordError(null)
|
||||
}
|
||||
}
|
||||
const handlePasswordConfirmChange = e => {
|
||||
setPasswordConfirm(e.target.value)
|
||||
if (e.target.value !== password) {
|
||||
setPasswordConfirmationError('Passwords do not match')
|
||||
} else {
|
||||
setPasswordConfirmationError(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (passwordError != null || passworConfirmationError != null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_URL}/auth/password?c=${verifiticationCode}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ password: password }),
|
||||
},
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
setUpdateStatusOk(true)
|
||||
// wait 3 seconds and then redirect to login:
|
||||
setTimeout(() => {
|
||||
navigate('/login')
|
||||
}, 3000)
|
||||
} else {
|
||||
setUpdateStatusOk(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setUpdateStatusOk(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Container component='main' maxWidth='xs'>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Sheet
|
||||
component='form'
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
padding: 2,
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<Typography level='h2'>
|
||||
Done
|
||||
<span
|
||||
style={{
|
||||
color: '#06b6d4',
|
||||
}}
|
||||
>
|
||||
tick
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography level='body2' mb={4}>
|
||||
Please enter your new password below
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<FormControl error>
|
||||
<Input
|
||||
placeholder='Password'
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
error={passwordError !== null}
|
||||
// onKeyDown={e => {
|
||||
// if (e.key === 'Enter' && validateForm(validateFormInput)) {
|
||||
// handleSubmit(e)
|
||||
// }
|
||||
// }}
|
||||
/>
|
||||
<FormHelperText>{passwordError}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl error>
|
||||
<Input
|
||||
placeholder='Confirm Password'
|
||||
type='password'
|
||||
value={passwordConfirm}
|
||||
onChange={handlePasswordConfirmChange}
|
||||
error={passworConfirmationError !== null}
|
||||
// onKeyDown={e => {
|
||||
// if (e.key === 'Enter' && validateForm(validateFormInput)) {
|
||||
// handleSubmit(e)
|
||||
// }
|
||||
// }}
|
||||
/>
|
||||
<FormHelperText>{passworConfirmationError}</FormHelperText>
|
||||
</FormControl>
|
||||
{/* helper to show password not matching : */}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
sx={{
|
||||
mt: 5,
|
||||
mb: 1,
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save Password
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='soft'
|
||||
onClick={() => {
|
||||
navigate('/login')
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Sheet>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={updateStatusOk !== true}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => {
|
||||
setUpdateStatusOk(null)
|
||||
}}
|
||||
>
|
||||
Password update failed, try again later
|
||||
</Snackbar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdatePasswordView
|
Loading…
Add table
Add a link
Reference in a new issue