/
Ormon
/
notebook-app
Обзор
Документация
Войти
/
Ormon
/
notebook-app
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/main.js
472 строки
16 KB
Sergey
новый проект
07 мар 2026, 22:26
07 мар 2026, 22:26
e6a8352
Код
Авторство
О чём код?
const API_BASE = 'http://localhost:8080/api'; // Point to API Gateway // State let notes = []; let tags = new Set(); let currentFilter = null; // null means 'All Notes' (based on Tag) let searchQuery = ''; let searchType = 'title'; let currentPage = 0; let totalPages = 1; const pageSize = 25; // DOM Elements const notesGrid = document.getElementById('notes-grid'); const tagList = document.getElementById('tag-list'); const btnNewNote = document.getElementById('btn-new-note'); const modal = document.getElementById('note-modal'); const form = document.getElementById('note-form'); const btnCloseModal = document.getElementById('btn-close-modal'); const btnCancel = document.getElementById('btn-cancel'); const btnEditNote = document.getElementById('btn-edit-note'); const noteView = document.getElementById('note-view'); // Search & Pagination Elements const searchInput = document.getElementById('search-input'); const searchTypeSelect = document.getElementById('search-type'); const paginationControls = document.getElementById('pagination-controls'); const btnPrevPage = document.getElementById('btn-prev-page'); const btnNextPage = document.getElementById('btn-next-page'); const pageInfo = document.getElementById('page-info'); // Inputs const inputTitle = document.getElementById('note-title'); const inputContent = document.getElementById('note-content'); const inputTags = document.getElementById('note-tags'); const inputFile = document.getElementById('note-file'); const fileStatus = document.getElementById('file-status'); // Read Mode View Elements const viewTitle = document.getElementById('view-title'); const viewDate = document.getElementById('view-date'); const viewTags = document.getElementById('view-tags'); const viewReminders = document.getElementById('view-reminders'); const viewContent = document.getElementById('view-content'); const viewAttachments = document.getElementById('view-attachments'); const editFilesList = document.getElementById('edit-files-list'); const newFilesList = document.getElementById('new-files-list'); const marqueeContainer = document.getElementById('marquee-container'); const marqueeContent = document.getElementById('marquee-content'); const rightColumn = document.getElementById('right-column'); const remindersList = document.getElementById('reminders-list'); // Init let currentNote = null; // Track note being viewed/edited let newFiles = []; async function init() { await fetchNotes(); renderSidebar(); // Poll for AI updates every 15 seconds setInterval(async () => { if (!searchQuery && !currentFilter) { await fetchNotes(); } }, 15000); } // Fetch Tags separately to ensure sidebar is always up to date async function fetchTags() { try { const res = await fetch(`${API_BASE}/tags`); const data = await res.json(); tags.clear(); data.forEach(t => tags.add(t.name)); renderSidebar(); } catch (e) { console.error("Failed to fetch tags", e); } } // Fetch Notes (Enhanced with Search and Pagination) async function fetchNotes() { try { let url = `${API_BASE}/notes/search?page=${currentPage}&size=${pageSize}`; if (searchQuery) url += `&query=${encodeURIComponent(searchQuery)}&type=${searchType}`; if (currentFilter) url += `&tag=${encodeURIComponent(currentFilter)}`; const res = await fetch(url); const data = await res.json(); notes = data.content; totalPages = data.totalPages; // Always fetch latest tags to keep sidebar in sync await fetchTags(); renderNotes(); renderPagination(); updateMarquee(notes); updateRightColumn(notes); } catch (e) { console.error("Failed to fetch notes", e); notesGrid.innerHTML = `<p style="color: var(--danger-color)">Could not connect to backend.</p>`; } } // Render Notes function renderNotes() { notesGrid.innerHTML = ''; if (notes.length === 0) { notesGrid.innerHTML = `<p style="color: var(--text-muted)">No notes found.</p>`; return; } notes.forEach(note => { const div = document.createElement('div'); div.className = 'note-card'; div.innerHTML = ` <h3>${escapeHtml(note.title)}</h3> <button class="delete-btn" onclick="deleteNote(${note.id})">×</button> <div class="date">${new Date(note.createdAt).toLocaleString()}</div> <div class="note-tags"> ${note.tags ? note.tags.map(t => `<span class="tag-badge">${escapeHtml(t)}</span>`).join('') : ''} </div> <div class="body">${escapeHtml(note.content).replace(/\n/g, '<br>')}</div> ${note.reminders && note.reminders.length > 0 ? `<div class="note-tags"> ${note.reminders.map(r => `<span class="reminder-badge">⏰ ${escapeHtml(r)}</span>`).join('')} </div>` : ''} ${note.fileIds && note.fileIds.length > 0 ? note.fileIds.map(fid => ` <div class="attachment-badge" onclick="downloadFile('${fid}')"> 📎 Attachment </div> `).join('') : ''} `; div.addEventListener('click', (e) => { if (!e.target.classList.contains('delete-btn') && !e.target.classList.contains('attachment-badge')) { openModal(note); } }); notesGrid.appendChild(div); }); } // Render Sidebar (Tags) function renderSidebar() { tagList.innerHTML = `<li class="${currentFilter === null ? 'active' : ''}" onclick="setFilter(null)">Last 25 notes</li>`; Array.from(tags).sort().forEach(tag => { tagList.innerHTML += `<li class="${currentFilter === tag ? 'active' : ''}" onclick="setFilter('${escapeHtml(tag)}')">#${escapeHtml(tag)}</li>`; }); } // Render Pagination function renderPagination() { if (totalPages <= 1) { paginationControls.classList.add('hidden'); return; } paginationControls.classList.remove('hidden'); pageInfo.textContent = `Page ${currentPage + 1} of ${totalPages}`; btnPrevPage.disabled = currentPage === 0; btnNextPage.disabled = currentPage >= totalPages - 1; } // API Helpers window.deleteNote = async function (id) { if (!confirm('Are you sure you want to delete this note?')) return; try { await fetch(`${API_BASE}/notes/${id}`, { method: 'DELETE' }); await fetchNotes(); } catch (e) { console.error('Delete failed', e); } }; window.setFilter = function (tag) { currentFilter = tag; currentPage = 0; fetchNotes(); }; window.downloadFile = function (fileId) { window.open(`${API_BASE}/files/${fileId}`, '_blank'); }; // Search Logic let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { searchQuery = e.target.value; currentPage = 0; fetchNotes(); }, 300); }); searchTypeSelect.addEventListener('change', (e) => { searchType = e.target.value; if (searchQuery) { currentPage = 0; fetchNotes(); } }); // Pagination Events btnPrevPage.addEventListener('click', () => { if (currentPage > 0) { currentPage--; fetchNotes(); } }); btnNextPage.addEventListener('click', () => { if (currentPage < totalPages - 1) { currentPage++; fetchNotes(); } }); // Modal Logic function openModal(note = null) { currentNote = note; modal.classList.remove('hidden'); if (note) { enterReadMode(note); } else { enterEditMode(); } } function getFilenameFromId(fid) { if (fid && fid.includes('_')) { return fid.substring(fid.indexOf('_') + 1); } return 'Attachment'; } function enterReadMode(note) { document.getElementById('modal-title').textContent = 'View Note'; noteView.classList.remove('hidden'); form.classList.add('hidden'); btnEditNote.classList.remove('hidden'); viewTitle.textContent = note.title; viewDate.textContent = new Date(note.createdAt).toLocaleString(); viewTags.innerHTML = note.tags ? note.tags.map(t => `<span class="tag-badge">#${escapeHtml(t)}</span>`).join('') : ''; viewReminders.innerHTML = ''; if (note.reminders && note.reminders.length > 0) { viewReminders.innerHTML = note.reminders.map(r => `<span class="reminder-badge">⏰ ${escapeHtml(r)}</span>`).join(''); } viewContent.innerHTML = escapeHtml(note.content).replace(/\n/g, '<br>'); viewAttachments.innerHTML = ''; if (note.fileIds && note.fileIds.length > 0) { viewAttachments.innerHTML = '<h4>Attachments</h4>'; note.fileIds.forEach(fid => { const div = document.createElement('div'); div.className = 'attachment-badge'; div.innerHTML = `📎 ${escapeHtml(getFilenameFromId(fid))}`; div.onclick = () => downloadFile(fid); viewAttachments.appendChild(div); }); } } function enterEditMode() { const isNew = !currentNote; document.getElementById('modal-title').textContent = isNew ? 'Create Note' : 'Edit Note'; noteView.classList.add('hidden'); form.classList.remove('hidden'); btnEditNote.classList.add('hidden'); document.getElementById('note-id').value = currentNote ? currentNote.id : ''; inputTitle.value = currentNote ? currentNote.title : ''; inputContent.value = currentNote ? currentNote.content : ''; inputTags.value = currentNote && currentNote.tags ? currentNote.tags.join(' ') : ''; inputFile.value = ''; newFiles = []; renderNewFilesList(); // Show existing files with removal option editFilesList.innerHTML = ''; if (currentNote && currentNote.fileIds && currentNote.fileIds.length > 0) { currentNote.fileIds.forEach(fid => { const div = document.createElement('div'); div.className = 'edit-file-item'; div.innerHTML = ` <span>📎 ${escapeHtml(getFilenameFromId(fid))}</span> <span class="remove-file" title="Remove attachment">×</span> `; div.querySelector('.remove-file').onclick = () => { currentNote.fileIds = currentNote.fileIds.filter(id => id !== fid); div.remove(); }; editFilesList.appendChild(div); }); } } function closeModal() { modal.classList.add('hidden'); currentNote = null; newFiles = []; // Clear new files on modal close renderNewFilesList(); // Update display } inputFile.addEventListener('change', (e) => { const files = Array.from(e.target.files); newFiles = [...newFiles, ...files]; renderNewFilesList(); inputFile.value = ''; // Reset to allow re-selecting same file }); function renderNewFilesList() { newFilesList.innerHTML = ''; if (newFiles.length > 0) { newFiles.forEach((file, index) => { const div = document.createElement('div'); div.className = 'edit-file-item new-file-item'; div.innerHTML = ` <span>📎 ${escapeHtml(file.name)} <small>(ready to upload)</small></span> <span class="remove-file" title="Remove">×</span> `; div.querySelector('.remove-file').onclick = () => { newFiles.splice(index, 1); renderNewFilesList(); }; newFilesList.appendChild(div); }); } if (newFiles.length === 0) { fileStatus.textContent = 'No files chosen'; } else { fileStatus.textContent = `${newFiles.length} files selected`; } } btnNewNote.addEventListener('click', () => openModal()); btnCloseModal.addEventListener('click', closeModal); btnCancel.addEventListener('click', closeModal); btnEditNote.addEventListener('click', () => enterEditMode()); form.addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('note-id').value; const title = inputTitle.value; const content = inputContent.value; const tagsArr = inputTags.value.split(/\s+/).map(t => t.trim()).filter(t => t); try { let url = `${API_BASE}/notes`; let method = 'POST'; const formData = new FormData(); formData.append('title', title); formData.append('content', content); tagsArr.forEach(t => formData.append('tags', t)); if (newFiles.length > 0) { newFiles.forEach(file => { formData.append('file', file); }); } if (id) { method = 'PUT'; url += `/${id}`; // If editing, use currentNote.fileIds (after possible removals) if (currentNote && currentNote.fileIds) { if (currentNote.fileIds.length === 0) { formData.append('fileIds', ''); // Ensure key exists to signal empty list } else { currentNote.fileIds.forEach(fid => formData.append('fileIds', fid)); } } } const options = { method: method, body: formData }; const response = await fetch(url, options); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const savedNote = await response.json(); currentNote = savedNote; // Update current note reference await fetchNotes(); // Refresh list in background enterReadMode(savedNote); // Switch to read mode with updated data } catch (err) { console.error("Save failed", err); alert("Could not save note."); } }); function updateMarquee(notes) { const today = new Date().toLocaleDateString('ru-RU'); // dd.mm.yyyy const todayShort = today.substring(0, 5); // dd.mm // Notes that have today's date or 'tomorrow' (for demo/testing) const todayNotes = notes.filter(n => n.reminders && n.reminders.some(r => r.includes(today) || r.includes(todayShort) || r.toLowerCase().includes('tomorrow')) ); if (todayNotes.length > 0) { marqueeContainer.classList.remove('hidden'); document.body.style.paddingBottom = '40px'; marqueeContent.innerHTML = todayNotes.map(n => ` <div class="marquee-item" onclick="openNoteById(${n.id})"> <span class="date-tag">TODAY</span> ${escapeHtml(n.title)} </div> `).join(''); } else { marqueeContainer.classList.add('hidden'); document.body.style.paddingBottom = '0'; } } function updateRightColumn(notes) { // Notes that have a time format hh:mm const timedNotes = notes.filter(n => n.reminders && n.reminders.some(r => /\d{1,2}:\d{2}/.test(r)) ); if (timedNotes.length > 0) { rightColumn.classList.remove('hidden'); remindersList.innerHTML = timedNotes.map(n => { const timeMatch = n.reminders.find(r => /\d{1,2}:\d{2}/.test(r)).match(/\d{1,2}:\d{2}/)[0]; return ` <div class="reminder-item" onclick="openNoteById(${n.id})"> <span class="time">⏰ ${timeMatch}</span> <span class="title">${escapeHtml(n.title)}</span> </div> `; }).join(''); } else { rightColumn.classList.add('hidden'); } } window.openNoteById = function (id) { const note = notes.find(n => n.id === id); if (note) { openModal(note); } }; // Utility function escapeHtml(unsafe) { return (unsafe || '').toString() .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } init();