fix: improve Bible reader loading UX with skeleton states

- Remove full-screen loading backdrop that hides entire UI
- Add skeleton loading components for chapter headers and verses
- Implement smooth content transitions without UI disappearance
- Change initial loading state to prevent immediate UI hide
- Enhance Suspense fallbacks with better loading messages
- Keep Bible reader interface visible during all loading states

Fixes issue where:
- Entire reader disappeared during chapter changes
- Users saw only header/footer during loading
- Poor perceived performance with jarring transitions

Now provides professional skeleton loading within the reader
interface for a smooth, responsive user experience.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-29 20:14:51 +00:00
parent b337b82fde
commit 44831a096f
3 changed files with 114 additions and 25 deletions

View File

@@ -120,8 +120,17 @@ export default async function BibleChapterPage({ params }: PageProps) {
// Pass the parameters as props instead of URLSearchParams // Pass the parameters as props instead of URLSearchParams
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={
<BibleReader <div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '200px'
}}>
Loading Bible reader...
</div>
}>
<BibleReader
initialVersion={resources.versionId} initialVersion={resources.versionId}
initialBook={resources.bookId} initialBook={resources.bookId}
initialChapter={chapter} initialChapter={chapter}

View File

@@ -50,7 +50,16 @@ export default async function BiblePage({ searchParams, params }: PageProps) {
} }
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '200px'
}}>
Loading Bible reader...
</div>
}>
<BibleReader /> <BibleReader />
</Suspense> </Suspense>
) )

View File

@@ -175,7 +175,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
const [selectedBook, setSelectedBook] = useState<string>('') const [selectedBook, setSelectedBook] = useState<string>('')
const [selectedChapter, setSelectedChapter] = useState<number>(1) const [selectedChapter, setSelectedChapter] = useState<number>(1)
const [verses, setVerses] = useState<BibleVerse[]>([]) const [verses, setVerses] = useState<BibleVerse[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(false)
const [versionsLoading, setVersionsLoading] = useState(true) const [versionsLoading, setVersionsLoading] = useState(true)
const [showAllVersions, setShowAllVersions] = useState(false) const [showAllVersions, setShowAllVersions] = useState(false)
@@ -1382,13 +1382,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
</Dialog> </Dialog>
) )
if (loading && books.length === 0) { // Always render the UI - loading will be handled within components
return (
<Backdrop open>
<CircularProgress color="inherit" />
</Backdrop>
)
}
return ( return (
<Box <Box
@@ -1480,24 +1474,101 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
<Box sx={{ opacity: loading ? 0.3 : 1, transition: 'opacity 0.3s ease' }}> <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 {loading && !currentBook ? (
variant="h3" // Skeleton loading for chapter header
component="h1" <>
sx={{ <Box
mb: 2, sx={{
fontFamily: preferences.fontFamily === 'serif' ? 'Georgia, serif' : 'Arial, sans-serif' height: 40,
}} backgroundColor: 'action.hover',
> borderRadius: 1,
{currentBook?.name} {selectedChapter} mb: 2,
</Typography> margin: '0 auto',
<Typography variant="body2" color="text.secondary"> width: '200px'
{(loading && previousVerses.length > 0 ? previousVerses : verses).length} {t('verses')} }}
</Typography> />
<Box
sx={{
height: 16,
backgroundColor: 'action.hover',
borderRadius: 1,
margin: '0 auto',
width: '80px'
}}
/>
</>
) : (
<>
<Typography
variant="h3"
component="h1"
sx={{
mb: 2,
fontFamily: preferences.fontFamily === 'serif' ? 'Georgia, serif' : 'Arial, sans-serif'
}}
>
{currentBook?.name} {selectedChapter}
</Typography>
<Typography variant="body2" color="text.secondary">
{(loading && previousVerses.length > 0 ? previousVerses : verses).length} {t('verses')}
</Typography>
</>
)}
</Box> </Box>
{/* Verses */} {/* Verses */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
{(loading && previousVerses.length > 0 ? previousVerses : verses).map(renderVerse)} {loading && verses.length === 0 && previousVerses.length === 0 ? (
// Skeleton loading for verses
<>
{Array.from({ length: 8 }).map((_, index) => (
<Box key={`skeleton-${index}`} sx={{ mb: 2, display: 'flex', alignItems: 'flex-start' }}>
<Box
sx={{
width: 32,
height: 20,
backgroundColor: 'action.hover',
borderRadius: 1,
mr: 2,
flexShrink: 0
}}
/>
<Box sx={{ width: '100%' }}>
<Box
sx={{
height: 16,
backgroundColor: 'action.hover',
borderRadius: 1,
mb: 1,
width: `${Math.random() * 40 + 60}%`
}}
/>
<Box
sx={{
height: 16,
backgroundColor: 'action.hover',
borderRadius: 1,
mb: 1,
width: `${Math.random() * 50 + 40}%`
}}
/>
{Math.random() > 0.5 && (
<Box
sx={{
height: 16,
backgroundColor: 'action.hover',
borderRadius: 1,
width: `${Math.random() * 30 + 20}%`
}}
/>
)}
</Box>
</Box>
))}
</>
) : (
(loading && previousVerses.length > 0 ? previousVerses : verses).map(renderVerse)
)}
</Box> </Box>
{/* Chapter Navigation */} {/* Chapter Navigation */}