Notes: raise limits (3,000 notes, 200k chars each), reject instead of truncating oversize notes, save-error feedback, richer editor toolbar + word count, copy note; v2.42.0
Publish Container / build (push) Successful in 19m58s
Publish Container / build (push) Successful in 19m58s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae0016cfee
commit
9710834b16
6 files changed
+57
-6
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.41.0",
|
||||
"version": "2.42.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+2
-2
@@ -122,8 +122,8 @@ export const MAX_STUDY_COMMENTS = 10000
|
||||
export const MAX_STUDY_COMMENT_LENGTH = 2000
|
||||
export const MAX_STUDY_USERS = 5000
|
||||
export const MAX_STUDY_ENROLLMENTS_PER_USER = 100
|
||||
export const MAX_STUDY_NOTES_PER_USER = 500
|
||||
export const MAX_STUDY_NOTE_LENGTH = 12000
|
||||
export const MAX_STUDY_NOTES_PER_USER = 3000
|
||||
export const MAX_STUDY_NOTE_LENGTH = 200000
|
||||
export const EMAIL_CHANGE_TOKEN_TTL_MS = 24 * 60 * 60 * 1000
|
||||
export const EMAIL_VERIFY_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
||||
export const PASSWORD_RESET_TOKEN_TTL_MS = 60 * 60 * 1000
|
||||
|
||||
@@ -147,7 +147,11 @@ export function register(app) {
|
||||
return
|
||||
}
|
||||
const rawNote = typeof req.body?.note === 'string' ? req.body.note : ''
|
||||
const note = rawNote.trim().slice(0, MAX_STUDY_NOTE_LENGTH)
|
||||
const note = rawNote.trim()
|
||||
if (note.length > MAX_STUDY_NOTE_LENGTH) {
|
||||
res.status(413).json({ message: 'This lesson\'s notes are too long to save. Please trim some text.' })
|
||||
return
|
||||
}
|
||||
const notes = await loadUserNotes(user.id)
|
||||
|
||||
if (!note) {
|
||||
|
||||
@@ -14157,3 +14157,8 @@ a.sl-pager-link:hover { border-color: rgba(201, 168, 76, 0.6); transform: transl
|
||||
.start-gospel p { color: #a89a6a; line-height: 1.7; margin: 0 auto 1.2rem; max-width: 56ch; }
|
||||
.start-gospel .eyebrow { margin-bottom: 0; color: #c9a84c; }
|
||||
.header-nav-link { white-space: nowrap; }
|
||||
|
||||
.note-card-footer { padding: 0.35rem 0.9rem; font-size: 0.75rem; color: #7a7060; text-align: right; border-top: 1px solid rgba(201, 168, 76, 0.14); }
|
||||
.notes-reader-meta { margin: 0.25rem 0 1rem; font-size: 0.82rem; color: #7a7060; }
|
||||
.notes-copy-btn { background: none; border: 0; padding: 0; color: #c9a84c; cursor: pointer; font: inherit; text-decoration: underline; }
|
||||
.notes-copy-btn:hover { color: #e3c46a; }
|
||||
@@ -13,6 +13,7 @@ interface NoteCardProps {
|
||||
export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }: NoteCardProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
shouldRerenderOnTransaction: true,
|
||||
content: html || '<p></p>',
|
||||
onUpdate({ editor }) {
|
||||
const next = editor.getHTML()
|
||||
@@ -32,6 +33,8 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
||||
|
||||
if (!editor) return null
|
||||
|
||||
const wordCount = editor.getText().split(/\s+/).filter(Boolean).length
|
||||
|
||||
return (
|
||||
<div className="note-card">
|
||||
<div className="note-card-header">
|
||||
@@ -71,6 +74,27 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
||||
aria-label="Numbered list"
|
||||
title="Numbered list"
|
||||
>1.</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`note-tool${editor.isActive('heading', { level: 3 }) ? ' note-tool--active' : ''}`}
|
||||
onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleHeading({ level: 3 }).run() }}
|
||||
aria-label="Heading"
|
||||
title="Heading"
|
||||
>H</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`note-tool${editor.isActive('blockquote') ? ' note-tool--active' : ''}`}
|
||||
onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBlockquote().run() }}
|
||||
aria-label="Quote"
|
||||
title="Quote"
|
||||
>❝</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`note-tool${editor.isActive('strike') ? ' note-tool--active' : ''}`}
|
||||
onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleStrike().run() }}
|
||||
aria-label="Strikethrough"
|
||||
title="Strikethrough"
|
||||
><s>S</s></button>
|
||||
<div className="note-tool-divider" />
|
||||
<button
|
||||
type="button"
|
||||
@@ -91,6 +115,7 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
||||
</div>
|
||||
|
||||
<EditorContent editor={editor} className="note-card-editor" />
|
||||
<div className="note-card-footer">{wordCount.toLocaleString()} {wordCount === 1 ? 'word' : 'words'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+19
-2
@@ -1269,7 +1269,7 @@ export function StudySignupPage() {
|
||||
<p className="eyebrow">Free Student Account</p>
|
||||
<h2 className="auth-aside-title">Make every lesson your own.</h2>
|
||||
<ul className="auth-benefits">
|
||||
<li><span aria-hidden="true">📝</span><div><strong>Notes on every lesson</strong><p>Up to 500 notes across all tracks, each holding about 2,000 words.</p></div></li>
|
||||
<li><span aria-hidden="true">📝</span><div><strong>Notes on every lesson</strong><p>Up to 3,000 lesson notes across all tracks, each holding tens of thousands of words, with rich formatting.</p></div></li>
|
||||
<li><span aria-hidden="true">✅</span><div><strong>Track your progress</strong><p>Pick up exactly where you left off, on any device.</p></div></li>
|
||||
<li><span aria-hidden="true">🔒</span><div><strong>Private to you</strong><p>Your notes are tied to your account. Nobody else can read them.</p></div></li>
|
||||
<li><span aria-hidden="true">⚡</span><div><strong>Free, no ads</strong><p>Just an email and password. You stay signed in for 30 days.</p></div></li>
|
||||
@@ -2051,10 +2051,12 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ note: JSON.stringify(noteMap) }),
|
||||
})
|
||||
setNoteMessage('')
|
||||
setAutoSaveMsg('Auto-saved.')
|
||||
setTimeout(() => setAutoSaveMsg(''), 3000)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
noteMapDirtyRef.current = true
|
||||
setNoteMessage(err instanceof Error ? err.message : 'Unable to save your note.')
|
||||
}
|
||||
}, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
@@ -2927,6 +2929,21 @@ export function ColossiansStudyNotesPage({ content }: Props) {
|
||||
<article className="notes-reader" aria-label="Note">
|
||||
<p className="notes-reader-label">{active.section ? `Lesson ${getLessonNumber(study.sections, active.section.id)}` : 'Saved note'}{active.section?.reference ? ` · ${active.section.reference}` : ''}</p>
|
||||
<h2>{active.section?.title ?? active.sectionId}</h2>
|
||||
<p className="notes-reader-meta">
|
||||
{noteText(active.note).split(/\s+/).filter(Boolean).length.toLocaleString()} words
|
||||
{' · '}
|
||||
<button
|
||||
type="button"
|
||||
className="notes-copy-btn"
|
||||
onClick={() => {
|
||||
const text = Object.entries(parseNoteMap(active.note))
|
||||
.filter(([, html]) => html?.trim())
|
||||
.map(([key, html]) => `${ANCHOR_LABELS[key] ?? key}\n${stripHtml(html)}`)
|
||||
.join('\n\n')
|
||||
navigator.clipboard?.writeText(text).then(() => setMessage('Note copied to clipboard.'), () => setMessage('Unable to copy this note.'))
|
||||
}}
|
||||
>Copy note</button>
|
||||
</p>
|
||||
{(() => {
|
||||
const anchorMap = parseNoteMap(active.note)
|
||||
const blocks = Object.entries(anchorMap).filter(([, html]) => html?.trim())
|
||||
|
||||
Reference in new issue
Block a user