Files
biblical-guide.com/app/[locale]/bible/page.tsx
andupetcu 686f498300 Create comprehensive bookmarks management page
Add complete bookmarks page with navigation functionality:

Features:
- Dedicated /bookmarks page for viewing all saved bookmarks
- Support for both chapter and verse bookmarks in unified view
- Statistics dashboard showing total, chapter, and verse bookmark counts
- Tabbed filtering (All, Chapters, Verses) for easy organization
- Direct navigation to Bible reading page with URL parameters
- Delete functionality for individual bookmarks
- Empty state with call-to-action to start reading

Navigation Integration:
- Add Bookmarks to main navigation menu (authenticated users only)
- Add Bookmarks to user profile dropdown menu
- Dynamic navigation based on authentication state

Bible Page Enhancements:
- URL parameter support for bookmark navigation (book, chapter, verse)
- Verse highlighting when navigating from bookmarks
- Auto-clear highlight after 3 seconds for better UX

API Endpoints:
- /api/bookmarks/all - Unified endpoint for all user bookmarks
- Returns transformed data optimized for frontend consumption

Multilingual Support:
- Full Romanian and English translations
- Consistent messaging across all bookmark interfaces

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 01:29:46 +03:00

564 lines
18 KiB
TypeScript

'use client'
import {
Container,
Grid,
Card,
CardContent,
Typography,
Box,
Select,
MenuItem,
FormControl,
InputLabel,
Paper,
List,
ListItem,
ListItemButton,
ListItemText,
Divider,
Button,
Chip,
useTheme,
CircularProgress,
Skeleton,
IconButton,
} from '@mui/material'
import {
MenuBook,
NavigateBefore,
NavigateNext,
Bookmark,
BookmarkBorder,
Share,
} from '@mui/icons-material'
import { useState, useEffect } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useSearchParams } from 'next/navigation'
interface BibleVerse {
id: string
verseNum: number
text: string
}
interface BibleChapter {
id: string
chapterNum: number
verses: BibleVerse[]
}
interface BibleBook {
id: string
name: string
testament: string
orderNum: number
bookKey: string
chapters: BibleChapter[]
}
export default function BiblePage() {
const theme = useTheme()
const t = useTranslations('pages.bible')
const locale = useLocale()
const searchParams = useSearchParams()
const [books, setBooks] = useState<BibleBook[]>([])
const [selectedBook, setSelectedBook] = useState<string>('')
const [selectedChapter, setSelectedChapter] = useState<number>(1)
const [verses, setVerses] = useState<BibleVerse[]>([])
const [loading, setLoading] = useState(true)
const [isBookmarked, setIsBookmarked] = useState(false)
const [bookmarkLoading, setBookmarkLoading] = useState(false)
const [verseBookmarks, setVerseBookmarks] = useState<{[key: string]: any}>({})
const [verseBookmarkLoading, setVerseBookmarkLoading] = useState<{[key: string]: boolean}>({})
const [highlightedVerse, setHighlightedVerse] = useState<number | null>(null)
const { user } = useAuth()
// Fetch available books
useEffect(() => {
fetch(`/api/bible/books?locale=${locale}`)
.then(res => res.json())
.then(data => {
setBooks(data.books || [])
if (data.books && data.books.length > 0) {
setSelectedBook(data.books[0].id)
}
setLoading(false)
})
.catch(err => {
console.error('Error fetching books:', err)
setLoading(false)
})
}, [])
// Handle URL parameters for navigation from bookmarks
useEffect(() => {
if (books.length > 0) {
const bookParam = searchParams.get('book')
const chapterParam = searchParams.get('chapter')
const verseParam = searchParams.get('verse')
if (bookParam) {
const book = books.find(b => b.id === bookParam)
if (book) {
setSelectedBook(bookParam)
if (chapterParam) {
const chapter = parseInt(chapterParam)
if (chapter > 0) {
setSelectedChapter(chapter)
}
}
if (verseParam) {
const verse = parseInt(verseParam)
if (verse > 0) {
setHighlightedVerse(verse)
// Clear highlight after 3 seconds
setTimeout(() => setHighlightedVerse(null), 3000)
}
}
}
}
}
}, [books, searchParams])
// Fetch verses when book/chapter changes
useEffect(() => {
if (selectedBook && selectedChapter) {
setLoading(true)
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`)
.then(res => res.json())
.then(data => {
setVerses(data.verses || [])
setLoading(false)
})
.catch(err => {
console.error('Error fetching verses:', err)
setLoading(false)
})
}
}, [selectedBook, selectedChapter])
// Check if chapter is bookmarked
useEffect(() => {
if (selectedBook && selectedChapter && user) {
const token = localStorage.getItem('authToken')
if (token) {
fetch(`/api/bookmarks/chapter/check?bookId=${selectedBook}&chapterNum=${selectedChapter}&locale=${locale}`, {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(res => res.json())
.then(data => {
setIsBookmarked(data.isBookmarked || false)
})
.catch(err => {
console.error('Error checking bookmark:', err)
})
}
} else {
setIsBookmarked(false)
}
}, [selectedBook, selectedChapter, user, locale])
// Check verse bookmarks when verses change
useEffect(() => {
if (verses.length > 0 && user) {
const token = localStorage.getItem('authToken')
if (token) {
const verseIds = verses.map(verse => verse.id)
fetch(`/api/bookmarks/verse/bulk-check?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verseIds })
})
.then(res => res.json())
.then(data => {
setVerseBookmarks(data.bookmarks || {})
})
.catch(err => {
console.error('Error checking verse bookmarks:', err)
})
}
} else {
setVerseBookmarks({})
}
}, [verses, user, locale])
const currentBook = books.find(book => book.id === selectedBook)
const maxChapters = currentBook?.chapters?.length || 50 // Default fallback
const handlePreviousChapter = () => {
if (selectedChapter > 1) {
setSelectedChapter(selectedChapter - 1)
} else if (selectedBook > 1) {
setSelectedBook(selectedBook - 1)
setSelectedChapter(50) // Will be adjusted by actual chapter count
}
}
const handleNextChapter = () => {
if (selectedChapter < maxChapters) {
setSelectedChapter(selectedChapter + 1)
} else {
const nextBook = books.find(book => book.id === selectedBook + 1)
if (nextBook) {
setSelectedBook(selectedBook + 1)
setSelectedChapter(1)
}
}
}
const handleBookmarkToggle = async () => {
if (!user || !selectedBook || !selectedChapter) return
setBookmarkLoading(true)
const token = localStorage.getItem('authToken')
if (!token) {
setBookmarkLoading(false)
return
}
try {
if (isBookmarked) {
// Remove bookmark
const response = await fetch(`/api/bookmarks/chapter?bookId=${selectedBook}&chapterNum=${selectedChapter}&locale=${locale}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
setIsBookmarked(false)
}
} else {
// Add bookmark
const response = await fetch(`/api/bookmarks/chapter?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
bookId: selectedBook,
chapterNum: selectedChapter
})
})
if (response.ok) {
setIsBookmarked(true)
}
}
} catch (error) {
console.error('Error toggling bookmark:', error)
} finally {
setBookmarkLoading(false)
}
}
const handleVerseBookmarkToggle = async (verse: BibleVerse) => {
if (!user) return
setVerseBookmarkLoading(prev => ({ ...prev, [verse.id]: true }))
const token = localStorage.getItem('authToken')
if (!token) {
setVerseBookmarkLoading(prev => ({ ...prev, [verse.id]: false }))
return
}
try {
const isCurrentlyBookmarked = !!verseBookmarks[verse.id]
if (isCurrentlyBookmarked) {
// Remove verse bookmark
const response = await fetch(`/api/bookmarks/verse?verseId=${verse.id}&locale=${locale}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
setVerseBookmarks(prev => {
const newBookmarks = { ...prev }
delete newBookmarks[verse.id]
return newBookmarks
})
}
} else {
// Add verse bookmark
const response = await fetch(`/api/bookmarks/verse?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
verseId: verse.id
})
})
if (response.ok) {
const data = await response.json()
setVerseBookmarks(prev => ({
...prev,
[verse.id]: data.bookmark
}))
}
}
} catch (error) {
console.error('Error toggling verse bookmark:', error)
} finally {
setVerseBookmarkLoading(prev => ({ ...prev, [verse.id]: false }))
}
}
if (loading && books.length === 0) {
return (
<Box>
<Container maxWidth="lg" sx={{ py: 4 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<CircularProgress size={48} />
<Typography variant="h6" color="text.secondary">
{t('loading')}
</Typography>
</Box>
</Container>
</Box>
)
}
return (
<Box>
<Container maxWidth="lg" sx={{ py: 4 }}>
{/* Header */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h3" component="h1" gutterBottom>
<MenuBook sx={{ fontSize: 40, mr: 2, verticalAlign: 'middle' }} />
{t('title')}
</Typography>
<Typography variant="body1" color="text.secondary">
{t('subtitle')}
</Typography>
</Box>
<Grid container spacing={4}>
{/* Left Sidebar - Book Selection */}
<Grid item xs={12} md={3}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{t('selectBook')}
</Typography>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>{t('book')}</InputLabel>
<Select
value={selectedBook}
label={t('book')}
onChange={(e) => {
setSelectedBook(e.target.value)
setSelectedChapter(1)
}}
>
{books.map((book) => (
<MenuItem key={book.id} value={book.id}>
{book.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>{t('chapter')}</InputLabel>
<Select
value={selectedChapter}
label={t('chapter')}
onChange={(e) => setSelectedChapter(Number(e.target.value))}
>
{Array.from({ length: maxChapters }, (_, i) => (
<MenuItem key={i + 1} value={i + 1}>
{t('chapter')} {i + 1}
</MenuItem>
))}
</Select>
</FormControl>
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<Chip
label={currentBook?.testament || 'Vechiul Testament'}
size="small"
color="primary"
variant="outlined"
/>
</Box>
</CardContent>
</Card>
</Grid>
{/* Main Content - Bible Text */}
<Grid item xs={12} md={9}>
<Card>
<CardContent>
{/* Chapter Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" component="h2">
{currentBook?.name || 'Geneza'} {selectedChapter}
</Typography>
<Typography variant="body2" color="text.secondary">
{verses.length} {t('verses')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
startIcon={<Bookmark />}
variant={isBookmarked ? "contained" : "outlined"}
size="small"
onClick={handleBookmarkToggle}
disabled={!user || bookmarkLoading}
color={isBookmarked ? "primary" : "inherit"}
>
{bookmarkLoading ? t('saving') : (isBookmarked ? t('saved') : t('save'))}
</Button>
<Button
startIcon={<Share />}
variant="outlined"
size="small"
>
{t('share')}
</Button>
</Box>
</Box>
<Divider sx={{ mb: 3 }} />
{/* Bible Verses */}
{loading ? (
<Box>
{Array.from({ length: 5 }).map((_, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Skeleton variant="text" width="100%" height={40} />
<Skeleton variant="text" width="90%" height={30} />
<Skeleton variant="text" width="95%" height={30} />
</Box>
))}
</Box>
) : verses.length > 0 ? (
<Box>
{verses.map((verse) => {
const isVerseBookmarked = !!verseBookmarks[verse.id]
const isVerseLoading = !!verseBookmarkLoading[verse.id]
return (
<Box
key={verse.id}
sx={{
mb: 2,
display: 'flex',
alignItems: 'flex-start',
gap: 1,
'&:hover .verse-bookmark': {
opacity: 1
}
}}
>
<Box sx={{ flex: 1 }}>
<Typography
variant="body1"
component="p"
sx={{
lineHeight: 1.8,
fontSize: '1.1rem',
bgcolor: highlightedVerse === verse.verseNum
? 'primary.light'
: isVerseBookmarked
? 'warning.light'
: 'transparent',
borderRadius: (isVerseBookmarked || highlightedVerse === verse.verseNum) ? 1 : 0,
p: (isVerseBookmarked || highlightedVerse === verse.verseNum) ? 1 : 0,
transition: 'all 0.3s ease',
border: highlightedVerse === verse.verseNum ? '2px solid' : 'none',
borderColor: 'primary.main',
}}
>
<Typography
component="span"
sx={{
fontWeight: 'bold',
color: 'primary.main',
mr: 1,
fontSize: '0.9rem',
}}
>
{verse.verseNum}
</Typography>
{verse.text}
</Typography>
</Box>
{user && (
<IconButton
className="verse-bookmark"
size="small"
onClick={() => handleVerseBookmarkToggle(verse)}
disabled={isVerseLoading}
sx={{
opacity: isVerseBookmarked ? 1 : 0.3,
transition: 'opacity 0.2s',
color: isVerseBookmarked ? 'warning.main' : 'action.active'
}}
>
{isVerseBookmarked ? <Bookmark fontSize="small" /> : <BookmarkBorder fontSize="small" />}
</IconButton>
)}
</Box>
)
})}
</Box>
) : (
<Typography textAlign="center" color="text.secondary">
{t('noVerses')}
</Typography>
)}
{/* Navigation */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4, pt: 3, borderTop: 1, borderColor: 'divider' }}>
<Button
startIcon={<NavigateBefore />}
onClick={handlePreviousChapter}
disabled={selectedBook === 1 && selectedChapter === 1}
>
{t('previousChapter')}
</Button>
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: 'center' }}>
{currentBook?.name} {selectedChapter}
</Typography>
<Button
endIcon={<NavigateNext />}
onClick={handleNextChapter}
disabled={selectedBook === books.length && selectedChapter === maxChapters}
>
{t('nextChapter')}
</Button>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
</Box>
)
}