build: production build with Phase 1 2025 Bible Reader implementation complete
Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
866
RICH_TEXT_NOTES_PLAN.md
Normal file
866
RICH_TEXT_NOTES_PLAN.md
Normal file
@@ -0,0 +1,866 @@
|
||||
# Rich Text Study Notes - Implementation Plan
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Implement a comprehensive rich text note-taking system allowing users to create detailed, formatted study notes with images, links, and advanced organization features for deep Bible study.
|
||||
|
||||
**Status:** Planning Phase
|
||||
**Priority:** 🟡 Medium
|
||||
**Estimated Time:** 2 weeks (80 hours)
|
||||
**Target Completion:** TBD
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals & Objectives
|
||||
|
||||
### Primary Goals
|
||||
1. Provide rich text editing capabilities for study notes
|
||||
2. Enable advanced formatting (bold, italic, lists, headers)
|
||||
3. Support multimedia content (images, links, videos)
|
||||
4. Organize notes with folders and tags
|
||||
5. Enable search and filtering across all notes
|
||||
|
||||
### User Value Proposition
|
||||
- **For students**: Comprehensive study journal
|
||||
- **For scholars**: Research documentation
|
||||
- **For teachers**: Lesson planning and preparation
|
||||
- **For small groups**: Collaborative study materials
|
||||
- **For personal growth**: Spiritual reflection journal
|
||||
|
||||
---
|
||||
|
||||
## ✨ Feature Specifications
|
||||
|
||||
### 1. Note Data Model
|
||||
|
||||
```typescript
|
||||
interface StudyNote {
|
||||
id: string
|
||||
userId: string
|
||||
|
||||
// Content
|
||||
title: string
|
||||
content: string // Rich text (HTML or JSON)
|
||||
contentType: 'html' | 'json' | 'markdown'
|
||||
plainText: string // For search indexing
|
||||
|
||||
// References
|
||||
verseReferences: VerseReference[]
|
||||
relatedNotes: string[] // Note IDs
|
||||
|
||||
// Organization
|
||||
folderId: string | null
|
||||
tags: string[]
|
||||
color: string // For visual organization
|
||||
isPinned: boolean
|
||||
isFavorite: boolean
|
||||
|
||||
// Collaboration
|
||||
visibility: 'private' | 'shared' | 'public'
|
||||
sharedWith: string[] // User IDs
|
||||
|
||||
// Metadata
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
lastViewedAt: Date
|
||||
version: number // For version history
|
||||
wordCount: number
|
||||
readingTime: number // minutes
|
||||
}
|
||||
|
||||
interface NoteFolder {
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
description?: string
|
||||
parentId: string | null // For nested folders
|
||||
color: string
|
||||
icon: string
|
||||
order: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
interface VerseReference {
|
||||
book: string
|
||||
chapter: number
|
||||
verse: number
|
||||
endVerse?: number
|
||||
context?: string // Surrounding text snippet
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Rich Text Editor (TipTap)
|
||||
|
||||
```typescript
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import Table from '@tiptap/extension-table'
|
||||
import TableRow from '@tiptap/extension-table-row'
|
||||
import TableCell from '@tiptap/extension-table-cell'
|
||||
import TableHeader from '@tiptap/extension-table-header'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
|
||||
// Custom verse reference extension
|
||||
const VerseReference = Node.create({
|
||||
name: 'verseReference',
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
book: { default: null },
|
||||
chapter: { default: null },
|
||||
verse: { default: null },
|
||||
text: { default: null }
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'span[data-verse-ref]' }]
|
||||
},
|
||||
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
return [
|
||||
'span',
|
||||
{
|
||||
...HTMLAttributes,
|
||||
'data-verse-ref': true,
|
||||
class: 'verse-reference-chip',
|
||||
contenteditable: 'false'
|
||||
},
|
||||
node.attrs.text || `${node.attrs.book} ${node.attrs.chapter}:${node.attrs.verse}`
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
interface NoteEditorProps {
|
||||
note: StudyNote
|
||||
onSave: (content: string) => void
|
||||
autoSave?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
export const NoteEditor: React.FC<NoteEditorProps> = ({
|
||||
note,
|
||||
onSave,
|
||||
autoSave = true,
|
||||
readOnly = false
|
||||
}) => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3, 4] },
|
||||
code: { HTMLAttributes: { class: 'code-block' } }
|
||||
}),
|
||||
Highlight.configure({ multicolor: true }),
|
||||
Typography,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: { class: 'prose-link' }
|
||||
}),
|
||||
Image.configure({
|
||||
inline: true,
|
||||
HTMLAttributes: { class: 'note-image' }
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true
|
||||
}),
|
||||
Table.configure({ resizable: true }),
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
Placeholder.configure({
|
||||
placeholder: 'Start writing your study notes...',
|
||||
showOnlyWhenEditable: true
|
||||
}),
|
||||
VerseReference
|
||||
],
|
||||
content: note.content,
|
||||
editable: !readOnly,
|
||||
autofocus: !readOnly,
|
||||
onUpdate: ({ editor }) => {
|
||||
if (autoSave) {
|
||||
debouncedSave(editor.getHTML())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const debouncedSave = useDebounce((content: string) => {
|
||||
onSave(content)
|
||||
}, 1000)
|
||||
|
||||
if (!editor) return null
|
||||
|
||||
return (
|
||||
<Box className="note-editor">
|
||||
{!readOnly && <EditorToolbar editor={editor} />}
|
||||
<EditorContent editor={editor} />
|
||||
<EditorFooter editor={editor} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Editor Toolbar
|
||||
|
||||
```typescript
|
||||
const EditorToolbar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
const [linkDialogOpen, setLinkDialogOpen] = useState(false)
|
||||
const [imageDialogOpen, setImageDialogOpen] = useState(false)
|
||||
const [verseRefDialogOpen, setVerseRefDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Box className="editor-toolbar" sx={{
|
||||
p: 1,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 0.5
|
||||
}}>
|
||||
{/* Text Formatting */}
|
||||
<ButtonGroup size="small">
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
color={editor.isActive('bold') ? 'primary' : 'default'}
|
||||
title="Bold (Ctrl+B)"
|
||||
>
|
||||
<FormatBoldIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
color={editor.isActive('italic') ? 'primary' : 'default'}
|
||||
title="Italic (Ctrl+I)"
|
||||
>
|
||||
<FormatItalicIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
color={editor.isActive('underline') ? 'primary' : 'default'}
|
||||
title="Underline (Ctrl+U)"
|
||||
>
|
||||
<FormatUnderlinedIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
color={editor.isActive('strike') ? 'primary' : 'default'}
|
||||
title="Strikethrough"
|
||||
>
|
||||
<FormatStrikethroughIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Headings */}
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<Select
|
||||
value={
|
||||
editor.isActive('heading', { level: 1 }) ? 'h1' :
|
||||
editor.isActive('heading', { level: 2 }) ? 'h2' :
|
||||
editor.isActive('heading', { level: 3 }) ? 'h3' :
|
||||
editor.isActive('paragraph') ? 'p' : 'p'
|
||||
}
|
||||
onChange={(e) => {
|
||||
const level = e.target.value
|
||||
if (level === 'p') {
|
||||
editor.chain().focus().setParagraph().run()
|
||||
} else {
|
||||
const headingLevel = parseInt(level.substring(1)) as 1 | 2 | 3
|
||||
editor.chain().focus().setHeading({ level: headingLevel }).run()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="p">Paragraph</MenuItem>
|
||||
<MenuItem value="h1">Heading 1</MenuItem>
|
||||
<MenuItem value="h2">Heading 2</MenuItem>
|
||||
<MenuItem value="h3">Heading 3</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Lists */}
|
||||
<ButtonGroup size="small">
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
color={editor.isActive('bulletList') ? 'primary' : 'default'}
|
||||
title="Bullet List"
|
||||
>
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
color={editor.isActive('orderedList') ? 'primary' : 'default'}
|
||||
title="Numbered List"
|
||||
>
|
||||
<FormatListNumberedIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
||||
color={editor.isActive('taskList') ? 'primary' : 'default'}
|
||||
title="Task List"
|
||||
>
|
||||
<CheckBoxIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Alignment */}
|
||||
<ButtonGroup size="small">
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
color={editor.isActive({ textAlign: 'left' }) ? 'primary' : 'default'}
|
||||
title="Align Left"
|
||||
>
|
||||
<FormatAlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
color={editor.isActive({ textAlign: 'center' }) ? 'primary' : 'default'}
|
||||
title="Align Center"
|
||||
>
|
||||
<FormatAlignCenterIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
color={editor.isActive({ textAlign: 'right' }) ? 'primary' : 'default'}
|
||||
title="Align Right"
|
||||
>
|
||||
<FormatAlignRightIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Highlights */}
|
||||
<HighlightColorPicker
|
||||
editor={editor}
|
||||
onSelect={(color) => {
|
||||
editor.chain().focus().toggleHighlight({ color }).run()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Media & References */}
|
||||
<ButtonGroup size="small">
|
||||
<IconButton
|
||||
onClick={() => setLinkDialogOpen(true)}
|
||||
color={editor.isActive('link') ? 'primary' : 'default'}
|
||||
title="Insert Link"
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => setImageDialogOpen(true)}
|
||||
title="Insert Image"
|
||||
>
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => setVerseRefDialogOpen(true)}
|
||||
title="Insert Verse Reference"
|
||||
>
|
||||
<MenuBookIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
color={editor.isActive('codeBlock') ? 'primary' : 'default'}
|
||||
title="Code Block"
|
||||
>
|
||||
<CodeIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
||||
{/* Undo/Redo */}
|
||||
<ButtonGroup size="small">
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
title="Undo (Ctrl+Z)"
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
title="Redo (Ctrl+Y)"
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Dialogs */}
|
||||
<LinkDialog
|
||||
open={linkDialogOpen}
|
||||
onClose={() => setLinkDialogOpen(false)}
|
||||
onInsert={(url, text) => {
|
||||
editor.chain().focus().setLink({ href: url }).insertContent(text).run()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ImageDialog
|
||||
open={imageDialogOpen}
|
||||
onClose={() => setImageDialogOpen(false)}
|
||||
onInsert={(url, alt) => {
|
||||
editor.chain().focus().setImage({ src: url, alt }).run()
|
||||
}}
|
||||
/>
|
||||
|
||||
<VerseReferenceDialog
|
||||
open={verseRefDialogOpen}
|
||||
onClose={() => setVerseRefDialogOpen(false)}
|
||||
onInsert={(ref) => {
|
||||
editor.chain().focus().insertContent({
|
||||
type: 'verseReference',
|
||||
attrs: {
|
||||
book: ref.book,
|
||||
chapter: ref.chapter,
|
||||
verse: ref.verse,
|
||||
text: `${ref.book} ${ref.chapter}:${ref.verse}`
|
||||
}
|
||||
}).run()
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Notes List & Organization
|
||||
|
||||
```typescript
|
||||
const NotesPage: React.FC = () => {
|
||||
const [notes, setNotes] = useState<StudyNote[]>([])
|
||||
const [folders, setFolders] = useState<NoteFolder[]>([])
|
||||
const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [sortBy, setSortBy] = useState<'updated' | 'created' | 'title'>('updated')
|
||||
const [viewMode, setViewMode] = useState<'list' | 'grid' | 'compact'>('list')
|
||||
|
||||
// Load notes
|
||||
useEffect(() => {
|
||||
loadNotes()
|
||||
}, [selectedFolder, searchQuery, sortBy])
|
||||
|
||||
const loadNotes = async () => {
|
||||
const params = new URLSearchParams({
|
||||
...(selectedFolder && { folderId: selectedFolder }),
|
||||
...(searchQuery && { search: searchQuery }),
|
||||
sortBy
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/notes?${params}`)
|
||||
const data = await response.json()
|
||||
setNotes(data.notes)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100vh' }}>
|
||||
{/* Sidebar - Folders */}
|
||||
<Box sx={{ width: 250, borderRight: 1, borderColor: 'divider', p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Study Notes
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => createNewNote()}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
New Note
|
||||
</Button>
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
button
|
||||
selected={selectedFolder === null}
|
||||
onClick={() => setSelectedFolder(null)}
|
||||
>
|
||||
<ListItemIcon><AllInboxIcon /></ListItemIcon>
|
||||
<ListItemText primary="All Notes" secondary={notes.length} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon><StarIcon /></ListItemIcon>
|
||||
<ListItemText primary="Favorites" />
|
||||
</ListItem>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<ListSubheader>Folders</ListSubheader>
|
||||
|
||||
{folders.map(folder => (
|
||||
<ListItem
|
||||
key={folder.id}
|
||||
button
|
||||
selected={selectedFolder === folder.id}
|
||||
onClick={() => setSelectedFolder(folder.id)}
|
||||
sx={{ pl: 3 }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FolderIcon style={{ color: folder.color }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={folder.name} />
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
<ListItem button onClick={() => createFolder()}>
|
||||
<ListItemIcon><AddIcon /></ListItemIcon>
|
||||
<ListItemText primary="New Folder" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
{/* Main Content - Notes List */}
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Toolbar */}
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<TextField
|
||||
placeholder="Search notes..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
size="small"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<Select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
|
||||
<MenuItem value="updated">Last Updated</MenuItem>
|
||||
<MenuItem value="created">Date Created</MenuItem>
|
||||
<MenuItem value="title">Title</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<ToggleButtonGroup
|
||||
value={viewMode}
|
||||
exclusive
|
||||
onChange={(_, value) => value && setViewMode(value)}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="list"><ViewListIcon /></ToggleButton>
|
||||
<ToggleButton value="grid"><ViewModuleIcon /></ToggleButton>
|
||||
<ToggleButton value="compact"><ViewHeadlineIcon /></ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Notes Display */}
|
||||
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
||||
{viewMode === 'grid' ? (
|
||||
<Grid container spacing={2}>
|
||||
{notes.map(note => (
|
||||
<Grid item key={note.id} xs={12} sm={6} md={4}>
|
||||
<NoteCard note={note} onClick={() => openNote(note)} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<List>
|
||||
{notes.map(note => (
|
||||
<NoteListItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
compact={viewMode === 'compact'}
|
||||
onClick={() => openNote(note)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Note Templates
|
||||
|
||||
```typescript
|
||||
const NOTE_TEMPLATES = [
|
||||
{
|
||||
id: 'sermon-notes',
|
||||
name: 'Sermon Notes',
|
||||
icon: '📝',
|
||||
content: `
|
||||
<h1>Sermon Notes</h1>
|
||||
<p><strong>Date:</strong> </p>
|
||||
<p><strong>Speaker:</strong> </p>
|
||||
<p><strong>Topic:</strong> </p>
|
||||
<h2>Main Points</h2>
|
||||
<ol>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ol>
|
||||
<h2>Key Verses</h2>
|
||||
<p></p>
|
||||
<h2>Personal Application</h2>
|
||||
<p></p>
|
||||
<h2>Prayer Points</h2>
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'bible-study',
|
||||
name: 'Bible Study',
|
||||
icon: '📖',
|
||||
content: `
|
||||
<h1>Bible Study</h1>
|
||||
<h2>Passage</h2>
|
||||
<p></p>
|
||||
<h2>Context</h2>
|
||||
<p><strong>Historical Context:</strong> </p>
|
||||
<p><strong>Literary Context:</strong> </p>
|
||||
<h2>Observation</h2>
|
||||
<ul>
|
||||
<li>What does the text say?</li>
|
||||
</ul>
|
||||
<h2>Interpretation</h2>
|
||||
<ul>
|
||||
<li>What does it mean?</li>
|
||||
</ul>
|
||||
<h2>Application</h2>
|
||||
<ul>
|
||||
<li>How does this apply to my life?</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'character-study',
|
||||
name: 'Character Study',
|
||||
icon: '👤',
|
||||
content: `
|
||||
<h1>Character Study: [Name]</h1>
|
||||
<h2>Background</h2>
|
||||
<p><strong>Family:</strong> </p>
|
||||
<p><strong>Occupation:</strong> </p>
|
||||
<p><strong>Time Period:</strong> </p>
|
||||
<h2>Key Events</h2>
|
||||
<ol>
|
||||
<li></li>
|
||||
</ol>
|
||||
<h2>Character Traits</h2>
|
||||
<ul>
|
||||
<li><strong>Strengths:</strong> </li>
|
||||
<li><strong>Weaknesses:</strong> </li>
|
||||
</ul>
|
||||
<h2>Lessons Learned</h2>
|
||||
<p></p>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'topical-study',
|
||||
name: 'Topical Study',
|
||||
icon: '🏷️',
|
||||
content: `
|
||||
<h1>Topical Study: [Topic]</h1>
|
||||
<h2>Definition</h2>
|
||||
<p></p>
|
||||
<h2>Key Verses</h2>
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
<h2>What the Bible Says</h2>
|
||||
<p></p>
|
||||
<h2>Practical Application</h2>
|
||||
<p></p>
|
||||
`
|
||||
}
|
||||
]
|
||||
|
||||
const TemplateSelector: React.FC<{
|
||||
onSelect: (template: string) => void
|
||||
}> = ({ onSelect }) => {
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{NOTE_TEMPLATES.map(template => (
|
||||
<Grid item key={template.id} xs={12} sm={6} md={4}>
|
||||
<Card
|
||||
sx={{ cursor: 'pointer', '&:hover': { boxShadow: 4 } }}
|
||||
onClick={() => onSelect(template.content)}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h4" textAlign="center" mb={1}>
|
||||
{template.icon}
|
||||
</Typography>
|
||||
<Typography variant="h6" textAlign="center">
|
||||
{template.name}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Full-Text Search
|
||||
|
||||
```typescript
|
||||
// API endpoint with PostgreSQL full-text search
|
||||
export async function POST(request: Request) {
|
||||
const { query } = await request.json()
|
||||
const userId = await getUserIdFromAuth(request)
|
||||
|
||||
const notes = await prisma.$queryRaw`
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
"plainText",
|
||||
ts_rank(to_tsvector('english', title || ' ' || "plainText"), plainto_tsquery('english', ${query})) AS rank
|
||||
FROM "StudyNote"
|
||||
WHERE
|
||||
"userId" = ${userId}
|
||||
AND to_tsvector('english', title || ' ' || "plainText") @@ plainto_tsquery('english', ${query})
|
||||
ORDER BY rank DESC
|
||||
LIMIT 50
|
||||
`
|
||||
|
||||
return NextResponse.json({ notes })
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Schema
|
||||
|
||||
```prisma
|
||||
model StudyNote {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
title String
|
||||
content String @db.Text
|
||||
contentType String @default("html")
|
||||
plainText String @db.Text // For search
|
||||
|
||||
folderId String?
|
||||
folder NoteFolder? @relation(fields: [folderId], references: [id])
|
||||
|
||||
tags String[]
|
||||
color String?
|
||||
isPinned Boolean @default(false)
|
||||
isFavorite Boolean @default(false)
|
||||
|
||||
visibility String @default("private")
|
||||
sharedWith String[]
|
||||
|
||||
wordCount Int @default(0)
|
||||
readingTime Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lastViewedAt DateTime @default(now())
|
||||
version Int @default(1)
|
||||
|
||||
verseReferences NoteVerseReference[]
|
||||
|
||||
@@index([userId, updatedAt])
|
||||
@@index([userId, folderId])
|
||||
@@index([userId, isPinned])
|
||||
}
|
||||
|
||||
model NoteFolder {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
name String
|
||||
description String?
|
||||
parentId String?
|
||||
parent NoteFolder? @relation("FolderHierarchy", fields: [parentId], references: [id])
|
||||
children NoteFolder[] @relation("FolderHierarchy")
|
||||
|
||||
color String @default("#1976d2")
|
||||
icon String @default("folder")
|
||||
order Int @default(0)
|
||||
|
||||
notes StudyNote[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId, parentId])
|
||||
}
|
||||
|
||||
model NoteVerseReference {
|
||||
id String @id @default(cuid())
|
||||
noteId String
|
||||
note StudyNote @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
||||
|
||||
book String
|
||||
chapter Int
|
||||
verse Int
|
||||
endVerse Int?
|
||||
context String?
|
||||
|
||||
@@index([noteId])
|
||||
@@index([book, chapter, verse])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Implementation Timeline
|
||||
|
||||
### Week 1
|
||||
**Day 1-2:** Setup & Editor
|
||||
- [ ] Create database schema
|
||||
- [ ] Set up TipTap editor
|
||||
- [ ] Build basic toolbar
|
||||
|
||||
**Day 3-4:** Core Features
|
||||
- [ ] Implement save/autosave
|
||||
- [ ] Add formatting options
|
||||
- [ ] Build media insertion
|
||||
|
||||
**Day 5:** Organization
|
||||
- [ ] Create folders system
|
||||
- [ ] Add tags support
|
||||
- [ ] Implement search
|
||||
|
||||
### Week 2
|
||||
**Day 1-2:** Advanced Features
|
||||
- [ ] Build templates
|
||||
- [ ] Add verse references
|
||||
- [ ] Implement version history
|
||||
|
||||
**Day 3-4:** Polish
|
||||
- [ ] Mobile optimization
|
||||
- [ ] Performance tuning
|
||||
- [ ] UI refinement
|
||||
|
||||
**Day 5:** Testing & Launch
|
||||
- [ ] Bug fixes
|
||||
- [ ] Documentation
|
||||
- [ ] Deployment
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2025-10-13
|
||||
**Status:** Ready for Implementation
|
||||
Reference in New Issue
Block a user