/
viktorphp
/
front-socket
Обзор
Документация
Войти
/
viktorphp
/
front-socket
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/widgets/profile/client/ui/ProfileForm.tsx
340 строк
10 KB
Viktorphp84
feature-socket: Поправить верстку на странице профиля, исправить сохранение дат.
12 окт 2025, 01:53
12 окт 2025, 01:53
93fb0bf
Код
Авторство
О чём код?
'use client'; import type { TProfileFormData } from '@shared/types'; import { startTransition, useActionState, useEffect, useRef, useState, } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { PatternFormat } from 'react-number-format'; import { zodResolver } from '@hookform/resolvers/zod'; import { Avatar, Box, Button, Grid2, Stack, TextField, Typography, } from '@mui/material'; import { styled } from '@mui/material/styles'; import { updateProfile } from '@features/profile'; import { socketService } from '@shared/api/socket'; import { EProfileFormFields, IUser, profileSchema } from '@shared/types'; import { AlertMessage, LoaderOverlay } from '@shared/ui'; const StyledForm = styled('form')(({ theme }) => ({ display: 'flex', flexDirection: 'column', width: '100%', boxSizing: 'border-box', '& .MuiTextField-root': { marginBottom: theme.spacing(2), }, })); export const ProfileForm = ({ userData, avatarSrc, }: { userData: IUser; avatarSrc: string; }) => { const [state, formAction, isPending] = useActionState(updateProfile, { status: '', message: '', userData: null, }); const [previewUrl, setPreviewUrl] = useState<string>(''); const [displayedAvatar, setDisplayedAvatar] = useState<string>(avatarSrc); // ref для input file const fileInputRef = useRef<HTMLInputElement>(null); const { control, register, handleSubmit, formState: { errors }, reset, setValue, } = useForm<TProfileFormData>({ resolver: zodResolver(profileSchema), defaultValues: { username: userData.username || '', lastName: userData.lastName || '', firstName: userData.firstName || '', patronymic: userData.patronymic || '', email: userData.email || '', phoneNumber: userData.phoneNumber || '', birthDate: userData.birthDate ? new Date(userData.birthDate).toLocaleDateString('ru-RU') : '', avatar: userData.avatar || undefined, }, }); // Сброс формы при успехе + обновление списка чатов useEffect(() => { if (state?.status === 'success' && state.userData) { reset({ username: state.userData.username || '', lastName: state.userData.lastName || '', firstName: state.userData.firstName || '', patronymic: state.userData.patronymic || '', email: state.userData.email || '', phoneNumber: state.userData.phoneNumber || '', birthDate: state.userData.birthDate ? new Date(state.userData.birthDate).toLocaleDateString('ru-RU') : '', avatar: state.userData.avatar || undefined, }); // после успешного апдейта профиля обновляем список чатов socketService.sendGetChatsForEveryone(); } }, [state, reset, setValue]); // Обработчик отправки формы const onSubmit = (data: TProfileFormData) => { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { if (value) formData.append(key, value); }); startTransition(() => formAction(formData)); }; const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; if (file) { setValue(EProfileFormFields.AVATAR, file); // Создаем URL для предварительного просмотра const objectUrl = URL.createObjectURL(file); setPreviewUrl(objectUrl); // Обновляем отображаемый аватар setDisplayedAvatar(objectUrl); } }; // Очищаем URL при размонтировании компонента useEffect(() => { return () => { if (previewUrl) { URL.revokeObjectURL(previewUrl); } }; }, [previewUrl]); // Обновляем отображаемый аватар при изменении avatarSrc useEffect(() => { if (!previewUrl) { setDisplayedAvatar(avatarSrc); } }, [avatarSrc]); return ( <StyledForm onSubmit={handleSubmit(onSubmit)}> {isPending && <LoaderOverlay />} {state?.status === 'error' && ( <AlertMessage key={`error-${Date.now()}`} status={state.status} message={state.message} /> )} {state?.status === 'success' && ( <AlertMessage key={`success-${Date.now()}`} status={state.status} message={state.message} /> )} <Box sx={{ display: 'flex', alignItems: 'center', mb: 3, pb: 2, borderBottom: 1, borderColor: 'primary.light', flex: 1, boxSizing: 'border-box', }} > <input type={'file'} ref={fileInputRef} style={{ display: 'none' }} accept={'.jpg,.jpeg,.png,.gif'} onChange={handleFileSelect} /> <Avatar src={displayedAvatar} alt={`${userData.lastName} ${userData.firstName}`} sx={{ width: 80, height: 80, bgcolor: 'secondary.light', mr: 2, boxShadow: '0 0 10px rgba(0, 0, 0, 0.2)', // добавляем размытую рамку cursor: 'pointer', '&:hover': { opacity: 0.8, }, }} onClick={() => fileInputRef.current?.click()} /> <Stack spacing={2} sx={{ flex: 1 }}> <Grid2 container gap={2}> <TextField label={'Фамилия'} {...register(EProfileFormFields.LAST_NAME)} size={'small'} error={!!errors.lastName} helperText={errors.lastName?.message} sx={{ flex: 1 }} /> <TextField label={'Имя'} {...register(EProfileFormFields.FIRST_NAME)} size={'small'} error={!!errors.firstName} helperText={errors.firstName?.message} sx={{ flex: 1 }} /> <TextField label={'Отчество'} {...register(EProfileFormFields.PATRONYMIC)} size={'small'} error={!!errors.patronymic} helperText={errors.patronymic?.message} sx={{ flex: 1 }} /> </Grid2> <Grid2 container gap={2}> <Typography variant={'subtitle1'} color={'secondary.main'} sx={{ flex: 1 }} > {userData.role} </Typography> <Stack flex={1} /> <TextField label={'Логин'} {...register(EProfileFormFields.USER_NAME)} size={'small'} error={!!errors.username} helperText={errors.username?.message} sx={{ flex: 1 }} /> </Grid2> </Stack> </Box> <Grid2 container spacing={2}> <Grid2 component={'div'} size={6} sx={{ display: 'flex', flexDirection: 'column', gap: 2, // вертикальный отступ между элементами }} > <Typography variant={'subtitle2'} color={'secondary.dark'} gutterBottom > Контактная информация </Typography> <TextField fullWidth label={'Email'} {...register(EProfileFormFields.EMAIL)} size={'small'} error={!!errors.email} helperText={errors.email?.message} /> <Controller name={EProfileFormFields.PHONE_NUMBER} control={control} render={({ field }) => ( <PatternFormat format={'+7 (###) ###-##-##'} value={field.value} onChange={field.onChange} allowEmptyFormatting={true} mask={'_'} customInput={TextField} label={'Телефон'} size={'small'} fullWidth error={!!errors.phoneNumber} helperText={errors.phoneNumber?.message} /> )} /> </Grid2> <Grid2 component={'div'} size={6} sx={{ display: 'flex', flexDirection: 'column', gap: 2, // вертикальный отступ между элементами }} > <Typography variant={'subtitle2'} color={'secondary.dark'} gutterBottom > Личная информация </Typography> <Controller name={EProfileFormFields.BIRTH_DATE} control={control} render={({ field }) => ( <PatternFormat format={'##.##.####'} value={field.value} onChange={field.onChange} allowEmptyFormatting={true} mask={'_'} customInput={TextField} label={'Дата рождения'} size={'small'} fullWidth error={!!errors.birthDate} helperText={errors.birthDate?.message} /> )} /> </Grid2> </Grid2> <Stack direction={'row'} justifyContent={'flex-end'} sx={{ mt: 3 }}> <Button type={'submit'} variant={'contained'} color={'primary'} size={'large'} > Сохранить изменения </Button> </Stack> </StyledForm> ); };