Favorites improvements

This commit is contained in:
2025-12-08 15:04:33 -05:00
parent 24da4d2589
commit 46331a9596
10 changed files with 1451 additions and 235 deletions

View File

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

View File

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