IP
This commit is contained in:
Binary file not shown.
Binary file not shown.
Generated
+1345
-1
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
class Book {
|
||||
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
Submodule frontend/public/books/WaysOfTheWorld-Strayer updated: 86e5dfa129...e837c9f329
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user