reading-log.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // ==================== Reading Log Functions ====================
  2. let booksPerMonthChart = null;
  3. let genresChart = null;
  4. async function loadReadingStats() {
  5. try {
  6. const response = await fetch('/api/reading-log/stats');
  7. if (response.status === 401) {
  8. window.location.href = '/login';
  9. return;
  10. }
  11. const stats = await response.json();
  12. // Hide loading, show stats
  13. document.getElementById('stats-loading').classList.add('hidden');
  14. document.getElementById('stats-container').classList.remove('hidden');
  15. // Update stat cards
  16. document.getElementById('stat-total-books').textContent = stats.total_books || 0;
  17. document.getElementById('stat-total-hours').textContent = stats.total_hours || 0;
  18. document.getElementById('stat-avg-rating').textContent =
  19. stats.average_rating ? `${stats.average_rating}/5` : 'N/A';
  20. document.getElementById('stat-streak').textContent = stats.current_streak || 0;
  21. // Render charts
  22. renderBooksPerMonthChart(stats.books_by_month || []);
  23. renderGenresChart(stats.books_by_genre || []);
  24. // Render recent books
  25. renderRecentBooks(stats.recent_books || []);
  26. } catch (error) {
  27. console.error('Error loading stats:', error);
  28. showMessage('Error loading statistics: ' + error.message, 'error');
  29. }
  30. }
  31. function renderBooksPerMonthChart(booksPerMonth) {
  32. const ctx = document.getElementById('books-per-month-chart');
  33. if (!ctx) return;
  34. // Destroy existing chart
  35. if (booksPerMonthChart) {
  36. booksPerMonthChart.destroy();
  37. }
  38. // Prepare data - show last 12 months
  39. const labels = booksPerMonth.slice(-12).map(item => `${item.month_name} ${item.year}`);
  40. const data = booksPerMonth.slice(-12).map(item => item.count);
  41. booksPerMonthChart = new Chart(ctx, {
  42. type: 'bar',
  43. data: {
  44. labels: labels,
  45. datasets: [{
  46. label: 'Books Finished',
  47. data: data,
  48. backgroundColor: 'rgba(99, 102, 241, 0.7)',
  49. borderColor: 'rgba(99, 102, 241, 1)',
  50. borderWidth: 1
  51. }]
  52. },
  53. options: {
  54. responsive: true,
  55. maintainAspectRatio: true,
  56. scales: {
  57. y: {
  58. beginAtZero: true,
  59. ticks: {
  60. stepSize: 1
  61. }
  62. }
  63. },
  64. plugins: {
  65. legend: {
  66. display: false
  67. }
  68. }
  69. }
  70. });
  71. }
  72. function renderGenresChart(booksByGenre) {
  73. const ctx = document.getElementById('genres-chart');
  74. if (!ctx) return;
  75. // Destroy existing chart
  76. if (genresChart) {
  77. genresChart.destroy();
  78. }
  79. // Show top 8 genres
  80. const topGenres = booksByGenre.slice(0, 8);
  81. const labels = topGenres.map(item => item.genre);
  82. const data = topGenres.map(item => item.count);
  83. // Generate colors
  84. const colors = [
  85. 'rgba(239, 68, 68, 0.7)',
  86. 'rgba(249, 115, 22, 0.7)',
  87. 'rgba(234, 179, 8, 0.7)',
  88. 'rgba(34, 197, 94, 0.7)',
  89. 'rgba(20, 184, 166, 0.7)',
  90. 'rgba(59, 130, 246, 0.7)',
  91. 'rgba(99, 102, 241, 0.7)',
  92. 'rgba(168, 85, 247, 0.7)'
  93. ];
  94. genresChart = new Chart(ctx, {
  95. type: 'doughnut',
  96. data: {
  97. labels: labels,
  98. datasets: [{
  99. data: data,
  100. backgroundColor: colors,
  101. borderWidth: 2,
  102. borderColor: '#ffffff'
  103. }]
  104. },
  105. options: {
  106. responsive: true,
  107. maintainAspectRatio: true,
  108. plugins: {
  109. legend: {
  110. position: 'right'
  111. }
  112. }
  113. }
  114. });
  115. }
  116. function renderRecentBooks(recentBooks) {
  117. const listEl = document.getElementById('books-list');
  118. const loadingEl = document.getElementById('books-loading');
  119. const emptyEl = document.getElementById('books-empty');
  120. loadingEl.classList.add('hidden');
  121. if (recentBooks.length === 0) {
  122. emptyEl.classList.remove('hidden');
  123. return;
  124. }
  125. listEl.classList.remove('hidden');
  126. const html = recentBooks.map(book => {
  127. const finishedDate = new Date(book.finished_at).toLocaleDateString();
  128. const ratingStars = book.rating ? '★'.repeat(book.rating) + '☆'.repeat(5 - book.rating) : 'Not rated';
  129. return `
  130. <div class="book-card">
  131. ${book.cover_url ? `<img src="${book.cover_url}" alt="${book.title}" class="book-cover">` : ''}
  132. <div class="book-details">
  133. <h3 class="book-title">${book.title}</h3>
  134. <p class="book-author">by ${book.author}</p>
  135. <div class="book-meta">
  136. <span class="book-date">Finished: ${finishedDate}</span>
  137. ${book.listening_duration ? `<span class="book-duration">${book.listening_duration}h</span>` : ''}
  138. </div>
  139. <div class="book-rating">
  140. <span class="rating-stars">${ratingStars}</span>
  141. <button class="btn btn-small" onclick="promptRating(${book.book_id})">Rate</button>
  142. </div>
  143. </div>
  144. </div>
  145. `;
  146. }).join('');
  147. listEl.innerHTML = html;
  148. }
  149. async function promptRating(sessionId) {
  150. const rating = prompt('Rate this book (1-5 stars):');
  151. if (!rating) return;
  152. const ratingNum = parseInt(rating);
  153. if (isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {
  154. showMessage('Please enter a rating between 1 and 5', 'error');
  155. return;
  156. }
  157. try {
  158. const formData = new FormData();
  159. formData.append('rating', ratingNum);
  160. const response = await fetch(`/api/sessions/${sessionId}/rating`, {
  161. method: 'PUT',
  162. body: formData
  163. });
  164. if (response.status === 401) {
  165. window.location.href = '/login';
  166. return;
  167. }
  168. const data = await response.json();
  169. if (data.status === 'success') {
  170. showMessage('Rating updated successfully!', 'success');
  171. loadReadingStats(); // Reload stats
  172. } else {
  173. showMessage(data.message || 'Failed to update rating', 'error');
  174. }
  175. } catch (error) {
  176. showMessage('Error updating rating: ' + error.message, 'error');
  177. }
  178. }