Spaces:
Running
Running
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>📚 Estante Digital</title> | |
| <link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📚</text></svg>"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#4f46e5', | |
| secondary: '#f9fafb', | |
| accent: '#f3f4f6' | |
| }, | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'] | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| .book-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| .form-input:focus { | |
| border-color: #4f46e5; | |
| box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-secondary min-h-screen font-sans"> | |
| <div class="max-w-6xl mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="mb-12 text-center py-8 bg-white rounded-xl shadow-sm"> | |
| <h1 class="text-4xl md:text-5xl font-bold text-gray-800 flex items-center justify-center gap-3"> | |
| <span class="text-5xl">📚</span> Estante Digital | |
| </h1> | |
| <p class="mt-3 text-gray-600 max-w-2xl mx-auto"> | |
| Gerencie os livros de história | |
| </p> | |
| </header> | |
| <!-- Search Section --> | |
| <section class="mb-16 bg-white rounded-xl shadow-sm p-6"> | |
| <h2 class="text-2xl font-semibold text-gray-800 mb-6 pb-2 border-b">Localizar Livros</h2> | |
| <form id="searchForm" class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchTitle">Título do Livro</label> | |
| <input | |
| type="text" | |
| id="searchTitle" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Digite o título do livro"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchAuthor">Autor</label> | |
| <input | |
| type="text" | |
| id="searchAuthor" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Nome do autor"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchPublisher">Editora</label> | |
| <input | |
| type="text" | |
| id="searchPublisher" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Nome da editora"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchYear">Ano de Publicação</label> | |
| <input | |
| type="number" | |
| id="searchYear" | |
| min="1000" | |
| max="2024" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Ano de publicação"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchRoom">Sala</label> | |
| <input | |
| type="text" | |
| id="searchRoom" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Número ou nome da sala"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="searchCabinet">Armário</label> | |
| <select | |
| id="searchCabinet" | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200"> | |
| <option value="">Todos os armários</option> | |
| <option value="Armário 1">Armário 1</option> | |
| <option value="Armário 2">Armário 2</option> | |
| <option value="Armário 3">Armário 3</option> | |
| <option value="Armário 4">Armário 4</option> | |
| <option value="Armário 5">Armário 5</option> | |
| </select> | |
| </div> | |
| <div class="md:col-span-2 flex gap-3 mt-4"> | |
| <button | |
| type="submit" | |
| class="flex-1 px-6 py-3 bg-primary hover:bg-indigo-700 text-white font-medium rounded-lg shadow-md transition duration-300 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"> | |
| 🔍 Pesquisar Livros | |
| </button> | |
| <button | |
| type="button" | |
| id="clearSearch" | |
| class="px-6 py-3 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-lg shadow-md transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300"> | |
| Limpar | |
| </button> | |
| </div> | |
| </form> | |
| <div class="mt-4 text-center text-sm text-gray-500"> | |
| developed by Vitão | |
| </div> | |
| </section> | |
| <!-- Form Section --> | |
| <section class="mb-16 bg-white rounded-xl shadow-sm p-6"> | |
| <div class="border-b border-gray-200"> | |
| <button id="toggleForm" class="flex items-center justify-between w-full py-4 text-left font-medium text-gray-800 hover:text-primary transition-colors duration-200"> | |
| <span class="text-2xl font-semibold">Cadastro de Livros</span> | |
| <svg id="formIcon" class="w-5 h-5 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| <form id="bookForm" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6 hidden"> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="title">Título do Livro</label> | |
| <input | |
| type="text" | |
| id="title" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Digite o título do livro"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="author">Autor</label> | |
| <input | |
| type="text" | |
| id="author" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Nome do autor"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="publisher">Editora</label> | |
| <input | |
| type="text" | |
| id="publisher" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Nome da editora"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="year">Ano de Publicação</label> | |
| <input | |
| type="number" | |
| id="year" | |
| min="1000" | |
| max="2024" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Ano de publicação"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="room">Sala</label> | |
| <input | |
| type="text" | |
| id="room" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200" | |
| placeholder="Número ou nome da sala"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 font-medium mb-2" for="cabinet">Armário</label> | |
| <select | |
| id="cabinet" | |
| required | |
| class="form-input w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition duration-200"> | |
| <option value="">Selecione um armário</option> | |
| <option value="Armário 1">Armário 1</option> | |
| <option value="Armário 2">Armário 2</option> | |
| <option value="Armário 3">Armário 3</option> | |
| <option value="Armário 4">Armário 4</option> | |
| <option value="Armário 5">Armário 5</option> | |
| </select> | |
| </div> | |
| <div class="md:col-span-2 mt-4"> | |
| <button | |
| type="submit" | |
| class="w-full md:w-auto px-6 py-3 bg-primary hover:bg-indigo-700 text-white font-medium rounded-lg shadow-md transition duration-300 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"> | |
| 📖 Salvar Livro | |
| </button> | |
| </div> | |
| </form> | |
| <div class="mt-4 text-center text-sm text-gray-500"> | |
| developed by Vitão | |
| </div> | |
| </section> | |
| <!-- Books List Section --> | |
| <section class="mb-16"> | |
| <h2 class="text-2xl font-semibold text-gray-800 mb-6 pb-2 border-b">Livros Cadastrados</h2> | |
| <div id="booksList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Book cards will be inserted here by JavaScript --> | |
| <div class="col-span-full text-center py-12 text-gray-500 hidden" id="noBooksMessage"> | |
| <div class="text-6xl mb-4">📖</div> | |
| <p class="text-xl">Nenhum livro cadastrado ainda</p> | |
| <p class="mt-2">Adicione seu primeiro livro usando o formulário acima</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Import/Export Section --> | |
| <section class="bg-white rounded-xl shadow-sm p-6"> | |
| <h2 class="text-2xl font-semibold text-gray-800 mb-6 pb-2 border-b">Importar/Exportar Livros</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div class="border border-gray-200 rounded-lg p-6"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-4">📥 Importar Livros</h3> | |
| <p class="text-gray-600 mb-4">Importe livros a partir de um arquivo CSV. Campos não preenchidos serão ignorados.</p> | |
| <input type="file" id="csvFile" accept=".csv" class="w-full text-sm text-gray-500 | |
| file:mr-4 file:py-2 file:px-4 | |
| file:rounded-lg file:border-0 | |
| file:text-sm file:font-semibold | |
| file:bg-primary file:text-white | |
| hover:file:bg-indigo-700 | |
| cursor-pointer"> | |
| <button id="importBtn" class="mt-4 w-full px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-lg transition duration-200"> | |
| Importar CSV | |
| </button> | |
| </div> | |
| <div class="border border-gray-200 rounded-lg p-6"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-4">📤 Exportar Livros</h3> | |
| <p class="text-gray-600 mb-4">Exporte todos os livros cadastrados para um arquivo Excel (.xlsx).</p> | |
| <button id="exportBtn" class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition duration-200"> | |
| Exportar para Excel | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-4 text-center text-sm text-gray-500"> | |
| developed by Vitão | |
| </div> | |
| </section> | |
| </div> | |
| <script> | |
| // Sample books data | |
| let books = JSON.parse(localStorage.getItem('books')) || []; | |
| // DOM Elements | |
| const bookForm = document.getElementById('bookForm'); | |
| const searchForm = document.getElementById('searchForm'); | |
| const booksList = document.getElementById('booksList'); | |
| const noBooksMessage = document.getElementById('noBooksMessage'); | |
| const clearSearchBtn = document.getElementById('clearSearch'); | |
| const toggleFormBtn = document.getElementById('toggleForm'); | |
| const formIcon = document.getElementById('formIcon'); | |
| // Render books | |
| function renderBooks(filteredBooks = null) { | |
| const booksToRender = filteredBooks || books; | |
| if (booksToRender.length === 0) { | |
| noBooksMessage.classList.remove('hidden'); | |
| booksList.innerHTML = ''; | |
| booksList.appendChild(noBooksMessage); | |
| return; | |
| } | |
| noBooksMessage.classList.add('hidden'); | |
| booksList.innerHTML = ''; | |
| booksToRender.forEach((book, index) => { | |
| const bookCard = document.createElement('div'); | |
| bookCard.className = 'book-card bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300'; | |
| bookCard.innerHTML = ` | |
| <div class="p-6"> | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="text-xl font-bold text-gray-800 mb-1">${book.title}</h3> | |
| <p class="text-gray-600 mb-3">por ${book.author}</p> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="bg-indigo-100 text-indigo-800 text-xs font-semibold px-2.5 py-1 rounded-full"> | |
| ${book.cabinet} | |
| </span> | |
| <span class="bg-green-100 text-green-800 text-xs font-semibold px-2.5 py-1 rounded-full"> | |
| Sala ${book.room} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="mt-4 space-y-2"> | |
| <div class="flex items-center text-gray-600 mb-1"> | |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path> | |
| </svg> | |
| <span class="font-semibold">Sala ${book.room}</span> | |
| </div> | |
| <div class="flex items-center text-gray-600 mb-2"> | |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path> | |
| </svg> | |
| <span class="font-semibold">${book.cabinet}</span> | |
| </div> | |
| <div class="flex items-center text-gray-600"> | |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> | |
| </svg> | |
| <span>${book.publisher}</span> | |
| </div> | |
| <div class="flex items-center text-gray-600"> | |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> | |
| </svg> | |
| <span>${book.year}</span> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-100 flex justify-end space-x-2"> | |
| <button class="edit-book px-3 py-1 text-sm bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200 transition" data-index="${index}"> | |
| Editar | |
| </button> | |
| <button class="delete-book px-3 py-1 text-sm bg-red-100 text-red-800 rounded hover:bg-red-200 transition" data-index="${index}"> | |
| Excluir | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| booksList.appendChild(bookCard); | |
| }); | |
| } | |
| // Edit book | |
| function setupEditBook(index) { | |
| const book = books[index]; | |
| document.getElementById('title').value = book.title; | |
| document.getElementById('author').value = book.author; | |
| document.getElementById('publisher').value = book.publisher; | |
| document.getElementById('year').value = book.year; | |
| document.getElementById('room').value = book.room; | |
| document.getElementById('cabinet').value = book.cabinet; | |
| // Change form button to "Atualizar" | |
| const submitBtn = bookForm.querySelector('button[type="submit"]'); | |
| submitBtn.textContent = '📖 Atualizar Livro'; | |
| submitBtn.dataset.editIndex = index; | |
| // Show form if hidden | |
| bookForm.classList.remove('hidden'); | |
| formIcon.classList.remove('rotate-180'); | |
| // Scroll to form | |
| bookForm.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| // Delete book | |
| function deleteBook(index) { | |
| if (confirm('Tem certeza que deseja excluir este livro?')) { | |
| books.splice(index, 1); | |
| localStorage.setItem('books', JSON.stringify(books)); | |
| renderBooks(); | |
| } | |
| } | |
| // Handle book form submission | |
| bookForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const editIndex = e.target.querySelector('button[type="submit"]').dataset.editIndex; | |
| const bookData = { | |
| title: document.getElementById('title').value, | |
| author: document.getElementById('author').value, | |
| publisher: document.getElementById('publisher').value, | |
| year: document.getElementById('year').value, | |
| room: document.getElementById('room').value, | |
| cabinet: document.getElementById('cabinet').value | |
| }; | |
| if (editIndex !== undefined) { | |
| // Update existing book | |
| books[editIndex] = bookData; | |
| // Reset form button | |
| const submitBtn = bookForm.querySelector('button[type="submit"]'); | |
| submitBtn.textContent = '📖 Salvar Livro'; | |
| delete submitBtn.dataset.editIndex; | |
| } else { | |
| // Add new book | |
| books.push(bookData); | |
| } | |
| localStorage.setItem('books', JSON.stringify(books)); | |
| // Reset form | |
| bookForm.reset(); | |
| // Re-render books | |
| renderBooks(); | |
| // Scroll to books section | |
| document.querySelector('section:last-child').scrollIntoView({ behavior: 'smooth' }); | |
| }); | |
| // Handle search form submission | |
| searchForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const searchTitle = document.getElementById('searchTitle').value.toLowerCase(); | |
| const searchAuthor = document.getElementById('searchAuthor').value.toLowerCase(); | |
| const searchPublisher = document.getElementById('searchPublisher').value.toLowerCase(); | |
| const searchYear = document.getElementById('searchYear').value; | |
| const searchRoom = document.getElementById('searchRoom').value.toLowerCase(); | |
| const searchCabinet = document.getElementById('searchCabinet').value; | |
| const filteredBooks = books.filter(book => { | |
| return ( | |
| (searchTitle === '' || book.title.toLowerCase().includes(searchTitle)) && | |
| (searchAuthor === '' || book.author.toLowerCase().includes(searchAuthor)) && | |
| (searchPublisher === '' || book.publisher.toLowerCase().includes(searchPublisher)) && | |
| (searchYear === '' || book.year.includes(searchYear)) && | |
| (searchRoom === '' || book.room.toLowerCase().includes(searchRoom)) && | |
| (searchCabinet === '' || book.cabinet === searchCabinet) | |
| ); | |
| }); | |
| renderBooks(filteredBooks); | |
| }); | |
| // Clear search form | |
| clearSearchBtn.addEventListener('click', function() { | |
| searchForm.reset(); | |
| renderBooks(); | |
| }); | |
| // Toggle form visibility | |
| toggleFormBtn.addEventListener('click', function() { | |
| bookForm.classList.toggle('hidden'); | |
| formIcon.classList.toggle('rotate-180'); | |
| }); | |
| // Event delegation for edit/delete buttons | |
| document.addEventListener('click', function(e) { | |
| if (e.target.classList.contains('edit-book')) { | |
| setupEditBook(e.target.dataset.index); | |
| } | |
| if (e.target.classList.contains('delete-book')) { | |
| deleteBook(e.target.dataset.index); | |
| } | |
| }); | |
| // Import books from CSV | |
| document.getElementById('importBtn').addEventListener('click', function() { | |
| const fileInput = document.getElementById('csvFile'); | |
| const file = fileInput.files[0]; | |
| if (!file) { | |
| alert('Por favor, selecione um arquivo CSV para importar.'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const contents = e.target.result; | |
| const lines = contents.split('\n'); | |
| const headers = lines[0].split(','); | |
| for (let i = 1; i < lines.length; i++) { | |
| if (lines[i].trim() === '') continue; | |
| const values = lines[i].split(','); | |
| const book = {}; | |
| for (let j = 0; j < headers.length; j++) { | |
| const header = headers[j].trim().toLowerCase(); | |
| const value = values[j] ? values[j].trim() : ''; | |
| if (header === 'título do livro' || header === 'titulo do livro') book.title = value; | |
| else if (header === 'autor') book.author = value; | |
| else if (header === 'editora') book.publisher = value; | |
| else if (header === 'ano') book.year = value; | |
| else if (header === 'sala') book.room = value; | |
| else if (header === 'armário' || header === 'armario') book.cabinet = value; | |
| } | |
| if (book.title) { | |
| books.push(book); | |
| } | |
| } | |
| localStorage.setItem('books', JSON.stringify(books)); | |
| renderBooks(); | |
| alert(`Importados ${lines.length - 1} livros com sucesso!`); | |
| fileInput.value = ''; | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| // Export books to Excel | |
| document.getElementById('exportBtn').addEventListener('click', function() { | |
| if (books.length === 0) { | |
| alert('Não há livros para exportar.'); | |
| return; | |
| } | |
| // Create CSV content with semicolon delimiter | |
| let csvContent = "Titulo do Livro;Autor;Editora;Ano;Sala;Armario\n"; | |
| books.forEach(book => { | |
| csvContent += `${book.title};${book.author};${book.publisher};${book.year};${book.room};${book.cabinet}\n`; | |
| }); | |
| // Create download link | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
| const bom = new Uint8Array([0xEF, 0xBB, 0xBF]); | |
| const blobWithBOM = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.setAttribute('href', url); | |
| link.setAttribute('download', 'livros_estante_digital.csv'); | |
| link.style.visibility = 'hidden'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| }); | |
| // Initial render | |
| renderBooks(); | |
| </script> | |
| </body> | |
| </html> | |