commit 743d953beaeb45790f35b5b5179b26567ab1cdf8 Author: Sergey Petrov Date: Mon Mar 10 22:18:45 2025 +0300 Init commit diff --git a/.env b/.env new file mode 100644 index 0000000..c785493 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +PORT=3000 +API_TOKEN=b4c8e9c9-73e4-4669-b75b-3ad625721286 +API_URL=https://sd.nubes.ru/pub/v1/app/employees/employees_upload/list +API_PAGE_SIZE=1000 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57cf12b --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ + # Зависимости +node_modules/ +package-lock.json +.idea/ + +# Переменные окружения +.env.* + +# Логи +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Системные файлы +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..75b1a5c --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Телефонный справочник + +Простое stateless приложение на Node.js для отображения телефонного справочника. + +## Установка + +```bash +npm install +``` + +## Запуск + +```bash +npm start +``` + +Приложение будет доступно по адресу: http://localhost:3000 + +## Описание + +Приложение получает данные из внешнего API (в данном примере используется JSONPlaceholder) и отображает их в виде простой таблицы. + +## Технологии + +- Node.js +- Express +- node-fetch \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b91a6cd --- /dev/null +++ b/index.js @@ -0,0 +1,82 @@ +import express from 'express'; +import fetch from 'node-fetch'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Serve static files from public directory +app.use(express.static('public')); + +// API endpoint to get phone book data +async function getPhoneBookData() { + try { + const response = await fetch(process.env.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.API_TOKEN}` + }, + body: JSON.stringify({ + "active": true, + "fields": { + "fullname": true, + "mobile_number": true, + "fixed_number": true, + "title": true, + "department": true, + "email_string": true + }, + "from": 0, + "size": parseInt(process.env.API_PAGE_SIZE) || 1000 + }) + }); + + const data = await response.json(); + + if (!data.success || !data.result?.result) { + throw new Error('Неверный формат данных от API'); + } + + return data.result.result.map(employee => ({ + name: formatFullName(employee.fullname), + phone: employee.mobile_number || employee.fixed_number || '-', + title: employee.title || '-', + department: employee.department || '-', + email: employee.email_string || '-' + })); + } catch (error) { + console.error('Ошибка при запросе к API:', error.message); + return []; + } +} + +function formatFullName(fullname) { + if (!fullname) return '-'; + + const { lastname = '', firstname = '', middlename = '' } = fullname; + return [lastname, firstname, middlename].filter(Boolean).join(' ') || '-'; +} + +// API endpoint for contacts +app.get('/api/contacts', async (req, res) => { + const contacts = await getPhoneBookData(); + res.json(contacts); +}); + +// Serve index.html for all other routes +app.get('*', (req, res) => { + res.sendFile(join(__dirname, 'public', 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Сервер запущен на порту ${PORT}`); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4fbd407 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "phone-book", + "version": "1.0.0", + "description": "Simple phone book application", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.18.2", + "node-fetch": "^3.3.2", + "dotenv": "^16.3.1" + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f57dc6f --- /dev/null +++ b/public/index.html @@ -0,0 +1,39 @@ + + + + + + Телефонный справочник + + + +
+

Телефонный справочник

+
+ +
+
+ +
+ + + + + + + + + + + + + +
ФИОДолжностьОтделТелефонEmail
+ + +
+ + + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..b7f7646 --- /dev/null +++ b/public/script.js @@ -0,0 +1,52 @@ +document.addEventListener('DOMContentLoaded', async () => { + const searchInput = document.getElementById('searchInput'); + const contactsList = document.getElementById('contactsList'); + const noResults = document.querySelector('.no-results'); + let contacts = []; + + // Загрузка данных с сервера + try { + const response = await fetch('/api/contacts'); + contacts = await response.json(); + renderContacts(contacts); + } catch (error) { + console.error('Ошибка при загрузке контактов:', error); + noResults.textContent = 'Ошибка при загрузке контактов'; + noResults.style.display = 'block'; + } + + function formatPhoneNumbers(phone) { + if (!phone || phone === '-') return '-'; + return phone.split(';').map(num => num.trim()).filter(Boolean).join('\n'); + } + + function renderContacts(contactsToRender) { + contactsList.innerHTML = contactsToRender.map(contact => ` + + ${contact.name} + ${contact.title} + ${contact.department} + ${formatPhoneNumbers(contact.phone)} + ${contact.email} + + `).join(''); + } + + function filterContacts(searchTerm) { + searchTerm = searchTerm.toLowerCase(); + const filteredContacts = contacts.filter(contact => + contact.name.toLowerCase().includes(searchTerm) || + contact.title.toLowerCase().includes(searchTerm) || + contact.department.toLowerCase().includes(searchTerm) || + (contact.phone && contact.phone !== '-' && contact.phone.toLowerCase().includes(searchTerm)) || + (contact.email && contact.email !== '-' && contact.email.toLowerCase().includes(searchTerm)) + ); + + renderContacts(filteredContacts); + noResults.style.display = filteredContacts.length ? 'none' : 'block'; + } + + searchInput.addEventListener('input', (e) => { + filterContacts(e.target.value); + }); +}); \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..fda06bf --- /dev/null +++ b/public/styles.css @@ -0,0 +1,168 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f5f5f5; + height: 100vh; + display: flex; + flex-direction: column; +} + +.header { + background-color: white; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.table-container { + margin-top: 160px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-left: 20px; + margin-right: 20px; + margin-bottom: 20px; + overflow: auto; + max-height: calc(100vh - 200px); +} + +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +/* Стили для заголовка таблицы */ +thead { + position: sticky; + top: 0; + z-index: 3; + background: #f8f9fa; +} + +thead::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: #f8f9fa; + z-index: -1; +} + +th { + background: #f8f9fa; + position: sticky; + top: 0; + z-index: 3; + padding: 15px; + text-align: left; + font-weight: 600; + color: #2c3e50; + border-bottom: 2px solid #e9ecef; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Стили для ячеек таблицы */ +tbody tr { + background: white; + transition: all 0.2s ease; +} + +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #f2f2f2; + background: white; + font-size: 14px; + line-height: 1.4; + color: #444; +} + +/* Разрешаем перенос текста для определенных столбцов */ +td:nth-child(1), /* ФИО */ +td:nth-child(2), /* Должность */ +td:nth-child(3) { /* Отдел */ + white-space: normal; + min-width: 150px; + max-width: 250px; +} + +/* Телефон */ +td:nth-child(4) { + white-space: pre-line; + min-width: 120px; + font-family: monospace; + font-size: 13px; +} + +/* Email */ +td:nth-child(5) { + min-width: 180px; + font-family: monospace; + font-size: 13px; + color: #2962ff; +} + +/* Эффект при наведении */ +tr:hover td { + background: #f8f9fa; +} + +/* Чередование строк */ +tbody tr:nth-child(even) { + background-color: #fcfcfc; +} + +h1 { + color: #2c3e50; + text-align: center; + margin: 0 0 20px 0; + font-size: 24px; + font-weight: 500; +} + +.search-container { + padding: 0; +} + +#searchInput { + width: 100%; + padding: 12px 15px; + border: 1px solid #e0e0e0; + border-radius: 6px; + font-size: 15px; + box-sizing: border-box; + transition: all 0.2s ease; + background-color: #f8f9fa; +} + +#searchInput:focus { + outline: none; + border-color: #2962ff; + box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1); + background-color: white; +} + +#searchInput::placeholder { + color: #999; +} + +.no-results { + text-align: center; + padding: 30px; + color: #666; + font-style: italic; + background: #f8f9fa; + border-radius: 6px; + margin: 20px; + font-size: 15px; +} \ No newline at end of file