move to Donetick Org, First commit frontend

This commit is contained in:
Mo Tarbin 2024-06-30 18:55:39 -04:00
commit 2657469964
105 changed files with 21572 additions and 0 deletions

View 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>
)
}

View 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

View 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

View 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

View 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