Fix Bible reader page jumping and layout stability issues

Layout Stability Improvements:
- Add consistent minHeight (60vh) to content containers to prevent layout shifts
- Implement sticky navigation bar with smooth transitions
- Position loading spinner absolutely to prevent content jumping
- Add smooth opacity transitions during content loading

Content Loading Enhancements:
- Preserve previous verses during loading to prevent content flashing
- Display previous verse count during transitions for visual continuity
- Use requestAnimationFrame and setTimeout for smoother state transitions
- Implement content fade-in effects with reduced opacity during loading

Scroll Position Management:
- Store scroll position before loading new content
- Restore scroll position after content loads (maintains reading flow)
- Smart scroll restoration that only applies when not navigating to specific verse
- Prevent scroll jumping during chapter transitions

User Experience Improvements:
- Eliminate page "jumping" between chapter transitions
- Remove container resizing issues that disrupted reading flow
- Provide professional, book-like reading experience
- Maintain visual continuity during all navigation actions
- Smooth loading states with professional transitions

Technical Implementation:
- Add previousVerses state for content continuity
- Implement position: sticky for navigation stability
- Use CSS transitions for smooth visual effects
- Optimize loading timing with requestAnimationFrame
- Maintain consistent container dimensions throughout app lifecycle

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-24 20:31:34 +00:00
parent 274f57f95d
commit 218d94107d

View File

@@ -147,6 +147,7 @@ export default function BibleReaderNew() {
const [preferences, setPreferences] = useState<ReadingPreferences>(defaultPreferences) const [preferences, setPreferences] = useState<ReadingPreferences>(defaultPreferences)
const [highlightedVerse, setHighlightedVerse] = useState<number | null>(null) const [highlightedVerse, setHighlightedVerse] = useState<number | null>(null)
const [showScrollTop, setShowScrollTop] = useState(false) const [showScrollTop, setShowScrollTop] = useState(false)
const [previousVerses, setPreviousVerses] = useState<BibleVerse[]>([]) // Keep previous content during loading
// Bookmark state // Bookmark state
const [isChapterBookmarked, setIsChapterBookmarked] = useState(false) const [isChapterBookmarked, setIsChapterBookmarked] = useState(false)
@@ -387,11 +388,35 @@ export default function BibleReaderNew() {
useEffect(() => { useEffect(() => {
if (selectedBook && selectedChapter) { if (selectedBook && selectedChapter) {
setLoading(true) setLoading(true)
// Store scroll position to prevent jumping
const scrollPosition = window.pageYOffset
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`) fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`)
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
setVerses(data.verses || []) const newVerses = data.verses || []
setLoading(false)
// Store previous verses before updating
setPreviousVerses(verses)
// Use requestAnimationFrame to ensure smooth transition
requestAnimationFrame(() => {
setVerses(newVerses)
// Small delay to allow content to render before removing loading state
setTimeout(() => {
setLoading(false)
setPreviousVerses([]) // Clear previous content after transition
// Restore scroll position if we're not navigating to a specific verse
const urlVerse = new URLSearchParams(window.location.search).get('verse')
if (!urlVerse) {
// Maintain scroll position for better UX
window.scrollTo({ top: Math.min(scrollPosition, document.body.scrollHeight), behavior: 'auto' })
}
}, 50) // Small delay for smoother transition
})
}) })
.catch(err => { .catch(err => {
console.error('Error fetching verses:', err) console.error('Error fetching verses:', err)
@@ -808,7 +833,11 @@ export default function BibleReaderNew() {
p: preferences.readingMode ? 1 : 2, p: preferences.readingMode ? 1 : 2,
...getThemeStyles(), ...getThemeStyles(),
border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}`, border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}`,
backgroundColor: preferences.readingMode ? 'transparent' : getThemeStyles().backgroundColor backgroundColor: preferences.readingMode ? 'transparent' : getThemeStyles().backgroundColor,
position: 'sticky',
top: 0,
zIndex: 1,
transition: 'all 0.2s ease'
}} }}
> >
{/* First Row: Navigation Filters */} {/* First Row: Navigation Filters */}
@@ -1188,24 +1217,37 @@ export default function BibleReaderNew() {
sx={{ sx={{
maxWidth: preferences.columnLayout ? 'none' : '800px', maxWidth: preferences.columnLayout ? 'none' : '800px',
mx: 'auto', mx: 'auto',
width: '100%' width: '100%',
minHeight: '60vh', // Prevent layout shifts
position: 'relative'
}} }}
> >
{loading ? ( <Paper
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> elevation={preferences.readingMode ? 0 : 1}
<CircularProgress /> sx={{
</Box> ...getThemeStyles(),
) : ( borderRadius: preferences.readingMode ? 0 : 2,
<Paper p: preferences.readingMode ? 4 : 3,
elevation={preferences.readingMode ? 0 : 1} minHeight: preferences.readingMode ? '100vh' : '60vh', // Consistent minimum height
sx={{ border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}`,
...getThemeStyles(), position: 'relative'
borderRadius: preferences.readingMode ? 0 : 2, }}
p: preferences.readingMode ? 4 : 3, >
minHeight: preferences.readingMode ? '100vh' : 'auto', {loading && (
border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}` <Box
}} sx={{
> position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 2
}}
>
<CircularProgress />
</Box>
)}
<Box sx={{ opacity: loading ? 0.3 : 1, transition: 'opacity 0.3s ease' }}>
{/* Chapter Header */} {/* Chapter Header */}
<Box sx={{ mb: 4, textAlign: 'center' }}> <Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography <Typography
@@ -1219,13 +1261,13 @@ export default function BibleReaderNew() {
{currentBook?.name} {selectedChapter} {currentBook?.name} {selectedChapter}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{verses.length} {t('verses')} {(loading && previousVerses.length > 0 ? previousVerses : verses).length} {t('verses')}
</Typography> </Typography>
</Box> </Box>
{/* Verses */} {/* Verses */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
{verses.map(renderVerse)} {(loading && previousVerses.length > 0 ? previousVerses : verses).map(renderVerse)}
</Box> </Box>
{/* Chapter Navigation */} {/* Chapter Navigation */}
@@ -1248,8 +1290,8 @@ export default function BibleReaderNew() {
{t('nextChapter')} {t('nextChapter')}
</Button> </Button>
</Box> </Box>
</Paper> </Box>
)} </Paper>
</Box> </Box>
</Container> </Container>