noted/index.html
King Omar abf25b34ae major feature update: multi-note, dark mode, markdown, draw tools, D1 sync, share
- Multiple named notes with sidebar (persisted in localStorage + D1)
- Dark mode toggle, persisted
- Markdown preview toggle (Ctrl+M) with styled rendering
- Drawing: eraser tool, undo/redo (40-step stack)
- Auto-save indicator, word/char count status bar
- Export note as .txt download
- Keyboard shortcuts: Ctrl+S save, Ctrl+D draw, Ctrl+M markdown, Ctrl+Shift+N new note
- D1 backend (noted-db) with Pages Functions API for cross-device sync
- Share note: generates read-only link at /share/:token
- Updated service worker to v2, skip API routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 22:13:50 +10:00

832 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>noted.</title>
<link rel="icon" href="fry.png" type="image/png">
<link rel="manifest" href="manifest.json">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg: #fff;
--fg: #000;
--accent: #FFD700;
--accent-hover: #FFA500;
--controls-bg: #f1f1f1;
--border: #ddd;
--sidebar-bg: #f8f8f8;
--sidebar-hover: #efefef;
--sidebar-active-bg: #FFD700;
--sidebar-active-fg: #000;
--input-bg: #fff;
--muted: #888;
--header-bg: #FFD700;
--header-fg: #000;
}
[data-dark] {
--bg: #1a1a1a;
--fg: #e0e0e0;
--accent: #c9a800;
--accent-hover: #a88800;
--controls-bg: #252525;
--border: #3a3a3a;
--sidebar-bg: #1f1f1f;
--sidebar-hover: #2a2a2a;
--sidebar-active-bg: #3a2e00;
--sidebar-active-fg: #FFD700;
--input-bg: #2a2a2a;
--muted: #666;
--header-bg: #2a2200;
--header-fg: #c9a800;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
height: 100vh;
display: flex;
flex-direction: column;
font-family: 'Roboto Mono', monospace;
background: var(--bg);
color: var(--fg);
overflow: hidden;
}
/* ── Header ── */
.header {
display: flex;
align-items: center;
height: 44px;
padding: 0 10px;
gap: 8px;
background: var(--header-bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.header-title {
font-weight: 700;
font-size: 18px;
flex: 1;
color: var(--header-fg);
}
.header-right {
display: flex;
align-items: center;
gap: 6px;
}
.save-indicator {
font-size: 11px;
color: var(--header-fg);
opacity: 0;
transition: opacity 0.4s;
white-space: nowrap;
}
.save-indicator.visible { opacity: 0.7; }
/* ── Buttons ── */
button {
padding: 5px 10px;
cursor: pointer;
background: var(--accent);
color: #000;
border: none;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 12px;
white-space: nowrap;
}
button:hover { background: var(--accent-hover); }
button.on { background: var(--accent-hover); box-shadow: inset 0 2px 3px rgba(0,0,0,0.2); }
.btn-ghost {
background: transparent;
padding: 4px 7px;
font-size: 15px;
}
.btn-ghost:hover { background: rgba(0,0,0,0.1); }
[data-dark] .btn-ghost:hover { background: rgba(255,255,255,0.1); }
/* ── Layout ── */
.body {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Sidebar ── */
.sidebar {
width: 190px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
overflow: hidden;
transition: width 0.2s;
}
.sidebar.closed { width: 0; }
.sidebar-top {
padding: 8px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.sidebar-top button { width: 100%; text-align: left; }
.notes-list { flex: 1; overflow-y: auto; }
.note-item {
display: flex;
align-items: center;
padding: 9px 10px;
cursor: pointer;
border-bottom: 1px solid var(--border);
font-size: 12px;
gap: 4px;
}
.note-item:hover { background: var(--sidebar-hover); }
.note-item.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-fg); font-weight: 700; }
.note-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-del {
background: transparent;
padding: 0 3px;
font-size: 13px;
opacity: 0;
color: inherit;
}
.note-item:hover .note-del { opacity: 0.5; }
.note-del:hover { opacity: 1 !important; background: transparent; }
/* ── Editor ── */
.editor {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.note-title-bar {
display: flex;
align-items: center;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#note-title {
flex: 1;
padding: 10px 14px;
font-size: 17px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
border: none;
outline: none;
background: var(--bg);
color: var(--fg);
}
#note-title::placeholder { color: var(--muted); }
.notepad {
flex: 1;
width: 100%;
padding: 14px;
font-size: 24px;
border: none;
outline: none;
resize: none;
line-height: 1.5;
font-family: 'Roboto Mono', monospace;
color: var(--fg);
background: var(--bg);
}
.md-view {
flex: 1;
padding: 14px 18px;
overflow-y: auto;
background: var(--bg);
color: var(--fg);
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 1.7;
display: none;
}
.md-view h1,.md-view h2,.md-view h3 { margin: 16px 0 8px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.md-view p { margin: 8px 0; }
.md-view ul,.md-view ol { margin: 8px 0 8px 20px; }
.md-view code { background: var(--controls-bg); padding: 1px 5px; border-radius: 3px; font-family: 'Roboto Mono', monospace; font-size: 14px; }
.md-view pre { background: var(--controls-bg); padding: 12px; border-radius: 4px; overflow-x: auto; margin: 8px 0; }
.md-view pre code { background: none; padding: 0; }
.md-view blockquote { border-left: 3px solid var(--accent); padding-left: 12px; color: var(--muted); margin: 8px 0; }
.md-view a { color: var(--accent); }
.canvas-wrap {
flex: 1;
display: none;
position: relative;
background: var(--bg);
}
#drawing-canvas { width: 100%; height: 100%; cursor: crosshair; display: block; }
/* ── Drawing controls ── */
.draw-controls {
display: none;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 7px 10px;
background: var(--controls-bg);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.draw-controls.visible { display: flex; }
/* ── Main controls ── */
.controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 7px 10px;
background: var(--controls-bg);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.cgroup { display: flex; align-items: center; gap: 5px; }
.cgroup label { font-size: 11px; color: var(--muted); }
.controls-right { margin-left: auto; display: flex; gap: 5px; flex-wrap: wrap; }
select {
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--input-bg);
color: var(--fg);
font-family: 'Roboto Mono', monospace;
font-size: 12px;
}
input[type="color"] {
width: 36px;
height: 28px;
padding: 2px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--input-bg);
cursor: pointer;
}
input[type="range"] { width: 70px; }
/* ── Status bar ── */
.status-bar {
display: flex;
justify-content: space-between;
padding: 2px 12px;
font-size: 11px;
color: var(--muted);
background: var(--controls-bg);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
/* ── Share modal ── */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal {
background: var(--bg);
color: var(--fg);
border-radius: 8px;
padding: 22px;
width: 380px;
max-width: 92vw;
}
.modal h3 { margin-bottom: 10px; }
.modal p { font-size: 12px; color: var(--muted); margin-bottom: 10px; }
.modal input[type="text"] {
width: 100%;
padding: 7px;
border: 1px solid var(--border);
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 12px;
background: var(--input-bg);
color: var(--fg);
margin-bottom: 12px;
}
.modal-btns { display: flex; gap: 8px; justify-content: flex-end; }
/* ── Share read-only view ── */
.share-page {
padding: 40px;
max-width: 760px;
margin: 0 auto;
overflow-y: auto;
height: 100vh;
}
.share-page h1 { font-size: 26px; margin-bottom: 20px; }
.share-page .content { font-family: 'Roboto', sans-serif; font-size: 16px; line-height: 1.7; }
.share-page .content h1,.share-page .content h2,.share-page .content h3 { margin: 16px 0 8px; }
.share-page .content code { background: var(--controls-bg); padding: 1px 5px; border-radius: 3px; }
.share-page .content pre { background: var(--controls-bg); padding: 12px; border-radius: 4px; overflow-x: auto; margin: 8px 0; }
.share-page .content blockquote { border-left: 3px solid var(--accent); padding-left: 12px; color: var(--muted); }
.share-banner { font-size: 12px; color: var(--muted); margin-bottom: 24px; }
</style>
</head>
<body>
<div class="header">
<button class="btn-ghost" id="sidebar-toggle"></button>
<span class="header-title">noted.</span>
<div class="header-right">
<span class="save-indicator" id="save-indicator">✓ saved</span>
<button class="btn-ghost" id="dark-toggle" title="Toggle dark mode">🌙</button>
<button id="share-btn">Share</button>
</div>
</div>
<div class="body">
<div class="sidebar" id="sidebar">
<div class="sidebar-top">
<button id="new-note-btn">+ New Note</button>
</div>
<div class="notes-list" id="notes-list"></div>
</div>
<div class="editor">
<div class="note-title-bar">
<input type="text" id="note-title" placeholder="Untitled">
</div>
<textarea class="notepad" id="notepad" placeholder="Start typing..."></textarea>
<div class="md-view" id="md-view"></div>
<div class="canvas-wrap" id="canvas-wrap">
<canvas id="drawing-canvas"></canvas>
</div>
</div>
</div>
<div class="draw-controls" id="draw-controls">
<div class="cgroup">
<label>Size</label>
<input type="range" id="pen-size" min="1" max="30" value="3">
<span id="pen-size-val">3px</span>
</div>
<div class="cgroup">
<label>Color</label>
<input type="color" id="pen-color" value="#000000">
</div>
<button id="eraser-btn">Eraser</button>
<button id="undo-btn">↩ Undo</button>
<button id="redo-btn">↪ Redo</button>
<button id="clear-canvas-btn">Clear</button>
</div>
<div class="controls">
<div class="cgroup">
<label>Font</label>
<select id="font-family">
<option value="'Roboto Mono', monospace">Roboto Mono</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Courier Prime', monospace">Courier Prime</option>
<option value="Arial, sans-serif">Arial</option>
<option value="'Times New Roman', serif">Times New Roman</option>
</select>
</div>
<div class="cgroup">
<label>Size</label>
<button id="dec-font">A-</button>
<span id="font-size-label">24px</span>
<button id="inc-font">A+</button>
</div>
<div class="cgroup">
<label>Color</label>
<input type="color" id="text-color" value="#000000">
</div>
<div class="controls-right">
<button id="md-btn" title="Toggle Markdown preview (Ctrl+M)">MD</button>
<button id="draw-btn" title="Toggle drawing mode (Ctrl+D)">Draw</button>
<button id="export-btn">Export</button>
<button id="save-btn" title="Save (Ctrl+S)">Save</button>
<button id="clear-btn">Clear</button>
</div>
</div>
<div class="status-bar">
<span id="word-count">0 words · 0 chars</span>
<span id="sync-status"></span>
</div>
<script>
(() => {
// ── State ────────────────────────────────────────────────────────────────
const USER_ID = (() => {
let id = localStorage.getItem('noted_uid');
if (!id) { id = crypto.randomUUID(); localStorage.setItem('noted_uid', id); }
return id;
})();
let notes = JSON.parse(localStorage.getItem('noted_notes') || '{}');
let currentId = null;
let dark = document.documentElement.hasAttribute('data-dark');
let isMd = false;
let isDraw = false;
let isEraser = false;
let ctx = null;
let undoStack = [], redoStack = [];
let autoSaveTimer = null, syncTimer = null, saveFlashTimer = null;
// ── DOM ──────────────────────────────────────────────────────────────────
const $ = id => document.getElementById(id);
const notepad = $('notepad');
const noteTitle = $('note-title');
const mdView = $('md-view');
const canvasWrap = $('canvas-wrap');
const canvas = $('drawing-canvas');
const drawCtrls = $('draw-controls');
const sidebar = $('sidebar');
const notesList = $('notes-list');
const saveInd = $('save-indicator');
const wordCount = $('word-count');
const syncStatus = $('sync-status');
// ── Persist ──────────────────────────────────────────────────────────────
function persist() { localStorage.setItem('noted_notes', JSON.stringify(notes)); }
function flashSaved() {
saveInd.classList.add('visible');
clearTimeout(saveFlashTimer);
saveFlashTimer = setTimeout(() => saveInd.classList.remove('visible'), 2000);
}
function updateWordCount() {
const t = notepad.value;
const w = t.trim() ? t.trim().split(/\s+/).length : 0;
wordCount.textContent = `${w} word${w !== 1 ? 's' : ''} · ${t.length} char${t.length !== 1 ? 's' : ''}`;
}
// ── Dark mode ─────────────────────────────────────────────────────────────
function applyDark() {
document.documentElement.toggleAttribute('data-dark', dark);
$('dark-toggle').textContent = dark ? '☀️' : '🌙';
}
$('dark-toggle').addEventListener('click', () => {
dark = !dark;
localStorage.setItem('noted_dark', dark);
applyDark();
});
// ── Sidebar ──────────────────────────────────────────────────────────────
let sidebarOpen = localStorage.getItem('noted_sidebar') !== 'false';
function applySidebar() { sidebar.classList.toggle('closed', !sidebarOpen); }
$('sidebar-toggle').addEventListener('click', () => {
sidebarOpen = !sidebarOpen;
localStorage.setItem('noted_sidebar', sidebarOpen);
applySidebar();
});
// ── Notes list ───────────────────────────────────────────────────────────
function renderList() {
const sorted = Object.values(notes).sort((a, b) => b.updatedAt - a.updatedAt);
notesList.innerHTML = '';
for (const n of sorted) {
const el = document.createElement('div');
el.className = 'note-item' + (n.id === currentId ? ' active' : '');
el.innerHTML = `<span class="note-item-name">${n.title || 'Untitled'}</span><button class="note-del" title="Delete">✕</button>`;
el.querySelector('.note-item-name').addEventListener('click', () => openNote(n.id));
el.querySelector('.note-del').addEventListener('click', e => { e.stopPropagation(); deleteNote(n.id); });
notesList.appendChild(el);
}
}
// ── Note ops ─────────────────────────────────────────────────────────────
function createNote() {
const id = crypto.randomUUID();
notes[id] = { id, title: '', content: '', drawing: null, fontSize: '24px', fontFamily: "'Roboto Mono', monospace", textColor: '#000000', createdAt: Date.now(), updatedAt: Date.now() };
persist();
openNote(id);
noteTitle.focus();
scheduleSync();
}
function openNote(id) {
if (currentId === id) return;
if (currentId) saveNote(true);
currentId = id;
const n = notes[id];
noteTitle.value = n.title || '';
notepad.value = n.content || '';
notepad.style.fontSize = n.fontSize || '24px';
notepad.style.fontFamily = n.fontFamily || "'Roboto Mono', monospace";
notepad.style.color = n.textColor || '#000000';
$('font-size-label').textContent = n.fontSize || '24px';
$('font-family').value = n.fontFamily || "'Roboto Mono', monospace";
$('text-color').value = n.textColor || '#000000';
updateWordCount();
renderMd();
undoStack = []; redoStack = [];
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (n.drawing) { const img = new Image(); img.onload = () => ctx.drawImage(img, 0, 0); img.src = n.drawing; }
}
localStorage.setItem('noted_last', id);
renderList();
}
function saveNote(silent = false) {
if (!currentId || !notes[currentId]) return;
const n = notes[currentId];
n.title = noteTitle.value;
n.content = notepad.value;
n.fontSize = notepad.style.fontSize || '24px';
n.fontFamily = notepad.style.fontFamily || "'Roboto Mono', monospace";
n.textColor = notepad.style.color || '#000000';
n.updatedAt = Date.now();
if (ctx && isDraw) n.drawing = canvas.toDataURL();
persist();
if (!silent) { flashSaved(); renderList(); scheduleSync(); }
}
function deleteNote(id) {
if (!confirm('Delete this note?')) return;
delete notes[id];
persist();
syncDelete(id);
if (currentId === id) {
currentId = null;
const ids = Object.keys(notes);
if (ids.length) openNote(ids[0]); else createNote();
} else {
renderList();
}
}
$('new-note-btn').addEventListener('click', createNote);
// ── Auto-save ─────────────────────────────────────────────────────────────
function scheduleAutoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => saveNote(), 3000);
}
notepad.addEventListener('input', () => { updateWordCount(); renderMd(); scheduleAutoSave(); });
noteTitle.addEventListener('input', () => { if (notes[currentId]) notes[currentId].title = noteTitle.value; renderList(); scheduleAutoSave(); });
// ── Markdown ──────────────────────────────────────────────────────────────
function renderMd() { if (isMd) mdView.innerHTML = marked.parse(notepad.value || ''); }
$('md-btn').addEventListener('click', () => {
isMd = !isMd;
$('md-btn').classList.toggle('on', isMd);
notepad.style.display = isMd ? 'none' : '';
mdView.style.display = isMd ? 'block' : 'none';
if (isMd) renderMd();
});
// ── Font controls ─────────────────────────────────────────────────────────
$('dec-font').addEventListener('click', () => {
const sz = parseInt(getComputedStyle(notepad).fontSize);
if (sz > 10) { const v = (sz - 2) + 'px'; notepad.style.fontSize = v; $('font-size-label').textContent = v; scheduleAutoSave(); }
});
$('inc-font').addEventListener('click', () => {
const sz = parseInt(getComputedStyle(notepad).fontSize);
const v = (sz + 2) + 'px'; notepad.style.fontSize = v; $('font-size-label').textContent = v; scheduleAutoSave();
});
$('font-family').addEventListener('change', () => { notepad.style.fontFamily = $('font-family').value; scheduleAutoSave(); });
$('text-color').addEventListener('input', () => { notepad.style.color = $('text-color').value; scheduleAutoSave(); });
// ── Save / Clear / Export ─────────────────────────────────────────────────
$('save-btn').addEventListener('click', () => saveNote());
$('clear-btn').addEventListener('click', () => {
if (!confirm('Clear this note?')) return;
notepad.value = ''; renderMd(); updateWordCount(); saveNote();
});
$('export-btn').addEventListener('click', () => {
const n = notes[currentId];
if (!n) return;
const text = (n.title ? n.title + '\n' + '='.repeat(n.title.length) + '\n\n' : '') + n.content;
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(new Blob([text], { type: 'text/plain' })),
download: (n.title || 'note') + '.txt'
});
a.click(); URL.revokeObjectURL(a.href);
});
// ── Drawing ───────────────────────────────────────────────────────────────
function initCanvas() {
const resize = () => {
const data = ctx ? canvas.toDataURL() : null;
canvas.width = canvasWrap.clientWidth;
canvas.height = canvasWrap.clientHeight;
if (data) { const img = new Image(); img.onload = () => ctx.drawImage(img, 0, 0); img.src = data; }
};
resize();
ctx = canvas.getContext('2d');
const n = notes[currentId];
if (n?.drawing) { const img = new Image(); img.onload = () => ctx.drawImage(img, 0, 0); img.src = n.drawing; }
window.addEventListener('resize', resize);
let drawing = false;
const pt = e => { const r = canvas.getBoundingClientRect(); const s = e.touches?.[0] ?? e; return [s.clientX - r.left, s.clientY - r.top]; };
canvas.addEventListener('mousedown', start); canvas.addEventListener('touchstart', start, { passive: false });
canvas.addEventListener('mousemove', move); canvas.addEventListener('touchmove', move, { passive: false });
window.addEventListener('mouseup', stop); window.addEventListener('touchend', stop);
function start(e) {
drawing = true; pushUndo(); ctx.beginPath();
const [x, y] = pt(e); ctx.moveTo(x, y); e.preventDefault?.();
}
function move(e) {
if (!drawing) return;
ctx.lineWidth = $('pen-size').value; ctx.lineCap = 'round';
ctx.globalCompositeOperation = isEraser ? 'destination-out' : 'source-over';
ctx.strokeStyle = isEraser ? 'rgba(0,0,0,1)' : $('pen-color').value;
const [x, y] = pt(e); ctx.lineTo(x, y); ctx.stroke(); e.preventDefault?.();
}
function stop() {
if (!drawing) return; drawing = false;
ctx.globalCompositeOperation = 'source-over';
if (notes[currentId]) { notes[currentId].drawing = canvas.toDataURL(); persist(); }
}
}
function pushUndo() { undoStack.push(canvas.toDataURL()); if (undoStack.length > 40) undoStack.shift(); redoStack = []; }
$('draw-btn').addEventListener('click', () => {
isDraw = !isDraw;
$('draw-btn').classList.toggle('on', isDraw);
canvasWrap.style.display = isDraw ? 'block' : 'none';
drawCtrls.classList.toggle('visible', isDraw);
notepad.style.display = (!isDraw && !isMd) ? '' : 'none';
mdView.style.display = (!isDraw && isMd) ? 'block' : 'none';
if (isDraw && !ctx) initCanvas();
});
$('pen-size').addEventListener('input', () => { $('pen-size-val').textContent = $('pen-size').value + 'px'; });
$('eraser-btn').addEventListener('click', () => {
isEraser = !isEraser;
$('eraser-btn').classList.toggle('on', isEraser);
$('eraser-btn').textContent = isEraser ? 'Pen' : 'Eraser';
});
$('undo-btn').addEventListener('click', () => {
if (!undoStack.length) return;
redoStack.push(canvas.toDataURL());
const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); }; img.src = undoStack.pop();
});
$('redo-btn').addEventListener('click', () => {
if (!redoStack.length) return;
undoStack.push(canvas.toDataURL());
const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); }; img.src = redoStack.pop();
});
$('clear-canvas-btn').addEventListener('click', () => {
if (!confirm('Clear canvas?')) return;
pushUndo(); ctx.clearRect(0, 0, canvas.width, canvas.height);
if (notes[currentId]) { notes[currentId].drawing = null; persist(); }
});
// ── Keyboard shortcuts ────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (!(e.ctrlKey || e.metaKey)) return;
if (e.key === 's') { e.preventDefault(); saveNote(); }
if (e.key === 'd') { e.preventDefault(); $('draw-btn').click(); }
if (e.key === 'm') { e.preventDefault(); $('md-btn').click(); }
if (e.key === 'N' && e.shiftKey) { e.preventDefault(); createNote(); }
});
// ── Share ─────────────────────────────────────────────────────────────────
$('share-btn').addEventListener('click', async () => {
saveNote(true);
const n = notes[currentId];
if (!n) return;
try {
const res = await fetch('/api/notes/' + n.id + '/share', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: USER_ID })
});
const data = await res.json();
if (data.token) showShareModal(location.origin + '/share/' + data.token);
else alert('Share failed: ' + (data.error || 'unknown error'));
} catch { alert('Share failed — are you online?'); }
});
function showShareModal(url) {
const ov = document.createElement('div');
ov.className = 'overlay';
ov.innerHTML = `<div class="modal">
<h3>Share Note</h3>
<p>Anyone with this link can view a read-only copy:</p>
<input type="text" value="${url}" readonly onclick="this.select()">
<div class="modal-btns">
<button id="_copy">Copy Link</button>
<button id="_close">Close</button>
</div>
</div>`;
document.body.appendChild(ov);
ov.querySelector('#_copy').addEventListener('click', async function() {
await navigator.clipboard.writeText(url); this.textContent = 'Copied!';
});
ov.querySelector('#_close').addEventListener('click', () => ov.remove());
ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });
}
// ── D1 Sync ───────────────────────────────────────────────────────────────
function scheduleSync() { clearTimeout(syncTimer); syncTimer = setTimeout(syncCurrent, 5000); }
async function syncCurrent() {
const n = notes[currentId];
if (!n) return;
try {
syncStatus.textContent = 'syncing…';
await fetch('/api/notes/' + n.id, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...n, user_id: USER_ID })
});
syncStatus.textContent = 'synced ✓';
} catch { syncStatus.textContent = 'offline'; }
}
async function syncDelete(id) {
try {
await fetch('/api/notes/' + id, {
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: USER_ID })
});
} catch { /* offline */ }
}
async function pullNotes() {
try {
const res = await fetch('/api/notes?user_id=' + USER_ID);
if (!res.ok) return;
const { notes: remote } = await res.json();
let changed = false;
for (const r of (remote || [])) {
const local = notes[r.id];
if (!local || r.updatedAt > local.updatedAt) { notes[r.id] = r; changed = true; }
}
if (changed) { persist(); renderList(); }
syncStatus.textContent = 'synced ✓';
} catch { syncStatus.textContent = 'offline'; }
}
// ── Share read-only view ──────────────────────────────────────────────────
async function loadShareView(token) {
document.querySelector('.body').remove();
document.querySelector('.draw-controls').remove();
document.querySelector('.controls').remove();
document.querySelector('.status-bar').remove();
document.querySelector('.header').innerHTML = '<span class="header-title" style="padding:0 12px">noted.</span>';
const page = document.createElement('div');
page.className = 'share-page';
try {
const res = await fetch('/api/share/' + token);
const { note, error } = await res.json();
if (error || !note) { page.innerHTML = '<p style="color:var(--muted)">Note not found or link expired.</p>'; }
else {
document.title = (note.title || 'noted.') + ' — noted.';
page.innerHTML = `<div class="share-banner">shared via noted.</div><h1>${note.title || 'Untitled'}</h1><div class="content">${marked.parse(note.content || '')}</div>`;
}
} catch { page.innerHTML = '<p>Failed to load note.</p>'; }
document.body.appendChild(page);
}
// ── Init ──────────────────────────────────────────────────────────────────
const shareMatch = location.pathname.match(/^\/share\/([a-zA-Z0-9]+)$/);
if (shareMatch) { loadShareView(shareMatch[1]); return; }
dark = localStorage.getItem('noted_dark') === 'true';
applyDark();
applySidebar();
const ids = Object.keys(notes);
if (!ids.length) { createNote(); }
else {
const lastId = localStorage.getItem('noted_last');
openNote(notes[lastId] ? lastId : ids[0]);
}
renderList();
pullNotes();
window.addEventListener('beforeunload', () => { saveNote(true); });
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js').catch(() => {});
}
})();
</script>
</body>
</html>