chore: add example
This commit is contained in:
		
							
								
								
									
										3
									
								
								example/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								example/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "extends": "next/core-web-vitals" | ||||
| } | ||||
							
								
								
									
										36
									
								
								example/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								example/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
|  | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
| .pnp.js | ||||
|  | ||||
| # testing | ||||
| /coverage | ||||
|  | ||||
| # 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'> | ||||
|         <Translation | ||||
|           i18nKey='common:about' | ||||
|           components={[<TextSpecial key='special' />]} | ||||
|         /> | ||||
|       </p> | ||||
|     </section> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										64
									
								
								example/components/FormExample.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								example/components/FormExample.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import { Form, HandleUseFormCallback, useForm } 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 wait = 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 wait(4000) | ||||
|     console.log('onSubmit:', formData) | ||||
|     formElement.reset() | ||||
|     return { | ||||
|       type: 'success', | ||||
|       message: t('common:success') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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'> | ||||
|           Submit | ||||
|         </Button> | ||||
|       </Form> | ||||
|       <FormState id='message' state={fetchState} message={message} /> | ||||
|     </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> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										76
									
								
								example/components/Header/Language/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								example/components/Header/Language/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| 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) => !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> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										32
									
								
								example/components/design/Input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								example/components/design/Input.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| 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} | ||||
|         /> | ||||
|         <FormState 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> | ||||
|   ) | ||||
| } | ||||
										
											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,38 +0,0 @@ | ||||
| import { createRoot } from 'react-dom/client' | ||||
| 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 { errors, handleUseForm } = useForm(schema) | ||||
|  | ||||
|   const onSubmit: HandleUseFormCallback<typeof schema> = ( | ||||
|     formData, | ||||
|     formElement | ||||
|   ) => { | ||||
|     console.log(formData) // { inputName: 'value of the input validated' } | ||||
|     formElement.reset() | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Form onSubmit={handleUseForm(onSubmit)}> | ||||
|       <input type='text' name='inputName' /> | ||||
|       {errors.inputName != null && <p>{errors.inputName[0].message}</p>} | ||||
|  | ||||
|       <button type='submit'>Submit</button> | ||||
|     </Form> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const container = document.getElementById('root') as HTMLElement | ||||
| const root = createRoot(container) | ||||
| root.render(<Example />) | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										8627
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8627
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,22 +1,31 @@ | ||||
| { | ||||
|   "name": "example", | ||||
|   "type": "module", | ||||
|   "version": "1.0.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "start": "parcel index.html", | ||||
|     "build": "parcel build index.html --public-url \"/react-component-form/\"" | ||||
|     "dev": "next dev", | ||||
|     "build": "next build", | ||||
|     "start": "next start", | ||||
|     "lint": "next lint" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "react": "file:../node_modules/react", | ||||
|     "clsx": "1.2.1", | ||||
|     "next": "12.2.5", | ||||
|     "next-themes": "0.2.0", | ||||
|     "next-translate": "1.5.0", | ||||
|     "react": "18.2.0", | ||||
|     "react-component-form": "file:..", | ||||
|     "react-dom": "file:../node_modules/react-dom" | ||||
|     "react-dom": "18.2.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@parcel/transformer-image": "2.7.0", | ||||
|     "@types/node": "18.7.13", | ||||
|     "@types/react": "18.0.17", | ||||
|     "@types/react-dom": "18.0.6", | ||||
|     "parcel": "2.7.0", | ||||
|     "process": "^0.11.10", | ||||
|     "typescript": "4.7.4" | ||||
|     "autoprefixer": "10.4.8", | ||||
|     "eslint": "8.22.0", | ||||
|     "eslint-config-next": "12.2.5", | ||||
|     "postcss": "8.4.16", | ||||
|     "tailwindcss": "3.1.8", | ||||
|     "typescript": "4.8.2" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										14
									
								
								example/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								example/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import type { AppProps } from 'next/app' | ||||
| import { ThemeProvider } from 'next-themes' | ||||
|  | ||||
| import '../styles/globals.css' | ||||
|  | ||||
| const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { | ||||
|   return ( | ||||
|     <ThemeProvider attribute='class' defaultTheme='light'> | ||||
|       <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,20 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "module": "ESNext", | ||||
|     "lib": ["ESNext", "DOM", "DOM.Iterable"], | ||||
|     "target": "ESNext", | ||||
|     "jsx": "react", | ||||
|     "moduleResolution": "node", | ||||
|     "sourceMap": true, | ||||
|     "module": "ESNext", | ||||
|     "lib": ["dom", "dom.iterable", "esnext"], | ||||
|     "allowJs": true, | ||||
|     "skipLibCheck": true, | ||||
|     "strict": true, | ||||
|     "esModuleInterop": true | ||||
|   } | ||||
|     "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"] | ||||
| } | ||||
|   | ||||
| @@ -48,8 +48,8 @@ export interface UseFormResult<K extends Schema> { | ||||
|   /** | ||||
|    * Global message of the form (not specific to a property). | ||||
|    */ | ||||
|   readonly message: string | null | ||||
|   setMessage: React.Dispatch<React.SetStateAction<string | null>> | ||||
|   readonly message?: string | ||||
|   setMessage: React.Dispatch<React.SetStateAction<string | undefined>> | ||||
|  | ||||
|   /** | ||||
|    * Errors for each property. | ||||
| @@ -69,7 +69,7 @@ export const useForm = <K extends Schema>( | ||||
|   }, [validationSchema]) | ||||
|  | ||||
|   const [fetchState, setFetchState] = useFetchState() | ||||
|   const [message, setMessage] = useState<string | null>(null) | ||||
|   const [message, setMessage] = useState<string | undefined>(undefined) | ||||
|   const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>( | ||||
|     {} as any | ||||
|   ) | ||||
| @@ -81,7 +81,7 @@ export const useForm = <K extends Schema>( | ||||
|   const handleUseForm: HandleUseForm<typeof validationSchema> = (callback) => { | ||||
|     return async (formData, formElement) => { | ||||
|       setErrors({} as any) | ||||
|       setMessage(null) | ||||
|       setMessage(undefined) | ||||
|       formData = handleOptionalEmptyStringToNull( | ||||
|         formData, | ||||
|         validationSchemaObject.required | ||||
| @@ -110,7 +110,7 @@ export const useForm = <K extends Schema>( | ||||
|             formElement | ||||
|           ) | ||||
|           if (message != null) { | ||||
|             const { value = null, type, properties } = message | ||||
|             const { value = undefined, type, properties } = message | ||||
|             setMessage(value) | ||||
|             setFetchState(type) | ||||
|             if (type === 'error') { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user