chore: better Prettier config for easier reviews

This commit is contained in:
Théo LUDWIG 2023-10-23 23:26:27 +02:00
parent 1224ece116
commit a49e844c70
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
50 changed files with 4642 additions and 3862 deletions

View File

@ -1,8 +1,8 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,4 +1,4 @@
name: 'Build' name: "Build"
on: on:
push: push:
@ -8,21 +8,21 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v3.5.3"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.6.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build Package' - name: "Build Package"
run: 'npm run build' run: "npm run build"
- name: 'Build Example' - name: "Build Example"
run: 'cd example && npm clean-install && npm run build' run: "cd example && npm clean-install && npm run build"

View File

@ -1,4 +1,4 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
@ -8,21 +8,21 @@ on:
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v3.5.3"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.6.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- run: 'npm run lint:commit -- --to "${{ github.sha }}"' - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- run: 'npm run lint:editorconfig' - run: "npm run lint:editorconfig"
- run: 'npm run lint:markdown' - run: "npm run lint:markdown"
- run: 'npm run lint:eslint' - run: "npm run lint:eslint"
- run: 'npm run lint:prettier' - run: "npm run lint:prettier"

View File

@ -1,4 +1,4 @@
name: 'Release' name: "Release"
on: on:
push: push:
@ -6,32 +6,32 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
permissions: permissions:
contents: 'write' contents: "write"
issues: 'write' issues: "write"
pull-requests: 'write' pull-requests: "write"
id-token: 'write' id-token: "write"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v3.5.3"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.6.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build Package' - name: "Build Package"
run: 'npm run build' run: "npm run build"
- name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies' - name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies"
run: 'npm audit signatures' run: "npm audit signatures"
- name: 'Release' - name: "Release"
run: 'npm run release' run: "npm run release"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,4 +1,4 @@
name: 'Test' name: "Test"
on: on:
push: push:
@ -8,41 +8,41 @@ on:
jobs: jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v3.5.3"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.6.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Test' - name: "Test"
run: 'npm run test' run: "npm run test"
test-e2e: test-e2e:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v3.5.3"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.6.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build Package' - name: "Build Package"
run: 'npm run build' run: "npm run build"
- name: 'Build Example' - name: "Build Example"
run: 'cd example && npm clean-install && npm run build' run: "cd example && npm clean-install && npm run build"
- name: 'End To End (e2e) Test Example' - name: "End To End (e2e) Test Example"
run: 'cd example && npm run test:e2e' run: "cd example && npm run test:e2e"

View File

@ -1,6 +1,3 @@
{ {
"singleQuote": true, "semi": false
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

View File

@ -37,9 +37,9 @@ npm install --save react-component-form
_Note: The examples use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._ _Note: The examples use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._
```tsx ```tsx
import React from 'react' import React from "react"
import { Form } from 'react-component-form' import { Form } from "react-component-form"
import type { HandleForm } from 'react-component-form' import type { HandleForm } from "react-component-form"
export const Example = () => { export const Example = () => {
const handleSubmit: HandleForm = (formData, formElement) => { const handleSubmit: HandleForm = (formData, formElement) => {
@ -49,8 +49,8 @@ export const Example = () => {
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<input type='text' name='inputName' /> <input type="text" name="inputName" />
<button type='submit'>Submit</button> <button type="submit">Submit</button>
</Form> </Form>
) )
} }
@ -70,16 +70,16 @@ This example shows how to use the `<Form />` component with `useForm` hook to va
You can see a more detailled example in the [./example](./example) folder. You can see a more detailled example in the [./example](./example) folder.
```tsx ```tsx
import React from 'react' import React from "react"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
const schema = { const schema = {
inputName: { inputName: {
type: 'string', type: "string",
minLength: 3, minLength: 3,
maxLength: 20 maxLength: 20,
} },
} }
export const Example = () => { export const Example = () => {
@ -87,24 +87,24 @@ export const Example = () => {
const onSubmit: HandleUseFormCallback<typeof schema> = ( const onSubmit: HandleUseFormCallback<typeof schema> = (
formData, formData,
formElement formElement,
) => { ) => {
console.log(formData) // { inputName: 'value of the input validated and type-safe' } console.log(formData) // { inputName: 'value of the input validated and type-safe' }
formElement.reset() formElement.reset()
// The return can be either `null` or an object with a global message of type `'error' | 'success'`. // The return can be either `null` or an object with a global message of type `'error' | 'success'`.
return { return {
type: 'success', type: "success",
message: 'Success: Form submitted' message: "Success: Form submitted",
} }
} }
return ( return (
<Form onSubmit={handleUseForm(onSubmit)}> <Form onSubmit={handleUseForm(onSubmit)}>
<input type='text' name='inputName' /> <input type="text" name="inputName" />
{errors.inputName != null && <p>{errors.inputName[0].message}</p>} {errors.inputName != null && <p>{errors.inputName[0].message}</p>}
<button type='submit'>Submit</button> <button type="submit">Submit</button>
{message != null && <p>{message}</p>} {message != null && <p>{message}</p>}
</Form> </Form>

View File

@ -1,27 +1,27 @@
import Translation from 'next-translate/Trans' import Translation from "next-translate/Trans"
import { Link } from './design/Link' import { Link } from "./design/Link"
import { TextSpecial } from './design/TextSpecial' import { TextSpecial } from "./design/TextSpecial"
export const About: React.FC = () => { export const About: React.FC = () => {
return ( return (
<section className='text-center mt-6'> <section className="text-center mt-6">
<h1 className='text-4xl'>{'<Form />'}</h1> <h1 className="text-4xl">{"<Form />"}</h1>
<h2 className='text-xl dark:text-gray-300 text-gray-600 mt-4'> <h2 className="text-xl dark:text-gray-300 text-gray-600 mt-4">
npm install --save{' '} npm install --save{" "}
<Link <Link
href='https://www.npmjs.com/package/react-component-form' href="https://www.npmjs.com/package/react-component-form"
target='_blank' target="_blank"
rel='noopener noreferrer' rel="noopener noreferrer"
> >
react-component-form react-component-form
</Link> </Link>
</h2> </h2>
<p className='max-w-lg mt-6 text-base' data-cy='main-description'> <p className="max-w-lg mt-6 text-base" data-cy="main-description">
<Translation <Translation
i18nKey='common:about' i18nKey="common:about"
components={[<TextSpecial key='special' />]} components={[<TextSpecial key="special" />]}
/> />
</p> </p>
</section> </section>

View File

@ -1,14 +1,14 @@
'use client' "use client"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { Input } from './design/Input' import { Input } from "./design/Input"
import { Button } from './design/Button' import { Button } from "./design/Button"
import { useFormTranslation } from '../hooks/useFormTranslation' import { useFormTranslation } from "../hooks/useFormTranslation"
import { userSchema } from '../models/User' import { userSchema } from "../models/User"
import { FormState } from './design/FormState' import { FormState } from "./design/FormState"
const fakeServerRequest = async (ms: number): Promise<void> => { const fakeServerRequest = async (ms: number): Promise<void> => {
return await new Promise((resolve) => { return await new Promise((resolve) => {
@ -23,47 +23,47 @@ export const FormExample: React.FC = () => {
const onSubmit: HandleUseFormCallback<typeof userSchema> = async ( const onSubmit: HandleUseFormCallback<typeof userSchema> = async (
formData, formData,
formElement formElement,
) => { ) => {
await fakeServerRequest(2_000) await fakeServerRequest(2_000)
console.log('onSubmit:', formData) console.log("onSubmit:", formData)
formElement.reset() formElement.reset()
return { return {
type: 'success', type: "success",
message: 'common:success-message' message: "common:success-message",
} }
} }
return ( return (
<section> <section>
<Form <Form
className='mt-6 w-[90%] max-w-xs' className="mt-6 w-[90%] max-w-xs"
noValidate noValidate
onSubmit={handleUseForm(onSubmit)} onSubmit={handleUseForm(onSubmit)}
> >
<Input <Input
type='text' type="text"
placeholder={t('common:name')} placeholder={t("common:name")}
name='name' name="name"
label={t('common:name')} label={t("common:name")}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
/> />
<Input <Input
type='text' type="text"
placeholder='Email' placeholder="Email"
name='email' name="email"
label='Email' label="Email"
error={getFirstErrorTranslation(errors.email)} error={getFirstErrorTranslation(errors.email)}
/> />
<Button className='mt-6 w-full' type='submit' data-cy='submit'> <Button className="mt-6 w-full" type="submit" data-cy="submit">
Submit Submit
</Button> </Button>
</Form> </Form>
<FormState <FormState
id='message' id="message"
state={fetchState} state={fetchState}
message={message != null ? t(message) : undefined} message={message != null ? t(message) : undefined}
/> />

View File

@ -1,9 +1,9 @@
import { Language } from './Language' import { Language } from "./Language"
import { SwitchTheme } from './SwitchTheme' import { SwitchTheme } from "./SwitchTheme"
export const Header: React.FC = () => { export const Header: React.FC = () => {
return ( return (
<header className='flex justify-center mt-6'> <header className="flex justify-center mt-6">
<Language /> <Language />
<SwitchTheme /> <SwitchTheme />
</header> </header>

View File

@ -1,15 +1,15 @@
export const Arrow: React.FC = () => { export const Arrow: React.FC = () => {
return ( return (
<svg <svg
width='12' width="12"
height='8' height="8"
viewBox='0 0 12 8' viewBox="0 0 12 8"
fill='none' fill="none"
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
> >
<path <path
className='fill-current text-black dark:text-white' className="fill-current text-black dark:text-white"
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z' d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
/> />
</svg> </svg>
) )

View File

@ -1,4 +1,4 @@
import Image from 'next/image' import Image from "next/image"
export interface LanguageFlagProps { export interface LanguageFlagProps {
language: string language: string
@ -16,7 +16,7 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
src={`/images/languages/${language}.svg`} src={`/images/languages/${language}.svg`}
alt={language} alt={language}
/> />
<p data-cy='language-flag-text' className='mx-2 text-base'> <p data-cy="language-flag-text" className="mx-2 text-base">
{language.toUpperCase()} {language.toUpperCase()}
</p> </p>
</> </>

View File

@ -1,11 +1,11 @@
import { useCallback, useEffect, useState, useRef } from 'react' import { useCallback, useEffect, useState, useRef } from "react"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import setLanguage from 'next-translate/setLanguage' import setLanguage from "next-translate/setLanguage"
import classNames from 'clsx' import classNames from "clsx"
import i18n from '../../../i18n.json' import i18n from "../../../i18n.json"
import { Arrow } from './Arrow' import { Arrow } from "./Arrow"
import { LanguageFlag } from './LanguageFlag' import { LanguageFlag } from "./LanguageFlag"
export const Language: React.FC = () => { export const Language: React.FC = () => {
const { lang: currentLanguage } = useTranslation() const { lang: currentLanguage } = useTranslation()
@ -28,10 +28,10 @@ export const Language: React.FC = () => {
} }
} }
window.document.addEventListener('click', handleClickEvent) window.document.addEventListener("click", handleClickEvent)
return () => { return () => {
return window.removeEventListener('click', handleClickEvent) return window.removeEventListener("click", handleClickEvent)
} }
}, []) }, [])
@ -40,11 +40,11 @@ export const Language: React.FC = () => {
} }
return ( return (
<div className='flex cursor-pointer flex-col items-center justify-center'> <div className="flex cursor-pointer flex-col items-center justify-center">
<div <div
ref={languageClickRef} ref={languageClickRef}
data-cy='language-click' data-cy="language-click"
className='mr-5 flex items-center' className="mr-5 flex items-center"
onClick={handleHiddenMenu} onClick={handleHiddenMenu}
> >
<LanguageFlag language={currentLanguage} /> <LanguageFlag language={currentLanguage} />
@ -52,10 +52,10 @@ export const Language: React.FC = () => {
</div> </div>
<ul <ul
data-cy='languages-list' data-cy="languages-list"
className={classNames( className={classNames(
'absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag', "absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
{ hidden: hiddenMenu } { hidden: hiddenMenu },
)} )}
> >
{i18n.locales.map((language, index) => { {i18n.locales.map((language, index) => {
@ -65,7 +65,7 @@ export const Language: React.FC = () => {
return ( return (
<li <li
key={index} key={index}
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20' className="flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20"
onClick={async () => { onClick={async () => {
await handleLanguage(language) await handleLanguage(language)
}} }}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from "react"
import classNames from 'clsx' import classNames from "clsx"
import { useTheme } from 'next-themes' import { useTheme } from "next-themes"
export const SwitchTheme: React.FC = () => { export const SwitchTheme: React.FC = () => {
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
@ -15,61 +15,61 @@ export const SwitchTheme: React.FC = () => {
} }
const handleClick = (): void => { const handleClick = (): void => {
setTheme(theme === 'dark' ? 'light' : 'dark') setTheme(theme === "dark" ? "light" : "dark")
} }
return ( return (
<div <div
className='flex items-center' className="flex items-center"
data-cy='switch-theme-click' data-cy="switch-theme-click"
onClick={handleClick} onClick={handleClick}
> >
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'> <div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'> <div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
<div <div
data-cy='switch-theme-dark' data-cy="switch-theme-dark"
className={classNames( className={classNames(
'absolute top-0 bottom-0 left-[8px] mt-auto mb-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out', "absolute top-0 bottom-0 left-[8px] mt-auto mb-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out",
{ {
'opacity-100': theme === 'dark', "opacity-100": theme === "dark",
'opacity-0': theme === 'light' "opacity-0": theme === "light",
} },
)} )}
> >
<span className='relative flex h-[10px] w-[10px] items-center justify-center'> <span className="relative flex h-[10px] w-[10px] items-center justify-center">
🌜 🌜
</span> </span>
</div> </div>
<div <div
data-cy='switch-theme-light' data-cy="switch-theme-light"
className={classNames( className={classNames(
'absolute right-[10px] top-0 bottom-0 mt-auto mb-auto h-[10px] w-[10px] leading-[0]', "absolute right-[10px] top-0 bottom-0 mt-auto mb-auto h-[10px] w-[10px] leading-[0]",
{ {
'opacity-100': theme === 'light', "opacity-100": theme === "light",
'opacity-0': theme === 'dark' "opacity-0": theme === "dark",
} },
)} )}
> >
<span className='relative flex h-[10px] w-[10px] items-center justify-center'> <span className="relative flex h-[10px] w-[10px] items-center justify-center">
🌞 🌞
</span> </span>
</div> </div>
</div> </div>
<div <div
className={classNames( className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out', "absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
{ {
'left-[27px]': theme === 'dark', "left-[27px]": theme === "dark",
'left-0': theme === 'light' "left-0": theme === "light",
} },
)} )}
style={{ border: '1px solid #4d4d4d' }} style={{ border: "1px solid #4d4d4d" }}
/> />
<input <input
data-cy='switch-theme-input' data-cy="switch-theme-input"
type='checkbox' type="checkbox"
aria-label='Dark mode toggle' aria-label="Dark mode toggle"
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0' className="absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0"
defaultChecked defaultChecked
/> />
</div> </div>

View File

@ -1 +1 @@
export * from './Header' export * from "./Header"

View File

@ -1,6 +1,6 @@
import classNames from 'clsx' import classNames from "clsx"
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {} export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {}
export const Button: React.FC<ButtonProps> = (props) => { export const Button: React.FC<ButtonProps> = (props) => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
@ -8,8 +8,8 @@ export const Button: React.FC<ButtonProps> = (props) => {
return ( return (
<button <button
className={classNames( className={classNames(
'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 border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400', "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 border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400",
className className,
)} )}
{...rest} {...rest}
> >

View File

@ -1,10 +1,10 @@
import classNames from 'clsx' import classNames from "clsx"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import type { FetchState as FormStateType } from 'react-component-form' import type { FetchState as FormStateType } from "react-component-form"
import { Loader } from './Loader' import { Loader } from "./Loader"
export interface FormStateProps extends React.ComponentPropsWithoutRef<'div'> { export interface FormStateProps extends React.ComponentPropsWithoutRef<"div"> {
state: FormStateType state: FormStateType
message?: string message?: string
id?: string id?: string
@ -14,15 +14,15 @@ export const FormState: React.FC<FormStateProps> = (props) => {
const { state, message, id, ...rest } = props const { state, message, id, ...rest } = props
const { t } = useTranslation() const { t } = useTranslation()
if (state === 'loading') { if (state === "loading") {
return ( return (
<div data-cy='loader' className='mt-8 flex justify-center'> <div data-cy="loader" className="mt-8 flex justify-center">
<Loader /> <Loader />
</div> </div>
) )
} }
if (state === 'idle' || message == null) { if (state === "idle" || message == null) {
return null return null
} }
@ -32,15 +32,15 @@ export const FormState: React.FC<FormStateProps> = (props) => {
{...rest} {...rest}
className={classNames( className={classNames(
props.className, props.className,
'mt-6 flex max-w-xl items-center text-center font-medium', "mt-6 flex max-w-xl items-center text-center font-medium",
{ {
'text-red-800 dark:text-red-400': state === 'error', "text-red-800 dark:text-red-400": state === "error",
'text-green-800 dark:text-green-400': state === 'success' "text-green-800 dark:text-green-400": state === "success",
} },
)} )}
> >
<div className='inline bg-cover font-headline' /> <div className="inline bg-cover font-headline" />
<span id={id} className='pl-2'> <span id={id} className="pl-2">
<b>{t(`common:${state}`)}:</b> {message} <b>{t(`common:${state}`)}:</b> {message}
</span> </span>
</div> </div>

View File

@ -1,8 +1,8 @@
import classNames from 'clsx' import classNames from "clsx"
import { FormState } from './FormState' import { FormState } from "./FormState"
export interface InputProps extends React.ComponentPropsWithRef<'input'> { export interface InputProps extends React.ComponentPropsWithRef<"input"> {
label: string label: string
error?: string error?: string
className?: string className?: string
@ -12,23 +12,23 @@ export const Input: React.FC<InputProps> = (props) => {
const { label, name, className, error, ...rest } = props const { label, name, className, error, ...rest } = props
return ( return (
<div className='flex flex-col'> <div className="flex flex-col">
<div className={classNames('mt-6 mb-2 flex justify-between', className)}> <div className={classNames("mt-6 mb-2 flex justify-between", className)}>
<label className='pl-1' htmlFor={name}> <label className="pl-1" htmlFor={name}>
{label} {label}
</label> </label>
</div> </div>
<div className='relative mt-0'> <div className="relative mt-0">
<input <input
className='h-11 w-full rounded-lg border border-transparent bg-[#f1f1f1] px-3 font-paragraph leading-10 text-[#2a2a2a] caret-green-600 focus:border focus:shadow-green focus:outline-none' className="h-11 w-full rounded-lg border border-transparent bg-[#f1f1f1] px-3 font-paragraph leading-10 text-[#2a2a2a] caret-green-600 focus:border focus:shadow-green focus:outline-none"
{...rest} {...rest}
id={name} id={name}
name={name} name={name}
data-cy={`input-${name ?? 'name'}`} data-cy={`input-${name ?? "name"}`}
/> />
<FormState <FormState
id={`error-${name ?? 'input'}`} id={`error-${name ?? "input"}`}
state={error == null ? 'idle' : 'error'} state={error == null ? "idle" : "error"}
message={error} message={error}
/> />
</div> </div>

View File

@ -1,6 +1,6 @@
import classNames from 'clsx' import classNames from "clsx"
export interface LinkProps extends React.ComponentPropsWithoutRef<'a'> {} export interface LinkProps extends React.ComponentPropsWithoutRef<"a"> {}
export const Link: React.FC<LinkProps> = (props) => { export const Link: React.FC<LinkProps> = (props) => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
@ -8,8 +8,8 @@ export const Link: React.FC<LinkProps> = (props) => {
return ( return (
<a <a
className={classNames( className={classNames(
'text-green-800 hover:underline dark:text-green-400', "text-green-800 hover:underline dark:text-green-400",
className className,
)} )}
{...rest} {...rest}
> >

View File

@ -1,4 +1,4 @@
import styles from './Loader.module.css' import styles from "./Loader.module.css"
export interface LoaderProps { export interface LoaderProps {
width?: number width?: number
@ -12,19 +12,19 @@ export const Loader: React.FC<LoaderProps> = (props) => {
return ( return (
<div className={props.className}> <div className={props.className}>
<div <div
data-cy='progress-spinner' data-cy="progress-spinner"
className='relative my-0 mx-auto before:content-none before:block before:pt-[100%]' className="relative my-0 mx-auto before:content-none before:block before:pt-[100%]"
style={{ width: `${width}px`, height: `${height}px` }} style={{ width: `${width}px`, height: `${height}px` }}
> >
<svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'> <svg className={styles["progressSpinnerSvg"]} viewBox="25 25 50 50">
<circle <circle
className={styles['progressSpinnerCircle']} className={styles["progressSpinnerCircle"]}
cx='50' cx="50"
cy='50' cy="50"
r='20' r="20"
fill='none' fill="none"
strokeWidth='2' strokeWidth="2"
strokeMiterlimit='10' strokeMiterlimit="10"
/> />
</svg> </svg>
</div> </div>

View File

@ -1 +1 @@
export * from './Loader' export * from "./Loader"

View File

@ -1,14 +1,14 @@
import classNames from 'clsx' import classNames from "clsx"
export interface TextSpecialProps export interface TextSpecialProps
extends React.ComponentPropsWithoutRef<'span'> {} extends React.ComponentPropsWithoutRef<"span"> {}
export const TextSpecial: React.FC<TextSpecialProps> = (props) => { export const TextSpecial: React.FC<TextSpecialProps> = (props) => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
return ( return (
<span <span
className={classNames('text-green-800 dark:text-green-400', className)} className={classNames("text-green-800 dark:text-green-400", className)}
{...rest} {...rest}
> >
{children} {children}

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'cypress' import { defineConfig } from "cypress"
export default defineConfig({ export default defineConfig({
fixturesFolder: false, fixturesFolder: false,
@ -6,7 +6,7 @@ export default defineConfig({
downloadsFolder: undefined, downloadsFolder: undefined,
screenshotOnRunFailure: false, screenshotOnRunFailure: false,
e2e: { e2e: {
baseUrl: 'http://127.0.0.1:3000', baseUrl: "http://127.0.0.1:3000",
supportFile: false supportFile: false,
} },
}) })

View File

@ -1,63 +1,63 @@
describe('Form', () => { describe("Form", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/') cy.visit("/")
}) })
it('succeeds, reset input values and display the global success message', () => { it("succeeds, reset input values and display the global success message", () => {
cy.get('[data-cy=input-name]').type('John') cy.get("[data-cy=input-name]").type("John")
cy.get('[data-cy=input-email]').type('john@john.com') cy.get("[data-cy=input-email]").type("john@john.com")
cy.get('#error-name').should('not.exist') cy.get("#error-name").should("not.exist")
cy.get('#error-email').should('not.exist') cy.get("#error-email").should("not.exist")
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click()
cy.get('[data-cy=input-name]').should('have.value', '') cy.get("[data-cy=input-name]").should("have.value", "")
cy.get('[data-cy=input-email]').should('have.value', '') cy.get("[data-cy=input-email]").should("have.value", "")
cy.get('#message').should( cy.get("#message").should(
'have.text', "have.text",
'Success: The form has been submitted.' "Success: The form has been submitted.",
) )
}) })
it('fails with all inputs as required with error messages and update error messages when updating language (translation)', () => { it("fails with all inputs as required with error messages and update error messages when updating language (translation)", () => {
const requiredErrorMessage = { const requiredErrorMessage = {
en: 'Error: Oops, this field is required 🙈.', en: "Error: Oops, this field is required 🙈.",
fr: 'Erreur: Oups, ce champ est obligatoire 🙈.' fr: "Erreur: Oups, ce champ est obligatoire 🙈.",
} }
cy.get('#error-name').should('not.exist') cy.get("#error-name").should("not.exist")
cy.get('#error-email').should('not.exist') cy.get("#error-email").should("not.exist")
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click()
cy.get('#error-name').should('have.text', requiredErrorMessage.en) cy.get("#error-name").should("have.text", requiredErrorMessage.en)
cy.get('#error-email').should('have.text', requiredErrorMessage.en) cy.get("#error-email").should("have.text", requiredErrorMessage.en)
cy.get('[data-cy=language-click]').click() cy.get("[data-cy=language-click]").click()
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click() cy.get("[data-cy=languages-list] > li:first-child").contains("FR").click()
cy.get('#error-name').should('have.text', requiredErrorMessage.fr) cy.get("#error-name").should("have.text", requiredErrorMessage.fr)
cy.get('#error-email').should('have.text', requiredErrorMessage.fr) cy.get("#error-email").should("have.text", requiredErrorMessage.fr)
}) })
it('fails with invalid name (less than 3 characters)', () => { it("fails with invalid name (less than 3 characters)", () => {
cy.get('[data-cy=input-name]').type('a') cy.get("[data-cy=input-name]").type("a")
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click()
cy.get('#error-name').should( cy.get("#error-name").should(
'have.text', "have.text",
'Error: The field must contain at least 3 characters.' "Error: The field must contain at least 3 characters.",
) )
}) })
it('fails with invalid name (more than 10 characters)', () => { it("fails with invalid name (more than 10 characters)", () => {
cy.get('[data-cy=input-name]').type('12345678910aaaa') cy.get("[data-cy=input-name]").type("12345678910aaaa")
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click()
cy.get('#error-name').should( cy.get("#error-name").should(
'have.text', "have.text",
'Error: The field must contain at most 10 characters.' "Error: The field must contain at most 10 characters.",
) )
}) })
it('fails with wrong email format', () => { it("fails with wrong email format", () => {
cy.get('#error-email').should('not.exist') cy.get("#error-email").should("not.exist")
cy.get('[data-cy=input-email]').type('test') cy.get("[data-cy=input-email]").type("test")
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click()
cy.get('#error-email').should( cy.get("#error-email").should(
'have.text', "have.text",
'Error: Mmm… It seems that this email is not valid 🤔.' "Error: Mmm… It seems that this email is not valid 🤔.",
) )
}) })
}) })

View File

@ -1,47 +1,47 @@
describe('Header', () => { describe("Header", () => {
beforeEach(() => cy.visit('/')) beforeEach(() => cy.visit("/"))
describe('Switch theme color (dark/light)', () => { describe("Switch theme color (dark/light)", () => {
it('should switch theme from `dark` (default) to `light`', () => { it("should switch theme from `dark` (default) to `light`", () => {
cy.get('[data-cy=switch-theme-dark]').should('be.visible') cy.get("[data-cy=switch-theme-dark]").should("be.visible")
cy.get('[data-cy=switch-theme-light]').should('not.be.visible') cy.get("[data-cy=switch-theme-light]").should("not.be.visible")
cy.get('body').should( cy.get("body").should(
'not.have.css', "not.have.css",
'background-color', "background-color",
'rgb(255, 255, 255)' "rgb(255, 255, 255)",
) )
cy.get('[data-cy=switch-theme-click]').click() cy.get("[data-cy=switch-theme-click]").click()
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible') cy.get("[data-cy=switch-theme-dark]").should("not.be.visible")
cy.get('[data-cy=switch-theme-light]').should('be.visible') cy.get("[data-cy=switch-theme-light]").should("be.visible")
cy.get('body').should( cy.get("body").should(
'have.css', "have.css",
'background-color', "background-color",
'rgb(255, 255, 255)' "rgb(255, 255, 255)",
) )
}) })
}) })
describe('Switch Language', () => { describe("Switch Language", () => {
it('should switch language from EN (default) to FR', () => { it("should switch language from EN (default) to FR", () => {
cy.get('[data-cy=main-description]').contains('This is an example') cy.get("[data-cy=main-description]").contains("This is an example")
cy.get('[data-cy=language-flag-text]').contains('EN') cy.get("[data-cy=language-flag-text]").contains("EN")
cy.get('[data-cy=languages-list]').should('not.be.visible') cy.get("[data-cy=languages-list]").should("not.be.visible")
cy.get('[data-cy=language-click]').click() cy.get("[data-cy=language-click]").click()
cy.get('[data-cy=languages-list]').should('be.visible') cy.get("[data-cy=languages-list]").should("be.visible")
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click() cy.get("[data-cy=languages-list] > li:first-child").contains("FR").click()
cy.get('[data-cy=languages-list]').should('not.be.visible') cy.get("[data-cy=languages-list]").should("not.be.visible")
cy.get('[data-cy=language-flag-text]').contains('FR') cy.get("[data-cy=language-flag-text]").contains("FR")
cy.get('[data-cy=main-description]').contains('Ceci est un exemple') cy.get("[data-cy=main-description]").contains("Ceci est un exemple")
}) })
it('should close the language list menu when clicking outside', () => { it("should close the language list menu when clicking outside", () => {
cy.get('[data-cy=languages-list]').should('not.be.visible') cy.get("[data-cy=languages-list]").should("not.be.visible")
cy.get('[data-cy=language-click]').click() cy.get("[data-cy=language-click]").click()
cy.get('[data-cy=languages-list]').should('be.visible') cy.get("[data-cy=languages-list]").should("be.visible")
cy.get('[data-cy=main-description]').click() cy.get("[data-cy=main-description]").click()
cy.get('[data-cy=languages-list]').should('not.be.visible') cy.get("[data-cy=languages-list]").should("not.be.visible")
}) })
}) })
}) })

View File

@ -1,45 +1,45 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import type { Error } from 'react-component-form' import type { Error } from "react-component-form"
const knownErrorKeywords = ['minLength', 'maxLength', 'format'] const knownErrorKeywords = ["minLength", "maxLength", "format"]
const getErrorTranslationKey = (error: Error): string => { const getErrorTranslationKey = (error: Error): string => {
if (knownErrorKeywords.includes(error?.keyword)) { if (knownErrorKeywords.includes(error?.keyword)) {
if ( if (
error.keyword === 'minLength' && error.keyword === "minLength" &&
typeof error.data === 'string' && typeof error.data === "string" &&
error.data.length === 0 error.data.length === 0
) { ) {
return 'common:required' return "common:required"
} }
if (error.keyword === 'format') { if (error.keyword === "format") {
if (error.params['format'] === 'email') { if (error.params["format"] === "email") {
return 'common:invalid-email' return "common:invalid-email"
} }
return 'common:invalid' return "common:invalid"
} }
return `common:${error.keyword}` return `common:${error.keyword}`
} }
return 'common:invalid' return "common:invalid"
} }
export const useFormTranslation = () => { export const useFormTranslation = () => {
const { t } = useTranslation() const { t } = useTranslation()
const getErrorTranslation = ( const getErrorTranslation = (
error: Error | undefined error: Error | undefined,
): string | undefined => { ): string | undefined => {
if (error != null) { if (error != null) {
return t(getErrorTranslationKey(error)).replace( return t(getErrorTranslationKey(error)).replace(
'{expected}', "{expected}",
error?.params?.['limit'] error?.params?.["limit"],
) )
} }
return undefined return undefined
} }
const getFirstErrorTranslation = ( const getFirstErrorTranslation = (
errors: Error[] | undefined errors: Error[] | undefined,
): string | undefined => { ): string | undefined => {
if (errors != null) { if (errors != null) {
return getErrorTranslation(errors[0]) return getErrorTranslation(errors[0])

View File

@ -1,9 +1,9 @@
import type { Static } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
export const userSchema = { export const userSchema = {
name: Type.String({ minLength: 3, maxLength: 10 }), name: Type.String({ minLength: 3, maxLength: 10 }),
email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }) email: Type.String({ minLength: 1, maxLength: 254, format: "email" }),
} }
export const userObjectSchema = Type.Object(userSchema) export const userObjectSchema = Type.Object(userSchema)

View File

@ -1,8 +1,8 @@
const nextTranslate = require('next-translate-plugin') const nextTranslate = require("next-translate-plugin")
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true reactStrictMode: true,
} }
module.exports = nextTranslate(nextConfig) module.exports = nextTranslate(nextConfig)

1611
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"" "test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\""
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.29.6", "@sinclair/typebox": "0.31.18",
"clsx": "2.0.0", "clsx": "2.0.0",
"next": "13.2.4", "next": "13.2.4",
"next-themes": "0.2.1", "next-themes": "0.2.1",
@ -21,18 +21,18 @@
"react-dom": "18.2.0" "react-dom": "18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/strictest": "2.0.1", "@tsconfig/strictest": "2.0.2",
"@types/node": "20.4.2", "@types/node": "20.8.7",
"@types/react": "18.2.15", "@types/react": "18.2.31",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.14",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.16",
"cypress": "12.17.1", "cypress": "13.3.2",
"eslint": "8.45.0", "eslint": "8.52.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"next-translate-plugin": "2.0.5", "next-translate-plugin": "2.0.5",
"postcss": "8.4.26", "postcss": "8.4.31",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.1",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"typescript": "5.1.6" "typescript": "5.2.2"
} }
} }

View File

@ -1,11 +1,11 @@
import type { AppType } from 'next/app' import type { AppType } from "next/app"
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from "next-themes"
import '../styles/globals.css' import "../styles/globals.css"
const MyApp: AppType = ({ Component, pageProps }) => { const MyApp: AppType = ({ Component, pageProps }) => {
return ( return (
<ThemeProvider attribute='class' defaultTheme='dark'> <ThemeProvider attribute="class" defaultTheme="dark">
<Component {...pageProps} /> <Component {...pageProps} />
</ThemeProvider> </ThemeProvider>
) )

View File

@ -1,10 +1,10 @@
import { Html, Head, Main, NextScript } from 'next/document' import { Html, Head, Main, NextScript } from "next/document"
const Document: React.FC = () => { const Document: React.FC = () => {
return ( return (
<Html> <Html>
<Head /> <Head />
<body className='bg-white text-black dark:bg-black dark:text-white'> <body className="bg-white text-black dark:bg-black dark:text-white">
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View File

@ -1,21 +1,21 @@
import type { GetStaticProps, NextPage } from 'next' import type { GetStaticProps, NextPage } from "next"
import Head from 'next/head' import Head from "next/head"
import { About } from '../components/About' import { About } from "../components/About"
import { FormExample } from '../components/FormExample' import { FormExample } from "../components/FormExample"
import { Header } from '../components/Header' import { Header } from "../components/Header"
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
<> <>
<Head> <Head>
<title>react-component-form</title> <title>react-component-form</title>
<meta name='description' content='Manage React Forms with ease.' /> <meta name="description" content="Manage React Forms with ease." />
<link rel='icon' href='/favicon.ico' /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<Header /> <Header />
<main className='flex flex-col justify-center items-center mt-4'> <main className="flex flex-col justify-center items-center mt-4">
<About /> <About />
<FormExample /> <FormExample />
</main> </main>

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {},
} },
} }

View File

@ -1,20 +1,20 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const tailwindConfig = { const tailwindConfig = {
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx}', "./pages/**/*.{js,ts,jsx,tsx}",
'./components/**/*.{js,ts,jsx,tsx}' "./components/**/*.{js,ts,jsx,tsx}",
], ],
darkMode: 'class', darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {
black: '#212121', black: "#212121",
success: '#45C85A', success: "#45C85A",
error: '#C84545' error: "#C84545",
} },
} },
}, },
plugins: [] plugins: [],
} }
module.exports = tailwindConfig module.exports = tailwindConfig

5898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,39 +41,39 @@
"react": ">=18.2.0" "react": ">=18.2.0"
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.29.6", "@sinclair/typebox": "0.31.18",
"ajv": "8.12.0", "ajv": "8.12.0",
"ajv-formats": "2.1.1" "ajv-formats": "2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.6.6", "@commitlint/cli": "18.0.0",
"@commitlint/config-conventional": "17.6.6", "@commitlint/config-conventional": "18.0.0",
"@testing-library/react": "14.0.0", "@testing-library/react": "14.0.0",
"@tsconfig/strictest": "2.0.1", "@tsconfig/strictest": "2.0.2",
"@types/jest": "29.5.3", "@types/jest": "29.5.6",
"@types/react": "18.2.15", "@types/react": "18.2.31",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.14",
"@typescript-eslint/eslint-plugin": "6.1.0", "@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/parser": "6.1.0", "@typescript-eslint/parser": "6.9.0",
"editorconfig-checker": "5.1.1", "editorconfig-checker": "5.1.1",
"esbuild": "0.18.14", "esbuild": "0.19.5",
"esbuild-jest": "0.5.0", "esbuild-jest": "0.5.0",
"eslint": "8.45.0", "eslint": "8.52.0",
"eslint-config-conventions": "11.0.1", "eslint-config-conventions": "12.0.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "5.0.0", "eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "48.0.0", "eslint-plugin-unicorn": "48.0.1",
"jest": "29.6.1", "jest": "29.7.0",
"jest-environment-jsdom": "29.6.1", "jest-environment-jsdom": "29.7.0",
"markdownlint-cli2": "0.8.1", "markdownlint-cli2": "0.10.0",
"markdownlint-rule-relative-links": "2.1.0", "markdownlint-rule-relative-links": "2.1.0",
"prettier": "3.0.0", "prettier": "3.0.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"semantic-release": "21.0.7", "semantic-release": "22.0.5",
"tsup": "7.1.0", "tsup": "7.2.0",
"typescript": "5.1.6" "typescript": "5.2.2"
} }
} }

View File

@ -1,13 +1,13 @@
import React from 'react' import React from "react"
import { render, cleanup, fireEvent } from '@testing-library/react' import { render, cleanup, fireEvent } from "@testing-library/react"
import type { HandleForm } from '..' import type { HandleForm } from ".."
import { Form } from '..' import { Form } from ".."
afterEach(cleanup) afterEach(cleanup)
describe('<Form />', () => { describe("<Form />", () => {
it('should get the formData and formElement onSubmit and onChange', () => { it("should get the formData and formElement onSubmit and onChange", () => {
let formData: { [k: string]: any } = {} let formData: { [k: string]: any } = {}
let formElement: any = null let formElement: any = null
const handleSubmitChange: HandleForm = (data, element) => { const handleSubmitChange: HandleForm = (data, element) => {
@ -16,27 +16,27 @@ describe('<Form />', () => {
} }
const formComponent = render( const formComponent = render(
<Form onSubmit={handleSubmitChange} onChange={handleSubmitChange}> <Form onSubmit={handleSubmitChange} onChange={handleSubmitChange}>
<input data-testid='input-form' type='text' name='inputName' /> <input data-testid="input-form" type="text" name="inputName" />
<button data-testid='button-submit' type='submit'> <button data-testid="button-submit" type="submit">
Submit Submit
</button> </button>
</Form> </Form>,
) )
const inputForm = formComponent.getByTestId( const inputForm = formComponent.getByTestId(
'input-form' "input-form",
) as HTMLInputElement ) as HTMLInputElement
const buttonSubmit = formComponent.getByTestId('button-submit') const buttonSubmit = formComponent.getByTestId("button-submit")
const text = 'some random text' const text = "some random text"
fireEvent.change(inputForm, { target: { value: text } }) fireEvent.change(inputForm, { target: { value: text } })
expect(formData['inputName']).toEqual(text) expect(formData["inputName"]).toEqual(text)
expect(formElement instanceof HTMLFormElement).toBeTruthy() expect(formElement instanceof HTMLFormElement).toBeTruthy()
formData = {} formData = {}
formElement = null formElement = null
fireEvent.click(buttonSubmit) fireEvent.click(buttonSubmit)
expect(Object.keys(formData).length).toEqual(1) expect(Object.keys(formData).length).toEqual(1)
expect(formData['inputName']).toEqual(text) expect(formData["inputName"]).toEqual(text)
expect(formElement instanceof HTMLFormElement).toBeTruthy() expect(formElement instanceof HTMLFormElement).toBeTruthy()
}) })
}) })

View File

@ -1,4 +1,4 @@
import React, { useRef } from 'react' import React, { useRef } from "react"
export interface FormDataObject { export interface FormDataObject {
[key: string]: FormDataEntryValue [key: string]: FormDataEntryValue
@ -10,11 +10,11 @@ export interface FormDataObject {
*/ */
export type HandleForm = ( export type HandleForm = (
formData: FormDataObject, formData: FormDataObject,
formElement: HTMLFormElement formElement: HTMLFormElement,
) => void | Promise<void> ) => void | Promise<void>
interface ReactFormProps interface ReactFormProps
extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {} extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit" | "onChange"> {}
export interface FormProps extends ReactFormProps { export interface FormProps extends ReactFormProps {
onSubmit?: HandleForm onSubmit?: HandleForm
@ -22,7 +22,7 @@ export interface FormProps extends ReactFormProps {
} }
export const getFormDataObject = ( export const getFormDataObject = (
formElement: HTMLFormElement formElement: HTMLFormElement,
): FormDataObject => { ): FormDataObject => {
return Object.fromEntries<FormDataEntryValue>(new FormData(formElement)) return Object.fromEntries<FormDataEntryValue>(new FormData(formElement))
} }

View File

@ -1,14 +1,14 @@
import { useState } from 'react' import { useState } from "react"
export const fetchState = ['idle', 'loading', 'error', 'success'] as const export const fetchState = ["idle", "loading", "error", "success"] as const
export type FetchState = (typeof fetchState)[number] export type FetchState = (typeof fetchState)[number]
export const useFetchState = ( export const useFetchState = (
initialFetchState: FetchState = 'idle' initialFetchState: FetchState = "idle",
): [ ): [
fetchState: FetchState, fetchState: FetchState,
setFetchState: React.Dispatch<React.SetStateAction<FetchState>> setFetchState: React.Dispatch<React.SetStateAction<FetchState>>,
] => { ] => {
const [fetchState, setFetchState] = useState<FetchState>(initialFetchState) const [fetchState, setFetchState] = useState<FetchState>(initialFetchState)
return [fetchState, setFetchState] return [fetchState, setFetchState]

View File

@ -1,14 +1,14 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from "react"
import type { Static, TObject } from '@sinclair/typebox' import type { Static, TObject } from "@sinclair/typebox"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import type { ErrorObject } from 'ajv' import type { ErrorObject } from "ajv"
import type { HandleForm } from '../components/Form' import type { HandleForm } from "../components/Form"
import type { FetchState } from './useFetchState' import type { FetchState } from "./useFetchState"
import { useFetchState } from './useFetchState' import { useFetchState } from "./useFetchState"
import { ajv } from '../utils/ajv' import { ajv } from "../utils/ajv"
import { handleCheckboxBoolean } from '../utils/handleCheckboxBoolean' import { handleCheckboxBoolean } from "../utils/handleCheckboxBoolean"
import { handleOptionalEmptyStringToNull } from '../utils/handleOptionalEmptyStringToNull' import { handleOptionalEmptyStringToNull } from "../utils/handleOptionalEmptyStringToNull"
export interface Schema { export interface Schema {
[property: string | symbol]: any [property: string | symbol]: any
@ -29,21 +29,21 @@ export type HandleUseFormCallbackResult<K extends Schema> = Message<K> | null
*/ */
export type HandleUseFormCallback<K extends Schema> = ( export type HandleUseFormCallback<K extends Schema> = (
formData: Static<TObject<K>>, formData: Static<TObject<K>>,
formElement: HTMLFormElement formElement: HTMLFormElement,
) => Promise<HandleUseFormCallbackResult<K>> | HandleUseFormCallbackResult<K> ) => Promise<HandleUseFormCallbackResult<K>> | HandleUseFormCallbackResult<K>
export type HandleUseForm<K extends Schema> = ( export type HandleUseForm<K extends Schema> = (
callback?: HandleUseFormCallback<K> callback?: HandleUseFormCallback<K>,
) => HandleForm ) => HandleForm
export interface GlobalMessage { export interface GlobalMessage {
type: 'error' | 'success' type: "error" | "success"
message?: string message?: string
properties?: undefined properties?: undefined
} }
export interface PropertiesMessage<K extends Schema> { export interface PropertiesMessage<K extends Schema> {
type: 'error' type: "error"
message?: string message?: string
properties: { [key in keyof Partial<K>]: string } properties: { [key in keyof Partial<K>]: string }
} }
@ -81,7 +81,7 @@ export interface UseFormResult<K extends Schema> {
} }
export const useForm = <K extends Schema>( export const useForm = <K extends Schema>(
validationSchema: K validationSchema: K,
): UseFormResult<typeof validationSchema> => { ): UseFormResult<typeof validationSchema> => {
const validationSchemaObject = useMemo(() => { const validationSchemaObject = useMemo(() => {
return Type.Object(validationSchema) return Type.Object(validationSchema)
@ -90,7 +90,7 @@ export const useForm = <K extends Schema>(
const [fetchState, setFetchState] = useFetchState() const [fetchState, setFetchState] = useFetchState()
const [message, setMessage] = useState<string | undefined>(undefined) const [message, setMessage] = useState<string | undefined>(undefined)
const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>( const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>(
{} as any {} as any,
) )
const validate = useMemo(() => { const validate = useMemo(() => {
@ -103,12 +103,12 @@ export const useForm = <K extends Schema>(
setMessage(undefined) setMessage(undefined)
formData = handleOptionalEmptyStringToNull( formData = handleOptionalEmptyStringToNull(
formData, formData,
validationSchemaObject.required validationSchemaObject.required,
) )
formData = handleCheckboxBoolean(formData, validationSchemaObject) formData = handleCheckboxBoolean(formData, validationSchemaObject)
const isValid = validate(formData) const isValid = validate(formData)
if (!isValid) { if (!isValid) {
setFetchState('error') setFetchState("error")
const errors: ErrorsObject<typeof validationSchema> = {} as any const errors: ErrorsObject<typeof validationSchema> = {} as any
for (const property in validationSchemaObject.properties) { for (const property in validationSchemaObject.properties) {
const errorsForProperty = validate.errors?.filter((error) => { const errorsForProperty = validate.errors?.filter((error) => {
@ -123,28 +123,28 @@ export const useForm = <K extends Schema>(
} else { } else {
setErrors({} as any) setErrors({} as any)
if (callback != null) { if (callback != null) {
setFetchState('loading') setFetchState("loading")
const message = await callback( const message = await callback(
formData as Static<TObject<typeof validationSchema>>, formData as Static<TObject<typeof validationSchema>>,
formElement formElement,
) )
if (message != null) { if (message != null) {
const { message: messageValue, type, properties } = message const { message: messageValue, type, properties } = message
setMessage(messageValue) setMessage(messageValue)
setFetchState(type) setFetchState(type)
if (type === 'error') { if (type === "error") {
const propertiesErrors: ErrorsObject<typeof validationSchema> = const propertiesErrors: ErrorsObject<typeof validationSchema> =
{} as any {} as any
for (const property in properties) { for (const property in properties) {
propertiesErrors[property] = [ propertiesErrors[property] = [
{ {
keyword: 'message', keyword: "message",
message: properties[property], message: properties[property],
instancePath: `/${property}`, instancePath: `/${property}`,
schemaPath: `#/properties/${property}/message`, schemaPath: `#/properties/${property}/message`,
params: {}, params: {},
data: formData[property] data: formData[property],
} },
] ]
} }
setErrors(propertiesErrors) setErrors(propertiesErrors)
@ -161,6 +161,6 @@ export const useForm = <K extends Schema>(
setFetchState, setFetchState,
message, message,
setMessage, setMessage,
errors errors,
} }
} }

View File

@ -1,4 +1,4 @@
export * from './components/Form' export * from "./components/Form"
export * from './hooks/useFetchState' export * from "./hooks/useFetchState"
export * from './hooks/useForm' export * from "./hooks/useForm"
export * from './utils/ajv' export * from "./utils/ajv"

View File

@ -1,25 +1,25 @@
import addFormats from 'ajv-formats' import addFormats from "ajv-formats"
import Ajv from 'ajv' import Ajv from "ajv"
export const ajv = addFormats( export const ajv = addFormats(
new Ajv({ new Ajv({
allErrors: true, allErrors: true,
verbose: true verbose: true,
}), }),
[ [
'date-time', "date-time",
'time', "time",
'date', "date",
'email', "email",
'hostname', "hostname",
'ipv4', "ipv4",
'ipv6', "ipv6",
'uri', "uri",
'uri-reference', "uri-reference",
'uuid', "uuid",
'uri-template', "uri-template",
'json-pointer', "json-pointer",
'relative-json-pointer', "relative-json-pointer",
'regex' "regex",
] ],
) )

View File

@ -1,15 +1,15 @@
import type { TObject } from '@sinclair/typebox' import type { TObject } from "@sinclair/typebox"
import type { Schema } from '../hooks/useForm' import type { Schema } from "../hooks/useForm"
export const handleCheckboxBoolean = ( export const handleCheckboxBoolean = (
object: Schema, object: Schema,
validateSchemaObject: TObject<Schema> validateSchemaObject: TObject<Schema>,
): Schema => { ): Schema => {
const booleanProperties: string[] = [] const booleanProperties: string[] = []
for (const property in validateSchemaObject.properties) { for (const property in validateSchemaObject.properties) {
const rule = validateSchemaObject.properties[property] const rule = validateSchemaObject.properties[property]
if (rule.type === 'boolean') { if (rule.type === "boolean") {
booleanProperties.push(property) booleanProperties.push(property)
} }
} }
@ -18,7 +18,7 @@ export const handleCheckboxBoolean = (
object[booleanProperty] = object[booleanProperty] =
validateSchemaObject.properties[booleanProperty].default validateSchemaObject.properties[booleanProperty].default
} else { } else {
object[booleanProperty] = object[booleanProperty] === 'on' object[booleanProperty] = object[booleanProperty] === "on"
} }
} }
return object return object

View File

@ -1,19 +1,19 @@
import type { Schema } from '../hooks/useForm' import type { Schema } from "../hooks/useForm"
export const handleOptionalEmptyStringToNull = <K extends Schema>( export const handleOptionalEmptyStringToNull = <K extends Schema>(
object: K, object: K,
required: string[] = [] required: string[] = [],
): K => { ): K => {
return Object.fromEntries( return Object.fromEntries(
Object.entries(object).map(([key, value]) => { Object.entries(object).map(([key, value]) => {
if ( if (
typeof value === 'string' && typeof value === "string" &&
value.length === 0 && value.length === 0 &&
!required.includes(key) !required.includes(key)
) { ) {
return [key, null] return [key, null]
} }
return [key, value] return [key, value]
}) }),
) as K ) as K
} }

View File

@ -1,13 +1,13 @@
import { defineConfig } from 'tsup' import { defineConfig } from "tsup"
export default defineConfig({ export default defineConfig({
entry: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}'], entry: ["src/**/*.{ts,tsx}", "!src/**/*.test.{ts,tsx}"],
sourcemap: false, sourcemap: false,
clean: true, clean: true,
platform: 'browser', platform: "browser",
target: 'esnext', target: "esnext",
format: ['esm'], format: ["esm"],
minify: false, minify: false,
outDir: 'build', outDir: "build",
dts: true dts: true,
}) })