From ceb20b88cedb7538fd93e304e0fb3daba3cfde4f Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Sun, 21 Sep 2025 01:25:50 +0300 Subject: [PATCH] Implement complete bookmark functionality for Bible reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add both chapter-level and verse-level bookmarking capabilities: Database Changes: - Add ChapterBookmark table for chapter-level bookmarks - Update schema with proper relationships to User and BibleBook models - Maintain existing Bookmark table for verse-level bookmarks API Endpoints: - /api/bookmarks/chapter (GET, POST, DELETE) with check endpoint - /api/bookmarks/verse (GET, POST, DELETE) with check and bulk-check endpoints - JWT authentication required for all bookmark operations - Multilingual error messages (Romanian/English) Frontend Implementation: - Chapter bookmark button in Bible page header with visual state feedback - Individual verse bookmark icons with hover-to-reveal UI - Highlighted background for bookmarked verses - Efficient bulk checking for verse bookmarks per chapter - Real-time UI updates without page refresh šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/[locale]/bible/page.tsx | 260 +++++++++++++++++--- app/api/bookmarks/chapter/check/route.ts | 75 ++++++ app/api/bookmarks/chapter/route.ts | 199 +++++++++++++++ app/api/bookmarks/verse/bulk-check/route.ts | 80 ++++++ app/api/bookmarks/verse/check/route.ts | 73 ++++++ app/api/bookmarks/verse/route.ts | 213 ++++++++++++++++ messages/en.json | 2 + messages/ro.json | 2 + prisma/schema.prisma | 17 ++ 9 files changed, 890 insertions(+), 31 deletions(-) create mode 100644 app/api/bookmarks/chapter/check/route.ts create mode 100644 app/api/bookmarks/chapter/route.ts create mode 100644 app/api/bookmarks/verse/bulk-check/route.ts create mode 100644 app/api/bookmarks/verse/check/route.ts create mode 100644 app/api/bookmarks/verse/route.ts diff --git a/app/[locale]/bible/page.tsx b/app/[locale]/bible/page.tsx index 744b09b..0f57079 100644 --- a/app/[locale]/bible/page.tsx +++ b/app/[locale]/bible/page.tsx @@ -21,16 +21,19 @@ import { 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' interface BibleVerse { id: string @@ -62,6 +65,11 @@ export default function BiblePage() { const [selectedChapter, setSelectedChapter] = useState(1) const [verses, setVerses] = useState([]) 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 { user } = useAuth() // Fetch available books useEffect(() => { @@ -97,6 +105,56 @@ export default function BiblePage() { } }, [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 @@ -121,6 +179,113 @@ export default function BiblePage() { } } + 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 ( @@ -223,10 +388,13 @@ export default function BiblePage() {