Loading shared/components/LoginDialog/LoginComp.tsx→frontend/src/components/Login/LoginComp.tsx +9 −8 Original line number Diff line number Diff line import { Button, Colors, FormGroup, InputGroup } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, ErrorMessage, Loading @@ -8,9 +9,10 @@ import { useWiggle, wiggleClass, } from '@inject/shared' import type { FC, FormEvent } from 'react' import { useNavigate } from '@tanstack/react-router' import type { FormEvent } from 'react' import { useCallback, useRef, useState } from 'react' import { useHost } from './variables' import { RegisterRoute } from '../../routes/register/route' const fields = css` display: flex; Loading @@ -32,14 +34,12 @@ const loginForm = css` width: 100%; ` type LoginProps = { toRegister: () => void } export const Login: FC<LoginProps> = ({ toRegister }) => { export const Login = () => { const host = useHost() const { beginWiggling, wiggling } = useWiggle() const navigate = useNavigate() const username = useRef<HTMLInputElement>(null) const password = useRef<HTMLInputElement>(null) const [showPassword, setShowPassword] = useState(false) Loading Loading @@ -90,6 +90,7 @@ export const Login: FC<LoginProps> = ({ toRegister }) => { throw res as unknown as { status: string; detail: string } } setSessionId(res.sessionid) navigate({ to: '/' }) } catch (err) { const { detail } = err as { status: string; detail: string } beginWiggling() Loading Loading @@ -151,7 +152,7 @@ export const Login: FC<LoginProps> = ({ toRegister }) => { <p> Do not have an account?{' '} <span onClick={toRegister} onClick={() => navigate({ to: RegisterRoute.to })} className={css` color: ${Colors.BLUE4}; :hover { Loading shared/components/LoginDialog/index.tsx→frontend/src/components/Login/LoginDialog.tsx +2 −7 Original line number Diff line number Diff line import { css } from '@emotion/css' import { Container } from '@inject/shared' import InjectLogo from '@inject/shared/svg/inject-logo--vertical-black.svg?react' import type { FC } from 'react' import { Login } from './LoginComp' const page = css` Loading @@ -15,13 +14,9 @@ const logo = css` height: 20rem; ` type LoginDialogProps = { toRegister: () => void } export const LoginDialog: FC<LoginDialogProps> = ({ toRegister }) => ( export const LoginDialog = () => ( <Container className={page}> <InjectLogo className={logo} /> <Login toRegister={toRegister} /> <Login /> </Container> ) frontend/src/components/Login/index.tsx 0 → 100644 +21 −0 Original line number Diff line number Diff line import { useSessionId } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' import { useEffect } from 'react' import { LoginDialog } from './LoginDialog' export const LoginPage = () => { const sessionId = useSessionId() const navigate = useNavigate() useEffect(() => { if (sessionId) { navigate({ to: '/' }) return } }, [navigate, sessionId]) if (sessionId) { return null } return <LoginDialog /> } frontend/src/components/Register/RegisterComp.tsx 0 → 100644 +265 −0 Original line number Diff line number Diff line import { Button, Colors, FormGroup, InputGroup } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, ErrorMessage, registerUrl, useWiggle, wiggleClass, } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' import type { FormEvent } from 'react' import { useCallback, useRef, useState } from 'react' import { LoginRoute } from '../../routes/login/route' const fields = css` display: flex; flex-direction: column; gap: 1rem; ` const label = css` flex: 1; margin-bottom: 0 !important; ` const loginForm = css` display: flex; flex-direction: column; gap: 0.5rem; margin: 0 auto; max-width: 35rem; padding: 1rem; width: 100%; ` export const Register = () => { const host = useHost() const { beginWiggling, wiggling } = useWiggle() const navigate = useNavigate() const username = useRef<HTMLInputElement>(null) const password = useRef<HTMLInputElement>(null) const passwordAgain = useRef<HTMLInputElement>(null) const firstName = useRef<HTMLInputElement>(null) const lastName = useRef<HTMLInputElement>(null) const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState({ username: false, firstName: false, lastName: false, password: false, }) const [submitError, setSubmitError] = useState('') const [passwordAgainError, setPasswordAgainError] = useState('') const [loading, setLoading] = useState(false) const register = useCallback(async () => { if (!host) { return } setError({ firstName: false, lastName: false, password: false, username: false, }) setPasswordAgainError('') setSubmitError('') if (!username.current?.value) { setError(prev => ({ ...prev, username: true })) return } if (!firstName.current?.value) { setError(prev => ({ ...prev, firstName: true })) return } if (!lastName.current?.value) { setError(prev => ({ ...prev, lastName: true })) return } if (!password.current?.value) { setError(prev => ({ ...prev, password: true })) return } if (!passwordAgain.current?.value) { setPasswordAgainError('Enter a password') return } if (passwordAgain.current.value !== password.current.value) { setPasswordAgainError('Passwords must match') return } try { setLoading(true) const req = await authenticatedFetch(registerUrl(host), { body: JSON.stringify({ username: username.current.value, password: password.current.value, first_name: firstName.current.value, last_name: lastName.current.value, }), method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }) const result: { status: string; detail: string } = await req.json() if (result.status === 'error') { beginWiggling() setSubmitError(result.detail) return } navigate({ to: LoginRoute.to }) } catch (err) { const { detail } = err as { status: string; detail: string } beginWiggling() setSubmitError(detail) } finally { setLoading(false) } }, [host, navigate, beginWiggling]) const handleSubmit = useCallback( (e: FormEvent<HTMLFormElement>) => { e.preventDefault() register() }, [register] ) return ( <div> <form onSubmit={handleSubmit} className={loginForm}> <div className={fields}> <FormGroup label='Username' labelInfo='(Email)' className={label} intent={error.username ? 'danger' : 'none'} helperText={error.username ? 'Enter an email' : ''} > <InputGroup id='username' inputRef={username} placeholder='email@inject.ex' autoComplete='email' intent={error.username ? 'danger' : 'none'} /> </FormGroup> <div className={css` display: flex; align-items: start; justify-content: space-between; column-gap: 1rem; `} > <FormGroup label='First name' className={label} intent={error.firstName ? 'danger' : 'none'} helperText={error.firstName ? 'Enter a first name' : ''} > <InputGroup intent={error.firstName ? 'danger' : 'none'} id='firstname' inputRef={firstName} placeholder='John' /> </FormGroup> <FormGroup label='Last name' className={label} intent={error.lastName ? 'danger' : 'none'} helperText={error.lastName ? 'Enter a last name' : ''} > <InputGroup intent={error.lastName ? 'danger' : 'none'} id='lastname' inputRef={lastName} placeholder='Doe' /> </FormGroup> </div> <FormGroup intent={error.password ? 'danger' : 'none'} helperText={error.password ? 'Enter a password' : ''} label='Password' className={label} > <InputGroup intent={error.password ? 'danger' : 'none'} id='password' inputRef={password} type={showPassword ? 'text' : 'password'} autoComplete='current-password' placeholder='password' rightElement={ <Button minimal icon={showPassword ? 'eye-off' : 'eye-open'} onClick={() => setShowPassword(prev => !prev)} /> } /> </FormGroup> <FormGroup intent={passwordAgainError ? 'danger' : 'none'} helperText={passwordAgainError} label='Repeat password' className={label} > <InputGroup intent={passwordAgainError ? 'danger' : 'none'} id='passwordAgain' inputRef={passwordAgain} type={showPassword ? 'text' : 'password'} autoComplete='current-password' placeholder='password again' /> </FormGroup> <p> Already have an account?{' '} <span onClick={() => navigate({ to: LoginRoute.to })} className={css` color: ${Colors.BLUE4}; :hover { cursor: pointer; text-decoration: underline; } `} > Login </span> </p> </div> <Button icon='new-person' type='submit' loading={loading} intent='primary' className={cx({ [wiggleClass]: wiggling })} > Register </Button> {submitError && <ErrorMessage minimal>{submitError}</ErrorMessage>} </form> </div> ) } frontend/src/components/Register/RegisterDialog.tsx 0 → 100644 +22 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import { Container } from '@inject/shared' import InjectLogo from '@inject/shared/svg/inject-logo--vertical-black.svg?react' import { Register } from './RegisterComp' const page = css` height: 100%; display: flex; flex-direction: column; ` const logo = css` width: 100%; height: 20rem; ` export const RegisterDialog = () => ( <Container className={page}> <InjectLogo className={logo} /> <Register /> </Container> ) Loading
shared/components/LoginDialog/LoginComp.tsx→frontend/src/components/Login/LoginComp.tsx +9 −8 Original line number Diff line number Diff line import { Button, Colors, FormGroup, InputGroup } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, ErrorMessage, Loading @@ -8,9 +9,10 @@ import { useWiggle, wiggleClass, } from '@inject/shared' import type { FC, FormEvent } from 'react' import { useNavigate } from '@tanstack/react-router' import type { FormEvent } from 'react' import { useCallback, useRef, useState } from 'react' import { useHost } from './variables' import { RegisterRoute } from '../../routes/register/route' const fields = css` display: flex; Loading @@ -32,14 +34,12 @@ const loginForm = css` width: 100%; ` type LoginProps = { toRegister: () => void } export const Login: FC<LoginProps> = ({ toRegister }) => { export const Login = () => { const host = useHost() const { beginWiggling, wiggling } = useWiggle() const navigate = useNavigate() const username = useRef<HTMLInputElement>(null) const password = useRef<HTMLInputElement>(null) const [showPassword, setShowPassword] = useState(false) Loading Loading @@ -90,6 +90,7 @@ export const Login: FC<LoginProps> = ({ toRegister }) => { throw res as unknown as { status: string; detail: string } } setSessionId(res.sessionid) navigate({ to: '/' }) } catch (err) { const { detail } = err as { status: string; detail: string } beginWiggling() Loading Loading @@ -151,7 +152,7 @@ export const Login: FC<LoginProps> = ({ toRegister }) => { <p> Do not have an account?{' '} <span onClick={toRegister} onClick={() => navigate({ to: RegisterRoute.to })} className={css` color: ${Colors.BLUE4}; :hover { Loading
shared/components/LoginDialog/index.tsx→frontend/src/components/Login/LoginDialog.tsx +2 −7 Original line number Diff line number Diff line import { css } from '@emotion/css' import { Container } from '@inject/shared' import InjectLogo from '@inject/shared/svg/inject-logo--vertical-black.svg?react' import type { FC } from 'react' import { Login } from './LoginComp' const page = css` Loading @@ -15,13 +14,9 @@ const logo = css` height: 20rem; ` type LoginDialogProps = { toRegister: () => void } export const LoginDialog: FC<LoginDialogProps> = ({ toRegister }) => ( export const LoginDialog = () => ( <Container className={page}> <InjectLogo className={logo} /> <Login toRegister={toRegister} /> <Login /> </Container> )
frontend/src/components/Login/index.tsx 0 → 100644 +21 −0 Original line number Diff line number Diff line import { useSessionId } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' import { useEffect } from 'react' import { LoginDialog } from './LoginDialog' export const LoginPage = () => { const sessionId = useSessionId() const navigate = useNavigate() useEffect(() => { if (sessionId) { navigate({ to: '/' }) return } }, [navigate, sessionId]) if (sessionId) { return null } return <LoginDialog /> }
frontend/src/components/Register/RegisterComp.tsx 0 → 100644 +265 −0 Original line number Diff line number Diff line import { Button, Colors, FormGroup, InputGroup } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useHost } from '@inject/graphql' import { authenticatedFetch, ErrorMessage, registerUrl, useWiggle, wiggleClass, } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' import type { FormEvent } from 'react' import { useCallback, useRef, useState } from 'react' import { LoginRoute } from '../../routes/login/route' const fields = css` display: flex; flex-direction: column; gap: 1rem; ` const label = css` flex: 1; margin-bottom: 0 !important; ` const loginForm = css` display: flex; flex-direction: column; gap: 0.5rem; margin: 0 auto; max-width: 35rem; padding: 1rem; width: 100%; ` export const Register = () => { const host = useHost() const { beginWiggling, wiggling } = useWiggle() const navigate = useNavigate() const username = useRef<HTMLInputElement>(null) const password = useRef<HTMLInputElement>(null) const passwordAgain = useRef<HTMLInputElement>(null) const firstName = useRef<HTMLInputElement>(null) const lastName = useRef<HTMLInputElement>(null) const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState({ username: false, firstName: false, lastName: false, password: false, }) const [submitError, setSubmitError] = useState('') const [passwordAgainError, setPasswordAgainError] = useState('') const [loading, setLoading] = useState(false) const register = useCallback(async () => { if (!host) { return } setError({ firstName: false, lastName: false, password: false, username: false, }) setPasswordAgainError('') setSubmitError('') if (!username.current?.value) { setError(prev => ({ ...prev, username: true })) return } if (!firstName.current?.value) { setError(prev => ({ ...prev, firstName: true })) return } if (!lastName.current?.value) { setError(prev => ({ ...prev, lastName: true })) return } if (!password.current?.value) { setError(prev => ({ ...prev, password: true })) return } if (!passwordAgain.current?.value) { setPasswordAgainError('Enter a password') return } if (passwordAgain.current.value !== password.current.value) { setPasswordAgainError('Passwords must match') return } try { setLoading(true) const req = await authenticatedFetch(registerUrl(host), { body: JSON.stringify({ username: username.current.value, password: password.current.value, first_name: firstName.current.value, last_name: lastName.current.value, }), method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }) const result: { status: string; detail: string } = await req.json() if (result.status === 'error') { beginWiggling() setSubmitError(result.detail) return } navigate({ to: LoginRoute.to }) } catch (err) { const { detail } = err as { status: string; detail: string } beginWiggling() setSubmitError(detail) } finally { setLoading(false) } }, [host, navigate, beginWiggling]) const handleSubmit = useCallback( (e: FormEvent<HTMLFormElement>) => { e.preventDefault() register() }, [register] ) return ( <div> <form onSubmit={handleSubmit} className={loginForm}> <div className={fields}> <FormGroup label='Username' labelInfo='(Email)' className={label} intent={error.username ? 'danger' : 'none'} helperText={error.username ? 'Enter an email' : ''} > <InputGroup id='username' inputRef={username} placeholder='email@inject.ex' autoComplete='email' intent={error.username ? 'danger' : 'none'} /> </FormGroup> <div className={css` display: flex; align-items: start; justify-content: space-between; column-gap: 1rem; `} > <FormGroup label='First name' className={label} intent={error.firstName ? 'danger' : 'none'} helperText={error.firstName ? 'Enter a first name' : ''} > <InputGroup intent={error.firstName ? 'danger' : 'none'} id='firstname' inputRef={firstName} placeholder='John' /> </FormGroup> <FormGroup label='Last name' className={label} intent={error.lastName ? 'danger' : 'none'} helperText={error.lastName ? 'Enter a last name' : ''} > <InputGroup intent={error.lastName ? 'danger' : 'none'} id='lastname' inputRef={lastName} placeholder='Doe' /> </FormGroup> </div> <FormGroup intent={error.password ? 'danger' : 'none'} helperText={error.password ? 'Enter a password' : ''} label='Password' className={label} > <InputGroup intent={error.password ? 'danger' : 'none'} id='password' inputRef={password} type={showPassword ? 'text' : 'password'} autoComplete='current-password' placeholder='password' rightElement={ <Button minimal icon={showPassword ? 'eye-off' : 'eye-open'} onClick={() => setShowPassword(prev => !prev)} /> } /> </FormGroup> <FormGroup intent={passwordAgainError ? 'danger' : 'none'} helperText={passwordAgainError} label='Repeat password' className={label} > <InputGroup intent={passwordAgainError ? 'danger' : 'none'} id='passwordAgain' inputRef={passwordAgain} type={showPassword ? 'text' : 'password'} autoComplete='current-password' placeholder='password again' /> </FormGroup> <p> Already have an account?{' '} <span onClick={() => navigate({ to: LoginRoute.to })} className={css` color: ${Colors.BLUE4}; :hover { cursor: pointer; text-decoration: underline; } `} > Login </span> </p> </div> <Button icon='new-person' type='submit' loading={loading} intent='primary' className={cx({ [wiggleClass]: wiggling })} > Register </Button> {submitError && <ErrorMessage minimal>{submitError}</ErrorMessage>} </form> </div> ) }
frontend/src/components/Register/RegisterDialog.tsx 0 → 100644 +22 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import { Container } from '@inject/shared' import InjectLogo from '@inject/shared/svg/inject-logo--vertical-black.svg?react' import { Register } from './RegisterComp' const page = css` height: 100%; display: flex; flex-direction: column; ` const logo = css` width: 100%; height: 20rem; ` export const RegisterDialog = () => ( <Container className={page}> <InjectLogo className={logo} /> <Register /> </Container> )