feat: improve Bible reader UX with dropdown menu and enhanced offline features

- Replace three separate verse action icons with compact three-dot dropdown menu
  - Bookmark, Copy Verse, and Ask AI now in a single menu
  - Better space utilization on mobile, tablet, and desktop

- Enhance offline Bible downloads UI
  - Move downloaded versions list to top for better visibility
  - Add inline progress bars during downloads
  - Show real-time download progress with chapter counts
  - Add refresh button for downloaded versions list
  - Remove duplicate header, keep only main header with online/offline status

- Improve build performance
  - Add .eslintignore to speed up linting phase
  - Already excludes large directories (bibles/, scripts/, csv_bibles/)

- Add debug logging for offline storage operations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-30 08:23:22 +00:00
parent 44831a096f
commit fee36dfdad
7 changed files with 424 additions and 178 deletions

16
.eslintignore Normal file
View File

@@ -0,0 +1,16 @@
node_modules/
.next/
out/
build/
dist/
bibles/
scripts/
csv_bibles/
venv/
*.csv
.env
.env.local
prisma/
public/
temp/
logs/

7
.gitignore vendored
View File

@@ -80,3 +80,10 @@ env/
# Import logs # Import logs
import_log.txt import_log.txt
# Large CSV files and conversion scripts
*.csv
csv_bibles/
all_bibles_combined.csv
create_combined_bible_csv.py
json_to_csv_converter.py

View File

@@ -6,6 +6,7 @@ import { useAuth } from '@/hooks/use-auth'
import { useSearchParams, useRouter } from 'next/navigation' import { useSearchParams, useRouter } from 'next/navigation'
import { OfflineDownloadManager } from '@/components/bible/offline-download-manager' import { OfflineDownloadManager } from '@/components/bible/offline-download-manager'
import { OfflineBibleReader } from '@/components/bible/offline-bible-reader' import { OfflineBibleReader } from '@/components/bible/offline-bible-reader'
import { offlineStorage } from '@/lib/offline-storage'
import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt' import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt'
import { import {
Box, Box,
@@ -75,7 +76,8 @@ import {
Chat, Chat,
CloudDownload, CloudDownload,
WifiOff, WifiOff,
Storage Storage,
MoreVert
} from '@mui/icons-material' } from '@mui/icons-material'
interface BibleVerse { interface BibleVerse {
@@ -218,6 +220,15 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
message: '' message: ''
}) })
// Verse menu state
const [verseMenuAnchor, setVerseMenuAnchor] = useState<{
element: HTMLElement | null
verse: BibleVerse | null
}>({
element: null,
verse: null
})
// Refs // Refs
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
const verseRefs = useRef<{[key: number]: HTMLDivElement}>({}) const verseRefs = useRef<{[key: number]: HTMLDivElement}>({})
@@ -450,16 +461,48 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
// Create an AbortController for this request // Create an AbortController for this request
const abortController = new AbortController() const abortController = new AbortController()
fetch(`/api/bible/books?locale=${locale}&version=${debouncedVersion}`, { // First, try to load books from offline storage
signal: abortController.signal const loadBooksOfflineFirst = async () => {
}) try {
.then(res => { // Check if we have the books offline
if (!res.ok) { const offlineBooks = await offlineStorage.getBooksForVersion(debouncedVersion)
throw new Error(`HTTP error! status: ${res.status}`)
if (offlineBooks && offlineBooks.length > 0 && !abortController.signal.aborted) {
console.log('Loading books from offline storage')
// Transform offline books to match the expected interface
const transformedBooks = offlineBooks.map(book => ({
id: book.id,
name: book.name,
testament: book.testament,
orderNum: book.orderNum,
bookKey: book.abbreviation, // Map abbreviation to bookKey
versionId: debouncedVersion,
chaptersCount: book.chaptersCount,
chapters: [] // Add empty chapters array to match interface
}))
setBooks(transformedBooks)
if (!initialBook) {
setSelectedBook(transformedBooks[0].id)
}
setLoading(false)
return // Exit early, we have offline content
} }
return res.json() } catch (offlineError) {
}) console.log('Offline storage not available or books not found, falling back to API')
.then(data => { }
// Fallback to API if offline content not available
try {
const response = await fetch(`/api/bible/books?locale=${locale}&version=${debouncedVersion}`, {
signal: abortController.signal
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
// Only update state if the request wasn't aborted // Only update state if the request wasn't aborted
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
setBooks(data.books || []) setBooks(data.books || [])
@@ -468,14 +511,16 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
} }
setLoading(false) setLoading(false)
} }
}) } catch (err) {
.catch(err => {
// Only log error if it's not an abort error // Only log error if it's not an abort error
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
console.error('Error fetching books:', err) console.error('Error fetching books:', err)
setLoading(false) setLoading(false)
} }
}) }
}
loadBooksOfflineFirst()
// Cleanup function to abort the request if component unmounts or dependencies change // Cleanup function to abort the request if component unmounts or dependencies change
return () => { return () => {
@@ -533,16 +578,53 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
// Create an AbortController for this request // Create an AbortController for this request
const abortController = new AbortController() const abortController = new AbortController()
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`, { // First, try to load from offline storage if available
signal: abortController.signal const loadChapterOfflineFirst = async () => {
}) try {
.then(res => { // Check if we have the chapter offline
if (!res.ok) { const offlineChapter = await offlineStorage.getChapter(selectedVersion, selectedBook, selectedChapter)
throw new Error(`HTTP error! status: ${res.status}`)
if (offlineChapter && !abortController.signal.aborted) {
console.log('Loading chapter from offline storage')
// Store previous verses before updating
setPreviousVerses(verses)
// Use requestAnimationFrame to ensure smooth transition
requestAnimationFrame(() => {
setVerses(offlineChapter.verses)
// 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
})
return // Exit early, we have offline content
} }
return res.json() } catch (offlineError) {
}) console.log('Offline storage not available or chapter not found, falling back to API')
.then(data => { }
// Fallback to API if offline content not available
try {
const response = await fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`, {
signal: abortController.signal
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
// Only update state if the request wasn't aborted // Only update state if the request wasn't aborted
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
const newVerses = data.verses || [] const newVerses = data.verses || []
@@ -568,21 +650,23 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
}, 50) // Small delay for smoother transition }, 50) // Small delay for smoother transition
}) })
} }
}) } catch (err) {
.catch(err => {
// Only log error if it's not an abort error // Only log error if it's not an abort error
if (!abortController.signal.aborted) { if (!abortController.signal.aborted) {
console.error('Error fetching verses:', err) console.error('Error fetching verses:', err)
setLoading(false) setLoading(false)
} }
}) }
}
loadChapterOfflineFirst()
// Cleanup function to abort the request if component unmounts or dependencies change // Cleanup function to abort the request if component unmounts or dependencies change
return () => { return () => {
abortController.abort() abortController.abort()
} }
} }
}, [selectedBook, selectedChapter]) }, [selectedBook, selectedChapter, selectedVersion])
// Check chapter bookmark status // Check chapter bookmark status
useEffect(() => { useEffect(() => {
@@ -883,6 +967,39 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
}, 200) }, 200)
} }
const handleVerseMenuOpen = (event: React.MouseEvent<HTMLElement>, verse: BibleVerse) => {
setVerseMenuAnchor({
element: event.currentTarget,
verse: verse
})
}
const handleVerseMenuClose = () => {
setVerseMenuAnchor({
element: null,
verse: null
})
}
const handleVerseMenuAction = (action: 'bookmark' | 'copy' | 'chat') => {
if (!verseMenuAnchor.verse) return
const verse = verseMenuAnchor.verse
handleVerseMenuClose()
switch (action) {
case 'bookmark':
handleVerseBookmark(verse)
break
case 'copy':
handleCopyVerse(verse)
break
case 'chat':
handleVerseChat(verse)
break
}
}
const getThemeStyles = () => { const getThemeStyles = () => {
switch (preferences.theme) { switch (preferences.theme) {
case 'dark': case 'dark':
@@ -968,8 +1085,6 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
sx={{ sx={{
opacity: preferences.readingMode ? 0.2 : 0.3, opacity: preferences.readingMode ? 0.2 : 0.3,
transition: 'opacity 0.2s', transition: 'opacity 0.2s',
display: 'flex',
gap: 0.5,
'&:hover': { '&:hover': {
opacity: 1 opacity: 1
} }
@@ -977,23 +1092,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
> >
<IconButton <IconButton
size="small" size="small"
onClick={() => handleVerseBookmark(verse)} onClick={(e) => handleVerseMenuOpen(e, verse)}
sx={{
color: isBookmarked ? 'warning.main' : 'action.active',
'&:hover': {
backgroundColor: preferences.readingMode
? (preferences.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' :
preferences.theme === 'sepia' ? 'rgba(92, 75, 58, 0.1)' :
'rgba(0, 0, 0, 0.05)')
: 'action.hover'
}
}}
>
{isBookmarked ? <Bookmark fontSize="small" /> : <BookmarkBorder fontSize="small" />}
</IconButton>
<IconButton
size="small"
onClick={() => handleCopyVerse(verse)}
sx={{ sx={{
color: 'action.active', color: 'action.active',
'&:hover': { '&:hover': {
@@ -1005,23 +1104,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
} }
}} }}
> >
<ContentCopy fontSize="small" /> <MoreVert fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={() => handleVerseChat(verse)}
sx={{
color: 'action.active',
'&:hover': {
backgroundColor: preferences.readingMode
? (preferences.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' :
preferences.theme === 'sepia' ? 'rgba(92, 75, 58, 0.1)' :
'rgba(0, 0, 0, 0.05)')
: 'action.hover'
}
}}
>
<Chat fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
@@ -1642,12 +1725,6 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
fullWidth fullWidth
fullScreen={isMobile} fullScreen={isMobile}
> >
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Storage color="primary" />
Offline Bible Downloads
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0 }}> <DialogContent sx={{ p: 0 }}>
<OfflineDownloadManager <OfflineDownloadManager
availableVersions={versions} availableVersions={versions}
@@ -1666,6 +1743,46 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
{/* PWA Install Prompt */} {/* PWA Install Prompt */}
<InstallPrompt autoShow={true} /> <InstallPrompt autoShow={true} />
{/* Verse Menu */}
<Menu
anchorEl={verseMenuAnchor.element}
open={Boolean(verseMenuAnchor.element)}
onClose={handleVerseMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<MenuItem onClick={() => handleVerseMenuAction('bookmark')}>
<ListItemIcon>
{verseMenuAnchor.verse && verseBookmarks[verseMenuAnchor.verse.id] ? (
<Bookmark fontSize="small" />
) : (
<BookmarkBorder fontSize="small" />
)}
</ListItemIcon>
<ListItemText>
{verseMenuAnchor.verse && verseBookmarks[verseMenuAnchor.verse.id] ? 'Remove Bookmark' : 'Bookmark'}
</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleVerseMenuAction('copy')}>
<ListItemIcon>
<ContentCopy fontSize="small" />
</ListItemIcon>
<ListItemText>Copy Verse</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleVerseMenuAction('chat')}>
<ListItemIcon>
<Chat fontSize="small" />
</ListItemIcon>
<ListItemText>Ask AI</ListItemText>
</MenuItem>
</Menu>
{/* Copy Feedback */} {/* Copy Feedback */}
<Snackbar <Snackbar
open={copyFeedback.open} open={copyFeedback.open}

View File

@@ -20,7 +20,8 @@ import {
DialogActions, DialogActions,
Chip, Chip,
Alert, Alert,
Tooltip Tooltip,
CircularProgress
} from '@mui/material' } from '@mui/material'
import { import {
Download, Download,
@@ -31,7 +32,8 @@ import {
WifiOff, WifiOff,
CheckCircle, CheckCircle,
Error, Error,
Info Info,
Refresh
} from '@mui/icons-material' } from '@mui/icons-material'
import { bibleDownloadManager, type BibleVersion, type DownloadProgress } from '@/lib/offline-storage' import { bibleDownloadManager, type BibleVersion, type DownloadProgress } from '@/lib/offline-storage'
@@ -69,7 +71,10 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
const loadDownloadedVersions = async () => { const loadDownloadedVersions = async () => {
try { try {
console.log('[OfflineDownloadManager] Loading downloaded versions...')
const versions = await bibleDownloadManager.getDownloadedVersions() const versions = await bibleDownloadManager.getDownloadedVersions()
console.log('[OfflineDownloadManager] Downloaded versions:', versions)
console.log('[OfflineDownloadManager] Number of versions:', versions.length)
setDownloadedVersions(versions) setDownloadedVersions(versions)
} catch (error: unknown) { } catch (error: unknown) {
console.error('Failed to load downloaded versions:', error) console.error('Failed to load downloaded versions:', error)
@@ -97,16 +102,35 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
return return
} }
console.log(`Starting download for version: ${version.name} (${version.id})`)
try { try {
// Initialize download progress immediately
setDownloads(prev => ({
...prev,
[version.id]: {
versionId: version.id,
status: 'pending',
progress: 0,
totalBooks: 0,
downloadedBooks: 0,
totalChapters: 0,
downloadedChapters: 0,
startedAt: new Date().toISOString()
}
}))
await bibleDownloadManager.downloadVersion( await bibleDownloadManager.downloadVersion(
version.id, version.id,
(progress: DownloadProgress) => { (progress: DownloadProgress) => {
console.log(`Download progress for ${version.name}:`, progress)
setDownloads(prev => ({ setDownloads(prev => ({
...prev, ...prev,
[version.id]: progress [version.id]: progress
})) }))
if (progress.status === 'completed') { if (progress.status === 'completed') {
console.log(`Download completed for ${version.name}`)
loadDownloadedVersions() loadDownloadedVersions()
loadStorageInfo() loadStorageInfo()
onVersionDownloaded?.(version.id) onVersionDownloaded?.(version.id)
@@ -118,13 +142,33 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
return rest return rest
}) })
}, 3000) }, 3000)
} else if (progress.status === 'failed') {
console.error(`Download failed for ${version.name}:`, progress.error)
alert(`Download failed: ${progress.error || 'Unknown error'}`)
} }
} }
) )
} catch (error: unknown) { } catch (error: unknown) {
console.error('Download failed:', error) console.error('Download failed:', error)
const errorMessage = 'Download failed' const errorMessage = (error as Error)?.message || 'Download failed'
alert(errorMessage)
// Update downloads state to show failure
setDownloads(prev => ({
...prev,
[version.id]: {
versionId: version.id,
status: 'failed' as const,
progress: 0,
totalBooks: 0,
downloadedBooks: 0,
totalChapters: 0,
downloadedChapters: 0,
startedAt: new Date().toISOString(),
error: errorMessage
}
}))
alert(`Download failed: ${errorMessage}`)
} }
} }
@@ -166,7 +210,8 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
} }
const isVersionDownloading = (versionId: string) => { const isVersionDownloading = (versionId: string) => {
return downloads[versionId]?.status === 'downloading' const status = downloads[versionId]?.status
return status === 'downloading' || status === 'pending'
} }
if (loading) { if (loading) {
@@ -192,6 +237,61 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
/> />
</Box> </Box>
{/* Downloaded Versions - Moved to top */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
Downloaded Versions ({downloadedVersions.length})
</Typography>
<Tooltip title="Refresh list">
<IconButton onClick={loadDownloadedVersions} size="small">
<Refresh />
</IconButton>
</Tooltip>
</Box>
{downloadedVersions.length === 0 ? (
<Alert severity="info">
No Bible versions downloaded yet. Download versions below to read offline.
</Alert>
) : (
<List dense>
{downloadedVersions.map((version) => (
<ListItem key={version.id}>
<ListItemText
primary={version.name}
secondary={
<Box>
<Typography variant="body2" color="text.secondary">
{version.abbreviation} - {version.language}
</Typography>
{version.downloadedAt && (
<Typography variant="caption" color="text.secondary">
Downloaded: {new Date(version.downloadedAt).toLocaleDateString()}
</Typography>
)}
</Box>
}
/>
<ListItemSecondaryAction>
<Tooltip title="Delete from offline storage">
<IconButton
edge="end"
onClick={() => setConfirmDelete(version.id)}
color="error"
size="small"
>
<Delete />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
)}
</CardContent>
</Card>
{/* Storage Info */} {/* Storage Info */}
<Card sx={{ mb: 3 }}> <Card sx={{ mb: 3 }}>
<CardContent> <CardContent>
@@ -219,45 +319,6 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
</CardContent> </CardContent>
</Card> </Card>
{/* Available Versions for Download */}
{isOnline && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Available for Download
</Typography>
<List dense>
{availableVersions
.filter(version => !isVersionDownloaded(version.id))
.map((version) => (
<ListItem key={version.id}>
<ListItemText
primary={version.name}
secondary={`${version.abbreviation} - ${version.language}`}
/>
<ListItemSecondaryAction>
<Button
startIcon={<Download />}
onClick={() => handleDownload(version)}
disabled={isVersionDownloading(version.id)}
variant="outlined"
size="small"
>
Download
</Button>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
{availableVersions.filter(v => !isVersionDownloaded(v.id)).length === 0 && (
<Typography color="text.secondary">
All available versions are already downloaded
</Typography>
)}
</CardContent>
</Card>
)}
{/* Active Downloads */} {/* Active Downloads */}
{Object.keys(downloads).length > 0 && ( {Object.keys(downloads).length > 0 && (
<Card sx={{ mb: 3 }}> <Card sx={{ mb: 3 }}>
@@ -301,53 +362,62 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
</Card> </Card>
)} )}
{/* Downloaded Versions */} {/* Available Versions for Download */}
<Card> {isOnline && (
<CardContent> <Card sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom> <CardContent>
Downloaded Versions <Typography variant="h6" gutterBottom>
</Typography> Available for Download
{downloadedVersions.length === 0 ? ( </Typography>
<Alert severity="info">
No Bible versions downloaded yet. Download versions above to read offline.
</Alert>
) : (
<List dense> <List dense>
{downloadedVersions.map((version) => ( {availableVersions
<ListItem key={version.id}> .filter(version => !isVersionDownloaded(version.id))
<ListItemText .map((version) => {
primary={version.name} const downloadProgress = downloads[version.id]
secondary={ const isDownloading = isVersionDownloading(version.id)
<Box>
<Typography variant="body2" color="text.secondary"> return (
{version.abbreviation} - {version.language} <ListItem key={version.id} sx={{ flexDirection: 'column', alignItems: 'stretch' }}>
</Typography> <Box sx={{ display: 'flex', width: '100%', alignItems: 'center' }}>
{version.downloadedAt && ( <ListItemText
<Typography variant="caption" color="text.secondary"> primary={version.name}
Downloaded: {new Date(version.downloadedAt).toLocaleDateString()} secondary={`${version.abbreviation} - ${version.language}`}
</Typography> />
)} <Button
startIcon={isDownloading ? <CircularProgress size={16} /> : <Download />}
onClick={() => handleDownload(version)}
disabled={isDownloading}
variant="outlined"
size="small"
sx={{ ml: 2 }}
>
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
</Box> </Box>
} {isDownloading && downloadProgress && (
/> <Box sx={{ width: '100%', mt: 1 }}>
<ListItemSecondaryAction> <LinearProgress
<Tooltip title="Delete from offline storage"> variant="determinate"
<IconButton value={downloadProgress.progress}
edge="end" sx={{ mb: 0.5 }}
onClick={() => setConfirmDelete(version.id)} />
color="error" <Typography variant="caption" color="text.secondary">
size="small" {downloadProgress.downloadedChapters} / {downloadProgress.totalChapters} chapters ({downloadProgress.progress}%)
> </Typography>
<Delete /> </Box>
</IconButton> )}
</Tooltip> </ListItem>
</ListItemSecondaryAction> )
</ListItem> })}
))}
</List> </List>
)} {availableVersions.filter(v => !isVersionDownloaded(v.id)).length === 0 && (
</CardContent> <Typography color="text.secondary">
</Card> All available versions are already downloaded
</Typography>
)}
</CardContent>
</Card>
)}
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog <Dialog

View File

@@ -125,10 +125,12 @@ class OfflineStorage {
async getAllVersions(): Promise<BibleVersion[]> { async getAllVersions(): Promise<BibleVersion[]> {
await this.init() await this.init()
return this.performTransaction('versions', 'readonly', (store) => { const versions = await this.performTransaction('versions', 'readonly', (store) => {
const versionsStore = store as IDBObjectStore const versionsStore = store as IDBObjectStore
return versionsStore.getAll() return versionsStore.getAll()
}) })
console.log('[OfflineStorage] getAllVersions result:', versions)
return versions
} }
async deleteVersion(versionId: string): Promise<void> { async deleteVersion(versionId: string): Promise<void> {
@@ -409,6 +411,10 @@ export class BibleDownloadManager {
} }
const versionData = await versionResponse.json() const versionData = await versionResponse.json()
if (!versionData.success) {
throw new Error(versionData.error || 'Failed to fetch version books')
}
const { version, books } = versionData const { version, books } = versionData
progress.totalBooks = books.length progress.totalBooks = books.length
@@ -416,10 +422,11 @@ export class BibleDownloadManager {
onProgress?.(progress) onProgress?.(progress)
// Save version info // Save version info
console.log('[Download] Saving version info:', version)
await offlineStorage.saveVersion(version) await offlineStorage.saveVersion(version)
// Calculate total chapters // Calculate total chapters from the chapters array in each book
progress.totalChapters = books.reduce((sum: number, book: any) => sum + book.chaptersCount, 0) progress.totalChapters = books.reduce((sum: number, book: any) => sum + (book.chapters?.length || 0), 0)
await offlineStorage.saveDownloadProgress(progress) await offlineStorage.saveDownloadProgress(progress)
onProgress?.(progress) onProgress?.(progress)
@@ -427,26 +434,48 @@ export class BibleDownloadManager {
for (const book of books) { for (const book of books) {
console.log(`[Download] Downloading book: ${book.name}`) console.log(`[Download] Downloading book: ${book.name}`)
await offlineStorage.saveBook(versionId, book) // Map the book data to match our interface
const bookData = {
id: book.id,
name: book.name,
abbreviation: book.bookKey,
orderNum: book.orderNum,
testament: book.testament,
chaptersCount: book.chapters?.length || 0
}
await offlineStorage.saveBook(versionId, bookData)
progress.downloadedBooks++ progress.downloadedBooks++
// Download all chapters for this book // Download all chapters for this book
for (let chapterNum = 1; chapterNum <= book.chaptersCount; chapterNum++) { if (book.chapters) {
const chapterResponse = await fetch(`/api/bible/chapter?book=${book.id}&chapter=${chapterNum}&version=${versionId}`) for (const chapterInfo of book.chapters) {
const chapterResponse = await fetch(`/api/bible/chapter?book=${book.id}&chapter=${chapterInfo.chapterNum}&version=${versionId}`)
if (chapterResponse.ok) { if (chapterResponse.ok) {
const chapterData = await chapterResponse.json() const chapterData = await chapterResponse.json()
await offlineStorage.saveChapter(versionId, book.id, chapterData.chapter) if (chapterData.chapter) {
// Map the chapter data to match our interface
const mappedChapter = {
id: chapterData.chapter.id,
bookId: book.id,
chapterNum: chapterData.chapter.chapterNum,
verseCount: chapterData.chapter.verses.length,
verses: chapterData.chapter.verses
}
await offlineStorage.saveChapter(versionId, book.id, mappedChapter)
}
}
progress.downloadedChapters++
progress.progress = Math.round((progress.downloadedChapters / progress.totalChapters) * 100)
await offlineStorage.saveDownloadProgress(progress)
onProgress?.(progress)
// Small delay to prevent overwhelming the API
await new Promise(resolve => setTimeout(resolve, 50))
} }
progress.downloadedChapters++
progress.progress = Math.round((progress.downloadedChapters / progress.totalChapters) * 100)
await offlineStorage.saveDownloadProgress(progress)
onProgress?.(progress)
// Small delay to prevent overwhelming the API
await new Promise(resolve => setTimeout(resolve, 50))
} }
} }

View File

@@ -35,14 +35,22 @@ const nextConfig = {
'**/node_modules/**', '**/node_modules/**',
'**/bibles/**', '**/bibles/**',
'**/scripts/**', '**/scripts/**',
'**/csv_bibles/**',
'**/*.csv',
'**/.git/**', '**/.git/**',
'**/.next/**' '**/.next/**'
] ]
} }
// Add ignore patterns for webpack - exclude entire directories // Add ignore patterns for webpack - exclude entire directories and CSV files
config.module.rules.push({ config.module.rules.push({
test: /[\\/](bibles|scripts)[\\/]/, test: /[\\/](bibles|scripts|csv_bibles)[\\/]/,
use: 'ignore-loader'
})
// Ignore CSV files specifically
config.module.rules.push({
test: /\.csv$/,
use: 'ignore-loader' use: 'ignore-loader'
}) })

File diff suppressed because one or more lines are too long