feat: frontend, sqlite integration
This commit is contained in:
@@ -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>
|
||||
<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 © 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>
|
||||
|
||||
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