| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- // ==================== Reading Log Functions ====================
- let booksPerMonthChart = null;
- let genresChart = null;
- async function loadReadingStats() {
- try {
- const response = await fetch('/api/reading-log/stats');
- if (response.status === 401) {
- window.location.href = '/login';
- return;
- }
- const stats = await response.json();
- // Hide loading, show stats
- document.getElementById('stats-loading').classList.add('hidden');
- document.getElementById('stats-container').classList.remove('hidden');
- // Update stat cards
- document.getElementById('stat-total-books').textContent = stats.total_books || 0;
- document.getElementById('stat-total-hours').textContent = stats.total_hours || 0;
- document.getElementById('stat-avg-rating').textContent =
- stats.average_rating ? `${stats.average_rating}/5` : 'N/A';
- document.getElementById('stat-streak').textContent = stats.current_streak || 0;
- // Render charts
- renderBooksPerMonthChart(stats.books_by_month || []);
- renderGenresChart(stats.books_by_genre || []);
- // Render recent books
- renderRecentBooks(stats.recent_books || []);
- } catch (error) {
- console.error('Error loading stats:', error);
- showMessage('Error loading statistics: ' + error.message, 'error');
- }
- }
- function renderBooksPerMonthChart(booksPerMonth) {
- const ctx = document.getElementById('books-per-month-chart');
- if (!ctx) return;
- // Destroy existing chart
- if (booksPerMonthChart) {
- booksPerMonthChart.destroy();
- }
- // Prepare data - show last 12 months
- const labels = booksPerMonth.slice(-12).map(item => `${item.month_name} ${item.year}`);
- const data = booksPerMonth.slice(-12).map(item => item.count);
- booksPerMonthChart = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: labels,
- datasets: [{
- label: 'Books Finished',
- data: data,
- backgroundColor: 'rgba(99, 102, 241, 0.7)',
- borderColor: 'rgba(99, 102, 241, 1)',
- borderWidth: 1
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: true,
- scales: {
- y: {
- beginAtZero: true,
- ticks: {
- stepSize: 1
- }
- }
- },
- plugins: {
- legend: {
- display: false
- }
- }
- }
- });
- }
- function renderGenresChart(booksByGenre) {
- const ctx = document.getElementById('genres-chart');
- if (!ctx) return;
- // Destroy existing chart
- if (genresChart) {
- genresChart.destroy();
- }
- // Show top 8 genres
- const topGenres = booksByGenre.slice(0, 8);
- const labels = topGenres.map(item => item.genre);
- const data = topGenres.map(item => item.count);
- // Generate colors
- const colors = [
- 'rgba(239, 68, 68, 0.7)',
- 'rgba(249, 115, 22, 0.7)',
- 'rgba(234, 179, 8, 0.7)',
- 'rgba(34, 197, 94, 0.7)',
- 'rgba(20, 184, 166, 0.7)',
- 'rgba(59, 130, 246, 0.7)',
- 'rgba(99, 102, 241, 0.7)',
- 'rgba(168, 85, 247, 0.7)'
- ];
- genresChart = new Chart(ctx, {
- type: 'doughnut',
- data: {
- labels: labels,
- datasets: [{
- data: data,
- backgroundColor: colors,
- borderWidth: 2,
- borderColor: '#ffffff'
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: true,
- plugins: {
- legend: {
- position: 'right'
- }
- }
- }
- });
- }
- function renderRecentBooks(recentBooks) {
- const listEl = document.getElementById('books-list');
- const loadingEl = document.getElementById('books-loading');
- const emptyEl = document.getElementById('books-empty');
- loadingEl.classList.add('hidden');
- if (recentBooks.length === 0) {
- emptyEl.classList.remove('hidden');
- return;
- }
- listEl.classList.remove('hidden');
- const html = recentBooks.map(book => {
- const finishedDate = new Date(book.finished_at).toLocaleDateString();
- const ratingStars = book.rating ? '★'.repeat(book.rating) + '☆'.repeat(5 - book.rating) : 'Not rated';
- return `
- <div class="book-card">
- ${book.cover_url ? `<img src="${book.cover_url}" alt="${book.title}" class="book-cover">` : ''}
- <div class="book-details">
- <h3 class="book-title">${book.title}</h3>
- <p class="book-author">by ${book.author}</p>
- <div class="book-meta">
- <span class="book-date">Finished: ${finishedDate}</span>
- ${book.listening_duration ? `<span class="book-duration">${book.listening_duration}h</span>` : ''}
- </div>
- <div class="book-rating">
- <span class="rating-stars">${ratingStars}</span>
- <button class="btn btn-small" onclick="promptRating(${book.book_id})">Rate</button>
- </div>
- </div>
- </div>
- `;
- }).join('');
- listEl.innerHTML = html;
- }
- async function promptRating(sessionId) {
- const rating = prompt('Rate this book (1-5 stars):');
- if (!rating) return;
- const ratingNum = parseInt(rating);
- if (isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {
- showMessage('Please enter a rating between 1 and 5', 'error');
- return;
- }
- try {
- const formData = new FormData();
- formData.append('rating', ratingNum);
- const response = await fetch(`/api/sessions/${sessionId}/rating`, {
- method: 'PUT',
- body: formData
- });
- if (response.status === 401) {
- window.location.href = '/login';
- return;
- }
- const data = await response.json();
- if (data.status === 'success') {
- showMessage('Rating updated successfully!', 'success');
- loadReadingStats(); // Reload stats
- } else {
- showMessage(data.message || 'Failed to update rating', 'error');
- }
- } catch (error) {
- showMessage('Error updating rating: ' + error.message, 'error');
- }
- }
|