beacon / frontend /src /components /NotebookDrawer.jsx
kiyer's picture
fix: timer cleanup on unmount in MarginNote/NotebookDrawer; wire live userNotes/paperNote into onSaveSession
4894c4f
Raw
History Blame Contribute Delete
3.27 kB
import React from 'react';
export default function NotebookDrawer({
open,
onClose,
paper,
userNotes,
paperNote,
onSavePaperNote,
onScrollToPara,
}) {
const [draft, setDraft] = React.useState(paperNote || '');
const timerRef = React.useRef(null);
React.useEffect(() => { setDraft(paperNote || ''); }, [paperNote]);
React.useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, []);
const handlePaperNoteChange = (e) => {
const val = e.target.value;
setDraft(val);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => onSavePaperNote && onSavePaperNote(val), 400);
};
const paras = paper?.paragraphs || [];
const notedParas = paras.filter((p) => userNotes[p.id]?.text);
const handleExport = () => {
const title = paper?.title || 'paper';
const date = new Date().toISOString().slice(0, 10);
const lines = [`# Notes: ${title}`, `*${date}*`, ''];
if (draft.trim()) {
lines.push('## General', '', draft.trim(), '');
}
notedParas.forEach((p) => {
lines.push(`## ${p.section}`, '', `> ${p.text.slice(0, 120)}…`, '',
userNotes[p.id].text, '');
});
const blob = new Blob([lines.join('\n')], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-notes.md`;
a.click();
URL.revokeObjectURL(url);
};
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
return (
<>
<div className={'ap-notebook-backdrop' + (open ? ' is-open' : '')} onClick={onClose} />
<aside className={'ap-notebook' + (open ? ' is-open' : '')}>
<div className="ap-notebook-head">
<span className="ap-notebook-title">Notebook</span>
<button className="ap-notebook-export" onClick={handleExport}>Export .md</button>
<button className="ap-notebook-close" onClick={onClose}>&times;</button>
</div>
<div className="ap-notebook-paper-note">
<div className="ap-notebook-label">General note</div>
<textarea
className="ap-notebook-paper-text"
value={draft}
placeholder="notes about this paper…"
onChange={handlePaperNoteChange}
rows={3}
/>
</div>
<div className="ap-notebook-entries">
{notedParas.length === 0 && (
<div className="ap-notebook-empty">No paragraph notes yet. Use the ✎ button in a margin card.</div>
)}
{notedParas.map((p) => (
<div key={p.id} className="ap-notebook-entry" onClick={() => onScrollToPara && onScrollToPara(p.id)}>
<div className="ap-notebook-entry-section">{p.section}</div>
<div className="ap-notebook-entry-snippet">{p.text.slice(0, 90)}…</div>
<div className="ap-notebook-entry-note">{userNotes[p.id].text}</div>
</div>
))}
</div>
</aside>
</>
);
}