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:
Ryderjj89
2025-09-28 12:13:37 -04:00
parent 5f832aecd4
commit ceeb465c8d
1199 changed files with 187 additions and 86 deletions

View File

@@ -1,5 +1,5 @@
{
"name": "esv-bible-markdown",
"name": "the-bible-backend",
"version": "1.0.0",
"description": "ESV Bible in Markdown format served via Docker",
"main": "src/index.js",

View File

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

View File

@@ -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}`);
});