Compare commits

1 Commits

Author SHA1 Message Date
ca4d4cd685 feat: frontend, sqlite integration 2025-12-26 21:05:11 +03:00
8 changed files with 1254 additions and 224 deletions

1
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
notes.db

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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 // количество заметок
});
});
// РОУТ 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;
async function initializeDatabase() {
db = await open({
filename: './notes.db',
driver: sqlite3.Database
});
// Если заметка не найдена
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);
// Получаем данные из тела запроса
const title = request.body.title;
const text = request.body.text;
const author = request.body.author;
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;
// Проверяем, все ли обязательные поля заполнены
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() // дата обновления (пока равна дате создания)
};
try {
const result = await db.run(
'INSERT INTO notes (title, text, author) VALUES (?, ?, ?)',
[title, text, author]
);
// Добавляем заметку в массив
notes.push(newNote);
// Увеличиваем счетчик ID для следующей заметки
noteIdCounter++;
const newNote = await db.get('SELECT * FROM notes WHERE id = ?', result.lastID);
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({
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);
app.put('/notes/:id', async (req, res) => {
try {
const existingNote = await db.get('SELECT * FROM notes WHERE id = ?', req.params.id);
const noteId = parseInt(request.params.id);
if (!existingNote) {
return res.status(404).json({
success: false,
message: 'Заметка с таким ID не найдена'
});
}
// Ищем индекс заметки в массиве
const noteIndex = notes.findIndex(function(note) {
return note.id === noteId;
});
const updatedTitle = req.body.title || existingNote.title;
const updatedText = req.body.text || existingNote.text;
const updatedAuthor = req.body.author || existingNote.author;
// Если заметка не найдена
if (noteIndex === -1) {
console.log('Заметка для удаления не найдена');
return response.status(404).json({
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;

View File

@@ -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);
}

View File

@@ -1,12 +1,136 @@
<!DOCTYPE html>
<html lang="ru" >
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
<h1>Привет с фронта!</h1>
<p>Вот мы и встретились... снова.</p>
<div class="container">
<!-- Шапка приложения -->
<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 &copy; 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>
</html>

View 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;
}

View File

@@ -1,7 +0,0 @@
// Какой-то скрипт на JS, просто чтобы был
function displayMessage() {
document.getElementById('message').innerText = 'Hello World';
}
window.onload = displayMessage;