235
src/App.js
235
src/App.js
@@ -1,23 +1,228 @@
|
||||
import logo from './logo.svg';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaBook, FaShare } from 'react-icons/fa';
|
||||
import BookList from './components/BookList';
|
||||
import EpubViewer from './components/EpubViewer';
|
||||
import GatewaySelector from './components/GatewaySelector';
|
||||
import { parseIPFSCatalog, IPFS_GATEWAYS, buildEpubUrl } from './utils/ipfsParser';
|
||||
import './App.css';
|
||||
|
||||
/**
|
||||
* Componente principal de La Biblioteca
|
||||
* Aplicación para visualizar documentos EPUB desde IPFS
|
||||
*/
|
||||
function App() {
|
||||
const [books, setBooks] = useState([]);
|
||||
const [selectedBook, setSelectedBook] = useState(null);
|
||||
const [selectedGateway, setSelectedGateway] = useState(IPFS_GATEWAYS[0].url);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadCatalog();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedGateway]);
|
||||
|
||||
useEffect(() => {
|
||||
// Verificar si hay un hash IPFS en la URL al cargar la página
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ipfsHash = params.get('epub');
|
||||
const gateway = params.get('gateway');
|
||||
|
||||
if (ipfsHash && books.length > 0) {
|
||||
// Si hay un gateway en la URL, usarlo
|
||||
if (gateway && gateway !== selectedGateway) {
|
||||
setSelectedGateway(gateway);
|
||||
}
|
||||
// Cargar el EPUB directamente
|
||||
const book = books.find(b => b.ipfsHash === ipfsHash);
|
||||
if (book) {
|
||||
setSelectedBook(book);
|
||||
} else {
|
||||
// Si no está en el catálogo, crear un objeto básico
|
||||
setSelectedBook({
|
||||
id: ipfsHash,
|
||||
ipfsHash: ipfsHash,
|
||||
title: 'Libro compartido',
|
||||
filename: `${ipfsHash}.epub`,
|
||||
href: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [books]);
|
||||
|
||||
/**
|
||||
* Carga el catálogo de libros desde IPFS
|
||||
*/
|
||||
const loadCatalog = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Cargar el catálogo desde IPFS usando el gateway seleccionado
|
||||
const catalogUrl = `${selectedGateway}/ipfs/QmYuyPJZs6J6LfKgVFsMPX5kApWYfSLcr1BXox8NMS7UpL/`;
|
||||
const response = await fetch(catalogUrl);
|
||||
const htmlContent = await response.text();
|
||||
|
||||
// Parsear el contenido y extraer los libros
|
||||
const parsedBooks = parseIPFSCatalog(htmlContent);
|
||||
setBooks(parsedBooks);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error cargando el catálogo:', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Carga un libro directamente desde su hash IPFS
|
||||
*/
|
||||
const loadBookFromHash = (ipfsHash) => {
|
||||
// Buscar el libro en el catálogo si ya está cargado
|
||||
const book = books.find(b => b.ipfsHash === ipfsHash);
|
||||
if (book) {
|
||||
setSelectedBook(book);
|
||||
} else {
|
||||
// Si no está en el catálogo, crear un objeto básico
|
||||
setSelectedBook({
|
||||
id: ipfsHash,
|
||||
ipfsHash: ipfsHash,
|
||||
title: 'Libro compartido',
|
||||
filename: `${ipfsHash}.epub`,
|
||||
href: ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maneja la selección de un libro
|
||||
*/
|
||||
const handleBookSelect = (book) => {
|
||||
// Actualizar la URL del navegador
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('epub', book.ipfsHash);
|
||||
url.searchParams.set('gateway', selectedGateway);
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
setSelectedBook(book);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cierra el visor de EPUB
|
||||
*/
|
||||
const handleCloseViewer = () => {
|
||||
// Limpiar la URL
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete('epub');
|
||||
url.searchParams.delete('gateway');
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
setSelectedBook(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Genera el enlace para compartir
|
||||
*/
|
||||
const getShareUrl = () => {
|
||||
if (!selectedBook) return '';
|
||||
const url = new URL(window.location.origin + window.location.pathname);
|
||||
url.searchParams.set('epub', selectedBook.ipfsHash);
|
||||
url.searchParams.set('gateway', selectedGateway);
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Copia el enlace al portapapeles
|
||||
*/
|
||||
const handleShare = async () => {
|
||||
const shareUrl = getShareUrl();
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
setShowShareDialog(true);
|
||||
setTimeout(() => setShowShareDialog(false), 3000);
|
||||
} catch (err) {
|
||||
console.error('Error al copiar:', err);
|
||||
alert('No se pudo copiar el enlace. URL: ' + shareUrl);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
<a href="#main-content" className="skip-link">Saltar al contenido principal</a>
|
||||
{selectedBook ? (
|
||||
<>
|
||||
<EpubViewer
|
||||
epubUrl={buildEpubUrl(selectedBook.ipfsHash, selectedGateway)}
|
||||
bookTitle={selectedBook.title}
|
||||
bookFilename={selectedBook.filename}
|
||||
onClose={handleCloseViewer}
|
||||
onShare={handleShare}
|
||||
/>
|
||||
{showShareDialog && (
|
||||
<div
|
||||
className="share-toast"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
✓ Enlace copiado al portapapeles
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header className="App-header" role="banner">
|
||||
<div className="header-content">
|
||||
<div className="logo-section">
|
||||
<FaBook className="app-logo" aria-hidden="true" />
|
||||
<h1>La Biblioteca</h1>
|
||||
</div>
|
||||
<p className="subtitle">Visor de documentos EPUB accesibles desde IPFS</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="App-main" id="main-content" role="main">
|
||||
<div className="controls-section">
|
||||
<GatewaySelector
|
||||
gateways={IPFS_GATEWAYS}
|
||||
selectedGateway={selectedGateway}
|
||||
onGatewayChange={setSelectedGateway}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="catalog-section">
|
||||
<BookList
|
||||
books={books}
|
||||
onBookSelect={handleBookSelect}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="App-footer" role="contentinfo">
|
||||
<p>
|
||||
<span aria-label="Libros disponibles">{books.length} libros disponibles en IPFS</span>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://ipfs.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Información sobre IPFS (abre en nueva ventana)"
|
||||
>
|
||||
¿Qué es IPFS?
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://git.manalejandro.com/ale/labiblioteca"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Repositorio git del proyecto (abre en nueva ventana)"
|
||||
>
|
||||
📦 Repositorio
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Referencia en una nueva incidencia
Block a user