mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
live lesson 13-01-24
This commit is contained in:
@@ -14,11 +14,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.0.5",
|
"@fontsource/roboto": "^5.0.5",
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.49.3",
|
||||||
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@it-incubator/eslint-config": "^0.1.3",
|
"@it-incubator/eslint-config": "^0.1.3",
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ dependencies:
|
|||||||
'@fontsource/roboto':
|
'@fontsource/roboto':
|
||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
version: 5.0.5
|
version: 5.0.5
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.3.4
|
||||||
|
version: 3.3.4(react-hook-form@7.49.3)
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -23,6 +26,12 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-hook-form:
|
||||||
|
specifier: ^7.49.3
|
||||||
|
version: 7.49.3(react@18.2.0)
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.4
|
||||||
|
version: 3.22.4
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@it-incubator/eslint-config':
|
'@it-incubator/eslint-config':
|
||||||
@@ -1747,6 +1756,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-IMXFq5AMgGx0sgNLfwWsmPuy3qa7lmDmQcXXihqwF4mT2UpD725cbxZj93ERY793OWon+6V1ANax02I3nt9+4w==}
|
resolution: {integrity: sha512-IMXFq5AMgGx0sgNLfwWsmPuy3qa7lmDmQcXXihqwF4mT2UpD725cbxZj93ERY793OWon+6V1ANax02I3nt9+4w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@hookform/resolvers@3.3.4(react-hook-form@7.49.3):
|
||||||
|
resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react-hook-form: ^7.0.0
|
||||||
|
dependencies:
|
||||||
|
react-hook-form: 7.49.3(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.10:
|
/@humanwhocodes/config-array@0.11.10:
|
||||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -7541,6 +7558,15 @@ packages:
|
|||||||
react-is: 18.1.0
|
react-is: 18.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react-hook-form@7.49.3(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==}
|
||||||
|
engines: {node: '>=18', pnpm: '8'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-inspector@6.0.2(react@18.2.0):
|
/react-inspector@6.0.2(react@18.2.0):
|
||||||
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
|
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9139,3 +9165,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/zod@3.22.4:
|
||||||
|
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||||
|
dev: false
|
||||||
|
|||||||
14
src/components/auth/login-form/login-form.stories.tsx
Normal file
14
src/components/auth/login-form/login-form.stories.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import { LoginForm } from './login-form'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Auth/LoginForm',
|
||||||
|
component: LoginForm,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof LoginForm>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Primary: Story = {}
|
||||||
52
src/components/auth/login-form/login-form.tsx
Normal file
52
src/components/auth/login-form/login-form.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '../../ui/button'
|
||||||
|
|
||||||
|
import { ControlledCheckbox } from '@/components/ui/controlled/controlled-checkbox.tsx'
|
||||||
|
import { ControlledTextField } from '@/components/ui/controlled/controlled-text-field.tsx'
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().email(''),
|
||||||
|
password: z.string().min(3).max(30),
|
||||||
|
rememberMe: z.literal(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof loginSchema>
|
||||||
|
export const LoginForm = () => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: FormValues) => {
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<ControlledTextField
|
||||||
|
control={control}
|
||||||
|
name={'email'}
|
||||||
|
label={'email'}
|
||||||
|
errorMessage={errors.email?.message}
|
||||||
|
/>
|
||||||
|
<ControlledTextField
|
||||||
|
control={control}
|
||||||
|
name={'password'}
|
||||||
|
label={'password'}
|
||||||
|
errorMessage={errors.password?.message}
|
||||||
|
/>
|
||||||
|
<ControlledCheckbox
|
||||||
|
control={control}
|
||||||
|
name={'rememberMe'}
|
||||||
|
label="I accept terms and conditions"
|
||||||
|
/>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC } from 'react'
|
import { ComponentPropsWithoutRef, forwardRef, ElementRef } from 'react'
|
||||||
|
|
||||||
import * as CheckboxRadix from '@radix-ui/react-checkbox'
|
import * as CheckboxRadix from '@radix-ui/react-checkbox'
|
||||||
import * as LabelRadix from '@radix-ui/react-label'
|
import * as LabelRadix from '@radix-ui/react-label'
|
||||||
@@ -9,58 +9,42 @@ import s from './checkbox.module.scss'
|
|||||||
import { Check } from '@/assets/icons'
|
import { Check } from '@/assets/icons'
|
||||||
import { Typography } from '@/components'
|
import { Typography } from '@/components'
|
||||||
|
|
||||||
export type CheckboxProps = {
|
export type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxRadix.Root> & {
|
||||||
className?: string
|
|
||||||
checked?: boolean
|
|
||||||
onChange?: (checked: boolean) => void
|
|
||||||
disabled?: boolean
|
|
||||||
required?: boolean
|
|
||||||
label?: string
|
label?: string
|
||||||
id?: string
|
|
||||||
position?: 'left'
|
position?: 'left'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox: FC<CheckboxProps> = ({
|
export const Checkbox = forwardRef<ElementRef<typeof CheckboxRadix.Root>, CheckboxProps>(
|
||||||
checked,
|
({ position, label, className, ...rest }, ref) => {
|
||||||
onChange,
|
const classNames = {
|
||||||
position,
|
container: clsx(s.container, className),
|
||||||
disabled,
|
buttonWrapper: clsx(
|
||||||
required,
|
s.buttonWrapper,
|
||||||
label,
|
rest.disabled && s.disabled,
|
||||||
id,
|
position === 'left' && s.left
|
||||||
className,
|
),
|
||||||
}) => {
|
root: s.root,
|
||||||
const classNames = {
|
indicator: s.indicator,
|
||||||
container: clsx(s.container, className),
|
label: clsx(s.label, rest.disabled && s.disabled),
|
||||||
buttonWrapper: clsx(s.buttonWrapper, disabled && s.disabled, position === 'left' && s.left),
|
}
|
||||||
root: s.root,
|
|
||||||
indicator: s.indicator,
|
|
||||||
label: clsx(s.label, disabled && s.disabled),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames.container}>
|
<div className={classNames.container}>
|
||||||
<LabelRadix.Root asChild>
|
<LabelRadix.Root asChild>
|
||||||
<Typography variant="body2" className={classNames.label} as={'label'}>
|
<Typography variant="body2" className={classNames.label} as={'label'}>
|
||||||
<div className={classNames.buttonWrapper}>
|
<div className={classNames.buttonWrapper}>
|
||||||
<CheckboxRadix.Root
|
<CheckboxRadix.Root className={classNames.root} {...rest} ref={ref}>
|
||||||
className={classNames.root}
|
<CheckboxRadix.Indicator className={classNames.indicator}>
|
||||||
checked={checked}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
required={required}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
{checked && (
|
|
||||||
<CheckboxRadix.Indicator className={classNames.indicator} forceMount>
|
|
||||||
<Check />
|
<Check />
|
||||||
</CheckboxRadix.Indicator>
|
</CheckboxRadix.Indicator>
|
||||||
)}
|
</CheckboxRadix.Root>
|
||||||
</CheckboxRadix.Root>
|
</div>
|
||||||
</div>
|
{label}
|
||||||
{label}
|
</Typography>
|
||||||
</Typography>
|
</LabelRadix.Root>
|
||||||
</LabelRadix.Root>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
|
Checkbox.displayName = 'Checkbox'
|
||||||
|
|||||||
25
src/components/ui/controlled/controlled-checkbox.tsx
Normal file
25
src/components/ui/controlled/controlled-checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { FieldValues, useController, UseControllerProps } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Checkbox, CheckboxProps } from '@/components'
|
||||||
|
|
||||||
|
type Props<T extends FieldValues> = Omit<
|
||||||
|
UseControllerProps<T>,
|
||||||
|
'disabled' | 'rules' | 'defaultValue'
|
||||||
|
> &
|
||||||
|
Omit<CheckboxProps, 'checked' | 'onValueChange'>
|
||||||
|
export const ControlledCheckbox = <T extends FieldValues>({
|
||||||
|
control,
|
||||||
|
shouldUnregister,
|
||||||
|
...rest
|
||||||
|
}: Props<T>) => {
|
||||||
|
const {
|
||||||
|
field: { value, onChange, onBlur, ref },
|
||||||
|
} = useController({
|
||||||
|
name: rest.name,
|
||||||
|
control,
|
||||||
|
shouldUnregister,
|
||||||
|
disabled: rest.disabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
return <Checkbox {...rest} checked={value} onCheckedChange={onChange} onBlur={onBlur} ref={ref} />
|
||||||
|
}
|
||||||
23
src/components/ui/controlled/controlled-text-field.tsx
Normal file
23
src/components/ui/controlled/controlled-text-field.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { FieldValues, useController, UseControllerProps } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { TextField, TextFieldProps } from '@/components/ui/text-field'
|
||||||
|
|
||||||
|
type Props<T extends FieldValues> = Omit<
|
||||||
|
UseControllerProps<T>,
|
||||||
|
'disabled' | 'rules' | 'defaultValue'
|
||||||
|
> &
|
||||||
|
Omit<TextFieldProps, 'value' | 'onChange' | 'onValueChange'>
|
||||||
|
export const ControlledTextField = <T extends FieldValues>({
|
||||||
|
control,
|
||||||
|
shouldUnregister,
|
||||||
|
...rest
|
||||||
|
}: Props<T>) => {
|
||||||
|
const { field } = useController({
|
||||||
|
name: rest.name,
|
||||||
|
control,
|
||||||
|
shouldUnregister,
|
||||||
|
disabled: rest.disabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
return <TextField {...rest} {...field} />
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user