reading-log.js 6.7 KB

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