fix
This commit is contained in:
parent
5c42f03e47
commit
7590afb55c
566
frontend/public/API_DOCUMENTATION.md
Normal file
566
frontend/public/API_DOCUMENTATION.md
Normal file
@ -0,0 +1,566 @@
|
||||
# API Documentation
|
||||
|
||||
## Аутентификация
|
||||
|
||||
Все API запросы (кроме авторизации) требуют токен в заголовке Authorization.
|
||||
|
||||
### Получение токена
|
||||
|
||||
**POST** `/api/auth/users/login`
|
||||
|
||||
Авторизация пользователя и получение JWT токена
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response:
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"role_id": 1
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### Проверка токена
|
||||
|
||||
**POST** `/api/auth/verify`
|
||||
|
||||
Проверка валидности JWT токена
|
||||
|
||||
#### Headers:
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Response:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"role_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Управление пользователями
|
||||
|
||||
### Получение списка пользователей
|
||||
|
||||
**GET** `/api/auth/users`
|
||||
|
||||
Получение списка всех пользователей
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание пользователя
|
||||
|
||||
**POST** `/api/auth/users`
|
||||
|
||||
Создание нового пользователя
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"email": "newuser@example.com",
|
||||
"password": "password123",
|
||||
"role": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
### Обновление пользователя
|
||||
|
||||
**PUT** `/api/auth/users/:id`
|
||||
|
||||
Обновление пользователя
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление пользователя
|
||||
|
||||
**DELETE** `/api/auth/users/:id`
|
||||
|
||||
Удаление пользователя
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление ролями
|
||||
|
||||
### Получение списка ролей
|
||||
|
||||
**GET** `/api/auth/roles`
|
||||
|
||||
Получение списка всех ролей
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание роли
|
||||
|
||||
**POST** `/api/auth/roles`
|
||||
|
||||
Создание новой роли
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление роли
|
||||
|
||||
**PUT** `/api/auth/roles/:id`
|
||||
|
||||
Обновление роли
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление роли
|
||||
|
||||
**DELETE** `/api/auth/roles/:id`
|
||||
|
||||
Удаление роли
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление разрешениями
|
||||
|
||||
### Получение списка разрешений
|
||||
|
||||
**GET** `/api/auth/permissions`
|
||||
|
||||
Получение списка всех разрешений
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание разрешения
|
||||
|
||||
**POST** `/api/auth/permissions`
|
||||
|
||||
Создание нового разрешения
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление разрешения
|
||||
|
||||
**PUT** `/api/auth/permissions/:id`
|
||||
|
||||
Обновление разрешения
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление разрешения
|
||||
|
||||
**DELETE** `/api/auth/permissions/:id`
|
||||
|
||||
Удаление разрешения
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление подписчиками
|
||||
|
||||
### Получение списка подписчиков
|
||||
|
||||
**GET** `/api/mail/subscribers`
|
||||
|
||||
Получение списка всех подписчиков
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание подписчика
|
||||
|
||||
**POST** `/api/mail/subscribers`
|
||||
|
||||
Создание нового подписчика
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"email": "subscriber@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение подписчика по ID
|
||||
|
||||
**GET** `/api/mail/subscribers/:id`
|
||||
|
||||
Получение подписчика по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление подписчика
|
||||
|
||||
**PUT** `/api/mail/subscribers/:id`
|
||||
|
||||
Обновление подписчика
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление подписчика
|
||||
|
||||
**DELETE** `/api/mail/subscribers/:id`
|
||||
|
||||
Удаление подписчика
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление группами рассылки
|
||||
|
||||
### Получение списка групп
|
||||
|
||||
**GET** `/api/mail/mailing-groups`
|
||||
|
||||
Получение списка всех групп рассылки
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание группы
|
||||
|
||||
**POST** `/api/mail/mailing-groups`
|
||||
|
||||
Создание новой группы рассылки
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"name": "Newsletter Subscribers",
|
||||
"description": "Main newsletter group"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение группы по ID
|
||||
|
||||
**GET** `/api/mail/mailing-groups/:id`
|
||||
|
||||
Получение группы по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление группы
|
||||
|
||||
**PUT** `/api/mail/mailing-groups/:id`
|
||||
|
||||
Обновление группы
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление группы
|
||||
|
||||
**DELETE** `/api/mail/mailing-groups/:id`
|
||||
|
||||
Удаление группы
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление шаблонами email
|
||||
|
||||
### Получение списка шаблонов
|
||||
|
||||
**GET** `/api/mail/email-templates`
|
||||
|
||||
Получение списка всех email шаблонов
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание шаблона
|
||||
|
||||
**POST** `/api/mail/email-templates`
|
||||
|
||||
Создание нового email шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"name": "Welcome Email",
|
||||
"subject": "Welcome to our service!",
|
||||
"content": "<h1>Welcome!</h1><p>Thank you for joining us.</p>"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение шаблона по ID
|
||||
|
||||
**GET** `/api/mail/email-templates/:id`
|
||||
|
||||
Получение шаблона по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление шаблона
|
||||
|
||||
**PUT** `/api/mail/email-templates/:id`
|
||||
|
||||
Обновление шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление шаблона
|
||||
|
||||
**DELETE** `/api/mail/email-templates/:id`
|
||||
|
||||
Удаление шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление версиями шаблонов
|
||||
|
||||
### Получение списка версий
|
||||
|
||||
**GET** `/api/mail/email-template-versions`
|
||||
|
||||
Получение списка всех версий шаблонов
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание версии
|
||||
|
||||
**POST** `/api/mail/email-template-versions`
|
||||
|
||||
Создание новой версии шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Получение версии по ID
|
||||
|
||||
**GET** `/api/mail/email-template-versions/:id`
|
||||
|
||||
Получение версии шаблона по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление версии
|
||||
|
||||
**PUT** `/api/mail/email-template-versions/:id`
|
||||
|
||||
Обновление версии шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление версии
|
||||
|
||||
**DELETE** `/api/mail/email-template-versions/:id`
|
||||
|
||||
Удаление версии шаблона
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление кампаниями
|
||||
|
||||
### Получение списка кампаний
|
||||
|
||||
**GET** `/api/mail/campaigns`
|
||||
|
||||
Получение списка всех кампаний
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание кампании
|
||||
|
||||
**POST** `/api/mail/campaigns`
|
||||
|
||||
Создание новой кампании
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"name": "Summer Newsletter",
|
||||
"subject": "Summer Sale!",
|
||||
"template_id": 1,
|
||||
"group_id": 1,
|
||||
"smtp_server_id": 1,
|
||||
"scheduled_at": "2024-06-01T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение кампании по ID
|
||||
|
||||
**GET** `/api/mail/campaigns/:id`
|
||||
|
||||
Получение кампании по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление кампании
|
||||
|
||||
**PUT** `/api/mail/campaigns/:id`
|
||||
|
||||
Обновление кампании
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление кампании
|
||||
|
||||
**DELETE** `/api/mail/campaigns/:id`
|
||||
|
||||
Удаление кампании
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление SMTP серверами
|
||||
|
||||
### Получение списка SMTP серверов
|
||||
|
||||
**GET** `/api/mail/smtp-servers`
|
||||
|
||||
Получение списка всех SMTP серверов
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Создание SMTP сервера
|
||||
|
||||
**POST** `/api/mail/smtp-servers`
|
||||
|
||||
Создание нового SMTP сервера
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"name": "Gmail SMTP",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"username": "user@gmail.com",
|
||||
"password": "app_password",
|
||||
"encryption": "tls"
|
||||
}
|
||||
```
|
||||
|
||||
### Получение SMTP сервера по ID
|
||||
|
||||
**GET** `/api/mail/smtp-servers/:id`
|
||||
|
||||
Получение SMTP сервера по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Обновление SMTP сервера
|
||||
|
||||
**PUT** `/api/mail/smtp-servers/:id`
|
||||
|
||||
Обновление SMTP сервера
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Удаление SMTP сервера
|
||||
|
||||
**DELETE** `/api/mail/smtp-servers/:id`
|
||||
|
||||
Удаление SMTP сервера
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## История доставки
|
||||
|
||||
### Получение истории доставки
|
||||
|
||||
**GET** `/api/mail/delivery-logs`
|
||||
|
||||
Получение истории доставки email
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Получение записи доставки по ID
|
||||
|
||||
**GET** `/api/mail/delivery-logs/:id`
|
||||
|
||||
Получение записи доставки по ID
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
## Управление отписавшимися
|
||||
|
||||
### Получение списка отписавшихся
|
||||
|
||||
**GET** `/api/mail/unsubscribed`
|
||||
|
||||
Получение списка отписавшихся пользователей
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
### Добавление в список отписавшихся
|
||||
|
||||
**POST** `/api/mail/unsubscribed`
|
||||
|
||||
Добавление email в список отписавшихся
|
||||
|
||||
**Требует авторизации**
|
||||
|
||||
#### Request Body:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"reason": "No longer interested"
|
||||
}
|
||||
```
|
||||
|
||||
## Общие заголовки
|
||||
|
||||
Для всех авторизованных запросов используйте:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
## Коды ответов
|
||||
|
||||
- **200** - Успешный запрос
|
||||
- **201** - Ресурс создан
|
||||
- **400** - Ошибка в запросе
|
||||
- **401** - Не авторизован
|
||||
- **403** - Доступ запрещен
|
||||
- **404** - Ресурс не найден
|
||||
- **500** - Внутренняя ошибка сервера
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Авторизация и получение токена
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/users/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@example.com",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Получение списка подписчиков
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/mail/subscribers \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
### Создание новой кампании
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/mail/campaigns \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Welcome Campaign",
|
||||
"subject": "Welcome to our service!",
|
||||
"template_id": 1,
|
||||
"group_id": 1,
|
||||
"smtp_server_id": 1,
|
||||
"scheduled_at": "2024-06-01T10:00:00Z"
|
||||
}'
|
||||
```
|
||||
@ -9,6 +9,7 @@ import UnsubscribedPage from './pages/UnsubscribedPage';
|
||||
import GroupsPage from './pages/GroupsPage';
|
||||
import DeliveryHistoryPage from './pages/DeliveryHistoryPage';
|
||||
import CampaignPage from './pages/CampaignPage';
|
||||
import ApiDocs from './pages/ApiDocs';
|
||||
import MainLayout from './components/MainLayout';
|
||||
import './App.css';
|
||||
import { useUser } from './context/UserContext';
|
||||
@ -122,6 +123,18 @@ function App() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/api-docs"
|
||||
element={
|
||||
user && token ? (
|
||||
<MainLayout>
|
||||
<ApiDocs />
|
||||
</MainLayout>
|
||||
) : (
|
||||
<Navigate to="/login" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
|
||||
@ -21,6 +21,7 @@ const MainLayout = ({ children }) => {
|
||||
else if (path === '/groups') setActive('groups');
|
||||
else if (path === '/history') setActive('history');
|
||||
else if (path === '/campaign') setActive('campaign');
|
||||
else if (path === '/api-docs') setActive('api-docs');
|
||||
else setActive('dashboard');
|
||||
}, [location.pathname]);
|
||||
|
||||
|
||||
@ -36,6 +36,9 @@ const SideMenu = ({ active, onSelect }) => {
|
||||
case 'campaign':
|
||||
navigate('/campaign');
|
||||
break;
|
||||
case 'api-docs':
|
||||
navigate('/api-docs');
|
||||
break;
|
||||
default:
|
||||
navigate('/dashboard');
|
||||
}
|
||||
@ -83,6 +86,7 @@ const SideMenu = ({ active, onSelect }) => {
|
||||
<div className={styles.section}>Администрирование</div>
|
||||
<ul>
|
||||
<li className={active === 'users' ? styles.active : ''} onClick={() => handleMenuSelect('users')}>Управление пользователями</li>
|
||||
<li className={active === 'api-docs' ? styles.active : ''} onClick={() => handleMenuSelect('api-docs')}>API Документация</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
249
frontend/src/pages/ApiDocs.js
Normal file
249
frontend/src/pages/ApiDocs.js
Normal file
@ -0,0 +1,249 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import styles from './ApiDocs.module.css';
|
||||
|
||||
const ApiDocs = () => {
|
||||
const [markdownContent, setMarkdownContent] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [expandedSections, setExpandedSections] = useState(new Set());
|
||||
const { isDark } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMarkdown = async () => {
|
||||
try {
|
||||
const response = await fetch('/API_DOCUMENTATION.md');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch API documentation');
|
||||
}
|
||||
const content = await response.text();
|
||||
setMarkdownContent(content);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMarkdown();
|
||||
}, []);
|
||||
|
||||
const toggleSection = (sectionId) => {
|
||||
setExpandedSections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sectionId)) {
|
||||
newSet.delete(sectionId);
|
||||
} else {
|
||||
newSet.add(sectionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const renderMarkdown = (markdown) => {
|
||||
// Улучшенный парсер markdown для отображения документации
|
||||
const lines = markdown.split('\n');
|
||||
const elements = [];
|
||||
let currentSection = null;
|
||||
let currentEndpoint = null;
|
||||
let currentCodeBlock = [];
|
||||
let inCodeBlock = false;
|
||||
let listItems = [];
|
||||
let currentH3Section = null;
|
||||
let h3Content = [];
|
||||
let h3Index = 0;
|
||||
|
||||
const processList = () => {
|
||||
if (listItems.length > 0) {
|
||||
const listElement = (
|
||||
<ul key={`list-${elements.length}`} className={styles.ul}>
|
||||
{listItems.map((item, idx) => (
|
||||
<li key={idx} className={styles.li}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(listElement);
|
||||
} else {
|
||||
elements.push(listElement);
|
||||
}
|
||||
|
||||
listItems = [];
|
||||
}
|
||||
};
|
||||
|
||||
const processH3Section = () => {
|
||||
if (currentH3Section && h3Content.length > 0) {
|
||||
const sectionId = `h3-${h3Index}`;
|
||||
const isExpanded = expandedSections.has(sectionId);
|
||||
|
||||
elements.push(
|
||||
<div key={`section-${h3Index}`} className={styles.collapsibleSection}>
|
||||
<div
|
||||
className={styles.sectionHeader}
|
||||
onClick={() => toggleSection(sectionId)}
|
||||
>
|
||||
<h3 className={styles.h3}>{currentH3Section}</h3>
|
||||
<span className={`${styles.expandIcon} ${isExpanded ? styles.expanded : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles.sectionContent} ${isExpanded ? styles.expanded : ''}`}>
|
||||
{h3Content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
currentH3Section = null;
|
||||
h3Content = [];
|
||||
h3Index++;
|
||||
}
|
||||
};
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Заголовки
|
||||
if (line.startsWith('# ')) {
|
||||
processList();
|
||||
processH3Section();
|
||||
elements.push(<h1 key={index} className={styles.h1}>{line.substring(2)}</h1>);
|
||||
} else if (line.startsWith('## ')) {
|
||||
processList();
|
||||
processH3Section();
|
||||
currentSection = line.substring(3);
|
||||
elements.push(<h2 key={index} className={styles.h2}>{currentSection}</h2>);
|
||||
} else if (line.startsWith('### ')) {
|
||||
processList();
|
||||
processH3Section();
|
||||
currentH3Section = line.substring(4);
|
||||
} else if (line.startsWith('#### ')) {
|
||||
processList();
|
||||
currentEndpoint = line.substring(5);
|
||||
const endpointElement = (
|
||||
<div key={index} className={styles.endpoint}>
|
||||
<h4 className={styles.h4}>{currentEndpoint}</h4>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(endpointElement);
|
||||
} else {
|
||||
elements.push(endpointElement);
|
||||
}
|
||||
} else if (line.startsWith('**') && line.includes('**') && line.includes('/')) {
|
||||
// HTTP methods with endpoints
|
||||
processList();
|
||||
const method = line.match(/\*\*(.*?)\*\*/);
|
||||
if (method) {
|
||||
const methodElement = (
|
||||
<div key={index} className={styles.endpoint}>
|
||||
<h4 className={styles.h4}>{method[1]}</h4>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(methodElement);
|
||||
} else {
|
||||
elements.push(methodElement);
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('```')) {
|
||||
processList();
|
||||
if (!inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
currentCodeBlock = [];
|
||||
} else {
|
||||
inCodeBlock = false;
|
||||
const codeElement = (
|
||||
<pre key={index} className={styles.code}>
|
||||
{currentCodeBlock.join('\n')}
|
||||
</pre>
|
||||
);
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(codeElement);
|
||||
} else {
|
||||
elements.push(codeElement);
|
||||
}
|
||||
|
||||
currentCodeBlock = [];
|
||||
}
|
||||
} else if (inCodeBlock) {
|
||||
currentCodeBlock.push(line);
|
||||
} else if (line.startsWith('- ')) {
|
||||
// Списки
|
||||
listItems.push(line.substring(2));
|
||||
} else if (line.startsWith('**Требует авторизации**')) {
|
||||
processList();
|
||||
const authElement = <p key={index} className={styles.authRequired}><strong>Требует авторизации</strong></p>;
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(authElement);
|
||||
} else {
|
||||
elements.push(authElement);
|
||||
}
|
||||
} else if (line.trim() === '') {
|
||||
// Пустые строки
|
||||
processList();
|
||||
if (elements.length > 0 && elements[elements.length - 1].type !== 'br') {
|
||||
const brElement = <br key={index} />;
|
||||
if (currentH3Section) {
|
||||
h3Content.push(brElement);
|
||||
} else {
|
||||
elements.push(brElement);
|
||||
}
|
||||
}
|
||||
} else if (line.trim()) {
|
||||
// Обычный текст
|
||||
processList();
|
||||
const textElement = <p key={index} className={styles.p}>{line}</p>;
|
||||
|
||||
if (currentH3Section) {
|
||||
h3Content.push(textElement);
|
||||
} else {
|
||||
elements.push(textElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Обработать оставшиеся элементы списка и секции
|
||||
processList();
|
||||
processH3Section();
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.loading}>Загрузка документации API...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.error}>
|
||||
<h2>Ошибка загрузки документации</h2>
|
||||
<p>{error}</p>
|
||||
<p>Убедитесь, что файл API_DOCUMENTATION.md находится в папке public.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${isDark ? styles.dark : styles.light}`}>
|
||||
<div className={styles.content}>
|
||||
{renderMarkdown(markdownContent)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiDocs;
|
||||
379
frontend/src/pages/ApiDocs.module.css
Normal file
379
frontend/src/pages/ApiDocs.module.css
Normal file
@ -0,0 +1,379 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--background-color, #ffffff);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.content h1, .h1 {
|
||||
color: var(--text-color, #333);
|
||||
border-bottom: 3px solid var(--primary-color, #007bff);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.content h2, .h2 {
|
||||
color: var(--text-color, #333);
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.8rem;
|
||||
border-left: 4px solid var(--primary-color, #007bff);
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.content h3, .h3 {
|
||||
color: var(--text-color, #333);
|
||||
margin-top: 15px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.content h4, .h4 {
|
||||
color: var(--primary-color, #007bff);
|
||||
margin-top: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content h5, .h5 {
|
||||
color: var(--text-color, #333);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content p, .p {
|
||||
color: var(--text-color, #555);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content ul, .ul {
|
||||
color: var(--text-color, #555);
|
||||
margin-bottom: 12px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.content li, .li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-color, #555);
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.error h2 {
|
||||
color: #dc3545;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.authRequired {
|
||||
color: #dc3545 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapsibleSection {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--border-color, #e9ecef);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collapsibleSection:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
background: var(--endpoint-bg, #f8f9fa);
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container.light .sectionHeader {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.container.dark .sectionHeader {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.sectionHeader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.sectionHeader:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.sectionHeader:hover {
|
||||
background: var(--hover-bg, #e9ecef);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.container.light .sectionHeader:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.container.dark .sectionHeader:hover {
|
||||
background: #4a5568;
|
||||
}
|
||||
|
||||
.sectionHeader h3 {
|
||||
margin: 0;
|
||||
color: var(--primary-color, #007bff);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s ease;
|
||||
color: var(--primary-color, #007bff);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.expandIcon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
background: var(--background-color, #ffffff);
|
||||
}
|
||||
|
||||
.sectionContent.expanded {
|
||||
max-height: 2000px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
background: var(--endpoint-bg, #f8f9fa);
|
||||
border: 1px solid var(--border-color, #e9ecef);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border-left: 4px solid var(--primary-color, #007bff);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.endpoint:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.endpoint h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-color, #007bff);
|
||||
}
|
||||
|
||||
.endpoint p {
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-color, #555);
|
||||
}
|
||||
|
||||
.code {
|
||||
background: var(--code-bg, #2d3748);
|
||||
color: var(--code-color, #e2e8f0);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
border: 1px solid var(--code-border, #4a5568);
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.code::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.code::-webkit-scrollbar-track {
|
||||
background: var(--code-bg, #2d3748);
|
||||
}
|
||||
|
||||
.code::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-color, #4a5568);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-hover, #718096);
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Light theme by default */
|
||||
.container {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.container.light {
|
||||
--background-color: #f8f9fa;
|
||||
--text-color: #333333;
|
||||
--primary-color: #007bff;
|
||||
--endpoint-bg: #ffffff;
|
||||
--border-color: #e9ecef;
|
||||
--code-bg: #f8f9fa;
|
||||
--code-color: #333333;
|
||||
--code-border: #dee2e6;
|
||||
--scrollbar-color: #4a5568;
|
||||
--scrollbar-hover: #718096;
|
||||
--hover-bg: #e9ecef;
|
||||
}
|
||||
|
||||
.container.light .content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.container.light .endpoint {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.container.light .sectionContent {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.container.light .code {
|
||||
background: #f8f9fa;
|
||||
color: #333333;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
.container.dark {
|
||||
--background-color: #1a202c;
|
||||
--text-color: #e2e8f0;
|
||||
--primary-color: #63b3ed;
|
||||
--endpoint-bg: #2d3748;
|
||||
--border-color: #4a5568;
|
||||
--code-bg: #1a202c;
|
||||
--code-color: #e2e8f0;
|
||||
--code-border: #4a5568;
|
||||
--scrollbar-color: #4a5568;
|
||||
--scrollbar-hover: #718096;
|
||||
--hover-bg: #4a5568;
|
||||
}
|
||||
|
||||
.container.dark .content {
|
||||
background: #1a202c;
|
||||
}
|
||||
|
||||
.container.dark .endpoint {
|
||||
background: #2d3748;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.container.dark .sectionContent {
|
||||
background: #1a202c;
|
||||
}
|
||||
|
||||
.container.dark .code {
|
||||
background: #1a202c;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.content h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.content h4 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.code {
|
||||
padding: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #f5f5f5 !important;
|
||||
color: #333 !important;
|
||||
border: 1px solid #ccc !important;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user