chore: initial commit
This commit is contained in:
130
hooks/useFastestValidator.ts
Normal file
130
hooks/useFastestValidator.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import Validator, {
|
||||
ValidationError,
|
||||
ValidationRule,
|
||||
ValidatorConstructorOptions
|
||||
} from 'fastest-validator'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export type ValidationResult<T> = { [key in keyof T]: ValidationError[] }
|
||||
|
||||
export type AddValidationErrors = (validationErrors: ValidationError[]) => void
|
||||
|
||||
export type GetErrorMessages<T = any> = (key: keyof T) => string[]
|
||||
|
||||
export type Validate = (value: any) => boolean
|
||||
|
||||
export interface UseValidatorResult<T> {
|
||||
validationResult: ValidationResult<T>
|
||||
addValidationErrors: AddValidationErrors
|
||||
getErrorMessages: GetErrorMessages<T>
|
||||
validate: Validate
|
||||
}
|
||||
|
||||
export type ValidatorSchema<T = any> = {
|
||||
[key in keyof T]: ValidationRule
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: ValidationError, message?: string): string => {
|
||||
if (error.message == null || message == null) {
|
||||
return error.type
|
||||
}
|
||||
return message
|
||||
.replace('{field}', error.field)
|
||||
.replace('{expected}', error.expected)
|
||||
}
|
||||
|
||||
export const useFastestValidator = <T = any>(
|
||||
validatorSchema: ValidatorSchema<T>,
|
||||
validatorOptions?: ValidatorConstructorOptions
|
||||
): UseValidatorResult<T> => {
|
||||
const fillEmptyValidation = (
|
||||
result: ValidationResult<T> = {} as any
|
||||
): ValidationResult<T> => {
|
||||
for (const key in validatorSchema) {
|
||||
if (result[key] == null) {
|
||||
result[key] = []
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const { lang } = useTranslation()
|
||||
|
||||
const emptyValidationResult = useMemo(() => {
|
||||
return fillEmptyValidation()
|
||||
}, [validatorSchema])
|
||||
|
||||
useEffect(() => {
|
||||
const result = { ...validationResult }
|
||||
for (const key in result) {
|
||||
result[key] = result[key].map((error) => {
|
||||
if (validatorOptions?.messages != null) {
|
||||
error.message = getErrorMessage(
|
||||
error,
|
||||
validatorOptions.messages[error.type]
|
||||
)
|
||||
}
|
||||
return error
|
||||
})
|
||||
}
|
||||
setValidation(result)
|
||||
}, [lang])
|
||||
|
||||
const [validationResult, setValidation] = useState<ValidationResult<T>>(
|
||||
emptyValidationResult
|
||||
)
|
||||
|
||||
const validator = useMemo(() => {
|
||||
return new Validator(validatorOptions).compile(validatorSchema)
|
||||
}, [validatorOptions, validatorSchema])
|
||||
|
||||
const validate: Validate = (value) => {
|
||||
const validationErrors = validator(value)
|
||||
if (!Array.isArray(validationErrors)) {
|
||||
setValidation(emptyValidationResult)
|
||||
return true
|
||||
}
|
||||
setValidationResult(validationErrors)
|
||||
return false
|
||||
}
|
||||
|
||||
const setValidationResult = (
|
||||
validationErrors: ValidationError[],
|
||||
validationResult: ValidationResult<T> = {} as any
|
||||
): void => {
|
||||
const result: ValidationResult<T> = validationResult
|
||||
validationErrors.forEach((error) => {
|
||||
if (result[error.field as keyof T] == null) {
|
||||
result[error.field as keyof T] = [error]
|
||||
} else {
|
||||
result[error.field as keyof T].push(error)
|
||||
}
|
||||
})
|
||||
const finalResult = fillEmptyValidation(result)
|
||||
setValidation(finalResult)
|
||||
}
|
||||
|
||||
const addValidationErrors: AddValidationErrors = (validationErrors) => {
|
||||
const result: ValidationResult<T> = { ...validationResult }
|
||||
validationErrors.map((error) => {
|
||||
if (validatorOptions?.messages != null) {
|
||||
error.message = validatorOptions.messages[error.type]
|
||||
}
|
||||
})
|
||||
setValidationResult(validationErrors, result)
|
||||
}
|
||||
|
||||
const getErrorMessages: GetErrorMessages<T> = (key) => {
|
||||
return validationResult[key].map((error) => {
|
||||
return getErrorMessage(error, error.message)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
validationResult,
|
||||
addValidationErrors,
|
||||
getErrorMessages,
|
||||
validate
|
||||
}
|
||||
}
|
118
hooks/useForm.ts
Normal file
118
hooks/useForm.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FormDataObject, HandleForm } from 'react-component-form'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { FormState, useFormState } from 'hooks/useFormState'
|
||||
import {
|
||||
GetErrorMessages,
|
||||
useFastestValidator,
|
||||
ValidatorSchema
|
||||
} from 'hooks/useFastestValidator'
|
||||
import { ValidationError } from 'fastest-validator'
|
||||
|
||||
export interface ErrorResponse {
|
||||
field?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface UseFormOptions {
|
||||
validatorSchema: ValidatorSchema
|
||||
}
|
||||
|
||||
export type HandleSubmit = (callback: HandleSubmitCallback) => HandleForm
|
||||
|
||||
export type HandleSubmitCallback = (
|
||||
formData: FormDataObject
|
||||
) => Promise<string | null>
|
||||
|
||||
export interface UseFormResult {
|
||||
message: string | undefined
|
||||
formState: FormState
|
||||
getErrorMessages: GetErrorMessages
|
||||
handleChange: HandleForm
|
||||
handleSubmit: HandleSubmit
|
||||
}
|
||||
|
||||
export const useForm = (options: UseFormOptions): UseFormResult => {
|
||||
const { validatorSchema } = options
|
||||
const { lang, t } = useTranslation()
|
||||
const errorsMessages = useMemo(() => {
|
||||
return {
|
||||
stringMin: t('errors:stringMin'),
|
||||
stringEmpty: t('errors:required'),
|
||||
emailEmpty: t('errors:required'),
|
||||
required: t('errors:required'),
|
||||
email: t('errors:email'),
|
||||
alreadyUsed: t('errors:alreadyUsed'),
|
||||
invalid: t('errors:invalid')
|
||||
}
|
||||
}, [lang])
|
||||
const [formState, setFormState] = useFormState()
|
||||
const {
|
||||
validate,
|
||||
getErrorMessages,
|
||||
addValidationErrors
|
||||
} = useFastestValidator(validatorSchema, {
|
||||
messages: errorsMessages
|
||||
})
|
||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
const handleChange: HandleForm = (formData) => {
|
||||
if (formState !== 'error') {
|
||||
setMessage(undefined)
|
||||
}
|
||||
const isValid = validate(formData)
|
||||
setFormState(!isValid ? 'error' : 'idle')
|
||||
}
|
||||
|
||||
const handleSubmit = (callback: HandleSubmitCallback): HandleForm => {
|
||||
return async (formData, formElement) => {
|
||||
const isValid = validate(formData)
|
||||
if (isValid) {
|
||||
setFormState('loading')
|
||||
try {
|
||||
const successMessage = await callback(formData)
|
||||
if (successMessage != null) {
|
||||
setMessage(successMessage)
|
||||
setFormState('success')
|
||||
formElement.reset()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response == null) {
|
||||
setFormState('error')
|
||||
setMessage(t('errors:server-error'))
|
||||
return
|
||||
}
|
||||
const errors = error.response.data.errors as ErrorResponse[]
|
||||
const validationErrors: ValidationError[] = []
|
||||
for (const error of errors) {
|
||||
if (error.field != null) {
|
||||
if (error.message.endsWith('already used')) {
|
||||
validationErrors.push({
|
||||
type: 'alreadyUsed',
|
||||
field: error.field
|
||||
})
|
||||
} else {
|
||||
validationErrors.push({
|
||||
type: 'invalid',
|
||||
field: error.field
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setFormState('error')
|
||||
setMessage(error.message)
|
||||
break
|
||||
}
|
||||
}
|
||||
addValidationErrors(validationErrors)
|
||||
setFormState('error')
|
||||
}
|
||||
} else {
|
||||
setMessage(undefined)
|
||||
setFormState('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { message, formState, getErrorMessages, handleChange, handleSubmit }
|
||||
}
|
15
hooks/useFormState.ts
Normal file
15
hooks/useFormState.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export const formState = ['idle', 'loading', 'error', 'success'] as const
|
||||
|
||||
export type FormState = typeof formState[number]
|
||||
|
||||
export const useFormState = (
|
||||
initialFormState: FormState = 'idle'
|
||||
): [
|
||||
formState: FormState,
|
||||
setFormState: React.Dispatch<React.SetStateAction<FormState>>
|
||||
] => {
|
||||
const [formState, setFormState] = useState<FormState>(initialFormState)
|
||||
return [formState, setFormState]
|
||||
}
|
76
hooks/usePagination.ts
Normal file
76
hooks/usePagination.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { useRef, useState } from 'react'
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
export type NextPage = () => Promise<void>
|
||||
|
||||
export interface PaginationData<T> {
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
totalItems: number
|
||||
hasMore: boolean
|
||||
rows: T[]
|
||||
}
|
||||
|
||||
interface UsePaginationOptions {
|
||||
api: AxiosInstance
|
||||
url: string
|
||||
defaultPaginationData?: PaginationData<any>
|
||||
inverse?: boolean
|
||||
}
|
||||
|
||||
export type SetData<T> = React.Dispatch<React.SetStateAction<PaginationData<T>>>
|
||||
|
||||
interface UsePaginationReturn<T> {
|
||||
data: PaginationData<T>
|
||||
nextPage: NextPage
|
||||
setData: SetData<T>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const defaultData: PaginationData<any> = {
|
||||
page: 0,
|
||||
itemsPerPage: 20,
|
||||
totalItems: 0,
|
||||
hasMore: true,
|
||||
rows: []
|
||||
}
|
||||
|
||||
export const usePagination = <T>(
|
||||
options: UsePaginationOptions
|
||||
): UsePaginationReturn<T> => {
|
||||
const {
|
||||
api,
|
||||
url,
|
||||
defaultPaginationData = defaultData,
|
||||
inverse = false
|
||||
} = options
|
||||
|
||||
const page = useRef(defaultPaginationData.page + 1)
|
||||
const [data, setData] = useState<PaginationData<T>>(defaultPaginationData)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const nextPage: NextPage = async () => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
const { data: newData } = await api.get<PaginationData<T>>(
|
||||
`${url}?itemsPerPage=${defaultPaginationData.itemsPerPage}&page=${page.current}`
|
||||
)
|
||||
const rows = inverse
|
||||
? [...newData.rows, ...data.rows]
|
||||
: [...data.rows, ...newData.rows]
|
||||
setData({
|
||||
page: page.current,
|
||||
itemsPerPage: defaultPaginationData.itemsPerPage,
|
||||
hasMore: newData.hasMore,
|
||||
totalItems: newData.totalItems,
|
||||
rows: uniqBy(rows, 'id')
|
||||
})
|
||||
setIsLoading(false)
|
||||
page.current += 1
|
||||
}
|
||||
|
||||
return { data, setData, nextPage, isLoading }
|
||||
}
|
Reference in New Issue
Block a user