исправлен лк
This commit is contained in:
@@ -3,33 +3,38 @@ import cors from 'cors';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import authRoutes from './modules/auth/auth.routes';
|
import authRoutes from './modules/auth/auth.routes';
|
||||||
|
|
||||||
// Загружаем переменные окружения из .env файла
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Инициализируем приложение Express
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Middleware для CORS
|
// ИСПРАВЛЕНО: более точная настройка CORS
|
||||||
// Это позволяет фронтенду (на другом порту) отправлять запросы на бэкенд
|
app.use(cors({
|
||||||
app.use(cors());
|
origin: ['http://localhost:3000', 'http://localhost:5173'], // Vite обычно использует 5173
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}));
|
||||||
|
|
||||||
// Middleware для парсинга JSON
|
|
||||||
// Это позволяет Express читать данные, которые приходят в теле запроса в формате JSON
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Основной маршрут для проверки работы сервера
|
// Добавим логирование для отладки
|
||||||
app.get('/', (req, res) => {
|
app.use((req, res, next) => {
|
||||||
res.send('Сервер ospab.host запущен!');
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'Сервер ospab.host запущен!',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Подключаем наши маршруты для аутентификации
|
|
||||||
// Все маршруты в authRoutes будут доступны по адресу /api/auth/...
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
|
|
||||||
// Получаем порт из переменных окружения или используем 5000 по умолчанию
|
|
||||||
const PORT = process.env.PORT || 5000;
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
// Запускаем сервер
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Сервер работает на порту ${PORT}`);
|
console.log(`🚀 Сервер запущен на порту ${PORT}`);
|
||||||
|
console.log(`📊 База данных: ${process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА'}`);
|
||||||
});
|
});
|
||||||
3
ospabhost/frontend/.gitignore
vendored
3
ospabhost/frontend/.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/app.tsx
|
|
||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||||
import Pagetempl from './components/pagetempl';
|
import Pagetempl from './components/pagetempl';
|
||||||
|
import DashboardTempl from './components/dashboardtempl';
|
||||||
import Homepage from './pages/index';
|
import Homepage from './pages/index';
|
||||||
import Dashboard from './pages/dashboard/mainpage';
|
import Dashboard from './pages/dashboard/mainpage';
|
||||||
import Loginpage from './pages/login';
|
import Loginpage from './pages/login';
|
||||||
@@ -8,19 +8,28 @@ import Registerpage from './pages/register';
|
|||||||
import Tariffspage from './pages/tariffs';
|
import Tariffspage from './pages/tariffs';
|
||||||
import Aboutpage from './pages/about';
|
import Aboutpage from './pages/about';
|
||||||
import Privateroute from './components/privateroute';
|
import Privateroute from './components/privateroute';
|
||||||
import { AuthProvider } from './context/authcontext'; // Import AuthProvider
|
import { AuthProvider } from './context/authcontext';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<AuthProvider> {/* Wrap the entire application with AuthProvider */}
|
<AuthProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
{/* Обычные страницы с footer */}
|
||||||
<Route path="/" element={<Pagetempl><Homepage /></Pagetempl>} />
|
<Route path="/" element={<Pagetempl><Homepage /></Pagetempl>} />
|
||||||
<Route path="/tariffs" element={<Pagetempl><Tariffspage /></Pagetempl>} />
|
<Route path="/tariffs" element={<Pagetempl><Tariffspage /></Pagetempl>} />
|
||||||
<Route path="/about" element={<Pagetempl><Aboutpage /></Pagetempl>} />
|
<Route path="/about" element={<Pagetempl><Aboutpage /></Pagetempl>} />
|
||||||
<Route path="/dashboard/*" element={<Pagetempl><Privateroute><Dashboard /></Privateroute></Pagetempl>} />
|
|
||||||
<Route path="/login" element={<Pagetempl><Loginpage /></Pagetempl>} />
|
<Route path="/login" element={<Pagetempl><Loginpage /></Pagetempl>} />
|
||||||
<Route path="/register" element={<Pagetempl><Registerpage /></Pagetempl>} />
|
<Route path="/register" element={<Pagetempl><Registerpage /></Pagetempl>} />
|
||||||
|
|
||||||
|
{/* Дашборд без footer */}
|
||||||
|
<Route path="/dashboard/*" element={
|
||||||
|
<DashboardTempl>
|
||||||
|
<Privateroute>
|
||||||
|
<Dashboard />
|
||||||
|
</Privateroute>
|
||||||
|
</DashboardTempl>
|
||||||
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
20
ospabhost/frontend/src/components/dashboardtempl.tsx
Normal file
20
ospabhost/frontend/src/components/dashboardtempl.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// frontend/src/components/dashboardtempl.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import Header from './header';
|
||||||
|
|
||||||
|
interface DashboardTemplProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardTempl: React.FC<DashboardTemplProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Header />
|
||||||
|
<div className="pt-16">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardTempl;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// 3. Исправляем frontend/src/pages/dashboard/billing.tsx
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
@@ -7,8 +8,9 @@ const Billing = () => {
|
|||||||
const [isPaymentGenerated, setIsPaymentGenerated] = useState(false);
|
const [isPaymentGenerated, setIsPaymentGenerated] = useState(false);
|
||||||
const [copyStatus, setCopyStatus] = useState('');
|
const [copyStatus, setCopyStatus] = useState('');
|
||||||
|
|
||||||
const cardNumber = process.env.REACT_APP_CARD_NUMBER || '';
|
// ИСПРАВЛЕНО: используем правильные переменные окружения для Vite
|
||||||
const sbpUrl = process.env.REACT_APP_SBP_QR_URL || '';
|
const cardNumber = import.meta.env.VITE_CARD_NUMBER || '';
|
||||||
|
const sbpUrl = import.meta.env.VITE_SBP_QR_URL || '';
|
||||||
|
|
||||||
const handleGeneratePayment = () => {
|
const handleGeneratePayment = () => {
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -59,38 +61,52 @@ const Billing = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg text-gray-700 mb-4">Для пополнения баланса, пожалуйста, переведите сумму **₽{amount}**.</p>
|
<p className="text-lg text-gray-700 mb-4">
|
||||||
|
Для пополнения баланса переведите <strong>₽{amount}</strong>.
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-6">
|
<p className="text-sm text-gray-500 mb-6">
|
||||||
Ваш заказ будет обработан вручную после проверки чека.
|
Ваш заказ будет обработан вручную после проверки чека.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="bg-gray-100 p-6 rounded-2xl inline-block mb-6">
|
{sbpUrl && (
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Оплата по СБП</h3>
|
<div className="bg-gray-100 p-6 rounded-2xl inline-block mb-6">
|
||||||
<div className="flex justify-center p-4 bg-white rounded-lg">
|
<h3 className="text-xl font-bold text-gray-800 mb-2">Оплата по СБП</h3>
|
||||||
<QRCode value={sbpUrl} size={256} />
|
<div className="flex justify-center p-4 bg-white rounded-lg">
|
||||||
|
<QRCode value={sbpUrl} size={256} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-600">
|
||||||
|
Отсканируйте QR-код через мобильное приложение вашего банка.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-sm text-gray-600">Отсканируйте QR-код через мобильное приложение вашего банка.</p>
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-100 p-6 rounded-2xl mb-6">
|
{cardNumber && (
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Оплата по номеру карты</h3>
|
<div className="bg-gray-100 p-6 rounded-2xl mb-6">
|
||||||
<p className="text-2xl font-bold text-gray-800 select-all">{cardNumber}</p>
|
<h3 className="text-xl font-bold text-gray-800 mb-2">Оплата по номеру карты</h3>
|
||||||
<button
|
<p className="text-2xl font-bold text-gray-800 select-all">{cardNumber}</p>
|
||||||
onClick={handleCopyCard}
|
<button
|
||||||
className="mt-4 px-4 py-2 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-gray-500 hover:bg-gray-700"
|
onClick={handleCopyCard}
|
||||||
>
|
className="mt-4 px-4 py-2 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-gray-500 hover:bg-gray-700"
|
||||||
Скопировать номер карты
|
>
|
||||||
</button>
|
Скопировать номер карты
|
||||||
{copyStatus && <p className="mt-2 text-sm text-green-500">{copyStatus}</p>}
|
</button>
|
||||||
</div>
|
{copyStatus && <p className="mt-2 text-sm text-green-500">{copyStatus}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-red-50 p-6 rounded-2xl border-l-4 border-red-500 text-left mb-6">
|
<div className="bg-red-50 p-6 rounded-2xl border-l-4 border-red-500 text-left mb-6">
|
||||||
<p className="font-bold text-red-800">Важно:</p>
|
<p className="font-bold text-red-800">Важно:</p>
|
||||||
<p className="text-sm text-red-700">После оплаты сделайте скриншот или сохраните чек и отправьте его нам в тикет поддержки, чтобы мы могли подтвердить платёж.</p>
|
<p className="text-sm text-red-700">
|
||||||
|
После оплаты сделайте скриншот или сохраните чек и отправьте его нам в тикет поддержки.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-4 text-gray-600">
|
<p className="mt-4 text-gray-600">
|
||||||
После подтверждения ваш баланс будет пополнен. Вы можете перейти в раздел <Link to="/dashboard/tickets" className="text-ospab-primary font-bold hover:underline">Тикеты</Link>, чтобы отправить нам чек.
|
После подтверждения ваш баланс будет пополнен. Перейдите в раздел{' '}
|
||||||
|
<Link to="/dashboard/tickets" className="text-ospab-primary font-bold hover:underline">
|
||||||
|
Тикеты
|
||||||
|
</Link>
|
||||||
|
, чтобы отправить нам чек.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
// frontend/src/pages/dashboard/mainpage.tsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Routes, Route, Link, useNavigate } from 'react-router-dom';
|
import { Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AuthContext from '../../context/authcontext';
|
import AuthContext from '../../context/authcontext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
@@ -10,44 +11,55 @@ import Servers from './servers';
|
|||||||
import Tickets from './tickets';
|
import Tickets from './tickets';
|
||||||
import Billing from './billing';
|
import Billing from './billing';
|
||||||
import Settings from './settings';
|
import Settings from './settings';
|
||||||
import CheckVerification from './checkverification.tsx';
|
import CheckVerification from './checkverification';
|
||||||
import TicketResponse from './ticketresponse.tsx';
|
import TicketResponse from './ticketresponse';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [activeTab, setActiveTab] = useState('summary');
|
|
||||||
const [userData, setUserData] = useState<import('./types').UserData | null>(null);
|
const [userData, setUserData] = useState<import('./types').UserData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { logout } = useContext(AuthContext);
|
const { logout } = useContext(AuthContext);
|
||||||
|
|
||||||
|
// Определяем активную вкладку из URL
|
||||||
|
const getActiveTab = () => {
|
||||||
|
const path = location.pathname.split('/dashboard/')[1] || '';
|
||||||
|
return path === '' ? 'summary' : path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(getActiveTab());
|
||||||
|
|
||||||
|
// Обновляем активную вкладку при изменении URL
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTab(getActiveTab());
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
console.log('Токен не найден, перенаправляем на логин');
|
||||||
logout();
|
logout();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = { Authorization: `Bearer ${token}` };
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
||||||
|
|
||||||
// Моделируем остальные данные
|
|
||||||
const serversRes = { data: { servers: [] } };
|
|
||||||
const ticketsRes = { data: { tickets: [] } };
|
|
||||||
|
|
||||||
setUserData({
|
setUserData({
|
||||||
user: userRes.data.user,
|
user: userRes.data.user,
|
||||||
balance: 1500, // Пример
|
balance: 1500,
|
||||||
servers: serversRes.data.servers,
|
servers: [],
|
||||||
tickets: ticketsRes.data.tickets,
|
tickets: [],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка загрузки данных:', err);
|
console.error('Ошибка загрузки данных:', err);
|
||||||
logout();
|
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||||
navigate('/login');
|
logout();
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -57,53 +69,201 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center pt-20">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<h1 className="text-2xl text-gray-800">Загрузка...</h1>
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-ospab-primary mx-auto mb-4"></div>
|
||||||
|
<h1 className="text-2xl text-gray-800">Загрузка...</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userData || !userData.user) {
|
if (!userData || !userData.user) {
|
||||||
return null;
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl text-gray-800 mb-4">Ошибка загрузки данных</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-ospab-primary text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Перезагрузить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOperator = userData.user.operator === 1;
|
const isOperator = userData.user.operator === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-gray-50 pt-20">
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
<div className="w-64 bg-white shadow-xl p-6 rounded-r-3xl h-full fixed">
|
{/* Sidebar - фиксированный слева */}
|
||||||
<nav className="mt-8">
|
<div className="w-64 bg-white shadow-xl flex flex-col">
|
||||||
<Link to="/dashboard" onClick={() => setActiveTab('summary')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${activeTab === 'summary' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Сводка</Link>
|
{/* Заголовок сайдбара */}
|
||||||
<Link to="/dashboard/servers" onClick={() => setActiveTab('servers')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'servers' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Серверы</Link>
|
<div className="p-6 border-b border-gray-200">
|
||||||
<Link to="/dashboard/tickets" onClick={() => setActiveTab('tickets')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'tickets' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Тикеты</Link>
|
<h2 className="text-xl font-bold text-gray-800">
|
||||||
<Link to="/dashboard/billing" onClick={() => setActiveTab('billing')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'billing' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Баланс</Link>
|
Привет, {userData.user.username}!
|
||||||
<Link to="/dashboard/settings" onClick={() => setActiveTab('settings')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'settings' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Настройки</Link>
|
</h2>
|
||||||
|
{isOperator && (
|
||||||
|
<span className="inline-block px-2 py-1 bg-red-100 text-red-800 text-xs font-semibold rounded-full mt-1">
|
||||||
|
Оператор
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 text-sm text-gray-600">
|
||||||
|
Баланс: <span className="font-semibold text-ospab-primary">₽{userData.balance}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Навигация */}
|
||||||
|
<nav className="flex-1 p-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'summary' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">📊</span>
|
||||||
|
Сводка
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/servers"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'servers' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">🖥️</span>
|
||||||
|
Серверы
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/tickets"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'tickets' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">🎫</span>
|
||||||
|
Тикеты
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/billing"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'billing' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">💳</span>
|
||||||
|
Пополнить баланс
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/settings"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'settings' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">⚙️</span>
|
||||||
|
Настройки
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isOperator && (
|
{isOperator && (
|
||||||
<>
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||||
<div className="border-t border-gray-200 my-4"></div>
|
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-4">
|
||||||
<Link to="/dashboard/checkverification" onClick={() => setActiveTab('checkverification')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'checkverification' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Проверка чеков</Link>
|
Админ панель
|
||||||
<Link to="/dashboard/ticketresponse" onClick={() => setActiveTab('ticketresponse')} className={`block py-3 px-4 rounded-xl font-semibold transition-colors duration-200 mt-2 ${activeTab === 'ticketresponse' ? 'bg-ospab-primary text-white' : 'text-gray-600 hover:bg-gray-100'}`}>Ответы на тикеты</Link>
|
</p>
|
||||||
</>
|
<div className="space-y-1">
|
||||||
|
<Link
|
||||||
|
to="/dashboard/checkverification"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'checkverification' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">✅</span>
|
||||||
|
Проверка чеков
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/ticketresponse"
|
||||||
|
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||||
|
activeTab === 'ticketresponse' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-3">💬</span>
|
||||||
|
Ответы на тикеты
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Футер сайдбара */}
|
||||||
|
<div className="p-6 border-t border-gray-200">
|
||||||
|
<div className="text-xs text-gray-500 text-center">
|
||||||
|
<p>© 2024 ospab.host</p>
|
||||||
|
<p className="mt-1">Версия 1.0.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 ml-64 p-8">
|
{/* Main Content - занимает оставшееся место */}
|
||||||
<Routes>
|
<div className="flex-1 flex flex-col">
|
||||||
<Route path="/" element={<Summary userData={userData} />} />
|
{/* Хлебные крошки/заголовок */}
|
||||||
<Route path="servers" element={<Servers servers={userData.servers} />} />
|
<div className="bg-white border-b border-gray-200 px-8 py-4">
|
||||||
<Route path="tickets" element={<Tickets tickets={userData.tickets} />} />
|
<div className="flex items-center justify-between">
|
||||||
<Route path="billing" element={<Billing />} />
|
<div>
|
||||||
<Route path="settings" element={<Settings />} />
|
<h1 className="text-2xl font-bold text-gray-900 capitalize">
|
||||||
|
{activeTab === 'summary' ? 'Сводка' :
|
||||||
|
activeTab === 'servers' ? 'Серверы' :
|
||||||
|
activeTab === 'tickets' ? 'Тикеты поддержки' :
|
||||||
|
activeTab === 'billing' ? 'Пополнение баланса' :
|
||||||
|
activeTab === 'settings' ? 'Настройки аккаунта' :
|
||||||
|
activeTab === 'checkverification' ? 'Проверка чеков' :
|
||||||
|
activeTab === 'ticketresponse' ? 'Ответы на тикеты' :
|
||||||
|
'Панель управления'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
{new Date().toLocaleDateString('ru-RU', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isOperator && (
|
{/* Быстрые действия */}
|
||||||
<>
|
<div className="flex space-x-3">
|
||||||
<Route path="checkverification" element={<CheckVerification />} />
|
<Link
|
||||||
<Route path="ticketresponse" element={<TicketResponse />} />
|
to="/dashboard/billing"
|
||||||
</>
|
className="px-4 py-2 bg-green-100 text-green-800 rounded-lg text-sm font-medium hover:bg-green-200 transition-colors"
|
||||||
)}
|
>
|
||||||
</Routes>
|
💰 Пополнить
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/tickets"
|
||||||
|
className="px-4 py-2 bg-blue-100 text-blue-800 rounded-lg text-sm font-medium hover:bg-blue-200 transition-colors"
|
||||||
|
>
|
||||||
|
🆘 Поддержка
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Контент страницы */}
|
||||||
|
<div className="flex-1 p-8">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Summary userData={userData} />} />
|
||||||
|
<Route path="servers" element={<Servers servers={userData.servers} />} />
|
||||||
|
<Route path="tickets" element={<Tickets tickets={userData.tickets} />} />
|
||||||
|
<Route path="billing" element={<Billing />} />
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
|
|
||||||
|
{isOperator && (
|
||||||
|
<>
|
||||||
|
<Route path="checkverification" element={<CheckVerification />} />
|
||||||
|
<Route path="ticketresponse" element={<TicketResponse />} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ const LoginPage = () => {
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:5000/api/auth/login', {
|
const response = await axios.post('http://localhost:5000/api/auth/login', {
|
||||||
@@ -21,7 +23,8 @@ const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
login(response.data.token);
|
login(response.data.token);
|
||||||
navigate('/dashboard/mainpage');
|
// ИСПРАВЛЕНО: правильный путь к дашборду
|
||||||
|
navigate('/dashboard');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (axios.isAxiosError(err) && err.response) {
|
if (axios.isAxiosError(err) && err.response) {
|
||||||
@@ -29,6 +32,8 @@ const LoginPage = () => {
|
|||||||
} else {
|
} else {
|
||||||
setError('Произошла ошибка сети. Пожалуйста, попробуйте позже.');
|
setError('Произошла ошибка сети. Пожалуйста, попробуйте позже.');
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +48,8 @@ const LoginPage = () => {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Электронная почта"
|
placeholder="Электронная почта"
|
||||||
className="w-full px-5 py-3 mb-4 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-ospab-primary"
|
className="w-full px-5 py-3 mb-4 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-ospab-primary"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -50,16 +57,21 @@ const LoginPage = () => {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Пароль"
|
placeholder="Пароль"
|
||||||
className="w-full px-5 py-3 mb-6 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-ospab-primary"
|
className="w-full px-5 py-3 mb-6 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-ospab-primary"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full px-5 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent"
|
disabled={isLoading}
|
||||||
|
className="w-full px-5 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Войти
|
{isLoading ? 'Входим...' : 'Войти'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="mt-4 text-sm text-red-500">{error}</p>
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-sm text-red-600">{error}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="mt-6 text-gray-600">
|
<p className="mt-6 text-gray-600">
|
||||||
Нет аккаунта?{' '}
|
Нет аккаунта?{' '}
|
||||||
@@ -73,3 +85,6 @@ const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user