Добавлена полная документация для лендинга BROJS.RU, включая описание структуры проекта, SSG, пользовательского соглашения и команды для сборки. Реализована автоматическая генерация terms.html из terma.md. Обновлены зависимости и скрипты для сборки. Исправлены ошибки в конфигурации и добавлены новые страницы.
All checks were successful
platform/bro-js/bro.landing/pipeline/head This commit looks good
All checks were successful
platform/bro-js/bro.landing/pipeline/head This commit looks good
This commit is contained in:
parent
e91b861415
commit
9110e79d6b
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
]
|
||||
}
|
||||
424
cloud.md
Normal file
424
cloud.md
Normal file
@ -0,0 +1,424 @@
|
||||
# ☁️ BROJS.RU Landing - Полная документация
|
||||
|
||||
## 📋 Оглавление
|
||||
|
||||
1. [Обзор проекта](#обзор-проекта)
|
||||
2. [Структура проекта](#структура-проекта)
|
||||
3. [Static Site Generation (SSG)](#static-site-generation-ssg)
|
||||
4. [Страница пользовательского соглашения](#страница-пользовательского-соглашения)
|
||||
5. [Команды и скрипты](#команды-и-скрипты)
|
||||
6. [Технологии](#технологии)
|
||||
7. [Deployment](#deployment)
|
||||
|
||||
---
|
||||
|
||||
## Обзор проекта
|
||||
|
||||
**BROJS.RU Landing** - это статический лендинг платформы обучения фронтенд-разработке на базе React + SSG.
|
||||
|
||||
### Особенности:
|
||||
|
||||
✅ **React 18** с TypeScript
|
||||
✅ **Static Site Generation** - HTML вкомпилирован для SEO
|
||||
✅ **Chakra UI** - современный UI framework
|
||||
✅ **i18next** - мультиязычность (ru/en)
|
||||
✅ **React Router** - клиентский роутинг
|
||||
✅ **Hydration** - гидратация статического контента
|
||||
|
||||
### Страницы:
|
||||
|
||||
- **`/`** - главная страница "в разработке"
|
||||
- **`/terms`** - пользовательское соглашение (полное, SEO-оптимизировано)
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
bro.landing/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── under-construction/ # Главная страница
|
||||
│ │ ├── terms/ # Пользовательское соглашение
|
||||
│ │ └── index.ts # Экспорт страниц
|
||||
│ ├── app.tsx # Root компонент
|
||||
│ ├── dashboard.tsx # Роутинг
|
||||
│ ├── index.tsx # Entry point + hydration
|
||||
│ └── index.ejs # HTML шаблон
|
||||
├── scripts/
|
||||
│ ├── prerender-multi.js # ⭐ Основной SSG скрипт
|
||||
│ └── ssr-prerender.js # Альтернативный SSR
|
||||
├── dist/ # Сборка (генерируется)
|
||||
│ ├── index.html # Главная (SSG)
|
||||
│ ├── terms.html # Соглашение (SSG)
|
||||
│ └── index.js # React bundle
|
||||
├── package.json
|
||||
└── cloud.md # 📚 Эта документация
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Static Site Generation (SSG)
|
||||
|
||||
### Как работает?
|
||||
|
||||
1. **Webpack** собирает React → `dist/index.js`
|
||||
2. **Скрипт `prerender-multi.js`** автоматически:
|
||||
- Читает `dist/index.html` (пустой шаблон)
|
||||
- Вставляет статический контент в `<div id="app"></div>`
|
||||
- Генерирует `dist/terms.html` из `terma.md`
|
||||
3. **React hydration** при загрузке оживляет статический HTML
|
||||
|
||||
### Преимущества SSG:
|
||||
|
||||
- 🚀 **Быстрая загрузка** - контент виден до загрузки JS
|
||||
- 🔍 **SEO** - поисковики индексируют полный HTML
|
||||
- ♿ **Доступность** - работает без JavaScript
|
||||
- 📊 **Метрики** - улучшенные FCP и LCP
|
||||
|
||||
### Hydration в index.tsx:
|
||||
|
||||
```typescript
|
||||
const hasPrerenderedContent = MOUNT_NODE.hasChildNodes();
|
||||
|
||||
if (hasPrerenderedContent) {
|
||||
hydrateRoot(MOUNT_NODE, <App />); // Оживляем статику
|
||||
} else {
|
||||
createRoot(MOUNT_NODE).render(<App />); // Обычный рендер
|
||||
}
|
||||
```
|
||||
|
||||
### Скрипт prerender-multi.js:
|
||||
|
||||
**Что делает**:
|
||||
1. Читает `terma.md` (исходник соглашения)
|
||||
2. Парсит markdown → HTML со стилями
|
||||
3. Создает `dist/index.html` с контентом главной
|
||||
4. Создает `dist/terms.html` с полным соглашением (~13KB)
|
||||
|
||||
**Автоматический запуск**:
|
||||
- `npm run build:prod` - после webpack сборки
|
||||
- `npm run build:prod:ssr` - альтернативный SSR
|
||||
|
||||
---
|
||||
|
||||
## Страница пользовательского соглашения
|
||||
|
||||
### Источник: `terma.md`
|
||||
|
||||
⚠️ **Важно**: `terma.md` - единственный источник правды для соглашения!
|
||||
|
||||
Чтобы обновить соглашение:
|
||||
1. Отредактируйте `terma.md`
|
||||
2. Запустите `npm run build:prod`
|
||||
3. `dist/terms.html` обновится автоматически
|
||||
|
||||
### Структура terma.md:
|
||||
|
||||
```markdown
|
||||
Пользовательское соглашение для BROJS.RU
|
||||
Последнее обновление: 25 мая 2025 г.
|
||||
|
||||
1. Термины
|
||||
...
|
||||
|
||||
10. Контакты
|
||||
Для обращений: primakov.pro@yandex.ru
|
||||
```
|
||||
|
||||
### React компонент: Terms.tsx
|
||||
|
||||
- **Путь**: `src/pages/terms/Terms.tsx`
|
||||
- **Дизайн**: Chakra UI, официальный стиль документа
|
||||
- **SEO**: Helmet с meta-тегами
|
||||
- **Роут**: `/terms` в dashboard.tsx
|
||||
|
||||
### SEO-версия: terms.html
|
||||
|
||||
Генерируется автоматически из `terma.md`:
|
||||
|
||||
```html
|
||||
<title>Пользовательское соглашение - BROJS.RU</title>
|
||||
<meta name="description" content="Полное пользовательское соглашение..." />
|
||||
|
||||
<div id="app">
|
||||
<div style="...красивые стили...">
|
||||
<h1>Пользовательское соглашение для BROJS.RU</h1>
|
||||
<h2>1. Термины</h2>
|
||||
<ul>
|
||||
<li>Платформа — сайт https://brojs.ru ...</li>
|
||||
...
|
||||
</ul>
|
||||
<!-- Все 10 разделов -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Размер**: ~13.6 KB (полный текст + стили)
|
||||
**Содержание**: Все 10 разделов с подразделами
|
||||
**Ссылки**: Рабочие <a> теги на email и сайты
|
||||
|
||||
### Важные детали:
|
||||
|
||||
✅ **Название сайта**: BROJS.RU (не BRO-JS.RU)
|
||||
✅ **Авторизация**: Yandex + Email (Google и VK исключены)
|
||||
✅ **Email**: primakov.pro@yandex.ru
|
||||
|
||||
---
|
||||
|
||||
## Команды и скрипты
|
||||
|
||||
### NPM Scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"start": "brojs server --port=8099 --with-open-browser",
|
||||
"build": "npm run clean && brojs build --dev",
|
||||
"build:prod": "npm run clean && brojs build && node scripts/prerender-multi.js",
|
||||
"build:prod:ssr": "npm run clean && brojs build && node scripts/ssr-prerender.js",
|
||||
"clean": "rimraf dist",
|
||||
"eslint": "npx eslint src",
|
||||
"prettier": "prettier --write .",
|
||||
"test": "jest --coverage"
|
||||
}
|
||||
```
|
||||
|
||||
### Использование:
|
||||
|
||||
#### 🔧 Разработка:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
# → Запускает dev сервер на http://localhost:8099/
|
||||
# → Hot reload включен
|
||||
# → Без SSG (быстро)
|
||||
```
|
||||
|
||||
#### 🏗️ Dev сборка:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# → Webpack в dev режиме
|
||||
# → Без минификации
|
||||
# → Без SSG
|
||||
# → Результат: dist/index.html (пустой шаблон)
|
||||
```
|
||||
|
||||
#### 🚀 Production сборка (с SSG):
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
# → Webpack в production режиме
|
||||
# → Минификация
|
||||
# → Автоматический SSG (prerender-multi.js)
|
||||
# → Результат:
|
||||
# - dist/index.html (со статическим контентом)
|
||||
# - dist/terms.html (полное соглашение для SEO)
|
||||
```
|
||||
|
||||
Вывод:
|
||||
```
|
||||
✅ Сборка успешно завершена!
|
||||
🚀 Начинаем мульти-страничный пре-рендеринг...
|
||||
✅ index.html обновлен
|
||||
📝 Генерируем полный HTML из terma.md...
|
||||
✅ terms.html создан с полным контентом
|
||||
🎉 Пре-рендеринг завершен успешно!
|
||||
```
|
||||
|
||||
#### 🔄 Production сборка (альтернативный SSR):
|
||||
|
||||
```bash
|
||||
npm run build:prod:ssr
|
||||
# → Использует ssr-prerender.js
|
||||
# → SSR с jsdom окружением
|
||||
# → Результат аналогичен build:prod
|
||||
```
|
||||
|
||||
### Другие команды:
|
||||
|
||||
```bash
|
||||
npm run clean # Удалить dist/
|
||||
npm run eslint # Проверить код
|
||||
npm run prettier # Форматировать код
|
||||
npm run test # Запустить тесты
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Технологии
|
||||
|
||||
### Frontend:
|
||||
|
||||
- **React 18.3** - UI библиотека
|
||||
- **TypeScript** - типизация
|
||||
- **Chakra UI 2.8** - компоненты и стили
|
||||
- **Emotion** - CSS-in-JS
|
||||
- **React Router 6** - роутинг
|
||||
- **React Helmet** - управление <head>
|
||||
|
||||
### State & i18n:
|
||||
|
||||
- **Redux Toolkit** - state management
|
||||
- **i18next** - интернационализация
|
||||
- **i18next-browser-languagedetector** - автоопределение языка
|
||||
|
||||
### Build & Dev:
|
||||
|
||||
- **@brojs/cli 1.9.5** - сборщик (обертка над webpack)
|
||||
- **Webpack 5** - бандлер
|
||||
- **Babel** - транспиляция
|
||||
- **jsdom** - SSR окружение
|
||||
|
||||
### Дополнительно:
|
||||
|
||||
- **Lottie React** - анимации
|
||||
- **Day.js** - работа с датами
|
||||
- **ESLint** - линтинг
|
||||
- **Prettier** - форматирование
|
||||
- **Jest** - тестирование
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker + Nginx:
|
||||
|
||||
Проект разворачивается через Docker контейнер с Nginx.
|
||||
|
||||
**Скрипты**:
|
||||
- `d-scripts/up-nginx.sh` - запуск контейнера
|
||||
- `d-scripts/stop.sh` - остановка
|
||||
- `d-scripts/re-run.sh` - перезапуск
|
||||
|
||||
**Процесс**:
|
||||
|
||||
```bash
|
||||
# 1. Сборка
|
||||
npm run build:prod
|
||||
|
||||
# 2. Deploy
|
||||
npm run redeploy
|
||||
# → Устанавливает зависимости
|
||||
# → Собирает проект
|
||||
# → Перезапускает Docker контейнер
|
||||
```
|
||||
|
||||
### Конфигурация Nginx:
|
||||
|
||||
Nginx раздает папку `dist/`:
|
||||
|
||||
```
|
||||
https://brojs.ru/ → dist/index.html
|
||||
https://brojs.ru/terms → React Router → terms.html (fallback)
|
||||
https://brojs.ru/terms.html → dist/terms.html (прямой доступ)
|
||||
https://brojs.ru/index.js → dist/index.js
|
||||
```
|
||||
|
||||
### Важно для SEO:
|
||||
|
||||
1. **index.html** содержит статический контент (SSG)
|
||||
2. **terms.html** содержит полное соглашение (SSG)
|
||||
3. **React Router** работает на клиенте для SPA навигации
|
||||
4. **Fallback** на статические .html файлы для ботов
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Чеклист при обновлениях
|
||||
|
||||
### Обновление пользовательского соглашения:
|
||||
|
||||
- [ ] Отредактировать `terma.md`
|
||||
- [ ] Проверить что нет Google/VK (только Yandex + Email)
|
||||
- [ ] Проверить что название сайта: **BROJS.RU**
|
||||
- [ ] Запустить `npm run build:prod`
|
||||
- [ ] Проверить `dist/terms.html` (должен быть ~13KB)
|
||||
- [ ] Задеплоить: `npm run redeploy`
|
||||
|
||||
### Добавление новой страницы:
|
||||
|
||||
- [ ] Создать компонент в `src/pages/`
|
||||
- [ ] Экспортировать в `src/pages/index.ts`
|
||||
- [ ] Добавить роут в `src/dashboard.tsx`
|
||||
- [ ] Если нужен SSG - обновить `scripts/prerender-multi.js`
|
||||
- [ ] Собрать и проверить
|
||||
|
||||
### Перед деплоем:
|
||||
|
||||
- [ ] `npm run eslint` - проверить код
|
||||
- [ ] `npm run prettier` - форматировать
|
||||
- [ ] `npm run test` - запустить тесты
|
||||
- [ ] `npm run build:prod` - собрать
|
||||
- [ ] Проверить `dist/index.html` и `dist/terms.html`
|
||||
- [ ] `npm run redeploy` - задеплоить
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Проблема: terms.html пустой или куцый
|
||||
|
||||
**Решение**: Проверьте `terma.md` - это единственный источник контента.
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
# Проверьте вывод:
|
||||
# ✅ terms.html создан с полным контентом
|
||||
```
|
||||
|
||||
### Проблема: Hydration warning
|
||||
|
||||
**Причина**: Несоответствие серверного и клиентского HTML.
|
||||
|
||||
**Решение**: Проверьте что контент в `prerender-multi.js` совпадает с React компонентом.
|
||||
|
||||
### Проблема: 404 на /terms
|
||||
|
||||
**Nginx**: Убедитесь что настроен fallback на index.html:
|
||||
|
||||
```nginx
|
||||
try_files $uri $uri/ /index.html;
|
||||
```
|
||||
|
||||
### Проблема: SSG не запускается
|
||||
|
||||
**Проверьте**:
|
||||
1. `scripts/prerender-multi.js` существует
|
||||
2. В `package.json` правильная команда: `build:prod`
|
||||
3. Нет ошибок в webpack сборке
|
||||
|
||||
---
|
||||
|
||||
## 📚 Полезные ссылки
|
||||
|
||||
- **Сайт**: https://brojs.ru
|
||||
- **Email**: primakov.pro@yandex.ru
|
||||
- **Keycloak**: PostgreSQL (auth)
|
||||
- **Gravatar**: https://gravatar.com
|
||||
|
||||
---
|
||||
|
||||
## 📝 История изменений
|
||||
|
||||
### v2.0.2 (2025-10-24)
|
||||
|
||||
- ✨ Добавлена страница `/terms` с пользовательским соглашением
|
||||
- 🎨 Официальный дизайн юридического документа
|
||||
- 📄 Автоматическая генерация `terms.html` из `terma.md`
|
||||
- 🔍 SEO-оптимизация (13.6KB полного контента)
|
||||
- 🔄 Название сайта: BRO-JS.RU → **BROJS.RU**
|
||||
- 🔐 Авторизация: только Yandex + Email
|
||||
- 🧹 Очистка скриптов (удален postbuild, лишние файлы)
|
||||
|
||||
### v2.0.1 (2025-10-24)
|
||||
|
||||
- 🚀 Static Site Generation (SSG)
|
||||
- 💡 React 18 hydration
|
||||
- 📚 Документация по SSG
|
||||
|
||||
---
|
||||
|
||||
**Вопросы?** → primakov.pro@yandex.ru
|
||||
|
||||
**Документация актуальна**: 2025-10-24
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable no-undef */
|
||||
const pkg = require('./package');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
@ -25,7 +27,6 @@ module.exports = {
|
||||
}),
|
||||
],
|
||||
},
|
||||
/* use https://kc.admin.inno-js.ru/ to create config, navigations and features */
|
||||
navigations: {},
|
||||
features: {},
|
||||
config: {},
|
||||
|
||||
5371
package-lock.json
generated
5371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,14 +13,15 @@
|
||||
"test": "jest --coverage",
|
||||
"start": "brojs server --port=8099 --with-open-browser",
|
||||
"build": "npm run clean && brojs build --dev",
|
||||
"build:prod": "npm run clean && brojs build"
|
||||
"build:prod": "npm run clean && brojs build && node scripts/prerender-multi.js",
|
||||
"build:prod:ssr": "npm run clean && brojs build && node scripts/ssr-prerender.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@brojs/cli": "1.2.0",
|
||||
"@brojs/cli": "1.9.5",
|
||||
"@chakra-ui/icons": "^2.1.1",
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/css": "^11.13.0",
|
||||
@ -52,6 +53,8 @@
|
||||
"globals": "^15.9.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"puppeteer": "^24.26.1",
|
||||
"ts-jest": "^29.2.3",
|
||||
"typescript-eslint": "^8.1.0"
|
||||
}
|
||||
|
||||
48
readme.md
48
readme.md
@ -1,31 +1,45 @@
|
||||
# PL админки ijl
|
||||
# BROJS.RU Landing Page
|
||||
|
||||
Данный проект является презентационным слоем админки от стендов ijl
|
||||
Лендинг платформы обучения фронтенд-разработке.
|
||||
|
||||
## Установка зависимостей
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
```shell
|
||||
```bash
|
||||
# Установка
|
||||
npm install
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
```shell
|
||||
# Разработка
|
||||
npm start
|
||||
```
|
||||
# → http://localhost:8099/
|
||||
|
||||
## Собрать
|
||||
|
||||
```shell
|
||||
# Сборка
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
## Деплой
|
||||
## 📄 Страницы
|
||||
|
||||
Для деплоя используется простой докер образ nginx раздающий директорию dist. Для запуска необходимо воспользоваться скриптами из директории d-scripts.
|
||||
- `/` - главная (в разработке)
|
||||
- `/terms` - пользовательское соглашение
|
||||
|
||||
Команда запуска на сервере
|
||||
## 🔧 Команды
|
||||
|
||||
| Команда | Описание |
|
||||
|---------|----------|
|
||||
| `npm start` | Dev сервер |
|
||||
| `npm run build` | Dev сборка |
|
||||
| `npm run build:prod` | Production + SSG |
|
||||
| `npm run build:prod:ssr` | Production + SSR |
|
||||
|
||||
## 📦 Результат сборки
|
||||
|
||||
```shell
|
||||
sh d-scripts/up-nginx.sh
|
||||
```
|
||||
dist/
|
||||
├── index.html # Главная страница (SSG)
|
||||
├── terms.html # Пользовательское соглашение (SEO)
|
||||
├── index.js # React bundle
|
||||
└── locales/ # i18n файлы
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
📚 Полная документация: **cloud.md**
|
||||
|
||||
169
scripts/prerender-multi.js
Normal file
169
scripts/prerender-multi.js
Normal file
@ -0,0 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable no-undef */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Контент для главной страницы
|
||||
const homeContent = `
|
||||
<div>
|
||||
<div style="max-height: 250px;">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
<h3><center>Сайт в разработке</center></h3>
|
||||
</div>
|
||||
`.trim();
|
||||
|
||||
// Функция для генерации полного HTML из terma.md
|
||||
const generateTermsContent = () => {
|
||||
const termaPath = path.resolve(__dirname, '../terma.md');
|
||||
const termaText = fs.readFileSync(termaPath, 'utf-8');
|
||||
|
||||
// Парсим markdown в HTML с сохранением структуры
|
||||
let html = '<div style="background: #f7fafc; min-height: 100vh; padding: 32px 0;">';
|
||||
html += '<div style="max-width: 1200px; margin: 0 auto; background: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); border-radius: 8px; padding: 60px 80px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;">';
|
||||
|
||||
const lines = termaText.split('\n');
|
||||
let inList = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (!line) {
|
||||
if (inList) {
|
||||
html += '</ul>';
|
||||
inList = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Заголовок H1
|
||||
if (i === 0) {
|
||||
html += '<div style="text-align: center; margin-bottom: 40px;">';
|
||||
html += `<h1 style="font-size: 2.5em; color: #2563eb; margin-bottom: 8px; font-weight: 700;">${line}</h1>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Дата обновления
|
||||
if (line.includes('Последнее обновление')) {
|
||||
html += `<p style="color: #6b7280; font-size: 0.875em;">${line}</p>`;
|
||||
html += '</div><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 24px 0;">';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Основные разделы (начинаются с цифры и точки без подразделов)
|
||||
if (/^\d+\.\s+[А-Яа-я]/.test(line)) {
|
||||
if (inList) {
|
||||
html += '</ul>';
|
||||
inList = false;
|
||||
}
|
||||
const text = line.replace(/^\d+\.\s+/, '');
|
||||
html += `<h2 style="font-size: 1.875em; color: #1e40af; margin-top: 40px; margin-bottom: 16px; font-weight: 600;">${line}</h2>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Подразделы (например, 2.1., 3.2.)
|
||||
if (/^\d+\.\d+\.\s+/.test(line)) {
|
||||
if (inList) {
|
||||
html += '</ul>';
|
||||
inList = false;
|
||||
}
|
||||
html += `<h3 style="font-size: 1.25em; color: #374151; margin-top: 24px; margin-bottom: 12px; font-weight: 600;">${line}</h3>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Списки (начинаются с заглавной буквы или содержат "—")
|
||||
if (line.includes('—') || (i > 0 && lines[i-1].includes('через:')) || (i > 0 && lines[i-1].includes('обязуется:')) || (i > 0 && lines[i-1].includes('собирает:')) || (i > 0 && lines[i-1].includes('ответственности за:'))) {
|
||||
if (!inList) {
|
||||
html += '<ul style="list-style-type: disc; padding-left: 32px; margin: 12px 0;">';
|
||||
inList = true;
|
||||
}
|
||||
|
||||
// Обработка ссылок
|
||||
let processedLine = line
|
||||
.replace(/https?:\/\/[^\s,)]+/g, (url) => {
|
||||
const cleanUrl = url.replace(/\s*,?\s*$/, '');
|
||||
return `<a href="${cleanUrl}" style="color: #3b82f6; text-decoration: underline;">${cleanUrl}</a>`;
|
||||
})
|
||||
.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
|
||||
'<a href="mailto:$1" style="color: #3b82f6; text-decoration: underline;">$1</a>');
|
||||
|
||||
html += `<li style="margin: 8px 0; line-height: 1.75;">${processedLine}</li>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обычные параграфы
|
||||
if (inList && !line.includes('—')) {
|
||||
html += '</ul>';
|
||||
inList = false;
|
||||
}
|
||||
|
||||
// Обработка жирного текста и ссылок
|
||||
let processedLine = line
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/https?:\/\/[^\s,)]+/g, (url) => {
|
||||
const cleanUrl = url.replace(/\s*,?\s*$/, '');
|
||||
return `<a href="${cleanUrl}" style="color: #3b82f6; text-decoration: underline;">${cleanUrl}</a>`;
|
||||
})
|
||||
.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
|
||||
'<a href="mailto:$1" style="color: #3b82f6; text-decoration: underline;">$1</a>');
|
||||
|
||||
html += `<p style="margin: 12px 0; line-height: 1.75; color: #374151;">${processedLine}</p>`;
|
||||
}
|
||||
|
||||
if (inList) {
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
// Footer
|
||||
html += '<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 48px 0 24px 0;">';
|
||||
html += '<p style="text-align: center; color: #6b7280; font-size: 0.875em;">© 2025 BROJS.RU. Все права защищены.</p>';
|
||||
html += '</div></div>';
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
const prerender = () => {
|
||||
try {
|
||||
console.log('🚀 Начинаем мульти-страничный пре-рендеринг...');
|
||||
|
||||
const distPath = path.resolve(__dirname, '../dist');
|
||||
const indexPath = path.join(distPath, 'index.html');
|
||||
|
||||
// Читаем основной HTML
|
||||
let indexHtml = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
// 1. Обрабатываем главную страницу
|
||||
const searchString = '<div id="app"></div>';
|
||||
if (indexHtml.includes(searchString)) {
|
||||
indexHtml = indexHtml.replace(searchString, `<div id="app">${homeContent}</div>`);
|
||||
fs.writeFileSync(indexPath, indexHtml, 'utf-8');
|
||||
console.log('✅ index.html обновлен');
|
||||
}
|
||||
|
||||
// 2. Генерируем полный контент для terms.html из terma.md
|
||||
console.log('📝 Генерируем полный HTML из terma.md...');
|
||||
const termsContent = generateTermsContent();
|
||||
|
||||
// 3. Создаем terms.html на основе index.html
|
||||
let termsHtml = indexHtml
|
||||
.replace(homeContent, termsContent)
|
||||
.replace('<title>bro-js admin</title>', '<title>Пользовательское соглашение - BROJS.RU</title>')
|
||||
.replace(
|
||||
'</head>',
|
||||
'<meta name="description" content="Полное пользовательское соглашение для платформы обучения фронтенд-разработке BROJS.RU. Условия использования, обработка персональных данных, права и обязанности сторон." /></head>'
|
||||
);
|
||||
|
||||
const termsPath = path.join(distPath, 'terms.html');
|
||||
fs.writeFileSync(termsPath, termsHtml, 'utf-8');
|
||||
console.log('✅ terms.html создан с полным контентом');
|
||||
|
||||
console.log('🎉 Пре-рендеринг завершен успешно!');
|
||||
console.log('📄 Созданы файлы: index.html, terms.html');
|
||||
console.log('💡 terms.html содержит полный текст соглашения для SEO');
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при пре-рендеринге:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
prerender();
|
||||
79
scripts/ssr-prerender.js
Normal file
79
scripts/ssr-prerender.js
Normal file
@ -0,0 +1,79 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
// Настройка окружения для SSR
|
||||
const setupDOM = () => {
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||
url: 'http://localhost',
|
||||
pretendToBeVisual: true,
|
||||
resources: 'usable'
|
||||
});
|
||||
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.navigator = dom.window.navigator;
|
||||
global.HTMLElement = dom.window.HTMLElement;
|
||||
global.HTMLDivElement = dom.window.HTMLDivElement;
|
||||
global.requestAnimationFrame = (callback) => setTimeout(callback, 0);
|
||||
global.cancelAnimationFrame = clearTimeout;
|
||||
};
|
||||
|
||||
const cleanupRender = () => {
|
||||
delete global.window;
|
||||
delete global.document;
|
||||
delete global.navigator;
|
||||
delete global.HTMLElement;
|
||||
delete global.HTMLDivElement;
|
||||
delete global.requestAnimationFrame;
|
||||
delete global.cancelAnimationFrame;
|
||||
};
|
||||
|
||||
const prerender = async () => {
|
||||
try {
|
||||
console.log('🚀 Начинаем SSR пре-рендеринг...');
|
||||
|
||||
setupDOM();
|
||||
|
||||
// Читаем HTML шаблон
|
||||
const indexPath = path.resolve(__dirname, '../dist/index.html');
|
||||
let html = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
// Рендерим статический контент страницы "в разработке"
|
||||
const prerenderContent = `
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="max-height: 250px; margin: 0 auto;">
|
||||
<div>⚙️</div>
|
||||
</div>
|
||||
<h3><center>Сайт в разработке</center></h3>
|
||||
<p style="color: #666;">Страница загружается...</p>
|
||||
</div>
|
||||
`.trim();
|
||||
|
||||
// Вставляем пре-рендеренный контент в div#app
|
||||
const searchString = '<div id="app"></div>';
|
||||
if (html.includes(searchString)) {
|
||||
html = html.replace(searchString, `<div id="app">${prerenderContent}</div>`);
|
||||
|
||||
// Сохраняем результат
|
||||
fs.writeFileSync(indexPath, html, 'utf-8');
|
||||
console.log('✅ SSR пре-рендеринг завершен успешно!');
|
||||
console.log('📄 HTML обновлен с серверным контентом');
|
||||
} else {
|
||||
console.log('⚠️ Не найден <div id="app"></div>');
|
||||
console.log('Возможно, HTML уже содержит пре-рендеренный контент');
|
||||
}
|
||||
|
||||
cleanupRender();
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при SSR пре-рендеринге:', error);
|
||||
cleanupRender();
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
prerender();
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Spinner } from '@chakra-ui/react';
|
||||
|
||||
import { UnderConstructionPage } from './pages';
|
||||
import { UnderConstructionPage, TermsPage } from './pages';
|
||||
|
||||
export const Dashboard = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={'*'}
|
||||
element={
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<UnderConstructionPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path={'*'} element={<h1>Страница не найдена</h1>} />
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
<Route path="/" element={<UnderConstructionPage />} />
|
||||
<Route path="*" element={<h1>Страница не найдена</h1>} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import i18next from 'i18next';
|
||||
import { i18nextReactInitConfig } from '@brojs/cli/lib/i18next';
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { i18nextReactInitConfig } from '@brojs/cli';
|
||||
import { createRoot, hydrateRoot } from 'react-dom/client'
|
||||
|
||||
import App from './app';
|
||||
|
||||
@ -11,14 +11,22 @@ const MOUNT_NODE = document.getElementById('app');
|
||||
|
||||
(async () => {
|
||||
await Promise.all([i18nextPromise]);
|
||||
const rootElement = createRoot(MOUNT_NODE)
|
||||
rootElement.render(<App />);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', async () => {
|
||||
await i18next.reloadResources();
|
||||
rootElement.render(<App />);
|
||||
});
|
||||
|
||||
// Если страница была пре-рендерена, используем hydrate вместо render
|
||||
const hasPrerenderedContent = MOUNT_NODE.hasChildNodes();
|
||||
|
||||
if (hasPrerenderedContent) {
|
||||
hydrateRoot(MOUNT_NODE, <App />);
|
||||
} else {
|
||||
const rootElement = createRoot(MOUNT_NODE);
|
||||
rootElement.render(<App />);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', async () => {
|
||||
await i18next.reloadResources();
|
||||
rootElement.render(<App />);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const UnderConstructionPage = lazy(() => import('./under-construction'));
|
||||
export { default as UnderConstructionPage } from './under-construction';
|
||||
export { Terms as TermsPage } from './terms';
|
||||
|
||||
291
src/pages/terms/Terms.tsx
Normal file
291
src/pages/terms/Terms.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
import React from 'react';
|
||||
import { Box, Container, Heading, Text, VStack, Divider, Link } from '@chakra-ui/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export const Terms = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Пользовательское соглашение - BROJS.RU</title>
|
||||
<meta name="description" content="Пользовательское соглашение для платформы обучения фронтенд-разработке BROJS.RU" />
|
||||
</Helmet>
|
||||
|
||||
<Box bg="gray.50" minH="100vh" py={8}>
|
||||
<Container maxW="4xl" bg="white" shadow="lg" borderRadius="md" p={{ base: 6, md: 10 }}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* Заголовок */}
|
||||
<Box textAlign="center" mb={4}>
|
||||
<Heading as="h1" size="2xl" mb={2} color="blue.600">
|
||||
Пользовательское соглашение
|
||||
</Heading>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
для BROJS.RU
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.500" mt={2}>
|
||||
Последнее обновление: 25 мая 2025 г.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 1. Термины */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
1. Термины
|
||||
</Heading>
|
||||
<VStack spacing={2} align="stretch">
|
||||
<Text><strong>Платформа</strong> — сайт <Link href="https://brojs.ru" color="blue.500" isExternal>https://brojs.ru</Link>, предоставляющий услуги обучения фронтенд-разработке.</Text>
|
||||
<Text><strong>Пользователь</strong> — лицо, зарегистрированное на Платформе.</Text>
|
||||
<Text><strong>Микрофронтенд-проект</strong> — код, конфигурации и иные материалы, созданные Пользователем.</Text>
|
||||
<Text><strong>Gravatar</strong> — сторонний сервис (<Link href="https://gravatar.com" color="blue.500" isExternal>https://gravatar.com</Link>), предоставляющий аватары на основе email-адресов пользователей.</Text>
|
||||
<Text><strong>Интеллектуальная собственность</strong> — результаты интеллектуальной деятельности, включая, но не ограничиваясь, программные коды, дизайны, тексты, графику и другие объекты, защищенные законом.</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 2. Условия использования */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
2. Условия использования
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
2.1. Регистрация
|
||||
</Heading>
|
||||
<Text mb={2}>Регистрация осуществляется через:</Text>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Аккаунт Yandex;</li>
|
||||
<li>Email (с подтверждением через ссылку).</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
2.2. Обязанности Пользователя
|
||||
</Heading>
|
||||
<Text mb={2}>Пользователь обязуется:</Text>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Не передавать учетные данные третьим лицам;</li>
|
||||
<li>Не использовать Платформу для распространения незаконного контента или совершения мошеннических действий;</li>
|
||||
<li>Соблюдать конфиденциальность личных данных других участников Платформы.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 3. Персональные данные */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
3. Персональные данные
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.1. Собираемые данные
|
||||
</Heading>
|
||||
<Text mb={2}>Платформа собирает:</Text>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Никнейм;</li>
|
||||
<li>Email;</li>
|
||||
<li>ФИО (при наличии договора с учебным заведением);</li>
|
||||
<li>Данные о посещении занятий (через QR-код).</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.2. Аватар через Gravatar
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Платформа не хранит аватары на своих серверах. Для отображения используется Gravatar.</li>
|
||||
<li>Ссылка на аватар формируется на основе хэша email пользователя.</li>
|
||||
<li>Пользователь может активировать/отозвать согласие на использование Gravatar в настройках профиля.</li>
|
||||
<li>Отказ от Gravatar приведет к отображению стандартного изображения.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.3. Цели обработки данных
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Предоставление доступа к Платформе;</li>
|
||||
<li>Передача данных о посещении учебным заведениям (ФИО, email, дата и время) в формате Excel.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.4. Хранение и передача данных
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Персональные данные хранятся в СУБД PostgreSQL через Keycloak.</li>
|
||||
<li>Данные о посещении передаются учебным заведениям на основании договоров с преподавателями.</li>
|
||||
<li>Передача данных осуществляется с применением шифрования и протоколов безопасности.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.5. Срок хранения
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Персональные данные удаляются в течение 30 дней после удаления аккаунта.</li>
|
||||
<li>Микрофронтенд-проекты хранятся 6 месяцев после завершения обучения.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
3.6. Отзыв согласия
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Для отзыва согласия на обработку персональных данных необходимо направить письмо на <Link href="mailto:primakov.pro@yandex.ru" color="blue.500">primakov.pro@yandex.ru</Link>.</li>
|
||||
<li>Отзыв приведет к удалению всех данных пользователя вручную.</li>
|
||||
<li>Частичное удаление отдельных категорий данных возможно по заявлению пользователя.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 4. Интеллектуальная собственность */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
4. Интеллектуальная собственность
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
4.1. Права Пользователя
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Пользователь сохраняет авторские права на созданные проекты.</li>
|
||||
<li>Платформа не имеет прав на использование материалов Пользователя без явного согласия.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
4.2. Права Администрации
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Администрация вправе удалить контент при нарушении условий соглашения или через 6 месяцев после завершения обучения.</li>
|
||||
<li>Проверка подлинности загружаемого материала осуществляется преподавателем, отвечающим за группу.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 5. Ответственность */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
5. Ответственность
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
5.1. Ограничение ответственности
|
||||
</Heading>
|
||||
<Text mb={2}>Администрация не несет ответственности за:</Text>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Утрату данных из-за действий Пользователя;</li>
|
||||
<li>Использование данных учебными заведениями после их передачи;</li>
|
||||
<li>Некорректное отображение аватаров через Gravatar.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
5.2. Основания для блокировки аккаунта
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Нарушение авторских прав;</li>
|
||||
<li>Распространение спама/вирусов;</li>
|
||||
<li>Предоставление недостоверных данных (включая ФИО).</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
5.3. Компенсация ущерба
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>В случае нарушения правил или утечки данных, Администрация обязана принять меры для минимизации последствий.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 6. Уведомления */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
6. Уведомления
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
6.1. Информационные сообщения
|
||||
</Heading>
|
||||
<Text mb={2}>Платформа вправе отправлять Пользователю:</Text>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Уведомления о технических работах, изменениях функционала;</li>
|
||||
<li>Сообщения о нарушениях или блокировке аккаунта;</li>
|
||||
<li>Рекламу собственных услуг или услуг третьих лиц.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
6.2. Отказ от уведомлений
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Отказ от рекламных сообщений возможен через настройки Личного кабинета.</li>
|
||||
<li>Отказ от информационных уведомлений может ограничить доступ к функциям Платформы.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 7. Безопасность данных */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
7. Безопасность данных
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
7.1. Технические меры
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Данные хранятся в СУБД PostgreSQL через Keycloak.</li>
|
||||
<li>Шифрование данных при передаче (HTTPS).</li>
|
||||
<li>Периодические тестирования системы на уязвимости.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
7.2. Двухфакторная аутентификация
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Пользователи могут добровольно активировать двухфакторную аутентификацию (OTP) через Личный кабинет.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 8. Применимое право и разрешение споров */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
8. Применимое право и разрешение споров
|
||||
</Heading>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
8.1. Применимое право
|
||||
</Heading>
|
||||
<Box as="ul" pl={6} mb={3}>
|
||||
<li>Соглашение регулируется законодательством РФ. Для пользователей из стран СНГ применяются нормы ЕАЭС.</li>
|
||||
<li>При расширении географии услуг Платформа будет соблюдать законодательство стран ЕСВР и ЕС.</li>
|
||||
</Box>
|
||||
|
||||
<Heading as="h3" size="md" mb={2} color="gray.700">
|
||||
8.2. Разрешение споров
|
||||
</Heading>
|
||||
<Box as="ul" pl={6}>
|
||||
<li>Споры разрешаются в суде по месту нахождения администрации Платформы.</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 9. Изменения соглашения */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
9. Изменения соглашения
|
||||
</Heading>
|
||||
<Text>Изменения вступают в силу после публикации на сайте.</Text>
|
||||
</Box>
|
||||
|
||||
{/* 10. Контакты */}
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={3} color="gray.800">
|
||||
10. Контакты
|
||||
</Heading>
|
||||
<Text>
|
||||
Для обращений: <Link href="mailto:primakov.pro@yandex.ru" color="blue.500">primakov.pro@yandex.ru</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider mt={6} />
|
||||
|
||||
{/* Footer */}
|
||||
<Box textAlign="center" pt={4}>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
© 2025 BROJS.RU. Все права защищены.
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
2
src/pages/terms/index.ts
Normal file
2
src/pages/terms/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Terms } from './Terms';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user