chore: initial commit

This commit is contained in:
Divlo
2021-10-24 05:19:39 +02:00
commit 21123c4477
145 changed files with 48821 additions and 0 deletions

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