Initial commit

This commit is contained in:
2026-01-14 20:23:10 +06:00
commit 954dd473d1
57 changed files with 8854 additions and 0 deletions

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://26.62.117.104:8000

3
frontend/.env.example Normal file
View File

@@ -0,0 +1,3 @@
# API URL (необязательно, по умолчанию определяется автоматически)
# Раскомментируйте и укажите ваш IP для удаленного доступа
# VITE_API_URL=http://26.123.45.67:8000

View File

@@ -0,0 +1,10 @@
# Создайте файл .env.local и раскомментируйте нужную строку
# Для локального использования (по умолчанию)
# VITE_API_URL=http://localhost:8000
# Для Radmin VPN (замените на ваш IP)
# VITE_API_URL=http://26.62.117.104:8000
# Для Hamachi (замените на ваш IP)
# VITE_API_URL=http://25.123.45.67:8000

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MC Panel</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2988
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "mc-panel",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview --host"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"axios": "^1.6.7",
"@xterm/xterm": "^5.3.0",
"lucide-react": "^0.323.0"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

392
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,392 @@
import { useState, useEffect } from 'react';
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X } from 'lucide-react';
import Console from './components/Console';
import FileManager from './components/FileManager';
import Stats from './components/Stats';
import ServerSettings from './components/ServerSettings';
import CreateServerModal from './components/CreateServerModal';
import Users from './components/Users';
import Auth from './components/Auth';
import ErrorBoundary from './components/ErrorBoundary';
import ThemeSelector from './components/ThemeSelector';
import axios from 'axios';
import { API_URL } from './config';
import { getTheme } from './themes';
function App() {
const [token, setToken] = useState(localStorage.getItem('token'));
const [user, setUser] = useState(null);
const [servers, setServers] = useState([]);
const [selectedServer, setSelectedServer] = useState(null);
const [activeTab, setActiveTab] = useState('console');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showUsers, setShowUsers] = useState(false);
const [connectionError, setConnectionError] = useState(false);
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
const [sidebarOpen, setSidebarOpen] = useState(true);
const currentTheme = getTheme(theme);
useEffect(() => {
if (token) {
loadUser();
loadServers();
const interval = setInterval(loadServers, 5000);
return () => clearInterval(interval);
}
}, [token]);
const loadUser = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` }
});
setUser(data);
} catch (error) {
console.error('Ошибка загрузки пользователя:', error);
handleLogout();
}
};
const loadServers = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers`, {
headers: { Authorization: `Bearer ${token}` }
});
setServers(data);
setConnectionError(false);
} catch (error) {
console.error('Ошибка загрузки серверов:', error);
if (error.response?.status === 401) {
handleLogout();
} else {
setConnectionError(true);
}
}
};
const handleLogin = async (username, password, isLogin) => {
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register';
const { data } = await axios.post(`${API_URL}${endpoint}`, {
username,
password
});
localStorage.setItem('token', data.access_token);
setToken(data.access_token);
setUser({ username: data.username, role: data.role });
};
const handleLogout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setServers([]);
setSelectedServer(null);
};
const handleServerDeleted = () => {
setSelectedServer(null);
loadServers();
};
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
const startServer = async (serverName) => {
try {
const response = await axios.post(
`${API_URL}/api/servers/${serverName}/start`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Сервер запущен:', response.data);
setTimeout(() => {
loadServers();
}, 1000);
} catch (error) {
console.error('Ошибка запуска сервера:', error);
alert(error.response?.data?.detail || 'Ошибка запуска сервера');
}
};
const stopServer = async (serverName) => {
try {
const response = await axios.post(
`${API_URL}/api/servers/${serverName}/stop`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Сервер остановлен:', response.data);
setTimeout(() => {
loadServers();
}, 1000);
} catch (error) {
console.error('Ошибка остановки сервера:', error);
alert(error.response?.data?.detail || 'Ошибка остановки сервера');
}
};
if (!token) {
return <Auth onLogin={handleLogin} />;
}
if (showUsers) {
return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold">MC Panel</h1>
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
<button
onClick={() => setShowUsers(false)}
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
>
<Server className="w-4 h-4" />
Серверы
</button>
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</div>
</header>
<Users token={token} theme={currentTheme} />
</div>
);
}
return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
{/* Header */}
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className={`lg:hidden ${currentTheme.hover} p-2 rounded-lg transition`}
>
{sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div>
<div>
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1>
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
{connectionError && (
<div className="bg-red-500 bg-opacity-20 border border-red-500 px-3 py-1.5 rounded-lg text-sm text-red-400">
Нет связи
</div>
)}
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
{user?.role === 'admin' && (
<button
onClick={() => setShowUsers(true)}
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<UsersIcon className="w-4 h-4" />
<span className="hidden sm:inline">Пользователи</span>
</button>
)}
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
<span className="hidden sm:inline">Выход</span>
</button>
</div>
</div>
</div>
</header>
<div className="flex h-[calc(100vh-89px)]">
{/* Sidebar */}
<aside className={`${sidebarOpen ? 'w-80' : 'w-0'} ${currentTheme.secondary} ${currentTheme.border} border-r transition-all duration-300 overflow-hidden`}>
<div className="p-4 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Мои серверы</h2>
<button
onClick={() => setShowCreateModal(true)}
className={`${currentTheme.accent} ${currentTheme.accentHover} p-2 rounded-lg transition text-white`}
title="Создать сервер"
>
<Plus className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto space-y-2">
{servers.map((server) => (
<div
key={server.name}
className={`p-4 rounded-lg cursor-pointer transition-all duration-200 ${
selectedServer === server.name
? `${currentTheme.accent} text-white shadow-lg`
: `${currentTheme.card} ${currentTheme.hover}`
}`}
onClick={() => setSelectedServer(server.name)}
>
<div className="flex items-center justify-between mb-3">
<span className="font-medium truncate">{server.displayName}</span>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
server.status === 'running' ? 'bg-green-400 animate-pulse' : 'bg-gray-500'
}`}
/>
</div>
</div>
<div className="flex gap-2">
{server.status === 'stopped' ? (
<button
onClick={(e) => {
e.stopPropagation();
startServer(server.name);
}}
className="flex-1 bg-green-600 hover:bg-green-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition"
>
<Play className="w-3.5 h-3.5" />
Запустить
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
stopServer(server.name);
}}
className="flex-1 bg-red-600 hover:bg-red-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition"
>
<Square className="w-3.5 h-3.5" />
Остановить
</button>
)}
</div>
</div>
))}
{servers.length === 0 && (
<div className={`text-center py-12 ${currentTheme.textSecondary}`}>
<Server className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">Нет серверов</p>
<p className="text-xs mt-1">Создайте первый сервер</p>
</div>
)}
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col overflow-hidden">
{selectedServer ? (
<>
{/* Tabs */}
<div className={`${currentTheme.secondary} ${currentTheme.border} border-b flex overflow-x-auto`}>
{[
{ id: 'console', icon: Terminal, label: 'Консоль' },
{ id: 'files', icon: FolderOpen, label: 'Файлы' },
{ id: 'stats', icon: HardDrive, label: 'Статистика' },
{ id: 'settings', icon: Settings, label: 'Настройки' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-6 py-4 flex items-center gap-2 transition-all duration-200 border-b-2 ${
activeTab === tab.id
? `${currentTheme.accent.replace('bg-', 'border-')} ${currentTheme.text}`
: `border-transparent ${currentTheme.textSecondary} ${currentTheme.hover}`
}`}
>
<tab.icon className="w-4 h-4" />
<span className="font-medium">{tab.label}</span>
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-hidden">
<ErrorBoundary>
{activeTab === 'console' && <Console serverName={selectedServer} token={token} theme={currentTheme} />}
{activeTab === 'files' && <FileManager serverName={selectedServer} token={token} theme={currentTheme} />}
{activeTab === 'stats' && <Stats serverName={selectedServer} token={token} theme={currentTheme} />}
{activeTab === 'settings' && (
<ServerSettings
serverName={selectedServer}
token={token}
user={user}
theme={currentTheme}
onDeleted={handleServerDeleted}
/>
)}
</ErrorBoundary>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className={`${currentTheme.card} p-8 rounded-2xl ${currentTheme.border} border`}>
<Server className={`w-16 h-16 mx-auto mb-4 ${currentTheme.textSecondary} opacity-50`} />
<p className="text-xl font-medium mb-2">Выберите сервер</p>
<p className={`text-sm ${currentTheme.textSecondary}`}>
Выберите сервер из списка слева или создайте новый
</p>
</div>
</div>
</div>
)}
</main>
</div>
{showCreateModal && (
<CreateServerModal
token={token}
theme={currentTheme}
onClose={() => setShowCreateModal(false)}
onCreated={loadServers}
/>
)}
</div>
);
}
export default App;

343
frontend/src/App1.jsx Normal file
View File

@@ -0,0 +1,343 @@
import { useState, useEffect } from 'react';
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut } from 'lucide-react';
import Console from './components/Console';
import FileManager from './components/FileManager';
import Stats from './components/Stats';
import ServerSettings from './components/ServerSettings';
import CreateServerModal from './components/CreateServerModal';
import Users from './components/Users';
import Auth from './components/Auth';
import ErrorBoundary from './components/ErrorBoundary';
import axios from 'axios';
import { API_URL } from './config';
function App() {
const [token, setToken] = useState(localStorage.getItem('token'));
const [user, setUser] = useState(null);
const [servers, setServers] = useState([]);
const [selectedServer, setSelectedServer] = useState(null);
const [activeTab, setActiveTab] = useState('console');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showUsers, setShowUsers] = useState(false);
const [connectionError, setConnectionError] = useState(false);
useEffect(() => {
if (token) {
loadUser();
loadServers();
const interval = setInterval(loadServers, 5000);
return () => clearInterval(interval);
}
}, [token]);
const loadUser = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` }
});
setUser(data);
} catch (error) {
console.error('Ошибка загрузки пользователя:', error);
handleLogout();
}
};
const loadServers = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers`, {
headers: { Authorization: `Bearer ${token}` }
});
setServers(data);
setConnectionError(false);
} catch (error) {
console.error('Ошибка загрузки серверов:', error);
if (error.response?.status === 401) {
handleLogout();
} else {
setConnectionError(true);
}
}
};
const handleLogin = async (username, password, isLogin) => {
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register';
const { data } = await axios.post(`${API_URL}${endpoint}`, {
username,
password
});
localStorage.setItem('token', data.access_token);
setToken(data.access_token);
setUser({ username: data.username, role: data.role });
};
const handleLogout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setServers([]);
setSelectedServer(null);
};
const handleServerDeleted = () => {
setSelectedServer(null);
loadServers();
};
const startServer = async (serverName) => {
try {
const response = await axios.post(
`${API_URL}/api/servers/${serverName}/start`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Сервер запущен:', response.data);
setTimeout(() => {
loadServers();
}, 1000);
} catch (error) {
console.error('Ошибка запуска сервера:', error);
alert(error.response?.data?.detail || 'Ошибка запуска сервера');
}
};
const stopServer = async (serverName) => {
try {
const response = await axios.post(
`${API_URL}/api/servers/${serverName}/stop`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Сервер остановлен:', response.data);
setTimeout(() => {
loadServers();
}, 1000);
} catch (error) {
console.error('Ошибка остановки сервера:', error);
alert(error.response?.data?.detail || 'Ошибка остановки сервера');
}
};
if (!token) {
return <Auth onLogin={handleLogin} />;
}
if (showUsers) {
return (
<div className="min-h-screen bg-gray-900 text-white">
<header className="bg-gray-800 border-b border-gray-700 p-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Server className="w-8 h-8" />
MC Panel
</h1>
<div className="flex items-center gap-4">
<span className="text-gray-400">
{user?.username} ({user?.role === 'admin' ? 'Админ' : 'Пользователь'})
</span>
<button
onClick={() => setShowUsers(false)}
className="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded"
>
Назад к серверам
</button>
<button
onClick={handleLogout}
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</header>
<Users token={token} />
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 text-white">
<header className="bg-gray-800 border-b border-gray-700 p-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Server className="w-8 h-8" />
MC Panel
</h1>
<div className="flex items-center gap-4">
{connectionError && (
<div className="bg-red-600 px-4 py-2 rounded text-sm">
Нет связи с сервером
</div>
)}
<span className="text-gray-400">
{user?.username} ({user?.role === 'admin' ? 'Админ' : 'Пользователь'})
</span>
{user?.role === 'admin' && (
<button
onClick={() => setShowUsers(true)}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2"
>
<UsersIcon className="w-4 h-4" />
Пользователи
</button>
)}
<button
onClick={handleLogout}
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</header>
<div className="flex h-[calc(100vh-73px)]">
<aside className="w-64 bg-gray-800 border-r border-gray-700 p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Серверы</h2>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-600 hover:bg-blue-700 p-2 rounded"
title="Создать сервер"
>
<Plus className="w-4 h-4" />
</button>
</div>
{servers.map((server) => (
<div
key={server.name}
className={`p-3 mb-2 rounded cursor-pointer transition ${
selectedServer === server.name
? 'bg-blue-600'
: 'bg-gray-700 hover:bg-gray-600'
}`}
onClick={() => setSelectedServer(server.name)}
>
<div className="flex items-center justify-between">
<span className="font-medium truncate">{server.displayName}</span>
<span
className={`w-2 h-2 rounded-full flex-shrink-0 ${
server.status === 'running' ? 'bg-green-500' : 'bg-red-500'
}`}
/>
</div>
<div className="flex gap-2 mt-2">
{server.status === 'stopped' ? (
<button
onClick={(e) => {
e.stopPropagation();
startServer(server.name);
}}
className="flex-1 bg-green-600 hover:bg-green-700 p-1 rounded text-sm flex items-center justify-center gap-1"
>
<Play className="w-3 h-3" />
Старт
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
stopServer(server.name);
}}
className="flex-1 bg-red-600 hover:bg-red-700 p-1 rounded text-sm flex items-center justify-center gap-1"
>
<Square className="w-3 h-3" />
Стоп
</button>
)}
</div>
</div>
))}
</aside>
<main className="flex-1 flex flex-col">
{selectedServer ? (
<>
<div className="bg-gray-800 border-b border-gray-700 flex">
<button
onClick={() => setActiveTab('console')}
className={`px-6 py-3 flex items-center gap-2 ${
activeTab === 'console'
? 'bg-gray-900 border-b-2 border-blue-500'
: 'hover:bg-gray-700'
}`}
>
<Terminal className="w-4 h-4" />
Консоль
</button>
<button
onClick={() => setActiveTab('files')}
className={`px-6 py-3 flex items-center gap-2 ${
activeTab === 'files'
? 'bg-gray-900 border-b-2 border-blue-500'
: 'hover:bg-gray-700'
}`}
>
<FolderOpen className="w-4 h-4" />
Файлы
</button>
<button
onClick={() => setActiveTab('stats')}
className={`px-6 py-3 flex items-center gap-2 ${
activeTab === 'stats'
? 'bg-gray-900 border-b-2 border-blue-500'
: 'hover:bg-gray-700'
}`}
>
<HardDrive className="w-4 h-4" />
Статистика
</button>
<button
onClick={() => setActiveTab('settings')}
className={`px-6 py-3 flex items-center gap-2 ${
activeTab === 'settings'
? 'bg-gray-900 border-b-2 border-blue-500'
: 'hover:bg-gray-700'
}`}
>
<Settings className="w-4 h-4" />
Настройки
</button>
</div>
<div className="flex-1 overflow-hidden">
<ErrorBoundary>
{activeTab === 'console' && <Console serverName={selectedServer} token={token} />}
{activeTab === 'files' && <FileManager serverName={selectedServer} token={token} />}
{activeTab === 'stats' && <Stats serverName={selectedServer} token={token} />}
{activeTab === 'settings' && (
<ServerSettings
serverName={selectedServer}
token={token}
user={user}
onDeleted={handleServerDeleted}
/>
)}
</ErrorBoundary>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="text-center">
<Server className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p className="text-xl">Выберите сервер</p>
</div>
</div>
)}
</main>
</div>
{showCreateModal && (
<CreateServerModal
token={token}
onClose={() => setShowCreateModal(false)}
onCreated={loadServers}
/>
)}
</div>
);
}
export default App;

View File

@@ -0,0 +1,148 @@
import { useState } from 'react';
import { Server, Eye, EyeOff } from 'lucide-react';
import { getTheme } from '../themes';
export default function Auth({ onLogin }) {
const [isLogin, setIsLogin] = useState(true);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [theme] = useState(localStorage.getItem('theme') || 'dark');
const currentTheme = getTheme(theme);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await onLogin(username, password, isLogin);
} catch (err) {
setError(err.message || 'Ошибка авторизации');
} finally {
setLoading(false);
}
};
return (
<div className={`min-h-screen ${currentTheme.primary} flex items-center justify-center p-4 transition-colors duration-300`}>
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className={`${currentTheme.accent} w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg`}>
<Server className="w-10 h-10 text-white" />
</div>
<h1 className={`text-3xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent mb-2`}>MC Panel</h1>
<p className={`${currentTheme.textSecondary}`}>Панель управления Minecraft серверами</p>
</div>
{/* Form Card */}
<div className={`${currentTheme.secondary} rounded-2xl shadow-2xl ${currentTheme.border} border p-8`}>
{/* Tabs */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setIsLogin(true)}
className={`flex-1 py-3 rounded-xl font-medium transition-all duration-200 ${
isLogin
? `${currentTheme.accent} text-white shadow-lg`
: `${currentTheme.card} ${currentTheme.text} ${currentTheme.hover}`
}`}
>
Вход
</button>
<button
onClick={() => setIsLogin(false)}
className={`flex-1 py-3 rounded-xl font-medium transition-all duration-200 ${
!isLogin
? `${currentTheme.accent} text-white shadow-lg`
: `${currentTheme.card} ${currentTheme.text} ${currentTheme.hover}`
}`}
>
Регистрация
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Username */}
<div>
<label className={`block text-sm font-medium ${currentTheme.text} mb-2`}>
Имя пользователя
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className={`w-full ${currentTheme.input} ${currentTheme.border} border rounded-xl px-4 py-3 ${currentTheme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
placeholder="admin"
/>
</div>
{/* Password */}
<div>
<label className={`block text-sm font-medium ${currentTheme.text} mb-2`}>
Пароль
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className={`w-full ${currentTheme.input} ${currentTheme.border} border rounded-xl px-4 py-3 pr-12 ${currentTheme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className={`absolute right-3 top-1/2 -translate-y-1/2 ${currentTheme.textSecondary} hover:${currentTheme.text} transition`}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* Error */}
{error && (
<div className="bg-red-500 bg-opacity-10 border border-red-500 rounded-xl p-3 text-red-400 text-sm">
{error}
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={loading}
className={`w-full ${currentTheme.accent} ${currentTheme.accentHover} text-white py-3 rounded-xl font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl`}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
Загрузка...
</span>
) : (
isLogin ? 'Войти' : 'Зарегистрироваться'
)}
</button>
</form>
{/* Default Credentials */}
{isLogin && (
<div className={`mt-6 text-center text-sm ${currentTheme.textSecondary}`}>
<p>Учётные данные по умолчанию:</p>
<p className={`${currentTheme.text} font-mono mt-1`}>admin / admin</p>
</div>
)}
</div>
{/* Footer */}
<div className={`text-center mt-6 text-sm ${currentTheme.textSecondary}`}>
<p>© 2024 MC Panel. Все права защищены.</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,109 @@
import { useState } from 'react';
import { Server } from 'lucide-react';
export default function Auth({ onLogin }) {
const [isLogin, setIsLogin] = useState(true);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await onLogin(username, password, isLogin);
} catch (err) {
setError(err.message || 'Ошибка авторизации');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="bg-gray-800 rounded-lg p-8 w-full max-w-md border border-gray-700">
<div className="flex items-center justify-center mb-8">
<Server className="w-12 h-12 text-blue-500 mr-3" />
<h1 className="text-3xl font-bold text-white">MC Panel</h1>
</div>
<div className="flex gap-2 mb-6">
<button
onClick={() => setIsLogin(true)}
className={`flex-1 py-2 rounded ${
isLogin
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
}`}
>
Вход
</button>
<button
onClick={() => setIsLogin(false)}
className={`flex-1 py-2 rounded ${
!isLogin
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
}`}
>
Регистрация
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Имя пользователя
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
placeholder="admin"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Пароль
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
placeholder="••••••••"
/>
</div>
{error && (
<div className="bg-red-600 bg-opacity-20 border border-red-600 rounded p-3 text-red-400 text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Загрузка...' : isLogin ? 'Войти' : 'Зарегистрироваться'}
</button>
</form>
{isLogin && (
<div className="mt-4 text-center text-sm text-gray-400">
<p>По умолчанию:</p>
<p className="text-gray-300">admin / admin</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { useState, useEffect, useRef } from 'react';
import { Send } from 'lucide-react';
import axios from 'axios';
import { API_URL, WS_URL } from '../config';
export default function Console({ serverName, token, theme }) {
const [logs, setLogs] = useState([]);
const [command, setCommand] = useState('');
const logsEndRef = useRef(null);
const wsRef = useRef(null);
useEffect(() => {
setLogs([]);
const ws = new WebSocket(`${WS_URL}/ws/servers/${serverName}/console`);
ws.onopen = () => {
console.log('WebSocket подключен');
};
ws.onmessage = (event) => {
setLogs((prev) => [...prev, event.data]);
};
ws.onerror = (error) => {
console.error('WebSocket ошибка:', error);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [serverName]);
useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [logs]);
const sendCommand = async (e) => {
e.preventDefault();
if (!command.trim()) return;
try {
await axios.post(
`${API_URL}/api/servers/${serverName}/command`,
{ command: command.trim() },
{ headers: { Authorization: `Bearer ${token}` } }
);
setCommand('');
} catch (error) {
console.error('Ошибка отправки команды:', error);
alert(error.response?.data?.detail || 'Ошибка отправки команды');
}
};
return (
<div className={`flex flex-col h-full ${theme.primary}`}>
<div className={`flex-1 overflow-y-auto p-4 font-mono text-sm ${theme.secondary}`}>
{logs.length === 0 ? (
<div className={theme.textSecondary}>Консоль пуста. Запустите сервер для просмотра логов.</div>
) : (
logs.map((log, index) => (
<div key={index} className={`${theme.text} whitespace-pre-wrap leading-relaxed`}>
{log}
</div>
))
)}
<div ref={logsEndRef} />
</div>
<form onSubmit={sendCommand} className={`${theme.border} border-t p-4 flex gap-2`}>
<input
type="text"
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder="Введите команду..."
className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
/>
<button
type="submit"
className={`${theme.accent} ${theme.accentHover} px-6 py-2 rounded-xl flex items-center gap-2 text-white transition`}
>
<Send className="w-4 h-4" />
Отправить
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import { useState } from 'react';
import { X } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function CreateServerModal({ token, theme, onClose, onCreated }) {
const [formData, setFormData] = useState({
name: '',
displayName: '',
startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui'
});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
await axios.post(
`${API_URL}/api/servers/create`,
formData,
{ headers: { Authorization: `Bearer ${token}` } }
);
onCreated();
onClose();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка создания сервера');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className={`${theme.secondary} rounded-2xl p-6 w-full max-w-md shadow-2xl ${theme.border} border`}>
<div className="flex items-center justify-between mb-6">
<h2 className={`text-xl font-bold ${theme.text}`}>Создать сервер</h2>
<button
onClick={onClose}
className={`${theme.textSecondary} hover:${theme.text} transition`}
>
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
Имя папки (только латиница, цифры, _ и -)
</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
placeholder="my_server"
/>
</div>
<div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
Отображаемое имя
</label>
<input
type="text"
required
value={formData.displayName}
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
placeholder="Мой сервер"
/>
</div>
<div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
Команда запуска
</label>
<input
type="text"
required
value={formData.startCommand}
onChange={(e) => setFormData({ ...formData, startCommand: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
/>
</div>
<div className="flex gap-2 pt-4">
<button
type="button"
onClick={onClose}
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`}
>
Отмена
</button>
<button
type="submit"
disabled={loading}
className={`flex-1 ${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl disabled:opacity-50 transition text-white`}
>
{loading ? 'Создание...' : 'Создать'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center h-full bg-gray-900 text-white p-8">
<div className="max-w-md text-center">
<h2 className="text-2xl font-bold mb-4">Что-то пошло не так</h2>
<p className="text-gray-400 mb-4">
Произошла ошибка при загрузке компонента
</p>
<pre className="bg-black p-4 rounded text-left text-sm overflow-auto mb-4">
{this.state.error?.toString()}
</pre>
<button
onClick={() => window.location.reload()}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
>
Перезагрузить страницу
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -0,0 +1,64 @@
import { useState, useEffect } from 'react';
import { X, Save } from 'lucide-react';
export default function FileEditorModal({ file, onClose, onSave }) {
const [content, setContent] = useState(file.content);
const [saving, setSaving] = useState(false);
const handleSave = async () => {
setSaving(true);
await onSave(file.path, content);
setSaving(false);
};
useEffect(() => {
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
handleSave();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [content]);
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-bold">Редактирование: {file.name}</h2>
<div className="flex gap-2">
<button
onClick={handleSave}
disabled={saving}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
<button
onClick={onClose}
className="text-gray-400 hover:text-white"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="flex-1 overflow-hidden p-4 bg-gray-900">
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full h-full bg-black text-gray-300 font-mono text-sm p-4 rounded border border-gray-700 focus:outline-none focus:border-blue-500 resize-none"
spellCheck={false}
/>
</div>
<div className="p-4 border-t border-gray-700 text-sm text-gray-400">
Используйте Ctrl+S для быстрого сохранения
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,303 @@
import { useState, useEffect } from 'react';
import { Folder, File, Download, Trash2, Upload, Edit, Eye } from 'lucide-react';
import axios from 'axios';
import FileEditorModal from './FileEditorModal';
import FileViewerModal from './FileViewerModal';
import { API_URL } from '../config';
export default function FileManager({ serverName, token }) {
const [files, setFiles] = useState([]);
const [currentPath, setCurrentPath] = useState('');
const [editingFile, setEditingFile] = useState(null);
const [viewingFile, setViewingFile] = useState(null);
const [renamingFile, setRenamingFile] = useState(null);
const [newFileName, setNewFileName] = useState('');
useEffect(() => {
loadFiles();
}, [serverName, currentPath]);
const loadFiles = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files`, {
params: { path: currentPath },
headers: { Authorization: `Bearer ${token}` }
});
setFiles(data);
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
};
const openFolder = (folderName) => {
setCurrentPath(currentPath ? `${currentPath}/${folderName}` : folderName);
};
const goBack = () => {
const parts = currentPath.split('/');
parts.pop();
setCurrentPath(parts.join('/'));
};
const downloadFile = async (fileName) => {
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
window.open(`${API_URL}/api/servers/${serverName}/files/download?path=${filePath}`, '_blank');
};
const deleteFile = async (fileName) => {
if (!confirm(`Удалить ${fileName}?`)) return;
try {
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
await axios.delete(`${API_URL}/api/servers/${serverName}/files`, {
params: { path: filePath },
headers: { Authorization: `Bearer ${token}` }
});
loadFiles();
} catch (error) {
alert('Ошибка удаления файла');
}
};
const uploadFile = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
await axios.post(
`${API_URL}/api/servers/${serverName}/files/upload?path=${currentPath}`,
formData,
{ headers: { Authorization: `Bearer ${token}` } }
);
loadFiles();
} catch (error) {
alert('Ошибка загрузки файла');
}
};
const viewFile = async (fileName) => {
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
try {
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files/content`, {
params: { path: filePath },
headers: { Authorization: `Bearer ${token}` }
});
setViewingFile({ name: fileName, path: filePath, content: data.content });
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка открытия файла');
}
};
const editFile = async (fileName) => {
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
try {
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files/content`, {
params: { path: filePath },
headers: { Authorization: `Bearer ${token}` }
});
setEditingFile({ name: fileName, path: filePath, content: data.content });
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка открытия файла');
}
};
const saveFile = async (filePath, content) => {
try {
await axios.put(
`${API_URL}/api/servers/${serverName}/files/content`,
{ content },
{
params: { path: filePath },
headers: { Authorization: `Bearer ${token}` }
}
);
setEditingFile(null);
alert('Файл сохранен');
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка сохранения файла');
}
};
const startRename = (fileName) => {
setRenamingFile(fileName);
setNewFileName(fileName);
};
const renameFile = async (oldName) => {
if (!newFileName.trim() || newFileName === oldName) {
setRenamingFile(null);
return;
}
const oldPath = currentPath ? `${currentPath}/${oldName}` : oldName;
try {
await axios.put(
`${API_URL}/api/servers/${serverName}/files/rename`,
null,
{
params: { old_path: oldPath, new_name: newFileName },
headers: { Authorization: `Bearer ${token}` }
}
);
setRenamingFile(null);
loadFiles();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка переименования файла');
}
};
const formatSize = (bytes) => {
if (bytes === 0) return '-';
const k = 1024;
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
return (
<div className="h-full flex flex-col bg-gray-900">
<div className="border-b border-gray-700 p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
{currentPath && (
<button
onClick={goBack}
className="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded"
>
Назад
</button>
)}
<span className="text-gray-400">/{currentPath || 'root'}</span>
</div>
<label className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded cursor-pointer flex items-center gap-2">
<Upload className="w-4 h-4" />
Загрузить
<input type="file" onChange={uploadFile} className="hidden" />
</label>
</div>
<div className="flex-1 overflow-y-auto">
<table className="w-full">
<thead className="bg-gray-800 sticky top-0">
<tr>
<th className="text-left p-4">Имя</th>
<th className="text-left p-4">Размер</th>
<th className="text-right p-4">Действия</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr
key={file.name}
className="border-b border-gray-800 hover:bg-gray-800"
>
<td className="p-4">
{renamingFile === file.name ? (
<div className="flex items-center gap-2">
{file.type === 'directory' ? (
<Folder className="w-5 h-5 text-blue-400" />
) : (
<File className="w-5 h-5 text-gray-400" />
)}
<input
type="text"
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
onBlur={() => renameFile(file.name)}
onKeyDown={(e) => {
if (e.key === 'Enter') renameFile(file.name);
if (e.key === 'Escape') setRenamingFile(null);
}}
autoFocus
className="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-blue-500"
/>
</div>
) : (
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => file.type === 'directory' && openFolder(file.name)}
onDoubleClick={() => file.type === 'file' && viewFile(file.name)}
>
{file.type === 'directory' ? (
<Folder className="w-5 h-5 text-blue-400" />
) : (
<File className="w-5 h-5 text-gray-400" />
)}
<span>{file.name}</span>
</div>
)}
</td>
<td className="p-4 text-gray-400">{formatSize(file.size)}</td>
<td className="p-4">
<div className="flex gap-2 justify-end">
{file.type === 'file' && (
<>
<button
onClick={() => viewFile(file.name)}
className="bg-blue-600 hover:bg-blue-700 p-2 rounded"
title="Просмотр"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => editFile(file.name)}
className="bg-purple-600 hover:bg-purple-700 p-2 rounded"
title="Редактировать"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => downloadFile(file.name)}
className="bg-green-600 hover:bg-green-700 p-2 rounded"
title="Скачать"
>
<Download className="w-4 h-4" />
</button>
</>
)}
<button
onClick={() => startRename(file.name)}
className="bg-yellow-600 hover:bg-yellow-700 p-2 rounded"
title="Переименовать"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => deleteFile(file.name)}
className="bg-red-600 hover:bg-red-700 p-2 rounded"
title="Удалить"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{viewingFile && (
<FileViewerModal
file={viewingFile}
onClose={() => setViewingFile(null)}
onEdit={() => {
setEditingFile(viewingFile);
setViewingFile(null);
}}
/>
)}
{editingFile && (
<FileEditorModal
file={editingFile}
onClose={() => setEditingFile(null)}
onSave={saveFile}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { X, Edit } from 'lucide-react';
export default function FileViewerModal({ file, onClose, onEdit }) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-bold">{file.name}</h2>
<div className="flex gap-2">
<button
onClick={onEdit}
className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded flex items-center gap-2"
>
<Edit className="w-4 h-4" />
Редактировать
</button>
<button
onClick={onClose}
className="text-gray-400 hover:text-white"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto p-4 bg-gray-900">
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
{file.content}
</pre>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { useState } from 'react';
import { Server } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function Login({ onLogin }) {
const [isRegister, setIsRegister] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login';
const { data } = await axios.post(`${API_URL}${endpoint}`, {
username,
password
});
localStorage.setItem('token', data.access_token);
localStorage.setItem('username', data.username);
localStorage.setItem('role', data.role);
onLogin(data);
} catch (err) {
setError(err.response?.data?.detail || 'Ошибка авторизации');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="bg-gray-800 rounded-lg p-8 w-full max-w-md">
<div className="flex items-center justify-center mb-8">
<Server className="w-12 h-12 text-blue-500 mr-3" />
<h1 className="text-3xl font-bold text-white">MC Panel</h1>
</div>
<h2 className="text-xl font-semibold text-white mb-6 text-center">
{isRegister ? 'Регистрация' : 'Вход'}
</h2>
{error && (
<div className="bg-red-600 text-white p-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Имя пользователя
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
placeholder="Введите имя пользователя"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Пароль
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
placeholder="Введите пароль"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded font-medium disabled:opacity-50"
>
{loading ? 'Загрузка...' : (isRegister ? 'Зарегистрироваться' : 'Войти')}
</button>
</form>
<div className="mt-6 text-center">
<button
onClick={() => setIsRegister(!isRegister)}
className="text-blue-400 hover:text-blue-300"
>
{isRegister ? 'Уже есть аккаунт? Войти' : 'Нет аккаунта? Зарегистрироваться'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,132 @@
import { useState, useEffect } from 'react';
import { X, UserPlus, Trash2 } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function ServerAccessModal({ serverName, onClose }) {
const [users, setUsers] = useState([]);
const [newUsername, setNewUsername] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
loadUsers();
}, [serverName]);
const loadUsers = async () => {
try {
const token = localStorage.getItem('token');
const { data } = await axios.get(
`${API_URL}/api/servers/${serverName}/access`,
{ headers: { Authorization: `Bearer ${token}` } }
);
setUsers(data.users);
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
}
};
const addUser = async () => {
if (!newUsername.trim()) return;
setLoading(true);
try {
const token = localStorage.getItem('token');
await axios.post(
`${API_URL}/api/servers/${serverName}/access`,
{ username: newUsername, server_name: serverName },
{ headers: { Authorization: `Bearer ${token}` } }
);
setNewUsername('');
loadUsers();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка добавления пользователя');
} finally {
setLoading(false);
}
};
const removeUser = async (username) => {
if (!confirm(`Удалить доступ для ${username}?`)) return;
try {
const token = localStorage.getItem('token');
await axios.delete(
`${API_URL}/api/servers/${serverName}/access`,
{
headers: { Authorization: `Bearer ${token}` },
params: { username, server_name: serverName }
}
);
loadUsers();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка удаления доступа');
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Управление доступом</h2>
<button onClick={onClose} className="text-gray-400 hover:text-white">
<X className="w-6 h-6" />
</button>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
Добавить пользователя
</label>
<div className="flex gap-2">
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
placeholder="Имя пользователя"
className="flex-1 bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
/>
<button
onClick={addUser}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2 disabled:opacity-50"
>
<UserPlus className="w-4 h-4" />
Добавить
</button>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2">Пользователи с доступом:</h3>
<div className="space-y-2 max-h-64 overflow-y-auto">
{users.length === 0 ? (
<p className="text-gray-400 text-sm">Нет пользователей</p>
) : (
users.map((user) => (
<div
key={user.username}
className="bg-gray-700 p-3 rounded flex items-center justify-between"
>
<div>
<span className="font-medium">{user.username}</span>
{user.role === 'admin' && (
<span className="ml-2 text-xs bg-blue-600 px-2 py-1 rounded">
Админ
</span>
)}
</div>
<button
onClick={() => removeUser(user.username)}
className="text-red-400 hover:text-red-300"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,268 @@
import { useState, useEffect } from 'react';
import { Save, Trash2, Users, UserPlus } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function ServerSettings({ serverName, token, user, onDeleted }) {
const [config, setConfig] = useState({
name: '',
displayName: '',
startCommand: '',
owner: ''
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [allUsers, setAllUsers] = useState([]);
const [serverUsers, setServerUsers] = useState([]);
const [showUserManagement, setShowUserManagement] = useState(false);
const isAdmin = user?.role === 'admin';
const isOwner = config.owner === user?.username;
const canManageAccess = isAdmin || isOwner;
useEffect(() => {
loadConfig();
if (canManageAccess) {
loadUsers();
}
}, [serverName, canManageAccess]);
const loadConfig = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/config`, {
headers: { Authorization: `Bearer ${token}` }
});
setConfig(data);
} catch (error) {
console.error('Ошибка загрузки настроек:', error);
} finally {
setLoading(false);
}
};
const loadUsers = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/users`, {
headers: { Authorization: `Bearer ${token}` }
});
setAllUsers(data);
// Находим пользователей с доступом к этому серверу
const usersWithAccess = data.filter(u =>
u.role === 'admin' || u.servers?.includes(serverName)
);
setServerUsers(usersWithAccess);
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
}
};
const toggleUserAccess = async (username) => {
const userHasAccess = serverUsers.some(u => u.username === username);
const targetUser = allUsers.find(u => u.username === username);
if (!targetUser) return;
const newServers = userHasAccess
? targetUser.servers.filter(s => s !== serverName)
: [...(targetUser.servers || []), serverName];
try {
await axios.put(
`${API_URL}/api/users/${username}/servers`,
{ servers: newServers },
{ headers: { Authorization: `Bearer ${token}` } }
);
loadUsers();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка изменения доступа');
}
};
const saveConfig = async () => {
setSaving(true);
try {
await axios.put(
`${API_URL}/api/servers/${serverName}/config`,
config,
{ headers: { Authorization: `Bearer ${token}` } }
);
alert('Настройки сохранены');
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка сохранения настроек');
} finally {
setSaving(false);
}
};
const deleteServer = async () => {
if (!confirm(`Вы уверены, что хотите удалить сервер "${config.displayName}"? Все файлы будут удалены!`)) {
return;
}
try {
await axios.delete(`${API_URL}/api/servers/${serverName}`, {
headers: { Authorization: `Bearer ${token}` }
});
onDeleted();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка удаления сервера');
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-gray-400">Загрузка...</div>
</div>
);
}
return (
<div className="p-8 bg-gray-900 h-full overflow-y-auto">
<h2 className="text-2xl font-bold mb-6">Настройки сервера</h2>
<div className="space-y-6 max-w-2xl">
<div>
<label className="block text-sm font-medium mb-2">
Имя папки (нельзя изменить)
</label>
<input
type="text"
value={config.name}
disabled
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 text-gray-500 cursor-not-allowed"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Отображаемое имя
</label>
<input
type="text"
value={config.displayName}
onChange={(e) => setConfig({ ...config, displayName: e.target.value })}
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Команда запуска
</label>
<input
type="text"
value={config.startCommand}
onChange={(e) => setConfig({ ...config, startCommand: e.target.value })}
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
/>
<p className="text-sm text-gray-400 mt-2">
Пример: java -Xmx2G -Xms1G -jar server.jar nogui
</p>
</div>
{config.owner && (
<div>
<label className="block text-sm font-medium mb-2">
Владелец
</label>
<div className="text-gray-300">{config.owner}</div>
</div>
)}
<div className="flex gap-4 pt-4">
<button
onClick={saveConfig}
disabled={saving}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded flex items-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
{canManageAccess && (
<div className="border-t border-gray-700 pt-6 mt-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Управление доступом</h3>
<button
onClick={() => setShowUserManagement(!showUserManagement)}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2"
>
<Users className="w-4 h-4" />
{showUserManagement ? 'Скрыть' : 'Показать пользователей'}
</button>
</div>
{showUserManagement && (
<div className="space-y-2">
<p className="text-sm text-gray-400 mb-4">
Выберите пользователей, которые могут управлять этим сервером:
</p>
{allUsers.filter(u => u.role !== 'admin').map((targetUser) => {
const hasAccess = serverUsers.some(u => u.username === targetUser.username);
return (
<div
key={targetUser.username}
className="flex items-center justify-between bg-gray-800 p-3 rounded"
>
<span>{targetUser.username}</span>
<button
onClick={() => toggleUserAccess(targetUser.username)}
className={`px-4 py-1 rounded text-sm ${
hasAccess
? 'bg-green-600 hover:bg-green-700'
: 'bg-gray-600 hover:bg-gray-500'
}`}
>
{hasAccess ? 'Есть доступ' : 'Нет доступа'}
</button>
</div>
);
})}
{allUsers.filter(u => u.role !== 'admin').length === 0 && (
<p className="text-gray-500 text-sm">Нет обычных пользователей</p>
)}
</div>
)}
<div className="mt-4">
<h4 className="text-sm font-medium mb-2 text-gray-400">
Пользователи с доступом:
</h4>
<div className="flex flex-wrap gap-2">
{serverUsers.map((u) => (
<span
key={u.username}
className={`px-3 py-1 rounded text-sm ${
u.role === 'admin'
? 'bg-blue-600'
: 'bg-green-600'
}`}
>
{u.username} {u.role === 'admin' && '(Админ)'}
</span>
))}
</div>
</div>
</div>
)}
<div className="border-t border-gray-700 pt-6 mt-8">
<h3 className="text-lg font-semibold mb-4 text-red-400">Опасная зона</h3>
<button
onClick={deleteServer}
className="bg-red-600 hover:bg-red-700 px-6 py-2 rounded flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Удалить сервер
</button>
<p className="text-sm text-gray-400 mt-2">
Это действие нельзя отменить. Все файлы сервера будут удалены.
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
import { useState, useEffect } from 'react';
import { Cpu, HardDrive, Activity } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function Stats({ serverName, token }) {
const [stats, setStats] = useState({
status: 'stopped',
cpu: 0,
memory: 0,
disk: 0
});
useEffect(() => {
loadStats();
const interval = setInterval(loadStats, 2000);
return () => clearInterval(interval);
}, [serverName]);
const loadStats = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/stats`, {
headers: { Authorization: `Bearer ${token}` }
});
setStats(data);
} catch (error) {
console.error('Ошибка загрузки статистики:', error);
}
};
return (
<div className="p-8 bg-gray-900">
<h2 className="text-2xl font-bold mb-6">Статистика сервера</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">CPU</h3>
<Cpu className="w-6 h-6 text-blue-400" />
</div>
<div className="text-3xl font-bold mb-2">{stats.cpu}%</div>
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min(stats.cpu, 100)}%` }}
/>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">ОЗУ</h3>
<Activity className="w-6 h-6 text-green-400" />
</div>
<div className="text-3xl font-bold mb-2">{stats.memory} МБ</div>
<div className="w-full bg-gray-700 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min((stats.memory / 2048) * 100, 100)}%` }}
/>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Диск</h3>
<HardDrive className="w-6 h-6 text-purple-400" />
</div>
<div className="text-3xl font-bold mb-2">{stats.disk} МБ</div>
<div className="text-sm text-gray-400 mt-2">
Использовано на диске
</div>
</div>
</div>
<div className="mt-8 bg-gray-800 rounded-lg p-6 border border-gray-700">
<h3 className="text-lg font-semibold mb-4">Статус</h3>
<div className="flex items-center gap-3">
<div
className={`w-4 h-4 rounded-full ${
stats.status === 'running' ? 'bg-green-500' : 'bg-red-500'
}`}
/>
<span className="text-xl">
{stats.status === 'running' ? 'Запущен' : 'Остановлен'}
</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { Palette } from 'lucide-react';
import { themes, getTheme } from '../themes';
export default function ThemeSelector({ currentTheme, onThemeChange }) {
const theme = getTheme(currentTheme);
const themeColors = {
dark: 'bg-gray-800',
light: 'bg-gray-100',
purple: 'bg-purple-600',
blue: 'bg-blue-600',
green: 'bg-green-600',
};
return (
<div className="relative group">
<button className={`p-2 rounded-lg ${theme.hover} transition`}>
<Palette className="w-5 h-5" />
</button>
<div className={`absolute right-0 mt-2 w-48 ${theme.secondary} rounded-lg shadow-xl ${theme.border} border opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50`}>
<div className="p-2">
<div className={`text-xs ${theme.textSecondary} px-2 py-1 mb-1`}>Выберите тему</div>
{Object.entries(themes).map(([key, themeItem]) => (
<button
key={key}
onClick={() => onThemeChange(key)}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg ${theme.hover} transition ${
currentTheme === key ? theme.tertiary : ''
}`}
>
<div className={`w-4 h-4 rounded ${themeColors[key]}`} />
<span className="text-sm">{themeItem.name}</span>
{currentTheme === key && (
<span className="ml-auto text-xs text-green-500"></span>
)}
</button>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,183 @@
import { useState, useEffect } from 'react';
import { Users as UsersIcon, Trash2, Shield, User } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
export default function Users({ token }) {
const [users, setUsers] = useState([]);
const [servers, setServers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const [usersRes, serversRes] = await Promise.all([
axios.get(`${API_URL}/api/users`, {
headers: { Authorization: `Bearer ${token}` }
}),
axios.get(`${API_URL}/api/servers`, {
headers: { Authorization: `Bearer ${token}` }
})
]);
setUsers(usersRes.data);
setServers(serversRes.data);
} catch (error) {
console.error('Ошибка загрузки данных:', error);
} finally {
setLoading(false);
}
};
const toggleServerAccess = async (username, serverName) => {
const user = users.find(u => u.username === username);
const currentServers = user.servers || [];
const newServers = currentServers.includes(serverName)
? currentServers.filter(s => s !== serverName)
: [...currentServers, serverName];
try {
await axios.put(
`${API_URL}/api/users/${username}/servers`,
{ servers: newServers },
{ headers: { Authorization: `Bearer ${token}` } }
);
loadData();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка обновления доступа');
}
};
const toggleRole = async (username, currentRole) => {
const newRole = currentRole === 'admin' ? 'user' : 'admin';
if (!confirm(`Изменить роль пользователя ${username} на ${newRole}?`)) {
return;
}
try {
await axios.put(
`${API_URL}/api/users/${username}/role`,
{ role: newRole },
{ headers: { Authorization: `Bearer ${token}` } }
);
loadData();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка изменения роли');
}
};
const deleteUser = async (username) => {
if (!confirm(`Удалить пользователя ${username}?`)) {
return;
}
try {
await axios.delete(`${API_URL}/api/users/${username}`, {
headers: { Authorization: `Bearer ${token}` }
});
loadData();
} catch (error) {
alert(error.response?.data?.detail || 'Ошибка удаления пользователя');
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-gray-400">Загрузка...</div>
</div>
);
}
return (
<div className="p-8 bg-gray-900 h-full overflow-y-auto">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<UsersIcon className="w-8 h-8" />
Управление пользователями
</h2>
<div className="space-y-4">
{users.map((user) => (
<div
key={user.username}
className="bg-gray-800 rounded-lg p-6 border border-gray-700"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded ${
user.role === 'admin' ? 'bg-blue-600' : 'bg-gray-700'
}`}>
{user.role === 'admin' ? (
<Shield className="w-6 h-6" />
) : (
<User className="w-6 h-6" />
)}
</div>
<div>
<h3 className="text-lg font-semibold">{user.username}</h3>
<p className="text-sm text-gray-400">
{user.role === 'admin' ? 'Администратор' : 'Пользователь'}
</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => toggleRole(user.username, user.role)}
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm"
>
{user.role === 'admin' ? 'Сделать пользователем' : 'Сделать админом'}
</button>
<button
onClick={() => deleteUser(user.username)}
className="bg-red-600 hover:bg-red-700 p-2 rounded"
title="Удалить"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{user.role !== 'admin' && (
<div>
<h4 className="text-sm font-medium mb-2 text-gray-400">
Доступ к серверам:
</h4>
<div className="flex flex-wrap gap-2">
{servers.map((server) => {
const hasAccess = user.servers?.includes(server.name);
return (
<button
key={server.name}
onClick={() => toggleServerAccess(user.username, server.name)}
className={`px-3 py-1 rounded text-sm transition ${
hasAccess
? 'bg-green-600 hover:bg-green-700'
: 'bg-gray-700 hover:bg-gray-600'
}`}
>
{server.displayName}
</button>
);
})}
{servers.length === 0 && (
<p className="text-gray-500 text-sm">Нет доступных серверов</p>
)}
</div>
</div>
)}
{user.role === 'admin' && (
<p className="text-sm text-gray-400">
Администратор имеет доступ ко всем серверам
</p>
)}
</div>
))}
</div>
</div>
);
}

25
frontend/src/config.js Normal file
View File

@@ -0,0 +1,25 @@
// Автоматически определяем API URL
const getApiUrl = () => {
// Если задана переменная окружения, используем её
if (import.meta.env.VITE_API_URL) {
return import.meta.env.VITE_API_URL;
}
// Иначе используем текущий хост с портом 8000
const protocol = window.location.protocol;
const hostname = window.location.hostname;
// Если localhost, используем localhost:8000
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return `${protocol}//localhost:8000`;
}
// Для удаленного доступа используем IP:8000
return `${protocol}//${hostname}:8000`;
};
export const API_URL = getApiUrl();
export const WS_URL = API_URL.replace('http', 'ws');
console.log('API URL:', API_URL);
console.log('WS URL:', WS_URL);

22
frontend/src/index.css Normal file
View File

@@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100%;
height: 100vh;
}

10
frontend/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

111
frontend/src/themes.js Normal file
View File

@@ -0,0 +1,111 @@
export const themes = {
dark: {
name: 'Тёмная',
gradient: 'from-blue-400 to-purple-600',
primary: 'bg-gray-950',
secondary: 'bg-gray-900',
tertiary: 'bg-gray-800',
accent: 'bg-blue-600',
accentHover: 'hover:bg-blue-700',
text: 'text-white',
textSecondary: 'text-gray-400',
border: 'border-gray-800',
hover: 'hover:bg-gray-800',
input: 'bg-gray-900 border-gray-700',
card: 'bg-gray-900',
cardHover: 'hover:bg-gray-800',
success: 'bg-green-600',
successHover: 'hover:bg-green-700',
danger: 'bg-red-600',
dangerHover: 'hover:bg-red-700',
warning: 'bg-yellow-600',
},
light: {
name: 'Светлая',
gradient: 'from-blue-600 to-purple-600',
primary: 'bg-gray-50',
secondary: 'bg-white',
tertiary: 'bg-gray-100',
accent: 'bg-blue-600',
accentHover: 'hover:bg-blue-700',
text: 'text-gray-900',
textSecondary: 'text-gray-600',
border: 'border-gray-200',
hover: 'hover:bg-gray-100',
input: 'bg-white border-gray-300',
card: 'bg-white',
cardHover: 'hover:bg-gray-50',
success: 'bg-green-600',
successHover: 'hover:bg-green-700',
danger: 'bg-red-600',
dangerHover: 'hover:bg-red-700',
warning: 'bg-yellow-600',
},
purple: {
name: 'Фиолетовая',
gradient: 'from-purple-400 to-pink-600',
primary: 'bg-slate-950',
secondary: 'bg-slate-900',
tertiary: 'bg-purple-900/30',
accent: 'bg-purple-600',
accentHover: 'hover:bg-purple-700',
text: 'text-white',
textSecondary: 'text-purple-300',
border: 'border-purple-900/50',
hover: 'hover:bg-purple-900/30',
input: 'bg-slate-900 border-purple-900/50',
card: 'bg-slate-900',
cardHover: 'hover:bg-purple-900/30',
success: 'bg-green-600',
successHover: 'hover:bg-green-700',
danger: 'bg-red-600',
dangerHover: 'hover:bg-red-700',
warning: 'bg-yellow-600',
},
blue: {
name: 'Синяя',
gradient: 'from-cyan-400 to-blue-600',
primary: 'bg-slate-950',
secondary: 'bg-slate-900',
tertiary: 'bg-blue-900/30',
accent: 'bg-blue-500',
accentHover: 'hover:bg-blue-600',
text: 'text-white',
textSecondary: 'text-blue-300',
border: 'border-blue-900/50',
hover: 'hover:bg-blue-900/30',
input: 'bg-slate-900 border-blue-900/50',
card: 'bg-slate-900',
cardHover: 'hover:bg-blue-900/30',
success: 'bg-green-600',
successHover: 'hover:bg-green-700',
danger: 'bg-red-600',
dangerHover: 'hover:bg-red-700',
warning: 'bg-yellow-600',
},
green: {
name: 'Зелёная',
gradient: 'from-emerald-400 to-green-600',
primary: 'bg-slate-950',
secondary: 'bg-slate-900',
tertiary: 'bg-green-900/30',
accent: 'bg-green-600',
accentHover: 'hover:bg-green-700',
text: 'text-white',
textSecondary: 'text-green-300',
border: 'border-green-900/50',
hover: 'hover:bg-green-900/30',
input: 'bg-slate-900 border-green-900/50',
card: 'bg-slate-900',
cardHover: 'hover:bg-green-900/30',
success: 'bg-green-600',
successHover: 'hover:bg-green-700',
danger: 'bg-red-600',
dangerHover: 'hover:bg-red-700',
warning: 'bg-yellow-600',
},
};
export const getTheme = (themeName) => {
return themes[themeName] || themes.dark;
};

5
frontend/start.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
echo Starting MC Panel Frontend...
cd /d "%~dp0"
npm run dev
pause

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

10
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
host: true
}
});