Создать новую заметку
+ +Все заметки
+ + +Загрузка заметок...
+Нет заметок
+Создайте свою первую заметку, используя форму выше.
+diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..6d474e2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +notes.db diff --git a/backend/package-lock.json b/backend/package-lock.json index d0dbbee..c8f4680 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "cors": "^2.8.5", "express": "^5.2.1", + "sqlite": "^5.1.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" } @@ -1017,6 +1018,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index c18605e..28dc8e0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "dependencies": { "cors": "^2.8.5", "express": "^5.2.1", + "sqlite": "^5.1.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" } diff --git a/backend/src/app.js b/backend/src/app.js index eac46db..be700fa 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,256 +1,182 @@ import express from 'express'; import { json } from 'express'; import cors from 'cors'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Создаем экземпляр приложения Express const app = express(); -// Подключаем наши middleware (промежуточные обработчики маршрутов) - -// middlware CORS разрешает запросы с других доменов. Без этого браузер -// запретит фронтенду общаться с бекендом! app.use(cors({ - origin: 'http://localhost:8080', // разрешаем только с этого домена - methods: ['GET', 'POST', 'PUT', 'DELETE'], // разрешаем только эти методы - allowedHeaders: ['Content-Type'] // разрешаем только эти заголовки + origin: 'http://localhost:8080', + methods: ['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders: ['Content-Type'] })); -// JSON парсер позволяет обрабатывать JSON в теле запросов app.use(json()); -// Создаем массив для хранения заметок в памяти -// В реальном приложении здесь была бы база данных -let notes = []; +let db; -// Переменная для генерации уникальных ID -// Каждая новая заметка получит ID на 1 больше предыдущей -let noteIdCounter = 1; - -// ==================== РАЗДЕЛ: РОУТЫ API (маршруты) ==================== - -// РОУТ 1: Получение всех заметок (READ) -// Метод: GET -// Адрес: /notes -app.get('/notes', function(request, response) { - /* - Эта функция обрабатывает запрос на получение всех заметок - request - объект запроса (содержит данные от клиента) - response - объект ответа (используем для отправки данных клиенту) - */ - console.log('Получен запрос на получение всех заметок'); - - // Отправляем клиенту все заметки в формате JSON - // status(200) - код 200 означает "Успешно" - response.status(200).json({ - success: true, // флаг успешного выполнения - data: notes, // сами заметки - count: notes.length // количество заметок +async function initializeDatabase() { + db = await open({ + filename: './notes.db', + driver: sqlite3.Database }); -}); -// РОУТ 2: Получение одной заметки по ID (READ) -// Метод: GET -// Адрес: /notes/:id -// :id - это параметр маршрута (динамическая часть URL) -app.get('/notes/:id', function(request, response) { - /* - Эта функция ищет заметку по ID - request.params.id - получаем ID из URL - */ - console.log('Получен запрос на получение заметки с ID:', request.params.id); - - // Преобразуем ID из строки в число - const noteId = parseInt(request.params.id); - - // Ищем заметку в массиве по ID. - // В качестве аргумента notes.find() используем - // функцию function(note) {}, которая определяет, - // совпадает ли id некоторой заметки с id нашей - const foundNote = notes.find(function(note) { - return note.id === noteId; - }); - - // Если заметка не найдена - if (!foundNote) { - console.log('Заметка не найдена'); - return response.status(404).json({ + await db.exec(` + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + text TEXT NOT NULL, + author TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); +} + +initializeDatabase(); + +app.get('/notes', async (req, res) => { + try { + const notes = await db.all('SELECT * FROM notes ORDER BY createdAt DESC'); + res.status(200).json({ + success: true, + data: notes, + count: notes.length + }); + } catch (error) { + res.status(500).json({ success: false, - message: 'Заметка с таким ID не найдена' + message: 'Ошибка при получении заметок' }); } - - // Если заметка найдена - отправляем ее клиенту - response.status(200).json({ - success: true, - data: foundNote - }); }); -// РОУТ 3: Создание новой заметки (CREATE) -// Метод: POST -// Адрес: /notes -app.post('/notes', function(request, response) { - /* - Эта функция создает новую заметку - request.body - содержит данные, отправленные клиентом - */ - console.log('Получен запрос на создание заметки'); +app.get('/notes/:id', async (req, res) => { + try { + const note = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id); + + if (!note) { + return res.status(404).json({ + success: false, + message: 'Заметка с таким ID не найдена' + }); + } + + res.status(200).json({ + success: true, + data: note + }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Ошибка при получении заметки' + }); + } +}); + +app.post('/notes', async (req, res) => { + const { title, text, author } = req.body; - // Получаем данные из тела запроса - const title = request.body.title; - const text = request.body.text; - const author = request.body.author; - - // Проверяем, все ли обязательные поля заполнены if (!title || !text || !author) { - console.log('Ошибка: не все поля заполнены'); - return response.status(400).json({ + return res.status(400).json({ success: false, message: 'Пожалуйста, заполните все поля: title, text, author' }); } - // Создаем новую заметку - const newNote = { - id: noteIdCounter, // присваиваем уникальный ID - title: title, // заголовок из запроса - text: text, // текст заметки из запроса - author: author, // автор из запроса - createdAt: new Date(), // дата создания (текущее время) - updatedAt: new Date() // дата обновления (пока равна дате создания) - }; - - // Добавляем заметку в массив - notes.push(newNote); - // Увеличиваем счетчик ID для следующей заметки - noteIdCounter++; - - console.log('Создана новая заметка с ID:', newNote.id); - - // Отправляем ответ с созданной заметкой - // status(201) - код 201 означает "Создано" - response.status(201).json({ - success: true, - message: 'Заметка успешно создана', - data: newNote - }); -}); - -// РОУТ 4: Обновление существующей заметки (UPDATE) -// Метод: PUT -// Адрес: /notes/:id -app.put('/notes/:id', function(request, response) { - /* - Эта функция обновляет существующую заметку - request.params.id - ID заметки для обновления - request.body - новые данные для заметки - */ - console.log('Получен запрос на обновление заметки с ID:', request.params.id); - - const noteId = parseInt(request.params.id); - - // Ищем индекс заметки в массиве - const noteIndex = notes.findIndex(function(note) { - return note.id === noteId; - }); - - // Если заметка не найдена - if (noteIndex === -1) { - console.log('Заметка для обновления не найдена'); - return response.status(404).json({ + try { + const result = await db.run( + 'INSERT INTO notes (title, text, author) VALUES (?, ?, ?)', + [title, text, author] + ); + + const newNote = await db.get('SELECT * FROM notes WHERE id = ?', result.lastID); + + res.status(201).json({ + success: true, + message: 'Заметка успешно создана', + data: newNote + }); + } catch (error) { + res.status(500).json({ success: false, - message: 'Заметка с таким ID не найдена' + message: 'Ошибка при создании заметки' }); } - - // Получаем данные из запроса - // Если какое-то поле не передано, используем старое значение - const updatedTitle = request.body.title || notes[noteIndex].title; - const updatedText = request.body.text || notes[noteIndex].text; - const updatedAuthor = request.body.author || notes[noteIndex].author; - - // Обновляем заметку - notes[noteIndex] = { - ...notes[noteIndex], // копируем все старые поля - title: updatedTitle, // обновляем заголовок - text: updatedText, // обновляем текст - author: updatedAuthor, // обновляем автора - updatedAt: new Date() // обновляем дату изменения - }; - - console.log('Заметка с ID', noteId, 'обновлена'); - - // Отправляем обновленную заметку - response.status(200).json({ - success: true, - message: 'Заметка успешно обновлена', - data: notes[noteIndex] - }); }); -// РОУТ 5: Удаление заметки (DELETE) -// Метод: DELETE -// Адрес: /notes/:id -app.delete('/notes/:id', function(request, response) { - /* - Эта функция удаляет заметку по ID - */ - console.log('Получен запрос на удаление заметки с ID:', request.params.id); - - const noteId = parseInt(request.params.id); - - // Ищем индекс заметки в массиве - const noteIndex = notes.findIndex(function(note) { - return note.id === noteId; - }); - - // Если заметка не найдена - if (noteIndex === -1) { - console.log('Заметка для удаления не найдена'); - return response.status(404).json({ +app.put('/notes/:id', async (req, res) => { + try { + const existingNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id); + + if (!existingNote) { + return res.status(404).json({ + success: false, + message: 'Заметка с таким ID не найдена' + }); + } + + const updatedTitle = req.body.title || existingNote.title; + const updatedText = req.body.text || existingNote.text; + const updatedAuthor = req.body.author || existingNote.author; + + await db.run( + 'UPDATE notes SET title = ?, text = ?, author = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = ?', + [updatedTitle, updatedText, updatedAuthor, req.params.id] + ); + + const updatedNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id); + + res.status(200).json({ + success: true, + message: 'Заметка успешно обновлена', + data: updatedNote + }); + } catch (error) { + res.status(500).json({ success: false, - message: 'Заметка с таким ID не найдена' + message: 'Ошибка при обновлении заметки' }); } - - // Удаляем заметку из массива - // splice(index, 1) удаляет 1 элемент начиная с позиции index - const deletedNote = notes.splice(noteIndex, 1)[0]; - - console.log('Заметка с ID', noteId, 'удалена'); - - // Отправляем подтверждение удаления - response.status(200).json({ - success: true, - message: 'Заметка успешно удалена', - data: deletedNote - }); }); -// РОУТ 7: Корневой маршрут (для проверки работы сервера) -// Метод: GET -// Адрес: / -app.get('/', function(_, response) { - /* - Простой маршрут для проверки, что сервер работает - */ - response.json({ +app.delete('/notes/:id', async (req, res) => { + try { + const existingNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id); + + if (!existingNote) { + return res.status(404).json({ + success: false, + message: 'Заметка с таким ID не найдена' + }); + } + + await db.run('DELETE FROM notes WHERE id = ?', req.params.id); + + res.status(200).json({ + success: true, + message: 'Заметка успешно удалена', + data: existingNote + }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Ошибка при удалении заметки' + }); + } +}); + +app.get('/', (_, res) => { + res.json({ message: 'Добро пожаловать в API для заметок!', endpoints: { getAllNotes: 'GET /notes', getOneNote: 'GET /notes/:id', createNote: 'POST /notes', updateNote: 'PUT /notes/:id', - deleteNote: 'DELETE /notes/:id', - docs: 'GET /docs' - }, - instructions: 'Используйте Postman или curl для тестирования API' + deleteNote: 'DELETE /notes/:id' + } }); }); -// Экспортируем приложение для использования в server.js export default app; diff --git a/frontend/src/public/css/style.css b/frontend/src/public/css/style.css index e69de29..ced54f2 100644 --- a/frontend/src/public/css/style.css +++ b/frontend/src/public/css/style.css @@ -0,0 +1,493 @@ +/* Основные стили для приложения заметок */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --primary-color: #4361ee; + --secondary-color: #3f37c9; + --success-color: #4cc9f0; + --danger-color: #f72585; + --warning-color: #ff9e00; + --light-color: #f8f9fa; + --dark-color: #212529; + --gray-color: #6c757d; + --light-gray: #e9ecef; + --border-color: #dee2e6; + --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-hover: 0 6px 12px rgba(0, 0, 0, 0.15); + --border-radius: 8px; + --transition: all 0.3s ease; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: var(--dark-color); + background-color: #f5f7fb; + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background-color: white; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + overflow: hidden; +} + +/* Шапка */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 30px; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; +} + +.logo { + display: flex; + align-items: center; + gap: 15px; +} + +.logo i { + font-size: 2.5rem; +} + +.logo h1 { + font-size: 1.8rem; + font-weight: 600; + color: var(--light-color); +} + +.stats { + display: flex; + align-items: center; + gap: 20px; +} + +#notes-count { + font-size: 1.1rem; + background-color: rgba(255, 255, 255, 0.2); + padding: 8px 15px; + border-radius: 50px; +} + +/* Кнопки */ +.btn { + padding: 10px 20px; + border: none; + border-radius: var(--border-radius); + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 8px; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); +} + +.btn:active { + transform: translateY(0); +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--secondary-color); +} + +.btn-secondary { + background-color: var(--light-gray); + color: var(--dark-color); +} + +.btn-secondary:hover { + background-color: #d1d5db; +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-danger { + background-color: var(--danger-color); + color: white; +} + +.btn-warning { + background-color: var(--warning-color); + color: white; +} + +.btn-refresh { + background-color: white; + color: var(--primary-color); +} + +.btn-refresh:hover { + background-color: var(--light-gray); +} + +/* Основной контент */ +.main-content { + display: grid; + grid-template-columns: 1fr; + gap: 30px; + padding: 30px; +} + +@media (min-width: 992px) { + .main-content { + grid-template-columns: 1fr 1fr; + } +} + +.note-form-section, .notes-section { + background-color: white; + padding: 25px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); +} + +.note-form-section h2, .notes-section h2 { + margin-bottom: 20px; + color: var(--primary-color); + padding-bottom: 10px; + border-bottom: 2px solid var(--light-gray); +} + +/* Форма */ +.note-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-weight: 600; + color: var(--dark-color); +} + +.form-group input, +.form-group textarea { + padding: 12px 15px; + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; + transition: var(--transition); +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2); +} + +.form-actions { + display: flex; + gap: 15px; + margin-top: 10px; +} + +/* Фильтры и поиск */ +.filters { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 20px; +} + +@media (min-width: 768px) { + .filters { + flex-direction: row; + justify-content: space-between; + align-items: center; + } +} + +.search-box { + position: relative; + flex-grow: 1; +} + +.search-box i { + position: absolute; + left: 15px; + top: 50%; + transform: translateY(-50%); + color: var(--gray-color); +} + +.search-box input { + width: 100%; + padding: 12px 15px 12px 45px; + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; +} + +.search-box input:focus { + outline: none; + border-color: var(--primary-color); +} + +.sort-options select { + padding: 12px 15px; + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; + background-color: white; + cursor: pointer; + min-width: 200px; +} + +/* Контейнер заметок */ +.notes-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +/* Карточка заметки */ +.note-card { + background-color: white; + border-radius: var(--border-radius); + padding: 20px; + box-shadow: var(--shadow); + border-left: 5px solid var(--primary-color); + transition: var(--transition); + display: flex; + flex-direction: column; + gap: 15px; +} + +.note-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-hover); +} + +.note-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 10px; +} + +.note-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--dark-color); + margin: 0; + word-break: break-word; +} + +.note-actions { + display: flex; + gap: 10px; +} + +.note-action-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + padding: 5px; + border-radius: 4px; + transition: var(--transition); + color: var(--gray-color); +} + +.note-action-btn:hover { + background-color: var(--light-gray); + color: var(--dark-color); +} + +.note-action-btn.edit:hover { + color: var(--primary-color); +} + +.note-action-btn.delete:hover { + color: var(--danger-color); +} + +.note-text { + color: var(--gray-color); + line-height: 1.5; + word-break: break-word; + flex-grow: 1; +} + +.note-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 15px; + border-top: 1px solid var(--light-gray); + font-size: 0.9rem; + color: var(--gray-color); +} + +.note-author { + font-weight: 600; + color: var(--primary-color); +} + +.note-date { + text-align: right; +} + +/* Состояния */ +.loading { + text-align: center; + padding: 40px; + color: var(--gray-color); +} + +.loading i { + font-size: 3rem; + margin-bottom: 15px; + color: var(--primary-color); +} + +.empty-state { + text-align: center; + padding: 50px 20px; + color: var(--gray-color); + display: none; +} + +.empty-state i { + font-size: 4rem; + margin-bottom: 15px; + color: var(--light-gray); +} + +.empty-state h3 { + margin-bottom: 10px; + color: var(--dark-color); +} + +/* Подвал */ +.footer { + text-align: center; + padding: 20px; + background-color: var(--light-gray); + color: var(--gray-color); + border-top: 1px solid var(--border-color); +} + +#backend-status { + font-weight: 600; + padding: 4px 10px; + border-radius: 4px; +} + +.backend-connected { + background-color: rgba(76, 201, 240, 0.2); + color: #0c5460; +} + +.backend-disconnected { + background-color: rgba(247, 37, 133, 0.2); + color: #721c24; +} + +.api-info { + font-size: 0.9rem; + margin-top: 5px; +} + +/* Модальное окно */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal-content { + background-color: white; + padding: 30px; + border-radius: var(--border-radius); + max-width: 500px; + width: 90%; + box-shadow: var(--shadow-hover); +} + +.modal-content h3 { + margin-bottom: 15px; + color: var(--danger-color); +} + +.modal-content p { + margin-bottom: 25px; + line-height: 1.6; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 15px; +} + +/* Уведомления */ +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 15px 25px; + border-radius: var(--border-radius); + background-color: var(--primary-color); + color: white; + box-shadow: var(--shadow); + transform: translateY(100px); + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + z-index: 1001; +} + +.notification.show { + transform: translateY(0); + opacity: 1; +} + +.notification.success { + background-color: var(--success-color); +} + +.notification.error { + background-color: var(--danger-color); +} + +.notification.warning { + background-color: var(--warning-color); +} diff --git a/frontend/src/public/html/index.html b/frontend/src/public/html/index.html index 89d72e4..fcea235 100644 --- a/frontend/src/public/html/index.html +++ b/frontend/src/public/html/index.html @@ -1,12 +1,136 @@ - +
- - -Вот мы и встретились... снова.
+Загрузка заметок...
+Создайте свою первую заметку, используя форму выше.
+${escapeHtml(shortText)}
+ +