new api endpoint and api rate limit
This commit is contained in:
@@ -5,4 +5,4 @@ VITE_TURNSTILE_SITE_KEY=0x4AAAAAAB7306voAK0Pjx8O
|
||||
|
||||
# API URLs (с портом 5000)
|
||||
VITE_API_URL=https://ospab.host:5000
|
||||
VITE_SOCKET_URL=wss://ospab.host:5000
|
||||
VITE_SOCKET_URL=wss://ospab.host:5000
|
||||
|
||||
21
ospabhost/frontend/package-lock.json
generated
21
ospabhost/frontend/package-lock.json
generated
@@ -85,6 +85,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -1589,6 +1590,7 @@
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -1621,6 +1623,7 @@
|
||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1688,6 +1691,7 @@
|
||||
"integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.44.0",
|
||||
"@typescript-eslint/types": "8.44.0",
|
||||
@@ -1940,6 +1944,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2167,6 +2172,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -2840,6 +2846,7 @@
|
||||
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4235,6 +4242,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -4478,6 +4486,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -4490,6 +4499,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -4531,7 +4541,8 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-qr-code": {
|
||||
"version": "2.0.18",
|
||||
@@ -4612,6 +4623,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -4734,7 +4746,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -5309,6 +5322,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5368,6 +5382,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5492,6 +5507,7 @@
|
||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -5585,6 +5601,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, lazy, Suspense } from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import Pagetempl from './components/pagetempl';
|
||||
import DashboardTempl from './components/dashboardtempl';
|
||||
import Homepage from './pages/index';
|
||||
import Dashboard from './pages/dashboard/mainpage';
|
||||
import Loginpage from './pages/login';
|
||||
import Registerpage from './pages/register';
|
||||
import QRLoginPage from './pages/qr-login';
|
||||
import Aboutpage from './pages/about';
|
||||
import S3PlansPage from './pages/s3plans';
|
||||
import Privacy from './pages/privacy';
|
||||
import Terms from './pages/terms';
|
||||
import Blog from './pages/blog';
|
||||
import BlogPost from './pages/blogpost';
|
||||
import NotFound from './pages/404';
|
||||
import Unauthorized from './pages/401';
|
||||
import Forbidden from './pages/403';
|
||||
import ServerError from './pages/500';
|
||||
import BadGateway from './pages/502';
|
||||
import ServiceUnavailable from './pages/503';
|
||||
import GatewayTimeout from './pages/504';
|
||||
import ErrorPage from './pages/errors';
|
||||
import NetworkError from './pages/errors/NetworkError';
|
||||
|
||||
// Lazy loading для оптимизации
|
||||
const Homepage = lazy(() => import('./pages/index'));
|
||||
const Dashboard = lazy(() => import('./pages/dashboard/mainpage'));
|
||||
const Loginpage = lazy(() => import('./pages/login'));
|
||||
const Registerpage = lazy(() => import('./pages/register'));
|
||||
const QRLoginPage = lazy(() => import('./pages/qr-login'));
|
||||
const Aboutpage = lazy(() => import('./pages/about'));
|
||||
const S3PlansPage = lazy(() => import('./pages/s3plans'));
|
||||
const Privacy = lazy(() => import('./pages/privacy'));
|
||||
const Terms = lazy(() => import('./pages/terms'));
|
||||
const Blog = lazy(() => import('./pages/blog'));
|
||||
const BlogPost = lazy(() => import('./pages/blogpost'));
|
||||
const NotFound = lazy(() => import('./pages/404'));
|
||||
const Unauthorized = lazy(() => import('./pages/401'));
|
||||
const Forbidden = lazy(() => import('./pages/403'));
|
||||
const ServerError = lazy(() => import('./pages/500'));
|
||||
const BadGateway = lazy(() => import('./pages/502'));
|
||||
const ServiceUnavailable = lazy(() => import('./pages/503'));
|
||||
const GatewayTimeout = lazy(() => import('./pages/504'));
|
||||
const ErrorPage = lazy(() => import('./pages/errors'));
|
||||
const NetworkError = lazy(() => import('./pages/errors/NetworkError'));
|
||||
import Privateroute from './components/privateroute';
|
||||
import { AuthProvider } from './context/authcontext';
|
||||
import { WebSocketProvider } from './context/WebSocketContext';
|
||||
@@ -222,6 +224,102 @@ const SEO_CONFIG: Record<string, {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/tariffs': {
|
||||
ru: {
|
||||
title: 'Тарифы S3 хранилища',
|
||||
description: 'Выберите тариф для S3 хранилища ospab.host. Цена за GB, трафик, операции. Создайте бакет и начните хранить файлы.',
|
||||
keywords: 'тарифы S3, цены хранилища, облачное хранилище тарифы, S3 планы',
|
||||
og: {
|
||||
title: 'Тарифы S3 хранилища ospab.host',
|
||||
description: 'Выберите подходящий тариф для вашего проекта',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/tariffs',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'S3 Storage Plans',
|
||||
description: 'Choose a plan for ospab.host S3 storage. Price per GB, traffic, operations. Create a bucket and start storing files.',
|
||||
keywords: 'S3 plans, storage prices, cloud storage plans, S3 tariffs',
|
||||
og: {
|
||||
title: 'ospab.host S3 Storage Plans',
|
||||
description: 'Choose the right plan for your project',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/tariffs',
|
||||
},
|
||||
},
|
||||
},
|
||||
'/qr-login': {
|
||||
ru: {
|
||||
title: 'QR Вход',
|
||||
description: 'Быстрый вход через QR код. Сканируйте код в мобильном приложении Telegram для мгновенной авторизации.',
|
||||
keywords: 'QR вход, быстрый вход, Telegram авторизация, QR код вход',
|
||||
og: {
|
||||
title: 'QR Вход в ospab.host',
|
||||
description: 'Быстрая авторизация через QR код',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/qr-login',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'QR Login',
|
||||
description: 'Quick login via QR code. Scan the code in the Telegram mobile app for instant authorization.',
|
||||
keywords: 'QR login, quick login, Telegram authorization, QR code login',
|
||||
og: {
|
||||
title: 'QR Login to ospab.host',
|
||||
description: 'Fast authorization via QR code',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/qr-login',
|
||||
},
|
||||
},
|
||||
},
|
||||
'/blog/:url': {
|
||||
ru: {
|
||||
title: 'Статья блога',
|
||||
description: 'Прочитайте статью в блоге ospab.host. Полезные материалы о хостинге, S3 и DevOps.',
|
||||
keywords: 'блог статья, хостинг гайд, S3 туториал, DevOps',
|
||||
og: {
|
||||
title: 'Статья блога ospab.host',
|
||||
description: 'Полезные материалы о технологиях',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/blog',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Blog Post',
|
||||
description: 'Read an article on the ospab.host blog. Useful materials about hosting, S3 and DevOps.',
|
||||
keywords: 'blog post, hosting guide, S3 tutorial, DevOps',
|
||||
og: {
|
||||
title: 'ospab.host Blog Post',
|
||||
description: 'Useful materials about technologies',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/blog',
|
||||
},
|
||||
},
|
||||
},
|
||||
'/dashboard': {
|
||||
ru: {
|
||||
title: 'Панель управления',
|
||||
description: 'Личный кабинет ospab.host. Управляйте хранилищем, тикетами, балансом и настройками аккаунта.',
|
||||
keywords: 'панель управления, личный кабинет, дашборд, управление аккаунтом',
|
||||
og: {
|
||||
title: 'Панель управления ospab.host',
|
||||
description: 'Управление вашим аккаунтом и услугами',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/dashboard',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Control Panel',
|
||||
description: 'ospab.host personal account. Manage storage, tickets, balance and account settings.',
|
||||
keywords: 'control panel, personal account, dashboard, account management',
|
||||
og: {
|
||||
title: 'ospab.host Control Panel',
|
||||
description: 'Manage your account and services',
|
||||
image: 'https://ospab.host/og-image.jpg',
|
||||
url: 'https://ospab.host/dashboard',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Компонент для обновления SEO при изменении маршрута
|
||||
@@ -232,16 +330,24 @@ function SEOUpdater() {
|
||||
useEffect(() => {
|
||||
const pathname = location.pathname;
|
||||
|
||||
// Нормализуем путь, убирая префикс /en для поиска в SEO_CONFIG
|
||||
let normalizedPath = pathname;
|
||||
if (pathname.startsWith('/en/')) {
|
||||
normalizedPath = pathname.slice(3);
|
||||
} else if (pathname === '/en') {
|
||||
normalizedPath = '/';
|
||||
}
|
||||
|
||||
// Получаем SEO данные для текущего маршрута, иначе используем дефолтные
|
||||
const seoConfig = SEO_CONFIG[pathname];
|
||||
const seoConfig = SEO_CONFIG[normalizedPath];
|
||||
const seoData = seoConfig ? seoConfig[locale] : {
|
||||
title: locale === 'en' ? 'ospab.host - cloud hosting' : 'ospab.host - облачный хостинг',
|
||||
title: locale === 'en' ? 'ospab.host - cloud storage' : 'ospab.host - облачное хранилище',
|
||||
description: locale === 'en'
|
||||
? 'ospab.host - reliable cloud hosting and virtual machines in Veliky Novgorod.'
|
||||
: 'ospab.host - надёжный облачный хостинг и виртуальные машины в Великом Новгороде.',
|
||||
? 'ospab.host - reliable cloud S3-compatible storage in Veliky Novgorod. File storage, backups, media content.'
|
||||
: 'ospab.host - надёжное облачное S3-совместимое хранилище в Великом Новгороде. Хранение файлов, резервные копии, медиа-контент.',
|
||||
keywords: locale === 'en'
|
||||
? 'hosting, cloud hosting, VPS, VDS'
|
||||
: 'хостинг, облачный хостинг, VPS, VDS',
|
||||
? 'hosting, cloud storage, S3, file storage'
|
||||
: 'хостинг, облачное хранилище, S3, хранение файлов',
|
||||
};
|
||||
|
||||
// Устанавливаем title
|
||||
@@ -277,7 +383,7 @@ function SEOUpdater() {
|
||||
canonicalTag.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonicalTag);
|
||||
}
|
||||
canonicalTag.setAttribute('href', `https://ospab.host${pathname}`);
|
||||
canonicalTag.setAttribute('href', `https://ospab.host${normalizedPath}`);
|
||||
|
||||
// Open Graph теги
|
||||
if (seoData.og) {
|
||||
@@ -312,9 +418,10 @@ function App() {
|
||||
<WebSocketProvider>
|
||||
<ToastProvider>
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
{/* Русские маршруты (без префикса) */}
|
||||
<Route path="/" element={<Pagetempl><Homepage /></Pagetempl>} />
|
||||
<Suspense fallback={<div style={{display:'flex',justifyContent:'center',alignItems:'center',height:'100vh'}}>Loading...</div>}>
|
||||
<Routes>
|
||||
{/* Русские маршруты (без префикса) */}
|
||||
<Route path="/" element={<Pagetempl><Homepage /></Pagetempl>} />
|
||||
<Route path="/about" element={<Pagetempl><Aboutpage /></Pagetempl>} />
|
||||
<Route path="/tariffs" element={<Pagetempl><S3PlansPage /></Pagetempl>} />
|
||||
<Route path="/blog" element={<Pagetempl><Blog /></Pagetempl>} />
|
||||
@@ -379,6 +486,7 @@ function App() {
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</ToastProvider>
|
||||
</WebSocketProvider>
|
||||
|
||||
@@ -5,14 +5,6 @@
|
||||
const PRODUCTION_API_ORIGIN = 'https://api.ospab.host';
|
||||
|
||||
const resolveDefaultApiUrl = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return import.meta.env.DEV ? 'http://localhost:5000' : PRODUCTION_API_ORIGIN;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
return 'http://localhost:5000';
|
||||
}
|
||||
|
||||
return PRODUCTION_API_ORIGIN;
|
||||
};
|
||||
|
||||
|
||||
@@ -61,9 +61,92 @@ export const en: TranslationKeys = {
|
||||
about: {
|
||||
title: 'About Us',
|
||||
subtitle: 'Ospab.host — modern cloud storage platform',
|
||||
hero: {
|
||||
title: 'The Story of ospab.host',
|
||||
subtitle: 'The first data center in Veliky Novgorod.',
|
||||
},
|
||||
founder: {
|
||||
name: 'Georgy',
|
||||
title: 'Founder & CEO',
|
||||
age: '13 years old',
|
||||
location: 'Veliky Novgorod',
|
||||
github: 'Project source code',
|
||||
bio: "At 13, I decided to create something that didn't exist in my city — a modern data center. Starting with learning technologies and working on my first hosting, I'm gradually turning a dream into reality. With the help of an investor friend, we're building the infrastructure of the future for Veliky Novgorod.",
|
||||
alt: 'Georgy, founder of ospab.host',
|
||||
},
|
||||
story: {
|
||||
title: 'Our Story',
|
||||
text: 'We created ospab.host to provide reliable and affordable cloud storage for businesses and developers.',
|
||||
sections: {
|
||||
start: {
|
||||
title: 'September 2025 — The Beginning',
|
||||
text: "It all started with a simple idea: create a place where anyone can host their project, website, or server with maximum reliability and speed. Veliky Novgorod deserves its own data center, and I decided to take on this task.",
|
||||
},
|
||||
support: {
|
||||
title: 'Support and Development',
|
||||
text: "My investor friend believed in the project and helps with infrastructure development. We're building not just a business, but a community where every client is like a friend, and support is always nearby.",
|
||||
},
|
||||
future: {
|
||||
title: 'Present and Future',
|
||||
text: "Currently, we're actively working on hosting and preparing infrastructure for the future data center. ospab.host is the first step towards the digital future of Veliky Novgorod, and we're just getting started.",
|
||||
},
|
||||
},
|
||||
},
|
||||
mission: {
|
||||
title: 'Our Mission',
|
||||
subtitle: "Make quality hosting accessible to everyone, and the data center — the city's pride",
|
||||
features: {
|
||||
technologies: {
|
||||
title: 'Modern Technologies',
|
||||
description: 'We use the latest equipment and software for maximum performance',
|
||||
},
|
||||
security: {
|
||||
title: 'Data Security',
|
||||
description: 'Customer data protection is our priority. Regular backups and 24/7 monitoring',
|
||||
},
|
||||
support: {
|
||||
title: 'Personal Support',
|
||||
description: 'Every customer receives personal attention and help from the founder',
|
||||
},
|
||||
},
|
||||
},
|
||||
whyChoose: {
|
||||
title: 'Why choose ospab.host?',
|
||||
features: {
|
||||
first: {
|
||||
title: 'First data center in the city',
|
||||
description: "We're making Veliky Novgorod history",
|
||||
},
|
||||
pricing: {
|
||||
title: 'Affordable pricing',
|
||||
description: 'Quality hosting for everyone without overpaying',
|
||||
},
|
||||
fastSupport: {
|
||||
title: 'Fast support',
|
||||
description: "We'll answer questions anytime",
|
||||
},
|
||||
transparency: {
|
||||
title: 'Transparency',
|
||||
description: 'Honest about capabilities and limitations',
|
||||
},
|
||||
infrastructure: {
|
||||
title: 'Modern infrastructure',
|
||||
description: 'Up-to-date software and equipment',
|
||||
},
|
||||
dream: {
|
||||
title: 'A dream becoming reality',
|
||||
description: 'A story to be proud of',
|
||||
},
|
||||
openSource: {
|
||||
title: 'Source code on GitHub',
|
||||
},
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
title: 'Become part of history',
|
||||
subtitle: 'Join ospab.host and help create the digital future of Veliky Novgorod',
|
||||
startFree: 'Start for free',
|
||||
viewPlans: 'View plans',
|
||||
},
|
||||
team: {
|
||||
title: 'Our Team',
|
||||
@@ -324,8 +407,13 @@ export const en: TranslationKeys = {
|
||||
title: 'S3 Storage Pricing',
|
||||
subtitle: 'Choose the right plan for your needs',
|
||||
popular: 'Popular',
|
||||
features: 'Features',
|
||||
storage: 'Storage',
|
||||
features: 'Features', baseFeatures: [
|
||||
'S3-compatible API and AWS SDK support',
|
||||
'Deployment in ru-central-1 region',
|
||||
'Versioning and presigned URLs',
|
||||
'Access management via Access Key/Secret Key',
|
||||
'Notifications and monitoring in client panel'
|
||||
], storage: 'Storage',
|
||||
traffic: 'Outbound Traffic',
|
||||
requests: 'Requests',
|
||||
support: 'Support',
|
||||
@@ -338,5 +426,45 @@ export const en: TranslationKeys = {
|
||||
contactUs: 'Contact Us',
|
||||
customPlan: 'Need a custom plan?',
|
||||
customPlanDescription: 'Contact us to discuss special requirements.',
|
||||
error: {
|
||||
loadFailed: 'Failed to load plans',
|
||||
loadError: 'Error loading plans',
|
||||
},
|
||||
page: {
|
||||
title: 'Transparent pricing for any volume',
|
||||
subtitle: 'Pay only for the resources you need. 12 ready-made plans for teams of any size, with included traffic, requests and priority support.',
|
||||
network: 'network',
|
||||
api: 'S3-compatible API',
|
||||
loadReady: 'Load Ready',
|
||||
loadReadyDesc: 'Infrastructure ready for peak loads up to 10 Gbit/s per server.',
|
||||
security: 'Security',
|
||||
securityDesc: 'AES-256 encryption, regular audits and compliance with standards.',
|
||||
compatibility: 'Compatibility',
|
||||
compatibilityDesc: 'Full compatibility with AWS S3 API and SDK.',
|
||||
payAsYouGo: 'Pay as you go',
|
||||
payAsYouGoDesc: 'Pay only for used resources without hidden fees.',
|
||||
customPlanTitle: 'Custom Plan',
|
||||
customPlanDesc: 'Calculate cost for your project',
|
||||
gb: 'GB',
|
||||
calculate: 'Calculate',
|
||||
paymentError: 'Failed to start payment',
|
||||
creatingCart: 'Creating cart...',
|
||||
selectPlan: 'Select plan',
|
||||
customTitle: 'Custom Plan',
|
||||
customDesc: 'Specify the required amount of GB and get automatic cost calculation',
|
||||
gbQuestion: 'How many GB do you need?',
|
||||
useCases: {
|
||||
backups: 'Backups & DR',
|
||||
backupsDesc: 'Replication, Object Lock and object lifecycle allow storing backups and quick recovery.',
|
||||
media: 'Media Platforms',
|
||||
mediaDesc: 'CDN integration, presigned URLs and high bandwidth for video, images and audio.',
|
||||
saas: 'SaaS & Data Lake',
|
||||
saasDesc: 'IAM, API versions and audit logs ensure security and compliance with GDPR, 152-FZ and SOC 2.',
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready to deploy S3 storage?',
|
||||
subtitle: 'Create an account and get access to management console, API keys and detailed usage analytics.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,9 +59,92 @@ export const ru = {
|
||||
about: {
|
||||
title: 'О компании',
|
||||
subtitle: 'Ospab.host — современная платформа облачного хранилища',
|
||||
hero: {
|
||||
title: 'История ospab.host',
|
||||
subtitle: 'Первый дата-центр в Великом Новгороде.',
|
||||
},
|
||||
founder: {
|
||||
name: 'Георгий',
|
||||
title: 'Основатель и CEO',
|
||||
age: '13 лет',
|
||||
location: 'Великий Новгород',
|
||||
github: 'Исходный код проекта',
|
||||
bio: 'В 13 лет я решил создать то, чего не было в моём городе — современный дата-центр. Начав с изучения технологий и работы над первым хостингом, я постепенно превращаю мечту в реальность. С помощью друга-инвестора мы строим инфраструктуру будущего для Великого Новгорода.',
|
||||
alt: 'Георгий, основатель ospab.host',
|
||||
},
|
||||
story: {
|
||||
title: 'Наша история',
|
||||
text: 'Мы создали ospab.host чтобы предоставить надёжное и доступное облачное хранилище для бизнеса и разработчиков.',
|
||||
sections: {
|
||||
start: {
|
||||
title: 'Сентябрь 2025 — Начало пути',
|
||||
text: 'Всё началось с простой идеи: создать место, где любой сможет разместить свой проект, сайт или сервер с максимальной надёжностью и скоростью. Великий Новгород заслуживает свой дата-центр, и я решил взяться за эту задачу.',
|
||||
},
|
||||
support: {
|
||||
title: 'Поддержка и развитие',
|
||||
text: 'Мой друг-инвестор поверил в проект и помогает с развитием инфраструктуры. Мы строим не просто бизнес, а сообщество, где каждый клиент — как друг, а поддержка всегда рядом.',
|
||||
},
|
||||
future: {
|
||||
title: 'Настоящее и будущее',
|
||||
text: 'Сейчас мы активно работаем над хостингом и подготовкой инфраструктуры для будущего ЦОД. ospab.host — это первый шаг к цифровому будущему Великого Новгорода, и мы только начинаем.',
|
||||
},
|
||||
},
|
||||
},
|
||||
mission: {
|
||||
title: 'Наша миссия',
|
||||
subtitle: 'Сделать качественный хостинг доступным для всех, а ЦОД — гордостью города',
|
||||
features: {
|
||||
technologies: {
|
||||
title: 'Современные технологии',
|
||||
description: 'Используем новейшее оборудование и программное обеспечение для максимальной производительности',
|
||||
},
|
||||
security: {
|
||||
title: 'Безопасность данных',
|
||||
description: 'Защита информации клиентов — наш приоритет. Регулярные бэкапы и мониторинг 24/7',
|
||||
},
|
||||
support: {
|
||||
title: 'Личная поддержка',
|
||||
description: 'Каждый клиент получает персональное внимание и помощь от основателя',
|
||||
},
|
||||
},
|
||||
},
|
||||
whyChoose: {
|
||||
title: 'Почему выбирают ospab.host?',
|
||||
features: {
|
||||
first: {
|
||||
title: 'Первый ЦОД в городе',
|
||||
description: 'Мы создаём историю Великого Новгорода',
|
||||
},
|
||||
pricing: {
|
||||
title: 'Доступные тарифы',
|
||||
description: 'Качественный хостинг для всех без переплат',
|
||||
},
|
||||
fastSupport: {
|
||||
title: 'Быстрая поддержка',
|
||||
description: 'Ответим на вопросы в любое время',
|
||||
},
|
||||
transparency: {
|
||||
title: 'Прозрачность',
|
||||
description: 'Честно о возможностях и ограничениях',
|
||||
},
|
||||
infrastructure: {
|
||||
title: 'Современная инфраструктура',
|
||||
description: 'Актуальное ПО и оборудование',
|
||||
},
|
||||
dream: {
|
||||
title: 'Мечта становится реальностью',
|
||||
description: 'История, которой можно гордиться',
|
||||
},
|
||||
openSource: {
|
||||
title: 'Исходный код на GitHub',
|
||||
},
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
title: 'Станьте частью истории',
|
||||
subtitle: 'Присоединяйтесь к ospab.host и помогите создать цифровое будущее Великого Новгорода',
|
||||
startFree: 'Начать бесплатно',
|
||||
viewPlans: 'Посмотреть тарифы',
|
||||
},
|
||||
team: {
|
||||
title: 'Наша команда',
|
||||
@@ -323,6 +406,13 @@ export const ru = {
|
||||
subtitle: 'Выберите подходящий план для ваших задач',
|
||||
popular: 'Популярный',
|
||||
features: 'Возможности',
|
||||
baseFeatures: [
|
||||
'S3-совместимый API и совместимость с AWS SDK',
|
||||
'Развёртывание в регионе ru-central-1',
|
||||
'Версионирование и presigned URL',
|
||||
'Управление доступом через Access Key/Secret Key',
|
||||
'Уведомления и мониторинг в панели клиента'
|
||||
],
|
||||
storage: 'Хранилище',
|
||||
traffic: 'Исходящий трафик',
|
||||
requests: 'Запросов',
|
||||
@@ -336,6 +426,46 @@ export const ru = {
|
||||
contactUs: 'Связаться с нами',
|
||||
customPlan: 'Нужен индивидуальный план?',
|
||||
customPlanDescription: 'Свяжитесь с нами для обсуждения особых условий.',
|
||||
error: {
|
||||
loadFailed: 'Не удалось загрузить тарифы',
|
||||
loadError: 'Ошибка загрузки тарифов',
|
||||
},
|
||||
page: {
|
||||
title: 'Прозрачные тарифы для любого объёма',
|
||||
subtitle: 'Оплачивайте только за необходимые ресурсы. 12 готовых тарифов для команд любого размера, с включённым трафиком, запросами и приоритетной поддержкой.',
|
||||
network: 'сеть',
|
||||
api: 'S3-совместимый API',
|
||||
loadReady: 'Готовность к нагрузке',
|
||||
loadReadyDesc: 'Инфраструктура готова к пиковым нагрузкам до 10 Гбит/с на сервер.',
|
||||
security: 'Безопасность',
|
||||
securityDesc: 'Шифрование AES-256, регулярные аудиты и соответствие стандартам.',
|
||||
compatibility: 'Совместимость',
|
||||
compatibilityDesc: 'Полная совместимость с AWS S3 API и SDK.',
|
||||
payAsYouGo: 'Оплата по факту',
|
||||
payAsYouGoDesc: 'Оплачивайте только за использованные ресурсы без скрытых платежей.',
|
||||
customPlanTitle: 'Индивидуальный тариф',
|
||||
customPlanDesc: 'Рассчитайте стоимость для вашего проекта',
|
||||
gb: 'ГБ',
|
||||
calculate: 'Рассчитать',
|
||||
paymentError: 'Не удалось начать оплату',
|
||||
creatingCart: 'Создание корзины...',
|
||||
selectPlan: 'Выбрать план',
|
||||
customTitle: 'Кастомный тариф',
|
||||
customDesc: 'Укажите нужное количество GB и получите автоматический расчёт стоимости',
|
||||
gbQuestion: 'Сколько GB вам нужно?',
|
||||
useCases: {
|
||||
backups: 'Бэкапы и DR',
|
||||
backupsDesc: 'Репликация, Object Lock и цикл жизни объектов позволяют хранить резервные копии и быстро восстанавливаться.',
|
||||
media: 'Медиа-платформы',
|
||||
mediaDesc: 'CDN-интеграция, presigned URL и высокая пропускная способность для видео, изображений и аудио.',
|
||||
saas: 'SaaS & Data Lake',
|
||||
saasDesc: 'IAM, версии API и аудит логов обеспечивают безопасность и соответствие требованиям GDPR, 152-ФЗ и SOC 2.',
|
||||
},
|
||||
cta: {
|
||||
title: 'Готовы развернуть S3 хранилище?',
|
||||
subtitle: 'Создайте аккаунт и получите доступ к консоли управления, API ключам и детальной аналитике использования.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ type TranslationKey = NestedKeyOf<TranslationKeys>;
|
||||
/**
|
||||
* Получить значение по вложенному ключу
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): any {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
@@ -36,7 +36,7 @@ function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : path;
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,18 +46,20 @@ export function useTranslation() {
|
||||
const { locale, setLocale } = useLocale();
|
||||
|
||||
const t = useCallback(
|
||||
(key: TranslationKey, params?: Record<string, string | number>): string => {
|
||||
(key: TranslationKey, params?: Record<string, string | number>): any => {
|
||||
const translation = getNestedValue(
|
||||
translations[locale] as unknown as Record<string, unknown>,
|
||||
key
|
||||
);
|
||||
|
||||
if (!params) return translation;
|
||||
if (typeof translation === 'string' && params) {
|
||||
// Замена параметров {{param}}
|
||||
return translation.replace(/\{\{(\w+)\}\}/g, (_, paramKey) => {
|
||||
return params[paramKey]?.toString() ?? `{{${paramKey}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Замена параметров {{param}}
|
||||
return translation.replace(/\{\{(\w+)\}\}/g, (_, paramKey) => {
|
||||
return params[paramKey]?.toString() ?? `{{${paramKey}}}`;
|
||||
});
|
||||
return translation ?? key;
|
||||
},
|
||||
[locale]
|
||||
);
|
||||
|
||||
@@ -3,9 +3,8 @@ import { useTranslation } from '../i18n';
|
||||
import { useLocalePath } from '../middleware';
|
||||
|
||||
const AboutPage = () => {
|
||||
const { locale } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const localePath = useLocalePath();
|
||||
const isEn = locale === 'en';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
@@ -19,10 +18,10 @@ const AboutPage = () => {
|
||||
<div className="container mx-auto max-w-6xl relative z-10">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl md:text-6xl font-extrabold mb-6 leading-tight">
|
||||
{isEn ? 'The Story of ospab.host' : 'История ospab.host'}
|
||||
{t('about.hero.title')}
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-blue-100 max-w-3xl mx-auto">
|
||||
{isEn ? 'The first data center in Veliky Novgorod.' : 'Первый дата-центр в Великом Новгороде.'}
|
||||
{t('about.hero.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,7 +35,7 @@ const AboutPage = () => {
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src="/me.jpg"
|
||||
alt={isEn ? 'Georgy, founder of ospab.host' : 'Георгий, основатель ospab.host'}
|
||||
alt={t('about.founder.alt')}
|
||||
className="w-48 h-48 md:w-56 md:h-56 rounded-2xl shadow-2xl border-4 border-ospab-primary object-cover"
|
||||
width="224"
|
||||
height="224"
|
||||
@@ -45,23 +44,23 @@ const AboutPage = () => {
|
||||
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-3">{isEn ? 'Georgy' : 'Георгий'}</h2>
|
||||
<p className="text-xl text-ospab-primary font-semibold mb-2">{isEn ? 'Founder & CEO' : 'Основатель и CEO'}</p>
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-3">{t('about.founder.name')}</h2>
|
||||
<p className="text-xl text-ospab-primary font-semibold mb-2">{t('about.founder.title')}</p>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-gray-600">
|
||||
<span className="flex items-center gap-2">
|
||||
<FaUsers className="text-ospab-primary" />
|
||||
{isEn ? '13 years old' : '13 лет'}
|
||||
{t('about.founder.age')}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<FaServer className="text-ospab-primary" />
|
||||
{isEn ? 'Veliky Novgorod' : 'Великий Новгород'}
|
||||
{t('about.founder.location')}
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/ospab/ospabhost8.1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 hover:text-ospab-primary transition-colors"
|
||||
title={isEn ? 'Project source code' : 'Исходный код проекта'}
|
||||
title={t('about.founder.github')}
|
||||
>
|
||||
<FaGithub className="text-ospab-primary" />
|
||||
GitHub
|
||||
@@ -70,9 +69,7 @@ const AboutPage = () => {
|
||||
</div>
|
||||
|
||||
<p className="text-lg text-gray-700 leading-relaxed">
|
||||
{isEn
|
||||
? "At 13, I decided to create something that didn't exist in my city — a modern data center. Starting with learning technologies and working on my first hosting, I'm gradually turning a dream into reality. With the help of an investor friend, we're building the infrastructure of the future for Veliky Novgorod."
|
||||
: 'В 13 лет я решил создать то, чего не было в моём городе — современный дата-центр. Начав с изучения технологий и работы над первым хостингом, я постепенно превращаю мечту в реальность. С помощью друга-инвестора мы строим инфраструктуру будущего для Великого Новгорода.'}
|
||||
{t('about.founder.bio')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,43 +81,37 @@ const AboutPage = () => {
|
||||
<section className="py-20 px-4">
|
||||
<div className="container mx-auto max-w-4xl">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center text-gray-900 mb-12">
|
||||
{isEn ? 'Our Story' : 'Наша история'}
|
||||
{t('about.story.title')}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-white p-8 rounded-2xl border-l-4 border-ospab-primary shadow-lg">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-3">
|
||||
<FaRocket className="text-ospab-primary" />
|
||||
{isEn ? 'September 2025 — The Beginning' : 'Сентябрь 2025 — Начало пути'}
|
||||
{t('about.story.sections.start.title')}
|
||||
</h3>
|
||||
<p className="text-lg text-gray-700 leading-relaxed">
|
||||
{isEn
|
||||
? "It all started with a simple idea: create a place where anyone can host their project, website, or server with maximum reliability and speed. Veliky Novgorod deserves its own data center, and I decided to take on this task."
|
||||
: 'Всё началось с простой идеи: создать место, где любой сможет разместить свой проект, сайт или сервер с максимальной надёжностью и скоростью. Великий Новгород заслуживает свой дата-центр, и я решил взяться за эту задачу.'}
|
||||
{t('about.story.sections.start.text')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-purple-50 to-white p-8 rounded-2xl border-l-4 border-ospab-accent shadow-lg">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-3">
|
||||
<FaHeart className="text-ospab-accent" />
|
||||
{isEn ? 'Support and Development' : 'Поддержка и развитие'}
|
||||
{t('about.story.sections.support.title')}
|
||||
</h3>
|
||||
<p className="text-lg text-gray-700 leading-relaxed">
|
||||
{isEn
|
||||
? "My investor friend believed in the project and helps with infrastructure development. We're building not just a business, but a community where every client is like a friend, and support is always nearby."
|
||||
: 'Мой друг-инвестор поверил в проект и помогает с развитием инфраструктуры. Мы строим не просто бизнес, а сообщество, где каждый клиент — как друг, а поддержка всегда рядом.'}
|
||||
{t('about.story.sections.support.text')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-green-50 to-white p-8 rounded-2xl border-l-4 border-green-500 shadow-lg">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-3">
|
||||
<FaChartLine className="text-green-500" />
|
||||
{isEn ? 'Present and Future' : 'Настоящее и будущее'}
|
||||
{t('about.story.sections.future.title')}
|
||||
</h3>
|
||||
<p className="text-lg text-gray-700 leading-relaxed">
|
||||
{isEn
|
||||
? "Currently, we're actively working on hosting and preparing infrastructure for the future data center. ospab.host is the first step towards the digital future of Veliky Novgorod, and we're just getting started."
|
||||
: 'Сейчас мы активно работаем над хостингом и подготовкой инфраструктуры для будущего ЦОД. ospab.host — это первый шаг к цифровому будущему Великого Новгорода, и мы только начинаем.'}
|
||||
{t('about.story.sections.future.text')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,12 +123,10 @@ const AboutPage = () => {
|
||||
<div className="container mx-auto max-w-6xl">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
{isEn ? 'Our Mission' : 'Наша миссия'}
|
||||
{t('about.mission.title')}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
{isEn
|
||||
? "Make quality hosting accessible to everyone, and the data center — the city's pride"
|
||||
: 'Сделать качественный хостинг доступным для всех, а ЦОД — гордостью города'}
|
||||
{t('about.mission.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -146,11 +135,9 @@ const AboutPage = () => {
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mb-6">
|
||||
<FaServer className="text-3xl text-ospab-primary" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Modern Technologies' : 'Современные технологии'}</h3>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{t('about.mission.features.technologies.title')}</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{isEn
|
||||
? 'We use the latest equipment and software for maximum performance'
|
||||
: 'Используем новейшее оборудование и программное обеспечение для максимальной производительности'}
|
||||
{t('about.mission.features.technologies.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -158,11 +145,9 @@ const AboutPage = () => {
|
||||
<div className="w-16 h-16 bg-pink-100 rounded-2xl flex items-center justify-center mb-6">
|
||||
<FaShieldAlt className="text-3xl text-ospab-accent" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Data Security' : 'Безопасность данных'}</h3>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{t('about.mission.features.security.title')}</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{isEn
|
||||
? 'Customer data protection is our priority. Regular backups and 24/7 monitoring'
|
||||
: 'Защита информации клиентов — наш приоритет. Регулярные бэкапы и мониторинг 24/7'}
|
||||
{t('about.mission.features.security.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -170,11 +155,9 @@ const AboutPage = () => {
|
||||
<div className="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mb-6">
|
||||
<FaUsers className="text-3xl text-green-500" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Personal Support' : 'Личная поддержка'}</h3>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{t('about.mission.features.support.title')}</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{isEn
|
||||
? 'Every customer receives personal attention and help from the founder'
|
||||
: 'Каждый клиент получает персональное внимание и помощь от основателя'}
|
||||
{t('about.mission.features.support.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +169,7 @@ const AboutPage = () => {
|
||||
<div className="container mx-auto max-w-6xl">
|
||||
<div className="bg-gradient-to-br from-ospab-primary to-blue-700 rounded-3xl shadow-2xl p-12 md:p-16 text-white">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center mb-12">
|
||||
{isEn ? 'Why choose ospab.host?' : 'Почему выбирают ospab.host?'}
|
||||
{t('about.whyChoose.title')}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||
@@ -195,8 +178,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'First data center in the city' : 'Первый ЦОД в городе'}</h4>
|
||||
<p className="text-blue-100">{isEn ? "We're making Veliky Novgorod history" : 'Мы создаём историю Великого Новгорода'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.first.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.first.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,8 +188,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'Affordable pricing' : 'Доступные тарифы'}</h4>
|
||||
<p className="text-blue-100">{isEn ? 'Quality hosting for everyone without overpaying' : 'Качественный хостинг для всех без переплат'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.pricing.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.pricing.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,8 +198,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'Fast support' : 'Быстрая поддержка'}</h4>
|
||||
<p className="text-blue-100">{isEn ? "We'll answer questions anytime" : 'Ответим на вопросы в любое время'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.fastSupport.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.fastSupport.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,8 +208,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'Transparency' : 'Прозрачность'}</h4>
|
||||
<p className="text-blue-100">{isEn ? 'Honest about capabilities and limitations' : 'Честно о возможностях и ограничениях'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.transparency.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.transparency.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,8 +218,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'Modern infrastructure' : 'Современная инфраструктура'}</h4>
|
||||
<p className="text-blue-100">{isEn ? 'Up-to-date software and equipment' : 'Актуальное ПО и оборудование'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.infrastructure.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.infrastructure.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -245,8 +228,8 @@ const AboutPage = () => {
|
||||
<span className="text-white font-bold">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg mb-2">{isEn ? 'A dream becoming reality' : 'Мечта становится реальностью'}</h4>
|
||||
<p className="text-blue-100">{isEn ? 'A story to be proud of' : 'История, которой можно гордиться'}</p>
|
||||
<h4 className="font-bold text-lg mb-2">{t('about.whyChoose.features.dream.title')}</h4>
|
||||
<p className="text-blue-100">{t('about.whyChoose.features.dream.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -263,7 +246,7 @@ const AboutPage = () => {
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{isEn ? 'Source code on GitHub' : 'Исходный код на GitHub'}
|
||||
{t('about.whyChoose.features.openSource.title')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -277,25 +260,23 @@ const AboutPage = () => {
|
||||
<section className="py-20 px-4 bg-gray-50">
|
||||
<div className="container mx-auto max-w-4xl text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
{isEn ? 'Become part of history' : 'Станьте частью истории'}
|
||||
{t('about.cta.title')}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-10">
|
||||
{isEn
|
||||
? 'Join ospab.host and help create the digital future of Veliky Novgorod'
|
||||
: 'Присоединяйтесь к ospab.host и помогите создать цифровое будущее Великого Новгорода'}
|
||||
{t('about.cta.subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a
|
||||
href={localePath('/register')}
|
||||
className="px-8 py-4 bg-ospab-primary hover:bg-blue-700 text-white font-bold text-lg rounded-full transition-all transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
{isEn ? 'Start for free' : 'Начать бесплатно'}
|
||||
{t('about.cta.startFree')}
|
||||
</a>
|
||||
<a
|
||||
href={localePath('/tariffs')}
|
||||
className="px-8 py-4 bg-white hover:bg-gray-50 text-ospab-primary font-bold text-lg rounded-full border-2 border-ospab-primary transition-all transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
{isEn ? 'View plans' : 'Посмотреть тарифы'}
|
||||
{t('about.cta.viewPlans')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,19 +47,7 @@ const S3PlansPage = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectingPlan, setSelectingPlan] = useState<string | null>(null);
|
||||
|
||||
const BASE_FEATURES = locale === 'en' ? [
|
||||
'S3-compatible API and AWS SDK support',
|
||||
'Deployment in ru-central-1 region',
|
||||
'Versioning and presigned URLs',
|
||||
'Access management via Access Key/Secret Key',
|
||||
'Notifications and monitoring in client panel'
|
||||
] : [
|
||||
'S3-совместимый API и совместимость с AWS SDK',
|
||||
'Развёртывание в регионе ru-central-1',
|
||||
'Версионирование и presigned URL',
|
||||
'Управление доступом через Access Key/Secret Key',
|
||||
'Уведомления и мониторинг в панели клиента'
|
||||
];
|
||||
const BASE_FEATURES = t('tariffs.baseFeatures') as string[];
|
||||
|
||||
const formatMetric = (value: number, suffix: string) =>
|
||||
`${value.toLocaleString(locale === 'en' ? 'en-US' : 'ru-RU')} ${suffix}`;
|
||||
@@ -73,14 +61,14 @@ const S3PlansPage = () => {
|
||||
setError(null);
|
||||
const response = await fetch(`${API_URL}/api/storage/plans`);
|
||||
if (!response.ok) {
|
||||
throw new Error(locale === 'en' ? 'Failed to load plans' : 'Не удалось загрузить тарифы');
|
||||
throw new Error(t('tariffs.error.loadFailed'));
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!cancelled) {
|
||||
setPlans(Array.isArray(data?.plans) ? data.plans : []);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : (locale === 'en' ? 'Error loading plans' : 'Ошибка загрузки тарифов');
|
||||
const message = err instanceof Error ? err.message : t('tariffs.error.loadError');
|
||||
if (!cancelled) {
|
||||
setError(message);
|
||||
}
|
||||
@@ -164,7 +152,7 @@ const S3PlansPage = () => {
|
||||
}
|
||||
navigate(localePath(`/dashboard/checkout?cart=${encodeURIComponent(cartId)}`));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : (locale === 'en' ? 'Failed to start payment' : 'Не удалось начать оплату');
|
||||
const message = err instanceof Error ? err.message : t('tariffs.page.paymentError');
|
||||
setError(message);
|
||||
} finally {
|
||||
setSelectingPlan(null);
|
||||
@@ -180,22 +168,20 @@ const S3PlansPage = () => {
|
||||
<span>S3 Object Storage</span>
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold mb-6 text-gray-900">
|
||||
{locale === 'en' ? 'Transparent pricing for any volume' : 'Прозрачные тарифы для любого объёма'}
|
||||
{t('tariffs.page.title')}
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl text-gray-600 max-w-3xl mx-auto mb-8">
|
||||
{locale === 'en'
|
||||
? 'Pay only for the resources you need. 12 ready-made plans for teams of any size, with included traffic, requests and priority support.'
|
||||
: 'Оплачивайте только за необходимые ресурсы. 12 готовых тарифов для команд любого размера, с включённым трафиком, запросами и приоритетной поддержкой.'}
|
||||
{t('tariffs.page.subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4 text-sm text-gray-500">
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1 bg-white rounded-full shadow-sm">
|
||||
<FaBolt className="text-blue-500" /> NVMe + 10Gb/s {locale === 'en' ? 'network' : 'сеть'}
|
||||
<FaBolt className="text-blue-500" /> NVMe + 10Gb/s {t('tariffs.page.network')}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1 bg-white rounded-full shadow-sm">
|
||||
<FaLock className="text-emerald-500" /> AES-256 at-rest
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1 bg-white rounded-full shadow-sm">
|
||||
<FaInfinity className="text-purple-500" /> {locale === 'en' ? 'S3-compatible API' : 'S3-совместимый API'}
|
||||
<FaInfinity className="text-purple-500" /> {t('tariffs.page.api')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,33 +194,27 @@ const S3PlansPage = () => {
|
||||
<div className="w-14 h-14 bg-blue-100 rounded-full flex items-center justify-center mb-4">
|
||||
<FaBolt className="text-2xl text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{locale === 'en' ? 'Load Ready' : 'Готовность к нагрузке'}</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('tariffs.page.loadReady')}</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{locale === 'en'
|
||||
? 'Unified NVMe platform with auto-scaling, CDN integration and cross-region replication.'
|
||||
: 'Единая платформа на NVMe с автоматическим масштабированием, CDN-интеграцией и кросс-региональной репликацией.'}
|
||||
{t('tariffs.page.loadReadyDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-gray-50 rounded-xl">
|
||||
<div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<FaShieldAlt className="text-2xl text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{locale === 'en' ? 'Security by Default' : 'Безопасность по умолчанию'}</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('tariffs.page.security')}</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{locale === 'en'
|
||||
? '3 data copies, IAM roles, in-transit and at-rest encryption, audit logs, Object Lock and retention policies.'
|
||||
: '3 копии данных, IAM роли, шифрование in-transit и at-rest, audit логи, Object Lock и политики хранения.'}
|
||||
{t('tariffs.page.securityDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-gray-50 rounded-xl">
|
||||
<div className="w-14 h-14 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<FaCloud className="text-2xl text-purple-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{locale === 'en' ? 'AWS SDK Compatibility' : 'Совместимость с AWS SDK'}</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('tariffs.page.compatibility')}</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{locale === 'en'
|
||||
? 'Full S3 API, support for AWS CLI, Terraform, rclone, s3cmd and other tools without code changes.'
|
||||
: 'Полный S3 API, поддержка AWS CLI, Terraform, rclone, s3cmd и других инструментов без изменений в коде.'}
|
||||
{t('tariffs.page.compatibilityDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -325,11 +305,11 @@ const S3PlansPage = () => {
|
||||
{selectingPlan === plan.code ? (
|
||||
<>
|
||||
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
<span>{locale === 'en' ? 'Creating cart...' : 'Создание корзины...'}</span>
|
||||
<span>{t('tariffs.page.creatingCart')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{locale === 'en' ? 'Select plan' : 'Выбрать план'}</span>
|
||||
<span>{t('tariffs.page.selectPlan')}</span>
|
||||
<FaArrowRight />
|
||||
</>
|
||||
)}
|
||||
@@ -345,8 +325,8 @@ const S3PlansPage = () => {
|
||||
{customPlan && customPlanCalculated && (
|
||||
<div className="mt-20 pt-20 border-t border-gray-200">
|
||||
<div className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">{locale === 'en' ? 'Custom Plan' : 'Кастомный тариф'}</h2>
|
||||
<p className="text-gray-600">{locale === 'en' ? 'Specify the required amount of GB and get automatic cost calculation' : 'Укажите нужное количество GB и получите автоматический расчёт стоимости'}</p>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">{t('tariffs.page.customTitle')}</h2>
|
||||
<p className="text-gray-600">{t('tariffs.page.customDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-8 border border-blue-200">
|
||||
@@ -354,7 +334,7 @@ const S3PlansPage = () => {
|
||||
{/* Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-4">
|
||||
{locale === 'en' ? 'How many GB do you need?' : 'Сколько GB вам нужно?'}
|
||||
{t('tariffs.page.gbQuestion')}
|
||||
</label>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<input
|
||||
@@ -466,27 +446,21 @@ const S3PlansPage = () => {
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
<div className="p-6 bg-gray-50 rounded-xl">
|
||||
<h3 className="text-lg font-semibold mb-3">{locale === 'en' ? 'Backups & DR' : 'Бэкапы и DR'}</h3>
|
||||
<h3 className="text-lg font-semibold mb-3">{t('tariffs.page.useCases.backups')}</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{locale === 'en'
|
||||
? 'Replication, Object Lock and object lifecycle allow storing backups and quick recovery.'
|
||||
: 'Репликация, Object Lock и цикл жизни объектов позволяют хранить резервные копии и быстро восстанавливаться.'}
|
||||
{t('tariffs.page.useCases.backupsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-gray-50 rounded-xl">
|
||||
<h3 className="text-lg font-semibold mb-3">{locale === 'en' ? 'Media Platforms' : 'Медиа-платформы'}</h3>
|
||||
<h3 className="text-lg font-semibold mb-3">{t('tariffs.page.useCases.media')}</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{locale === 'en'
|
||||
? 'CDN integration, presigned URLs and high bandwidth for video, images and audio.'
|
||||
: 'CDN-интеграция, presigned URL и высокая пропускная способность для видео, изображений и аудио.'}
|
||||
{t('tariffs.page.useCases.mediaDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-gray-50 rounded-xl">
|
||||
<h3 className="text-lg font-semibold mb-3">SaaS & Data Lake</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{locale === 'en'
|
||||
? 'IAM, API versions and audit logs ensure security and compliance with GDPR, 152-FZ and SOC 2.'
|
||||
: 'IAM, версии API и аудит логов обеспечивают безопасность и соответствие требованиям GDPR, 152-ФЗ и SOC 2.'}
|
||||
{t('tariffs.page.useCases.saasDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,11 +469,9 @@ const S3PlansPage = () => {
|
||||
|
||||
<section className="py-20 px-6 sm:px-8 bg-gray-900 text-white">
|
||||
<div className="container mx-auto max-w-4xl text-center">
|
||||
<h2 className="text-4xl font-bold mb-6">{locale === 'en' ? 'Ready to deploy S3 storage?' : 'Готовы развернуть S3 хранилище?'}</h2>
|
||||
<h2 className="text-4xl font-bold mb-6">{t('tariffs.page.cta.title')}</h2>
|
||||
<p className="text-lg sm:text-xl mb-8 text-white/80">
|
||||
{locale === 'en'
|
||||
? 'Create an account and get access to management console, API keys and detailed usage analytics.'
|
||||
: 'Создайте аккаунт и получите доступ к консоли управления, API ключам и детальной аналитике использования.'}
|
||||
{t('tariffs.page.cta.subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user