Files
NeveTimePanel/frontend/src/components/Profile.jsx
arkonsadter fbfddf3c7a
All checks were successful
continuous-integration/drone/push Build is passing
Changed design and bug fixes
2026-01-16 15:40:14 +06:00

425 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { User, Lock, Server, MessageSquare, Shield, TrendingUp, Eye, EyeOff } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
import { notify } from './NotificationSystem';
export default function Profile({ token, user, onUsernameChange, viewingUsername }) {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
const isViewingOther = viewingUsername && viewingUsername !== user?.username;
// Форма смены имени
const [usernameForm, setUsernameForm] = useState({
new_username: '',
password: ''
});
const [usernameLoading, setUsernameLoading] = useState(false);
const [showUsernamePassword, setShowUsernamePassword] = useState(false);
// Форма смены пароля
const [passwordForm, setPasswordForm] = useState({
old_password: '',
new_password: '',
confirm_password: ''
});
const [passwordLoading, setPasswordLoading] = useState(false);
const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
useEffect(() => {
loadStats();
}, [viewingUsername]);
const loadStats = async () => {
try {
const endpoint = isViewingOther
? `${API_URL}/api/profile/stats/${viewingUsername}`
: `${API_URL}/api/profile/stats`;
const { data } = await axios.get(endpoint, {
headers: { Authorization: `Bearer ${token}` }
});
setStats(data);
setLoading(false);
} catch (error) {
console.error('Ошибка загрузки статистики:', error);
setLoading(false);
}
};
const handleUsernameChange = async (e) => {
e.preventDefault();
if (!usernameForm.new_username.trim() || !usernameForm.password) {
alert('Заполните все поля');
return;
}
setUsernameLoading(true);
try {
const { data } = await axios.put(
`${API_URL}/api/profile/username`,
usernameForm,
{ headers: { Authorization: `Bearer ${token}` } }
);
localStorage.setItem('token', data.access_token);
notify('success', 'Имя изменено', `Ваше новое имя: ${data.username}`);
alert('Имя пользователя успешно изменено!');
setUsernameForm({ new_username: '', password: '' });
if (onUsernameChange) {
onUsernameChange(data.access_token, data.username);
}
loadStats();
} catch (error) {
notify('error', 'Ошибка изменения', error.response?.data?.detail || 'Не удалось изменить имя');
alert(error.response?.data?.detail || 'Ошибка изменения имени пользователя');
} finally {
setUsernameLoading(false);
}
};
const handlePasswordChange = async (e) => {
e.preventDefault();
if (!passwordForm.old_password || !passwordForm.new_password || !passwordForm.confirm_password) {
alert('Заполните все поля');
return;
}
if (passwordForm.new_password !== passwordForm.confirm_password) {
alert('Новые пароли не совпадают');
return;
}
if (passwordForm.new_password.length < 6) {
alert('Новый пароль должен быть не менее 6 символов');
return;
}
setPasswordLoading(true);
try {
await axios.put(
`${API_URL}/api/profile/password`,
{
old_password: passwordForm.old_password,
new_password: passwordForm.new_password
},
{ headers: { Authorization: `Bearer ${token}` } }
);
notify('success', 'Пароль изменён', 'Ваш пароль успешно обновлен');
alert('Пароль успешно изменён!');
setPasswordForm({ old_password: '', new_password: '', confirm_password: '' });
} catch (error) {
notify('error', 'Ошибка изменения', error.response?.data?.detail || 'Не удалось изменить пароль');
alert(error.response?.data?.detail || 'Ошибка изменения пароля');
} finally {
setPasswordLoading(false);
}
};
const getRoleName = (role) => {
switch (role) {
case 'owner': return 'Владелец';
case 'admin': return 'Администратор';
case 'support': return 'Тех. поддержка';
case 'banned': return 'Забанен';
default: return 'Пользователь';
}
};
const getRoleColor = (role) => {
switch (role) {
case 'owner': return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
case 'admin': return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
case 'support': return 'bg-purple-500/20 text-purple-500 border-purple-500/50';
case 'banned': return 'bg-red-500/20 text-red-500 border-red-500/50';
default: return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
}
};
if (loading) {
return (
<div className="h-full bg-dark-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400">Загрузка профиля...</p>
</div>
</div>
);
}
return (
<div className="h-full bg-dark-900 text-white p-6 overflow-y-auto">
<div className="max-w-6xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold mb-2">
{isViewingOther ? `Профиль пользователя: ${viewingUsername}` : 'Личный кабинет'}
</h1>
<p className="text-gray-400">
{isViewingOther ? 'Просмотр профиля другого пользователя' : 'Управление профилем и настройками'}
</p>
</div>
{!isViewingOther && (
<div className="card mb-6 p-2 flex gap-2">
<button
onClick={() => setActiveTab('overview')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'overview' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
}`}
>
<TrendingUp className="w-4 h-4 inline mr-2" />
Обзор
</button>
<button
onClick={() => setActiveTab('username')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'username' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
}`}
>
<User className="w-4 h-4 inline mr-2" />
Имя пользователя
</button>
<button
onClick={() => setActiveTab('password')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'password' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
}`}
>
<Lock className="w-4 h-4 inline mr-2" />
Пароль
</button>
</div>
)}
{(activeTab === 'overview' || isViewingOther) && (
<div className="space-y-6">
<div className="card p-6">
<div className="flex items-center gap-4 mb-6">
<div className="bg-primary-600 p-4 rounded-2xl">
<User className="w-8 h-8 text-white" />
</div>
<div>
<h2 className="text-2xl font-bold">{stats?.username}</h2>
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-lg border mt-2 ${getRoleColor(stats?.role)}`}>
<Shield className="w-4 h-4" />
<span className="font-medium">{getRoleName(stats?.role)}</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="card p-6">
<div className="flex items-center gap-3 mb-4">
<div className="bg-primary-600 p-3 rounded-xl">
<Server className="w-6 h-6 text-white" />
</div>
<div>
<p className="text-sm text-gray-400">Всего серверов</p>
<p className="text-2xl font-bold">{stats?.total_servers || 0}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Мои серверы:</span>
<span className="font-medium">{stats?.owned_servers?.length || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Доступные:</span>
<span className="font-medium">{stats?.accessible_servers?.length || 0}</span>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center gap-3 mb-4">
<div className="bg-primary-600 p-3 rounded-xl">
<MessageSquare className="w-6 h-6 text-white" />
</div>
<div>
<p className="text-sm text-gray-400">Мои тикеты</p>
<p className="text-2xl font-bold">{stats?.tickets?.total || 0}</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-yellow-500">На рассмотрении:</span>
<span className="font-medium">{stats?.tickets?.pending || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-blue-500">В работе:</span>
<span className="font-medium">{stats?.tickets?.in_progress || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-green-500">Закрыто:</span>
<span className="font-medium">{stats?.tickets?.closed || 0}</span>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center gap-3 mb-4">
<div className="bg-primary-600 p-3 rounded-xl">
<Shield className="w-6 h-6 text-white" />
</div>
<div>
<p className="text-sm text-gray-400">Ваша роль</p>
<p className="text-xl font-bold">{getRoleName(stats?.role)}</p>
</div>
</div>
<div className="text-sm text-gray-400">
{stats?.role === 'owner' && '👑 Владелец панели - полный контроль над всеми функциями'}
{stats?.role === 'admin' && 'Полный доступ ко всем функциям панели'}
{stats?.role === 'support' && 'Доступ к системе тикетов и поддержке'}
{stats?.role === 'user' && 'Доступ к своим серверам и тикетам'}
{stats?.role === 'banned' && '⛔ Аккаунт заблокирован, доступ запрещён'}
</div>
</div>
</div>
{stats?.owned_servers?.length > 0 && (
<div className="card p-6">
<h3 className="text-lg font-bold mb-4">Мои серверы</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{stats.owned_servers.map((server) => (
<div key={server.name} className="bg-dark-700 border border-gray-700 rounded-xl p-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5" />
<div>
<p className="font-medium">{server.displayName}</p>
<p className="text-xs text-gray-400">{server.name}</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{activeTab === 'username' && (
<div className="card p-6 max-w-2xl mx-auto">
<h2 className="text-xl font-bold mb-6">Изменить имя пользователя</h2>
<form onSubmit={handleUsernameChange} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2 text-white">Текущее имя пользователя</label>
<input type="text" value={stats?.username} disabled className="input cursor-not-allowed opacity-50" />
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">Новое имя пользователя</label>
<input
type="text"
value={usernameForm.new_username}
onChange={(e) => setUsernameForm({ ...usernameForm, new_username: e.target.value })}
placeholder="Введите новое имя"
className="input"
/>
<p className="text-xs text-gray-400 mt-1">Минимум 3 символа</p>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">Подтвердите паролем</label>
<div className="relative">
<input
type={showUsernamePassword ? 'text' : 'password'}
value={usernameForm.password}
onChange={(e) => setUsernameForm({ ...usernameForm, password: e.target.value })}
placeholder="Введите текущий пароль"
className="input pr-12"
/>
<button
type="button"
onClick={() => setShowUsernamePassword(!showUsernamePassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
>
{showUsernamePassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div className="bg-dark-700 border border-gray-700 rounded-xl p-4">
<p className="text-sm text-gray-400">
После изменения имени пользователя вы будете автоматически перелогинены с новым именем.
</p>
</div>
<button type="submit" disabled={usernameLoading} className="btn-primary w-full disabled:opacity-50">
{usernameLoading ? 'Изменение...' : 'Изменить имя пользователя'}
</button>
</form>
</div>
)}
{activeTab === 'password' && (
<div className="card p-6 max-w-2xl mx-auto">
<h2 className="text-xl font-bold mb-6">Изменить пароль</h2>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2 text-white">Текущий пароль</label>
<div className="relative">
<input
type={showOldPassword ? 'text' : 'password'}
value={passwordForm.old_password}
onChange={(e) => setPasswordForm({ ...passwordForm, old_password: e.target.value })}
placeholder="Введите текущий пароль"
className="input pr-12"
/>
<button
type="button"
onClick={() => setShowOldPassword(!showOldPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
>
{showOldPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">Новый пароль</label>
<div className="relative">
<input
type={showNewPassword ? 'text' : 'password'}
value={passwordForm.new_password}
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
placeholder="Введите новый пароль"
className="input pr-12"
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
>
{showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
<p className="text-xs text-gray-400 mt-1">Минимум 6 символов</p>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">Подтвердите новый пароль</label>
<input
type="password"
value={passwordForm.confirm_password}
onChange={(e) => setPasswordForm({ ...passwordForm, confirm_password: e.target.value })}
placeholder="Повторите новый пароль"
className="input"
/>
</div>
<div className="bg-dark-700 border border-gray-700 rounded-xl p-4">
<p className="text-sm text-gray-400">
После изменения пароля используйте новый пароль для входа в систему.
</p>
</div>
<button type="submit" disabled={passwordLoading} className="btn-primary w-full disabled:opacity-50">
{passwordLoading ? 'Изменение...' : 'Изменить пароль'}
</button>
</form>
</div>
)}
</div>
</div>
);
}