Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4d4cd685 |
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
notes.db
|
||||||
7
backend/package-lock.json
generated
7
backend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1018,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,256 +1,182 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { json } from 'express';
|
import { json } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { fileURLToPath } from 'url';
|
import sqlite3 from 'sqlite3';
|
||||||
import { dirname, join } from 'path';
|
import { open } from 'sqlite';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
// Создаем экземпляр приложения Express
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Подключаем наши middleware (промежуточные обработчики маршрутов)
|
|
||||||
|
|
||||||
// middlware CORS разрешает запросы с других доменов. Без этого браузер
|
|
||||||
// запретит фронтенду общаться с бекендом!
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: 'http://localhost:8080', // разрешаем только с этого домена
|
origin: 'http://localhost:8080',
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE'], // разрешаем только эти методы
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type'] // разрешаем только эти заголовки
|
allowedHeaders: ['Content-Type']
|
||||||
}));
|
}));
|
||||||
// JSON парсер позволяет обрабатывать JSON в теле запросов
|
|
||||||
app.use(json());
|
app.use(json());
|
||||||
|
|
||||||
// Создаем массив для хранения заметок в памяти
|
let db;
|
||||||
// В реальном приложении здесь была бы база данных
|
|
||||||
let notes = [];
|
|
||||||
|
|
||||||
// Переменная для генерации уникальных ID
|
async function initializeDatabase() {
|
||||||
// Каждая новая заметка получит ID на 1 больше предыдущей
|
db = await open({
|
||||||
let noteIdCounter = 1;
|
filename: './notes.db',
|
||||||
|
driver: sqlite3.Database
|
||||||
// ==================== РАЗДЕЛ: РОУТЫ 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 // количество заметок
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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: 'Ошибка при получении заметок'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// РОУТ 2: Получение одной заметки по ID (READ)
|
app.get('/notes/:id', async (req, res) => {
|
||||||
// Метод: GET
|
try {
|
||||||
// Адрес: /notes/:id
|
const note = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id);
|
||||||
// :id - это параметр маршрута (динамическая часть URL)
|
|
||||||
app.get('/notes/:id', function(request, response) {
|
|
||||||
/*
|
|
||||||
Эта функция ищет заметку по ID
|
|
||||||
request.params.id - получаем ID из URL
|
|
||||||
*/
|
|
||||||
console.log('Получен запрос на получение заметки с ID:', request.params.id);
|
|
||||||
|
|
||||||
// Преобразуем ID из строки в число
|
if (!note) {
|
||||||
const noteId = parseInt(request.params.id);
|
return res.status(404).json({
|
||||||
|
|
||||||
// Ищем заметку в массиве по 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({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Заметка с таким ID не найдена'
|
message: 'Заметка с таким ID не найдена'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если заметка найдена - отправляем ее клиенту
|
res.status(200).json({
|
||||||
response.status(200).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
data: foundNote
|
data: note
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Ошибка при получении заметки'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// РОУТ 3: Создание новой заметки (CREATE)
|
app.post('/notes', async (req, res) => {
|
||||||
// Метод: POST
|
const { title, text, author } = req.body;
|
||||||
// Адрес: /notes
|
|
||||||
app.post('/notes', function(request, response) {
|
|
||||||
/*
|
|
||||||
Эта функция создает новую заметку
|
|
||||||
request.body - содержит данные, отправленные клиентом
|
|
||||||
*/
|
|
||||||
console.log('Получен запрос на создание заметки');
|
|
||||||
|
|
||||||
// Получаем данные из тела запроса
|
|
||||||
const title = request.body.title;
|
|
||||||
const text = request.body.text;
|
|
||||||
const author = request.body.author;
|
|
||||||
|
|
||||||
// Проверяем, все ли обязательные поля заполнены
|
|
||||||
if (!title || !text || !author) {
|
if (!title || !text || !author) {
|
||||||
console.log('Ошибка: не все поля заполнены');
|
return res.status(400).json({
|
||||||
return response.status(400).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Пожалуйста, заполните все поля: title, text, author'
|
message: 'Пожалуйста, заполните все поля: title, text, author'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем новую заметку
|
try {
|
||||||
const newNote = {
|
const result = await db.run(
|
||||||
id: noteIdCounter, // присваиваем уникальный ID
|
'INSERT INTO notes (title, text, author) VALUES (?, ?, ?)',
|
||||||
title: title, // заголовок из запроса
|
[title, text, author]
|
||||||
text: text, // текст заметки из запроса
|
);
|
||||||
author: author, // автор из запроса
|
|
||||||
createdAt: new Date(), // дата создания (текущее время)
|
|
||||||
updatedAt: new Date() // дата обновления (пока равна дате создания)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Добавляем заметку в массив
|
const newNote = await db.get('SELECT * FROM notes WHERE id = ?', result.lastID);
|
||||||
notes.push(newNote);
|
|
||||||
// Увеличиваем счетчик ID для следующей заметки
|
|
||||||
noteIdCounter++;
|
|
||||||
|
|
||||||
console.log('Создана новая заметка с ID:', newNote.id);
|
res.status(201).json({
|
||||||
|
|
||||||
// Отправляем ответ с созданной заметкой
|
|
||||||
// status(201) - код 201 означает "Создано"
|
|
||||||
response.status(201).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Заметка успешно создана',
|
message: 'Заметка успешно создана',
|
||||||
data: newNote
|
data: newNote
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Ошибка при создании заметки'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// РОУТ 4: Обновление существующей заметки (UPDATE)
|
app.put('/notes/:id', async (req, res) => {
|
||||||
// Метод: PUT
|
try {
|
||||||
// Адрес: /notes/:id
|
const existingNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.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);
|
if (!existingNote) {
|
||||||
|
return res.status(404).json({
|
||||||
// Ищем индекс заметки в массиве
|
|
||||||
const noteIndex = notes.findIndex(function(note) {
|
|
||||||
return note.id === noteId;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если заметка не найдена
|
|
||||||
if (noteIndex === -1) {
|
|
||||||
console.log('Заметка для обновления не найдена');
|
|
||||||
return response.status(404).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Заметка с таким ID не найдена'
|
message: 'Заметка с таким ID не найдена'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем данные из запроса
|
const updatedTitle = req.body.title || existingNote.title;
|
||||||
// Если какое-то поле не передано, используем старое значение
|
const updatedText = req.body.text || existingNote.text;
|
||||||
const updatedTitle = request.body.title || notes[noteIndex].title;
|
const updatedAuthor = req.body.author || existingNote.author;
|
||||||
const updatedText = request.body.text || notes[noteIndex].text;
|
|
||||||
const updatedAuthor = request.body.author || notes[noteIndex].author;
|
|
||||||
|
|
||||||
// Обновляем заметку
|
await db.run(
|
||||||
notes[noteIndex] = {
|
'UPDATE notes SET title = ?, text = ?, author = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
...notes[noteIndex], // копируем все старые поля
|
[updatedTitle, updatedText, updatedAuthor, req.params.id]
|
||||||
title: updatedTitle, // обновляем заголовок
|
);
|
||||||
text: updatedText, // обновляем текст
|
|
||||||
author: updatedAuthor, // обновляем автора
|
|
||||||
updatedAt: new Date() // обновляем дату изменения
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Заметка с ID', noteId, 'обновлена');
|
const updatedNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id);
|
||||||
|
|
||||||
// Отправляем обновленную заметку
|
res.status(200).json({
|
||||||
response.status(200).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Заметка успешно обновлена',
|
message: 'Заметка успешно обновлена',
|
||||||
data: notes[noteIndex]
|
data: updatedNote
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Ошибка при обновлении заметки'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// РОУТ 5: Удаление заметки (DELETE)
|
app.delete('/notes/:id', async (req, res) => {
|
||||||
// Метод: DELETE
|
try {
|
||||||
// Адрес: /notes/:id
|
const existingNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id);
|
||||||
app.delete('/notes/:id', function(request, response) {
|
|
||||||
/*
|
|
||||||
Эта функция удаляет заметку по ID
|
|
||||||
*/
|
|
||||||
console.log('Получен запрос на удаление заметки с ID:', request.params.id);
|
|
||||||
|
|
||||||
const noteId = parseInt(request.params.id);
|
if (!existingNote) {
|
||||||
|
return res.status(404).json({
|
||||||
// Ищем индекс заметки в массиве
|
|
||||||
const noteIndex = notes.findIndex(function(note) {
|
|
||||||
return note.id === noteId;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если заметка не найдена
|
|
||||||
if (noteIndex === -1) {
|
|
||||||
console.log('Заметка для удаления не найдена');
|
|
||||||
return response.status(404).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Заметка с таким ID не найдена'
|
message: 'Заметка с таким ID не найдена'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем заметку из массива
|
await db.run('DELETE FROM notes WHERE id = ?', req.params.id);
|
||||||
// splice(index, 1) удаляет 1 элемент начиная с позиции index
|
|
||||||
const deletedNote = notes.splice(noteIndex, 1)[0];
|
|
||||||
|
|
||||||
console.log('Заметка с ID', noteId, 'удалена');
|
res.status(200).json({
|
||||||
|
|
||||||
// Отправляем подтверждение удаления
|
|
||||||
response.status(200).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Заметка успешно удалена',
|
message: 'Заметка успешно удалена',
|
||||||
data: deletedNote
|
data: existingNote
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Ошибка при удалении заметки'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// РОУТ 7: Корневой маршрут (для проверки работы сервера)
|
app.get('/', (_, res) => {
|
||||||
// Метод: GET
|
res.json({
|
||||||
// Адрес: /
|
|
||||||
app.get('/', function(_, response) {
|
|
||||||
/*
|
|
||||||
Простой маршрут для проверки, что сервер работает
|
|
||||||
*/
|
|
||||||
response.json({
|
|
||||||
message: 'Добро пожаловать в API для заметок!',
|
message: 'Добро пожаловать в API для заметок!',
|
||||||
endpoints: {
|
endpoints: {
|
||||||
getAllNotes: 'GET /notes',
|
getAllNotes: 'GET /notes',
|
||||||
getOneNote: 'GET /notes/:id',
|
getOneNote: 'GET /notes/:id',
|
||||||
createNote: 'POST /notes',
|
createNote: 'POST /notes',
|
||||||
updateNote: 'PUT /notes/:id',
|
updateNote: 'PUT /notes/:id',
|
||||||
deleteNote: 'DELETE /notes/:id',
|
deleteNote: 'DELETE /notes/:id'
|
||||||
docs: 'GET /docs'
|
}
|
||||||
},
|
|
||||||
instructions: 'Используйте Postman или curl для тестирования API'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Экспортируем приложение для использования в server.js
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,136 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru" >
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Document</title>
|
<title>Notes App - Управление заметками</title>
|
||||||
|
<link rel="stylesheet" href="/css/normalize.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Привет с фронта!</h1>
|
<div class="container">
|
||||||
<p>Вот мы и встретились... снова.</p>
|
<!-- Шапка приложения -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="fas fa-sticky-note"></i>
|
||||||
|
<h1>Мои Заметки</h1>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<span id="notes-count">0 заметок</span>
|
||||||
|
<button class="btn btn-refresh" id="refresh-btn">
|
||||||
|
<i class="fas fa-sync-alt"></i> Обновить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Форма для создания/редактирования заметки -->
|
||||||
|
<section class="note-form-section">
|
||||||
|
<h2 id="form-title">Создать новую заметку</h2>
|
||||||
|
<form id="note-form" class="note-form">
|
||||||
|
<input type="hidden" id="note-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Заголовок</label>
|
||||||
|
<input type="text" id="title" required
|
||||||
|
placeholder="Введите заголовок заметки...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="text">Текст заметки</label>
|
||||||
|
<textarea id="text" rows="4" required
|
||||||
|
placeholder="Введите текст заметки..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="author">Автор</label>
|
||||||
|
<input type="text" id="author" required
|
||||||
|
placeholder="Ваше имя">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="save-btn">
|
||||||
|
<i class="fas fa-save"></i> Сохранить заметку
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="clear-btn">
|
||||||
|
<i class="fas fa-times"></i> Очистить форму
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Список заметок -->
|
||||||
|
<section class="notes-section">
|
||||||
|
<h2>Все заметки</h2>
|
||||||
|
|
||||||
|
<!-- Фильтры и поиск -->
|
||||||
|
<div class="filters">
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<input type="text" id="search-input"
|
||||||
|
placeholder="Поиск по заметкам...">
|
||||||
|
</div>
|
||||||
|
<div class="sort-options">
|
||||||
|
<select id="sort-select">
|
||||||
|
<option value="newest">Сначала новые</option>
|
||||||
|
<option value="oldest">Сначала старые</option>
|
||||||
|
<option value="title_asc">По заголовку (А-Я)</option>
|
||||||
|
<option value="title_desc">По заголовку (Я-А)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Состояние загрузки -->
|
||||||
|
<div id="loading" class="loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<p>Загрузка заметок...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Контейнер для заметок -->
|
||||||
|
<div id="notes-container" class="notes-container">
|
||||||
|
<!-- Заметки будут вставляться сюда динамически -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сообщение если заметок нет -->
|
||||||
|
<div id="empty-state" class="empty-state">
|
||||||
|
<i class="fas fa-clipboard"></i>
|
||||||
|
<h3>Нет заметок</h3>
|
||||||
|
<p>Создайте свою первую заметку, используя форму выше.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Подвал -->
|
||||||
|
<footer class="footer">
|
||||||
|
<p>Notes App © 2025 | Backend: <span id="backend-status">Не подключен</span></p>
|
||||||
|
<p class="api-info" id="api-info"></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно подтверждения удаления -->
|
||||||
|
<div id="delete-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Подтверждение удаления</h3>
|
||||||
|
<p>Вы уверены, что хотите удалить заметку "<span id="delete-note-title"></span>"?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="confirm-delete" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i> Удалить
|
||||||
|
</button>
|
||||||
|
<button id="cancel-delete" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Уведомления -->
|
||||||
|
<div id="notification" class="notification">
|
||||||
|
<span id="notification-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Подключаем JavaScript -->
|
||||||
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
485
frontend/src/public/js/app.js
Normal file
485
frontend/src/public/js/app.js
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
// Основной JavaScript файл для приложения заметок (упрощённая версия)
|
||||||
|
|
||||||
|
// Глобальные переменные состояния
|
||||||
|
var backendUrl = null;
|
||||||
|
var notes = [];
|
||||||
|
var noteToDelete = null;
|
||||||
|
|
||||||
|
// DOM элементы
|
||||||
|
var noteForm, noteIdInput, titleInput, textInput, authorInput;
|
||||||
|
var saveBtn, clearBtn, formTitle;
|
||||||
|
var notesContainer, loadingElement, emptyState, notesCount;
|
||||||
|
var refreshBtn, searchInput, sortSelect;
|
||||||
|
var deleteModal, deleteNoteTitle, confirmDeleteBtn, cancelDeleteBtn;
|
||||||
|
var notification, notificationText, backendStatus, apiInfo;
|
||||||
|
|
||||||
|
// Инициализация приложения после загрузки DOM
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeElements();
|
||||||
|
setupEventListeners();
|
||||||
|
initApp();
|
||||||
|
console.log('Notes App инициализирован');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Инициализация DOM элементов
|
||||||
|
function initializeElements() {
|
||||||
|
// Форма
|
||||||
|
noteForm = document.getElementById('note-form');
|
||||||
|
noteIdInput = document.getElementById('note-id');
|
||||||
|
titleInput = document.getElementById('title');
|
||||||
|
textInput = document.getElementById('text');
|
||||||
|
authorInput = document.getElementById('author');
|
||||||
|
saveBtn = document.getElementById('save-btn');
|
||||||
|
clearBtn = document.getElementById('clear-btn');
|
||||||
|
formTitle = document.getElementById('form-title');
|
||||||
|
|
||||||
|
// Контейнеры и состояние
|
||||||
|
notesContainer = document.getElementById('notes-container');
|
||||||
|
loadingElement = document.getElementById('loading');
|
||||||
|
emptyState = document.getElementById('empty-state');
|
||||||
|
notesCount = document.getElementById('notes-count');
|
||||||
|
|
||||||
|
// Кнопки и фильтры
|
||||||
|
refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
searchInput = document.getElementById('search-input');
|
||||||
|
sortSelect = document.getElementById('sort-select');
|
||||||
|
|
||||||
|
// Модальное окно
|
||||||
|
deleteModal = document.getElementById('delete-modal');
|
||||||
|
deleteNoteTitle = document.getElementById('delete-note-title');
|
||||||
|
confirmDeleteBtn = document.getElementById('confirm-delete');
|
||||||
|
cancelDeleteBtn = document.getElementById('cancel-delete');
|
||||||
|
|
||||||
|
// Уведомления
|
||||||
|
notification = document.getElementById('notification');
|
||||||
|
notificationText = document.getElementById('notification-text');
|
||||||
|
|
||||||
|
// Статус
|
||||||
|
backendStatus = document.getElementById('backend-status');
|
||||||
|
apiInfo = document.getElementById('api-info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная инициализация приложения
|
||||||
|
async function initApp() {
|
||||||
|
try {
|
||||||
|
await loadConfig();
|
||||||
|
await loadNotes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициализации приложения:', error);
|
||||||
|
showNotification('Не удалось инициализировать приложение', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка конфигурации
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
var response = await fetch('/config.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = await response.json();
|
||||||
|
backendUrl = config.backendUrl;
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
throw new Error('URL бэкенда не указан в конфигурации');
|
||||||
|
}
|
||||||
|
|
||||||
|
apiInfo.textContent = 'API: ' + backendUrl;
|
||||||
|
await checkBackendConnection();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки конфигурации:', error);
|
||||||
|
showNotification('Ошибка загрузки конфигурации: ' + error.message, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка соединения с бэкендом
|
||||||
|
async function checkBackendConnection() {
|
||||||
|
try {
|
||||||
|
var response = await fetch(backendUrl + '/');
|
||||||
|
if (response.ok) {
|
||||||
|
backendStatus.textContent = 'Подключен';
|
||||||
|
backendStatus.className = 'backend-connected';
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
backendStatus.textContent = 'Ошибка подключения';
|
||||||
|
backendStatus.className = 'backend-disconnected';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
backendStatus.textContent = 'Не подключен';
|
||||||
|
backendStatus.className = 'backend-disconnected';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка обработчиков событий
|
||||||
|
function setupEventListeners() {
|
||||||
|
noteForm.addEventListener('submit', handleFormSubmit);
|
||||||
|
clearBtn.addEventListener('click', clearForm);
|
||||||
|
refreshBtn.addEventListener('click', loadNotes);
|
||||||
|
searchInput.addEventListener('input', filterNotes);
|
||||||
|
sortSelect.addEventListener('change', sortNotes);
|
||||||
|
confirmDeleteBtn.addEventListener('click', deleteNote);
|
||||||
|
cancelDeleteBtn.addEventListener('click', closeDeleteModal);
|
||||||
|
|
||||||
|
deleteModal.addEventListener('click', function(e) {
|
||||||
|
if (e.target === deleteModal) {
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && deleteModal.style.display === 'flex') {
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка заметок с сервера
|
||||||
|
async function loadNotes() {
|
||||||
|
if (!backendUrl) {
|
||||||
|
showNotification('Конфигурация не загружена. Пожалуйста, обновите страницу.', 'error');
|
||||||
|
showLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch(backendUrl + '/notes');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('HTTP ошибка! статус: ' + response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
notes = result.data;
|
||||||
|
renderNotes();
|
||||||
|
updateNotesCount();
|
||||||
|
showNotification('Загружено ' + result.count + ' заметок', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Не удалось загрузить заметки');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки заметок:', error);
|
||||||
|
showNotification('Ошибка загрузки: ' + error.message, 'error');
|
||||||
|
renderNotes();
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
checkBackendConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображение списка заметок
|
||||||
|
function renderNotes() {
|
||||||
|
if (!notes || notes.length === 0) {
|
||||||
|
notesContainer.innerHTML = '';
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyState.style.display = 'none';
|
||||||
|
|
||||||
|
var filteredNotes = filterNotesBySearch();
|
||||||
|
filteredNotes = sortNotesList(filteredNotes);
|
||||||
|
|
||||||
|
var notesHTML = filteredNotes.map(function(note) {
|
||||||
|
return createNoteHTML(note);
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
notesContainer.innerHTML = notesHTML;
|
||||||
|
addNoteActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание HTML для одной заметки
|
||||||
|
function createNoteHTML(note) {
|
||||||
|
var date = new Date(note.createdAt);
|
||||||
|
var formattedDate = date.toLocaleDateString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
var shortText = note.text.length > 100
|
||||||
|
? note.text.substring(0, 100) + '...'
|
||||||
|
: note.text;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="note-card" data-id="${note.id}">
|
||||||
|
<div class="note-header">
|
||||||
|
<h3 class="note-title">${escapeHtml(note.title)}</h3>
|
||||||
|
<div class="note-actions">
|
||||||
|
<button class="note-action-btn edit" title="Редактировать">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="note-action-btn delete" title="Удалить">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="note-text">${escapeHtml(shortText)}</p>
|
||||||
|
<div class="note-footer">
|
||||||
|
<div class="note-author">
|
||||||
|
<i class="fas fa-user"></i> ${escapeHtml(note.author)}
|
||||||
|
</div>
|
||||||
|
<div class="note-date" title="${date.toLocaleString('ru-RU')}">
|
||||||
|
<i class="far fa-calendar"></i> ${formattedDate}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление обработчиков для кнопок действий в заметках
|
||||||
|
function addNoteActionListeners() {
|
||||||
|
var editButtons = document.querySelectorAll('.note-action-btn.edit');
|
||||||
|
editButtons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
var noteCard = e.target.closest('.note-card');
|
||||||
|
var noteId = parseInt(noteCard.dataset.id);
|
||||||
|
editNote(noteId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var deleteButtons = document.querySelectorAll('.note-action-btn.delete');
|
||||||
|
deleteButtons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
var noteCard = e.target.closest('.note-card');
|
||||||
|
var noteId = parseInt(noteCard.dataset.id);
|
||||||
|
var noteTitle = noteCard.querySelector('.note-title').textContent;
|
||||||
|
openDeleteModal(noteId, noteTitle);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редактирование заметки
|
||||||
|
function editNote(noteId) {
|
||||||
|
var note = notes.find(function(n) {
|
||||||
|
return n.id === noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!note) return;
|
||||||
|
|
||||||
|
noteIdInput.value = note.id;
|
||||||
|
titleInput.value = note.title;
|
||||||
|
textInput.value = note.text;
|
||||||
|
authorInput.value = note.author;
|
||||||
|
|
||||||
|
formTitle.textContent = 'Редактировать заметку';
|
||||||
|
saveBtn.innerHTML = '<i class="fas fa-save"></i> Обновить заметку';
|
||||||
|
|
||||||
|
titleInput.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
titleInput.focus();
|
||||||
|
|
||||||
|
showNotification('Редактирование заметки: "' + note.title + '"', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открытие модального окна удаления
|
||||||
|
function openDeleteModal(noteId, noteTitle) {
|
||||||
|
noteToDelete = noteId;
|
||||||
|
deleteNoteTitle.textContent = noteTitle;
|
||||||
|
deleteModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрытие модального окна удаления
|
||||||
|
function closeDeleteModal() {
|
||||||
|
deleteModal.style.display = 'none';
|
||||||
|
noteToDelete = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаление заметки
|
||||||
|
async function deleteNote() {
|
||||||
|
if (!noteToDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch(backendUrl + '/notes/' + noteToDelete, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('Заметка удалена: "' + result.data.title + '"', 'success');
|
||||||
|
loadNotes();
|
||||||
|
closeDeleteModal();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Не удалось удалить заметку');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления заметки:', error);
|
||||||
|
showNotification('Ошибка удаления: ' + error.message, 'error');
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка отправки формы
|
||||||
|
async function handleFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
showNotification('Конфигурация не загружена. Пожалуйста, обновите страницу.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteData = {
|
||||||
|
title: titleInput.value.trim(),
|
||||||
|
text: textInput.value.trim(),
|
||||||
|
author: authorInput.value.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!noteData.title || !noteData.text || !noteData.author) {
|
||||||
|
showNotification('Пожалуйста, заполните все поля', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteId = noteIdInput.value;
|
||||||
|
var isEditing = !!noteId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response, result;
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
response = await fetch(backendUrl + '/notes/' + noteId, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(noteData)
|
||||||
|
});
|
||||||
|
|
||||||
|
result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('Заметка обновлена: "' + noteData.title + '"', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Не удалось обновить заметку');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response = await fetch(backendUrl + '/notes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(noteData)
|
||||||
|
});
|
||||||
|
|
||||||
|
result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('Заметка создана: "' + noteData.title + '"', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Не удалось создать заметку');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearForm();
|
||||||
|
loadNotes();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка сохранения заметки:', error);
|
||||||
|
showNotification('Ошибка сохранения: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистка формы
|
||||||
|
function clearForm() {
|
||||||
|
noteIdInput.value = '';
|
||||||
|
titleInput.value = '';
|
||||||
|
textInput.value = '';
|
||||||
|
authorInput.value = '';
|
||||||
|
|
||||||
|
formTitle.textContent = 'Создать новую заметку';
|
||||||
|
saveBtn.innerHTML = '<i class="fas fa-save"></i> Сохранить заметку';
|
||||||
|
|
||||||
|
titleInput.focus();
|
||||||
|
showNotification('Форма очищена', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтрация заметок по поисковому запросу
|
||||||
|
function filterNotesBySearch() {
|
||||||
|
var searchTerm = searchInput.value.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
return notes.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes.filter(function(note) {
|
||||||
|
return note.title.toLowerCase().includes(searchTerm) ||
|
||||||
|
note.text.toLowerCase().includes(searchTerm) ||
|
||||||
|
note.author.toLowerCase().includes(searchTerm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортировка заметок
|
||||||
|
function sortNotesList(notesArray) {
|
||||||
|
var sortValue = sortSelect.value;
|
||||||
|
|
||||||
|
return notesArray.slice().sort(function(a, b) {
|
||||||
|
switch (sortValue) {
|
||||||
|
case 'newest':
|
||||||
|
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||||
|
case 'oldest':
|
||||||
|
return new Date(a.createdAt) - new Date(b.createdAt);
|
||||||
|
case 'title_asc':
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
case 'title_desc':
|
||||||
|
return b.title.localeCompare(a.title);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики для фильтрации и сортировки
|
||||||
|
function filterNotes() {
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortNotes() {
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление счетчика заметок
|
||||||
|
function updateNotesCount() {
|
||||||
|
var count = notes.length;
|
||||||
|
notesCount.textContent = count + ' ' + getRussianPlural(count, 'заметка', 'заметки', 'заметок');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательные функции
|
||||||
|
function showLoading(show) {
|
||||||
|
loadingElement.style.display = show ? 'block' : 'none';
|
||||||
|
notesContainer.style.display = show ? 'none' : 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type) {
|
||||||
|
notificationText.textContent = message;
|
||||||
|
notification.className = 'notification show ' + type;
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRussianPlural(number, one, few, many) {
|
||||||
|
var n = Math.abs(number) % 100;
|
||||||
|
var n1 = n % 10;
|
||||||
|
|
||||||
|
if (n > 10 && n < 20) return many;
|
||||||
|
if (n1 > 1 && n1 < 5) return few;
|
||||||
|
if (n1 === 1) return one;
|
||||||
|
return many;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// Какой-то скрипт на JS, просто чтобы был
|
|
||||||
|
|
||||||
function displayMessage() {
|
|
||||||
document.getElementById('message').innerText = 'Hello World';
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = displayMessage;
|
|
||||||
Reference in New Issue
Block a user