diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5fdf2b8b..4be0dc5d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,6 +4,7 @@ import { Book, ChevronRight, Moon, Sun } from 'lucide-react';
import BookSelector from './components/BookSelector';
import ChapterSelector from './components/ChapterSelector';
import BibleReader from './components/BibleReader';
+import FavoritesMenu from './components/FavoritesMenu';
import { getBooks } from './services/api';
interface BookData {
@@ -223,7 +224,7 @@ function App() {
// Helper function to convert display name back to file name
const getBookFileName = (displayName: string): string => {
// Find the book that matches the display name
- const book = books.find(b => formatBookName(b) === displayName);
+ const book = books.find((b: string) => formatBookName(b) === displayName);
return book || displayName;
};
@@ -352,8 +353,15 @@ function App() {
)}
- {/* User Authentication & Dark Mode */}
+ {/* User Authentication, Favorites & Dark Mode */}
+ {/* Favorites Menu - Only for authenticated users */}
+
+
{/* Authentication Button */}
{authAvailable && (
diff --git a/frontend/src/components/FavoritesMenu.tsx b/frontend/src/components/FavoritesMenu.tsx
new file mode 100644
index 00000000..dbfb2f18
--- /dev/null
+++ b/frontend/src/components/FavoritesMenu.tsx
@@ -0,0 +1,180 @@
+import React, { useState, useEffect } from 'react';
+import { Star, ChevronDown, ChevronUp, X } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+
+interface Favorite {
+ id: number;
+ book: string;
+ chapter?: string;
+ verse_start?: number;
+ verse_end?: number;
+ note?: string;
+ created_at: string;
+}
+
+interface FavoritesMenuProps {
+ user: any;
+ formatBookName: (bookName: string) => string;
+ getBookUrlName: (bookName: string) => string;
+}
+
+const FavoritesMenu: React.FC
= ({ user, formatBookName, getBookUrlName }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [favorites, setFavorites] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ // Load favorites when user is available
+ useEffect(() => {
+ if (user) {
+ loadFavorites();
+ }
+ }, [user]);
+
+ const loadFavorites = async () => {
+ if (!user) return;
+
+ setLoading(true);
+ try {
+ const response = await fetch('/api/favorites', {
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setFavorites(data.favorites || []);
+ }
+ } catch (error) {
+ console.error('Failed to load favorites:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const removeFavorite = async (favoriteId: number) => {
+ try {
+ const response = await fetch(`/api/favorites/${favoriteId}`, {
+ method: 'DELETE',
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ setFavorites(favorites.filter(f => f.id !== favoriteId));
+ }
+ } catch (error) {
+ console.error('Failed to remove favorite:', error);
+ }
+ };
+
+ const navigateToFavorite = (favorite: Favorite) => {
+ const urlBookName = getBookUrlName(favorite.book);
+
+ if (favorite.chapter) {
+ // Navigate to chapter
+ navigate(`/book/${urlBookName}/chapter/${favorite.chapter}`);
+ } else {
+ // Navigate to book
+ navigate(`/book/${urlBookName}`);
+ }
+ setIsOpen(false);
+ };
+
+ const getFavoriteDisplayText = (favorite: Favorite) => {
+ const bookName = formatBookName(favorite.book);
+
+ if (favorite.verse_start && favorite.verse_end) {
+ return `${bookName} ${favorite.chapter}:${favorite.verse_start}-${favorite.verse_end}`;
+ } else if (favorite.verse_start) {
+ return `${bookName} ${favorite.chapter}:${favorite.verse_start}`;
+ } else if (favorite.chapter) {
+ return `${bookName} Chapter ${favorite.chapter}`;
+ } else {
+ return bookName;
+ }
+ };
+
+ if (!user) {
+ return null; // Don't show favorites menu for non-authenticated users
+ }
+
+ return (
+
+ {/* Favorites Toggle Button */}
+
setIsOpen(!isOpen)}
+ className="flex items-center space-x-1 px-3 py-1 text-sm bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded hover:bg-yellow-200 dark:hover:bg-yellow-800 transition-colors"
+ >
+
+ Favorites
+ ★
+ {favorites.length > 0 && (
+
+ {favorites.length}
+
+ )}
+ {isOpen ? : }
+
+
+ {/* Favorites Dropdown */}
+ {isOpen && (
+
+
+
+
My Favorites
+ setIsOpen(false)}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
+ >
+
+
+
+
+
+
+ {loading ? (
+
Loading favorites...
+ ) : favorites.length === 0 ? (
+
+
+
No favorites yet
+
Click the ★ next to books, chapters, or verses to add them here
+
+ ) : (
+
+ {favorites.map((favorite) => (
+
+
navigateToFavorite(favorite)}
+ className="flex-1 text-left"
+ >
+
+ {getFavoriteDisplayText(favorite)}
+
+ {favorite.note && (
+
+ {favorite.note}
+
+ )}
+
+
removeFavorite(favorite.id)}
+ className="p-1 opacity-0 group-hover:opacity-100 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-opacity"
+ title="Remove favorite"
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ );
+};
+
+export default FavoritesMenu;