diff --git a/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx b/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx index 0385913..341daf2 100644 --- a/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx +++ b/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx @@ -15,6 +15,8 @@ interface PageProps { // Helper function to convert readable names to IDs async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: string) { try { + console.log('[SEO URL] Looking for version:', versionSlug, 'book:', bookSlug, 'chapter:', chapterNum) + // Find version by abbreviation (slug) const version = await prisma.bibleVersion.findFirst({ where: { @@ -26,9 +28,12 @@ async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: }) if (!version) { + console.log('[SEO URL] Version not found:', versionSlug) return null } + console.log('[SEO URL] Found version:', version.abbreviation, version.id) + // Find book by bookKey (slug) within this version const book = await prisma.bibleBook.findFirst({ where: { @@ -41,12 +46,23 @@ async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: }) if (!book) { + console.log('[SEO URL] Book not found for bookKey:', bookSlug, 'in version:', version.abbreviation) + // List available books for debugging + const availableBooks = await prisma.bibleBook.findMany({ + where: { versionId: version.id }, + select: { bookKey: true, name: true }, + take: 5 + }) + console.log('[SEO URL] Sample available books:', availableBooks) return null } + console.log('[SEO URL] Found book:', book.name, book.id) + // Validate chapter number const chapter = parseInt(chapterNum) if (isNaN(chapter) || chapter < 1) { + console.log('[SEO URL] Invalid chapter number:', chapterNum) return null } @@ -59,9 +75,12 @@ async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: }) if (!chapterRecord) { + console.log('[SEO URL] Chapter not found:', chapter, 'in book:', book.name) return null } + console.log('[SEO URL] Successfully resolved:', version.abbreviation, book.name, chapter) + return { versionId: version.id, bookId: book.id, @@ -70,7 +89,7 @@ async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: book: book } } catch (error) { - console.error('Error resolving resource IDs:', error) + console.error('[SEO URL] Error resolving resource IDs:', error) return null } } diff --git a/app/[locale]/bible/reader.tsx b/app/[locale]/bible/reader.tsx index 1b0e2e5..4022d3a 100644 --- a/app/[locale]/bible/reader.tsx +++ b/app/[locale]/bible/reader.tsx @@ -49,7 +49,8 @@ import { InputLabel, Select, Container, - Autocomplete + Autocomplete, + LinearProgress } from '@mui/material' import { Menu as MenuIcon, @@ -110,6 +111,7 @@ interface BibleBook { orderNum: number bookKey: string chapters: BibleChapter[] + chaptersCount?: number } interface ReadingPreferences { @@ -203,6 +205,10 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha const [verseBookmarks, setVerseBookmarks] = useState<{[key: string]: any}>({}) const [bookmarkLoading, setBookmarkLoading] = useState(false) + // Reading progress state + const [readingProgress, setReadingProgress] = useState(null) + const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false) + // Note dialog state const [noteDialog, setNoteDialog] = useState<{ open: boolean @@ -334,7 +340,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha const loadVersions = async () => { setVersionsLoading(true) const url = showAllVersions - ? '/api/bible/versions?all=true&limit=200' // Limit to first 200 for performance + ? '/api/bible/versions?all=true' // Load ALL versions, no limit : `/api/bible/versions?language=${locale}` try { @@ -344,9 +350,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha if (data.success && data.versions) { setVersions(data.versions) - // Keep current selection if it exists in new list, otherwise select default/first - const currentVersionExists = data.versions.some((v: BibleVersion) => v.id === selectedVersion) - if (!currentVersionExists || !selectedVersion) { + // Only auto-select if there's NO current selection + if (!selectedVersion) { // Try to load user's favorite version first let versionToSelect = null @@ -389,7 +394,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } loadVersions() - }, [locale, showAllVersions, selectedVersion, user]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [locale, showAllVersions, user]) // Removed selectedVersion from dependencies to prevent infinite loop // Handle URL parameters for bookmark navigation useEffect(() => { @@ -490,6 +496,41 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } }, [selectedVersion, debouncedVersion]) + // Load reading progress when version changes + useEffect(() => { + // Only run on client side to avoid hydration mismatch + if (typeof window === 'undefined') return + + const loadProgress = async () => { + if (debouncedVersion && user && !hasLoadedInitialProgress) { + const progress = await loadReadingProgress(debouncedVersion) + if (progress) { + setReadingProgress(progress) + // Only restore position if we haven't loaded from URL params + if (!effectiveParams.get('book') && !effectiveParams.get('chapter')) { + setSelectedBook(progress.bookId) + setSelectedChapter(progress.chapterNum) + } + } + setHasLoadedInitialProgress(true) + } + } + loadProgress() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedVersion, user, hasLoadedInitialProgress, effectiveParams]) + + // Save reading progress when chapter changes + useEffect(() => { + if (selectedVersion && selectedBook && selectedChapter && user) { + // Debounce saving to avoid too many API calls + const timer = setTimeout(() => { + saveReadingProgress(selectedVersion, selectedBook, selectedChapter) + }, 2000) // Save after 2 seconds of no changes + + return () => clearTimeout(timer) + } + }, [selectedVersion, selectedBook, selectedChapter, user]) + // Fetch books when debounced version changes useEffect(() => { if (debouncedVersion) { @@ -540,9 +581,14 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha // Only update state if the request wasn't aborted if (!abortController.signal.aborted) { - setBooks(data.books || []) - if (data.books && data.books.length > 0 && !initialBook) { - setSelectedBook(data.books[0].id) + // Transform books to include chaptersCount + const transformedBooks = (data.books || []).map((book: any) => ({ + ...book, + chaptersCount: book.chapters?.length || 0 + })) + setBooks(transformedBooks) + if (transformedBooks.length > 0 && !initialBook) { + setSelectedBook(transformedBooks[0].id) } setLoading(false) } @@ -1069,6 +1115,84 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } } + // Save reading progress + const saveReadingProgress = async (versionId: string, bookId: string, chapterNum: number) => { + if (!user) return + + const token = localStorage.getItem('authToken') + if (!token) return + + try { + await fetch('/api/user/reading-progress', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + versionId, + bookId, + chapterNum, + verseNum: null + }) + }) + } catch (error) { + console.error('Error saving reading progress:', error) + } + } + + // Load reading progress for current version + const loadReadingProgress = async (versionId: string) => { + if (!user) return null + + const token = localStorage.getItem('authToken') + if (!token) return null + + try { + const response = await fetch(`/api/user/reading-progress?versionId=${versionId}`, { + headers: { 'Authorization': `Bearer ${token}` } + }) + const data = await response.json() + if (data.success && data.progress) { + return data.progress + } + } catch (error) { + console.error('Error loading reading progress:', error) + } + return null + } + + // Calculate reading progress percentage + const calculateProgress = () => { + if (!books.length || !selectedBook || !selectedChapter) return 0 + + // Find current book index and total chapters before current position + let totalChaptersBefore = 0 + let foundCurrentBook = false + let currentBookChapters = 0 + + for (const book of books) { + if (book.id === selectedBook) { + foundCurrentBook = true + currentBookChapters = book.chaptersCount || 0 + // Add chapters from current book up to current chapter + totalChaptersBefore += selectedChapter + break + } + if (!foundCurrentBook) { + totalChaptersBefore += book.chaptersCount || 0 + } + } + + // Calculate total chapters in entire Bible + const totalChapters = books.reduce((sum, book) => sum + (book.chaptersCount || 0), 0) + + if (totalChapters === 0) return 0 + + const percentage = (totalChaptersBefore / totalChapters) * 100 + return Math.min(Math.round(percentage), 100) + } + const getThemeStyles = () => { switch (preferences.theme) { case 'dark': @@ -1432,6 +1556,33 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha )} + + {/* Reading Progress Bar */} + {user && books.length > 0 && ( + + + + Reading Progress + + + {calculateProgress()}% + + + + + )} ) diff --git a/app/api/user/reading-progress/route.ts b/app/api/user/reading-progress/route.ts new file mode 100644 index 0000000..da8bb29 --- /dev/null +++ b/app/api/user/reading-progress/route.ts @@ -0,0 +1,113 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/db' +import jwt from 'jsonwebtoken' + +export const runtime = 'nodejs' + +// Get user's reading progress for a specific version +export async function GET(request: Request) { + try { + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const token = authHeader.substring(7) + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string } + + const { searchParams } = new URL(request.url) + const versionId = searchParams.get('versionId') + + if (!versionId) { + // Get all reading progress for user + const allProgress = await prisma.readingHistory.findMany({ + where: { userId: decoded.userId }, + orderBy: { viewedAt: 'desc' } + }) + + return NextResponse.json({ + success: true, + progress: allProgress + }) + } + + // Get reading progress for specific version + const progress = await prisma.readingHistory.findUnique({ + where: { + userId_versionId: { + userId: decoded.userId, + versionId: versionId + } + } + }) + + return NextResponse.json({ + success: true, + progress: progress || null + }) + } catch (error) { + console.error('Error getting reading progress:', error) + return NextResponse.json( + { success: false, error: 'Failed to get reading progress' }, + { status: 500 } + ) + } +} + +// Save/update user's reading progress +export async function POST(request: Request) { + try { + const authHeader = request.headers.get('authorization') + if (!authHeader?.startsWith('Bearer ')) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const token = authHeader.substring(7) + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string } + + const body = await request.json() + const { versionId, bookId, chapterNum, verseNum } = body + + if (!versionId || !bookId || chapterNum === undefined) { + return NextResponse.json( + { success: false, error: 'versionId, bookId, and chapterNum are required' }, + { status: 400 } + ) + } + + // Upsert reading progress (update if exists, create if not) + const progress = await prisma.readingHistory.upsert({ + where: { + userId_versionId: { + userId: decoded.userId, + versionId: versionId + } + }, + update: { + bookId, + chapterNum, + verseNum, + viewedAt: new Date() + }, + create: { + userId: decoded.userId, + versionId, + bookId, + chapterNum, + verseNum + } + }) + + return NextResponse.json({ + success: true, + message: 'Reading progress saved', + progress + }) + } catch (error) { + console.error('Error saving reading progress:', error) + return NextResponse.json( + { success: false, error: 'Failed to save reading progress' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1a4ec24..498d22b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -277,6 +277,7 @@ model UserPrayer { model ReadingHistory { id String @id @default(uuid()) userId String + versionId String // Bible version ID bookId String chapterNum Int verseNum Int? @@ -285,6 +286,8 @@ model ReadingHistory { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, viewedAt]) + @@index([userId, versionId]) + @@unique([userId, versionId]) // Only one reading position per user per version } model UserPreference { diff --git a/public/sw.js b/public/sw.js index cce9cd3..dfbae38 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,8 @@ // Biblical Guide Service Worker -const CACHE_NAME = 'biblical-guide-v1.0.0'; -const STATIC_CACHE = 'biblical-guide-static-v1.0.0'; -const DYNAMIC_CACHE = 'biblical-guide-dynamic-v1.0.0'; -const BIBLE_CACHE = 'biblical-guide-bible-v1.0.0'; +const CACHE_NAME = 'biblical-guide-v1.0.1'; +const STATIC_CACHE = 'biblical-guide-static-v1.0.1'; +const DYNAMIC_CACHE = 'biblical-guide-dynamic-v1.0.1'; +const BIBLE_CACHE = 'biblical-guide-bible-v1.0.1'; // Static resources that should be cached immediately const STATIC_ASSETS = [ diff --git a/screenshots/IMG_1517.PNG b/screenshots/IMG_1517.PNG new file mode 100644 index 0000000..7422a68 Binary files /dev/null and b/screenshots/IMG_1517.PNG differ diff --git a/screenshots/IMG_1518.PNG b/screenshots/IMG_1518.PNG new file mode 100644 index 0000000..cfaf0bc Binary files /dev/null and b/screenshots/IMG_1518.PNG differ diff --git a/screenshots/Screenshot 2025-09-30 at 11.06.48.png b/screenshots/Screenshot 2025-09-30 at 11.06.48.png new file mode 100644 index 0000000..5706fbb Binary files /dev/null and b/screenshots/Screenshot 2025-09-30 at 11.06.48.png differ