chore: better Prettier config for easier reviews
This commit is contained in:
		
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '🐛 Bug Report' | ||||
| about: 'Report an unexpected problem or unintended behavior.' | ||||
| title: '[Bug]' | ||||
| labels: 'bug' | ||||
| name: "🐛 Bug Report" | ||||
| about: "Report an unexpected problem or unintended behavior." | ||||
| title: "[Bug]" | ||||
| labels: "bug" | ||||
| --- | ||||
|  | ||||
| <!-- | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '📜 Documentation' | ||||
| about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' | ||||
| title: '[Documentation]' | ||||
| labels: 'documentation' | ||||
| name: "📜 Documentation" | ||||
| about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)." | ||||
| title: "[Documentation]" | ||||
| labels: "documentation" | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '✨ Feature Request' | ||||
| about: 'Suggest a new feature idea.' | ||||
| title: '[Feature]' | ||||
| labels: 'feature request' | ||||
| name: "✨ Feature Request" | ||||
| about: "Suggest a new feature idea." | ||||
| title: "[Feature]" | ||||
| labels: "feature request" | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '🔧 Improvement' | ||||
| about: 'Improve structure/format/performance/refactor/tests of the code.' | ||||
| title: '[Improvement]' | ||||
| labels: 'improvement' | ||||
| name: "🔧 Improvement" | ||||
| about: "Improve structure/format/performance/refactor/tests of the code." | ||||
| title: "[Improvement]" | ||||
| labels: "improvement" | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '🙋 Question' | ||||
| about: 'Further information is requested.' | ||||
| title: '[Question]' | ||||
| labels: 'question' | ||||
| name: "🙋 Question" | ||||
| about: "Further information is requested." | ||||
| title: "[Question]" | ||||
| labels: "question" | ||||
| --- | ||||
|  | ||||
| ### Question | ||||
|   | ||||
							
								
								
									
										26
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: 'Build' | ||||
| name: "Build" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -8,21 +8,21 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v3.5.3' | ||||
|       - uses: "actions/checkout@v3.5.3" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.6.0' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.6.0" | ||||
|         with: | ||||
|           node-version: 'lts/*' | ||||
|           cache: 'npm' | ||||
|           node-version: "lts/*" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Build Package' | ||||
|         run: 'npm run build' | ||||
|       - name: "Build Package" | ||||
|         run: "npm run build" | ||||
|  | ||||
|       - name: 'Build Example' | ||||
|         run: 'cd example && npm clean-install && npm run build' | ||||
|       - name: "Build Example" | ||||
|         run: "cd example && npm clean-install && npm run build" | ||||
|   | ||||
							
								
								
									
										26
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: 'Lint' | ||||
| name: "Lint" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -8,21 +8,21 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v3.5.3' | ||||
|       - uses: "actions/checkout@v3.5.3" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.6.0' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.6.0" | ||||
|         with: | ||||
|           node-version: 'lts/*' | ||||
|           cache: 'npm' | ||||
|           node-version: "lts/*" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - run: 'npm run lint:commit -- --to "${{ github.sha }}"' | ||||
|       - run: 'npm run lint:editorconfig' | ||||
|       - run: 'npm run lint:markdown' | ||||
|       - run: 'npm run lint:eslint' | ||||
|       - run: 'npm run lint:prettier' | ||||
|       - run: "npm run lint:editorconfig" | ||||
|       - run: "npm run lint:markdown" | ||||
|       - run: "npm run lint:eslint" | ||||
|       - run: "npm run lint:prettier" | ||||
|   | ||||
							
								
								
									
										38
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: 'Release' | ||||
| name: "Release" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -6,32 +6,32 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     permissions: | ||||
|       contents: 'write' | ||||
|       issues: 'write' | ||||
|       pull-requests: 'write' | ||||
|       id-token: 'write' | ||||
|       contents: "write" | ||||
|       issues: "write" | ||||
|       pull-requests: "write" | ||||
|       id-token: "write" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v3.5.3' | ||||
|       - uses: "actions/checkout@v3.5.3" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.6.0' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.6.0" | ||||
|         with: | ||||
|           node-version: 'lts/*' | ||||
|           cache: 'npm' | ||||
|           node-version: "lts/*" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Build Package' | ||||
|         run: 'npm run build' | ||||
|       - name: "Build Package" | ||||
|         run: "npm run build" | ||||
|  | ||||
|       - name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies' | ||||
|         run: 'npm audit signatures' | ||||
|       - name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies" | ||||
|         run: "npm audit signatures" | ||||
|  | ||||
|       - name: 'Release' | ||||
|         run: 'npm run release' | ||||
|       - name: "Release" | ||||
|         run: "npm run release" | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|           NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||
|   | ||||
							
								
								
									
										50
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										50
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: 'Test' | ||||
| name: "Test" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -8,41 +8,41 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v3.5.3' | ||||
|       - uses: "actions/checkout@v3.5.3" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.6.0' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.6.0" | ||||
|         with: | ||||
|           node-version: 'lts/*' | ||||
|           cache: 'npm' | ||||
|           node-version: "lts/*" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Test' | ||||
|         run: 'npm run test' | ||||
|       - name: "Test" | ||||
|         run: "npm run test" | ||||
|  | ||||
|   test-e2e: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v3.5.3' | ||||
|       - uses: "actions/checkout@v3.5.3" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.6.0' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.6.0" | ||||
|         with: | ||||
|           node-version: 'lts/*' | ||||
|           cache: 'npm' | ||||
|           node-version: "lts/*" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Build Package' | ||||
|         run: 'npm run build' | ||||
|       - name: "Build Package" | ||||
|         run: "npm run build" | ||||
|  | ||||
|       - name: 'Build Example' | ||||
|         run: 'cd example && npm clean-install && npm run build' | ||||
|       - name: "Build Example" | ||||
|         run: "cd example && npm clean-install && npm run build" | ||||
|  | ||||
|       - name: 'End To End (e2e) Test Example' | ||||
|         run: 'cd example && npm run test:e2e' | ||||
|       - name: "End To End (e2e) Test Example" | ||||
|         run: "cd example && npm run test:e2e" | ||||
|   | ||||
| @@ -1,6 +1,3 @@ | ||||
| { | ||||
|   "singleQuote": true, | ||||
|   "jsxSingleQuote": true, | ||||
|   "semi": false, | ||||
|   "trailingComma": "none" | ||||
|   "semi": false | ||||
| } | ||||
|   | ||||
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.md
									
									
									
									
									
								
							| @@ -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._ | ||||
|  | ||||
| ```tsx | ||||
| import React from 'react' | ||||
| import { Form } from 'react-component-form' | ||||
| import type { HandleForm } from 'react-component-form' | ||||
| import React from "react" | ||||
| import { Form } from "react-component-form" | ||||
| import type { HandleForm } from "react-component-form" | ||||
|  | ||||
| export const Example = () => { | ||||
|   const handleSubmit: HandleForm = (formData, formElement) => { | ||||
| @@ -49,8 +49,8 @@ export const Example = () => { | ||||
|  | ||||
|   return ( | ||||
|     <Form onSubmit={handleSubmit}> | ||||
|       <input type='text' name='inputName' /> | ||||
|       <button type='submit'>Submit</button> | ||||
|       <input type="text" name="inputName" /> | ||||
|       <button type="submit">Submit</button> | ||||
|     </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. | ||||
|  | ||||
| ```tsx | ||||
| import React from 'react' | ||||
| import { Form, useForm } from 'react-component-form' | ||||
| import type { HandleUseFormCallback } from 'react-component-form' | ||||
| import React from "react" | ||||
| import { Form, useForm } from "react-component-form" | ||||
| import type { HandleUseFormCallback } from "react-component-form" | ||||
|  | ||||
| const schema = { | ||||
|   inputName: { | ||||
|     type: 'string', | ||||
|     type: "string", | ||||
|     minLength: 3, | ||||
|     maxLength: 20 | ||||
|   } | ||||
|     maxLength: 20, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const Example = () => { | ||||
| @@ -87,24 +87,24 @@ export const Example = () => { | ||||
|  | ||||
|   const onSubmit: HandleUseFormCallback<typeof schema> = ( | ||||
|     formData, | ||||
|     formElement | ||||
|     formElement, | ||||
|   ) => { | ||||
|     console.log(formData) // { inputName: 'value of the input validated and type-safe' } | ||||
|     formElement.reset() | ||||
|  | ||||
|     // The return can be either `null` or an object with a global message of type `'error' | 'success'`. | ||||
|     return { | ||||
|       type: 'success', | ||||
|       message: 'Success: Form submitted' | ||||
|       type: "success", | ||||
|       message: "Success: Form submitted", | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Form onSubmit={handleUseForm(onSubmit)}> | ||||
|       <input type='text' name='inputName' /> | ||||
|       <input type="text" name="inputName" /> | ||||
|       {errors.inputName != null && <p>{errors.inputName[0].message}</p>} | ||||
|  | ||||
|       <button type='submit'>Submit</button> | ||||
|       <button type="submit">Submit</button> | ||||
|  | ||||
|       {message != null && <p>{message}</p>} | ||||
|     </Form> | ||||
|   | ||||
| @@ -1,27 +1,27 @@ | ||||
| import Translation from 'next-translate/Trans' | ||||
| import Translation from "next-translate/Trans" | ||||
|  | ||||
| import { Link } from './design/Link' | ||||
| import { TextSpecial } from './design/TextSpecial' | ||||
| import { Link } from "./design/Link" | ||||
| import { TextSpecial } from "./design/TextSpecial" | ||||
|  | ||||
| export const About: React.FC = () => { | ||||
|   return ( | ||||
|     <section className='text-center mt-6'> | ||||
|       <h1 className='text-4xl'>{'<Form />'}</h1> | ||||
|       <h2 className='text-xl dark:text-gray-300 text-gray-600 mt-4'> | ||||
|         npm install --save{' '} | ||||
|     <section className="text-center mt-6"> | ||||
|       <h1 className="text-4xl">{"<Form />"}</h1> | ||||
|       <h2 className="text-xl dark:text-gray-300 text-gray-600 mt-4"> | ||||
|         npm install --save{" "} | ||||
|         <Link | ||||
|           href='https://www.npmjs.com/package/react-component-form' | ||||
|           target='_blank' | ||||
|           rel='noopener noreferrer' | ||||
|           href="https://www.npmjs.com/package/react-component-form" | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|         > | ||||
|           react-component-form | ||||
|         </Link> | ||||
|       </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 | ||||
|           i18nKey='common:about' | ||||
|           components={[<TextSpecial key='special' />]} | ||||
|           i18nKey="common:about" | ||||
|           components={[<TextSpecial key="special" />]} | ||||
|         /> | ||||
|       </p> | ||||
|     </section> | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| 'use client' | ||||
| "use client" | ||||
|  | ||||
| import { Form, useForm } from 'react-component-form' | ||||
| import type { HandleUseFormCallback } from 'react-component-form' | ||||
| import useTranslation from 'next-translate/useTranslation' | ||||
| import { Form, useForm } from "react-component-form" | ||||
| import type { HandleUseFormCallback } from "react-component-form" | ||||
| import useTranslation from "next-translate/useTranslation" | ||||
|  | ||||
| import { Input } from './design/Input' | ||||
| import { Button } from './design/Button' | ||||
| import { useFormTranslation } from '../hooks/useFormTranslation' | ||||
| import { userSchema } from '../models/User' | ||||
| import { FormState } from './design/FormState' | ||||
| import { Input } from "./design/Input" | ||||
| import { Button } from "./design/Button" | ||||
| import { useFormTranslation } from "../hooks/useFormTranslation" | ||||
| import { userSchema } from "../models/User" | ||||
| import { FormState } from "./design/FormState" | ||||
|  | ||||
| const fakeServerRequest = async (ms: number): Promise<void> => { | ||||
|   return await new Promise((resolve) => { | ||||
| @@ -23,47 +23,47 @@ export const FormExample: React.FC = () => { | ||||
|  | ||||
|   const onSubmit: HandleUseFormCallback<typeof userSchema> = async ( | ||||
|     formData, | ||||
|     formElement | ||||
|     formElement, | ||||
|   ) => { | ||||
|     await fakeServerRequest(2_000) | ||||
|     console.log('onSubmit:', formData) | ||||
|     console.log("onSubmit:", formData) | ||||
|     formElement.reset() | ||||
|     return { | ||||
|       type: 'success', | ||||
|       message: 'common:success-message' | ||||
|       type: "success", | ||||
|       message: "common:success-message", | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <section> | ||||
|       <Form | ||||
|         className='mt-6 w-[90%] max-w-xs' | ||||
|         className="mt-6 w-[90%] max-w-xs" | ||||
|         noValidate | ||||
|         onSubmit={handleUseForm(onSubmit)} | ||||
|       > | ||||
|         <Input | ||||
|           type='text' | ||||
|           placeholder={t('common:name')} | ||||
|           name='name' | ||||
|           label={t('common:name')} | ||||
|           type="text" | ||||
|           placeholder={t("common:name")} | ||||
|           name="name" | ||||
|           label={t("common:name")} | ||||
|           error={getFirstErrorTranslation(errors.name)} | ||||
|         /> | ||||
|  | ||||
|         <Input | ||||
|           type='text' | ||||
|           placeholder='Email' | ||||
|           name='email' | ||||
|           label='Email' | ||||
|           type="text" | ||||
|           placeholder="Email" | ||||
|           name="email" | ||||
|           label="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 | ||||
|         </Button> | ||||
|       </Form> | ||||
|  | ||||
|       <FormState | ||||
|         id='message' | ||||
|         id="message" | ||||
|         state={fetchState} | ||||
|         message={message != null ? t(message) : undefined} | ||||
|       /> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { Language } from './Language' | ||||
| import { SwitchTheme } from './SwitchTheme' | ||||
| import { Language } from "./Language" | ||||
| import { SwitchTheme } from "./SwitchTheme" | ||||
|  | ||||
| export const Header: React.FC = () => { | ||||
|   return ( | ||||
|     <header className='flex justify-center mt-6'> | ||||
|     <header className="flex justify-center mt-6"> | ||||
|       <Language /> | ||||
|       <SwitchTheme /> | ||||
|     </header> | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| export const Arrow: React.FC = () => { | ||||
|   return ( | ||||
|     <svg | ||||
|       width='12' | ||||
|       height='8' | ||||
|       viewBox='0 0 12 8' | ||||
|       fill='none' | ||||
|       xmlns='http://www.w3.org/2000/svg' | ||||
|       width="12" | ||||
|       height="8" | ||||
|       viewBox="0 0 12 8" | ||||
|       fill="none" | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|     > | ||||
|       <path | ||||
|         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' | ||||
|         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" | ||||
|       /> | ||||
|     </svg> | ||||
|   ) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Image from 'next/image' | ||||
| import Image from "next/image" | ||||
|  | ||||
| export interface LanguageFlagProps { | ||||
|   language: string | ||||
| @@ -16,7 +16,7 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => { | ||||
|         src={`/images/languages/${language}.svg`} | ||||
|         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()} | ||||
|       </p> | ||||
|     </> | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { useCallback, useEffect, useState, useRef } from 'react' | ||||
| import useTranslation from 'next-translate/useTranslation' | ||||
| import setLanguage from 'next-translate/setLanguage' | ||||
| import classNames from 'clsx' | ||||
| import { useCallback, useEffect, useState, useRef } from "react" | ||||
| import useTranslation from "next-translate/useTranslation" | ||||
| import setLanguage from "next-translate/setLanguage" | ||||
| import classNames from "clsx" | ||||
|  | ||||
| import i18n from '../../../i18n.json' | ||||
| import { Arrow } from './Arrow' | ||||
| import { LanguageFlag } from './LanguageFlag' | ||||
| import i18n from "../../../i18n.json" | ||||
| import { Arrow } from "./Arrow" | ||||
| import { LanguageFlag } from "./LanguageFlag" | ||||
|  | ||||
| export const Language: React.FC = () => { | ||||
|   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 window.removeEventListener('click', handleClickEvent) | ||||
|       return window.removeEventListener("click", handleClickEvent) | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
| @@ -40,11 +40,11 @@ export const Language: React.FC = () => { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className='flex cursor-pointer flex-col items-center justify-center'> | ||||
|     <div className="flex cursor-pointer flex-col items-center justify-center"> | ||||
|       <div | ||||
|         ref={languageClickRef} | ||||
|         data-cy='language-click' | ||||
|         className='mr-5 flex items-center' | ||||
|         data-cy="language-click" | ||||
|         className="mr-5 flex items-center" | ||||
|         onClick={handleHiddenMenu} | ||||
|       > | ||||
|         <LanguageFlag language={currentLanguage} /> | ||||
| @@ -52,10 +52,10 @@ export const Language: React.FC = () => { | ||||
|       </div> | ||||
|  | ||||
|       <ul | ||||
|         data-cy='languages-list' | ||||
|         data-cy="languages-list" | ||||
|         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', | ||||
|           { hidden: hiddenMenu } | ||||
|           "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 }, | ||||
|         )} | ||||
|       > | ||||
|         {i18n.locales.map((language, index) => { | ||||
| @@ -65,7 +65,7 @@ export const Language: React.FC = () => { | ||||
|           return ( | ||||
|             <li | ||||
|               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 () => { | ||||
|                 await handleLanguage(language) | ||||
|               }} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { useEffect, useState } from 'react' | ||||
| import classNames from 'clsx' | ||||
| import { useTheme } from 'next-themes' | ||||
| import { useEffect, useState } from "react" | ||||
| import classNames from "clsx" | ||||
| import { useTheme } from "next-themes" | ||||
|  | ||||
| export const SwitchTheme: React.FC = () => { | ||||
|   const [mounted, setMounted] = useState(false) | ||||
| @@ -15,61 +15,61 @@ export const SwitchTheme: React.FC = () => { | ||||
|   } | ||||
|  | ||||
|   const handleClick = (): void => { | ||||
|     setTheme(theme === 'dark' ? 'light' : 'dark') | ||||
|     setTheme(theme === "dark" ? "light" : "dark") | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className='flex items-center' | ||||
|       data-cy='switch-theme-click' | ||||
|       className="flex items-center" | ||||
|       data-cy="switch-theme-click" | ||||
|       onClick={handleClick} | ||||
|     > | ||||
|       <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="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 | ||||
|             data-cy='switch-theme-dark' | ||||
|             data-cy="switch-theme-dark" | ||||
|             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-0': theme === 'light' | ||||
|               } | ||||
|                 "opacity-100": theme === "dark", | ||||
|                 "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> | ||||
|           </div> | ||||
|           <div | ||||
|             data-cy='switch-theme-light' | ||||
|             data-cy="switch-theme-light" | ||||
|             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-0': theme === 'dark' | ||||
|               } | ||||
|                 "opacity-100": theme === "light", | ||||
|                 "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> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div | ||||
|           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-0': theme === 'light' | ||||
|             } | ||||
|               "left-[27px]": theme === "dark", | ||||
|               "left-0": theme === "light", | ||||
|             }, | ||||
|           )} | ||||
|           style={{ border: '1px solid #4d4d4d' }} | ||||
|           style={{ border: "1px solid #4d4d4d" }} | ||||
|         /> | ||||
|         <input | ||||
|           data-cy='switch-theme-input' | ||||
|           type='checkbox' | ||||
|           aria-label='Dark mode toggle' | ||||
|           className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0' | ||||
|           data-cy="switch-theme-input" | ||||
|           type="checkbox" | ||||
|           aria-label="Dark mode toggle" | ||||
|           className="absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0" | ||||
|           defaultChecked | ||||
|         /> | ||||
|       </div> | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| export * from './Header' | ||||
| export * from "./Header" | ||||
|   | ||||
| @@ -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) => { | ||||
|   const { children, className, ...rest } = props | ||||
| @@ -8,8 +8,8 @@ export const Button: React.FC<ButtonProps> = (props) => { | ||||
|   return ( | ||||
|     <button | ||||
|       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', | ||||
|         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 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, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import classNames from 'clsx' | ||||
| import useTranslation from 'next-translate/useTranslation' | ||||
| import type { FetchState as FormStateType } from 'react-component-form' | ||||
| import classNames from "clsx" | ||||
| import useTranslation from "next-translate/useTranslation" | ||||
| 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 | ||||
|   message?: string | ||||
|   id?: string | ||||
| @@ -14,15 +14,15 @@ export const FormState: React.FC<FormStateProps> = (props) => { | ||||
|   const { state, message, id, ...rest } = props | ||||
|   const { t } = useTranslation() | ||||
|  | ||||
|   if (state === 'loading') { | ||||
|   if (state === "loading") { | ||||
|     return ( | ||||
|       <div data-cy='loader' className='mt-8 flex justify-center'> | ||||
|       <div data-cy="loader" className="mt-8 flex justify-center"> | ||||
|         <Loader /> | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   if (state === 'idle' || message == null) { | ||||
|   if (state === "idle" || message == null) { | ||||
|     return null | ||||
|   } | ||||
|  | ||||
| @@ -32,15 +32,15 @@ export const FormState: React.FC<FormStateProps> = (props) => { | ||||
|         {...rest} | ||||
|         className={classNames( | ||||
|           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-green-800 dark:text-green-400': state === 'success' | ||||
|           } | ||||
|             "text-red-800 dark:text-red-400": state === "error", | ||||
|             "text-green-800 dark:text-green-400": state === "success", | ||||
|           }, | ||||
|         )} | ||||
|       > | ||||
|         <div className='inline bg-cover font-headline' /> | ||||
|         <span id={id} className='pl-2'> | ||||
|         <div className="inline bg-cover font-headline" /> | ||||
|         <span id={id} className="pl-2"> | ||||
|           <b>{t(`common:${state}`)}:</b> {message} | ||||
|         </span> | ||||
|       </div> | ||||
|   | ||||
| @@ -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 | ||||
|   error?: string | ||||
|   className?: string | ||||
| @@ -12,23 +12,23 @@ export const Input: React.FC<InputProps> = (props) => { | ||||
|   const { label, name, className, error, ...rest } = props | ||||
|  | ||||
|   return ( | ||||
|     <div className='flex flex-col'> | ||||
|       <div className={classNames('mt-6 mb-2 flex justify-between', className)}> | ||||
|         <label className='pl-1' htmlFor={name}> | ||||
|     <div className="flex flex-col"> | ||||
|       <div className={classNames("mt-6 mb-2 flex justify-between", className)}> | ||||
|         <label className="pl-1" htmlFor={name}> | ||||
|           {label} | ||||
|         </label> | ||||
|       </div> | ||||
|       <div className='relative mt-0'> | ||||
|       <div className="relative mt-0"> | ||||
|         <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} | ||||
|           id={name} | ||||
|           name={name} | ||||
|           data-cy={`input-${name ?? 'name'}`} | ||||
|           data-cy={`input-${name ?? "name"}`} | ||||
|         /> | ||||
|         <FormState | ||||
|           id={`error-${name ?? 'input'}`} | ||||
|           state={error == null ? 'idle' : 'error'} | ||||
|           id={`error-${name ?? "input"}`} | ||||
|           state={error == null ? "idle" : "error"} | ||||
|           message={error} | ||||
|         /> | ||||
|       </div> | ||||
|   | ||||
| @@ -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) => { | ||||
|   const { children, className, ...rest } = props | ||||
| @@ -8,8 +8,8 @@ export const Link: React.FC<LinkProps> = (props) => { | ||||
|   return ( | ||||
|     <a | ||||
|       className={classNames( | ||||
|         'text-green-800 hover:underline dark:text-green-400', | ||||
|         className | ||||
|         "text-green-800 hover:underline dark:text-green-400", | ||||
|         className, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import styles from './Loader.module.css' | ||||
| import styles from "./Loader.module.css" | ||||
|  | ||||
| export interface LoaderProps { | ||||
|   width?: number | ||||
| @@ -12,19 +12,19 @@ export const Loader: React.FC<LoaderProps> = (props) => { | ||||
|   return ( | ||||
|     <div className={props.className}> | ||||
|       <div | ||||
|         data-cy='progress-spinner' | ||||
|         className='relative my-0 mx-auto before:content-none before:block before:pt-[100%]' | ||||
|         data-cy="progress-spinner" | ||||
|         className="relative my-0 mx-auto before:content-none before:block before:pt-[100%]" | ||||
|         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 | ||||
|             className={styles['progressSpinnerCircle']} | ||||
|             cx='50' | ||||
|             cy='50' | ||||
|             r='20' | ||||
|             fill='none' | ||||
|             strokeWidth='2' | ||||
|             strokeMiterlimit='10' | ||||
|             className={styles["progressSpinnerCircle"]} | ||||
|             cx="50" | ||||
|             cy="50" | ||||
|             r="20" | ||||
|             fill="none" | ||||
|             strokeWidth="2" | ||||
|             strokeMiterlimit="10" | ||||
|           /> | ||||
|         </svg> | ||||
|       </div> | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| export * from './Loader' | ||||
| export * from "./Loader" | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import classNames from 'clsx' | ||||
| import classNames from "clsx" | ||||
|  | ||||
| export interface TextSpecialProps | ||||
|   extends React.ComponentPropsWithoutRef<'span'> {} | ||||
|   extends React.ComponentPropsWithoutRef<"span"> {} | ||||
|  | ||||
| export const TextSpecial: React.FC<TextSpecialProps> = (props) => { | ||||
|   const { children, className, ...rest } = props | ||||
|  | ||||
|   return ( | ||||
|     <span | ||||
|       className={classNames('text-green-800 dark:text-green-400', className)} | ||||
|       className={classNames("text-green-800 dark:text-green-400", className)} | ||||
|       {...rest} | ||||
|     > | ||||
|       {children} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { defineConfig } from 'cypress' | ||||
| import { defineConfig } from "cypress" | ||||
|  | ||||
| export default defineConfig({ | ||||
|   fixturesFolder: false, | ||||
| @@ -6,7 +6,7 @@ export default defineConfig({ | ||||
|   downloadsFolder: undefined, | ||||
|   screenshotOnRunFailure: false, | ||||
|   e2e: { | ||||
|     baseUrl: 'http://127.0.0.1:3000', | ||||
|     supportFile: false | ||||
|   } | ||||
|     baseUrl: "http://127.0.0.1:3000", | ||||
|     supportFile: false, | ||||
|   }, | ||||
| }) | ||||
|   | ||||
| @@ -1,63 +1,63 @@ | ||||
| describe('Form', () => { | ||||
| describe("Form", () => { | ||||
|   beforeEach(() => { | ||||
|     cy.visit('/') | ||||
|     cy.visit("/") | ||||
|   }) | ||||
|  | ||||
|   it('succeeds, reset input values and display the global success message', () => { | ||||
|     cy.get('[data-cy=input-name]').type('John') | ||||
|     cy.get('[data-cy=input-email]').type('john@john.com') | ||||
|     cy.get('#error-name').should('not.exist') | ||||
|     cy.get('#error-email').should('not.exist') | ||||
|     cy.get('[data-cy=submit]').click() | ||||
|     cy.get('[data-cy=input-name]').should('have.value', '') | ||||
|     cy.get('[data-cy=input-email]').should('have.value', '') | ||||
|     cy.get('#message').should( | ||||
|       'have.text', | ||||
|       'Success: The form has been submitted.' | ||||
|   it("succeeds, reset input values and display the global success message", () => { | ||||
|     cy.get("[data-cy=input-name]").type("John") | ||||
|     cy.get("[data-cy=input-email]").type("john@john.com") | ||||
|     cy.get("#error-name").should("not.exist") | ||||
|     cy.get("#error-email").should("not.exist") | ||||
|     cy.get("[data-cy=submit]").click() | ||||
|     cy.get("[data-cy=input-name]").should("have.value", "") | ||||
|     cy.get("[data-cy=input-email]").should("have.value", "") | ||||
|     cy.get("#message").should( | ||||
|       "have.text", | ||||
|       "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 = { | ||||
|       en: 'Error: Oops, this field is required 🙈.', | ||||
|       fr: 'Erreur: Oups, ce champ est obligatoire 🙈.' | ||||
|       en: "Error: Oops, this field is required 🙈.", | ||||
|       fr: "Erreur: Oups, ce champ est obligatoire 🙈.", | ||||
|     } | ||||
|     cy.get('#error-name').should('not.exist') | ||||
|     cy.get('#error-email').should('not.exist') | ||||
|     cy.get('[data-cy=submit]').click() | ||||
|     cy.get('#error-name').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=languages-list] > li:first-child').contains('FR').click() | ||||
|     cy.get('#error-name').should('have.text', requiredErrorMessage.fr) | ||||
|     cy.get('#error-email').should('have.text', requiredErrorMessage.fr) | ||||
|     cy.get("#error-name").should("not.exist") | ||||
|     cy.get("#error-email").should("not.exist") | ||||
|     cy.get("[data-cy=submit]").click() | ||||
|     cy.get("#error-name").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=languages-list] > li:first-child").contains("FR").click() | ||||
|     cy.get("#error-name").should("have.text", requiredErrorMessage.fr) | ||||
|     cy.get("#error-email").should("have.text", requiredErrorMessage.fr) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid name (less than 3 characters)', () => { | ||||
|     cy.get('[data-cy=input-name]').type('a') | ||||
|     cy.get('[data-cy=submit]').click() | ||||
|     cy.get('#error-name').should( | ||||
|       'have.text', | ||||
|       'Error: The field must contain at least 3 characters.' | ||||
|   it("fails with invalid name (less than 3 characters)", () => { | ||||
|     cy.get("[data-cy=input-name]").type("a") | ||||
|     cy.get("[data-cy=submit]").click() | ||||
|     cy.get("#error-name").should( | ||||
|       "have.text", | ||||
|       "Error: The field must contain at least 3 characters.", | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid name (more than 10 characters)', () => { | ||||
|     cy.get('[data-cy=input-name]').type('12345678910aaaa') | ||||
|     cy.get('[data-cy=submit]').click() | ||||
|     cy.get('#error-name').should( | ||||
|       'have.text', | ||||
|       'Error: The field must contain at most 10 characters.' | ||||
|   it("fails with invalid name (more than 10 characters)", () => { | ||||
|     cy.get("[data-cy=input-name]").type("12345678910aaaa") | ||||
|     cy.get("[data-cy=submit]").click() | ||||
|     cy.get("#error-name").should( | ||||
|       "have.text", | ||||
|       "Error: The field must contain at most 10 characters.", | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with wrong email format', () => { | ||||
|     cy.get('#error-email').should('not.exist') | ||||
|     cy.get('[data-cy=input-email]').type('test') | ||||
|     cy.get('[data-cy=submit]').click() | ||||
|     cy.get('#error-email').should( | ||||
|       'have.text', | ||||
|       'Error: Mmm… It seems that this email is not valid 🤔.' | ||||
|   it("fails with wrong email format", () => { | ||||
|     cy.get("#error-email").should("not.exist") | ||||
|     cy.get("[data-cy=input-email]").type("test") | ||||
|     cy.get("[data-cy=submit]").click() | ||||
|     cy.get("#error-email").should( | ||||
|       "have.text", | ||||
|       "Error: Mmm… It seems that this email is not valid 🤔.", | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,47 +1,47 @@ | ||||
| describe('Header', () => { | ||||
|   beforeEach(() => cy.visit('/')) | ||||
| describe("Header", () => { | ||||
|   beforeEach(() => cy.visit("/")) | ||||
|  | ||||
|   describe('Switch theme color (dark/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-light]').should('not.be.visible') | ||||
|       cy.get('body').should( | ||||
|         'not.have.css', | ||||
|         'background-color', | ||||
|         'rgb(255, 255, 255)' | ||||
|   describe("Switch theme color (dark/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-light]").should("not.be.visible") | ||||
|       cy.get("body").should( | ||||
|         "not.have.css", | ||||
|         "background-color", | ||||
|         "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-light]').should('be.visible') | ||||
|       cy.get('body').should( | ||||
|         'have.css', | ||||
|         'background-color', | ||||
|         'rgb(255, 255, 255)' | ||||
|       cy.get("[data-cy=switch-theme-dark]").should("not.be.visible") | ||||
|       cy.get("[data-cy=switch-theme-light]").should("be.visible") | ||||
|       cy.get("body").should( | ||||
|         "have.css", | ||||
|         "background-color", | ||||
|         "rgb(255, 255, 255)", | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   describe('Switch Language', () => { | ||||
|     it('should switch language from EN (default) to FR', () => { | ||||
|       cy.get('[data-cy=main-description]').contains('This is an example') | ||||
|       cy.get('[data-cy=language-flag-text]').contains('EN') | ||||
|       cy.get('[data-cy=languages-list]').should('not.be.visible') | ||||
|       cy.get('[data-cy=language-click]').click() | ||||
|       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]').should('not.be.visible') | ||||
|       cy.get('[data-cy=language-flag-text]').contains('FR') | ||||
|       cy.get('[data-cy=main-description]').contains('Ceci est un exemple') | ||||
|   describe("Switch Language", () => { | ||||
|     it("should switch language from EN (default) to FR", () => { | ||||
|       cy.get("[data-cy=main-description]").contains("This is an example") | ||||
|       cy.get("[data-cy=language-flag-text]").contains("EN") | ||||
|       cy.get("[data-cy=languages-list]").should("not.be.visible") | ||||
|       cy.get("[data-cy=language-click]").click() | ||||
|       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]").should("not.be.visible") | ||||
|       cy.get("[data-cy=language-flag-text]").contains("FR") | ||||
|       cy.get("[data-cy=main-description]").contains("Ceci est un exemple") | ||||
|     }) | ||||
|  | ||||
|     it('should close the language list menu when clicking outside', () => { | ||||
|       cy.get('[data-cy=languages-list]').should('not.be.visible') | ||||
|       cy.get('[data-cy=language-click]').click() | ||||
|       cy.get('[data-cy=languages-list]').should('be.visible') | ||||
|       cy.get('[data-cy=main-description]').click() | ||||
|       cy.get('[data-cy=languages-list]').should('not.be.visible') | ||||
|     it("should close the language list menu when clicking outside", () => { | ||||
|       cy.get("[data-cy=languages-list]").should("not.be.visible") | ||||
|       cy.get("[data-cy=language-click]").click() | ||||
|       cy.get("[data-cy=languages-list]").should("be.visible") | ||||
|       cy.get("[data-cy=main-description]").click() | ||||
|       cy.get("[data-cy=languages-list]").should("not.be.visible") | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,45 +1,45 @@ | ||||
| import useTranslation from 'next-translate/useTranslation' | ||||
| import type { Error } from 'react-component-form' | ||||
| import useTranslation from "next-translate/useTranslation" | ||||
| import type { Error } from "react-component-form" | ||||
|  | ||||
| const knownErrorKeywords = ['minLength', 'maxLength', 'format'] | ||||
| const knownErrorKeywords = ["minLength", "maxLength", "format"] | ||||
|  | ||||
| const getErrorTranslationKey = (error: Error): string => { | ||||
|   if (knownErrorKeywords.includes(error?.keyword)) { | ||||
|     if ( | ||||
|       error.keyword === 'minLength' && | ||||
|       typeof error.data === 'string' && | ||||
|       error.keyword === "minLength" && | ||||
|       typeof error.data === "string" && | ||||
|       error.data.length === 0 | ||||
|     ) { | ||||
|       return 'common:required' | ||||
|       return "common:required" | ||||
|     } | ||||
|     if (error.keyword === 'format') { | ||||
|       if (error.params['format'] === 'email') { | ||||
|         return 'common:invalid-email' | ||||
|     if (error.keyword === "format") { | ||||
|       if (error.params["format"] === "email") { | ||||
|         return "common:invalid-email" | ||||
|       } | ||||
|       return 'common:invalid' | ||||
|       return "common:invalid" | ||||
|     } | ||||
|     return `common:${error.keyword}` | ||||
|   } | ||||
|   return 'common:invalid' | ||||
|   return "common:invalid" | ||||
| } | ||||
|  | ||||
| export const useFormTranslation = () => { | ||||
|   const { t } = useTranslation() | ||||
|  | ||||
|   const getErrorTranslation = ( | ||||
|     error: Error | undefined | ||||
|     error: Error | undefined, | ||||
|   ): string | undefined => { | ||||
|     if (error != null) { | ||||
|       return t(getErrorTranslationKey(error)).replace( | ||||
|         '{expected}', | ||||
|         error?.params?.['limit'] | ||||
|         "{expected}", | ||||
|         error?.params?.["limit"], | ||||
|       ) | ||||
|     } | ||||
|     return undefined | ||||
|   } | ||||
|  | ||||
|   const getFirstErrorTranslation = ( | ||||
|     errors: Error[] | undefined | ||||
|     errors: Error[] | undefined, | ||||
|   ): string | undefined => { | ||||
|     if (errors != null) { | ||||
|       return getErrorTranslation(errors[0]) | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import type { Static } from '@sinclair/typebox' | ||||
| import { Type } from '@sinclair/typebox' | ||||
| import type { Static } from "@sinclair/typebox" | ||||
| import { Type } from "@sinclair/typebox" | ||||
|  | ||||
| export const userSchema = { | ||||
|   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) | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| const nextTranslate = require('next-translate-plugin') | ||||
| const nextTranslate = require("next-translate-plugin") | ||||
|  | ||||
| /** @type {import('next').NextConfig} */ | ||||
| const nextConfig = { | ||||
|   reactStrictMode: true | ||||
|   reactStrictMode: true, | ||||
| } | ||||
|  | ||||
| module.exports = nextTranslate(nextConfig) | ||||
|   | ||||
							
								
								
									
										1611
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1611
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -11,7 +11,7 @@ | ||||
|     "test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@sinclair/typebox": "0.29.6", | ||||
|     "@sinclair/typebox": "0.31.18", | ||||
|     "clsx": "2.0.0", | ||||
|     "next": "13.2.4", | ||||
|     "next-themes": "0.2.1", | ||||
| @@ -21,18 +21,18 @@ | ||||
|     "react-dom": "18.2.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tsconfig/strictest": "2.0.1", | ||||
|     "@types/node": "20.4.2", | ||||
|     "@types/react": "18.2.15", | ||||
|     "@types/react-dom": "18.2.7", | ||||
|     "autoprefixer": "10.4.14", | ||||
|     "cypress": "12.17.1", | ||||
|     "eslint": "8.45.0", | ||||
|     "@tsconfig/strictest": "2.0.2", | ||||
|     "@types/node": "20.8.7", | ||||
|     "@types/react": "18.2.31", | ||||
|     "@types/react-dom": "18.2.14", | ||||
|     "autoprefixer": "10.4.16", | ||||
|     "cypress": "13.3.2", | ||||
|     "eslint": "8.52.0", | ||||
|     "eslint-config-next": "13.2.4", | ||||
|     "next-translate-plugin": "2.0.5", | ||||
|     "postcss": "8.4.26", | ||||
|     "start-server-and-test": "2.0.0", | ||||
|     "postcss": "8.4.31", | ||||
|     "start-server-and-test": "2.0.1", | ||||
|     "tailwindcss": "3.3.3", | ||||
|     "typescript": "5.1.6" | ||||
|     "typescript": "5.2.2" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import type { AppType } from 'next/app' | ||||
| import { ThemeProvider } from 'next-themes' | ||||
| import type { AppType } from "next/app" | ||||
| import { ThemeProvider } from "next-themes" | ||||
|  | ||||
| import '../styles/globals.css' | ||||
| import "../styles/globals.css" | ||||
|  | ||||
| const MyApp: AppType = ({ Component, pageProps }) => { | ||||
|   return ( | ||||
|     <ThemeProvider attribute='class' defaultTheme='dark'> | ||||
|     <ThemeProvider attribute="class" defaultTheme="dark"> | ||||
|       <Component {...pageProps} /> | ||||
|     </ThemeProvider> | ||||
|   ) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { Html, Head, Main, NextScript } from 'next/document' | ||||
| import { Html, Head, Main, NextScript } from "next/document" | ||||
|  | ||||
| const Document: React.FC = () => { | ||||
|   return ( | ||||
|     <Html> | ||||
|       <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 /> | ||||
|         <NextScript /> | ||||
|       </body> | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| import type { GetStaticProps, NextPage } from 'next' | ||||
| import Head from 'next/head' | ||||
| import type { GetStaticProps, NextPage } from "next" | ||||
| import Head from "next/head" | ||||
|  | ||||
| import { About } from '../components/About' | ||||
| import { FormExample } from '../components/FormExample' | ||||
| import { Header } from '../components/Header' | ||||
| import { About } from "../components/About" | ||||
| import { FormExample } from "../components/FormExample" | ||||
| import { Header } from "../components/Header" | ||||
|  | ||||
| const Home: NextPage = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>react-component-form</title> | ||||
|         <meta name='description' content='Manage React Forms with ease.' /> | ||||
|         <link rel='icon' href='/favicon.ico' /> | ||||
|         <meta name="description" content="Manage React Forms with ease." /> | ||||
|         <link rel="icon" href="/favicon.ico" /> | ||||
|       </Head> | ||||
|  | ||||
|       <Header /> | ||||
|       <main className='flex flex-col justify-center items-center mt-4'> | ||||
|       <main className="flex flex-col justify-center items-center mt-4"> | ||||
|         <About /> | ||||
|         <FormExample /> | ||||
|       </main> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {} | ||||
|   } | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
| const tailwindConfig = { | ||||
|   content: [ | ||||
|     './pages/**/*.{js,ts,jsx,tsx}', | ||||
|     './components/**/*.{js,ts,jsx,tsx}' | ||||
|     "./pages/**/*.{js,ts,jsx,tsx}", | ||||
|     "./components/**/*.{js,ts,jsx,tsx}", | ||||
|   ], | ||||
|   darkMode: 'class', | ||||
|   darkMode: "class", | ||||
|   theme: { | ||||
|     extend: { | ||||
|       colors: { | ||||
|         black: '#212121', | ||||
|         success: '#45C85A', | ||||
|         error: '#C84545' | ||||
|       } | ||||
|     } | ||||
|         black: "#212121", | ||||
|         success: "#45C85A", | ||||
|         error: "#C84545", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [] | ||||
|   plugins: [], | ||||
| } | ||||
|  | ||||
| module.exports = tailwindConfig | ||||
|   | ||||
							
								
								
									
										5898
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5898
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								package.json
									
									
									
									
									
								
							| @@ -41,39 +41,39 @@ | ||||
|     "react": ">=18.2.0" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@sinclair/typebox": "0.29.6", | ||||
|     "@sinclair/typebox": "0.31.18", | ||||
|     "ajv": "8.12.0", | ||||
|     "ajv-formats": "2.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "17.6.6", | ||||
|     "@commitlint/config-conventional": "17.6.6", | ||||
|     "@commitlint/cli": "18.0.0", | ||||
|     "@commitlint/config-conventional": "18.0.0", | ||||
|     "@testing-library/react": "14.0.0", | ||||
|     "@tsconfig/strictest": "2.0.1", | ||||
|     "@types/jest": "29.5.3", | ||||
|     "@types/react": "18.2.15", | ||||
|     "@types/react-dom": "18.2.7", | ||||
|     "@typescript-eslint/eslint-plugin": "6.1.0", | ||||
|     "@typescript-eslint/parser": "6.1.0", | ||||
|     "@tsconfig/strictest": "2.0.2", | ||||
|     "@types/jest": "29.5.6", | ||||
|     "@types/react": "18.2.31", | ||||
|     "@types/react-dom": "18.2.14", | ||||
|     "@typescript-eslint/eslint-plugin": "6.9.0", | ||||
|     "@typescript-eslint/parser": "6.9.0", | ||||
|     "editorconfig-checker": "5.1.1", | ||||
|     "esbuild": "0.18.14", | ||||
|     "esbuild": "0.19.5", | ||||
|     "esbuild-jest": "0.5.0", | ||||
|     "eslint": "8.45.0", | ||||
|     "eslint-config-conventions": "11.0.1", | ||||
|     "eslint-config-prettier": "8.8.0", | ||||
|     "eslint-plugin-import": "2.27.5", | ||||
|     "eslint-plugin-prettier": "5.0.0", | ||||
|     "eslint": "8.52.0", | ||||
|     "eslint-config-conventions": "12.0.0", | ||||
|     "eslint-config-prettier": "9.0.0", | ||||
|     "eslint-plugin-import": "2.29.0", | ||||
|     "eslint-plugin-prettier": "5.0.1", | ||||
|     "eslint-plugin-promise": "6.1.1", | ||||
|     "eslint-plugin-unicorn": "48.0.0", | ||||
|     "jest": "29.6.1", | ||||
|     "jest-environment-jsdom": "29.6.1", | ||||
|     "markdownlint-cli2": "0.8.1", | ||||
|     "eslint-plugin-unicorn": "48.0.1", | ||||
|     "jest": "29.7.0", | ||||
|     "jest-environment-jsdom": "29.7.0", | ||||
|     "markdownlint-cli2": "0.10.0", | ||||
|     "markdownlint-rule-relative-links": "2.1.0", | ||||
|     "prettier": "3.0.0", | ||||
|     "prettier": "3.0.3", | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "semantic-release": "21.0.7", | ||||
|     "tsup": "7.1.0", | ||||
|     "typescript": "5.1.6" | ||||
|     "semantic-release": "22.0.5", | ||||
|     "tsup": "7.2.0", | ||||
|     "typescript": "5.2.2" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import React from 'react' | ||||
| import { render, cleanup, fireEvent } from '@testing-library/react' | ||||
| import React from "react" | ||||
| import { render, cleanup, fireEvent } from "@testing-library/react" | ||||
|  | ||||
| import type { HandleForm } from '..' | ||||
| import { Form } from '..' | ||||
| import type { HandleForm } from ".." | ||||
| import { Form } from ".." | ||||
|  | ||||
| afterEach(cleanup) | ||||
|  | ||||
| describe('<Form />', () => { | ||||
|   it('should get the formData and formElement onSubmit and onChange', () => { | ||||
| describe("<Form />", () => { | ||||
|   it("should get the formData and formElement onSubmit and onChange", () => { | ||||
|     let formData: { [k: string]: any } = {} | ||||
|     let formElement: any = null | ||||
|     const handleSubmitChange: HandleForm = (data, element) => { | ||||
| @@ -16,27 +16,27 @@ describe('<Form />', () => { | ||||
|     } | ||||
|     const formComponent = render( | ||||
|       <Form onSubmit={handleSubmitChange} onChange={handleSubmitChange}> | ||||
|         <input data-testid='input-form' type='text' name='inputName' /> | ||||
|         <button data-testid='button-submit' type='submit'> | ||||
|         <input data-testid="input-form" type="text" name="inputName" /> | ||||
|         <button data-testid="button-submit" type="submit"> | ||||
|           Submit | ||||
|         </button> | ||||
|       </Form> | ||||
|       </Form>, | ||||
|     ) | ||||
|     const inputForm = formComponent.getByTestId( | ||||
|       'input-form' | ||||
|       "input-form", | ||||
|     ) as HTMLInputElement | ||||
|     const buttonSubmit = formComponent.getByTestId('button-submit') | ||||
|     const text = 'some random text' | ||||
|     const buttonSubmit = formComponent.getByTestId("button-submit") | ||||
|     const text = "some random text" | ||||
|  | ||||
|     fireEvent.change(inputForm, { target: { value: text } }) | ||||
|     expect(formData['inputName']).toEqual(text) | ||||
|     expect(formData["inputName"]).toEqual(text) | ||||
|     expect(formElement instanceof HTMLFormElement).toBeTruthy() | ||||
|     formData = {} | ||||
|     formElement = null | ||||
|  | ||||
|     fireEvent.click(buttonSubmit) | ||||
|     expect(Object.keys(formData).length).toEqual(1) | ||||
|     expect(formData['inputName']).toEqual(text) | ||||
|     expect(formData["inputName"]).toEqual(text) | ||||
|     expect(formElement instanceof HTMLFormElement).toBeTruthy() | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useRef } from 'react' | ||||
| import React, { useRef } from "react" | ||||
|  | ||||
| export interface FormDataObject { | ||||
|   [key: string]: FormDataEntryValue | ||||
| @@ -10,11 +10,11 @@ export interface FormDataObject { | ||||
|  */ | ||||
| export type HandleForm = ( | ||||
|   formData: FormDataObject, | ||||
|   formElement: HTMLFormElement | ||||
|   formElement: HTMLFormElement, | ||||
| ) => void | Promise<void> | ||||
|  | ||||
| interface ReactFormProps | ||||
|   extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {} | ||||
|   extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit" | "onChange"> {} | ||||
|  | ||||
| export interface FormProps extends ReactFormProps { | ||||
|   onSubmit?: HandleForm | ||||
| @@ -22,7 +22,7 @@ export interface FormProps extends ReactFormProps { | ||||
| } | ||||
|  | ||||
| export const getFormDataObject = ( | ||||
|   formElement: HTMLFormElement | ||||
|   formElement: HTMLFormElement, | ||||
| ): FormDataObject => { | ||||
|   return Object.fromEntries<FormDataEntryValue>(new FormData(formElement)) | ||||
| } | ||||
|   | ||||
| @@ -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 const useFetchState = ( | ||||
|   initialFetchState: FetchState = 'idle' | ||||
|   initialFetchState: FetchState = "idle", | ||||
| ): [ | ||||
|   fetchState: FetchState, | ||||
|   setFetchState: React.Dispatch<React.SetStateAction<FetchState>> | ||||
|   setFetchState: React.Dispatch<React.SetStateAction<FetchState>>, | ||||
| ] => { | ||||
|   const [fetchState, setFetchState] = useState<FetchState>(initialFetchState) | ||||
|   return [fetchState, setFetchState] | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import { useMemo, useState } from 'react' | ||||
| import type { Static, TObject } from '@sinclair/typebox' | ||||
| import { Type } from '@sinclair/typebox' | ||||
| import type { ErrorObject } from 'ajv' | ||||
| import { useMemo, useState } from "react" | ||||
| import type { Static, TObject } from "@sinclair/typebox" | ||||
| import { Type } from "@sinclair/typebox" | ||||
| import type { ErrorObject } from "ajv" | ||||
|  | ||||
| import type { HandleForm } from '../components/Form' | ||||
| import type { FetchState } from './useFetchState' | ||||
| import { useFetchState } from './useFetchState' | ||||
| import { ajv } from '../utils/ajv' | ||||
| import { handleCheckboxBoolean } from '../utils/handleCheckboxBoolean' | ||||
| import { handleOptionalEmptyStringToNull } from '../utils/handleOptionalEmptyStringToNull' | ||||
| import type { HandleForm } from "../components/Form" | ||||
| import type { FetchState } from "./useFetchState" | ||||
| import { useFetchState } from "./useFetchState" | ||||
| import { ajv } from "../utils/ajv" | ||||
| import { handleCheckboxBoolean } from "../utils/handleCheckboxBoolean" | ||||
| import { handleOptionalEmptyStringToNull } from "../utils/handleOptionalEmptyStringToNull" | ||||
|  | ||||
| export interface Schema { | ||||
|   [property: string | symbol]: any | ||||
| @@ -29,21 +29,21 @@ export type HandleUseFormCallbackResult<K extends Schema> = Message<K> | null | ||||
|  */ | ||||
| export type HandleUseFormCallback<K extends Schema> = ( | ||||
|   formData: Static<TObject<K>>, | ||||
|   formElement: HTMLFormElement | ||||
|   formElement: HTMLFormElement, | ||||
| ) => Promise<HandleUseFormCallbackResult<K>> | HandleUseFormCallbackResult<K> | ||||
|  | ||||
| export type HandleUseForm<K extends Schema> = ( | ||||
|   callback?: HandleUseFormCallback<K> | ||||
|   callback?: HandleUseFormCallback<K>, | ||||
| ) => HandleForm | ||||
|  | ||||
| export interface GlobalMessage { | ||||
|   type: 'error' | 'success' | ||||
|   type: "error" | "success" | ||||
|   message?: string | ||||
|   properties?: undefined | ||||
| } | ||||
|  | ||||
| export interface PropertiesMessage<K extends Schema> { | ||||
|   type: 'error' | ||||
|   type: "error" | ||||
|   message?: string | ||||
|   properties: { [key in keyof Partial<K>]: string } | ||||
| } | ||||
| @@ -81,7 +81,7 @@ export interface UseFormResult<K extends Schema> { | ||||
| } | ||||
|  | ||||
| export const useForm = <K extends Schema>( | ||||
|   validationSchema: K | ||||
|   validationSchema: K, | ||||
| ): UseFormResult<typeof validationSchema> => { | ||||
|   const validationSchemaObject = useMemo(() => { | ||||
|     return Type.Object(validationSchema) | ||||
| @@ -90,7 +90,7 @@ export const useForm = <K extends Schema>( | ||||
|   const [fetchState, setFetchState] = useFetchState() | ||||
|   const [message, setMessage] = useState<string | undefined>(undefined) | ||||
|   const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>( | ||||
|     {} as any | ||||
|     {} as any, | ||||
|   ) | ||||
|  | ||||
|   const validate = useMemo(() => { | ||||
| @@ -103,12 +103,12 @@ export const useForm = <K extends Schema>( | ||||
|       setMessage(undefined) | ||||
|       formData = handleOptionalEmptyStringToNull( | ||||
|         formData, | ||||
|         validationSchemaObject.required | ||||
|         validationSchemaObject.required, | ||||
|       ) | ||||
|       formData = handleCheckboxBoolean(formData, validationSchemaObject) | ||||
|       const isValid = validate(formData) | ||||
|       if (!isValid) { | ||||
|         setFetchState('error') | ||||
|         setFetchState("error") | ||||
|         const errors: ErrorsObject<typeof validationSchema> = {} as any | ||||
|         for (const property in validationSchemaObject.properties) { | ||||
|           const errorsForProperty = validate.errors?.filter((error) => { | ||||
| @@ -123,28 +123,28 @@ export const useForm = <K extends Schema>( | ||||
|       } else { | ||||
|         setErrors({} as any) | ||||
|         if (callback != null) { | ||||
|           setFetchState('loading') | ||||
|           setFetchState("loading") | ||||
|           const message = await callback( | ||||
|             formData as Static<TObject<typeof validationSchema>>, | ||||
|             formElement | ||||
|             formElement, | ||||
|           ) | ||||
|           if (message != null) { | ||||
|             const { message: messageValue, type, properties } = message | ||||
|             setMessage(messageValue) | ||||
|             setFetchState(type) | ||||
|             if (type === 'error') { | ||||
|             if (type === "error") { | ||||
|               const propertiesErrors: ErrorsObject<typeof validationSchema> = | ||||
|                 {} as any | ||||
|               for (const property in properties) { | ||||
|                 propertiesErrors[property] = [ | ||||
|                   { | ||||
|                     keyword: 'message', | ||||
|                     keyword: "message", | ||||
|                     message: properties[property], | ||||
|                     instancePath: `/${property}`, | ||||
|                     schemaPath: `#/properties/${property}/message`, | ||||
|                     params: {}, | ||||
|                     data: formData[property] | ||||
|                   } | ||||
|                     data: formData[property], | ||||
|                   }, | ||||
|                 ] | ||||
|               } | ||||
|               setErrors(propertiesErrors) | ||||
| @@ -161,6 +161,6 @@ export const useForm = <K extends Schema>( | ||||
|     setFetchState, | ||||
|     message, | ||||
|     setMessage, | ||||
|     errors | ||||
|     errors, | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| export * from './components/Form' | ||||
| export * from './hooks/useFetchState' | ||||
| export * from './hooks/useForm' | ||||
| export * from './utils/ajv' | ||||
| export * from "./components/Form" | ||||
| export * from "./hooks/useFetchState" | ||||
| export * from "./hooks/useForm" | ||||
| export * from "./utils/ajv" | ||||
|   | ||||
| @@ -1,25 +1,25 @@ | ||||
| import addFormats from 'ajv-formats' | ||||
| import Ajv from 'ajv' | ||||
| import addFormats from "ajv-formats" | ||||
| import Ajv from "ajv" | ||||
|  | ||||
| export const ajv = addFormats( | ||||
|   new Ajv({ | ||||
|     allErrors: true, | ||||
|     verbose: true | ||||
|     verbose: true, | ||||
|   }), | ||||
|   [ | ||||
|     'date-time', | ||||
|     'time', | ||||
|     'date', | ||||
|     'email', | ||||
|     'hostname', | ||||
|     'ipv4', | ||||
|     'ipv6', | ||||
|     'uri', | ||||
|     'uri-reference', | ||||
|     'uuid', | ||||
|     'uri-template', | ||||
|     'json-pointer', | ||||
|     'relative-json-pointer', | ||||
|     'regex' | ||||
|   ] | ||||
|     "date-time", | ||||
|     "time", | ||||
|     "date", | ||||
|     "email", | ||||
|     "hostname", | ||||
|     "ipv4", | ||||
|     "ipv6", | ||||
|     "uri", | ||||
|     "uri-reference", | ||||
|     "uuid", | ||||
|     "uri-template", | ||||
|     "json-pointer", | ||||
|     "relative-json-pointer", | ||||
|     "regex", | ||||
|   ], | ||||
| ) | ||||
|   | ||||
| @@ -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 = ( | ||||
|   object: Schema, | ||||
|   validateSchemaObject: TObject<Schema> | ||||
|   validateSchemaObject: TObject<Schema>, | ||||
| ): Schema => { | ||||
|   const booleanProperties: string[] = [] | ||||
|   for (const property in validateSchemaObject.properties) { | ||||
|     const rule = validateSchemaObject.properties[property] | ||||
|     if (rule.type === 'boolean') { | ||||
|     if (rule.type === "boolean") { | ||||
|       booleanProperties.push(property) | ||||
|     } | ||||
|   } | ||||
| @@ -18,7 +18,7 @@ export const handleCheckboxBoolean = ( | ||||
|       object[booleanProperty] = | ||||
|         validateSchemaObject.properties[booleanProperty].default | ||||
|     } else { | ||||
|       object[booleanProperty] = object[booleanProperty] === 'on' | ||||
|       object[booleanProperty] = object[booleanProperty] === "on" | ||||
|     } | ||||
|   } | ||||
|   return object | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import type { Schema } from '../hooks/useForm' | ||||
| import type { Schema } from "../hooks/useForm" | ||||
|  | ||||
| export const handleOptionalEmptyStringToNull = <K extends Schema>( | ||||
|   object: K, | ||||
|   required: string[] = [] | ||||
|   required: string[] = [], | ||||
| ): K => { | ||||
|   return Object.fromEntries( | ||||
|     Object.entries(object).map(([key, value]) => { | ||||
|       if ( | ||||
|         typeof value === 'string' && | ||||
|         typeof value === "string" && | ||||
|         value.length === 0 && | ||||
|         !required.includes(key) | ||||
|       ) { | ||||
|         return [key, null] | ||||
|       } | ||||
|       return [key, value] | ||||
|     }) | ||||
|     }), | ||||
|   ) as K | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { defineConfig } from 'tsup' | ||||
| import { defineConfig } from "tsup" | ||||
|  | ||||
| export default defineConfig({ | ||||
|   entry: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}'], | ||||
|   entry: ["src/**/*.{ts,tsx}", "!src/**/*.test.{ts,tsx}"], | ||||
|   sourcemap: false, | ||||
|   clean: true, | ||||
|   platform: 'browser', | ||||
|   target: 'esnext', | ||||
|   format: ['esm'], | ||||
|   platform: "browser", | ||||
|   target: "esnext", | ||||
|   format: ["esm"], | ||||
|   minify: false, | ||||
|   outDir: 'build', | ||||
|   dts: true | ||||
|   outDir: "build", | ||||
|   dts: true, | ||||
| }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user