new api endpoint and api rate limit

This commit is contained in:
Georgiy Syralev
2026-01-01 16:55:17 +03:00
parent 4690bdf23e
commit bdb333958a
32 changed files with 884 additions and 377 deletions

View File

@@ -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>

View File

@@ -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;
};

View File

@@ -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.',
},
},
},
};

View File

@@ -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 ключам и детальной аналитике использования.',
},
},
},
};

View File

@@ -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]
);

View File

@@ -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>

View File

@@ -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