Creation chat
ClarkZhang
create an application to track books I read
02-25 11:40
暂无回复
No comments yet
登录 后可以评论
Assistant
(HTML code has been generated and updated; see above for the current version.)
This book tracking application includes:
1. **Modern UI with Dark Mode Support** - Using the Modern Blue theme with responsive design
2. **Book Management Features**:
- Add new books with title, author, genre, rating, date, and notes
- Delete books from your collection
- Search through your books
- Sort by date, title, or rating
3. **Data Persistence** - All books are saved to localStorage so they persist between sessions
4. **Responsive Design** - Works well on mobile, tablet, and desktop devices
5. **Visual Elements**:
- Star rating display
- Clean book cards with all relevant information
- Empty state when no books are added
- Stats counter showing total books tracked
6. **User Experience Features**:
- Today's date is pre-filled for convenience
- Real-time rating slider feedback
- Clear visual hierarchy and spacing
- Intuitive form controls
To use the application:
1. Fill out the form on the left to add books
2. View your books in the list on the right
3. Search or sort using the controls above the book list
4. Delete books using the trash icon
All data is automatically saved to your browser's localStorage. The console at the bottom-right of the preview will show logs when you add or delete books.
▼ Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100">
<!-- Theme toggle -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="Toggle theme" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){ var el=document.documentElement; var isDark=!el.classList.contains('dark'); el.classList.toggle('dark',isDark); localStorage.setItem('theme',isDark?'dark':'light'); applyTheme(isDark); }
function applyTheme(isDark){ document.getElementById('icon-moon').style.display=isDark?'none':'block'; document.getElementById('icon-sun').style.display=isDark?'block':'none'; }
document.addEventListener('DOMContentLoaded',function(){ var saved=localStorage.getItem('theme'); var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark',isDark); applyTheme(isDark); });
</script>
<!-- Custom modal -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">OK</button>
</div>
</div>
<script>
function showModal(msg){ var el=document.getElementById('custom-modal'); document.getElementById('custom-modal-msg').textContent=msg||''; el.classList.remove('hidden'); }
function hideModal(){ document.getElementById('custom-modal').classList.add('hidden'); }
</script>
<header class="bg-white dark:bg-slate-800 shadow-md">
<div class="container mx-auto px-4 py-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div class="flex items-center mb-4 md:mb-0">
<i class="fas fa-book-open text-blue-600 dark:text-blue-400 text-2xl mr-3"></i>
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100">My Book Tracker</h1>
</div>
<div class="flex items-center space-x-4">
<div class="bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 px-3 py-1 rounded-full text-sm font-medium">
<span id="total-books">0</span> books tracked
</div>
</div>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Add Book Form -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 border border-slate-200 dark:border-slate-700">
<h2 class="text-xl font-bold mb-4 text-slate-900 dark:text-slate-100">Add New Book</h2>
<form id="book-form" class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Book Title</label>
<input type="text" id="title" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="author" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Author</label>
<input type="text" id="author" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="genre" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Genre</label>
<input type="text" id="genre" class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="rating" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Rating (1-5)</label>
<div class="flex items-center">
<input type="range" id="rating" min="1" max="5" value="3" class="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer">
<span id="rating-value" class="ml-3 text-slate-700 dark:text-slate-300 font-medium">3</span>
</div>
</div>
<div>
<label for="date" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Date Finished</label>
<input type="date" id="date" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="notes" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Notes</label>
<textarea id="notes" rows="3" class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
Add Book
</button>
</form>
</div>
</div>
<!-- Book List -->
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 border border-slate-200 dark:border-slate-700">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-2 sm:mb-0">My Reading List</h2>
<div class="flex space-x-2">
<input type="text" id="search-input" placeholder="Search books..." class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm w-full sm:w-auto">
<select id="sort-select" class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
<option value="title">Title A-Z</option>
<option value="rating">Highest Rated</option>
</select>
</div>
</div>
<div id="book-list" class="space-y-4">
<!-- Books will be dynamically inserted here -->
<div class="text-center py-12" id="empty-state">
<i class="fas fa-book-open text-4xl text-slate-300 dark:text-slate-600 mb-4"></i>
<h3 class="text-lg font-medium text-slate-700 dark:text-slate-300 mb-2">No books added yet</h3>
<p class="text-slate-500 dark:text-slate-400">Add your first book using the form on the left</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Book Item Template -->
<template id="book-template">
<div class="book-item bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 border border-slate-200 dark:border-slate-600 hover:shadow-md transition-shadow">
<div class="flex">
<div class="flex-shrink-0 mr-4">
<div class="bg-slate-200 dark:bg-slate-600 border-2 border-dashed rounded-xl w-16 h-16 flex items-center justify-center">
<i class="fas fa-book text-slate-400 dark:text-slate-500"></i>
</div>
</div>
<div class="flex-grow">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start">
<div>
<h3 class="font-bold text-lg text-slate-900 dark:text-slate-100 book-title"></h3>
<p class="text-slate-600 dark:text-slate-400 book-author"></p>
<div class="flex items-center mt-1">
<div class="book-rating text-amber-500 flex"></div>
<span class="ml-2 text-sm text-slate-500 dark:text-slate-400 book-genre"></span>
</div>
</div>
<div class="mt-2 sm:mt-0 text-sm text-slate-500 dark:text-slate-400 book-date"></div>
</div>
<div class="mt-3 book-notes text-slate-700 dark:text-slate-300 text-sm"></div>
</div>
<div class="flex-shrink-0 ml-4">
<button class="delete-btn text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-colors">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
</template>
<script>
// Initialize state
let books = JSON.parse(localStorage.getItem('books')) || [];
// DOM Elements
const bookForm = document.getElementById('book-form');
const bookList = document.getElementById('book-list');
const totalBooksElement = document.getElementById('total-books');
const ratingSlider = document.getElementById('rating');
const ratingValue = document.getElementById('rating-value');
const searchInput = document.getElementById('search-input');
const sortSelect = document.getElementById('sort-select');
const emptyState = document.getElementById('empty-state');
// Update rating display
ratingSlider.addEventListener('input', () => {
ratingValue.textContent = ratingSlider.value;
});
// Set today's date as default
document.getElementById('date').valueAsDate = new Date();
// Load books from localStorage
function loadBooks() {
updateTotalBooks();
renderBooks();
}
// Save books to localStorage
function saveBooks() {
localStorage.setItem('books', JSON.stringify(books));
updateTotalBooks();
}
// Update total books counter
function updateTotalBooks() {
totalBooksElement.textContent = books.length;
}
// Render books to the list
function renderBooks(filteredBooks = null) {
const booksToRender = filteredBooks || books;
if (booksToRender.length === 0) {
bookList.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
bookList.innerHTML = '';
booksToRender.forEach(book => {
const bookElement = createBookElement(book);
bookList.appendChild(bookElement);
});
}
// Create book element from template
function createBookElement(book) {
const template = document.getElementById('book-template');
const bookElement = template.content.cloneNode(true);
bookElement.querySelector('.book-title').textContent = book.title;
bookElement.querySelector('.book-author').textContent = `by ${book.author}`;
bookElement.querySelector('.book-genre').textContent = book.genre;
bookElement.querySelector('.book-date').textContent = formatDate(book.date);
bookElement.querySelector('.book-notes').textContent = book.notes;
// Create star rating
const ratingElement = bookElement.querySelector('.book-rating');
for (let i = 1; i <= 5; i++) {
const star = document.createElement('i');
star.className = 'fas fa-star';
if (i <= book.rating) {
star.classList.add('text-amber-500');
} else {
star.classList.add('text-slate-300', 'dark:text-slate-600');
}
ratingElement.appendChild(star);
}
// Add delete functionality
const deleteBtn = bookElement.querySelector('.delete-btn');
deleteBtn.addEventListener('click', () => {
deleteBook(book.id);
});
// Create wrapper div to hold data attributes
const wrapper = document.createElement('div');
wrapper.appendChild(bookElement);
wrapper.querySelector('.book-item').dataset.id = book.id;
return wrapper.firstChild;
}
// Format date for display
function formatDate(dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
// Add new book
bookForm.addEventListener('submit', (e) => {
e.preventDefault();
const newBook = {
id: Date.now(),
title: document.getElementById('title').value,
author: document.getElementById('author').value,
genre: document.getElementById('genre').value,
rating: parseInt(ratingSlider.value),
date: document.getElementById('date').value,
notes: document.getElementById('notes').value
};
books.push(newBook);
saveBooks();
renderBooks();
bookForm.reset();
document.getElementById('date').valueAsDate = new Date();
ratingSlider.value = 3;
ratingValue.textContent = '3';
console.log('Added new book:', newBook);
});
// Delete book
function deleteBook(id) {
books = books.filter(book => book.id !== id);
saveBooks();
renderBooks();
console.log('Deleted book with ID:', id);
}
// Search and filter books
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
if (!searchTerm) {
renderBooks();
return;
}
const filteredBooks = books.filter(book =>
book.title.toLowerCase().includes(searchTerm) ||
book.author.toLowerCase().includes(searchTerm) ||
book.genre.toLowerCase().includes(searchTerm)
);
renderBooks(filteredBooks);
});
// Sort books
sortSelect.addEventListener('change', () => {
const sortBy = sortSelect.value;
switch(sortBy) {
case 'newest':
books.sort((a, b) => new Date(b.date) - new Date(a.date));
break;
case 'oldest':
books.sort((a, b) => new Date(a.date) - new Date(b.date));
break;
case 'title':
books.sort((a, b) => a.title.localeCompare(b.title));
break;
case 'rating':
books.sort((a, b) => b.rating - a.rating);
break;
}
renderBooks();
});
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
loadBooks();
// Sort initially by newest
sortSelect.value = 'newest';
books.sort((a, b) => new Date(b.date) - new Date(a.date));
renderBooks();
});
</script>
</body>
</html>
02-25 11:43
暂无回复
No comments yet
登录 后可以评论
ClarkZhang
After I've add book, there's 1 books tracked, but in "My Reading List" area there is no such book.
02-25 11:53
暂无回复
No comments yet
登录 后可以评论
Assistant
I've identified and fixed the issue. The problem was with how the book elements were being created from the template. Here's the corrected version:
(HTML code has been generated and updated; see above for the current version.)
The main changes I made to fix the issue:
1. **Fixed Template Cloning**: Changed how the template is cloned to ensure the book item is properly created:
```javascript
const bookClone = template.content.firstElementChild.cloneNode(true);
```
2. **Proper Element Selection**: Instead of wrapping the cloned content, we now directly work with the cloned element:
```javascript
bookClone.querySelector('.book-title').textContent = book.title;
```
3. **Cleaner Rating Implementation**: Completely rebuilt the star rating system to avoid any DOM issues:
```javascript
ratingElement.innerHTML = '';
for (let i = 1; i <= 5; i++) {
// Create star elements
}
```
4. **Direct Data Attribute Assignment**: Added the data-id directly to the book item element:
```javascript
bookClone.dataset.id = book.id;
```
Now when you add a book, it will correctly appear in the "My Reading List" section while maintaining the accurate book count. The deletion functionality also works properly now.
The console at the bottom-right of the preview will show logs when you add or delete books. All data is automatically saved to your browser's localStorage.
▼ Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100">
<!-- Theme toggle -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="Toggle theme" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){ var el=document.documentElement; var isDark=!el.classList.contains('dark'); el.classList.toggle('dark',isDark); localStorage.setItem('theme',isDark?'dark':'light'); applyTheme(isDark); }
function applyTheme(isDark){ document.getElementById('icon-moon').style.display=isDark?'none':'block'; document.getElementById('icon-sun').style.display=isDark?'block':'none'; }
document.addEventListener('DOMContentLoaded',function(){ var saved=localStorage.getItem('theme'); var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark',isDark); applyTheme(isDark); });
</script>
<!-- Custom modal -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">OK</button>
</div>
</div>
<script>
function showModal(msg){ var el=document.getElementById('custom-modal'); document.getElementById('custom-modal-msg').textContent=msg||''; el.classList.remove('hidden'); }
function hideModal(){ document.getElementById('custom-modal').classList.add('hidden'); }
</script>
<header class="bg-white dark:bg-slate-800 shadow-md">
<div class="container mx-auto px-4 py-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div class="flex items-center mb-4 md:mb-0">
<i class="fas fa-book-open text-blue-600 dark:text-blue-400 text-2xl mr-3"></i>
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100">My Book Tracker</h1>
</div>
<div class="flex items-center space-x-4">
<div class="bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 px-3 py-1 rounded-full text-sm font-medium">
<span id="total-books">0</span> books tracked
</div>
</div>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Add Book Form -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 border border-slate-200 dark:border-slate-700">
<h2 class="text-xl font-bold mb-4 text-slate-900 dark:text-slate-100">Add New Book</h2>
<form id="book-form" class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Book Title</label>
<input type="text" id="title" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="author" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Author</label>
<input type="text" id="author" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="genre" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Genre</label>
<input type="text" id="genre" class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="rating" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Rating (1-5)</label>
<div class="flex items-center">
<input type="range" id="rating" min="1" max="5" value="3" class="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer">
<span id="rating-value" class="ml-3 text-slate-700 dark:text-slate-300 font-medium">3</span>
</div>
</div>
<div>
<label for="date" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Date Finished</label>
<input type="date" id="date" required class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="notes" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Notes</label>
<textarea id="notes" rows="3" class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
Add Book
</button>
</form>
</div>
</div>
<!-- Book List -->
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 border border-slate-200 dark:border-slate-700">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-2 sm:mb-0">My Reading List</h2>
<div class="flex space-x-2">
<input type="text" id="search-input" placeholder="Search books..." class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm w-full sm:w-auto">
<select id="sort-select" class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
<option value="title">Title A-Z</option>
<option value="rating">Highest Rated</option>
</select>
</div>
</div>
<div id="book-list" class="space-y-4">
<!-- Books will be dynamically inserted here -->
<div class="text-center py-12" id="empty-state">
<i class="fas fa-book-open text-4xl text-slate-300 dark:text-slate-600 mb-4"></i>
<h3 class="text-lg font-medium text-slate-700 dark:text-slate-300 mb-2">No books added yet</h3>
<p class="text-slate-500 dark:text-slate-400">Add your first book using the form on the left</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Book Item Template -->
<template id="book-template">
<div class="book-item bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 border border-slate-200 dark:border-slate-600 hover:shadow-md transition-shadow">
<div class="flex">
<div class="flex-shrink-0 mr-4">
<div class="bg-slate-200 dark:bg-slate-600 border-2 border-dashed rounded-xl w-16 h-16 flex items-center justify-center">
<i class="fas fa-book text-slate-400 dark:text-slate-500"></i>
</div>
</div>
<div class="flex-grow">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start">
<div>
<h3 class="font-bold text-lg text-slate-900 dark:text-slate-100 book-title"></h3>
<p class="text-slate-600 dark:text-slate-400 book-author"></p>
<div class="flex items-center mt-1">
<div class="book-rating text-amber-500 flex"></div>
<span class="ml-2 text-sm text-slate-500 dark:text-slate-400 book-genre"></span>
</div>
</div>
<div class="mt-2 sm:mt-0 text-sm text-slate-500 dark:text-slate-400 book-date"></div>
</div>
<div class="mt-3 book-notes text-slate-700 dark:text-slate-300 text-sm"></div>
</div>
<div class="flex-shrink-0 ml-4">
<button class="delete-btn text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-colors">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
</template>
<script>
// Initialize state
let books = JSON.parse(localStorage.getItem('books')) || [];
// DOM Elements
const bookForm = document.getElementById('book-form');
const bookList = document.getElementById('book-list');
const totalBooksElement = document.getElementById('total-books');
const ratingSlider = document.getElementById('rating');
const ratingValue = document.getElementById('rating-value');
const searchInput = document.getElementById('search-input');
const sortSelect = document.getElementById('sort-select');
const emptyState = document.getElementById('empty-state');
// Update rating display
ratingSlider.addEventListener('input', () => {
ratingValue.textContent = ratingSlider.value;
});
// Set today's date as default
document.getElementById('date').valueAsDate = new Date();
// Load books from localStorage
function loadBooks() {
updateTotalBooks();
renderBooks();
}
// Save books to localStorage
function saveBooks() {
localStorage.setItem('books', JSON.stringify(books));
updateTotalBooks();
}
// Update total books counter
function updateTotalBooks() {
totalBooksElement.textContent = books.length;
}
// Render books to the list
function renderBooks(filteredBooks = null) {
const booksToRender = filteredBooks || books;
if (booksToRender.length === 0) {
bookList.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
bookList.innerHTML = '';
booksToRender.forEach(book => {
const bookElement = createBookElement(book);
bookList.appendChild(bookElement);
});
}
// Create book element from template
function createBookElement(book) {
const template = document.getElementById('book-template');
const bookClone = template.content.firstElementChild.cloneNode(true);
bookClone.querySelector('.book-title').textContent = book.title;
bookClone.querySelector('.book-author').textContent = `by ${book.author}`;
bookClone.querySelector('.book-genre').textContent = book.genre;
bookClone.querySelector('.book-date').textContent = formatDate(book.date);
bookClone.querySelector('.book-notes').textContent = book.notes;
// Create star rating
const ratingElement = bookClone.querySelector('.book-rating');
ratingElement.innerHTML = '';
for (let i = 1; i <= 5; i++) {
const star = document.createElement('i');
star.className = 'fas fa-star';
if (i <= book.rating) {
star.classList.add('text-amber-500');
} else {
star.classList.add('text-slate-300', 'dark:text-slate-600');
}
ratingElement.appendChild(star);
}
// Add delete functionality
const deleteBtn = bookClone.querySelector('.delete-btn');
deleteBtn.addEventListener('click', () => {
deleteBook(book.id);
});
// Add data-id attribute to the book item
bookClone.dataset.id = book.id;
return bookClone;
}
// Format date for display
function formatDate(dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
// Add new book
bookForm.addEventListener('submit', (e) => {
e.preventDefault();
const newBook = {
id: Date.now(),
title: document.getElementById('title').value,
author: document.getElementById('author').value,
genre: document.getElementById('genre').value,
rating: parseInt(ratingSlider.value),
date: document.getElementById('date').value,
notes: document.getElementById('notes').value
};
books.push(newBook);
saveBooks();
renderBooks();
bookForm.reset();
document.getElementById('date').valueAsDate = new Date();
ratingSlider.value = 3;
ratingValue.textContent = '3';
console.log('Added new book:', newBook);
});
// Delete book
function deleteBook(id) {
books = books.filter(book => book.id !== id);
saveBooks();
renderBooks();
console.log('Deleted book with ID:', id);
}
// Search and filter books
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
if (!searchTerm) {
renderBooks();
return;
}
const filteredBooks = books.filter(book =>
book.title.toLowerCase().includes(searchTerm) ||
book.author.toLowerCase().includes(searchTerm) ||
book.genre.toLowerCase().includes(searchTerm)
);
renderBooks(filteredBooks);
});
// Sort books
sortSelect.addEventListener('change', () => {
const sortBy = sortSelect.value;
switch(sortBy) {
case 'newest':
books.sort((a, b) => new Date(b.date) - new Date(a.date));
break;
case 'oldest':
books.sort((a, b) => new Date(a.date) - new Date(b.date));
break;
case 'title':
books.sort((a, b) => a.title.localeCompare(b.title));
break;
case 'rating':
books.sort((a, b) => b.rating - a.rating);
break;
}
renderBooks();
});
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
loadBooks();
// Sort initially by newest
sortSelect.value = 'newest';
books.sort((a, b) => new Date(b.date) - new Date(a.date));
renderBooks();
});
</script>
</body>
</html>
02-25 11:55
暂无回复
No comments yet
登录 后可以评论
Comments
暂无回复
No comments yet
登录 后可以评论