Добавлена логика загрузки аватаров пользователей через API с использованием S3. Реализованы проверки на тип и размер файла, а также обработка ошибок. Обновлены компоненты UserSettings и useAuth для интеграции новой функциональности. Оптимизирована работа с кэшем Apollo Client для мгновенного обновления аватара в интерфейсе.
This commit is contained in:
@ -3,6 +3,8 @@
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useMutation } from '@apollo/client'
|
||||
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
|
||||
import { GET_ME } from '@/graphql/queries'
|
||||
import { apolloClient } from '@/lib/apollo-client'
|
||||
import { formatPhone } from '@/lib/utils'
|
||||
import S3Service from '@/services/s3-service'
|
||||
import { Card } from '@/components/ui/card'
|
||||
@ -35,12 +37,12 @@ import {
|
||||
Settings,
|
||||
Camera
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
export function UserSettings() {
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const { user } = useAuth()
|
||||
const { user, updateUser } = useAuth()
|
||||
const [updateUserProfile, { loading: isSaving }] = useMutation(UPDATE_USER_PROFILE)
|
||||
const [updateOrganizationByInn, { loading: isUpdatingOrganization }] = useMutation(UPDATE_ORGANIZATION_BY_INN)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
@ -48,6 +50,9 @@ export function UserSettings() {
|
||||
const [partnerLink, setPartnerLink] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
const [localAvatarUrl, setLocalAvatarUrl] = useState<string | null>(null)
|
||||
const phoneInputRef = useRef<HTMLInputElement>(null)
|
||||
const whatsappInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Инициализируем данные из пользователя и организации
|
||||
const [formData, setFormData] = useState({
|
||||
@ -127,10 +132,10 @@ export function UserSettings() {
|
||||
}
|
||||
|
||||
setFormData({
|
||||
orgPhone: orgPhone,
|
||||
orgPhone: orgPhone || '+7',
|
||||
managerName: user?.managerName || '',
|
||||
telegram: customContacts?.telegram || '',
|
||||
whatsapp: customContacts?.whatsapp || '',
|
||||
whatsapp: customContacts?.whatsapp || '+7',
|
||||
email: email,
|
||||
orgName: org.name || '',
|
||||
address: org.address || '',
|
||||
@ -291,27 +296,73 @@ export function UserSettings() {
|
||||
try {
|
||||
const avatarUrl = await S3Service.uploadAvatar(file, user.id)
|
||||
|
||||
// Сразу обновляем локальное состояние для мгновенного отображения
|
||||
setLocalAvatarUrl(avatarUrl)
|
||||
|
||||
// Обновляем аватар пользователя через GraphQL
|
||||
const result = await updateUserProfile({
|
||||
variables: {
|
||||
input: {
|
||||
avatar: avatarUrl
|
||||
}
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
if (data?.updateUserProfile?.success) {
|
||||
// Обновляем кеш Apollo Client
|
||||
try {
|
||||
const existingData: any = cache.readQuery({ query: GET_ME })
|
||||
if (existingData?.me) {
|
||||
cache.writeQuery({
|
||||
query: GET_ME,
|
||||
data: {
|
||||
me: {
|
||||
...existingData.me,
|
||||
avatar: avatarUrl
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Cache update error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (result.data?.updateUserProfile?.success) {
|
||||
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен! Обновляем страницу...' })
|
||||
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен!' })
|
||||
|
||||
// Обновляем локальное состояние в useAuth для мгновенного отображения в сайдбаре
|
||||
updateUser({ avatar: avatarUrl })
|
||||
|
||||
// Принудительно обновляем Apollo Client кеш
|
||||
await apolloClient.refetchQueries({
|
||||
include: [GET_ME]
|
||||
})
|
||||
|
||||
// Очищаем input файла
|
||||
if (event.target) {
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
// Очищаем сообщение через 3 секунды
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
setSaveMessage(null)
|
||||
}, 3000)
|
||||
} else {
|
||||
throw new Error('Failed to update avatar')
|
||||
throw new Error(result.data?.updateUserProfile?.message || 'Failed to update avatar')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading avatar:', error)
|
||||
setSaveMessage({ type: 'error', text: 'Ошибка при загрузке аватара' })
|
||||
// Сбрасываем локальное состояние при ошибке
|
||||
setLocalAvatarUrl(null)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Ошибка при загрузке аватара'
|
||||
setSaveMessage({ type: 'error', text: errorMessage })
|
||||
// Очищаем сообщение об ошибке через 5 секунд
|
||||
setTimeout(() => {
|
||||
setSaveMessage(null)
|
||||
}, 5000)
|
||||
} finally {
|
||||
setIsUploadingAvatar(false)
|
||||
}
|
||||
@ -324,14 +375,69 @@ export function UserSettings() {
|
||||
}
|
||||
|
||||
const formatPhoneInput = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '')
|
||||
if (cleaned.length <= 1) return cleaned
|
||||
// Убираем все нецифровые символы
|
||||
const digitsOnly = value.replace(/\D/g, '')
|
||||
|
||||
// Если строка пустая, возвращаем +7
|
||||
if (!digitsOnly) return '+7'
|
||||
|
||||
// Если пользователь ввел первую цифру не 7, добавляем 7 перед ней
|
||||
let cleaned = digitsOnly
|
||||
if (!cleaned.startsWith('7')) {
|
||||
cleaned = '7' + cleaned
|
||||
}
|
||||
|
||||
// Ограничиваем до 11 цифр (7 + 10 цифр номера)
|
||||
cleaned = cleaned.slice(0, 11)
|
||||
|
||||
// Форматируем в зависимости от длины
|
||||
if (cleaned.length <= 1) return '+7'
|
||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
||||
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
||||
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
||||
if (cleaned.length <= 11) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9)}`
|
||||
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
||||
}
|
||||
|
||||
const handlePhoneInputChange = (field: string, value: string, inputRef?: React.RefObject<HTMLInputElement>) => {
|
||||
const currentInput = inputRef?.current
|
||||
const currentCursorPosition = currentInput?.selectionStart || 0
|
||||
const currentValue = formData[field as keyof typeof formData] as string || ''
|
||||
|
||||
// Если пользователь пытается удалить +7, предотвращаем это
|
||||
if (value.length < 2) {
|
||||
value = '+7'
|
||||
}
|
||||
|
||||
const formatted = formatPhoneInput(value)
|
||||
setFormData(prev => ({ ...prev, [field]: formatted }))
|
||||
|
||||
// Вычисляем новую позицию курсора
|
||||
if (currentInput) {
|
||||
setTimeout(() => {
|
||||
let newCursorPosition = currentCursorPosition
|
||||
|
||||
// Если длина увеличилась (добавили цифру), передвигаем курсор
|
||||
if (formatted.length > currentValue.length) {
|
||||
newCursorPosition = currentCursorPosition + (formatted.length - currentValue.length)
|
||||
}
|
||||
// Если длина уменьшилась (удалили цифру), оставляем курсор на месте или сдвигаем немного
|
||||
else if (formatted.length < currentValue.length) {
|
||||
newCursorPosition = Math.min(currentCursorPosition, formatted.length)
|
||||
}
|
||||
|
||||
// Не позволяем курсору находиться перед +7
|
||||
newCursorPosition = Math.max(newCursorPosition, 2)
|
||||
|
||||
// Ограничиваем курсор длиной строки
|
||||
newCursorPosition = Math.min(newCursorPosition, formatted.length)
|
||||
|
||||
currentInput.setSelectionRange(newCursorPosition, newCursorPosition)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTelegram = (value: string) => {
|
||||
// Убираем все символы кроме букв, цифр, _ и @
|
||||
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, '')
|
||||
@ -665,9 +771,9 @@ export function UserSettings() {
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative">
|
||||
<Avatar className="h-16 w-16">
|
||||
{user?.avatar ? (
|
||||
{(localAvatarUrl || user?.avatar) ? (
|
||||
<Image
|
||||
src={user.avatar}
|
||||
src={localAvatarUrl || user.avatar}
|
||||
alt="Аватар"
|
||||
width={64}
|
||||
height={64}
|
||||
@ -724,8 +830,16 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Номер телефона организации</Label>
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
value={formData.orgPhone || ''}
|
||||
onChange={(e) => handleInputChange('orgPhone', e.target.value)}
|
||||
onChange={(e) => handlePhoneInputChange('orgPhone', e.target.value, phoneInputRef)}
|
||||
onKeyDown={(e) => {
|
||||
// Предотвращаем удаление +7
|
||||
if ((e.key === 'Backspace' || e.key === 'Delete') &&
|
||||
phoneInputRef.current?.selectionStart <= 2) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
@ -789,8 +903,16 @@ export function UserSettings() {
|
||||
WhatsApp
|
||||
</Label>
|
||||
<Input
|
||||
ref={whatsappInputRef}
|
||||
value={formData.whatsapp || ''}
|
||||
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
|
||||
onChange={(e) => handlePhoneInputChange('whatsapp', e.target.value, whatsappInputRef)}
|
||||
onKeyDown={(e) => {
|
||||
// Предотвращаем удаление +7
|
||||
if ((e.key === 'Backspace' || e.key === 'Delete') &&
|
||||
whatsappInputRef.current?.selectionStart <= 2) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
|
Reference in New Issue
Block a user