diff --git a/frontend/public/API_DOCUMENTATION.md b/frontend/public/API_DOCUMENTATION.md new file mode 100644 index 0000000..cc6cd59 --- /dev/null +++ b/frontend/public/API_DOCUMENTATION.md @@ -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 +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": "

Welcome!

Thank you for joining us.

" +} +``` + +### Получение шаблона по 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 +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" + }' +``` \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 2b07db6..b990212 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -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() { ) } /> + + + + ) : ( + + ) + } + /> } /> diff --git a/frontend/src/components/MainLayout.js b/frontend/src/components/MainLayout.js index 3c491ac..43b24f9 100644 --- a/frontend/src/components/MainLayout.js +++ b/frontend/src/components/MainLayout.js @@ -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]); diff --git a/frontend/src/components/SideMenu.js b/frontend/src/components/SideMenu.js index e300122..2cd3d93 100644 --- a/frontend/src/components/SideMenu.js +++ b/frontend/src/components/SideMenu.js @@ -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 }) => {
Администрирование
  • handleMenuSelect('users')}>Управление пользователями
  • +
  • handleMenuSelect('api-docs')}>API Документация
diff --git a/frontend/src/pages/ApiDocs.js b/frontend/src/pages/ApiDocs.js new file mode 100644 index 0000000..56d5781 --- /dev/null +++ b/frontend/src/pages/ApiDocs.js @@ -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 = ( +
    + {listItems.map((item, idx) => ( +
  • {item}
  • + ))} +
+ ); + + 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( +
+
toggleSection(sectionId)} + > +

{currentH3Section}

+ + ▼ + +
+
+ {h3Content} +
+
+ ); + + currentH3Section = null; + h3Content = []; + h3Index++; + } + }; + + lines.forEach((line, index) => { + // Заголовки + if (line.startsWith('# ')) { + processList(); + processH3Section(); + elements.push(

{line.substring(2)}

); + } else if (line.startsWith('## ')) { + processList(); + processH3Section(); + currentSection = line.substring(3); + elements.push(

{currentSection}

); + } else if (line.startsWith('### ')) { + processList(); + processH3Section(); + currentH3Section = line.substring(4); + } else if (line.startsWith('#### ')) { + processList(); + currentEndpoint = line.substring(5); + const endpointElement = ( +
+

{currentEndpoint}

+
+ ); + + 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 = ( +
+

{method[1]}

+
+ ); + + if (currentH3Section) { + h3Content.push(methodElement); + } else { + elements.push(methodElement); + } + } + } else if (line.startsWith('```')) { + processList(); + if (!inCodeBlock) { + inCodeBlock = true; + currentCodeBlock = []; + } else { + inCodeBlock = false; + const codeElement = ( +
+              {currentCodeBlock.join('\n')}
+            
+ ); + + 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 =

Требует авторизации

; + + 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 =
; + if (currentH3Section) { + h3Content.push(brElement); + } else { + elements.push(brElement); + } + } + } else if (line.trim()) { + // Обычный текст + processList(); + const textElement =

{line}

; + + if (currentH3Section) { + h3Content.push(textElement); + } else { + elements.push(textElement); + } + } + }); + + // Обработать оставшиеся элементы списка и секции + processList(); + processH3Section(); + + return elements; + }; + + if (loading) { + return ( +
+
+
Загрузка документации API...
+
+
+ ); + } + + if (error) { + return ( +
+
+
+

Ошибка загрузки документации

+

{error}

+

Убедитесь, что файл API_DOCUMENTATION.md находится в папке public.

+
+
+
+ ); + } + + return ( +
+
+ {renderMarkdown(markdownContent)} +
+
+ ); +}; + +export default ApiDocs; \ No newline at end of file diff --git a/frontend/src/pages/ApiDocs.module.css b/frontend/src/pages/ApiDocs.module.css new file mode 100644 index 0000000..33706a7 --- /dev/null +++ b/frontend/src/pages/ApiDocs.module.css @@ -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; + } +} \ No newline at end of file