/
greenfielder
/
quicknotes
Обзор
Документация
Войти
/
greenfielder
/
quicknotes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
web/app.js
236 строк
7 KB
Dmitry Savenko
mvp version 0.0.1
25 июн 2026, 14:42
25 июн 2026, 14:42
96abad3
Код
Авторство
О чём код?
const app = document.getElementById('app'); const header = document.getElementById('header'); const userLoginEl = document.getElementById('userLogin'); let currentUser = null; async function api(path, opts = {}) { const res = await fetch(path, { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) }, ...opts, }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (e) {} if (!res.ok) { const err = new Error((data && data.error) || res.statusText); err.status = res.status; throw err; } return data; } function renderTemplate(id) { const tpl = document.getElementById(id); app.innerHTML = ''; app.appendChild(tpl.content.cloneNode(true)); } function navigate(hash) { if (location.hash !== hash) location.hash = hash; else route(); } async function checkAuth() { try { currentUser = await api('/api/me'); userLoginEl.textContent = currentUser.login; header.classList.remove('hidden'); return true; } catch (e) { currentUser = null; header.classList.add('hidden'); return false; } } async function route() { const hash = location.hash || '#/login'; const authed = await checkAuth(); if (hash === '#/login' || hash === '#/register' || hash === '' || hash === '#') { if (authed) { navigate('#/projects'); return; } if (hash === '#/register') renderRegister(); else renderLogin(); return; } if (!authed) { navigate('#/login'); return; } if (hash === '#/projects') { renderProjects(); } else if (hash.startsWith('#/project/')) { const id = hash.split('/')[2]; renderProject(id); } else { navigate('#/projects'); } } function renderLogin() { renderTemplate('tpl-login'); const form = document.getElementById('loginForm'); const errEl = document.getElementById('loginError'); form.addEventListener('submit', async (e) => { e.preventDefault(); errEl.textContent = ''; const fd = new FormData(form); try { await api('/api/login', { method: 'POST', body: JSON.stringify({ login: fd.get('login'), password: fd.get('password'), remember: fd.get('remember') === 'on', }), }); navigate('#/projects'); } catch (err) { errEl.textContent = err.message; } }); } function renderRegister() { renderTemplate('tpl-register'); const form = document.getElementById('registerForm'); const errEl = document.getElementById('registerError'); form.addEventListener('submit', async (e) => { e.preventDefault(); errEl.textContent = ''; const fd = new FormData(form); try { await api('/api/register', { method: 'POST', body: JSON.stringify({ login: fd.get('login'), email: fd.get('email'), password: fd.get('password'), }), }); // Сразу логиним после регистрации await api('/api/login', { method: 'POST', body: JSON.stringify({ login: fd.get('login'), password: fd.get('password'), remember: true, }), }); navigate('#/projects'); } catch (err) { errEl.textContent = err.message; } }); } async function renderProjects() { renderTemplate('tpl-projects'); const tiles = document.getElementById('tiles'); const addTile = document.createElement('button'); addTile.className = 'tile tile-add'; addTile.innerHTML = '<div class="plus">+</div><div>Новый проект</div>'; addTile.addEventListener('click', async () => { addTile.disabled = true; try { const res = await api('/api/projects', { method: 'POST' }); navigate('#/project/' + res.id); } catch (e) { alert('Ошибка: ' + e.message); addTile.disabled = false; } }); tiles.appendChild(addTile); try { const list = await api('/api/projects'); list.forEach(p => { const a = document.createElement('a'); a.className = 'tile'; a.href = '#/project/' + p.id; const d = new Date(p.updatedAt); const dateStr = d.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric', }); a.innerHTML = ` <div class="tile-title">${escapeHtml(p.title || 'Без названия')}</div> <div class="tile-date">${dateStr}</div> `; tiles.appendChild(a); }); } catch (e) { tiles.innerHTML = '<div>Ошибка загрузки: ' + escapeHtml(e.message) + '</div>'; } } function renderProject(id) { renderTemplate('tpl-project'); const titleEl = document.getElementById('projectTitle'); const contentEl = document.getElementById('projectContent'); const statusEl = document.getElementById('saveStatus'); const deleteBtn = document.getElementById('deleteBtn'); let saveTimer = null; function setStatus(text, cls = '') { statusEl.textContent = text; statusEl.className = 'save-status ' + cls; } async function load() { try { const p = await api('/api/projects/' + id); titleEl.value = p.title || ''; contentEl.value = p.content || ''; setStatus('Сохранено'); } catch (e) { setStatus('Ошибка загрузки', 'error'); } } function scheduleSave(field, value) { setStatus('Сохранение...', 'saving'); clearTimeout(saveTimer); saveTimer = setTimeout(async () => { try { await api('/api/projects/' + id, { method: 'PUT', body: JSON.stringify({ [field]: value }), }); setStatus('Сохранено'); } catch (e) { setStatus('Ошибка сохранения', 'error'); } }, 800); } titleEl.addEventListener('input', () => scheduleSave('title', titleEl.value)); contentEl.addEventListener('input', () => scheduleSave('content', contentEl.value)); deleteBtn.addEventListener('click', async () => { if (!confirm('Удалить проект?')) return; try { await api('/api/projects/' + id, { method: 'DELETE' }); navigate('#/projects'); } catch (e) { alert('Ошибка: ' + e.message); } }); load(); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } document.getElementById('logoutBtn').addEventListener('click', async () => { try { await api('/api/logout', { method: 'POST' }); } catch (e) {} navigate('#/login'); }); window.addEventListener('hashchange', route); route();