Начат фронтенд

This commit is contained in:
Georgiy Syralev
2025-09-16 14:47:30 +03:00
parent f37e85e2e0
commit 40de29041d
2100 changed files with 305701 additions and 11807 deletions

View File

@@ -1,13 +1,16 @@
<!doctype html>
<html lang="en">
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
<title>ospab.host - первый хостинг в Великом Новгороде</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -10,19 +10,24 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1"
"react-icons": "^5.5.0"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.4.21",
"react-router-dom": "^7.9.1",
"tailwindcss": "^3.3.3",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"

View File

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

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,32 +1,28 @@
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import PageTmpl from './components/pagetempl';
import HomePage from './pages/index';
import MainPage from './pages/dashboard/mainpage';
import LogoutPage from './pages/dashboard/logout';
import LoginPage from './pages/login';
import RegisterPage from './pages/register';
import DashboardPage from './pages/dashboard/index';
import TariffsPage from './pages/tariffs';
import AboutPage from './pages/about';
import PrivateRoute from './components/privateroute';
const App = () => {
function App() {
return (
<Router>
<header className="bg-gray-800 text-white p-4">
<nav className="container mx-auto flex justify-between items-center">
<Link to="/" className="text-2xl font-bold">ospab.host</Link>
<div>
<Link to="/login" className="px-4 py-2 hover:bg-gray-700 rounded-md">Вход</Link>
<Link to="/register" className="px-4 py-2 ml-2 hover:bg-gray-700 rounded-md">Регистрация</Link>
<Link to="/dashboard" className="px-4 py-2 ml-2 hover:bg-gray-700 rounded-md">Дашборд</Link>
</div>
</nav>
</header>
<main className="p-4">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</main>
<Routes>
<Route path="/" element={<PageTmpl><HomePage /></PageTmpl>} />
<Route path="/tariffs" element={<PageTmpl><TariffsPage /></PageTmpl>} />
<Route path="/about" element={<PageTmpl><AboutPage /></PageTmpl>} />
<Route path="/dashboard" element={<PageTmpl><PrivateRoute><MainPage /></PrivateRoute></PageTmpl>} />
<Route path="/login" element={<PageTmpl><LoginPage /></PageTmpl>} />
<Route path="/register" element={<PageTmpl><RegisterPage /></PageTmpl>} />
<Route path="/logout" element={<PageTmpl><LogoutPage /></PageTmpl>} />
</Routes>
</Router>
);
};
}
export default App;

View File

@@ -0,0 +1,49 @@
import { Link } from 'react-router-dom';
const Footer = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-gray-800 text-white py-12">
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center md:text-left">
{/* About Section */}
<div>
<h3 className="text-xl font-bold mb-4">О нас</h3>
<p className="text-sm text-gray-400">
ospab.host - это надежный хостинг для ваших проектов. Мы предлагаем высокую производительность и круглосуточную поддержку.
</p>
</div>
{/* Quick Links */}
<div>
<h3 className="text-xl font-bold mb-4">Навигация</h3>
<ul className="space-y-2 text-sm">
<li><Link to="/" className="text-gray-400 hover:text-white transition-colors">Главная</Link></li>
<li><Link to="/tariffs" className="text-gray-400 hover:text-white transition-colors">Тарифы</Link></li>
<li><Link to="/about" className="text-gray-400 hover:text-white transition-colors">О нас</Link></li>
<li><Link to="/login" className="text-gray-400 hover:text-white transition-colors">Войти</Link></li>
</ul>
</div>
{/* Legal Documents */}
<div>
<h3 className="text-xl font-bold mb-4">Документы</h3>
<ul className="space-y-2 text-sm">
<li><a href="#" className="text-gray-400 hover:text-white transition-colors">Политика конфиденциальности</a></li>
<li><a href="#" className="text-gray-400 hover:text-white transition-colors">Условия использования</a></li>
</ul>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-700 text-center">
<p className="text-sm text-gray-400">
&copy; {currentYear} ospab.host. Все права защищены.
</p>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,58 @@
import { Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
const Header = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const checkLoginStatus = () => {
setIsLoggedIn(localStorage.getItem('isLoggedIn') === 'true');
};
checkLoginStatus();
window.addEventListener('storage', checkLoginStatus);
return () => {
window.removeEventListener('storage', checkLoginStatus);
};
}, []);
return (
<nav className="bg-white shadow-lg fixed w-full z-10 top-0">
<div className="container mx-auto px-6 py-3">
<div className="flex justify-between items-center">
<div className="text-xl font-bold text-gray-800">
<Link to="/" className="font-mono text-2xl">ospab.host</Link>
</div>
<div className="flex items-center space-x-4">
<Link to="/tariffs" className="text-gray-600 hover:text-ospab-primary transition-colors">Тарифы</Link>
<Link to="/about" className="text-gray-600 hover:text-ospab-primary transition-colors">О нас</Link>
{isLoggedIn ? (
<>
<Link to="/dashboard" className="text-gray-600 hover:text-ospab-primary transition-colors">Личный кабинет</Link>
<Link
to="/logout"
className="px-4 py-2 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-gray-500 hover:bg-red-500"
>
Выйти
</Link>
</>
) : (
<>
<Link to="/login" className="text-gray-600 hover:text-ospab-primary transition-colors">Войти</Link>
<Link
to="/register"
className="px-4 py-2 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent"
>
Зарегистрироваться
</Link>
</>
)}
</div>
</div>
</div>
</nav>
);
};
export default Header;

View File

@@ -0,0 +1,21 @@
import Header from './header';
import Footer from './footer';
import React from 'react';
interface PageTmplProps {
children: React.ReactNode;
}
const PageTmpl: React.FC<PageTmplProps> = ({ children }) => {
return (
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow pt-16">
{children}
</main>
<Footer />
</div>
);
};
export default PageTmpl;

View File

@@ -0,0 +1,13 @@
import { Navigate } from 'react-router-dom';
import React from 'react';
interface PrivateRouteProps {
children: React.ReactNode;
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true';
return isAuthenticated ? children : <Navigate to="/login" replace />;
};
export default PrivateRoute;

View File

@@ -1,68 +1,3 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,17 @@
const AboutPage = () => {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-20 px-4">
<div className="bg-white p-10 rounded-3xl shadow-2xl max-w-4xl mx-auto text-center">
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">О компании ospab.host</h1>
<p className="text-lg text-gray-700 mb-4">
Мы предоставляем надежные и масштабируемые хостинг-решения для разработчиков, стартапов и крупных компаний. Наша миссия дать вам инструменты, необходимые для реализации ваших идей в интернете, обеспечивая при этом высокую производительность и безопасность.
</p>
<p className="text-lg text-gray-700">
Наша инфраструктура построена на современных технологиях, а круглосуточная поддержка всегда готова помочь вам. Мы верим, что качественный хостинг должен быть доступен каждому.
</p>
</div>
</div>
);
};
export default AboutPage;

View File

@@ -1,3 +0,0 @@
const DashboardPage = () => <div>Дашборд (заглушка)</div>;
export default DashboardPage;

View File

@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
const LogoutPage = () => {
const navigate = useNavigate();
useEffect(() => {
// Удаляем все токены и флаг входа из localStorage
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('isLoggedIn');
console.log('Выполняется выход из системы...');
// После выхода перенаправляем пользователя на главную страницу
navigate('/');
}, [navigate]);
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<h1 className="text-xl font-bold text-gray-800">Выполняется выход...</h1>
</div>
);
};
export default LogoutPage;

View File

@@ -0,0 +1,26 @@
import { Link } from 'react-router-dom';
const MainPage = () => {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-4">
<div className="bg-white p-10 rounded-3xl shadow-2xl text-center max-w-2xl mx-auto">
<h1 className="text-4xl md:text-5xl font-extrabold text-gray-900 leading-tight">
Добро пожаловать в личный кабинет!
</h1>
<p className="mt-4 text-lg text-gray-600">
Здесь будет информация о твоих проектах и статистика.
</p>
<div className="mt-8">
<Link
to="/logout"
className="px-6 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent"
>
Выйти
</Link>
</div>
</div>
</div>
);
};
export default MainPage;

View File

@@ -1,8 +1,88 @@
import { Link } from 'react-router-dom';
import { FaServer, FaCloud, FaShieldAlt } from 'react-icons/fa';
const HomePage = () => {
return (
<div className="text-center mt-20">
<h1 className="text-4xl font-bold">Добро пожаловать на ospab.host!</h1>
<p className="text-lg mt-4">Мы работаем над нашим сайтом.</p>
<div className="min-h-screen bg-gray-50 text-gray-800">
{/* Hero Section */}
<section className="relative bg-gradient-to-b from-blue-100 to-white pt-24 pb-32">
<div className="container mx-auto text-center px-4">
<h1 className="text-5xl md:text-6xl font-extrabold leading-tight tracking-tighter text-gray-900">
Масштабируемый хостинг <br /> для ваших идей
</h1>
<p className="mt-6 text-lg md:text-xl max-w-2xl mx-auto text-gray-700">
Запускайте, масштабируйте и управляйте своими проектами с надёжной и высокопроизводительной инфраструктурой.
</p>
<div className="mt-10 flex flex-col sm:flex-row justify-center space-y-4 sm:space-y-0 sm:space-x-4">
<Link
to="/register"
className="px-8 py-4 rounded-full text-white font-bold text-lg transition-transform transform hover:scale-105 shadow-lg bg-ospab-primary hover:bg-ospab-accent"
>
Начать бесплатно
</Link>
<Link
to="/login"
className="px-8 py-4 rounded-full text-gray-800 font-bold text-lg border-2 border-gray-400 transition-colors hover:bg-gray-200 hover:border-gray-300"
>
Войти в аккаунт
</Link>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-20 px-4">
<div className="container mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 text-gray-900">Наши возможности</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-shadow duration-300 transform hover:scale-105">
<div className="flex justify-center mb-4">
<FaServer className="text-5xl text-blue-500" />
</div>
<h3 className="text-2xl font-bold text-center text-gray-900">Высокая производительность</h3>
<p className="mt-2 text-center text-gray-700">
Оптимизированные серверы для максимальной скорости загрузки вашего сайта.
</p>
</div>
<div className="bg-white p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-shadow duration-300 transform hover:scale-105">
<div className="flex justify-center mb-4">
<FaCloud className="text-5xl text-blue-500" />
</div>
<h3 className="text-2xl font-bold text-center text-gray-900">Масштабируемость</h3>
<p className="mt-2 text-center text-gray-700">
Легко увеличивайте или уменьшайте ресурсы по мере роста вашего проекта.
</p>
</div>
<div className="bg-white p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-shadow duration-300 transform hover:scale-105">
<div className="flex justify-center mb-4">
<FaShieldAlt className="text-5xl text-blue-500" />
</div>
<h3 className="text-2xl font-bold text-center text-gray-900">Надежность и безопасность</h3>
<p className="mt-2 text-center text-gray-700">
Ваши данные и приложения всегда под надёжной защитой.
</p>
</div>
</div>
</div>
</section>
{/* Call to Action Section */}
<section className="bg-gray-800 py-20 px-4 text-white text-center">
<h2 className="text-4xl md:text-5xl font-bold leading-tight">
Готовы начать?
</h2>
<p className="mt-4 text-lg md:text-xl max-w-2xl mx-auto text-gray-400">
Присоединяйтесь к тысячам разработчиков, которые доверяют нам.
</p>
<div className="mt-8">
<Link
to="/register"
className="px-8 py-4 rounded-full text-white font-bold text-lg transition-transform transform hover:scale-105 shadow-lg bg-ospab-primary hover:bg-ospab-accent"
>
Начать бесплатно
</Link>
</div>
</section>
</div>
);
};

View File

@@ -1,75 +1,69 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setMessage('');
try {
const response = await fetch('http://localhost:5000/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
const response = await axios.post('http://localhost:5000/api/auth/login', {
email: email,
password: password,
});
const data = await response.json();
if (response.ok) {
setMessage(data.message);
localStorage.setItem('token', data.token);
navigate('/dashboard'); // Перенаправляем на дашборд после успешного входа
} else {
setMessage(data.message || 'Ошибка входа. Проверьте email и пароль.');
}
// Сохраняем токен в localStorage
localStorage.setItem('access_token', response.data.token);
localStorage.setItem('isLoggedIn', 'true');
console.log('Успешный вход:', response.data);
navigate('/dashboard'); // Перенаправляем на личный кабинет
} catch (error) {
console.error('Ошибка:', error);
setMessage('Не удалось подключиться к серверу.');
let errMsg = 'Ошибка входа. Проверьте правильность email и пароля.';
if (axios.isAxiosError(error)) {
errMsg = error.response?.data?.message || errMsg;
console.error('Ошибка входа:', error.response?.data || error.message);
} else {
console.error('Ошибка входа:', error);
}
alert(errMsg);
}
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 className="text-3xl font-bold mb-4">Вход</h1>
<form onSubmit={handleLogin} className="bg-white p-8 rounded-lg shadow-md w-96">
<div className="mb-4">
<label className="block text-gray-700">Email</label>
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white p-8 md:p-10 rounded-3xl shadow-2xl w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-gray-800 mb-6">Вход в аккаунт</h1>
<form onSubmit={handleLogin}>
<input
type="email"
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
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"
/>
</div>
<div className="mb-6">
<label className="block text-gray-700">Пароль</label>
<input
type="password"
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
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"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 transition-colors"
>
Войти
</button>
</form>
{message && <p className="mt-4 text-center text-red-500">{message}</p>}
<p className="mt-4">
Нет аккаунта? <a href="/register" className="text-blue-500 hover:underline">Зарегистрироваться</a>
</p>
<button
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"
>
Войти
</button>
</form>
<p className="mt-6 text-gray-600">
Нет аккаунта?{' '}
<Link to="/register" className="text-ospab-primary font-bold hover:underline">
Зарегистрироваться
</Link>
</p>
</div>
</div>
);
};

View File

@@ -1,86 +1,75 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
const RegisterPage = () => {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const navigate = useNavigate();
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setMessage('');
try {
const response = await fetch('http://localhost:5000/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, email, password }),
const response = await axios.post('http://localhost:5000/api/auth/register', {
username: username,
email: email,
password: password
});
const data = await response.json();
if (response.ok) {
setMessage(data.message);
localStorage.setItem('token', data.token);
navigate('/dashboard'); // Перенаправляем на дашборд после успешной регистрации
} else {
setMessage(data.message || 'Ошибка регистрации.');
}
console.log('Успешная регистрация:', response.data);
navigate('/login'); // Перенаправляем пользователя на страницу входа
} catch (error) {
console.error('Ошибка:', error);
setMessage('Не удалось подключиться к серверу.');
let errMsg = 'Ошибка регистрации. Пожалуйста, попробуйте снова.';
if (axios.isAxiosError(error)) {
errMsg = error.response?.data?.message || errMsg;
console.error('Ошибка регистрации:', error.response?.data || error.message);
} else {
console.error('Ошибка регистрации:', error);
}
alert(errMsg);
}
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 className="text-3xl font-bold mb-4">Регистрация</h1>
<form onSubmit={handleRegister} className="bg-white p-8 rounded-lg shadow-md w-96">
<div className="mb-4">
<label className="block text-gray-700">Имя пользователя</label>
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white p-8 md:p-10 rounded-3xl shadow-2xl w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-gray-800 mb-6">Регистрация</h1>
<form onSubmit={handleRegister}>
<input
type="text"
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
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"
/>
</div>
<div className="mb-4">
<label className="block text-gray-700">Email</label>
<input
type="email"
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
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"
/>
</div>
<div className="mb-6">
<label className="block text-gray-700">Пароль</label>
<input
type="password"
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
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"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 transition-colors"
>
Зарегистрироваться
</button>
</form>
{message && <p className="mt-4 text-center text-red-500">{message}</p>}
<p className="mt-4">
Уже есть аккаунт? <a href="/login" className="text-blue-500 hover:underline">Войти</a>
</p>
<button
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"
>
Зарегистрироваться
</button>
</form>
<p className="mt-6 text-gray-600">
Уже есть аккаунт?{' '}
<Link to="/login" className="text-ospab-primary font-bold hover:underline">
Войти
</Link>
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,130 @@
import { useState } from 'react';
const TariffsPage = () => {
const [cpu, setCpu] = useState(1);
const [ram, setRam] = useState(1);
const [storage, setStorage] = useState(50);
const cpuPrice = 500;
const ramPrice = 300;
const storagePrice = 5;
const total = cpu * cpuPrice + ram * ramPrice + storage * storagePrice;
return (
<div className="min-h-screen bg-gray-50 py-20">
<div className="container mx-auto px-4">
<h1 className="text-4xl md:text-5xl font-bold text-center mb-16 text-gray-900">Выберите подходящий тариф</h1>
{/* Basic Tariffs Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-20">
{/* Tariff Card 1 */}
<div className="bg-white p-8 rounded-2xl shadow-xl text-center transition-transform hover:scale-105 duration-300">
<h2 className="text-3xl font-bold text-gray-800">Базовый</h2>
<p className="mt-4 text-4xl font-extrabold text-ospab-primary">1500<span className="text-lg font-normal text-gray-500">/мес</span></p>
<ul className="mt-4 text-gray-600 space-y-2">
<li>1 ядро CPU</li>
<li>2 ГБ RAM</li>
<li>100 ГБ SSD</li>
<li>Неограниченный трафик</li>
</ul>
<button className="mt-8 px-6 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent">
Выбрать
</button>
</div>
{/* Tariff Card 2 */}
<div className="bg-white p-8 rounded-2xl shadow-xl text-center border-4 border-ospab-primary transition-transform hover:scale-105 duration-300">
<h2 className="text-3xl font-bold text-gray-800">Профессиональный</h2>
<p className="mt-4 text-4xl font-extrabold text-ospab-primary">4000<span className="text-lg font-normal text-gray-500">/мес</span></p>
<ul className="mt-4 text-gray-600 space-y-2">
<li>4 ядра CPU</li>
<li>8 ГБ RAM</li>
<li>250 ГБ SSD</li>
<li>Приоритетная поддержка</li>
</ul>
<button className="mt-8 px-6 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent">
Выбрать
</button>
</div>
{/* Tariff Card 3 */}
<div className="bg-white p-8 rounded-2xl shadow-xl text-center transition-transform hover:scale-105 duration-300">
<h2 className="text-3xl font-bold text-gray-800">Бизнес</h2>
<p className="mt-4 text-4xl font-extrabold text-ospab-primary">8000<span className="text-lg font-normal text-gray-500">/мес</span></p>
<ul className="mt-4 text-gray-600 space-y-2">
<li>8 ядер CPU</li>
<li>16 ГБ RAM</li>
<li>500 ГБ SSD</li>
<li>24/7 Мониторинг</li>
</ul>
<button className="mt-8 px-6 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent">
Выбрать
</button>
</div>
</div>
{/* Server Constructor Section */}
<div className="bg-white p-10 rounded-3xl shadow-2xl max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-8 text-gray-900">Соберите свой сервер</h2>
<div className="space-y-6">
{/* CPU Slider */}
<div>
<label htmlFor="cpu" className="block text-lg font-medium text-gray-700">Ядра CPU: {cpu}</label>
<input
type="range"
id="cpu"
min="1"
max="16"
value={cpu}
onChange={(e) => setCpu(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<p className="text-sm text-gray-500 mt-1">Цена: {cpu * cpuPrice}</p>
</div>
{/* RAM Slider */}
<div>
<label htmlFor="ram" className="block text-lg font-medium text-gray-700">Оперативная память (ГБ): {ram}</label>
<input
type="range"
id="ram"
min="1"
max="32"
value={ram}
onChange={(e) => setRam(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<p className="text-sm text-gray-500 mt-1">Цена: {ram * ramPrice}</p>
</div>
{/* Storage Slider */}
<div>
<label htmlFor="storage" className="block text-lg font-medium text-gray-700">Диск (ГБ): {storage}</label>
<input
type="range"
id="storage"
min="50"
max="2000"
step="50"
value={storage}
onChange={(e) => setStorage(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<p className="text-sm text-gray-500 mt-1">Цена: {storage * storagePrice}</p>
</div>
</div>
<div className="mt-8 text-center">
<p className="text-2xl font-bold text-gray-800">Итого: {total}<span className="text-lg font-normal text-gray-500">/мес</span></p>
<button className="mt-4 px-8 py-4 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent">
Собрать сервер
</button>
</div>
</div>
</div>
</div>
);
};
export default TariffsPage;

View File

@@ -0,0 +1,19 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'ospab-primary': '#3B82F6', // Голубой
'ospab-accent': '#FF13F0', // Розовый
},
fontFamily: {
mono: ['Share Tech Mono', 'monospace'],
},
},
},
plugins: [],
}