179 líneas
6.1 KiB
JavaScript
179 líneas
6.1 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { FaBook, FaSearch, FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
|
import '../styles/BookList.css';
|
|
|
|
/**
|
|
* Componente para mostrar y buscar libros en el catálogo
|
|
*/
|
|
const BookList = ({ books, onBookSelect, isLoading }) => {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
|
|
|
// Filtrar libros según el término de búsqueda
|
|
const filteredBooks = books.filter((book) =>
|
|
book.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
// Calcular paginación
|
|
const totalPages = Math.ceil(filteredBooks.length / itemsPerPage);
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = startIndex + itemsPerPage;
|
|
const currentBooks = filteredBooks.slice(startIndex, endIndex);
|
|
|
|
// Resetear a la primera página cuando cambia la búsqueda o items por página
|
|
const handleSearchChange = (value) => {
|
|
setSearchTerm(value);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleItemsPerPageChange = (value) => {
|
|
setItemsPerPage(Number(value));
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const goToPage = (page) => {
|
|
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="book-list-container">
|
|
<div className="loading">
|
|
<div className="spinner"></div>
|
|
<p>Cargando catálogo desde IPFS...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="book-list-container">
|
|
<div className="search-box" role="search">
|
|
<label htmlFor="search-input" className="visually-hidden">Buscar libros por título</label>
|
|
<FaSearch className="search-icon" aria-hidden="true" />
|
|
<input
|
|
id="search-input"
|
|
type="search"
|
|
placeholder="Buscar libros por título..."
|
|
value={searchTerm}
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
className="search-input"
|
|
aria-label="Buscar libros"
|
|
aria-describedby="search-results-count"
|
|
aria-controls="book-list"
|
|
/>
|
|
{searchTerm && (
|
|
<button
|
|
className="clear-search"
|
|
onClick={() => handleSearchChange('')}
|
|
aria-label="Limpiar búsqueda"
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="controls-bar">
|
|
<div className="book-count" id="search-results-count" role="status" aria-live="polite" aria-atomic="true">
|
|
{filteredBooks.length} {filteredBooks.length === 1 ? 'libro encontrado' : 'libros encontrados'}
|
|
</div>
|
|
|
|
<div className="items-per-page">
|
|
<label htmlFor="items-per-page">Mostrar:</label>
|
|
<select
|
|
id="items-per-page"
|
|
value={itemsPerPage}
|
|
onChange={(e) => handleItemsPerPageChange(e.target.value)}
|
|
className="items-select"
|
|
aria-label="Libros por página"
|
|
aria-describedby="search-results-count"
|
|
aria-controls="book-list"
|
|
>
|
|
<option value={10}>10</option>
|
|
<option value={50}>50</option>
|
|
<option value={100}>100</option>
|
|
</select>
|
|
<span>por página</span>
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="book-list" id="book-list" role="list" aria-label="Lista de libros">
|
|
{filteredBooks.length === 0 ? (
|
|
<li className="no-results" role="status">
|
|
<FaBook className="no-results-icon" aria-hidden="true" />
|
|
<p>No se encontraron libros</p>
|
|
<small>Intenta con otros términos de búsqueda</small>
|
|
</li>
|
|
) : (
|
|
currentBooks.map((book, index) => (
|
|
<li key={book.id} className="book-list-item" role="listitem">
|
|
<a
|
|
href={`?epub=${book.ipfsHash}&gateway=${encodeURIComponent(window.location.origin)}`}
|
|
className="book-item"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
onBookSelect(book);
|
|
}}
|
|
aria-label={`Abrir libro: ${book.title}`}
|
|
>
|
|
<FaBook className="book-icon" aria-hidden="true" />
|
|
<div className="book-info">
|
|
<h3 className="book-title">{book.title}</h3>
|
|
<p className="book-filename" aria-label={`Archivo: ${book.filename}`}>
|
|
<small>{book.filename}</small>
|
|
</p>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
))
|
|
)}
|
|
</ul>
|
|
|
|
{totalPages > 1 && (
|
|
<nav className="pagination" role="navigation" aria-label="Paginación de libros">
|
|
<button
|
|
className="pagination-button"
|
|
onClick={() => goToPage(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
aria-label="Ir a página anterior"
|
|
aria-disabled={currentPage === 1}
|
|
>
|
|
<FaChevronLeft aria-hidden="true" />
|
|
<span className="visually-hidden">Anterior</span>
|
|
</button>
|
|
|
|
<div className="pagination-info">
|
|
<label htmlFor="page-input" className="visually-hidden">Ir a página número</label>
|
|
<span aria-hidden="true">Página </span>
|
|
<input
|
|
id="page-input"
|
|
type="number"
|
|
min="1"
|
|
max={totalPages}
|
|
value={currentPage}
|
|
onChange={(e) => goToPage(Number(e.target.value))}
|
|
className="page-input"
|
|
aria-label={`Página actual ${currentPage} de ${totalPages}`}
|
|
/>
|
|
<span aria-hidden="true"> de {totalPages}</span>
|
|
</div>
|
|
|
|
<button
|
|
className="pagination-button"
|
|
onClick={() => goToPage(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
aria-label="Ir a página siguiente"
|
|
aria-disabled={currentPage === totalPages}
|
|
>
|
|
<FaChevronRight aria-hidden="true" />
|
|
<span className="visually-hidden">Siguiente</span>
|
|
</button>
|
|
</nav>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BookList;
|