234 lines
11 KiB
HTML
234 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Notes — Yggdrasil</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='retro.css') }}">
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<a href="{{ url_for('home') }}">Home</a>
|
|
<a href="{{ url_for('sensors_page') }}">Sensors</a>
|
|
<a href="{{ url_for('notes_page') }}">Notes</a>
|
|
<a href="{{ url_for('clipboard_page') }}">Clipboard</a>
|
|
<a href="{{ url_for('calendar_page') }}">Calendar</a>
|
|
<a href="{{ url_for('notifications_page') }}">Notifications{% if unread_notifications %} <span class="nav-badge">{{ unread_notifications }}</span>{% endif %}</a>
|
|
<a href="{{ url_for('api_docs') }}">API</a>
|
|
<a href="{{ url_for('logout') }}">Log out</a>
|
|
<img class="nav-logo" src="{{ url_for('static', filename='yggdrasil-logo.png') }}" alt="Yggdrasil logo">
|
|
</nav>
|
|
<h1>Notes</h1>
|
|
|
|
<div class="panel">
|
|
<h2>New note</h2>
|
|
<form method="post" action="{{ url_for('notes_create') }}" data-note-id="new">
|
|
<label for="new-note-title">Title</label>
|
|
<input type="text" id="new-note-title" name="title" maxlength="200"
|
|
placeholder="Note title" required>
|
|
<label for="new-note-content">Text</label>
|
|
<textarea id="new-note-content" name="content" rows="3" maxlength="2000"
|
|
placeholder="Write a short note..."></textarea>
|
|
<button type="submit">[ Add note ]</button>
|
|
</form>
|
|
</div>
|
|
|
|
{% if not notes %}
|
|
<p class="hint">No notes yet. Add your first note above.</p>
|
|
{% endif %}
|
|
|
|
{% for note in notes %}
|
|
<section class="note-card" id="note-{{ note.id }}">
|
|
<form method="post" action="{{ url_for('notes_update', note_id=note.id) }}" data-note-id="{{ note.id }}">
|
|
<div class="note-title-row">
|
|
<label for="note-title-{{ note.id }}">Title</label>
|
|
<span class="note-save-status"></span>
|
|
<button class="note-menu-btn" type="button" aria-label="More options">⋮</button>
|
|
<div class="note-menu-dropdown hidden">
|
|
<button type="button" class="menu-delete" data-note-id="{{ note.id }}">Delete note</button>
|
|
</div>
|
|
</div>
|
|
<input type="text" id="note-title-{{ note.id }}" name="title"
|
|
value="{{ note.title }}" maxlength="200" required>
|
|
<label for="note-content-{{ note.id }}">Text</label>
|
|
<textarea id="note-content-{{ note.id }}" name="content" rows="2"
|
|
maxlength="2000">{{ note.content }}</textarea>
|
|
<div class="note-actions">
|
|
<span class="note-meta">{{ note.updated_at.strftime("%Y-%m-%d %H:%M") }}</span>
|
|
</div>
|
|
</form>
|
|
|
|
<ul class="checklist">
|
|
{% for item in note.ordered_items %}
|
|
<li class="{{ 'done' if item.is_done else '' }}">
|
|
<form method="post"
|
|
action="{{ url_for('notes_toggle_item', note_id=note.id, item_id=item.id) }}"
|
|
class="check-form">
|
|
<input type="checkbox" name="is_done" value="1"
|
|
{% if item.is_done %}checked{% endif %}
|
|
onchange="this.form.submit()"
|
|
aria-label="Mark '{{ item.text }}' as done">
|
|
</form>
|
|
<form method="post"
|
|
action="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}"
|
|
class="item-edit-form">
|
|
<input type="text" name="text" value="{{ item.text }}"
|
|
maxlength="255" required aria-label="Checklist item text"
|
|
data-autosave-url="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}">
|
|
</form>
|
|
<form method="post"
|
|
action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}"
|
|
class="item-delete-form">
|
|
<button type="submit" title="Delete checklist item"
|
|
aria-label="Delete checklist item">[x]</button>
|
|
</form>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<form method="post" action="{{ url_for('notes_add_item', note_id=note.id) }}"
|
|
class="add-item-form">
|
|
<input type="text" name="text" maxlength="255" required
|
|
placeholder="New checklist item">
|
|
<button type="submit">[ Add item ]</button>
|
|
</form>
|
|
|
|
<form id="delete-note-form-{{ note.id }}" method="post"
|
|
action="{{ url_for('notes_delete', note_id=note.id) }}"
|
|
class="hidden"></form>
|
|
</section>
|
|
{% endfor %}
|
|
|
|
<footer class="statusbar">
|
|
<span>{{ user.username }}@yggdrasil:~/notes$</span>
|
|
</footer>
|
|
<script>
|
|
(function () {
|
|
var timers = {};
|
|
function autosave(form, url, payload) {
|
|
var id = form.getAttribute('data-note-id');
|
|
var key = id === 'new' ? 'new' : id;
|
|
clearTimeout(timers[key]);
|
|
timers[key] = setTimeout(function () {
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
|
if (data && data.id && id === 'new') {
|
|
form.setAttribute('data-note-id', data.id);
|
|
}
|
|
});
|
|
}, 3000);
|
|
}
|
|
document.querySelectorAll('form[data-note-id]').forEach(function (form) {
|
|
var id = form.getAttribute('data-note-id');
|
|
var key = id === 'new' ? 'new' : id;
|
|
var statusEl = form.querySelector('.note-save-status');
|
|
var dirty = false;
|
|
|
|
function setStatus(text, cls) {
|
|
if (!statusEl) return;
|
|
statusEl.textContent = text;
|
|
statusEl.className = 'note-save-status' + (cls ? ' ' + cls : '');
|
|
}
|
|
|
|
form.querySelectorAll('input[name="title"], textarea[name="content"]').forEach(function (input) {
|
|
input.addEventListener('input', function () {
|
|
dirty = true;
|
|
setStatus('not saved', 'unsaved');
|
|
clearTimeout(timers[key]);
|
|
timers[key] = setTimeout(function () {
|
|
var title = form.querySelector('input[name="title"]').value;
|
|
var content = form.querySelector('textarea[name="content"]').value;
|
|
fetch('/notes/autosave', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
id: id === 'new' ? null : parseInt(id, 10),
|
|
title: title,
|
|
content: content
|
|
})
|
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
|
dirty = false;
|
|
setStatus('saved', 'saved');
|
|
setTimeout(function () {
|
|
if (!dirty) setStatus('', '');
|
|
}, 3000);
|
|
if (data && data.id && id === 'new') {
|
|
form.setAttribute('data-note-id', data.id);
|
|
}
|
|
});
|
|
}, 3000);
|
|
});
|
|
});
|
|
});
|
|
document.querySelectorAll('.item-edit-form input[name="text"]').forEach(function (input) {
|
|
var form = input.closest('form');
|
|
var url = input.getAttribute('data-autosave-url');
|
|
var card = input.closest('.note-card');
|
|
var statusEl = card.querySelector('.note-save-status');
|
|
var key = form.getAttribute('action') + ':' + input.name;
|
|
var dirty = false;
|
|
|
|
function setStatus(text, cls) {
|
|
if (!statusEl) return;
|
|
statusEl.textContent = text;
|
|
statusEl.className = 'note-save-status' + (cls ? ' ' + cls : '');
|
|
}
|
|
|
|
input.addEventListener('input', function () {
|
|
dirty = true;
|
|
setStatus('not saved', 'unsaved');
|
|
clearTimeout(timers[key]);
|
|
timers[key] = setTimeout(function () {
|
|
var text = input.value;
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ text: text })
|
|
}).then(function (r) { return r.json(); }).then(function () {
|
|
dirty = false;
|
|
setStatus('saved', 'saved');
|
|
setTimeout(function () {
|
|
if (!dirty) setStatus('', '');
|
|
}, 3000);
|
|
});
|
|
}, 3000);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.note-menu-btn').forEach(function (btn) {
|
|
var card = btn.closest('.note-card');
|
|
var dropdown = card.querySelector('.note-menu-dropdown');
|
|
var deleteBtn = card.querySelector('.menu-delete');
|
|
var form = card.querySelector('form[id^="delete-note-form-"]');
|
|
|
|
btn.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
document.querySelectorAll('.note-menu-dropdown').forEach(function (d) {
|
|
if (d !== dropdown) d.classList.add('hidden');
|
|
});
|
|
dropdown.classList.toggle('hidden');
|
|
});
|
|
|
|
deleteBtn.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
if (confirm('Are you sure you want to delete this note?')) {
|
|
form.submit();
|
|
}
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function () {
|
|
document.querySelectorAll('.note-menu-dropdown').forEach(function (d) {
|
|
d.classList.add('hidden');
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|