Update App to pass user prop to components and add star buttons to BookSelector
This commit is contained in:
@@ -11,72 +11,6 @@ interface BookData {
|
||||
books: string[];
|
||||
}
|
||||
|
||||
// Component for the home page
|
||||
function HomePage({ books, formatBookName, getBookUrlName }: { books: string[], formatBookName: (name: string) => string, getBookUrlName: (name: string) => string }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBookSelect = (book: string) => {
|
||||
const urlName = getBookUrlName(book);
|
||||
navigate(`/book/${urlName}`);
|
||||
};
|
||||
|
||||
return <BookSelector books={books} onBookSelect={handleBookSelect} formatBookName={formatBookName} />;
|
||||
}
|
||||
|
||||
// Component for book chapters page
|
||||
function BookPage({ books, formatBookName, getBookFromUrl }: { books: string[], formatBookName: (name: string) => string, getBookFromUrl: (urlName: string) => string }) {
|
||||
const { bookName } = useParams<{ bookName: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const actualBookName = bookName ? getBookFromUrl(bookName) : '';
|
||||
|
||||
const handleChapterSelect = (chapter: string) => {
|
||||
navigate(`/book/${bookName}/chapter/${chapter}`);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
if (!bookName || !actualBookName || !books.includes(actualBookName)) {
|
||||
return <div>Book not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChapterSelector
|
||||
book={actualBookName}
|
||||
onChapterSelect={handleChapterSelect}
|
||||
onBack={handleBack}
|
||||
formatBookName={formatBookName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Component for chapter reading page
|
||||
function ChapterPage({ formatBookName, getBookFromUrl }: { formatBookName: (name: string) => string, getBookFromUrl: (urlName: string) => string }) {
|
||||
const { bookName, chapterNumber } = useParams<{ bookName: string, chapterNumber: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const actualBookName = bookName ? getBookFromUrl(bookName) : '';
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(`/book/${bookName}`);
|
||||
};
|
||||
|
||||
if (!bookName || !chapterNumber || !actualBookName) {
|
||||
return <div>Chapter not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BibleReader
|
||||
book={actualBookName}
|
||||
chapter={chapterNumber}
|
||||
onBack={handleBack}
|
||||
formatBookName={formatBookName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [books, setBooks] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -274,6 +208,75 @@ function App() {
|
||||
|
||||
const { currentBook, currentChapter } = getCurrentNavInfo();
|
||||
|
||||
// Component for the home page
|
||||
const HomePage = () => {
|
||||
const handleBookSelect = (book: string) => {
|
||||
const urlName = getBookUrlName(book);
|
||||
navigate(`/book/${urlName}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<BookSelector
|
||||
books={books}
|
||||
onBookSelect={handleBookSelect}
|
||||
formatBookName={formatBookName}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Component for book chapters page
|
||||
const BookPage = () => {
|
||||
const { bookName } = useParams<{ bookName: string }>();
|
||||
const actualBookName = bookName ? getBookFromUrl(bookName) : '';
|
||||
|
||||
const handleChapterSelect = (chapter: string) => {
|
||||
navigate(`/book/${bookName}/chapter/${chapter}`);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
if (!bookName || !actualBookName || !books.includes(actualBookName)) {
|
||||
return <div>Book not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChapterSelector
|
||||
book={actualBookName}
|
||||
onChapterSelect={handleChapterSelect}
|
||||
onBack={handleBack}
|
||||
formatBookName={formatBookName}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Component for chapter reading page
|
||||
const ChapterPage = () => {
|
||||
const { bookName, chapterNumber } = useParams<{ bookName: string, chapterNumber: string }>();
|
||||
const actualBookName = bookName ? getBookFromUrl(bookName) : '';
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(`/book/${bookName}`);
|
||||
};
|
||||
|
||||
if (!bookName || !chapterNumber || !actualBookName) {
|
||||
return <div>Chapter not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BibleReader
|
||||
book={actualBookName}
|
||||
chapter={chapterNumber}
|
||||
onBack={handleBack}
|
||||
formatBookName={formatBookName}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
@@ -407,9 +410,9 @@ function App() {
|
||||
{/* Main Content */}
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage books={books} formatBookName={formatBookName} getBookUrlName={getBookUrlName} />} />
|
||||
<Route path="/book/:bookName" element={<BookPage books={books} formatBookName={formatBookName} getBookFromUrl={getBookFromUrl} />} />
|
||||
<Route path="/book/:bookName/chapter/:chapterNumber" element={<ChapterPage formatBookName={formatBookName} getBookFromUrl={getBookFromUrl} />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/book/:bookName" element={<BookPage />} />
|
||||
<Route path="/book/:bookName/chapter/:chapterNumber" element={<ChapterPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,102 @@
|
||||
import React from 'react';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BookOpen, Star } from 'lucide-react';
|
||||
|
||||
interface BookSelectorProps {
|
||||
books: string[];
|
||||
onBookSelect: (book: string) => void;
|
||||
formatBookName: (bookName: string) => string;
|
||||
user?: any;
|
||||
}
|
||||
|
||||
const BookSelector: React.FC<BookSelectorProps> = ({ books, onBookSelect, formatBookName }) => {
|
||||
const BookSelector: React.FC<BookSelectorProps> = ({ books, onBookSelect, formatBookName, user }) => {
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 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();
|
||||
const bookFavorites = new Set(
|
||||
data.favorites
|
||||
.filter((fav: any) => !fav.chapter) // Only book-level favorites
|
||||
.map((fav: any) => fav.book)
|
||||
);
|
||||
setFavorites(bookFavorites);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load favorites:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFavorite = async (book: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation(); // Prevent book selection when clicking star
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const isFavorited = favorites.has(book);
|
||||
|
||||
try {
|
||||
if (isFavorited) {
|
||||
// Remove favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const bookFavorite = data.favorites.find((fav: any) => fav.book === book && !fav.chapter);
|
||||
|
||||
if (bookFavorite) {
|
||||
await fetch(`/api/favorites/${bookFavorite.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
setFavorites(prev => {
|
||||
const newFavorites = new Set(prev);
|
||||
newFavorites.delete(book);
|
||||
return newFavorites;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setFavorites(prev => new Set(prev).add(book));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Group books by testament
|
||||
const oldTestament = books.slice(0, 39); // First 39 books
|
||||
const newTestament = books.slice(39); // Remaining books
|
||||
@@ -19,16 +108,34 @@ const BookSelector: React.FC<BookSelectorProps> = ({ books, onBookSelect, format
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{books.map((book) => (
|
||||
<button
|
||||
key={book}
|
||||
onClick={() => onBookSelect(book)}
|
||||
className="group bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-200 text-center"
|
||||
>
|
||||
<BookOpen className="mx-auto h-8 w-8 text-blue-600 dark:text-blue-400 mb-2 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
|
||||
{formatBookName(book)}
|
||||
</span>
|
||||
</button>
|
||||
<div key={book} className="relative">
|
||||
<button
|
||||
onClick={() => onBookSelect(book)}
|
||||
className="group bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-200 text-center w-full"
|
||||
>
|
||||
<BookOpen className="mx-auto h-8 w-8 text-blue-600 dark:text-blue-400 mb-2 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
|
||||
{formatBookName(book)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Star button - only show for authenticated users */}
|
||||
{user && (
|
||||
<button
|
||||
onClick={(e) => toggleFavorite(book, e)}
|
||||
className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title={favorites.has(book) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Star
|
||||
className={`h-4 w-4 ${
|
||||
favorites.has(book)
|
||||
? 'text-yellow-500 fill-yellow-500'
|
||||
: 'text-gray-400 hover:text-yellow-500'
|
||||
} transition-colors`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,6 +150,11 @@ const BookSelector: React.FC<BookSelectorProps> = ({ books, onBookSelect, format
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
Select a book to begin reading
|
||||
</p>
|
||||
{user && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
Click the ★ to add books to your favorites
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookGroup title="Old Testament" books={oldTestament} />
|
||||
|
||||
Reference in New Issue
Block a user