/** * Sci-Fi Books Showcase Application * Fully offline version - uses local cover images and pre-fetched summaries. */ // State management const state = { books: [], filteredBooks: [], currentFilter: { author: 'all', format: 'all', search: '' } }; // DOM elements cache const elements = { booksGrid: null, bookCount: null, authorFilter: null, formatFilter: null, searchInput: null, modal: null, modalContent: null }; // Initialize the application async function init() { cacheElements(); setupUI(); await initializeBooks(); } function setupUI() { setupFilters(); setupModal(); } async function initializeBooks() { const books = await loadBooks(); if (!books) return; state.books = books; state.filteredBooks = [...books]; populateAuthorFilter(); renderBooks(); } // Cache DOM element references function cacheElements() { elements.booksGrid = document.getElementById('books-grid'); elements.bookCount = document.getElementById('book-count'); elements.authorFilter = document.getElementById('author-filter'); elements.formatFilter = document.getElementById('format-filter'); elements.searchInput = document.getElementById('search-input'); elements.modal = document.getElementById('book-modal'); elements.modalContent = document.getElementById('modal-content'); } // Load books from JSON file async function loadBooks() { try { const response = await fetch('data/books.json'); if (!response.ok) throw new Error('Failed to load books'); const data = await response.json(); return sortBooksByYear(data.books); } catch (error) { renderLoadError(error); return null; } } function renderLoadError(error) { console.error('Error loading books:', error); elements.booksGrid.innerHTML = `
`; } function sortBooksByYear(books) { return [...books].sort((a, b) => { if (a.year !== b.year) return a.year - b.year; const authorCompare = a.author.localeCompare(b.author); if (authorCompare !== 0) return authorCompare; return a.title.localeCompare(b.title); }); } // Populate author filter dropdown with unique authors function populateAuthorFilter() { const authors = [...new Set(state.books.map(book => book.author))].sort(); authors.forEach(author => { const option = document.createElement('option'); option.value = author; option.textContent = author; elements.authorFilter.appendChild(option); }); } // Setup filter event listeners function setupFilters() { elements.authorFilter.addEventListener('change', handleFilterChange); elements.formatFilter.addEventListener('change', handleFilterChange); elements.searchInput.addEventListener('input', debounce(handleFilterChange, 300)); } // Handle filter changes and update displayed books function handleFilterChange() { state.currentFilter.author = elements.authorFilter.value; state.currentFilter.format = elements.formatFilter.value; state.currentFilter.search = elements.searchInput.value.toLowerCase().trim(); state.filteredBooks = state.books.filter(book => { const matchesAuthor = state.currentFilter.author === 'all' || book.author === state.currentFilter.author; const matchesFormat = state.currentFilter.format === 'all' || book.format.toLowerCase() === state.currentFilter.format; const matchesSearch = !state.currentFilter.search || book.title.toLowerCase().includes(state.currentFilter.search) || book.author.toLowerCase().includes(state.currentFilter.search); return matchesAuthor && matchesFormat && matchesSearch; }); renderBooks(); } // Get cover image URL - uses local file if available, otherwise shows placeholder function getCoverUrl(book) { if (book.coverLocal) { return book.coverLocal; } return null; } // Render books grid function renderBooks() { if (state.filteredBooks.length === 0) { elements.booksGrid.innerHTML = `No books found matching your filters.
${escapeHtml(p)}
`).join('\n'); summaryHtml = `No plot summary available for this book.
`; } // Render modal content elements.modalContent.innerHTML = `