diff --git a/backend/src/database.js b/backend/src/database.js
index 69e86340..99f1c7c8 100644
--- a/backend/src/database.js
+++ b/backend/src/database.js
@@ -65,6 +65,21 @@ function initializeTables() {
}
});
+ // Favorite sections table
+ db.run(`
+ CREATE TABLE IF NOT EXISTS favorite_sections (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ color TEXT DEFAULT '#6B7280',
+ sort_order INTEGER DEFAULT 0,
+ is_default BOOLEAN DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
+ UNIQUE(user_id, name)
+ )
+ `);
+
// Favorites table (with IF NOT EXISTS for safety)
db.run(`
CREATE TABLE IF NOT EXISTS favorites (
@@ -76,12 +91,28 @@ function initializeTables() {
verse_end INTEGER,
version TEXT DEFAULT 'esv',
note TEXT,
+ section_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
+ FOREIGN KEY (section_id) REFERENCES favorite_sections (id) ON DELETE SET NULL,
UNIQUE(user_id, book, chapter, verse_start, verse_end, version)
)
`);
+ // Migration: Add section_id column to existing favorites table if it doesn't exist
+ db.all("PRAGMA table_info(favorites)", [], (err, columns) => {
+ if (!err && columns.length > 0) {
+ const hasSectionIdColumn = columns.some(col => col.name === 'section_id');
+ if (!hasSectionIdColumn) {
+ db.run("ALTER TABLE favorites ADD COLUMN section_id INTEGER REFERENCES favorite_sections(id) ON DELETE SET NULL", (err) => {
+ if (!err) {
+ console.log('Added section_id column to favorites table');
+ }
+ });
+ }
+ }
+ });
+
console.log('Database tables initialized');
}
@@ -171,12 +202,126 @@ const preferencesOps = {
}
};
+// Sections operations
+const sectionsOps = {
+ // Get all sections for user
+ getSections: (userId, callback) => {
+ db.all(
+ 'SELECT * FROM favorite_sections WHERE user_id = ? ORDER BY sort_order ASC, created_at ASC',
+ [userId],
+ callback
+ );
+ },
+
+ // Create a new section
+ createSection: (userId, section, callback) => {
+ const { name, color } = section;
+
+ // Get the next sort_order
+ db.get(
+ 'SELECT COALESCE(MAX(sort_order), -1) + 1 as next_order FROM favorite_sections WHERE user_id = ?',
+ [userId],
+ (err, row) => {
+ if (err) return callback(err);
+
+ db.run(
+ `INSERT INTO favorite_sections (user_id, name, color, sort_order)
+ VALUES (?, ?, ?, ?)`,
+ [userId, name, color || '#6B7280', row.next_order],
+ function(err) {
+ if (err) return callback(err);
+ callback(null, { id: this.lastID, name, color: color || '#6B7280', sort_order: row.next_order });
+ }
+ );
+ }
+ );
+ },
+
+ // Update a section
+ updateSection: (userId, sectionId, updates, callback) => {
+ const { name, color, is_default } = updates;
+
+ // If setting as default, first unset any existing default
+ if (is_default) {
+ db.run(
+ 'UPDATE favorite_sections SET is_default = 0 WHERE user_id = ?',
+ [userId],
+ (err) => {
+ if (err) return callback(err);
+
+ db.run(
+ `UPDATE favorite_sections
+ SET name = COALESCE(?, name),
+ color = COALESCE(?, color),
+ is_default = ?
+ WHERE id = ? AND user_id = ?`,
+ [name, color, is_default ? 1 : 0, sectionId, userId],
+ callback
+ );
+ }
+ );
+ } else {
+ db.run(
+ `UPDATE favorite_sections
+ SET name = COALESCE(?, name),
+ color = COALESCE(?, color),
+ is_default = COALESCE(?, is_default)
+ WHERE id = ? AND user_id = ?`,
+ [name, color, is_default !== undefined ? (is_default ? 1 : 0) : null, sectionId, userId],
+ callback
+ );
+ }
+ },
+
+ // Delete a section (favorites will have section_id set to NULL via ON DELETE SET NULL)
+ deleteSection: (userId, sectionId, callback) => {
+ db.run(
+ 'DELETE FROM favorite_sections WHERE id = ? AND user_id = ?',
+ [sectionId, userId],
+ callback
+ );
+ },
+
+ // Reorder sections
+ reorderSections: (userId, sectionIds, callback) => {
+ const updates = sectionIds.map((id, index) => {
+ return new Promise((resolve, reject) => {
+ db.run(
+ 'UPDATE favorite_sections SET sort_order = ? WHERE id = ? AND user_id = ?',
+ [index, id, userId],
+ (err) => {
+ if (err) reject(err);
+ else resolve();
+ }
+ );
+ });
+ });
+
+ Promise.all(updates)
+ .then(() => callback(null))
+ .catch(callback);
+ },
+
+ // Get default section for user
+ getDefaultSection: (userId, callback) => {
+ db.get(
+ 'SELECT * FROM favorite_sections WHERE user_id = ? AND is_default = 1',
+ [userId],
+ callback
+ );
+ }
+};
+
// Favorites operations
const favoritesOps = {
- // Get user favorites
+ // Get user favorites with section info
getFavorites: (userId, callback) => {
db.all(
- 'SELECT * FROM favorites WHERE user_id = ? ORDER BY created_at DESC',
+ `SELECT f.*, s.name as section_name, s.color as section_color
+ FROM favorites f
+ LEFT JOIN favorite_sections s ON f.section_id = s.id
+ WHERE f.user_id = ?
+ ORDER BY f.created_at DESC`,
[userId],
callback
);
@@ -184,12 +329,12 @@ const favoritesOps = {
// Add favorite
addFavorite: (userId, favorite, callback) => {
- const { book, chapter, verse_start, verse_end, version, note } = favorite;
+ const { book, chapter, verse_start, verse_end, version, note, section_id } = favorite;
db.run(
- `INSERT INTO favorites (user_id, book, chapter, verse_start, verse_end, version, note)
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
- [userId, book, chapter, verse_start, verse_end, version || 'esv', note],
+ `INSERT INTO favorites (user_id, book, chapter, verse_start, verse_end, version, note, section_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+ [userId, book, chapter, verse_start, verse_end, version || 'esv', note, section_id || null],
callback
);
},
@@ -203,6 +348,15 @@ const favoritesOps = {
);
},
+ // Update favorite's section
+ updateFavoriteSection: (userId, favoriteId, sectionId, callback) => {
+ db.run(
+ 'UPDATE favorites SET section_id = ? WHERE id = ? AND user_id = ?',
+ [sectionId, favoriteId, userId],
+ callback
+ );
+ },
+
// Check if verse is favorited
isFavorited: (userId, book, chapter, verse_start, verse_end, version, callback) => {
db.get(
@@ -220,5 +374,6 @@ module.exports = {
db,
userOps,
preferencesOps,
- favoritesOps
+ favoritesOps,
+ sectionsOps
};
diff --git a/backend/src/index.js b/backend/src/index.js
index 58a5cc25..81b7ba5a 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -4,7 +4,7 @@ const helmet = require('helmet');
const path = require('path');
const fs = require('fs').promises;
const { configureAuth, requireAuth, optionalAuth } = require('./auth');
-const { preferencesOps, favoritesOps } = require('./database');
+const { preferencesOps, favoritesOps, sectionsOps } = require('./database');
const SearchDatabase = require('./searchDatabase');
const app = express();
@@ -384,7 +384,7 @@ app.get('/api/favorites', requireAuth, (req, res) => {
});
app.post('/api/favorites', requireAuth, (req, res) => {
- const { book, chapter, verse_start, verse_end, note, version } = req.body;
+ const { book, chapter, verse_start, verse_end, note, version, section_id } = req.body;
// Book is required, but chapter is optional (for book-level favorites)
if (!book) {
@@ -397,7 +397,8 @@ app.post('/api/favorites', requireAuth, (req, res) => {
verse_start: verse_start || null,
verse_end: verse_end || null,
version: version || 'esv',
- note: note || null
+ note: note || null,
+ section_id: section_id || null
};
favoritesOps.addFavorite(req.user.id, favorite, function(err) {
@@ -407,7 +408,7 @@ app.post('/api/favorites', requireAuth, (req, res) => {
}
return res.status(500).json({ error: 'Failed to add favorite' });
}
- res.json({
+ res.json({
message: 'Favorite added successfully',
id: this.lastID
});
@@ -427,16 +428,16 @@ app.delete('/api/favorites/:id', requireAuth, (req, res) => {
app.get('/api/favorites/check', requireAuth, (req, res) => {
const { book, chapter, verse_start, verse_end } = req.query;
-
+
if (!book) {
return res.status(400).json({ error: 'Book is required' });
}
favoritesOps.isFavorited(
- req.user.id,
- book,
- chapter || null,
- verse_start || null,
+ req.user.id,
+ book,
+ chapter || null,
+ verse_start || null,
verse_end || null,
(err, isFavorited) => {
if (err) {
@@ -447,6 +448,91 @@ app.get('/api/favorites/check', requireAuth, (req, res) => {
);
});
+// Update favorite's section
+app.put('/api/favorites/:id/section', requireAuth, (req, res) => {
+ const favoriteId = req.params.id;
+ const { section_id } = req.body;
+
+ favoritesOps.updateFavoriteSection(req.user.id, favoriteId, section_id, (err) => {
+ if (err) {
+ return res.status(500).json({ error: 'Failed to update favorite section' });
+ }
+ res.json({ message: 'Favorite section updated successfully' });
+ });
+});
+
+// Sections routes
+app.get('/api/sections', requireAuth, (req, res) => {
+ sectionsOps.getSections(req.user.id, (err, sections) => {
+ if (err) {
+ return res.status(500).json({ error: 'Failed to get sections' });
+ }
+ res.json({ sections });
+ });
+});
+
+app.post('/api/sections', requireAuth, (req, res) => {
+ const { name, color } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: 'Section name is required' });
+ }
+
+ sectionsOps.createSection(req.user.id, { name: name.trim(), color }, (err, section) => {
+ if (err) {
+ if (err.code === 'SQLITE_CONSTRAINT') {
+ return res.status(409).json({ error: 'A section with this name already exists' });
+ }
+ return res.status(500).json({ error: 'Failed to create section' });
+ }
+ res.json({
+ message: 'Section created successfully',
+ section
+ });
+ });
+});
+
+app.put('/api/sections/:id', requireAuth, (req, res) => {
+ const sectionId = req.params.id;
+ const { name, color, is_default } = req.body;
+
+ sectionsOps.updateSection(req.user.id, sectionId, { name, color, is_default }, (err) => {
+ if (err) {
+ if (err.code === 'SQLITE_CONSTRAINT') {
+ return res.status(409).json({ error: 'A section with this name already exists' });
+ }
+ return res.status(500).json({ error: 'Failed to update section' });
+ }
+ res.json({ message: 'Section updated successfully' });
+ });
+});
+
+app.delete('/api/sections/:id', requireAuth, (req, res) => {
+ const sectionId = req.params.id;
+
+ sectionsOps.deleteSection(req.user.id, sectionId, (err) => {
+ if (err) {
+ return res.status(500).json({ error: 'Failed to delete section' });
+ }
+ res.json({ message: 'Section deleted successfully' });
+ });
+});
+
+app.put('/api/sections/reorder', requireAuth, (req, res) => {
+ const { sectionIds } = req.body;
+
+ if (!Array.isArray(sectionIds)) {
+ return res.status(400).json({ error: 'sectionIds must be an array' });
+ }
+
+ sectionsOps.reorderSections(req.user.id, sectionIds, (err) => {
+ if (err) {
+ return res.status(500).json({ error: 'Failed to reorder sections' });
+ }
+ res.json({ message: 'Sections reordered successfully' });
+ });
+});
+
// Catch-all handler: send back React's index.html for client-side routing
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../../frontend/build/index.html'));
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8f2d4273..4f46716b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -8,6 +8,7 @@ import FavoritesMenu from './components/FavoritesMenu';
import SearchComponent from './components/SearchComponent';
import VersionSelector from './components/VersionSelector';
import { getBooks } from './services/api';
+import { Section } from './types/favorites';
interface BookData {
books: string[];
@@ -29,6 +30,7 @@ function App() {
const [versionSelected, setVersionSelected] = useState(false); // Track if version has been chosen
const [availableVersions, setAvailableVersions] = useState([]);
const [favorites, setFavorites] = useState([]); // Centralized favorites state
+ const [sections, setSections] = useState([]); // Sections for organizing favorites
const location = useLocation();
const navigate = useNavigate();
@@ -72,8 +74,10 @@ function App() {
if (user) {
loadUserPreferences();
loadFavorites(); // Load favorites once when user logs in
+ loadSections(); // Load sections when user logs in
} else {
setFavorites([]); // Clear favorites when user logs out
+ setSections([]); // Clear sections when user logs out
}
}, [user]);
@@ -278,6 +282,34 @@ function App() {
}
};
+ // Load sections for the user
+ const loadSections = async () => {
+ if (!user) {
+ setSections([]);
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/sections', {
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSections(data.sections);
+ console.log('Loaded sections:', data.sections.length);
+ }
+ } catch (error) {
+ console.error('Failed to load sections:', error);
+ setSections([]);
+ }
+ };
+
+ // Handler for section changes (create, update, delete)
+ const handleSectionChange = async () => {
+ await loadSections();
+ };
+
// Helper functions to filter favorites by type
const getBookFavorites = (version: string) => {
return new Set(
@@ -361,6 +393,8 @@ function App() {
onBack={handleBack}
onSearchClick={() => setShowSearch(true)}
favorites={versionId ? getBookFavorites(versionId) : new Set()}
+ sections={sections}
+ onSectionChange={handleSectionChange}
/>
);
};
@@ -393,6 +427,8 @@ function App() {
version={versionId}
onSearchClick={() => setShowSearch(true)}
favorites={versionId ? getChapterFavorites(actualBookName, versionId) : new Set()}
+ sections={sections}
+ onSectionChange={handleSectionChange}
/>
);
};
@@ -421,6 +457,8 @@ function App() {
version={selectedVersion}
onSearchClick={() => setShowSearch(true)}
favorites={getVerseFavorites(actualBookName, chapterNumber, selectedVersion)}
+ sections={sections}
+ onSectionChange={handleSectionChange}
/>
);
};
@@ -547,6 +585,8 @@ function App() {
getBookUrlName={getBookUrlName}
setSelectedVersion={setSelectedVersion}
onFavoriteChange={handleFavoriteChange}
+ sections={sections}
+ onSectionChange={handleSectionChange}
/>
diff --git a/frontend/src/components/BibleReader.tsx b/frontend/src/components/BibleReader.tsx
index 6b9f5c80..eef99c26 100644
--- a/frontend/src/components/BibleReader.tsx
+++ b/frontend/src/components/BibleReader.tsx
@@ -1,6 +1,8 @@
import React, { useState, useEffect, memo } from 'react';
import { ArrowLeft, BookOpen, ChevronLeft, ChevronRight, Star, Search } from 'lucide-react';
import { getChapter, getBook } from '../services/api';
+import { Section } from '../types/favorites';
+import SectionPicker from './SectionPicker';
interface BibleReaderProps {
book: string;
@@ -12,6 +14,8 @@ interface BibleReaderProps {
version?: string;
onSearchClick?: () => void;
favorites?: Set; // Favorites passed from parent (centralized state)
+ sections?: Section[];
+ onSectionChange?: () => void;
}
const BibleReader: React.FC = memo(({
@@ -23,7 +27,9 @@ const BibleReader: React.FC = memo(({
onFavoriteChange,
version = 'esv',
onSearchClick,
- favorites = new Set() // Default to empty set if not provided
+ favorites = new Set(), // Default to empty set if not provided
+ sections = [],
+ onSectionChange
}) => {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
@@ -33,6 +39,8 @@ const BibleReader: React.FC = memo(({
const saved = localStorage.getItem('fontSize');
return (saved as 'small' | 'medium' | 'large') || 'medium';
});
+ const [showSectionPicker, setShowSectionPicker] = useState(false);
+ const [pendingVerse, setPendingVerse] = useState(null);
useEffect(() => {
loadChapter();
@@ -59,18 +67,18 @@ const BibleReader: React.FC = memo(({
}
}, [loading, content]);
- const toggleFavorite = async (verseNumber: string) => {
+ const toggleFavorite = async (verseNumber: string, sectionId?: number | null) => {
if (!user) return;
const isFavorited = favorites.has(verseNumber);
-
+
try {
if (isFavorited) {
// Remove favorite
const response = await fetch('/api/favorites', {
credentials: 'include'
});
-
+
if (response.ok) {
const data = await response.json();
const verseFavorite = data.favorites.find((fav: any) =>
@@ -79,7 +87,7 @@ const BibleReader: React.FC = memo(({
fav.verse_start === parseInt(verseNumber) &&
fav.version === version
);
-
+
if (verseFavorite) {
const deleteResponse = await fetch(`/api/favorites/${verseFavorite.id}`, {
method: 'DELETE',
@@ -88,33 +96,26 @@ const BibleReader: React.FC = memo(({
if (deleteResponse.ok) {
console.log('Removed verse favorite:', verseNumber);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ onFavoriteChange?.();
}
}
}
} else {
- // Add favorite
- const response = await fetch('/api/favorites', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- credentials: 'include',
- body: JSON.stringify({
- book: book,
- chapter: chapter,
- verse_start: parseInt(verseNumber),
- version: version
- })
- });
-
- if (response.ok) {
- console.log('Added verse favorite:', verseNumber);
- onFavoriteChange?.(); // Notify parent to refresh favorites
- } else if (response.status === 409) {
- // 409 means it already exists, which is fine
- console.log('Verse favorite already exists:', verseNumber);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ // Add favorite - check if we need section picker
+ if (sections.length > 0 && sectionId === undefined) {
+ // User has sections, check for default
+ const defaultSection = sections.find(s => s.is_default);
+ if (defaultSection) {
+ // Use default section automatically
+ await addFavoriteWithSection(verseNumber, defaultSection.id);
+ } else {
+ // Show section picker
+ setPendingVerse(verseNumber);
+ setShowSectionPicker(true);
+ }
+ } else {
+ // No sections or section already specified
+ await addFavoriteWithSection(verseNumber, sectionId ?? null);
}
}
} catch (error) {
@@ -122,6 +123,43 @@ const BibleReader: React.FC = memo(({
}
};
+ const addFavoriteWithSection = async (verseNumber: string, sectionId: number | null) => {
+ try {
+ const response = await fetch('/api/favorites', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ book: book,
+ chapter: chapter,
+ verse_start: parseInt(verseNumber),
+ version: version,
+ section_id: sectionId
+ })
+ });
+
+ if (response.ok) {
+ console.log('Added verse favorite:', verseNumber, 'to section:', sectionId);
+ onFavoriteChange?.();
+ } else if (response.status === 409) {
+ console.log('Verse favorite already exists:', verseNumber);
+ onFavoriteChange?.();
+ }
+ } catch (error) {
+ console.error('Failed to add favorite:', error);
+ }
+ };
+
+ const handleSectionSelect = async (sectionId: number | null) => {
+ if (pendingVerse) {
+ await addFavoriteWithSection(pendingVerse, sectionId);
+ setPendingVerse(null);
+ }
+ setShowSectionPicker(false);
+ };
+
const loadChapters = async () => {
try {
const response = await fetch(`/books/${book}?version=${version}`);
@@ -400,6 +438,19 @@ const BibleReader: React.FC = memo(({
)}
+
+ {/* Section Picker Modal */}
+ {showSectionPicker && (
+ {
+ setShowSectionPicker(false);
+ setPendingVerse(null);
+ }}
+ onSectionCreated={onSectionChange}
+ />
+ )}
);
});
diff --git a/frontend/src/components/BookSelector.tsx b/frontend/src/components/BookSelector.tsx
index aa76c476..17b7ca47 100644
--- a/frontend/src/components/BookSelector.tsx
+++ b/frontend/src/components/BookSelector.tsx
@@ -1,5 +1,7 @@
-import React, { useState, useEffect, memo } from 'react';
-import { BookOpen, Star, ChevronLeft, Search } from 'lucide-react';
+import React, { useState, memo } from 'react';
+import { BookOpen, Star, Search } from 'lucide-react';
+import { Section } from '../types/favorites';
+import SectionPicker from './SectionPicker';
interface BookSelectorProps {
books: string[];
@@ -10,7 +12,9 @@ interface BookSelectorProps {
version?: string;
onBack?: () => void;
onSearchClick?: () => void;
- favorites?: Set; // Favorites passed from parent (centralized state)
+ favorites?: Set;
+ sections?: Section[];
+ onSectionChange?: () => void;
}
const BookSelector: React.FC = memo(({
@@ -22,27 +26,31 @@ const BookSelector: React.FC = memo(({
version = 'esv',
onBack,
onSearchClick,
- favorites = new Set() // Default to empty set if not provided
+ favorites = new Set(),
+ sections = [],
+ onSectionChange
}) => {
+ const [showSectionPicker, setShowSectionPicker] = useState(false);
+ const [pendingBook, setPendingBook] = useState(null);
+
+ const toggleFavorite = async (book: string, event: React.MouseEvent, sectionId?: number | null) => {
+ event.stopPropagation();
- 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) {
const deleteResponse = await fetch(`/api/favorites/${bookFavorite.id}`, {
method: 'DELETE',
@@ -51,29 +59,22 @@ const BookSelector: React.FC = memo(({
if (deleteResponse.ok) {
console.log('Removed book favorite:', book);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ onFavoriteChange?.();
}
}
}
} else {
- // Add favorite - simplified like ChapterSelector
- const response = await fetch('/api/favorites', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- credentials: 'include',
- body: JSON.stringify({
- book: book,
- version: version
- })
- });
-
- if (response.ok) {
- console.log('Added book favorite:', book);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ // Add favorite - check if we need section picker
+ if (sections.length > 0 && sectionId === undefined) {
+ const defaultSection = sections.find(s => s.is_default);
+ if (defaultSection) {
+ await addFavoriteWithSection(book, defaultSection.id);
+ } else {
+ setPendingBook(book);
+ setShowSectionPicker(true);
+ }
} else {
- console.error('Failed to add favorite:', response.status, response.statusText);
+ await addFavoriteWithSection(book, sectionId ?? null);
}
}
} catch (error) {
@@ -81,9 +82,43 @@ const BookSelector: React.FC = memo(({
}
};
+ const addFavoriteWithSection = async (book: string, sectionId: number | null) => {
+ try {
+ const response = await fetch('/api/favorites', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ book: book,
+ version: version,
+ section_id: sectionId
+ })
+ });
+
+ if (response.ok) {
+ console.log('Added book favorite:', book, 'to section:', sectionId);
+ onFavoriteChange?.();
+ } else {
+ console.error('Failed to add favorite:', response.status, response.statusText);
+ }
+ } catch (error) {
+ console.error('Failed to add favorite:', error);
+ }
+ };
+
+ const handleSectionSelect = async (sectionId: number | null) => {
+ if (pendingBook) {
+ await addFavoriteWithSection(pendingBook, sectionId);
+ setPendingBook(null);
+ }
+ setShowSectionPicker(false);
+ };
+
// Group books by testament
- const oldTestament = books.slice(0, 39); // First 39 books
- const newTestament = books.slice(39); // Remaining books
+ const oldTestament = books.slice(0, 39);
+ const newTestament = books.slice(39);
const BookGroup: React.FC<{ title: string; books: string[] }> = ({ title, books }) => (
@@ -102,18 +137,17 @@ const BookSelector: React.FC
= memo(({
{formatBookName(book)}
-
- {/* Star button - only show for authenticated users */}
+
{user && (
+
+ {/* Section Picker Modal */}
+ {showSectionPicker && (
+ {
+ setShowSectionPicker(false);
+ setPendingBook(null);
+ }}
+ onSectionCreated={onSectionChange}
+ />
+ )}
);
});
diff --git a/frontend/src/components/ChapterSelector.tsx b/frontend/src/components/ChapterSelector.tsx
index b664976a..18d4d57d 100644
--- a/frontend/src/components/ChapterSelector.tsx
+++ b/frontend/src/components/ChapterSelector.tsx
@@ -1,6 +1,8 @@
import React, { useState, useEffect, memo } from 'react';
import { ArrowLeft, FileText, Star, ChevronRight, Search } from 'lucide-react';
import { getBook } from '../services/api';
+import { Section } from '../types/favorites';
+import SectionPicker from './SectionPicker';
interface ChapterSelectorProps {
book: string;
@@ -11,7 +13,9 @@ interface ChapterSelectorProps {
onFavoriteChange?: () => void;
version?: string;
onSearchClick?: () => void;
- favorites?: Set; // Favorites passed from parent (centralized state)
+ favorites?: Set;
+ sections?: Section[];
+ onSectionChange?: () => void;
}
const ChapterSelector: React.FC = memo(({
@@ -23,35 +27,39 @@ const ChapterSelector: React.FC = memo(({
onFavoriteChange,
version = 'esv',
onSearchClick,
- favorites = new Set() // Default to empty set if not provided
+ favorites = new Set(),
+ sections = [],
+ onSectionChange
}) => {
const [chapters, setChapters] = useState([]);
const [loading, setLoading] = useState(true);
+ const [showSectionPicker, setShowSectionPicker] = useState(false);
+ const [pendingChapter, setPendingChapter] = useState(null);
useEffect(() => {
loadChapters();
}, [book]);
- const toggleFavorite = async (chapter: string, event: React.MouseEvent) => {
- event.stopPropagation(); // Prevent chapter selection when clicking star
-
+ const toggleFavorite = async (chapter: string, event: React.MouseEvent, sectionId?: number | null) => {
+ event.stopPropagation();
+
if (!user) return;
const isFavorited = favorites.has(chapter);
-
+
try {
if (isFavorited) {
// Remove favorite
const response = await fetch('/api/favorites', {
credentials: 'include'
});
-
+
if (response.ok) {
const data = await response.json();
- const chapterFavorite = data.favorites.find((fav: any) =>
+ const chapterFavorite = data.favorites.find((fav: any) =>
fav.book === book && fav.chapter === chapter && !fav.verse_start
);
-
+
if (chapterFavorite) {
await fetch(`/api/favorites/${chapterFavorite.id}`, {
method: 'DELETE',
@@ -59,27 +67,21 @@ const ChapterSelector: React.FC = memo(({
});
console.log('Removed chapter favorite:', chapter);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ onFavoriteChange?.();
}
}
} else {
- // Add favorite
- const response = await fetch('/api/favorites', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- credentials: 'include',
- body: JSON.stringify({
- book: book,
- chapter: chapter,
- version: version
- })
- });
-
- if (response.ok) {
- console.log('Added chapter favorite:', chapter);
- onFavoriteChange?.(); // Notify parent to refresh favorites
+ // Add favorite - check if we need section picker
+ if (sections.length > 0 && sectionId === undefined) {
+ const defaultSection = sections.find(s => s.is_default);
+ if (defaultSection) {
+ await addFavoriteWithSection(chapter, defaultSection.id);
+ } else {
+ setPendingChapter(chapter);
+ setShowSectionPicker(true);
+ }
+ } else {
+ await addFavoriteWithSection(chapter, sectionId ?? null);
}
}
} catch (error) {
@@ -87,14 +89,45 @@ const ChapterSelector: React.FC = memo(({
}
};
+ const addFavoriteWithSection = async (chapter: string, sectionId: number | null) => {
+ try {
+ const response = await fetch('/api/favorites', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ book: book,
+ chapter: chapter,
+ version: version,
+ section_id: sectionId
+ })
+ });
+
+ if (response.ok) {
+ console.log('Added chapter favorite:', chapter, 'to section:', sectionId);
+ onFavoriteChange?.();
+ }
+ } catch (error) {
+ console.error('Failed to add favorite:', error);
+ }
+ };
+
+ const handleSectionSelect = async (sectionId: number | null) => {
+ if (pendingChapter) {
+ await addFavoriteWithSection(pendingChapter, sectionId);
+ setPendingChapter(null);
+ }
+ setShowSectionPicker(false);
+ };
+
const loadChapters = async () => {
try {
setLoading(true);
const response = await getBook(book, version);
- // The API now returns { chapters: ["1", "2", "3", ...] }
if (response.chapters) {
- // Sort chapters numerically to ensure proper order
const sortedChapters = response.chapters.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
setChapters(sortedChapters);
} else {
@@ -103,7 +136,6 @@ const ChapterSelector: React.FC = memo(({
}
} catch (error) {
console.error('Failed to load chapters:', error);
- // Don't show fallback chapters - just show an empty list
setChapters([]);
} finally {
setLoading(false);
@@ -160,7 +192,7 @@ const ChapterSelector: React.FC = memo(({
{user && (
- Click the ★ to add chapters to your favorites
+ Click the star to add chapters to your favorites
)}
@@ -178,18 +210,17 @@ const ChapterSelector: React.FC = memo(({
{chapter}
-
- {/* Star button - only show for authenticated users */}
+
{user && (