diff --git a/package.json b/package.json
index b479fc5..e79f8dd 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
- "version": "2.41.0",
+ "version": "2.42.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/server/config.js b/server/config.js
index 1b1f355..36d879d 100644
--- a/server/config.js
+++ b/server/config.js
@@ -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
diff --git a/server/routes/study-data.js b/server/routes/study-data.js
index 8689d05..1df09e4 100644
--- a/server/routes/study-data.js
+++ b/server/routes/study-data.js
@@ -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) {
diff --git a/src/App.css b/src/App.css
index 0c4b034..1e7b578 100644
--- a/src/App.css
+++ b/src/App.css
@@ -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; }
diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx
index 9599492..1342b77 100644
--- a/src/components/NoteCard.tsx
+++ b/src/components/NoteCard.tsx
@@ -13,6 +13,7 @@ interface NoteCardProps {
export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }: NoteCardProps) {
const editor = useEditor({
extensions: [StarterKit],
+ shouldRerenderOnTransaction: true,
content: html || '
',
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 (
@@ -71,6 +74,27 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
aria-label="Numbered list"
title="Numbered list"
>1.
+
+
+
)
}
diff --git a/src/studyPages.tsx b/src/studyPages.tsx
index 07daf5f..9fa5d4f 100644
--- a/src/studyPages.tsx
+++ b/src/studyPages.tsx
@@ -1269,7 +1269,7 @@ export function StudySignupPage() {
Free Student Account
Make every lesson your own.
- - ๐
Notes on every lessonUp to 500 notes across all tracks, each holding about 2,000 words.
+ - ๐
Notes on every lessonUp to 3,000 lesson notes across all tracks, each holding tens of thousands of words, with rich formatting.
- โ
Track your progressPick up exactly where you left off, on any device.
- ๐
Private to youYour notes are tied to your account. Nobody else can read them.
- โก
Free, no adsJust an email and password. You stay signed in for 30 days.
@@ -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) {
{active.section ? `Lesson ${getLessonNumber(study.sections, active.section.id)}` : 'Saved note'}{active.section?.reference ? ` ยท ${active.section.reference}` : ''}
{active.section?.title ?? active.sectionId}
+
+ {noteText(active.note).split(/\s+/).filter(Boolean).length.toLocaleString()} words
+ {' ยท '}
+
+
{(() => {
const anchorMap = parseNoteMap(active.note)
const blocks = Object.entries(anchorMap).filter(([, html]) => html?.trim())