chore: add example

This commit is contained in:
Divlo
2022-08-26 20:19:31 +02:00
parent 37b4b9b990
commit 676a70b1a9
42 changed files with 6029 additions and 3666 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
</>
)
}

View File

@@ -0,0 +1 @@
export * from './Header'

View 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>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}