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",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.41.0",
|
"version": "2.42.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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_COMMENT_LENGTH = 2000
|
||||||
export const MAX_STUDY_USERS = 5000
|
export const MAX_STUDY_USERS = 5000
|
||||||
export const MAX_STUDY_ENROLLMENTS_PER_USER = 100
|
export const MAX_STUDY_ENROLLMENTS_PER_USER = 100
|
||||||
export const MAX_STUDY_NOTES_PER_USER = 500
|
export const MAX_STUDY_NOTES_PER_USER = 3000
|
||||||
export const MAX_STUDY_NOTE_LENGTH = 12000
|
export const MAX_STUDY_NOTE_LENGTH = 200000
|
||||||
export const EMAIL_CHANGE_TOKEN_TTL_MS = 24 * 60 * 60 * 1000
|
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 EMAIL_VERIFY_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
export const PASSWORD_RESET_TOKEN_TTL_MS = 60 * 60 * 1000
|
export const PASSWORD_RESET_TOKEN_TTL_MS = 60 * 60 * 1000
|
||||||
|
|||||||
@@ -147,7 +147,11 @@ export function register(app) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const rawNote = typeof req.body?.note === 'string' ? req.body.note : ''
|
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)
|
const notes = await loadUserNotes(user.id)
|
||||||
|
|
||||||
if (!note) {
|
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 p { color: #a89a6a; line-height: 1.7; margin: 0 auto 1.2rem; max-width: 56ch; }
|
||||||
.start-gospel .eyebrow { margin-bottom: 0; color: #c9a84c; }
|
.start-gospel .eyebrow { margin-bottom: 0; color: #c9a84c; }
|
||||||
.header-nav-link { white-space: nowrap; }
|
.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) {
|
export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }: NoteCardProps) {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [StarterKit],
|
extensions: [StarterKit],
|
||||||
|
shouldRerenderOnTransaction: true,
|
||||||
content: html || '<p></p>',
|
content: html || '<p></p>',
|
||||||
onUpdate({ editor }) {
|
onUpdate({ editor }) {
|
||||||
const next = editor.getHTML()
|
const next = editor.getHTML()
|
||||||
@@ -32,6 +33,8 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
|||||||
|
|
||||||
if (!editor) return null
|
if (!editor) return null
|
||||||
|
|
||||||
|
const wordCount = editor.getText().split(/\s+/).filter(Boolean).length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="note-card">
|
<div className="note-card">
|
||||||
<div className="note-card-header">
|
<div className="note-card-header">
|
||||||
@@ -71,6 +74,27 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
|||||||
aria-label="Numbered list"
|
aria-label="Numbered list"
|
||||||
title="Numbered list"
|
title="Numbered list"
|
||||||
>1.</button>
|
>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" />
|
<div className="note-tool-divider" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -91,6 +115,7 @@ export function NoteCard({ anchorLabel, html, autoSaveMsg, onChange, onClose }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditorContent editor={editor} className="note-card-editor" />
|
<EditorContent editor={editor} className="note-card-editor" />
|
||||||
|
<div className="note-card-footer">{wordCount.toLocaleString()} {wordCount === 1 ? 'word' : 'words'}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+19
-2
@@ -1269,7 +1269,7 @@ export function StudySignupPage() {
|
|||||||
<p className="eyebrow">Free Student Account</p>
|
<p className="eyebrow">Free Student Account</p>
|
||||||
<h2 className="auth-aside-title">Make every lesson your own.</h2>
|
<h2 className="auth-aside-title">Make every lesson your own.</h2>
|
||||||
<ul className="auth-benefits">
|
<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>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>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>
|
<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' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ note: JSON.stringify(noteMap) }),
|
body: JSON.stringify({ note: JSON.stringify(noteMap) }),
|
||||||
})
|
})
|
||||||
|
setNoteMessage('')
|
||||||
setAutoSaveMsg('Auto-saved.')
|
setAutoSaveMsg('Auto-saved.')
|
||||||
setTimeout(() => setAutoSaveMsg(''), 3000)
|
setTimeout(() => setAutoSaveMsg(''), 3000)
|
||||||
} catch {
|
} catch (err) {
|
||||||
noteMapDirtyRef.current = true
|
noteMapDirtyRef.current = true
|
||||||
|
setNoteMessage(err instanceof Error ? err.message : 'Unable to save your note.')
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
@@ -2927,6 +2929,21 @@ export function ColossiansStudyNotesPage({ content }: Props) {
|
|||||||
<article className="notes-reader" aria-label="Note">
|
<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>
|
<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>
|
<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 anchorMap = parseNoteMap(active.note)
|
||||||
const blocks = Object.entries(anchorMap).filter(([, html]) => html?.trim())
|
const blocks = Object.entries(anchorMap).filter(([, html]) => html?.trim())
|
||||||
|
|||||||
Reference in new issue
Block a user