refactor: usage of useForm hook from react-component-form
				
					
				
			This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import { PlusIcon, MenuIcon, UsersIcon, XIcon } from '@heroicons/react/solid'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import { useMediaQuery } from 'react-responsive'
 | 
			
		||||
import { useSwipeable } from 'react-swipeable'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { Form, useForm } from 'react-component-form'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { useGuildMember } from '../../../contexts/GuildMember'
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
@@ -17,6 +17,11 @@ import {
 | 
			
		||||
  ChannelWithDefaultChannelId
 | 
			
		||||
} from '../../../models/Channel'
 | 
			
		||||
import { ConfirmPopup } from '../ConfirmPopup'
 | 
			
		||||
import { useFormTranslation } from '../../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  name: channelSchema.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ChannelSettingsProps {
 | 
			
		||||
  channel: Channel
 | 
			
		||||
@@ -41,25 +46,19 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    handleUseForm,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    message,
 | 
			
		||||
    errors,
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    setMessageTranslationKey
 | 
			
		||||
  } = useForm({
 | 
			
		||||
    validateSchema: {
 | 
			
		||||
      name: channelSchema.name
 | 
			
		||||
    },
 | 
			
		||||
    replaceEmptyStringToNull: true,
 | 
			
		||||
    resetOnSuccess: false
 | 
			
		||||
  })
 | 
			
		||||
    setMessage
 | 
			
		||||
  } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await authentication.api.put(`/channels/${channel.id}`, formData)
 | 
			
		||||
      setInputValues(formData as any)
 | 
			
		||||
      setInputValues(formData)
 | 
			
		||||
      await router.push(`/application/${guild.id}/${channel.id}`)
 | 
			
		||||
      return null
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
@@ -91,9 +90,9 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setFetchState('error')
 | 
			
		||||
      if (axios.isAxiosError(error) && error.response?.status === 400) {
 | 
			
		||||
        setMessageTranslationKey('application:delete-channel-only-one')
 | 
			
		||||
        setMessage('application:delete-channel-only-one')
 | 
			
		||||
      } else {
 | 
			
		||||
        setMessageTranslationKey('errors:server-error')
 | 
			
		||||
        setMessage('errors:server-error')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -101,7 +100,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Form
 | 
			
		||||
        onSubmit={handleSubmit(onSubmit)}
 | 
			
		||||
        onSubmit={handleUseForm(onSubmit)}
 | 
			
		||||
        className='my-auto flex flex-col items-center justify-center py-12'
 | 
			
		||||
      >
 | 
			
		||||
        <div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'>
 | 
			
		||||
@@ -114,7 +113,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
 | 
			
		||||
                className='!mt-0'
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                value={inputValues.name}
 | 
			
		||||
                error={getErrorTranslation(errors.name)}
 | 
			
		||||
                error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
                data-cy='channel-name-input'
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { CogIcon } from '@heroicons/react/solid'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,32 @@
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { Form, useForm } from 'react-component-form'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
import { Main } from '../../design/Main'
 | 
			
		||||
import { Button } from '../../design/Button'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { Channel, channelSchema } from '../../../models/Channel'
 | 
			
		||||
import { useGuildMember } from '../../../contexts/GuildMember'
 | 
			
		||||
import { useFormTranslation } from '../../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  name: channelSchema.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CreateChannel: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { guild } = useGuildMember()
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: {
 | 
			
		||||
        name: channelSchema.name
 | 
			
		||||
      },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
  const { handleUseForm, fetchState, message, errors } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const { data: channel } = await authentication.api.post<Channel>(
 | 
			
		||||
        `/guilds/${guild.id}/channels`,
 | 
			
		||||
@@ -44,13 +44,13 @@ export const CreateChannel: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Main>
 | 
			
		||||
      <Form className='w-4/6 max-w-xs' onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
      <Form className='w-4/6 max-w-xs' onSubmit={handleUseForm(onSubmit)}>
 | 
			
		||||
        <Input
 | 
			
		||||
          type='text'
 | 
			
		||||
          placeholder={t('common:name')}
 | 
			
		||||
          name='name'
 | 
			
		||||
          label={t('common:name')}
 | 
			
		||||
          error={getErrorTranslation(errors.name)}
 | 
			
		||||
          error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
          data-cy='channel-name-input'
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,33 @@
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { Form, useForm } from 'react-component-form'
 | 
			
		||||
import { AxiosResponse } from 'axios'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
 | 
			
		||||
import { GuildComplete, guildSchema } from '../../../models/Guild'
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
import { Main } from '../../design/Main'
 | 
			
		||||
import { Button } from '../../design/Button'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { Textarea } from '../../design/Textarea'
 | 
			
		||||
import { useFormTranslation } from '../../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  name: guildSchema.name,
 | 
			
		||||
  description: guildSchema.description
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CreateGuild: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: {
 | 
			
		||||
        name: guildSchema.name,
 | 
			
		||||
        description: guildSchema.description
 | 
			
		||||
      },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
  const { handleUseForm, fetchState, message, errors } = useForm(schema as any)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<any> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const { data } = await authentication.api.post<
 | 
			
		||||
        any,
 | 
			
		||||
@@ -47,13 +47,13 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Main>
 | 
			
		||||
      <Form className='w-4/6 max-w-xs' onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
      <Form className='w-4/6 max-w-xs' onSubmit={handleUseForm(onSubmit)}>
 | 
			
		||||
        <Input
 | 
			
		||||
          type='text'
 | 
			
		||||
          placeholder={t('common:name')}
 | 
			
		||||
          name='name'
 | 
			
		||||
          label={t('common:name')}
 | 
			
		||||
          error={getErrorTranslation(errors.name)}
 | 
			
		||||
          error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
        />
 | 
			
		||||
        <Textarea
 | 
			
		||||
          label='Description'
 | 
			
		||||
@@ -64,7 +64,11 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
          {t('application:create')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Form>
 | 
			
		||||
      <FormState id='message' state={fetchState} message={message} />
 | 
			
		||||
      <FormState
 | 
			
		||||
        id='message'
 | 
			
		||||
        state={fetchState}
 | 
			
		||||
        message={message != null ? t(message) : undefined}
 | 
			
		||||
      />
 | 
			
		||||
    </Main>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,11 @@ import { useRouter } from 'next/router'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import { PhotographIcon } from '@heroicons/react/solid'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { Form, useForm } from 'react-component-form'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
 | 
			
		||||
import { guildSchema } from '../../../models/Guild'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { useGuildMember } from '../../../contexts/GuildMember'
 | 
			
		||||
@@ -16,6 +16,12 @@ import { Input } from '../../design/Input'
 | 
			
		||||
import { Button } from '../../design/Button'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { ConfirmPopup } from '../ConfirmPopup'
 | 
			
		||||
import { useFormTranslation } from '../../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  name: guildSchema.name,
 | 
			
		||||
  description: Type.Optional(guildSchema.description)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const GuildSettings: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
@@ -35,26 +41,19 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    handleUseForm,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    message,
 | 
			
		||||
    errors,
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    setMessageTranslationKey
 | 
			
		||||
  } = useForm({
 | 
			
		||||
    validateSchema: {
 | 
			
		||||
      name: guildSchema.name,
 | 
			
		||||
      description: Type.Optional(guildSchema.description)
 | 
			
		||||
    },
 | 
			
		||||
    replaceEmptyStringToNull: true,
 | 
			
		||||
    resetOnSuccess: false
 | 
			
		||||
  })
 | 
			
		||||
    setMessage
 | 
			
		||||
  } = useForm(schema as any)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<any> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await authentication.api.put(`/guilds/${guild.id}`, formData)
 | 
			
		||||
      setInputValues(formData as any)
 | 
			
		||||
      setInputValues(formData as unknown as any)
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        value: 'application:saved-information'
 | 
			
		||||
@@ -92,7 +91,7 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
        setFetchState('idle')
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        setMessageTranslationKey('errors:server-error')
 | 
			
		||||
        setMessage('errors:server-error')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -102,7 +101,7 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
      await authentication.api.delete(`/guilds/${guild.id}`)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setFetchState('error')
 | 
			
		||||
      setMessageTranslationKey('errors:server-error')
 | 
			
		||||
      setMessage('errors:server-error')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -112,14 +111,14 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
      await router.push('/application')
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setFetchState('error')
 | 
			
		||||
      setMessageTranslationKey('errors:server-error')
 | 
			
		||||
      setMessage('errors:server-error')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Form
 | 
			
		||||
        onSubmit={handleSubmit(onSubmit)}
 | 
			
		||||
        onSubmit={handleUseForm(onSubmit)}
 | 
			
		||||
        className='my-auto flex flex-col items-center justify-center py-12'
 | 
			
		||||
      >
 | 
			
		||||
        {member.isOwner && (
 | 
			
		||||
@@ -160,7 +159,7 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
                  className='!mt-0'
 | 
			
		||||
                  onChange={onChange}
 | 
			
		||||
                  value={inputValues.name}
 | 
			
		||||
                  error={getErrorTranslation(errors.name)}
 | 
			
		||||
                  error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
                  data-cy='guild-name-input'
 | 
			
		||||
                />
 | 
			
		||||
                <Textarea
 | 
			
		||||
@@ -204,7 +203,11 @@ export const GuildSettings: React.FC = () => {
 | 
			
		||||
          </div>
 | 
			
		||||
          <FormState
 | 
			
		||||
            state={fetchState}
 | 
			
		||||
            message={getErrorTranslation(errors.description) ?? message}
 | 
			
		||||
            message={
 | 
			
		||||
              message != null
 | 
			
		||||
                ? t(message)
 | 
			
		||||
                : getFirstErrorTranslation(errors.email)
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </Form>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import Image from 'next/image'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
import { Emoji } from '../../Emoji'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { PlusSmIcon, ArrowDownIcon } from '@heroicons/react/solid'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
 | 
			
		||||
import { PopupGuildCard } from './PopupGuildCard'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { useState, useRef } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import { ApplicationProps } from '..'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { useState, useMemo } from 'react'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { Form, useForm } from 'react-component-form'
 | 
			
		||||
import { EyeIcon, PhotographIcon } from '@heroicons/react/solid'
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
import { Checkbox } from '../../design/Checkbox'
 | 
			
		||||
@@ -16,10 +17,20 @@ import { Language } from '../../Header/Language'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { Button } from '../../design/Button'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { useForm, HandleSubmitCallback } from '../../../hooks/useForm'
 | 
			
		||||
import { userSchema } from '../../../models/User'
 | 
			
		||||
import { userSettingsSchema } from '../../../models/UserSettings'
 | 
			
		||||
import { ProviderOAuth, providers } from '../../../models/OAuth'
 | 
			
		||||
import { useFormTranslation } from '../../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  name: userSchema.name,
 | 
			
		||||
  status: Type.Optional(userSchema.status),
 | 
			
		||||
  email: Type.Optional(Type.Union([userSchema.email, Type.Null()])),
 | 
			
		||||
  website: Type.Optional(userSchema.website),
 | 
			
		||||
  biography: Type.Optional(userSchema.biography),
 | 
			
		||||
  isPublicGuilds: userSettingsSchema.isPublicGuilds,
 | 
			
		||||
  isPublicEmail: userSettingsSchema.isPublicEmail
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UserSettings: React.FC = () => {
 | 
			
		||||
  const { user, setUser, authentication } = useAuthentication()
 | 
			
		||||
@@ -35,32 +46,20 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    handleUseForm,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    message,
 | 
			
		||||
    setMessageTranslationKey,
 | 
			
		||||
    errors,
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    handleSubmit
 | 
			
		||||
  } = useForm({
 | 
			
		||||
    validateSchema: {
 | 
			
		||||
      name: userSchema.name,
 | 
			
		||||
      status: Type.Optional(userSchema.status),
 | 
			
		||||
      email: Type.Optional(Type.Union([userSchema.email, Type.Null()])),
 | 
			
		||||
      website: Type.Optional(userSchema.website),
 | 
			
		||||
      biography: Type.Optional(userSchema.biography),
 | 
			
		||||
      isPublicGuilds: userSettingsSchema.isPublicGuilds,
 | 
			
		||||
      isPublicEmail: userSettingsSchema.isPublicEmail
 | 
			
		||||
    },
 | 
			
		||||
    replaceEmptyStringToNull: true,
 | 
			
		||||
    resetOnSuccess: false
 | 
			
		||||
  })
 | 
			
		||||
    setMessage,
 | 
			
		||||
    errors
 | 
			
		||||
  } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const hasAllProviders = useMemo(() => {
 | 
			
		||||
    return providers.every((provider) => user.strategies.includes(provider))
 | 
			
		||||
  }, [user.strategies])
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const { isPublicGuilds, isPublicEmail, ...userData } = formData
 | 
			
		||||
      const userSettings = { isPublicEmail, isPublicGuilds }
 | 
			
		||||
@@ -68,7 +67,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
        `/users/current?redirectURI=${window.location.origin}/authentication/signin`,
 | 
			
		||||
        userData
 | 
			
		||||
      )
 | 
			
		||||
      setInputValues(formData as any)
 | 
			
		||||
      setInputValues(formData as unknown as any)
 | 
			
		||||
      const hasEmailChanged = user.email !== userCurrentData.user.email
 | 
			
		||||
      if (hasEmailChanged) {
 | 
			
		||||
        return {
 | 
			
		||||
@@ -165,7 +164,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
        setFetchState('idle')
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        setMessageTranslationKey('errors:server-error')
 | 
			
		||||
        setMessage('errors:server-error')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -176,7 +175,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
      await authentication.signoutServerSide()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setFetchState('error')
 | 
			
		||||
      setMessageTranslationKey('errors:server-error')
 | 
			
		||||
      setMessage('errors:server-error')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -195,10 +194,10 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        setMessageTranslationKey('application:success-deleted-provider')
 | 
			
		||||
        setMessage('application:success-deleted-provider')
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        setMessageTranslationKey('errors:server-error')
 | 
			
		||||
        setMessage('errors:server-error')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -217,7 +216,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Form
 | 
			
		||||
      onSubmit={handleSubmit(onSubmit)}
 | 
			
		||||
      onSubmit={handleUseForm(onSubmit)}
 | 
			
		||||
      className='my-auto flex flex-col items-center justify-center py-12 lg:min-w-[875px]'
 | 
			
		||||
    >
 | 
			
		||||
      <div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'>
 | 
			
		||||
@@ -257,7 +256,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
              className='!mt-0'
 | 
			
		||||
              onChange={onChange}
 | 
			
		||||
              value={inputValues.name ?? ''}
 | 
			
		||||
              error={getErrorTranslation(errors.name)}
 | 
			
		||||
              error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
            />
 | 
			
		||||
            <Input
 | 
			
		||||
              name='status'
 | 
			
		||||
@@ -266,7 +265,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
              className='!mt-4'
 | 
			
		||||
              onChange={onChange}
 | 
			
		||||
              value={inputValues.status ?? ''}
 | 
			
		||||
              error={getErrorTranslation(errors.status)}
 | 
			
		||||
              error={getFirstErrorTranslation(errors.status)}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -279,7 +278,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
            placeholder='Email'
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            value={inputValues.email ?? ''}
 | 
			
		||||
            error={getErrorTranslation(errors.email)}
 | 
			
		||||
            error={getFirstErrorTranslation(errors.email)}
 | 
			
		||||
          />
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            name='isPublicEmail'
 | 
			
		||||
@@ -294,7 +293,7 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
            placeholder={t('application:website')}
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            value={inputValues.website ?? ''}
 | 
			
		||||
            error={getErrorTranslation(errors.website)}
 | 
			
		||||
            error={getFirstErrorTranslation(errors.website)}
 | 
			
		||||
          />
 | 
			
		||||
          <Textarea
 | 
			
		||||
            name='biography'
 | 
			
		||||
@@ -376,7 +375,10 @@ export const UserSettings: React.FC = () => {
 | 
			
		||||
            {t('application:signout')}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <FormState state={fetchState} message={message} />
 | 
			
		||||
        <FormState
 | 
			
		||||
          state={fetchState}
 | 
			
		||||
          message={message != null ? t(message) : undefined}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Form>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useForm } from 'react-component-form'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { Main } from '../design/Main'
 | 
			
		||||
import { Input } from '../design/Input'
 | 
			
		||||
@@ -15,8 +18,8 @@ import {
 | 
			
		||||
  Tokens,
 | 
			
		||||
  Authentication as AuthenticationClass
 | 
			
		||||
} from '../../tools/authentication'
 | 
			
		||||
import { useForm, HandleSubmitCallback } from '../../hooks/useForm'
 | 
			
		||||
import { AuthenticationSocialMedia } from './AuthenticationSocialMedia'
 | 
			
		||||
import { useFormTranslation } from '../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
export interface AuthenticationProps {
 | 
			
		||||
  mode: 'signup' | 'signin'
 | 
			
		||||
@@ -29,23 +32,28 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
  const { lang, t } = useTranslation()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const { errors, fetchState, message, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: {
 | 
			
		||||
        ...(mode === 'signup' && { name: userSchema.name }),
 | 
			
		||||
        email: userSchema.email,
 | 
			
		||||
        password: userSchema.password
 | 
			
		||||
      },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
  const schema = useMemo(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...(mode === 'signup' && { name: userSchema.name }),
 | 
			
		||||
      email: userSchema.email,
 | 
			
		||||
      password: userSchema.password
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode])
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const { handleUseForm, errors, fetchState, message } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (
 | 
			
		||||
    formData,
 | 
			
		||||
    formElement
 | 
			
		||||
  ) => {
 | 
			
		||||
    if (mode === 'signup') {
 | 
			
		||||
      try {
 | 
			
		||||
        await api.post(
 | 
			
		||||
          `/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
 | 
			
		||||
          { ...formData, language: lang, theme }
 | 
			
		||||
        )
 | 
			
		||||
        formElement.reset()
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          value: 'authentication:success-signup'
 | 
			
		||||
@@ -97,14 +105,14 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
      <div className='pt-8 text-center font-paragraph text-lg'>
 | 
			
		||||
        {t('authentication:or')}
 | 
			
		||||
      </div>
 | 
			
		||||
      <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
      <AuthenticationForm onSubmit={handleUseForm(onSubmit)}>
 | 
			
		||||
        {mode === 'signup' && (
 | 
			
		||||
          <Input
 | 
			
		||||
            type='text'
 | 
			
		||||
            placeholder={t('common:name')}
 | 
			
		||||
            name='name'
 | 
			
		||||
            label={t('common:name')}
 | 
			
		||||
            error={getErrorTranslation(errors.name)}
 | 
			
		||||
            error={getFirstErrorTranslation(errors.name)}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <Input
 | 
			
		||||
@@ -112,7 +120,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
          placeholder='Email'
 | 
			
		||||
          name='email'
 | 
			
		||||
          label='Email'
 | 
			
		||||
          error={getErrorTranslation(errors.email)}
 | 
			
		||||
          error={getFirstErrorTranslation(errors.email)}
 | 
			
		||||
        />
 | 
			
		||||
        <Input
 | 
			
		||||
          type='password'
 | 
			
		||||
@@ -120,7 +128,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
          name='password'
 | 
			
		||||
          label={t('authentication:password')}
 | 
			
		||||
          showForgotPassword={mode === 'signin'}
 | 
			
		||||
          error={getErrorTranslation(errors.password)}
 | 
			
		||||
          error={getFirstErrorTranslation(errors.password)}
 | 
			
		||||
        />
 | 
			
		||||
        <Button data-cy='submit' className='mt-6 w-full' type='submit'>
 | 
			
		||||
          {t('authentication:submit')}
 | 
			
		||||
@@ -141,7 +149,11 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
          </Link>
 | 
			
		||||
        </p>
 | 
			
		||||
      </AuthenticationForm>
 | 
			
		||||
      <FormState id='message' state={fetchState} message={message} />
 | 
			
		||||
      <FormState
 | 
			
		||||
        id='message'
 | 
			
		||||
        state={fetchState}
 | 
			
		||||
        message={message != null ? t(message) : undefined}
 | 
			
		||||
      />
 | 
			
		||||
    </Main>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import { Form, FormProps } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
export const AuthenticationForm: React.FC<FormProps> = (props) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { useCallback, useEffect, useState, useRef } from 'react'
 | 
			
		||||
import { useCallback, useState, useRef } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import setLanguage from 'next-translate/setLanguage'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import i18n from '../../../i18n.json'
 | 
			
		||||
import { Arrow } from './Arrow'
 | 
			
		||||
import { LanguageFlag } from './LanguageFlag'
 | 
			
		||||
import i18n from '../../../i18n.json'
 | 
			
		||||
import { useClickOutsideAlerter } from '../../../hooks/useClickOutsideAlerter'
 | 
			
		||||
 | 
			
		||||
export interface LanguageProps {
 | 
			
		||||
  className?: string
 | 
			
		||||
@@ -21,31 +22,19 @@ export const Language: React.FC<LanguageProps> = (props) => {
 | 
			
		||||
    setHiddenMenu((oldHiddenMenu) => !oldHiddenMenu)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleClickEvent = (event: MouseEvent): void => {
 | 
			
		||||
      if (languageClickRef.current == null || event.target == null) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!languageClickRef.current.contains(event.target as Node)) {
 | 
			
		||||
        setHiddenMenu(true)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.document.addEventListener('click', handleClickEvent)
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      return window.removeEventListener('click', handleClickEvent)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
  useClickOutsideAlerter(languageClickRef, () => setHiddenMenu(true))
 | 
			
		||||
 | 
			
		||||
  const handleLanguage = async (language: string): Promise<void> => {
 | 
			
		||||
    await setLanguage(language)
 | 
			
		||||
    handleHiddenMenu()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='relative flex cursor-pointer flex-col items-center justify-center'>
 | 
			
		||||
    <div
 | 
			
		||||
      className='relative flex cursor-pointer flex-col items-center justify-center'
 | 
			
		||||
      ref={languageClickRef}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        ref={languageClickRef}
 | 
			
		||||
        data-cy='language-click'
 | 
			
		||||
        className='mr-5 flex items-center'
 | 
			
		||||
        onClick={handleHiddenMenu}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { forwardRef } from 'react'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
const className =
 | 
			
		||||
  'py-2 px-6 font-paragraph rounded-lg bg-transparent border  hover:text-white dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:text-white dark:focus:text-black'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
export interface CheckboxProps extends React.ComponentPropsWithRef<'input'> {
 | 
			
		||||
  className?: string
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import type { FetchState as FormStateType } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { FetchState as FormStateType } from '../../../hooks/useFetchState'
 | 
			
		||||
import { Loader } from '../Loader'
 | 
			
		||||
 | 
			
		||||
export interface FormStateProps extends React.ComponentPropsWithoutRef<'div'> {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
export interface IconButtonProps
 | 
			
		||||
  extends React.ComponentPropsWithoutRef<'button'> {}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
export interface IconLinkProps {
 | 
			
		||||
  selected?: boolean
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import { FormState } from '../FormState'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
export interface MainProps {
 | 
			
		||||
  className?: string
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
import { ProviderOAuth } from '../../../models/OAuth'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
import { getErrorTranslationKey } from '../../../../hooks/useForm/getErrorTranslationKey'
 | 
			
		||||
 | 
			
		||||
const errorObject: ErrorObject = {
 | 
			
		||||
  instancePath: '/path',
 | 
			
		||||
  keyword: 'keyword',
 | 
			
		||||
  params: {},
 | 
			
		||||
  schemaPath: '/path'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('hooks/useForm/getErrorTranslationKey', () => {
 | 
			
		||||
  it('returns `errors:invalid` with unknown keyword', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'unknownkeyword'
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:invalid')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns `errors:invalid` with format != email', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'format',
 | 
			
		||||
        params: { format: 'email' }
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:email')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns `errors:email` with format = email', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'format',
 | 
			
		||||
        params: { format: 'email' }
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:email')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns `errors:required` with minLength and limit = 1', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'minLength',
 | 
			
		||||
        params: { limit: 1 }
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:required')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns `errors:minLength` with minLength and limit > 1', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'minLength',
 | 
			
		||||
        params: { limit: 5 }
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:minLength')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns `errors:maxLength` with maxLength', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
        keyword: 'maxLength',
 | 
			
		||||
        params: { limit: 5 }
 | 
			
		||||
      })
 | 
			
		||||
    ).equal('errors:maxLength')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import { handleCheckboxBoolean } from '../../../../hooks/useForm/handleCheckboxBoolean'
 | 
			
		||||
 | 
			
		||||
const schema = Type.Object({
 | 
			
		||||
  myBoolean: Type.Boolean(),
 | 
			
		||||
  myString: Type.String()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('hooks/useForm/handleCheckboxBoolean', () => {
 | 
			
		||||
  it('should convert all checkbox property to boolean', () => {
 | 
			
		||||
    const object = {
 | 
			
		||||
      myBoolean: 'on',
 | 
			
		||||
      myString: 'on'
 | 
			
		||||
    }
 | 
			
		||||
    const result = handleCheckboxBoolean(object, schema)
 | 
			
		||||
    expect(result).deep.equal({
 | 
			
		||||
      myBoolean: true,
 | 
			
		||||
      myString: 'on'
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
import { replaceEmptyStringInObjectToNull } from '../../../../hooks/useForm/replaceEmptyStringInObjectToNull'
 | 
			
		||||
 | 
			
		||||
describe('hooks/useForm/replaceEmptyStringInObjectToNull', () => {
 | 
			
		||||
  it('should replace empty string in object to null except for required properties', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      replaceEmptyStringInObjectToNull(
 | 
			
		||||
        {
 | 
			
		||||
          foo: '',
 | 
			
		||||
          bar: 'bar',
 | 
			
		||||
          baz: ''
 | 
			
		||||
        },
 | 
			
		||||
        ['baz']
 | 
			
		||||
      )
 | 
			
		||||
    ).deep.equal({
 | 
			
		||||
      foo: null,
 | 
			
		||||
      bar: 'bar',
 | 
			
		||||
      baz: ''
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import classNames from 'clsx'
 | 
			
		||||
 | 
			
		||||
export interface {{ properCase name }}Props {
 | 
			
		||||
  className?: string
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								hooks/useClickOutsideAlerter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								hooks/useClickOutsideAlerter.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
export const useClickOutsideAlerter = (
 | 
			
		||||
  ref: React.RefObject<HTMLElement>,
 | 
			
		||||
  callback: () => void
 | 
			
		||||
): void => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleClickOutside = (event: MouseEvent): void => {
 | 
			
		||||
      if (
 | 
			
		||||
        event.target != null &&
 | 
			
		||||
        ref.current != null &&
 | 
			
		||||
        !ref.current.contains(event.target as Node)
 | 
			
		||||
      ) {
 | 
			
		||||
        callback()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    document.addEventListener('mousedown', handleClickOutside)
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.removeEventListener('mousedown', handleClickOutside)
 | 
			
		||||
    }
 | 
			
		||||
  }, [ref, callback])
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export const fetchState = ['idle', 'loading', 'error', 'success'] as const
 | 
			
		||||
 | 
			
		||||
export type FetchState = typeof fetchState[number]
 | 
			
		||||
 | 
			
		||||
export const useFetchState = (
 | 
			
		||||
  initialFetchState: FetchState = 'idle'
 | 
			
		||||
): [
 | 
			
		||||
  fetchState: FetchState,
 | 
			
		||||
  setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
 | 
			
		||||
] => {
 | 
			
		||||
  const [fetchState, setFetchState] = useState<FetchState>(initialFetchState)
 | 
			
		||||
  return [fetchState, setFetchState]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
const knownErrorKeywords = ['minLength', 'maxLength', 'format']
 | 
			
		||||
 | 
			
		||||
export const getErrorTranslationKey = (error: ErrorObject): string => {
 | 
			
		||||
  if (knownErrorKeywords.includes(error?.keyword)) {
 | 
			
		||||
    if (error.keyword === 'minLength' && error.params.limit === 1) {
 | 
			
		||||
      return 'errors:required'
 | 
			
		||||
    }
 | 
			
		||||
    if (error.keyword === 'format') {
 | 
			
		||||
      if (error.params.format === 'email') {
 | 
			
		||||
        return 'errors:email'
 | 
			
		||||
      }
 | 
			
		||||
      return 'errors:invalid'
 | 
			
		||||
    }
 | 
			
		||||
    return `errors:${error.keyword}`
 | 
			
		||||
  }
 | 
			
		||||
  return 'errors:invalid'
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
import type { TObject } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
 | 
			
		||||
export const handleCheckboxBoolean = (
 | 
			
		||||
  object: ObjectAny,
 | 
			
		||||
  validateSchemaObject: TObject<ObjectAny>
 | 
			
		||||
): ObjectAny => {
 | 
			
		||||
  const booleanProperties: string[] = []
 | 
			
		||||
  for (const property in validateSchemaObject.properties) {
 | 
			
		||||
    const rule = validateSchemaObject.properties[property]
 | 
			
		||||
    if (rule.type === 'boolean') {
 | 
			
		||||
      booleanProperties.push(property)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (const booleanProperty of booleanProperties) {
 | 
			
		||||
    if (object[booleanProperty] == null) {
 | 
			
		||||
      object[booleanProperty] =
 | 
			
		||||
        validateSchemaObject.properties[booleanProperty].default
 | 
			
		||||
    } else {
 | 
			
		||||
      object[booleanProperty] = object[booleanProperty] === 'on'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return object
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
export * from './useForm'
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
 | 
			
		||||
export const replaceEmptyStringInObjectToNull = (
 | 
			
		||||
  object: ObjectAny,
 | 
			
		||||
  required: string[] = []
 | 
			
		||||
): ObjectAny => {
 | 
			
		||||
  return Object.fromEntries(
 | 
			
		||||
    Object.entries(object).map(([key, value]) => {
 | 
			
		||||
      if (
 | 
			
		||||
        typeof value === 'string' &&
 | 
			
		||||
        value.length === 0 &&
 | 
			
		||||
        !required.includes(key)
 | 
			
		||||
      ) {
 | 
			
		||||
        return [key, null]
 | 
			
		||||
      }
 | 
			
		||||
      return [key, value]
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,134 +0,0 @@
 | 
			
		||||
import { useMemo, useState } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import type { HandleForm } from 'react-component-form'
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
import { FetchState, useFetchState } from '../useFetchState'
 | 
			
		||||
import { ajv } from '../../tools/ajv'
 | 
			
		||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
 | 
			
		||||
import { replaceEmptyStringInObjectToNull } from './replaceEmptyStringInObjectToNull'
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
import { handleCheckboxBoolean } from './handleCheckboxBoolean'
 | 
			
		||||
 | 
			
		||||
interface Errors {
 | 
			
		||||
  [key: string]: ErrorObject<string, any> | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const findError = (
 | 
			
		||||
  field: string
 | 
			
		||||
): ((value: ErrorObject, index: number, object: ErrorObject[]) => boolean) => {
 | 
			
		||||
  return (validationError) => validationError.instancePath === field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type GetErrorTranslation = (error?: ErrorObject | null) => string | null
 | 
			
		||||
 | 
			
		||||
export interface UseFormOptions {
 | 
			
		||||
  validateSchema: { [key: string]: any }
 | 
			
		||||
  replaceEmptyStringToNull?: boolean
 | 
			
		||||
  resetOnSuccess?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HandleSubmit = (callback: HandleSubmitCallback) => HandleForm
 | 
			
		||||
 | 
			
		||||
interface Message {
 | 
			
		||||
  type: 'error' | 'success'
 | 
			
		||||
  value: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HandleSubmitCallback = (
 | 
			
		||||
  formData: ObjectAny,
 | 
			
		||||
  formElement: HTMLFormElement
 | 
			
		||||
) => Promise<Message | null>
 | 
			
		||||
 | 
			
		||||
export interface UseFormResult {
 | 
			
		||||
  message: string | null
 | 
			
		||||
  setMessageTranslationKey: React.Dispatch<
 | 
			
		||||
    React.SetStateAction<string | undefined>
 | 
			
		||||
  >
 | 
			
		||||
  fetchState: FetchState
 | 
			
		||||
  setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
 | 
			
		||||
  getErrorTranslation: GetErrorTranslation
 | 
			
		||||
  handleSubmit: HandleSubmit
 | 
			
		||||
  errors: Errors
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  const {
 | 
			
		||||
    validateSchema,
 | 
			
		||||
    replaceEmptyStringToNull = false,
 | 
			
		||||
    resetOnSuccess = false
 | 
			
		||||
  } = options
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const [fetchState, setFetchState] = useFetchState()
 | 
			
		||||
  const [messageTranslationKey, setMessageTranslationKey] = useState<
 | 
			
		||||
    string | undefined
 | 
			
		||||
  >(undefined)
 | 
			
		||||
  const [errors, setErrors] = useState<Errors>({})
 | 
			
		||||
 | 
			
		||||
  const validateSchemaObject = useMemo(() => {
 | 
			
		||||
    return Type.Object(validateSchema)
 | 
			
		||||
  }, [validateSchema])
 | 
			
		||||
 | 
			
		||||
  const validate = useMemo(() => {
 | 
			
		||||
    return ajv.compile(validateSchemaObject)
 | 
			
		||||
  }, [validateSchemaObject])
 | 
			
		||||
 | 
			
		||||
  const getErrorTranslation = (error?: ErrorObject | null): string | null => {
 | 
			
		||||
    if (error != null) {
 | 
			
		||||
      return t(getErrorTranslationKey(error)).replace(
 | 
			
		||||
        '{expected}',
 | 
			
		||||
        error?.params?.limit
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit: HandleSubmit = (callback) => {
 | 
			
		||||
    return async (formData: ObjectAny, formElement) => {
 | 
			
		||||
      setErrors({})
 | 
			
		||||
      setMessageTranslationKey(undefined)
 | 
			
		||||
      if (replaceEmptyStringToNull) {
 | 
			
		||||
        formData = replaceEmptyStringInObjectToNull(
 | 
			
		||||
          formData,
 | 
			
		||||
          validateSchemaObject.required
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      formData = handleCheckboxBoolean(formData, validateSchemaObject)
 | 
			
		||||
      const isValid = validate(formData)
 | 
			
		||||
      if (!isValid) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        const errors: Errors = {}
 | 
			
		||||
        for (const property in validateSchemaObject.properties) {
 | 
			
		||||
          errors[property] = validate.errors?.find(findError(`/${property}`))
 | 
			
		||||
        }
 | 
			
		||||
        setErrors(errors)
 | 
			
		||||
      } else {
 | 
			
		||||
        setErrors({})
 | 
			
		||||
        setFetchState('loading')
 | 
			
		||||
        const message = await callback(formData, formElement)
 | 
			
		||||
        if (message != null) {
 | 
			
		||||
          setMessageTranslationKey(message.value)
 | 
			
		||||
          if (message.type === 'success') {
 | 
			
		||||
            setFetchState('success')
 | 
			
		||||
            if (resetOnSuccess) {
 | 
			
		||||
              formElement.reset()
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            setFetchState('error')
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    errors,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    message: messageTranslationKey != null ? t(messageTranslationKey) : null,
 | 
			
		||||
    setMessageTranslationKey
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								hooks/useFormTranslation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								hooks/useFormTranslation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import type { Error } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
const knownErrorKeywords = ['minLength', 'maxLength', 'format']
 | 
			
		||||
 | 
			
		||||
const getErrorTranslationKey = (error: Error): string => {
 | 
			
		||||
  if (knownErrorKeywords.includes(error?.keyword)) {
 | 
			
		||||
    if (
 | 
			
		||||
      error.keyword === 'minLength' &&
 | 
			
		||||
      typeof error.data === 'string' &&
 | 
			
		||||
      error.data.length === 0
 | 
			
		||||
    ) {
 | 
			
		||||
      return 'errors:required'
 | 
			
		||||
    }
 | 
			
		||||
    if (error.keyword === 'format') {
 | 
			
		||||
      if (error.params.format === 'email') {
 | 
			
		||||
        return 'errors:invalid-email'
 | 
			
		||||
      }
 | 
			
		||||
      return 'errors:invalid'
 | 
			
		||||
    }
 | 
			
		||||
    return `errors:${error.keyword}`
 | 
			
		||||
  }
 | 
			
		||||
  return 'errors:invalid'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type GetErrorTranslation = (
 | 
			
		||||
  error: Error | undefined
 | 
			
		||||
) => string | undefined
 | 
			
		||||
 | 
			
		||||
export type GetFirstErrorTranslation = (
 | 
			
		||||
  errors: Error[] | undefined
 | 
			
		||||
) => string | undefined
 | 
			
		||||
 | 
			
		||||
export interface UseFormTranslationResult {
 | 
			
		||||
  getErrorTranslation: GetErrorTranslation
 | 
			
		||||
  getFirstErrorTranslation: GetFirstErrorTranslation
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useFormTranslation = (): UseFormTranslationResult => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  const getErrorTranslation: GetErrorTranslation = (error) => {
 | 
			
		||||
    if (error != null) {
 | 
			
		||||
      return t(getErrorTranslationKey(error)).replace(
 | 
			
		||||
        '{expected}',
 | 
			
		||||
        error?.params?.limit
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getFirstErrorTranslation: GetFirstErrorTranslation = (errors) => {
 | 
			
		||||
    if (errors != null) {
 | 
			
		||||
      return getErrorTranslation(errors[0])
 | 
			
		||||
    }
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { getFirstErrorTranslation, getErrorTranslation }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { useState, useRef, useCallback } from 'react'
 | 
			
		||||
import { AxiosInstance } from 'axios'
 | 
			
		||||
import type { FetchState } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { FetchState } from './useFetchState'
 | 
			
		||||
import {
 | 
			
		||||
  CacheKey,
 | 
			
		||||
  getPaginationCache,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,6 @@
 | 
			
		||||
  "required": "Oops, this field is required 🙈.",
 | 
			
		||||
  "minLength": "The field must contain at least {expected} characters.",
 | 
			
		||||
  "maxLength": "The field must contain at most {expected} characters.",
 | 
			
		||||
  "email": "Mmm… It seems that this email is not valid 🤔.",
 | 
			
		||||
  "invalid-email": "Mmm… It seems that this email is not valid 🤔.",
 | 
			
		||||
  "invalid": "Invalid value."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,6 @@
 | 
			
		||||
  "required": "Oups, ce champ est obligatoire 🙈.",
 | 
			
		||||
  "minLength": "Le champ doit contenir au moins {expected} caractères.",
 | 
			
		||||
  "maxLength": "Le champ doit contenir au plus {expected} caractères.",
 | 
			
		||||
  "email": "Mmm… Il semblerait que cet email ne soit pas valide 🤔.",
 | 
			
		||||
  "invalid-email": "Mmm… Il semblerait que cet email ne soit pas valide 🤔.",
 | 
			
		||||
  "invalid": "Valeur invalide."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const guildSchema = {
 | 
			
		||||
  id,
 | 
			
		||||
  name: Type.String({ minLength: 1, maxLength: 30 }),
 | 
			
		||||
  icon: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
 | 
			
		||||
  description: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
 | 
			
		||||
  description: Type.String({ maxLength: 160 }),
 | 
			
		||||
  createdAt: date.createdAt,
 | 
			
		||||
  updatedAt: date.updatedAt
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1222
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1222
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
									
									
									
									
								
							@@ -36,20 +36,18 @@
 | 
			
		||||
    "@fontsource/roboto": "4.5.8",
 | 
			
		||||
    "@heroicons/react": "1.0.6",
 | 
			
		||||
    "@sinclair/typebox": "0.24.28",
 | 
			
		||||
    "ajv": "8.11.0",
 | 
			
		||||
    "ajv-formats": "2.1.1",
 | 
			
		||||
    "axios": "0.26.1",
 | 
			
		||||
    "classnames": "2.3.1",
 | 
			
		||||
    "clsx": "1.2.1",
 | 
			
		||||
    "date-and-time": "2.4.1",
 | 
			
		||||
    "emoji-mart": "3.0.1",
 | 
			
		||||
    "katex": "0.16.0",
 | 
			
		||||
    "katex": "0.16.1",
 | 
			
		||||
    "next": "12.2.5",
 | 
			
		||||
    "next-pwa": "5.6.0",
 | 
			
		||||
    "next-themes": "0.2.0",
 | 
			
		||||
    "next-translate": "1.5.0",
 | 
			
		||||
    "pretty-bytes": "6.0.0",
 | 
			
		||||
    "react": "18.2.0",
 | 
			
		||||
    "react-component-form": "3.0.1",
 | 
			
		||||
    "react-component-form": "3.1.0",
 | 
			
		||||
    "react-dom": "18.2.0",
 | 
			
		||||
    "react-infinite-scroll-component": "6.1.0",
 | 
			
		||||
    "react-markdown": "8.0.3",
 | 
			
		||||
@@ -69,31 +67,31 @@
 | 
			
		||||
    "universal-cookie": "4.0.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "17.0.3",
 | 
			
		||||
    "@commitlint/config-conventional": "17.0.3",
 | 
			
		||||
    "@commitlint/cli": "17.1.1",
 | 
			
		||||
    "@commitlint/config-conventional": "17.1.0",
 | 
			
		||||
    "@lhci/cli": "0.9.0",
 | 
			
		||||
    "@saithodev/semantic-release-backmerge": "2.1.2",
 | 
			
		||||
    "@semantic-release/git": "10.0.1",
 | 
			
		||||
    "@types/emoji-mart": "3.0.9",
 | 
			
		||||
    "@types/hast": "2.3.4",
 | 
			
		||||
    "@types/katex": "0.14.0",
 | 
			
		||||
    "@types/node": "18.7.11",
 | 
			
		||||
    "@types/node": "18.7.13",
 | 
			
		||||
    "@types/react": "18.0.17",
 | 
			
		||||
    "@types/react-responsive": "8.0.5",
 | 
			
		||||
    "@types/react-syntax-highlighter": "15.5.4",
 | 
			
		||||
    "@types/react-syntax-highlighter": "15.5.5",
 | 
			
		||||
    "@types/unist": "2.0.6",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "5.34.0",
 | 
			
		||||
    "@typescript-eslint/parser": "5.34.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "5.35.1",
 | 
			
		||||
    "@typescript-eslint/parser": "5.35.1",
 | 
			
		||||
    "autoprefixer": "10.4.8",
 | 
			
		||||
    "cypress": "10.6.0",
 | 
			
		||||
    "editorconfig-checker": "4.0.2",
 | 
			
		||||
    "eslint": "8.22.0",
 | 
			
		||||
    "eslint": "8.23.0",
 | 
			
		||||
    "eslint-config-conventions": "3.0.0",
 | 
			
		||||
    "eslint-config-next": "12.2.5",
 | 
			
		||||
    "eslint-config-prettier": "8.5.0",
 | 
			
		||||
    "eslint-plugin-import": "2.26.0",
 | 
			
		||||
    "eslint-plugin-prettier": "4.2.1",
 | 
			
		||||
    "eslint-plugin-promise": "6.0.0",
 | 
			
		||||
    "eslint-plugin-promise": "6.0.1",
 | 
			
		||||
    "eslint-plugin-unicorn": "43.0.2",
 | 
			
		||||
    "html-w3c-validator": "1.2.0",
 | 
			
		||||
    "husky": "8.0.1",
 | 
			
		||||
@@ -108,7 +106,7 @@
 | 
			
		||||
    "serve": "14.0.1",
 | 
			
		||||
    "start-server-and-test": "1.14.0",
 | 
			
		||||
    "tailwindcss": "3.1.8",
 | 
			
		||||
    "typescript": "4.7.4",
 | 
			
		||||
    "vercel": "28.1.2"
 | 
			
		||||
    "typescript": "4.8.2",
 | 
			
		||||
    "vercel": "28.2.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ import { NextPage } from 'next'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useForm } from 'react-component-form'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { AuthenticationForm } from '../../components/Authentication'
 | 
			
		||||
import { Head } from '../../components/Head'
 | 
			
		||||
@@ -15,24 +17,29 @@ import { authenticationFromServerSide } from '../../tools/authentication'
 | 
			
		||||
import { ScrollableBody } from '../../components/ScrollableBody'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { api } from '../../tools/api'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../hooks/useForm'
 | 
			
		||||
import { useFormTranslation } from '../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  email: userSchema.email
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: { email: userSchema.email },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
  const { handleUseForm, fetchState, message, errors } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (
 | 
			
		||||
    formData,
 | 
			
		||||
    formElement
 | 
			
		||||
  ) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.post(
 | 
			
		||||
        `/users/reset-password?redirectURI=${window.location.origin}/authentication/reset-password`,
 | 
			
		||||
        formData
 | 
			
		||||
      )
 | 
			
		||||
      formElement.reset()
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        value: 'authentication:success-forgot-password'
 | 
			
		||||
@@ -41,7 +48,7 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      if (axios.isAxiosError(error) && error.response?.status === 400) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          value: 'errors:email'
 | 
			
		||||
          value: 'errors:invalid-email'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
@@ -56,7 +63,7 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      <Head title={`Thream | ${t('authentication:forgot-password')}`} />
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Main>
 | 
			
		||||
        <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
        <AuthenticationForm onSubmit={handleUseForm(onSubmit)}>
 | 
			
		||||
          <Input type='email' placeholder='Email' name='email' label='Email' />
 | 
			
		||||
          <Button data-cy='submit' className='mt-6 w-full' type='submit'>
 | 
			
		||||
            {t('authentication:submit')}
 | 
			
		||||
@@ -71,7 +78,9 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
          id='message'
 | 
			
		||||
          state={fetchState}
 | 
			
		||||
          message={
 | 
			
		||||
            message != null ? message : getErrorTranslation(errors.email)
 | 
			
		||||
            message != null
 | 
			
		||||
              ? t(message)
 | 
			
		||||
              : getFirstErrorTranslation(errors.email)
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </Main>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ import { NextPage } from 'next'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useForm } from 'react-component-form'
 | 
			
		||||
import type { HandleUseFormCallback } from 'react-component-form'
 | 
			
		||||
 | 
			
		||||
import { Head } from '../../components/Head'
 | 
			
		||||
import { Header } from '../../components/Header'
 | 
			
		||||
@@ -13,22 +15,23 @@ import { Button } from '../../components/design/Button'
 | 
			
		||||
import { authenticationFromServerSide } from '../../tools/authentication'
 | 
			
		||||
import { AuthenticationForm } from '../../components/Authentication'
 | 
			
		||||
import { ScrollableBody } from '../../components/ScrollableBody'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../hooks/useForm'
 | 
			
		||||
import { api } from '../../tools/api'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { useFormTranslation } from '../../hooks/useFormTranslation'
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  password: userSchema.password
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: { password: userSchema.password },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
  const { handleUseForm, fetchState, message, errors } = useForm(schema)
 | 
			
		||||
  const { getFirstErrorTranslation } = useFormTranslation()
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
  const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.put(`/users/reset-password`, {
 | 
			
		||||
        ...formData,
 | 
			
		||||
@@ -55,7 +58,7 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      <Head title={`Thream | ${t('authentication:reset-password')}`} />
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Main>
 | 
			
		||||
        <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
        <AuthenticationForm onSubmit={handleUseForm(onSubmit)}>
 | 
			
		||||
          <Input
 | 
			
		||||
            type='password'
 | 
			
		||||
            placeholder='Password'
 | 
			
		||||
@@ -70,7 +73,9 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
          id='message'
 | 
			
		||||
          state={fetchState}
 | 
			
		||||
          message={
 | 
			
		||||
            message != null ? message : getErrorTranslation(errors.password)
 | 
			
		||||
            message != null
 | 
			
		||||
              ? t(message)
 | 
			
		||||
              : getFirstErrorTranslation(errors.password)
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </Main>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								tools/ajv.ts
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								tools/ajv.ts
									
									
									
									
									
								
							@@ -1,26 +0,0 @@
 | 
			
		||||
import Ajv from 'ajv'
 | 
			
		||||
import addFormats from 'ajv-formats'
 | 
			
		||||
 | 
			
		||||
export const ajv = addFormats(
 | 
			
		||||
  new Ajv({
 | 
			
		||||
    allErrors: true
 | 
			
		||||
  }),
 | 
			
		||||
  [
 | 
			
		||||
    'date-time',
 | 
			
		||||
    'time',
 | 
			
		||||
    'date',
 | 
			
		||||
    'email',
 | 
			
		||||
    'hostname',
 | 
			
		||||
    'ipv4',
 | 
			
		||||
    'ipv6',
 | 
			
		||||
    'uri',
 | 
			
		||||
    'uri-reference',
 | 
			
		||||
    'uuid',
 | 
			
		||||
    'uri-template',
 | 
			
		||||
    'json-pointer',
 | 
			
		||||
    'relative-json-pointer',
 | 
			
		||||
    'regex'
 | 
			
		||||
  ]
 | 
			
		||||
)
 | 
			
		||||
  .addKeyword('kind')
 | 
			
		||||
  .addKeyword('modifier')
 | 
			
		||||
		Reference in New Issue
	
	Block a user