Добавлена полная документация для лендинга BROJS.RU, включая описание структуры проекта, SSG, пользовательского соглашения и команды для сборки. Реализована автоматическая генерация terms.html из terma.md. Обновлены зависимости и скрипты для сборки. Исправлены ошибки в конфигурации и добавлены новые страницы.
All checks were successful
platform/bro-js/bro.landing/pipeline/head This commit looks good

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-10-24 11:04:15 +03:00
parent e91b861415
commit 9110e79d6b
13 changed files with 3427 additions and 3022 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"i18n-ally.localesPaths": [
"locales"
]
}

424
cloud.md Normal file
View 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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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
View 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
View 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();

View File

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

View File

@ -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 />);
});
}
}
})();

View File

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

@ -0,0 +1,2 @@
export { Terms } from './Terms';