create useform hook

This commit is contained in:
2025-05-16 19:29:46 +03:00
parent bb0440986c
commit a867699855
10 changed files with 672 additions and 21 deletions

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -0,0 +1,168 @@
'use client'
import { useState, ChangeEvent, FormEvent } from 'react'
import toast from 'react-hot-toast'
import { ValidationRules, ValidationErrors } from '../types'
type FormValue = string | number | boolean | string[] | number[]
type CustomChangeEvent = {
target: {
id: string
value: FormValue
type?: string
checked?: boolean
}
}
export function useForm<T extends Record<string, FormValue>>(
initialValues: T,
validationRules?: { [K in keyof T]?: ValidationRules },
onSubmit?: (values: T) => void
) {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<ValidationErrors>({})
const [isVisible, setIsVisible] = useState(false)
const handleChange = (
e:
| ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
| CustomChangeEvent
) => {
const { id, value, type } = e.target
const isCheckbox = type === 'checkbox' && 'checked' in e.target
setValues((prev) => ({
...prev,
[id]: isCheckbox ? (e.target as HTMLInputElement).checked : value,
}))
// скидываем ошибки
if (errors[id]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[id]
return newErrors
})
}
}
const resetField = (fieldName: keyof T, value: FormValue = '') => {
setValues((prev) => ({
...prev,
[fieldName]: value,
}))
// очищаем ошибки для этого поля, если они есть
if (errors[fieldName as string]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[fieldName as string]
return newErrors
})
}
}
const fieldNames: { [key: string]: string } = {
name: 'Имя',
surmame: 'Фамилия',
email: 'Email',
phone_number: 'Номер телефона',
password: 'Пароль',
privacy_policy: 'Политика конфиденциальности',
}
const validate = () => {
if (!validationRules) return true
const newErrors: ValidationErrors = {}
Object.keys(validationRules).forEach((key) => {
const value = values[key]
const rules = validationRules[key as keyof T]
if (rules?.required && !value) {
newErrors[key] = 'Это поле обязательно'
toast.error(
`Поле "${fieldNames[key] || key}" обязательно для заполнения`,
{
duration: 2000,
position: 'top-right',
style: {
background: '#FEE2E2',
color: '#991B1B',
padding: '16px',
borderRadius: '8px',
},
}
)
}
if (
rules?.minLength &&
typeof value === 'string' &&
value.length < rules.minLength
) {
newErrors[key] = `Минимальная длина ${rules.minLength} символов`
toast.error(
`Минимальная длина поля "${fieldNames[key] || key}" - ${
rules.minLength
} символов`,
{
duration: 2000,
position: 'top-right',
style: {
background: '#FEE2E2',
color: '#991B1B',
padding: '16px',
borderRadius: '8px',
},
}
)
}
if (
rules?.pattern &&
typeof value === 'string' &&
!rules.pattern.test(value)
) {
newErrors[key] = 'Неверный формат'
toast.error(`Поле "${fieldNames[key] || key}" заполнено некорректно`, {
duration: 2000,
position: 'top-right',
style: {
background: '#FEE2E2',
color: '#991B1B',
padding: '16px',
borderRadius: '8px',
},
})
}
})
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (validate() && onSubmit) {
onSubmit(values)
}
}
const togglePasswordVisibility = () => {
setIsVisible(!isVisible)
}
return {
values,
errors,
isVisible,
setValues,
handleChange,
handleSubmit,
togglePasswordVisibility,
resetField,
}
}

View File

@@ -0,0 +1,47 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { User, UserState } from '../types'
interface UserStore extends UserState {
// состояние
isAuthenticated: boolean
user: User | null
// действия
setUser: (user: User | null) => void
setAuthenticated: (isAuthenticated: boolean) => void
logout: () => void
// асинхронные действия
//! что пользователь может делать асинхронно?
}
const useUserStore = create<UserStore>()(
persist(
(set) => ({
// начальное состояние
isAuthenticated: false,
user: null,
favorites: [],
// синхронные действия
setUser: (user) => set({ user }),
setAuthenticated: (isAuthenticated) => set({ isAuthenticated }),
logout: () =>
set({
isAuthenticated: false,
user: null,
}),
}),
//! асинхронщина?
{ name: 'user-store' }
)
)
export default useUserStore
// пример использования
// const { user, isAuthenticated } = useUserStore() -- получаем данные из стора
// const { setUser, setAuthenticated } = useUserStore() -- устанавливаем данные в стор
// const { logout } = useUserStore() -- выходим из пользовательского аккаунта

View File

@@ -1,4 +1,4 @@
import Image, { StaticImageData } from 'next/image'
import { StaticImageData } from 'next/image'
export interface TextInputProps {
value: string
@@ -16,7 +16,7 @@ export interface ButtonProps {
onClick?: () => void
className?: string
text?: string
type?: 'button'
type?: 'button' | 'submit' | 'reset'
}
export interface SearchCardProps {
@@ -66,3 +66,46 @@ export interface NewsItem {
export interface NewsProps {
news: NewsItem[]
}
export interface ValidationRules {
required?: boolean
minLength?: number
pattern?: RegExp
}
export interface ValidationErrors {
[key: string]: string
}
export type ToastProps = {
type: 'error' | 'success' | 'loading'
message: string
action?: {
text: string
onClick: () => void
}
duration?: number
}
export type MembershipPlans = {
plan: 'lite' | 'standart' | 'premium'
}
export interface User {
id?: number | undefined | string
uuid?: string
name: string
surname: string
image?: string
phone_number?: string
email: string
country?: string
city?: string
plan: MembershipPlans
account_type?: string // user или manager
}
export interface UserState {
isAuthenticated: boolean
user: User | null
}