This commit is contained in:
2025-11-25 23:47:21 +00:00
parent ead26b81f9
commit 28a4cc86aa
13 changed files with 1560 additions and 257 deletions
Binary file not shown.
Binary file not shown.
+1345 -1
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -13,6 +13,7 @@
"dependencies": {
"cookie-parser": "^1.4.7",
"express": "^5.1.0",
"hbs": "^4.2.0"
"hbs": "^4.2.0",
"sqlite3": "^5.1.7"
}
}
+165 -35
View File
@@ -3,6 +3,7 @@ const hbs = require('hbs');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const app = express();
const PORT = process.env.PORT || 3000;
@@ -13,60 +14,99 @@ app.set('views', '../frontend/views');
// Register partials directory
hbs.registerPartials('../frontend/partials');
// Register Handlebars helpers
hbs.registerHelper('eq', function(a, b) {
return a === b;
});
hbs.registerHelper('encodeURIComponent', function(str) {
return encodeURIComponent(str);
});
// Database setup
const dbPath = path.join(__dirname, 'database/backend.db');
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database:', err.message);
} else {
console.log('Connected to SQLite database.');
}
});
// Middleware
app.use(express.static('../frontend/public'));
app.use(express.json());
app.use(cookieParser());
// Helper functions for server-side rendering
function loadBooks() {
return new Promise((resolve, reject) => {
const query = 'SELECT id, folder_name, display_name, author FROM books ORDER BY display_name';
db.all(query, [], (err, rows) => {
if (err) {
console.error('Error loading books from database:', err.message);
resolve([]);
} else {
const books = rows.map(row => ({
id: row.id,
name: row.folder_name,
displayName: row.display_name || row.folder_name.replace(/-/g, ' '),
author: row.author
}));
resolve(books);
}
});
});
}
function loadChapters(bookName) {
return new Promise((resolve, reject) => {
const query = `
SELECT c.id, c.chapter_number, c.display_name, c.filename, b.folder_name
FROM chapters c
JOIN books b ON c.book_id = b.id
WHERE b.folder_name = ?
ORDER BY c.chapter_number
`;
db.all(query, [bookName], (err, rows) => {
if (err) {
console.error('Error loading chapters from database:', err.message);
resolve([]);
} else {
const chapters = rows.map(row => ({
id: row.id,
chapterNumber: row.chapter_number,
filename: row.filename,
displayName: row.display_name,
url: `/books/${row.folder_name}/${encodeURIComponent(row.filename)}`
}));
resolve(chapters);
}
});
});
}
// API Routes
// Get list of available books
app.get('/api/books', (req, res) => {
const booksPath = path.join(__dirname, '../frontend/public/books');
app.get('/api/books', async (req, res) => {
try {
const books = fs.readdirSync(booksPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => ({
name: dirent.name,
displayName: dirent.name.replace(/-/g, ' ')
}));
const books = await loadBooks();
res.json({ books });
} catch (error) {
console.error('Error reading books directory:', error);
console.error('Error loading books:', error);
res.status(500).json({ error: 'Failed to load books' });
}
});
// Get list of chapters for a specific book
app.get('/api/books/:bookName/chapters', (req, res) => {
app.get('/api/books/:bookName/chapters', async (req, res) => {
const bookName = req.params.bookName;
const bookPath = path.join(__dirname, '../frontend/public/books', bookName);
try {
if (!fs.existsSync(bookPath)) {
return res.status(404).json({ error: 'Book not found' });
}
const files = fs.readdirSync(bookPath);
const chapters = files
.filter(file => file.toLowerCase().endsWith('.pdf'))
.map(file => ({
filename: file,
displayName: file.replace('.pdf', '').replace(/chapter\s*/i, 'Chapter '),
url: `/books/${bookName}/${encodeURIComponent(file)}`
}))
.sort((a, b) => {
// Sort chapters numerically
const aNum = parseInt(a.displayName.match(/\d+/)?.[0] || '0');
const bNum = parseInt(b.displayName.match(/\d+/)?.[0] || '0');
return aNum - bNum;
});
const chapters = await loadChapters(bookName);
res.json({ chapters });
} catch (error) {
console.error('Error reading chapters directory:', error);
console.error('Error loading chapters:', error);
res.status(500).json({ error: 'Failed to load chapters' });
}
});
@@ -93,12 +133,102 @@ app.get('/books/:bookName/:chapterFile', (req, res) => {
});
// Page Routes
app.get('/', (req, res) => {
res.render('index', { title: 'Book Viewer' });
app.get('/', async (req, res) => {
try {
const books = await loadBooks();
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: null,
selectedChapter: null,
chapters: []
});
} catch (error) {
console.error('Error loading books for home page:', error);
res.render('index', {
title: 'Book Viewer',
books: [],
selectedBook: null,
selectedChapter: null,
chapters: [],
error: 'Failed to load books'
});
}
});
// Route for viewing a specific book
app.get('/book/:bookName', async (req, res) => {
const bookName = req.params.bookName;
try {
const [books, chapters] = await Promise.all([
loadBooks(),
loadChapters(bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
if (!selectedBook) {
return res.redirect('/');
}
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: null,
chapters: chapters
});
} catch (error) {
console.error('Error loading book page:', error);
res.redirect('/');
}
});
// Route for viewing a specific chapter
app.get('/book/:bookName/chapter/:chapterFile', async (req, res) => {
const bookName = req.params.bookName;
const chapterFile = decodeURIComponent(req.params.chapterFile);
try {
const [books, chapters] = await Promise.all([
loadBooks(),
loadChapters(bookName)
]);
const selectedBook = books.find(book => book.name === bookName);
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
if (!selectedBook || !selectedChapter) {
return res.redirect('/');
}
res.render('index', {
title: 'Book Viewer',
books: books,
selectedBook: selectedBook,
selectedChapter: selectedChapter,
chapters: chapters
});
} catch (error) {
console.error('Error loading chapter page:', error);
res.redirect('/');
}
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down server...');
db.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
} else {
console.log('Database connection closed.');
}
process.exit(0);
});
});
-3
View File
@@ -1,3 +0,0 @@
class Book {
}
View File
+9
View File
@@ -0,0 +1,9 @@
{{#if books}}
{{#each books}}
<div class="book-item {{#if (eq this.name ../selectedBook.name)}}selected{{/if}}" data-book-name="{{this.name}}">
<a href="/book/{{this.name}}">{{this.displayName}}</a>
</div>
{{/each}}
{{else}}
<div class="error">No books available</div>
{{/if}}
+11
View File
@@ -0,0 +1,11 @@
{{#if chapters}}
{{#each chapters}}
<div class="chapter-item {{#if (eq this.filename ../selectedChapter.filename)}}selected{{/if}}" data-chapter-url="{{this.url}}">
<a href="/book/{{../selectedBook.name}}/chapter/{{encodeURIComponent this.filename}}">{{this.displayName}}</a>
</div>
{{/each}}
{{else}}
{{#if selectedBook}}
<div class="error">No chapters available for this book</div>
{{/if}}
{{/if}}
-208
View File
@@ -1,208 +0,0 @@
class BookViewer {
constructor() {
this.selectedBook = null;
this.selectedChapter = null;
this.books = [];
this.chapters = [];
this.init();
}
async init() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
}
async setup() {
this.bindElements();
await this.loadBooks();
}
bindElements() {
this.booksListEl = document.getElementById('books-list');
this.chaptersListEl = document.getElementById('chapters-list');
this.chaptersSectionEl = document.getElementById('chapters-section');
this.pdfPlaceholderEl = document.getElementById('pdf-placeholder');
this.pdfViewerEl = document.getElementById('pdf-viewer');
}
async loadBooks() {
try {
this.showLoading(this.booksListEl, 'Loading books...');
const response = await fetch('/api/books');
if (!response.ok) {
throw new Error('Failed to load books');
}
const data = await response.json();
this.books = data.books;
this.renderBooks();
} catch (error) {
console.error('Error loading books:', error);
this.showError(this.booksListEl, 'Failed to load books. Please refresh the page.');
}
}
async loadChapters(bookName) {
try {
this.showLoading(this.chaptersListEl, 'Loading chapters...');
const response = await fetch(`/api/books/${encodeURIComponent(bookName)}/chapters`);
if (!response.ok) {
throw new Error('Failed to load chapters');
}
const data = await response.json();
this.chapters = data.chapters;
this.renderChapters();
this.showChaptersSection();
} catch (error) {
console.error('Error loading chapters:', error);
this.showError(this.chaptersListEl, 'Failed to load chapters for this book.');
}
}
renderBooks() {
this.booksListEl.innerHTML = '';
if (this.books.length === 0) {
this.booksListEl.innerHTML = '<div class="error">No books available</div>';
return;
}
this.books.forEach(book => {
const bookEl = document.createElement('div');
bookEl.className = 'book-item';
bookEl.textContent = book.displayName;
bookEl.dataset.bookName = book.name;
bookEl.addEventListener('click', () => this.selectBook(book.name, bookEl));
this.booksListEl.appendChild(bookEl);
});
}
renderChapters() {
this.chaptersListEl.innerHTML = '';
if (this.chapters.length === 0) {
this.chaptersListEl.innerHTML = '<div class="error">No chapters available for this book</div>';
return;
}
this.chapters.forEach(chapter => {
const chapterEl = document.createElement('div');
chapterEl.className = 'chapter-item';
chapterEl.textContent = chapter.displayName;
chapterEl.dataset.chapterUrl = chapter.url;
chapterEl.addEventListener('click', () => this.selectChapter(chapter.url, chapterEl));
this.chaptersListEl.appendChild(chapterEl);
});
}
async selectBook(bookName, bookEl) {
// Update selected book UI
document.querySelectorAll('.book-item').forEach(el => el.classList.remove('selected'));
bookEl.classList.add('selected');
// Clear selected chapter
this.selectedChapter = null;
document.querySelectorAll('.chapter-item').forEach(el => el.classList.remove('selected'));
this.hidePDF();
this.selectedBook = bookName;
// Load chapters for selected book
await this.loadChapters(bookName);
}
selectChapter(chapterUrl, chapterEl) {
// Update selected chapter UI
document.querySelectorAll('.chapter-item').forEach(el => el.classList.remove('selected'));
chapterEl.classList.add('selected');
this.selectedChapter = chapterUrl;
this.showPDF(chapterUrl);
}
showPDF(url) {
this.pdfPlaceholderEl.style.display = 'none';
this.pdfViewerEl.style.display = 'block';
this.pdfViewerEl.src = url;
// Handle PDF load errors
this.pdfViewerEl.onload = () => {
// PDF loaded successfully
};
this.pdfViewerEl.onerror = () => {
this.showError(this.pdfPlaceholderEl, 'Failed to load PDF. Please try another chapter.');
this.hidePDF();
};
}
hidePDF() {
this.pdfViewerEl.style.display = 'none';
this.pdfPlaceholderEl.style.display = 'flex';
this.pdfViewerEl.src = '';
}
showChaptersSection() {
this.chaptersSectionEl.style.display = 'block';
}
hideChaptersSection() {
this.chaptersSectionEl.style.display = 'none';
}
showLoading(element, message = 'Loading...') {
element.innerHTML = `<div class="loading">${message}</div>`;
}
showError(element, message) {
element.innerHTML = `<div class="error">${message}</div>`;
}
}
// Initialize the book viewer when the script loads
const bookViewer = new BookViewer();
// Add some keyboard shortcuts for better UX
document.addEventListener('keydown', (e) => {
// ESC key to deselect
if (e.key === 'Escape') {
const selectedChapter = document.querySelector('.chapter-item.selected');
if (selectedChapter) {
selectedChapter.classList.remove('selected');
bookViewer.hidePDF();
}
}
// Arrow keys for navigation
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
const chapters = document.querySelectorAll('.chapter-item');
const selectedChapter = document.querySelector('.chapter-item.selected');
if (chapters.length > 0) {
let currentIndex = Array.from(chapters).indexOf(selectedChapter);
if (e.key === 'ArrowUp') {
currentIndex = Math.max(0, currentIndex - 1);
} else {
currentIndex = Math.min(chapters.length - 1, currentIndex + 1);
}
chapters[currentIndex]?.click();
e.preventDefault();
}
}
});
+20 -4
View File
@@ -115,15 +115,23 @@ body {
.book-item {
background-color: var(--secondary-color);
padding: 12px 15px;
padding: 0;
margin-bottom: 8px;
border-radius: var(--border-radius);
border: 2px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
.book-item a {
display: block;
padding: 12px 15px;
text-decoration: none;
color: inherit;
width: 100%;
height: 100%;
}
.book-item:hover {
background-color: var(--accent-color);
border-color: var(--primary-color);
@@ -148,15 +156,23 @@ body {
.chapter-item {
background-color: var(--secondary-color);
padding: 10px 12px;
padding: 0;
margin-bottom: 6px;
border-radius: var(--border-radius);
border: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.95rem;
}
.chapter-item a {
display: block;
padding: 10px 12px;
text-decoration: none;
color: inherit;
width: 100%;
height: 100%;
}
.chapter-item:hover {
background-color: var(--accent-color);
border-color: var(--primary-color);
+7 -4
View File
@@ -30,14 +30,15 @@
<div class="books-section">
<h4>Available Books</h4>
<div id="books-list" class="books-list">
<div class="loading">Loading books...</div>
{{> books}}
</div>
</div>
<!-- Chapters List -->
<div class="chapters-section" id="chapters-section" style="display: none;">
<div class="chapters-section" id="chapters-section" {{#unless selectedBook}}style="display: none;"{{/unless}}>
<h4>Chapters</h4>
<div id="chapters-list" class="chapters-list">
{{> chapters}}
</div>
</div>
</div>
@@ -46,6 +47,9 @@
<!-- Right Side - PDF Display Area -->
<main class="content-area">
<div class="pdf-container">
{{#if selectedChapter}}
<iframe id="pdf-viewer" class="pdf-viewer" src="{{selectedChapter.url}}"></iframe>
{{else}}
<div id="pdf-placeholder" class="pdf-placeholder">
<div class="placeholder-content">
<i class="fas fa-file-pdf"></i>
@@ -53,13 +57,12 @@
<p>Choose a book and chapter from the sidebar to display the PDF</p>
</div>
</div>
<iframe id="pdf-viewer" class="pdf-viewer" style="display: none;"></iframe>
{{/if}}
</div>
</main>
</div>
{{{body}}}
<script src="/scripts/app.js"></script>
</body>
</html>