Redesign p1
This commit is contained in:
@@ -7,15 +7,15 @@ import { usersApi, telegramApi, authApi } from '@/api'
|
||||
import type { UserStats } from '@/types'
|
||||
import { useToast } from '@/store/toast'
|
||||
import {
|
||||
Button, Input, Card, CardHeader, CardTitle, CardContent, clearAvatarCache
|
||||
NeonButton, Input, GlassCard, StatsCard, clearAvatarCache
|
||||
} from '@/components/ui'
|
||||
import {
|
||||
User, Camera, Trophy, Target, CheckCircle, Flame,
|
||||
Loader2, MessageCircle, Link2, Link2Off, ExternalLink,
|
||||
Eye, EyeOff, Save, KeyRound
|
||||
Eye, EyeOff, Save, KeyRound, Shield
|
||||
} from 'lucide-react'
|
||||
|
||||
// Схемы валидации
|
||||
// Schemas
|
||||
const nicknameSchema = z.object({
|
||||
nickname: z.string().min(2, 'Минимум 2 символа').max(50, 'Максимум 50 символов'),
|
||||
})
|
||||
@@ -36,7 +36,7 @@ export function ProfilePage() {
|
||||
const { user, updateUser } = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// Состояние
|
||||
// State
|
||||
const [stats, setStats] = useState<UserStats | null>(null)
|
||||
const [isLoadingStats, setIsLoadingStats] = useState(true)
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
@@ -53,7 +53,7 @@ export function ProfilePage() {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Формы
|
||||
// Forms
|
||||
const nicknameForm = useForm<NicknameForm>({
|
||||
resolver: zodResolver(nicknameSchema),
|
||||
defaultValues: { nickname: user?.nickname || '' },
|
||||
@@ -64,7 +64,7 @@ export function ProfilePage() {
|
||||
defaultValues: { current_password: '', new_password: '', confirm_password: '' },
|
||||
})
|
||||
|
||||
// Загрузка статистики
|
||||
// Load stats
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
return () => {
|
||||
@@ -72,7 +72,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Загрузка аватарки через API
|
||||
// Load avatar via API
|
||||
useEffect(() => {
|
||||
if (user?.id && user?.avatar_url) {
|
||||
loadAvatar(user.id)
|
||||
@@ -98,7 +98,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем форму никнейма при изменении user
|
||||
// Update nickname form when user changes
|
||||
useEffect(() => {
|
||||
if (user?.nickname) {
|
||||
nicknameForm.reset({ nickname: user.nickname })
|
||||
@@ -116,7 +116,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление никнейма
|
||||
// Update nickname
|
||||
const onNicknameSubmit = async (data: NicknameForm) => {
|
||||
try {
|
||||
const updatedUser = await usersApi.updateNickname(data)
|
||||
@@ -127,7 +127,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка аватара
|
||||
// Upload avatar
|
||||
const handleAvatarClick = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
@@ -136,7 +136,6 @@ export function ProfilePage() {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Валидация
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Файл должен быть изображением')
|
||||
return
|
||||
@@ -150,9 +149,7 @@ export function ProfilePage() {
|
||||
try {
|
||||
const updatedUser = await usersApi.uploadAvatar(file)
|
||||
updateUser({ avatar_url: updatedUser.avatar_url })
|
||||
// Перезагружаем аватарку через API
|
||||
if (user?.id) {
|
||||
// Очищаем старый blob URL и глобальный кэш
|
||||
if (avatarBlobUrl) {
|
||||
URL.revokeObjectURL(avatarBlobUrl)
|
||||
}
|
||||
@@ -167,7 +164,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Смена пароля
|
||||
// Change password
|
||||
const onPasswordSubmit = async (data: PasswordForm) => {
|
||||
try {
|
||||
await usersApi.changePassword({
|
||||
@@ -184,7 +181,7 @@ export function ProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Telegram функции
|
||||
// Telegram functions
|
||||
const startPolling = () => {
|
||||
setIsPolling(true)
|
||||
let attempts = 0
|
||||
@@ -245,265 +242,285 @@ export function ProfilePage() {
|
||||
}
|
||||
|
||||
const isLinked = !!user?.telegram_id
|
||||
// Приоритет: загруженная аватарка (blob) > телеграм аватарка
|
||||
const displayAvatar = avatarBlobUrl || user?.telegram_avatar_url
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<h1 className="text-2xl font-bold text-white">Мой профиль</h1>
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Мой профиль</h1>
|
||||
<p className="text-gray-400">Настройки вашего аккаунта</p>
|
||||
</div>
|
||||
|
||||
{/* Карточка профиля */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* Аватар */}
|
||||
<div className="relative group flex-shrink-0">
|
||||
{isLoadingAvatar ? (
|
||||
<div className="w-24 h-24 rounded-full bg-gray-700 animate-pulse" />
|
||||
) : (
|
||||
<button
|
||||
onClick={handleAvatarClick}
|
||||
disabled={isUploadingAvatar}
|
||||
className="relative w-24 h-24 rounded-full overflow-hidden bg-gray-700 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{displayAvatar ? (
|
||||
<img
|
||||
src={displayAvatar}
|
||||
alt={user?.nickname}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<User className="w-12 h-12 text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{isUploadingAvatar ? (
|
||||
<Loader2 className="w-6 h-6 text-white animate-spin" />
|
||||
) : (
|
||||
<Camera className="w-6 h-6 text-white" />
|
||||
)}
|
||||
{/* Profile Card */}
|
||||
<GlassCard variant="neon">
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
|
||||
{/* Avatar */}
|
||||
<div className="relative group flex-shrink-0">
|
||||
{isLoadingAvatar ? (
|
||||
<div className="w-28 h-28 rounded-2xl bg-dark-700 skeleton" />
|
||||
) : (
|
||||
<button
|
||||
onClick={handleAvatarClick}
|
||||
disabled={isUploadingAvatar}
|
||||
className="relative w-28 h-28 rounded-2xl overflow-hidden bg-dark-700 border-2 border-neon-500/30 hover:border-neon-500 transition-all group-hover:shadow-[0_0_30px_rgba(0,240,255,0.3)]"
|
||||
>
|
||||
{displayAvatar ? (
|
||||
<img
|
||||
src={displayAvatar}
|
||||
alt={user?.nickname}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-neon-500/20 to-accent-500/20">
|
||||
<User className="w-12 h-12 text-gray-500" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Форма никнейма */}
|
||||
<div className="flex-1">
|
||||
<form onSubmit={nicknameForm.handleSubmit(onNicknameSubmit)} className="space-y-4">
|
||||
<Input
|
||||
label="Никнейм"
|
||||
{...nicknameForm.register('nickname')}
|
||||
error={nicknameForm.formState.errors.nickname?.message}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isLoading={nicknameForm.formState.isSubmitting}
|
||||
disabled={!nicknameForm.formState.isDirty}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Сохранить
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Статистика */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||||
Статистика
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingStats ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="bg-gray-900 rounded-lg p-4 text-center">
|
||||
<div className="w-6 h-6 bg-gray-700 rounded mx-auto mb-2 animate-pulse" />
|
||||
<div className="h-8 w-12 bg-gray-700 rounded mx-auto mb-2 animate-pulse" />
|
||||
<div className="h-4 w-16 bg-gray-700 rounded mx-auto animate-pulse" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-dark-900/70 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{isUploadingAvatar ? (
|
||||
<Loader2 className="w-8 h-8 text-neon-500 animate-spin" />
|
||||
) : (
|
||||
<Camera className="w-8 h-8 text-neon-500" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-900 rounded-lg p-4 text-center">
|
||||
<Target className="w-6 h-6 text-blue-400 mx-auto mb-2" />
|
||||
<div className="text-2xl font-bold text-white">{stats.marathons_count}</div>
|
||||
<div className="text-sm text-gray-400">Марафонов</div>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Nickname Form */}
|
||||
<div className="flex-1 w-full sm:w-auto">
|
||||
<form onSubmit={nicknameForm.handleSubmit(onNicknameSubmit)} className="space-y-4">
|
||||
<Input
|
||||
label="Никнейм"
|
||||
{...nicknameForm.register('nickname')}
|
||||
error={nicknameForm.formState.errors.nickname?.message}
|
||||
/>
|
||||
<NeonButton
|
||||
type="submit"
|
||||
size="sm"
|
||||
isLoading={nicknameForm.formState.isSubmitting}
|
||||
disabled={!nicknameForm.formState.isDirty}
|
||||
icon={<Save className="w-4 h-4" />}
|
||||
>
|
||||
Сохранить
|
||||
</NeonButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
{/* Stats */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||||
Статистика
|
||||
</h2>
|
||||
{isLoadingStats ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="glass rounded-xl p-4">
|
||||
<div className="w-12 h-12 bg-dark-700 rounded-lg mb-3 skeleton" />
|
||||
<div className="h-8 w-16 bg-dark-700 rounded mb-2 skeleton" />
|
||||
<div className="h-4 w-20 bg-dark-700 rounded skeleton" />
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-4 text-center">
|
||||
<Trophy className="w-6 h-6 text-yellow-500 mx-auto mb-2" />
|
||||
<div className="text-2xl font-bold text-white">{stats.wins_count}</div>
|
||||
<div className="text-sm text-gray-400">Побед</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-4 text-center">
|
||||
<CheckCircle className="w-6 h-6 text-green-500 mx-auto mb-2" />
|
||||
<div className="text-2xl font-bold text-white">{stats.completed_assignments}</div>
|
||||
<div className="text-sm text-gray-400">Заданий</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-4 text-center">
|
||||
<Flame className="w-6 h-6 text-orange-500 mx-auto mb-2" />
|
||||
<div className="text-2xl font-bold text-white">{stats.total_points_earned}</div>
|
||||
<div className="text-sm text-gray-400">Очков</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400 text-center">Не удалось загрузить статистику</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatsCard
|
||||
label="Марафонов"
|
||||
value={stats.marathons_count}
|
||||
icon={<Target className="w-6 h-6" />}
|
||||
color="neon"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Побед"
|
||||
value={stats.wins_count}
|
||||
icon={<Trophy className="w-6 h-6" />}
|
||||
color="purple"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Заданий"
|
||||
value={stats.completed_assignments}
|
||||
icon={<CheckCircle className="w-6 h-6" />}
|
||||
color="neon"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Очков"
|
||||
value={stats.total_points_earned}
|
||||
icon={<Flame className="w-6 h-6" />}
|
||||
color="pink"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<GlassCard className="text-center py-8">
|
||||
<p className="text-gray-400">Не удалось загрузить статистику</p>
|
||||
</GlassCard>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Telegram */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageCircle className="w-5 h-5 text-blue-400" />
|
||||
Telegram
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLinked ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 p-4 bg-gray-900 rounded-lg">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-500/20 flex items-center justify-center overflow-hidden">
|
||||
{user?.telegram_avatar_url ? (
|
||||
<img
|
||||
src={user.telegram_avatar_url}
|
||||
alt="Telegram avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Link2 className="w-6 h-6 text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium">
|
||||
{user?.telegram_first_name} {user?.telegram_last_name}
|
||||
</p>
|
||||
{user?.telegram_username && (
|
||||
<p className="text-blue-400 text-sm">@{user.telegram_username}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={handleUnlinkTelegram}
|
||||
isLoading={telegramLoading}
|
||||
>
|
||||
<Link2Off className="w-4 h-4 mr-2" />
|
||||
Отвязать
|
||||
</Button>
|
||||
<GlassCard>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
||||
<MessageCircle className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Telegram</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
{isLinked ? 'Аккаунт привязан' : 'Привяжите для уведомлений'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLinked ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 p-4 bg-dark-700/50 rounded-xl border border-dark-600">
|
||||
<div className="w-14 h-14 rounded-xl bg-blue-500/20 flex items-center justify-center overflow-hidden border border-blue-500/30">
|
||||
{user?.telegram_avatar_url ? (
|
||||
<img
|
||||
src={user.telegram_avatar_url}
|
||||
alt="Telegram avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Link2 className="w-7 h-7 text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium">
|
||||
{user?.telegram_first_name} {user?.telegram_last_name}
|
||||
</p>
|
||||
{user?.telegram_username && (
|
||||
<p className="text-blue-400 text-sm">@{user.telegram_username}</p>
|
||||
)}
|
||||
</div>
|
||||
<NeonButton
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={handleUnlinkTelegram}
|
||||
isLoading={telegramLoading}
|
||||
icon={<Link2Off className="w-4 h-4" />}
|
||||
>
|
||||
Отвязать
|
||||
</NeonButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-400">
|
||||
Привяжи Telegram для получения уведомлений о событиях и марафонах.
|
||||
</p>
|
||||
{isPolling ? (
|
||||
<div className="p-4 bg-blue-500/20 border border-blue-500/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="w-5 h-5 text-blue-400 animate-spin" />
|
||||
<p className="text-blue-400">Ожидание привязки...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-400">
|
||||
Привяжите Telegram для получения уведомлений о событиях и марафонах.
|
||||
</p>
|
||||
{isPolling ? (
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="w-5 h-5 text-blue-400 animate-spin" />
|
||||
<p className="text-blue-400">Ожидание привязки...</p>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={handleLinkTelegram} isLoading={telegramLoading}>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Привязать Telegram
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Смена пароля */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<KeyRound className="w-5 h-5 text-gray-400" />
|
||||
Безопасность
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!showPasswordForm ? (
|
||||
<Button variant="secondary" onClick={() => setShowPasswordForm(true)}>
|
||||
Сменить пароль
|
||||
</Button>
|
||||
) : (
|
||||
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
label="Текущий пароль"
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('current_password')}
|
||||
error={passwordForm.formState.errors.current_password?.message}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
className="absolute right-3 top-8 text-gray-400 hover:text-white"
|
||||
>
|
||||
{showCurrentPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<NeonButton
|
||||
onClick={handleLinkTelegram}
|
||||
isLoading={telegramLoading}
|
||||
icon={<ExternalLink className="w-4 h-4" />}
|
||||
>
|
||||
Привязать Telegram
|
||||
</NeonButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</GlassCard>
|
||||
|
||||
<div className="relative">
|
||||
<Input
|
||||
label="Новый пароль"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('new_password')}
|
||||
error={passwordForm.formState.errors.new_password?.message}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
className="absolute right-3 top-8 text-gray-400 hover:text-white"
|
||||
>
|
||||
{showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
{/* Security */}
|
||||
<GlassCard>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-500/20 flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-accent-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Безопасность</h2>
|
||||
<p className="text-sm text-gray-400">Управление паролем</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showPasswordForm ? (
|
||||
<NeonButton
|
||||
variant="secondary"
|
||||
onClick={() => setShowPasswordForm(true)}
|
||||
icon={<KeyRound className="w-4 h-4" />}
|
||||
>
|
||||
Сменить пароль
|
||||
</NeonButton>
|
||||
) : (
|
||||
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Input
|
||||
label="Подтвердите новый пароль"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('confirm_password')}
|
||||
error={passwordForm.formState.errors.confirm_password?.message}
|
||||
label="Текущий пароль"
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('current_password')}
|
||||
error={passwordForm.formState.errors.current_password?.message}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
className="absolute right-3 top-9 text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{showCurrentPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" isLoading={passwordForm.formState.isSubmitting}>
|
||||
Сменить пароль
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false)
|
||||
passwordForm.reset()
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="relative">
|
||||
<Input
|
||||
label="Новый пароль"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('new_password')}
|
||||
error={passwordForm.formState.errors.new_password?.message}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
className="absolute right-3 top-9 text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Подтвердите новый пароль"
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
{...passwordForm.register('confirm_password')}
|
||||
error={passwordForm.formState.errors.confirm_password?.message}
|
||||
/>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<NeonButton
|
||||
type="submit"
|
||||
isLoading={passwordForm.formState.isSubmitting}
|
||||
icon={<Save className="w-4 h-4" />}
|
||||
>
|
||||
Сменить пароль
|
||||
</NeonButton>
|
||||
<NeonButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false)
|
||||
passwordForm.reset()
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</NeonButton>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</GlassCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user