Commit a59dcc9b authored by Filip Šenk's avatar Filip Šenk
Browse files

Functional /login and /register in frontend

parent 23dd6e70
Loading
Loading
Loading
Loading
+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,
@@ -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;
@@ -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)
@@ -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()
@@ -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 {
+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`
@@ -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>
)
+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 />
}
+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>
  )
}
+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