feat: add inline annotations for highlights
- Add note dialog with multi-line text input - Display notes below highlighted verses with theme-aware styling - Add/edit note button in color picker menu - Update highlight API to save notes - Visual note indicator with left border matching highlight color - Support for removing notes by saving empty text 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,8 @@ import {
|
|||||||
Storage,
|
Storage,
|
||||||
MoreVert,
|
MoreVert,
|
||||||
Star,
|
Star,
|
||||||
StarBorder
|
StarBorder,
|
||||||
|
Edit
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
interface BibleVerse {
|
interface BibleVerse {
|
||||||
@@ -242,9 +243,11 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
open: boolean
|
open: boolean
|
||||||
verse?: BibleVerse
|
verse?: BibleVerse
|
||||||
note: string
|
note: string
|
||||||
|
highlightId?: string
|
||||||
}>({
|
}>({
|
||||||
open: false,
|
open: false,
|
||||||
note: ''
|
note: '',
|
||||||
|
highlightId: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// Copy feedback
|
// Copy feedback
|
||||||
@@ -1243,6 +1246,58 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
handleVerseMenuClose()
|
handleVerseMenuClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenNoteDialog = (verse: BibleVerse) => {
|
||||||
|
const highlight = highlights[verse.id]
|
||||||
|
setNoteDialog({
|
||||||
|
open: true,
|
||||||
|
verse,
|
||||||
|
note: highlight?.note || '',
|
||||||
|
highlightId: highlight?.id
|
||||||
|
})
|
||||||
|
setHighlightColorPickerAnchor({ element: null, verse: null })
|
||||||
|
handleVerseMenuClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveNote = async () => {
|
||||||
|
if (!noteDialog.verse || !user) return
|
||||||
|
|
||||||
|
const token = localStorage.getItem('authToken')
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { verse, note, highlightId } = noteDialog
|
||||||
|
|
||||||
|
if (highlightId) {
|
||||||
|
// Update existing highlight's note
|
||||||
|
const response = await fetch(`/api/highlights/${highlightId}?locale=${locale}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ note: note.trim() || null })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setHighlights(prev => ({
|
||||||
|
...prev,
|
||||||
|
[verse.id]: data.highlight
|
||||||
|
}))
|
||||||
|
setCopyFeedback({
|
||||||
|
open: true,
|
||||||
|
message: note.trim() ? 'Note saved' : 'Note removed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setNoteDialog({ open: false, note: '', highlightId: undefined })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving note:', error)
|
||||||
|
alert('Failed to save note')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSetFavoriteVersion = async () => {
|
const handleSetFavoriteVersion = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}`)}`)
|
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}`)}`)
|
||||||
@@ -1451,6 +1506,28 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
)}
|
)}
|
||||||
{verse.text}
|
{verse.text}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Display highlight note if it exists */}
|
||||||
|
{highlight?.note && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
p: 1.5,
|
||||||
|
backgroundColor: preferences.theme === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
|
||||||
|
borderLeft: '3px solid',
|
||||||
|
borderColor: getHighlightColor(highlight.color, preferences.theme),
|
||||||
|
borderRadius: 1,
|
||||||
|
fontSize: '0.9em',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: preferences.theme === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', fontWeight: 600, mb: 0.5, opacity: 0.7 }}>
|
||||||
|
Note:
|
||||||
|
</Typography>
|
||||||
|
{highlight.note}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@@ -2301,12 +2378,27 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
{highlightColorPickerAnchor.verse && highlights[highlightColorPickerAnchor.verse.id] && (
|
{highlightColorPickerAnchor.verse && highlights[highlightColorPickerAnchor.verse.id] && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
startIcon={<Edit />}
|
||||||
|
onClick={() => {
|
||||||
|
if (highlightColorPickerAnchor.verse) {
|
||||||
|
handleOpenNoteDialog(highlightColorPickerAnchor.verse)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{highlights[highlightColorPickerAnchor.verse.id].note ? 'Edit Note' : 'Add Note'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 1 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (highlightColorPickerAnchor.verse) {
|
if (highlightColorPickerAnchor.verse) {
|
||||||
handleRemoveHighlight(highlightColorPickerAnchor.verse)
|
handleRemoveHighlight(highlightColorPickerAnchor.verse)
|
||||||
@@ -2315,10 +2407,48 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
>
|
>
|
||||||
Remove Highlight
|
Remove Highlight
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
{/* Note Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={noteDialog.open}
|
||||||
|
onClose={() => setNoteDialog({ open: false, note: '', highlightId: undefined })}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
{noteDialog.highlightId ? 'Edit Note' : 'Add Note'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Add your thoughts, insights, or reflections about this verse..."
|
||||||
|
value={noteDialog.note}
|
||||||
|
onChange={(e) => setNoteDialog(prev => ({ ...prev, note: e.target.value }))}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
/>
|
||||||
|
{noteDialog.verse && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
|
||||||
|
{currentBook?.name} {selectedChapter}:{noteDialog.verse.verseNum}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setNoteDialog({ open: false, note: '', highlightId: undefined })}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSaveNote} variant="contained">
|
||||||
|
Save Note
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Copy Feedback */}
|
{/* Copy Feedback */}
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={copyFeedback.open}
|
open={copyFeedback.open}
|
||||||
|
|||||||
Reference in New Issue
Block a user