Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
a37453a115 | |||
fcc2b2ea77 | |||
d213893d5d | |||
1125103c45 | |||
ce884c354d | |||
0819304e1e | |||
50d724eb6a | |||
c979bab553 | |||
52081972e9 | |||
5c49f94b53 | |||
01419426a3 | |||
676a70b1a9 | |||
37b4b9b990 | |||
7e3ef0f492 | |||
c0034d5af6 | |||
694d31e68d | |||
a2edafdc22 | |||
17656c149a | |||
c9bb631073 | |||
8cbe5c3bf2 |
@ -1,9 +1,11 @@
|
|||||||
|
# For more information see: https://editorconfig.org/
|
||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -2,9 +2,9 @@ name: 'Build'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master, develop]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -2,9 +2,9 @@ name: 'Lint'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master, develop]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@ -22,15 +22,6 @@ jobs:
|
|||||||
- name: 'Build Package'
|
- name: 'Build Package'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
|
|
||||||
- name: 'Build Example'
|
|
||||||
run: 'cd example && npm install && npm run build'
|
|
||||||
|
|
||||||
- name: 'Deploy Example'
|
|
||||||
uses: 'JamesIves/github-pages-deploy-action@v4.3.0'
|
|
||||||
with:
|
|
||||||
branch: 'gh-pages'
|
|
||||||
folder: 'example/dist'
|
|
||||||
|
|
||||||
- name: 'Release'
|
- name: 'Release'
|
||||||
run: 'npm run release'
|
run: 'npm run release'
|
||||||
env:
|
env:
|
||||||
|
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
@ -2,9 +2,9 @@ name: 'Test'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master, develop]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@ -23,3 +23,26 @@ jobs:
|
|||||||
|
|
||||||
- name: 'Test'
|
- name: 'Test'
|
||||||
run: 'npm run test'
|
run: 'npm run test'
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v3.1.0'
|
||||||
|
with:
|
||||||
|
node-version: 'lts/*'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Build Package'
|
||||||
|
run: 'npm run build'
|
||||||
|
|
||||||
|
- name: 'Build Example'
|
||||||
|
run: 'cd example && npm install && npm run build'
|
||||||
|
|
||||||
|
- name: 'End To End (e2e) Test Example'
|
||||||
|
run: 'cd example && npm run test:e2e'
|
||||||
|
11
.markdownlint-cli2.jsonc
Normal file
11
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
"MD024": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD041": false
|
||||||
|
},
|
||||||
|
"globs": ["**/*.{md,mdx}"],
|
||||||
|
"ignores": ["**/node_modules"]
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"default": true,
|
|
||||||
"MD013": false,
|
|
||||||
"MD033": false,
|
|
||||||
"MD041": false
|
|
||||||
}
|
|
89
README.md
89
README.md
@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
**react-component-form** is a lightweight form component for [React.js](https://reactjs.org/), it allows you to get the inputs values without state thanks to `onChange` or `onSubmit` props.
|
**react-component-form** is a lightweight form component for [React.js](https://reactjs.org/), it allows you to get the inputs values without state thanks to `onChange` or `onSubmit` props.
|
||||||
|
|
||||||
Demo : [https://divlo.github.io/react-component-form/](https://divlo.github.io/react-component-form/).
|
There is also a [React Hooks](https://reactjs.org/docs/hooks-intro.html) to be used in combination with the `<Form />` component to validate the data with [Ajv JSON schema validator](https://ajv.js.org/), see [advanced usage](#%EF%B8%8F-advanced-usage).
|
||||||
|
|
||||||
|
Example demo: [https://react-component-form.vercel.app/](https://react-component-form.vercel.app/).
|
||||||
|
|
||||||
## 💾 Install
|
## 💾 Install
|
||||||
|
|
||||||
@ -32,11 +34,14 @@ npm install --save react-component-form
|
|||||||
|
|
||||||
## ⚙️ Usage
|
## ⚙️ Usage
|
||||||
|
|
||||||
|
_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, HandleForm } from 'react-component-form'
|
import { Form } from 'react-component-form'
|
||||||
|
import type { HandleForm } from 'react-component-form'
|
||||||
|
|
||||||
const Example = () => {
|
export const Example = () => {
|
||||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
const handleSubmit: HandleForm = (formData, formElement) => {
|
||||||
console.log(formData) // { inputName: 'value of the input' }
|
console.log(formData) // { inputName: 'value of the input' }
|
||||||
formElement.reset()
|
formElement.reset()
|
||||||
@ -51,14 +56,82 @@ const Example = () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
_Note : The example use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._
|
Basically you have access to the same props of the HTML `form` tag in React, but the `onSubmit` and the `onChange` props are differents.
|
||||||
|
|
||||||
Basically you have access to the same props of the HTML `form` tag in React, but the onSubmit and the onChange props are differents.
|
Instead to get the `event` param you get `formData` and `formElement` parameters:
|
||||||
|
|
||||||
Instead to get the `event` param you get `formData` and `formElement` params :
|
- `formData`: Object where the keys are the name of your inputs and the current value. Behind the scene, it uses the [FormData](https://developer.mozilla.org/docs/Web/API/FormData) constructor.
|
||||||
|
- `formElement`: The HTML form element in the DOM so for example you can access the `.reset()` method on a [HTMLFormElement](https://developer.mozilla.org/docs/Web/API/HTMLFormElement).
|
||||||
|
|
||||||
- `formData`: It's an object where the keys are the name of your inputs and the current value. Behind the scene, it uses the [FormData](https://developer.mozilla.org/docs/Web/API/FormData) constructor.
|
## ⚙️ Advanced Usage
|
||||||
- `formElement`: It's the actual HTML form element in the DOM so for example you can access the `.reset()` method on a [HTMLFormElement](https://developer.mozilla.org/docs/Web/API/HTMLFormElement).
|
|
||||||
|
This example shows how to use the `<Form />` component with `useForm` hook to validate the data with [Ajv JSON schema validator](https://ajv.js.org/).
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
inputName: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 3,
|
||||||
|
maxLength: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example = () => {
|
||||||
|
const { handleUseForm, errors, message } = useForm(schema)
|
||||||
|
|
||||||
|
const onSubmit: HandleUseFormCallback<typeof schema> = (
|
||||||
|
formData,
|
||||||
|
formElement
|
||||||
|
) => {
|
||||||
|
console.log(formData) // { inputName: 'value of the input validated' }
|
||||||
|
formElement.reset()
|
||||||
|
|
||||||
|
// The return can be either `null` or an object with a global message of type `'error' | 'success'`.
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
value: 'Success: Form submitted'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleUseForm(onSubmit)}>
|
||||||
|
<input type='text' name='inputName' />
|
||||||
|
{errors.inputName != null && <p>{errors.inputName[0].message}</p>}
|
||||||
|
|
||||||
|
<button type='submit'>Submit</button>
|
||||||
|
|
||||||
|
{message != null && <p>{message}</p>}
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `useForm(schema)`
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
- `schema`: The JSON schema to validate the data (recommended to use [@sinclair/typebox](https://www.npmjs.com/package/@sinclair/typebox)).
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
- `handleUseForm(onSubmit)`: Function to be used with the `onSubmit` or `onChange` prop of the `<Form />` component.
|
||||||
|
- `fetchState = 'idle'`: The current state of the form (`'error' | 'success' | 'idle' | 'loading'`).
|
||||||
|
- `setFetchState`: Function to update the `fetchState`.
|
||||||
|
- `message`: Global message of the form (not specific to a property).
|
||||||
|
- `setMessage`: Function to update the `message`.
|
||||||
|
- `errors`: Object of errors:
|
||||||
|
- Key: correspond to a property in the JSON Schema.
|
||||||
|
- Value: array of [ajv `ErrorObject`](https://ajv.js.org/api.html#error-objects).
|
||||||
|
The array will always have at least one element (never empty) in case of errors.
|
||||||
|
If the value is `undefined`, it means there are no errors for this property.
|
||||||
|
|
||||||
## 💡 Contributing
|
## 💡 Contributing
|
||||||
|
|
||||||
|
3
example/.eslintrc.json
Normal file
3
example/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
39
example/.gitignore
vendored
Normal file
39
example/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
cypress/downloads
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
1
example/.npmrc
Normal file
1
example/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
save-exact=true
|
7
example/README.md
Normal file
7
example/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# example
|
||||||
|
|
||||||
|
This is an example for using `react-component-form` inside a Next.js application with translations thanks to [next-translate](https://www.npmjs.com/package/next-translate).
|
||||||
|
|
||||||
|
The application shows how to use the `<Form />` component with the `useForm` hook to validate and submit a form with a `name` input and an `email` input.
|
||||||
|
|
||||||
|
The interesting code is in [./components/FormExample.tsx](./components/FormExample.tsx).
|
29
example/components/About.tsx
Normal file
29
example/components/About.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Translation from 'next-translate/Trans'
|
||||||
|
|
||||||
|
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{' '}
|
||||||
|
<Link
|
||||||
|
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'>
|
||||||
|
<Translation
|
||||||
|
i18nKey='common:about'
|
||||||
|
components={[<TextSpecial key='special' />]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
70
example/components/FormExample.tsx
Normal file
70
example/components/FormExample.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
const simulateServerRequest = async (ms: number): Promise<void> => {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormExample: React.FC = () => {
|
||||||
|
const { handleUseForm, errors, fetchState, message } = useForm(userSchema)
|
||||||
|
const { getFirstErrorTranslation } = useFormTranslation()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const onSubmit: HandleUseFormCallback<typeof userSchema> = async (
|
||||||
|
formData,
|
||||||
|
formElement
|
||||||
|
) => {
|
||||||
|
await simulateServerRequest(2000)
|
||||||
|
console.log('onSubmit:', formData)
|
||||||
|
formElement.reset()
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
value: 'common:success-message'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Form
|
||||||
|
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')}
|
||||||
|
error={getFirstErrorTranslation(errors.name)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder='Email'
|
||||||
|
name='email'
|
||||||
|
label='Email'
|
||||||
|
error={getFirstErrorTranslation(errors.email)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className='mt-6 w-full' type='submit' data-cy='submit'>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<FormState
|
||||||
|
id='message'
|
||||||
|
state={fetchState}
|
||||||
|
message={message != null ? t(message) : undefined}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
11
example/components/Header/Header.tsx
Normal file
11
example/components/Header/Header.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Language } from './Language'
|
||||||
|
import { SwitchTheme } from './SwitchTheme'
|
||||||
|
|
||||||
|
export const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<header className='flex justify-center mt-6'>
|
||||||
|
<Language />
|
||||||
|
<SwitchTheme />
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
16
example/components/Header/Language/Arrow.tsx
Normal file
16
example/components/Header/Language/Arrow.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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'
|
||||||
|
>
|
||||||
|
<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'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
24
example/components/Header/Language/LanguageFlag.tsx
Normal file
24
example/components/Header/Language/LanguageFlag.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export interface LanguageFlagProps {
|
||||||
|
language: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||||
|
const { language } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
quality={100}
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
src={`/images/languages/${language}.svg`}
|
||||||
|
alt={language}
|
||||||
|
/>
|
||||||
|
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||||
|
{language.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
80
example/components/Header/Language/index.tsx
Normal file
80
example/components/Header/Language/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export const Language: React.FC = () => {
|
||||||
|
const { lang: currentLanguage } = useTranslation()
|
||||||
|
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||||
|
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const handleHiddenMenu = useCallback(() => {
|
||||||
|
setHiddenMenu((oldHiddenMenu) => {
|
||||||
|
return !oldHiddenMenu
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickEvent = (event: MouseEvent): void => {
|
||||||
|
if (languageClickRef.current == null || event.target == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||||
|
setHiddenMenu(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.document.addEventListener('click', handleClickEvent)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return window.removeEventListener('click', handleClickEvent)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLanguage = async (language: string): Promise<void> => {
|
||||||
|
await setLanguage(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||||
|
<div
|
||||||
|
ref={languageClickRef}
|
||||||
|
data-cy='language-click'
|
||||||
|
className='mr-5 flex items-center'
|
||||||
|
onClick={handleHiddenMenu}
|
||||||
|
>
|
||||||
|
<LanguageFlag language={currentLanguage} />
|
||||||
|
<Arrow />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
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 }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n.locales.map((language, index) => {
|
||||||
|
if (language === currentLanguage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||||
|
onClick={async () => {
|
||||||
|
await handleLanguage(language)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LanguageFlag language={language} />
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
126
example/components/Header/SwitchTheme.tsx
Normal file
126
example/components/Header/SwitchTheme.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
|
export const SwitchTheme: React.FC = () => {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (): void => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='flex items-center'
|
||||||
|
data-cy='switch-theme-click'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
|
||||||
|
<div className='toggle-track'>
|
||||||
|
<div
|
||||||
|
data-cy='switch-theme-dark'
|
||||||
|
className='toggle-track-check absolute'
|
||||||
|
>
|
||||||
|
<span className='toggle_Dark relative flex items-center justify-center'>
|
||||||
|
🌜
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-cy='switch-theme-light'
|
||||||
|
className='toggle-track-x absolute'
|
||||||
|
>
|
||||||
|
<span className='toggle_Light relative flex items-center justify-center'>
|
||||||
|
🌞
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='toggle-thumb absolute' />
|
||||||
|
<input
|
||||||
|
data-cy='switch-theme-input'
|
||||||
|
type='checkbox'
|
||||||
|
aria-label='Dark mode toggle'
|
||||||
|
className='toggle-screenreader-only absolute overflow-hidden'
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx>
|
||||||
|
{`
|
||||||
|
.toggle-theme-button {
|
||||||
|
touch-action: pan-x;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.toggle-track {
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.toggle-track-check {
|
||||||
|
width: 14px;
|
||||||
|
height: 10px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
line-height: 0;
|
||||||
|
left: 8px;
|
||||||
|
opacity: ${theme === 'dark' ? 1 : 0};
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
.toggle-track-x {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
line-height: 0;
|
||||||
|
right: 10px;
|
||||||
|
opacity: ${theme === 'dark' ? 0 : 1};
|
||||||
|
}
|
||||||
|
.toggle_Dark,
|
||||||
|
.toggle_Light {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
.toggle-thumb {
|
||||||
|
left: ${theme === 'dark' ? '27px' : '0px'};
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 1px solid #4d4d4d;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fafafa;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
top: 1px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.toggle-screenreader-only {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
1
example/components/Header/index.ts
Normal file
1
example/components/Header/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Header'
|
19
example/components/design/Button.tsx
Normal file
19
example/components/design/Button.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
|
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {}
|
||||||
|
|
||||||
|
export const Button: React.FC<ButtonProps> = (props) => {
|
||||||
|
const { children, className, ...rest } = 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
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
49
example/components/design/FormState.tsx
Normal file
49
example/components/design/FormState.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import type { FetchState as FormStateType } from 'react-component-form'
|
||||||
|
|
||||||
|
import { Loader } from './Loader'
|
||||||
|
|
||||||
|
export interface FormStateProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||||
|
state: FormStateType
|
||||||
|
message?: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormState: React.FC<FormStateProps> = (props) => {
|
||||||
|
const { state, message, id, ...rest } = props
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (state === 'loading') {
|
||||||
|
return (
|
||||||
|
<div data-cy='loader' className='mt-8 flex justify-center'>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'idle' || message == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
className={classNames(
|
||||||
|
props.className,
|
||||||
|
'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'
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='inline bg-cover font-headline' />
|
||||||
|
<span id={id} className='pl-2'>
|
||||||
|
<b>{t(`common:${state}`)}:</b> {message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
37
example/components/design/Input.tsx
Normal file
37
example/components/design/Input.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
|
import { FormState } from './FormState'
|
||||||
|
|
||||||
|
export interface InputProps extends React.ComponentPropsWithRef<'input'> {
|
||||||
|
label: string
|
||||||
|
error?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<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'
|
||||||
|
{...rest}
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
data-cy={`input-${name ?? 'name'}`}
|
||||||
|
/>
|
||||||
|
<FormState
|
||||||
|
id={`error-${name ?? 'input'}`}
|
||||||
|
state={error == null ? 'idle' : 'error'}
|
||||||
|
message={error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
19
example/components/design/Link.tsx
Normal file
19
example/components/design/Link.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
|
export interface LinkProps extends React.ComponentPropsWithoutRef<'a'> {}
|
||||||
|
|
||||||
|
export const Link: React.FC<LinkProps> = (props) => {
|
||||||
|
const { children, className, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
'text-green-800 hover:underline dark:text-green-400',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
81
example/components/design/Loader.tsx
Normal file
81
example/components/design/Loader.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
export interface LoaderProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loader: React.FC<LoaderProps> = (props) => {
|
||||||
|
const { width = 50, height = 50 } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<div data-cy='progress-spinner' className='progress-spinner'>
|
||||||
|
<svg className='progress-spinner-svg' viewBox='25 25 50 50'>
|
||||||
|
<circle
|
||||||
|
className='progress-spinner-circle'
|
||||||
|
cx='50'
|
||||||
|
cy='50'
|
||||||
|
r='20'
|
||||||
|
fill='none'
|
||||||
|
strokeWidth='2'
|
||||||
|
strokeMiterlimit='10'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx>
|
||||||
|
{`
|
||||||
|
.progress-spinner {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: ${width}px;
|
||||||
|
height: ${height}px;
|
||||||
|
}
|
||||||
|
.progress-spinner::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
padding-top: 100%;
|
||||||
|
}
|
||||||
|
.progress-spinner-svg {
|
||||||
|
animation: progress-spinner-rotate 2s linear infinite;
|
||||||
|
height: 100%;
|
||||||
|
transform-origin: center center;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.progress-spinner-circle {
|
||||||
|
stroke-dasharray: 89, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke: #27b05e;
|
||||||
|
animation: progress-spinner-dash 1.5s ease-in-out infinite;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
@keyframes progress-spinner-rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes progress-spinner-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 89, 200;
|
||||||
|
stroke-dashoffset: -35px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 89, 200;
|
||||||
|
stroke-dashoffset: -124px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
17
example/components/design/TextSpecial.tsx
Normal file
17
example/components/design/TextSpecial.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
|
export interface TextSpecialProps
|
||||||
|
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)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
13
example/cypress.config.ts
Normal file
13
example/cypress.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
fixturesFolder: false,
|
||||||
|
video: false,
|
||||||
|
downloadsFolder: undefined,
|
||||||
|
screenshotOnRunFailure: false,
|
||||||
|
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
supportFile: false
|
||||||
|
}
|
||||||
|
})
|
65
example/cypress/e2e/Form.cy.ts
Normal file
65
example/cypress/e2e/Form.cy.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
describe('Form', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('suceeds, 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)', () => {
|
||||||
|
const requiredErrorMessage = {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 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 🤔.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
49
example/cypress/e2e/Header.cy.ts
Normal file
49
example/cypress/e2e/Header.cy.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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)'
|
||||||
|
)
|
||||||
|
|
||||||
|
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)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
Binary file not shown.
Before Width: | Height: | Size: 83 KiB |
1
example/globals.d.ts
vendored
1
example/globals.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
declare module '*.jpg'
|
|
51
example/hooks/useFormTranslation.ts
Normal file
51
example/hooks/useFormTranslation.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import type { Error } from 'react-component-form'
|
||||||
|
|
||||||
|
const knownErrorKeywords = ['minLength', 'maxLength', 'format']
|
||||||
|
|
||||||
|
const getErrorTranslationKey = (error: Error): string => {
|
||||||
|
if (knownErrorKeywords.includes(error?.keyword)) {
|
||||||
|
if (
|
||||||
|
error.keyword === 'minLength' &&
|
||||||
|
typeof error.data === 'string' &&
|
||||||
|
error.data.length === 0
|
||||||
|
) {
|
||||||
|
return 'common:required'
|
||||||
|
}
|
||||||
|
if (error.keyword === 'format') {
|
||||||
|
if (error.params.format === 'email') {
|
||||||
|
return 'common:invalid-email'
|
||||||
|
}
|
||||||
|
return 'common:invalid'
|
||||||
|
}
|
||||||
|
return `common:${error.keyword}`
|
||||||
|
}
|
||||||
|
return 'common:invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFormTranslation = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getErrorTranslation = (
|
||||||
|
error: Error | undefined
|
||||||
|
): string | undefined => {
|
||||||
|
if (error != null) {
|
||||||
|
return t(getErrorTranslationKey(error)).replace(
|
||||||
|
'{expected}',
|
||||||
|
error?.params?.limit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFirstErrorTranslation = (
|
||||||
|
errors: Error[] | undefined
|
||||||
|
): string | undefined => {
|
||||||
|
if (errors != null) {
|
||||||
|
return getErrorTranslation(errors[0])
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getFirstErrorTranslation, getErrorTranslation }
|
||||||
|
}
|
7
example/i18n.json
Normal file
7
example/i18n.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"locales": ["en", "fr"],
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"pages": {
|
||||||
|
"*": ["common"]
|
||||||
|
}
|
||||||
|
}
|
@ -1,112 +0,0 @@
|
|||||||
*,
|
|
||||||
*::after,
|
|
||||||
*::before {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
||||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
|
||||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #212529;
|
|
||||||
text-align: left;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
h5 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.result-container {
|
|
||||||
border-top: 0.2px solid lightgray;
|
|
||||||
border-bottom: 0.2px solid lightgray;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.github-logo {
|
|
||||||
position: fixed;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
.title-install {
|
|
||||||
color: gray;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.form-control {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(1.5em + 0.75rem + 2px);
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #495057;
|
|
||||||
background-color: #fff;
|
|
||||||
background-clip: padding-box;
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #212529;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
|
||||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
.btn-primary:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #0069d9;
|
|
||||||
border-color: #0062cc;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="shortcut icon" href="./github.jpg" type="image/jpg" />
|
|
||||||
<title>react-component-form</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="./index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,67 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import { Form, HandleForm } from 'react-component-form'
|
|
||||||
|
|
||||||
import './index.css'
|
|
||||||
import GitHubLogo from 'url:./github.jpg'
|
|
||||||
|
|
||||||
global.React = React
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
|
||||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
|
||||||
console.clear()
|
|
||||||
console.log('onSubmit: ', formData)
|
|
||||||
formElement.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange: HandleForm = (formData) => {
|
|
||||||
console.log('onChange: ', formData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='container'>
|
|
||||||
<h2>{'<Form />'}</h2>
|
|
||||||
<h5 className='title-install'>npm install --save react-component-form</h5>
|
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit} onChange={handleChange}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label htmlFor='name'>Name :</label>
|
|
||||||
<input
|
|
||||||
className='form-control'
|
|
||||||
type='text'
|
|
||||||
name='name'
|
|
||||||
id='name'
|
|
||||||
placeholder='name'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type='submit' className='btn btn-primary'>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<div className='result-container'>
|
|
||||||
<h4>
|
|
||||||
Try the form and Inspect the console{' '}
|
|
||||||
<span role='img' aria-label='smiley'>
|
|
||||||
😃
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='github-logo'>
|
|
||||||
<a
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
href='https://github.com/Divlo/react-component-form'
|
|
||||||
>
|
|
||||||
<img width='30px' alt='github' src={GitHubLogo} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.getElementById('root') as HTMLElement
|
|
||||||
const root = createRoot(container)
|
|
||||||
root.render(<App />)
|
|
15
example/locales/en/common.json
Normal file
15
example/locales/en/common.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"about": "This is an example of using <0>`react-component-form`</0> inside a Next.js application. The application shows how to use the <0>`<Form />`</0> component with the <0>`useForm`</0> hook to validate and submit a form with a <0>`name`</0> and an <0>`email`</0> input.",
|
||||||
|
"name": "Name",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Success",
|
||||||
|
"success-message": "The form has been submitted.",
|
||||||
|
"page-not-found": "This page could not be found.",
|
||||||
|
"server-error": "Internal Server Error.",
|
||||||
|
"return-to-home-page": "Return to the home page?",
|
||||||
|
"required": "Oops, this field is required 🙈.",
|
||||||
|
"minLength": "The field must contain at least {expected} characters.",
|
||||||
|
"maxLength": "The field must contain at most {expected} characters.",
|
||||||
|
"invalid-email": "Mmm… It seems that this email is not valid 🤔.",
|
||||||
|
"invalid": "Invalid value."
|
||||||
|
}
|
15
example/locales/fr/common.json
Normal file
15
example/locales/fr/common.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"about": "Ceci est un exemple d'utilisation de <0>`react-component-form`</0> dans une application Next.js. L'application montre comment utiliser le composant <0>`<Form />`</0> avec le hook <0>`useForm`</0> hook pour valider et soumettre un formulaire avec un input `name` et `email`.",
|
||||||
|
"name": "Nom",
|
||||||
|
"error": "Erreur",
|
||||||
|
"success": "Succès",
|
||||||
|
"success-message": "Le formulaire a été envoyé.",
|
||||||
|
"page-not-found": "Cette page est introuvable.",
|
||||||
|
"server-error": "Erreur interne du serveur.",
|
||||||
|
"return-to-home-page": "Revenir à la page d'accueil ?",
|
||||||
|
"required": "Oups, ce champ est obligatoire 🙈.",
|
||||||
|
"minLength": "Le champ doit contenir au moins {expected} caractères.",
|
||||||
|
"maxLength": "Le champ doit contenir au plus {expected} caractères.",
|
||||||
|
"invalid-email": "Mmm… Il semblerait que cet email ne soit pas valide 🤔.",
|
||||||
|
"invalid": "Valeur invalide."
|
||||||
|
}
|
10
example/models/User.ts
Normal file
10
example/models/User.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Static, Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
export const userSchema = {
|
||||||
|
name: Type.String({ minLength: 3, maxLength: 10 }),
|
||||||
|
email: Type.String({ minLength: 1, maxLength: 254, format: 'email' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userObjectSchema = Type.Object(userSchema)
|
||||||
|
|
||||||
|
export type User = Static<typeof userObjectSchema>
|
8
example/next.config.js
Executable file
8
example/next.config.js
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
const nextTranslate = require('next-translate')
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextTranslate(nextConfig)
|
11489
example/package-lock.json
generated
11489
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "example",
|
"name": "example",
|
||||||
"type": "module",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "parcel index.html",
|
"dev": "next dev",
|
||||||
"build": "parcel build index.html --public-url \"/react-component-form/\""
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"",
|
||||||
|
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "file:../node_modules/react",
|
"@sinclair/typebox": "0.24.42",
|
||||||
"react-dom": "file:../node_modules/react-dom",
|
"clsx": "1.2.1",
|
||||||
"react-component-form": "file:.."
|
"next": "12.3.1",
|
||||||
|
"next-themes": "0.2.1",
|
||||||
|
"next-translate": "1.6.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-component-form": "file:..",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/transformer-image": "2.4.1",
|
"@types/node": "18.7.18",
|
||||||
"@types/react": "17.0.43",
|
"@types/react": "18.0.20",
|
||||||
"@types/react-dom": "17.0.14",
|
"@types/react-dom": "18.0.6",
|
||||||
"parcel": "2.4.1",
|
"autoprefixer": "10.4.12",
|
||||||
"typescript": "4.6.3"
|
"cypress": "10.8.0",
|
||||||
|
"eslint": "8.23.1",
|
||||||
|
"eslint-config-next": "12.3.1",
|
||||||
|
"postcss": "8.4.16",
|
||||||
|
"start-server-and-test": "1.14.0",
|
||||||
|
"tailwindcss": "3.1.8",
|
||||||
|
"typescript": "4.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
example/pages/_app.tsx
Normal file
14
example/pages/_app.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { AppType } from 'next/app'
|
||||||
|
import { ThemeProvider } from 'next-themes'
|
||||||
|
|
||||||
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute='class' defaultTheme='dark'>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
15
example/pages/_document.tsx
Normal file
15
example/pages/_document.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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'>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Document
|
30
example/pages/index.tsx
Normal file
30
example/pages/index.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
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' />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
<main className='flex flex-col justify-center items-center mt-4'>
|
||||||
|
<About />
|
||||||
|
<FormExample />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticProps: GetStaticProps = async () => {
|
||||||
|
return { props: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
6
example/postcss.config.js
Normal file
6
example/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
BIN
example/public/favicon.ico
Normal file
BIN
example/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
30
example/public/images/languages/en.svg
Normal file
30
example/public/images/languages/en.svg
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M46 4.6C41.3 1.7 35.9 0 30 0V4.6H46Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M30 9.2H51.6C49.9 7.5 48 5.9 46 4.6H30V9.2Z" fill="white"/>
|
||||||
|
<path d="M30 13.8H55.3C54.2 12.1 53 10.6 51.7 9.2H30V13.8Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M30 18.4H57.7C57 16.8 56.2 15.2 55.3 13.8H30V18.4Z" fill="white"/>
|
||||||
|
<path d="M30 23H59.2C58.8 21.4 58.3 19.9 57.7 18.4H30V23Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M30 27.7H59.9C59.8 26.1 59.5 24.6 59.2 23.1H30V27.7Z" fill="white"/>
|
||||||
|
<path d="M59.9 27.7H30V30H0C0 30.8 -9.68575e-08 31.5 0.0999999 32.3H59.9C60 31.5 60 30.8 60 30C60 29.2 60 28.4 59.9 27.7Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M0.800006 36.9H59.2C59.6 35.4 59.8 33.9 59.9 32.3H0.100006C0.200006 33.8 0.400006 35.4 0.800006 36.9Z" fill="white"/>
|
||||||
|
<path d="M2.3 41.5H57.7C58.3 40 58.8 38.5 59.2 36.9H0.800003C1.2 38.5 1.7 40 2.3 41.5Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M4.7 46.1H55.3C56.2 44.6 57 43.1 57.7 41.5H2.3C3 43.1 3.8 44.6 4.7 46.1Z" fill="white"/>
|
||||||
|
<path d="M8.3 50.7H51.7C53 49.3 54.3 47.7 55.3 46.1H4.7C5.7 47.8 7 49.3 8.3 50.7Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M13.9 55.3H46.1C48.2 54 50 52.4 51.7 50.7H8.3C10 52.5 11.9 54 13.9 55.3Z" fill="white"/>
|
||||||
|
<path d="M30 60C35.9 60 41.4 58.3 46.1 55.3H13.9C18.6 58.3 24.1 60 30 60Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M14 4.6C11.9 5.9 10 7.5 8.3 9.2C6.9 10.6 5.7 12.2 4.7 13.8C3.8 15.3 2.9 16.8 2.3 18.4C1.7 19.9 1.2 21.4 0.8 23C0.4 24.5 0.2 26 0.0999999 27.6C-9.68575e-08 28.4 0 29.2 0 30H30V0C24.1 0 18.7 1.7 14 4.6Z" fill="#428BC1"/>
|
||||||
|
<path d="M23 1L23.5 2.5H25L23.8 3.5L24.2 5L23 4.1L21.8 5L22.2 3.5L21 2.5H22.5L23 1Z" fill="white"/>
|
||||||
|
<path d="M27 7L27.5 8.5H29L27.8 9.5L28.2 11L27 10.1L25.8 11L26.2 9.5L25 8.5H26.5L27 7Z" fill="white"/>
|
||||||
|
<path d="M19 7L19.5 8.5H21L19.8 9.5L20.2 11L19 10.1L17.8 11L18.2 9.5L17 8.5H18.5L19 7Z" fill="white"/>
|
||||||
|
<path d="M23 13L23.5 14.5H25L23.8 15.5L24.2 17L23 16.1L21.8 17L22.2 15.5L21 14.5H22.5L23 13Z" fill="white"/>
|
||||||
|
<path d="M15 13L15.5 14.5H17L15.8 15.5L16.2 17L15 16.1L13.8 17L14.2 15.5L13 14.5H14.5L15 13Z" fill="white"/>
|
||||||
|
<path d="M7 13L7.5 14.5H9L7.8 15.5L8.2 17L7 16.1L5.8 17L6.2 15.5L5 14.5H6.5L7 13Z" fill="white"/>
|
||||||
|
<path d="M27 19L27.5 20.5H29L27.8 21.5L28.2 23L27 22.1L25.8 23L26.2 21.5L25 20.5H26.5L27 19Z" fill="white"/>
|
||||||
|
<path d="M19 19L19.5 20.5H21L19.8 21.5L20.2 23L19 22.1L17.8 23L18.2 21.5L17 20.5H18.5L19 19Z" fill="white"/>
|
||||||
|
<path d="M11 19L11.5 20.5H13L11.8 21.5L12.2 23L11 22.1L9.8 23L10.2 21.5L9 20.5H10.5L11 19Z" fill="white"/>
|
||||||
|
<path d="M23 25L23.5 26.5H25L23.8 27.5L24.2 29L23 28.1L21.8 29L22.2 27.5L21 26.5H22.5L23 25Z" fill="white"/>
|
||||||
|
<path d="M15 25L15.5 26.5H17L15.8 27.5L16.2 29L15 28.1L13.8 29L14.2 27.5L13 26.5H14.5L15 25Z" fill="white"/>
|
||||||
|
<path d="M7 25L7.5 26.5H9L7.8 27.5L8.2 29L7 28.1L5.8 29L6.2 27.5L5 26.5H6.5L7 25Z" fill="white"/>
|
||||||
|
<path d="M9.79999 11L11 10.1L12.2 11L11.7 9.5L12.9 8.5H11.4L11 7L10.5 8.5H9.09999L10.3 9.4L9.79999 11Z" fill="white"/>
|
||||||
|
<path d="M1.79999 23L2.99999 22.1L4.19999 23L3.69999 21.5L4.89999 20.5H3.49999L2.99999 19L2.49999 20.5H1.49999C1.49999 20.6 1.39999 20.7 1.39999 20.8L2.19999 21.4L1.79999 23Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
12
example/public/images/languages/fr.svg
Normal file
12
example/public/images/languages/fr.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path d="M0 17.5C0 25.1417 4.9 31.6167 11.6667 34.0084V0.991699C4.9 3.38337 0 9.85837 0 17.5Z" fill="#428BC1"/>
|
||||||
|
<path d="M35 17.5C35 9.85837 30.1584 3.38337 23.3334 0.991699V34.0084C30.1584 31.6167 35 25.1417 35 17.5Z" fill="#ED4C5C"/>
|
||||||
|
<path d="M11.6666 34.0083C13.475 34.65 15.4583 35 17.5 35C19.5416 35 21.525 34.65 23.3333 34.0083V0.991667C21.525 0.35 19.6 0 17.5 0C15.4 0 13.475 0.35 11.6666 0.991667V34.0083Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="35" height="35" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 659 B |
3
example/styles/globals.css
Normal file
3
example/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
18
example/tailwind.config.js
Normal file
18
example/tailwind.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx}'
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
black: '#212121',
|
||||||
|
success: '#45C85A',
|
||||||
|
error: '#C84545'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"jsx": "react",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"sourceMap": true,
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true
|
"types": ["cypress"],
|
||||||
}
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "jsdom",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"setupFilesAfterEnv": ["<rootDir>/__test__/setup.ts"],
|
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.tsx?$": "esbuild-jest"
|
"^.+\\.tsx?$": "esbuild-jest"
|
||||||
}
|
}
|
||||||
|
11591
package-lock.json
generated
11591
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@ -17,7 +17,7 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Divlo/react-component-form/issues"
|
"url": "https://github.com/Divlo/react-component-form/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Divlo/react-component-form#readme",
|
"homepage": "https://react-component-form.vercel.app/",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@ -28,7 +28,7 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"",
|
"lint:markdown": "markdownlint-cli2",
|
||||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||||
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||||
"release": "semantic-release"
|
"release": "semantic-release"
|
||||||
@ -36,32 +36,38 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16"
|
"react": ">=16"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.24.42",
|
||||||
|
"ajv": "8.11.0",
|
||||||
|
"ajv-formats": "2.1.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "16.2.3",
|
"@commitlint/cli": "17.1.2",
|
||||||
"@commitlint/config-conventional": "16.2.1",
|
"@commitlint/config-conventional": "17.1.0",
|
||||||
"@testing-library/react": "13.0.0",
|
"@testing-library/react": "13.4.0",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "29.0.3",
|
||||||
"@types/react": "17.0.43",
|
"@types/react": "18.0.20",
|
||||||
"@types/react-dom": "17.0.14",
|
"@types/react-dom": "18.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.18.0",
|
"@typescript-eslint/eslint-plugin": "5.38.0",
|
||||||
"@typescript-eslint/parser": "5.18.0",
|
"@typescript-eslint/parser": "5.38.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"eslint": "8.12.0",
|
"esbuild": "0.15.8",
|
||||||
"eslint-config-conventions": "2.0.0",
|
"esbuild-jest": "0.5.0",
|
||||||
|
"eslint": "8.23.1",
|
||||||
|
"eslint-config-conventions": "4.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.0.1",
|
||||||
"eslint-plugin-unicorn": "42.0.0",
|
"eslint-plugin-unicorn": "43.0.2",
|
||||||
"esbuild": "0.14.32",
|
"jest": "29.0.3",
|
||||||
"esbuild-jest": "0.5.0",
|
"jest-environment-jsdom": "29.0.3",
|
||||||
"jest": "27.5.1",
|
"markdownlint-cli2": "0.5.1",
|
||||||
"markdownlint-cli": "0.31.1",
|
"prettier": "2.7.1",
|
||||||
"prettier": "2.6.2",
|
"react": "18.2.0",
|
||||||
"react": "18.0.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dom": "18.0.0",
|
"semantic-release": "19.0.5",
|
||||||
"semantic-release": "19.0.2",
|
"tsup": "6.2.3",
|
||||||
"tsup": "5.12.4",
|
"typescript": "4.8.3"
|
||||||
"typescript": "4.6.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
import { render, cleanup, fireEvent } from '@testing-library/react'
|
import { render, cleanup, fireEvent } from '@testing-library/react'
|
||||||
|
|
||||||
import { Form, HandleForm } from '..'
|
import type { HandleForm } from '..'
|
||||||
|
import { Form } from '..'
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
global.React = React
|
|
@ -1,15 +1,19 @@
|
|||||||
import { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
|
|
||||||
export interface FormDataObject {
|
export interface FormDataObject {
|
||||||
[key: string]: FormDataEntryValue
|
[key: string]: FormDataEntryValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param formData Object where the keys are the name of your inputs and the current value.
|
||||||
|
* @param formElement The HTML form element in the DOM.
|
||||||
|
*/
|
||||||
export type HandleForm = (
|
export type HandleForm = (
|
||||||
formData: FormDataObject,
|
formData: FormDataObject,
|
||||||
formElement: HTMLFormElement
|
formElement: HTMLFormElement
|
||||||
) => void | Promise<void>
|
) => void | Promise<void>
|
||||||
|
|
||||||
export 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 {
|
15
src/hooks/useFetchState.ts
Normal file
15
src/hooks/useFetchState.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export const fetchState = ['idle', 'loading', 'error', 'success'] as const
|
||||||
|
|
||||||
|
export type FetchState = typeof fetchState[number]
|
||||||
|
|
||||||
|
export const useFetchState = (
|
||||||
|
initialFetchState: FetchState = 'idle'
|
||||||
|
): [
|
||||||
|
fetchState: FetchState,
|
||||||
|
setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
|
||||||
|
] => {
|
||||||
|
const [fetchState, setFetchState] = useState<FetchState>(initialFetchState)
|
||||||
|
return [fetchState, setFetchState]
|
||||||
|
}
|
166
src/hooks/useForm.ts
Normal file
166
src/hooks/useForm.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export interface Schema {
|
||||||
|
[property: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Error = ErrorObject
|
||||||
|
|
||||||
|
export type ErrorsObject<K extends Schema> = {
|
||||||
|
[key in keyof Partial<K>]: Error[] | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandleUseFormCallbackResult<K extends Schema> = Message<K> | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param formData Object where the keys are the name of your inputs and the current value.
|
||||||
|
* @param formElement The HTML form element in the DOM.
|
||||||
|
* @returns The return can be either `null` or an object with a global message of type `'error' | 'success'`.
|
||||||
|
*/
|
||||||
|
export type HandleUseFormCallback<K extends Schema> = (
|
||||||
|
formData: Static<TObject<K>>,
|
||||||
|
formElement: HTMLFormElement
|
||||||
|
) => Promise<HandleUseFormCallbackResult<K>> | HandleUseFormCallbackResult<K>
|
||||||
|
|
||||||
|
export type HandleUseForm<K extends Schema> = (
|
||||||
|
callback?: HandleUseFormCallback<K>
|
||||||
|
) => HandleForm
|
||||||
|
|
||||||
|
export interface GlobalMessage {
|
||||||
|
type: 'error' | 'success'
|
||||||
|
value?: string
|
||||||
|
properties?: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertiesMessage<K extends Schema> {
|
||||||
|
type: 'error'
|
||||||
|
value?: string
|
||||||
|
properties: { [key in keyof Partial<K>]: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Message<K extends Schema> = GlobalMessage | PropertiesMessage<K>
|
||||||
|
|
||||||
|
export interface UseFormResult<K extends Schema> {
|
||||||
|
/**
|
||||||
|
* Function to be used with the `onSubmit` or `onChange` prop of the `<Form />` component.
|
||||||
|
*/
|
||||||
|
handleUseForm: HandleUseForm<K>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of the form.
|
||||||
|
*/
|
||||||
|
readonly fetchState: FetchState
|
||||||
|
setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global message of the form (not specific to a property).
|
||||||
|
*/
|
||||||
|
readonly message?: string
|
||||||
|
setMessage: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of errors:
|
||||||
|
* - Key: correspond to a property in the JSON Schema.
|
||||||
|
* - Value: array of {@link ErrorObject}.
|
||||||
|
*
|
||||||
|
* The array will always have at least one element (never empty) in case of errors.
|
||||||
|
*
|
||||||
|
* If the value is `undefined`, it means there are no errors for this property.
|
||||||
|
*/
|
||||||
|
readonly errors: ErrorsObject<K>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useForm = <K extends Schema>(
|
||||||
|
validationSchema: K
|
||||||
|
): UseFormResult<typeof validationSchema> => {
|
||||||
|
const validationSchemaObject = useMemo(() => {
|
||||||
|
return Type.Object(validationSchema)
|
||||||
|
}, [validationSchema])
|
||||||
|
|
||||||
|
const [fetchState, setFetchState] = useFetchState()
|
||||||
|
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||||
|
const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>(
|
||||||
|
{} as any
|
||||||
|
)
|
||||||
|
|
||||||
|
const validate = useMemo(() => {
|
||||||
|
return ajv.compile(validationSchemaObject)
|
||||||
|
}, [validationSchemaObject])
|
||||||
|
|
||||||
|
const handleUseForm: HandleUseForm<typeof validationSchema> = (callback) => {
|
||||||
|
return async (formData, formElement) => {
|
||||||
|
setErrors({} as any)
|
||||||
|
setMessage(undefined)
|
||||||
|
formData = handleOptionalEmptyStringToNull(
|
||||||
|
formData,
|
||||||
|
validationSchemaObject.required
|
||||||
|
)
|
||||||
|
formData = handleCheckboxBoolean(formData, validationSchemaObject)
|
||||||
|
const isValid = validate(formData)
|
||||||
|
if (!isValid) {
|
||||||
|
setFetchState('error')
|
||||||
|
const errors: ErrorsObject<typeof validationSchema> = {} as any
|
||||||
|
for (const property in validationSchemaObject.properties) {
|
||||||
|
const errorsForProperty = validate.errors?.filter((error) => {
|
||||||
|
return error.instancePath === `/${property}`
|
||||||
|
})
|
||||||
|
errors[property as keyof typeof validationSchema] =
|
||||||
|
errorsForProperty != null && errorsForProperty.length > 0
|
||||||
|
? errorsForProperty
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
setErrors(errors)
|
||||||
|
} else {
|
||||||
|
setErrors({} as any)
|
||||||
|
if (callback != null) {
|
||||||
|
setFetchState('loading')
|
||||||
|
const message = await callback(
|
||||||
|
formData as Static<TObject<typeof validationSchema>>,
|
||||||
|
formElement
|
||||||
|
)
|
||||||
|
if (message != null) {
|
||||||
|
const { value, type, properties } = message
|
||||||
|
setMessage(value)
|
||||||
|
setFetchState(type)
|
||||||
|
if (type === 'error') {
|
||||||
|
const propertiesErrors: ErrorsObject<typeof validationSchema> =
|
||||||
|
{} as any
|
||||||
|
for (const property in properties) {
|
||||||
|
propertiesErrors[property] = [
|
||||||
|
{
|
||||||
|
keyword: 'message',
|
||||||
|
message: properties[property],
|
||||||
|
instancePath: `/${property}`,
|
||||||
|
schemaPath: `#/properties/${property}/message`,
|
||||||
|
params: {},
|
||||||
|
data: formData[property]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
setErrors(propertiesErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUseForm,
|
||||||
|
fetchState,
|
||||||
|
setFetchState,
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
4
src/index.ts
Normal file
4
src/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './components/Form'
|
||||||
|
export * from './hooks/useFetchState'
|
||||||
|
export * from './hooks/useForm'
|
||||||
|
export * from './utils/ajv'
|
25
src/utils/ajv.ts
Normal file
25
src/utils/ajv.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import addFormats from 'ajv-formats'
|
||||||
|
import Ajv from 'ajv'
|
||||||
|
|
||||||
|
export const ajv = addFormats(
|
||||||
|
new Ajv({
|
||||||
|
allErrors: true,
|
||||||
|
verbose: true
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
'date-time',
|
||||||
|
'time',
|
||||||
|
'date',
|
||||||
|
'email',
|
||||||
|
'hostname',
|
||||||
|
'ipv4',
|
||||||
|
'ipv6',
|
||||||
|
'uri',
|
||||||
|
'uri-reference',
|
||||||
|
'uuid',
|
||||||
|
'uri-template',
|
||||||
|
'json-pointer',
|
||||||
|
'relative-json-pointer',
|
||||||
|
'regex'
|
||||||
|
]
|
||||||
|
)
|
25
src/utils/handleCheckboxBoolean.ts
Normal file
25
src/utils/handleCheckboxBoolean.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { TObject } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
import type { ObjectAny } from './types'
|
||||||
|
|
||||||
|
export const handleCheckboxBoolean = (
|
||||||
|
object: ObjectAny,
|
||||||
|
validateSchemaObject: TObject<ObjectAny>
|
||||||
|
): ObjectAny => {
|
||||||
|
const booleanProperties: string[] = []
|
||||||
|
for (const property in validateSchemaObject.properties) {
|
||||||
|
const rule = validateSchemaObject.properties[property]
|
||||||
|
if (rule.type === 'boolean') {
|
||||||
|
booleanProperties.push(property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const booleanProperty of booleanProperties) {
|
||||||
|
if (object[booleanProperty] == null) {
|
||||||
|
object[booleanProperty] =
|
||||||
|
validateSchemaObject.properties[booleanProperty].default
|
||||||
|
} else {
|
||||||
|
object[booleanProperty] = object[booleanProperty] === 'on'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
17
src/utils/handleOptionalEmptyStringToNull.ts
Normal file
17
src/utils/handleOptionalEmptyStringToNull.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export const handleOptionalEmptyStringToNull = <K>(
|
||||||
|
object: K,
|
||||||
|
required: string[] = []
|
||||||
|
): K => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(object).map(([key, value]) => {
|
||||||
|
if (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
value.length === 0 &&
|
||||||
|
!required.includes(key)
|
||||||
|
) {
|
||||||
|
return [key, null]
|
||||||
|
}
|
||||||
|
return [key, value]
|
||||||
|
})
|
||||||
|
) as K
|
||||||
|
}
|
3
src/utils/types.ts
Normal file
3
src/utils/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface ObjectAny {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'tsup'
|
import { defineConfig } from 'tsup'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: ['src/index.tsx'],
|
entry: ['src/index.ts'],
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
|
Reference in New Issue
Block a user