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:
2025-10-10 12:32:53 +00:00
parent 09b8450bff
commit cb47f62caa

View File

@@ -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}