Update App to pass user prop to components and add star buttons to BookSelector

This commit is contained in:
Ryderjj89
2025-09-13 18:14:53 -04:00
parent 5301154a20
commit 3e3869c117
2 changed files with 197 additions and 82 deletions

View File

@@ -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>

View File

@@ -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} />