Favorites improvements
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user