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": {
|
||||
"@fontsource/roboto": "^5.0.5",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"clsx": "^2.0.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": {
|
||||
"@it-incubator/eslint-config": "^0.1.3",
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ dependencies:
|
||||
'@fontsource/roboto':
|
||||
specifier: ^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':
|
||||
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)
|
||||
@@ -23,6 +26,12 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: ^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:
|
||||
'@it-incubator/eslint-config':
|
||||
@@ -1747,6 +1756,14 @@ packages:
|
||||
resolution: {integrity: sha512-IMXFq5AMgGx0sgNLfwWsmPuy3qa7lmDmQcXXihqwF4mT2UpD725cbxZj93ERY793OWon+6V1ANax02I3nt9+4w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -7541,6 +7558,15 @@ packages:
|
||||
react-is: 18.1.0
|
||||
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):
|
||||
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
|
||||
peerDependencies:
|
||||
@@ -9139,3 +9165,7 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
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 LabelRadix from '@radix-ui/react-label'
|
||||
@@ -9,33 +9,23 @@ import s from './checkbox.module.scss'
|
||||
import { Check } from '@/assets/icons'
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export type CheckboxProps = {
|
||||
className?: string
|
||||
checked?: boolean
|
||||
onChange?: (checked: boolean) => void
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
export type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxRadix.Root> & {
|
||||
label?: string
|
||||
id?: string
|
||||
position?: 'left'
|
||||
}
|
||||
|
||||
export const Checkbox: FC<CheckboxProps> = ({
|
||||
checked,
|
||||
onChange,
|
||||
position,
|
||||
disabled,
|
||||
required,
|
||||
label,
|
||||
id,
|
||||
className,
|
||||
}) => {
|
||||
export const Checkbox = forwardRef<ElementRef<typeof CheckboxRadix.Root>, CheckboxProps>(
|
||||
({ position, label, className, ...rest }, ref) => {
|
||||
const classNames = {
|
||||
container: clsx(s.container, className),
|
||||
buttonWrapper: clsx(s.buttonWrapper, disabled && s.disabled, position === 'left' && s.left),
|
||||
buttonWrapper: clsx(
|
||||
s.buttonWrapper,
|
||||
rest.disabled && s.disabled,
|
||||
position === 'left' && s.left
|
||||
),
|
||||
root: s.root,
|
||||
indicator: s.indicator,
|
||||
label: clsx(s.label, disabled && s.disabled),
|
||||
label: clsx(s.label, rest.disabled && s.disabled),
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -43,19 +33,10 @@ export const Checkbox: FC<CheckboxProps> = ({
|
||||
<LabelRadix.Root asChild>
|
||||
<Typography variant="body2" className={classNames.label} as={'label'}>
|
||||
<div className={classNames.buttonWrapper}>
|
||||
<CheckboxRadix.Root
|
||||
className={classNames.root}
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
id={id}
|
||||
>
|
||||
{checked && (
|
||||
<CheckboxRadix.Indicator className={classNames.indicator} forceMount>
|
||||
<CheckboxRadix.Root className={classNames.root} {...rest} ref={ref}>
|
||||
<CheckboxRadix.Indicator className={classNames.indicator}>
|
||||
<Check />
|
||||
</CheckboxRadix.Indicator>
|
||||
)}
|
||||
</CheckboxRadix.Root>
|
||||
</div>
|
||||
{label}
|
||||
@@ -63,4 +44,7 @@ export const Checkbox: FC<CheckboxProps> = ({
|
||||
</LabelRadix.Root>
|
||||
</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