chore: better Prettier config for easier reviews
This commit is contained in:
parent
1224ece116
commit
a49e844c70
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -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"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
@ -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. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -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. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
@ -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. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@ -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
|
||||||
|
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@ -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"
|
||||||
|
26
.github/workflows/lint.yml
vendored
26
.github/workflows/lint.yml
vendored
@ -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"
|
||||||
|
38
.github/workflows/release.yml
vendored
38
.github/workflows/release.yml
vendored
@ -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 }}
|
||||||
|
50
.github/workflows/test.yml
vendored
50
.github/workflows/test.yml
vendored
@ -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"
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"semi": false
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"semi": false,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
}
|
||||||
|
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._
|
_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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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)
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
|
@ -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) => {
|
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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -1 +1 @@
|
|||||||
export * from './Loader'
|
export * from "./Loader"
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
@ -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 🤔.",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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])
|
||||||
|
@ -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)
|
||||||
|
@ -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
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\""
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
5896
package-lock.json
generated
5896
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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user