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 GroupsPage from './pages/GroupsPage';
|
||||||
import DeliveryHistoryPage from './pages/DeliveryHistoryPage';
|
import DeliveryHistoryPage from './pages/DeliveryHistoryPage';
|
||||||
import CampaignPage from './pages/CampaignPage';
|
import CampaignPage from './pages/CampaignPage';
|
||||||
|
import ApiDocs from './pages/ApiDocs';
|
||||||
import MainLayout from './components/MainLayout';
|
import MainLayout from './components/MainLayout';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { useUser } from './context/UserContext';
|
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 />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const MainLayout = ({ children }) => {
|
|||||||
else if (path === '/groups') setActive('groups');
|
else if (path === '/groups') setActive('groups');
|
||||||
else if (path === '/history') setActive('history');
|
else if (path === '/history') setActive('history');
|
||||||
else if (path === '/campaign') setActive('campaign');
|
else if (path === '/campaign') setActive('campaign');
|
||||||
|
else if (path === '/api-docs') setActive('api-docs');
|
||||||
else setActive('dashboard');
|
else setActive('dashboard');
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,9 @@ const SideMenu = ({ active, onSelect }) => {
|
|||||||
case 'campaign':
|
case 'campaign':
|
||||||
navigate('/campaign');
|
navigate('/campaign');
|
||||||
break;
|
break;
|
||||||
|
case 'api-docs':
|
||||||
|
navigate('/api-docs');
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
}
|
}
|
||||||
@ -83,6 +86,7 @@ const SideMenu = ({ active, onSelect }) => {
|
|||||||
<div className={styles.section}>Администрирование</div>
|
<div className={styles.section}>Администрирование</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className={active === 'users' ? styles.active : ''} onClick={() => handleMenuSelect('users')}>Управление пользователями</li>
|
<li className={active === 'users' ? styles.active : ''} onClick={() => handleMenuSelect('users')}>Управление пользователями</li>
|
||||||
|
<li className={active === 'api-docs' ? styles.active : ''} onClick={() => handleMenuSelect('api-docs')}>API Документация</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</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