chore: add example
This commit is contained in:
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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user