Add multi-version Bible support with ESV and NKJV translations
- Rename project from 'ESV Bible' to 'The Bible' - Implement version selection dropdown in homepage header - Add support for multiple Bible versions: * ESV (English Standard Version) - from mdbible * NKJV (New King James Version) - from local NKJV/ directory - Update all API endpoints to accept version parameter (?version=esv|?version=nkjv) - Add version-aware favorites system that stores and displays Bible version (e.g., 'Genesis 1:1 (ESV)') - Update database schema to include version column in favorites table - Maintain backward compatibility with existing data - Update Docker configuration and documentation
This commit is contained in:
@@ -50,7 +50,22 @@ function initializeTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Favorites table
|
||||
// Check if favorites table exists and needs migration
|
||||
db.all("PRAGMA table_info(favorites)", [], (err, columns) => {
|
||||
if (!err && columns.length > 0) {
|
||||
const hasVersionColumn = columns.some(col => col.name === 'version');
|
||||
if (!hasVersionColumn) {
|
||||
// Add version column to existing table
|
||||
db.run("ALTER TABLE favorites ADD COLUMN version TEXT DEFAULT 'esv'", (err) => {
|
||||
if (!err) {
|
||||
console.log('Added version column to favorites table');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Favorites table (with IF NOT EXISTS for safety)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -59,10 +74,11 @@ function initializeTables() {
|
||||
chapter TEXT,
|
||||
verse_start INTEGER,
|
||||
verse_end INTEGER,
|
||||
version TEXT DEFAULT 'esv',
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id, book, chapter, verse_start, verse_end)
|
||||
UNIQUE(user_id, book, chapter, verse_start, verse_end, version)
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -168,12 +184,12 @@ const favoritesOps = {
|
||||
|
||||
// Add favorite
|
||||
addFavorite: (userId, favorite, callback) => {
|
||||
const { book, chapter, verse_start, verse_end, note } = favorite;
|
||||
|
||||
const { book, chapter, verse_start, verse_end, version, note } = favorite;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO favorites (user_id, book, chapter, verse_start, verse_end, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[userId, book, chapter, verse_start, verse_end, note],
|
||||
`INSERT INTO favorites (user_id, book, chapter, verse_start, verse_end, version, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[userId, book, chapter, verse_start, verse_end, version || 'esv', note],
|
||||
callback
|
||||
);
|
||||
},
|
||||
@@ -188,10 +204,10 @@ const favoritesOps = {
|
||||
},
|
||||
|
||||
// Check if verse is favorited
|
||||
isFavorited: (userId, book, chapter, verse_start, verse_end, callback) => {
|
||||
isFavorited: (userId, book, chapter, verse_start, verse_end, version, callback) => {
|
||||
db.get(
|
||||
'SELECT id FROM favorites WHERE user_id = ? AND book = ? AND chapter = ? AND verse_start = ? AND verse_end = ?',
|
||||
[userId, book, chapter, verse_start, verse_end],
|
||||
'SELECT id FROM favorites WHERE user_id = ? AND book = ? AND chapter = ? AND verse_start = ? AND verse_end = ? AND version = ?',
|
||||
[userId, book, chapter, verse_start, verse_end, version || 'esv'],
|
||||
(err, row) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, !!row);
|
||||
|
||||
@@ -32,11 +32,20 @@ configureAuth(app);
|
||||
// Serve static files from the React build
|
||||
app.use(express.static(path.join(__dirname, '../../frontend/build')));
|
||||
|
||||
// Bible data directory
|
||||
const BIBLE_DATA_DIR = path.join(__dirname, '../../bible-data');
|
||||
// Bible data directories
|
||||
const ESV_DATA_DIR = path.join(__dirname, '../../bible-data'); // ESV from mdbible
|
||||
const NKJV_DATA_DIR = path.join(__dirname, '../NKJV'); // NKJV local files
|
||||
|
||||
// Initialize search engine
|
||||
const searchEngine = new BibleSearchEngine(BIBLE_DATA_DIR);
|
||||
// Initialize search engines for each version
|
||||
const esvSearchEngine = ESV_DATA_DIR ? new BibleSearchEngine(ESV_DATA_DIR) : null;
|
||||
const nkjvSearchEngine = new BibleSearchEngine(NKJV_DATA_DIR);
|
||||
|
||||
// Helper function to get data directory for version
|
||||
function getDataDir(version) {
|
||||
if (version === 'esv' && esvSearchEngine) return ESV_DATA_DIR;
|
||||
if (version === 'nkjv') return NKJV_DATA_DIR;
|
||||
return esvSearchEngine ? ESV_DATA_DIR : NKJV_DATA_DIR; // default to available version
|
||||
}
|
||||
|
||||
// Helper function to read markdown files
|
||||
async function readMarkdownFile(filePath) {
|
||||
@@ -48,14 +57,15 @@ async function readMarkdownFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get all books
|
||||
async function getBooks() {
|
||||
// Helper function to get all books for a version
|
||||
async function getBooks(version = 'esv') {
|
||||
try {
|
||||
const items = await fs.readdir(BIBLE_DATA_DIR);
|
||||
const dataDir = getDataDir(version);
|
||||
const items = await fs.readdir(dataDir);
|
||||
const bookDirs = [];
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(BIBLE_DATA_DIR, item);
|
||||
const itemPath = path.join(dataDir, item);
|
||||
const stat = await fs.stat(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
@@ -74,19 +84,28 @@ async function getBooks() {
|
||||
|
||||
return bookDirs;
|
||||
} catch (error) {
|
||||
throw new Error('Failed to read bible data directory');
|
||||
throw new Error(`Failed to read bible data directory for version ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK', message: 'ESV Bible API is running' });
|
||||
res.json({ status: 'OK', message: 'The Bible API is running' });
|
||||
});
|
||||
|
||||
app.get('/versions', (req, res) => {
|
||||
const availableVersions = [];
|
||||
if (esvSearchEngine) availableVersions.push({ id: 'esv', name: 'ESV - English Standard Version' });
|
||||
availableVersions.push({ id: 'nkjv', name: 'NKJV - New King James Version' });
|
||||
|
||||
res.json({ versions: availableVersions });
|
||||
});
|
||||
|
||||
app.get('/books', async (req, res) => {
|
||||
try {
|
||||
const books = await getBooks();
|
||||
res.json({ books });
|
||||
const { version = 'esv' } = req.query;
|
||||
const books = await getBooks(version);
|
||||
res.json({ books, version });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -95,12 +114,14 @@ app.get('/books', async (req, res) => {
|
||||
app.get('/books/:book', async (req, res) => {
|
||||
try {
|
||||
const { book } = req.params;
|
||||
const bookDir = path.join(BIBLE_DATA_DIR, book);
|
||||
const { version = 'esv' } = req.query;
|
||||
const dataDir = getDataDir(version);
|
||||
const bookDir = path.join(dataDir, book);
|
||||
|
||||
// Check if book directory exists
|
||||
const stat = await fs.stat(bookDir);
|
||||
if (!stat.isDirectory()) {
|
||||
return res.status(404).json({ error: `Book '${book}' not found` });
|
||||
return res.status(404).json({ error: `Book '${book}' not found in version '${version}'` });
|
||||
}
|
||||
|
||||
// Get all chapter files
|
||||
@@ -108,7 +129,7 @@ app.get('/books/:book', async (req, res) => {
|
||||
const chapterFiles = files.filter(file => file.endsWith('.md')).sort();
|
||||
|
||||
if (chapterFiles.length === 0) {
|
||||
return res.status(404).json({ error: `No chapters found for book '${book}'` });
|
||||
return res.status(404).json({ error: `No chapters found for book '${book}' in version '${version}'` });
|
||||
}
|
||||
|
||||
// Extract chapter numbers from filenames (e.g., "Chapter_01.md" -> "1")
|
||||
@@ -117,16 +138,17 @@ app.get('/books/:book', async (req, res) => {
|
||||
return match ? parseInt(match[1], 10).toString() : null;
|
||||
}).filter(Boolean);
|
||||
|
||||
res.json({ chapters });
|
||||
res.json({ chapters, version });
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: `Book '${req.params.book}' not found` });
|
||||
res.status(404).json({ error: `Book '${req.params.book}' not found in version '${req.query.version || 'esv'}'` });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/books/:book/:chapter', async (req, res) => {
|
||||
try {
|
||||
const { book, chapter } = req.params;
|
||||
|
||||
const { version = 'esv' } = req.query;
|
||||
|
||||
// Check if chapter already includes "Chapter_" prefix (from frontend)
|
||||
let chapterFileName;
|
||||
if (chapter.startsWith('Chapter_')) {
|
||||
@@ -136,25 +158,29 @@ app.get('/books/:book/:chapter', async (req, res) => {
|
||||
const paddedChapter = chapter.padStart(2, '0');
|
||||
chapterFileName = `Chapter_${paddedChapter}.md`;
|
||||
}
|
||||
|
||||
const chapterPath = path.join(BIBLE_DATA_DIR, book, chapterFileName);
|
||||
|
||||
const dataDir = getDataDir(version);
|
||||
const chapterPath = path.join(dataDir, book, chapterFileName);
|
||||
|
||||
const content = await readMarkdownFile(chapterPath);
|
||||
res.type('text/markdown').send(content);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: `Chapter ${chapter} not found in book '${book}'` });
|
||||
res.status(404).json({ error: `Chapter ${req.params.chapter} not found in book '${req.params.book}' for version '${req.query.version || 'esv'}'` });
|
||||
}
|
||||
});
|
||||
|
||||
// Search routes
|
||||
app.get('/api/search', async (req, res) => {
|
||||
try {
|
||||
const { q: query, book, limit, context } = req.query;
|
||||
|
||||
const { q: query, book, limit, context, version = 'esv' } = req.query;
|
||||
|
||||
if (!query || query.trim().length < 2) {
|
||||
return res.status(400).json({ error: 'Query must be at least 2 characters long' });
|
||||
}
|
||||
|
||||
// Get the appropriate search engine for the version
|
||||
const searchEngine = version === 'esv' && esvSearchEngine ? esvSearchEngine : nkjvSearchEngine;
|
||||
|
||||
const options = {
|
||||
bookFilter: book || null,
|
||||
limit: parseInt(limit) || 50,
|
||||
@@ -163,12 +189,13 @@ app.get('/api/search', async (req, res) => {
|
||||
};
|
||||
|
||||
const results = await searchEngine.search(query, options);
|
||||
|
||||
|
||||
res.json({
|
||||
query,
|
||||
results,
|
||||
total: results.length,
|
||||
hasMore: results.length === options.limit
|
||||
hasMore: results.length === options.limit,
|
||||
version
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
@@ -178,15 +205,18 @@ app.get('/api/search', async (req, res) => {
|
||||
|
||||
app.get('/api/search/suggestions', async (req, res) => {
|
||||
try {
|
||||
const { q: query, limit } = req.query;
|
||||
|
||||
const { q: query, limit, version = 'esv' } = req.query;
|
||||
|
||||
if (!query || query.trim().length < 2) {
|
||||
return res.json({ suggestions: [] });
|
||||
}
|
||||
|
||||
// Get the appropriate search engine for the version
|
||||
const searchEngine = version === 'esv' && esvSearchEngine ? esvSearchEngine : nkjvSearchEngine;
|
||||
|
||||
const suggestions = await searchEngine.getSearchSuggestions(query, parseInt(limit) || 10);
|
||||
|
||||
res.json({ suggestions });
|
||||
|
||||
res.json({ suggestions, version });
|
||||
} catch (error) {
|
||||
console.error('Search suggestions error:', error);
|
||||
res.status(500).json({ error: 'Failed to get search suggestions' });
|
||||
@@ -233,8 +263,8 @@ app.get('/api/favorites', requireAuth, (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/favorites', requireAuth, (req, res) => {
|
||||
const { book, chapter, verse_start, verse_end, note } = req.body;
|
||||
|
||||
const { book, chapter, verse_start, verse_end, note, version } = req.body;
|
||||
|
||||
// Book is required, but chapter is optional (for book-level favorites)
|
||||
if (!book) {
|
||||
return res.status(400).json({ error: 'Book is required' });
|
||||
@@ -245,6 +275,7 @@ app.post('/api/favorites', requireAuth, (req, res) => {
|
||||
chapter: chapter || null,
|
||||
verse_start: verse_start || null,
|
||||
verse_end: verse_end || null,
|
||||
version: version || 'esv',
|
||||
note: note || null
|
||||
};
|
||||
|
||||
@@ -308,6 +339,11 @@ app.use((err, req, res, next) => {
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`ESV Bible API server running on port ${PORT}`);
|
||||
console.log(`Bible data directory: ${BIBLE_DATA_DIR}`);
|
||||
console.log(`The Bible API server running on port ${PORT}`);
|
||||
if (esvSearchEngine) {
|
||||
console.log(`ESV data directory: ${ESV_DATA_DIR}`);
|
||||
} else {
|
||||
console.log(`ESV data not available - mdbible not found`);
|
||||
}
|
||||
console.log(`NKJV data directory: ${NKJV_DATA_DIR}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user