this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: extra responsivity

+167 -58
+59 -56
public/app.js
··· 102 102 const etagCache = { 103 103 stories: null, 104 104 totalStories: null, 105 - verifiedUsers: null 105 + verifiedUsers: null, 106 106 }; 107 - 107 + 108 108 // Cache for actual response data 109 109 const responseCache = { 110 110 stories: null, 111 111 totalStories: null, 112 - verifiedUsers: null 112 + verifiedUsers: null, 113 113 }; 114 114 115 115 // Fetch stories data 116 116 function fetchStories() { 117 117 // Update last refresh time 118 118 window.lastRefreshTime = Date.now(); 119 - 119 + 120 120 storyList.innerHTML = '<div class="loading">Loading stories...</div>'; 121 121 122 122 // Ensure live counters are running ··· 126 126 127 127 // Fetch total stories count first 128 128 const totalStoriesOptions = { 129 - headers: {} 129 + headers: {}, 130 130 }; 131 - 131 + 132 132 // Add If-None-Match header if we have an ETag 133 133 if (etagCache.totalStories) { 134 - totalStoriesOptions.headers['If-None-Match'] = etagCache.totalStories; 134 + totalStoriesOptions.headers["If-None-Match"] = etagCache.totalStories; 135 135 } 136 - 137 - fetch('/api/stats/total-stories', totalStoriesOptions) 136 + 137 + fetch("/api/stats/total-stories", totalStoriesOptions) 138 138 .then((response) => { 139 139 // Store the new ETag if available 140 - const etag = response.headers.get('ETag'); 140 + const etag = response.headers.get("ETag"); 141 141 if (etag) etagCache.totalStories = etag; 142 - 142 + 143 143 // If 304 Not Modified, use cached data 144 144 if (response.status === 304) { 145 - console.log('Total stories not modified, using cached data'); 145 + console.log("Total stories not modified, using cached data"); 146 146 return responseCache.totalStories; // Use cached data 147 147 } 148 - 148 + 149 149 return response.json(); 150 150 }) 151 151 .then((data) => { 152 152 if (data) { 153 153 // Store in cache for future 304 responses 154 154 responseCache.totalStories = data; 155 - 155 + 156 156 if (typeof data.count !== "undefined") { 157 157 totalStoriesCount = data.count; 158 158 currentFrontpageCountEl.textContent = totalStoriesCount; ··· 165 165 166 166 // Fetch verified user stats for the top row 167 167 const verifiedUsersOptions = { 168 - headers: {} 168 + headers: {}, 169 169 }; 170 - 170 + 171 171 // Add If-None-Match header if we have an ETag 172 172 if (etagCache.verifiedUsers) { 173 - verifiedUsersOptions.headers['If-None-Match'] = etagCache.verifiedUsers; 173 + verifiedUsersOptions.headers["If-None-Match"] = etagCache.verifiedUsers; 174 174 } 175 - 176 - fetch('/api/stats/verified-users', verifiedUsersOptions) 175 + 176 + fetch("/api/stats/verified-users", verifiedUsersOptions) 177 177 .then((response) => { 178 178 // Store the new ETag if available 179 - const etag = response.headers.get('ETag'); 179 + const etag = response.headers.get("ETag"); 180 180 if (etag) etagCache.verifiedUsers = etag; 181 - 181 + 182 182 // If 304 Not Modified, use cached data 183 183 if (response.status === 304) { 184 - console.log('Verified users stats not modified, using cached data'); 184 + console.log("Verified users stats not modified, using cached data"); 185 185 return responseCache.verifiedUsers; // Use cached data 186 186 } 187 - 187 + 188 188 return response.json(); 189 189 }) 190 190 .then((data) => { 191 191 if (data) { 192 192 // Store in cache for future 304 responses 193 193 responseCache.verifiedUsers = data; 194 - 194 + 195 195 verifiedUserStats = data; 196 196 updateTopStats(data); // Update UI with the new stats 197 197 } ··· 202 202 203 203 // Fetch stories 204 204 const storiesOptions = { 205 - headers: {} 205 + headers: {}, 206 206 }; 207 - 207 + 208 208 // Add If-None-Match header if we have an ETag 209 209 if (etagCache.stories) { 210 - storiesOptions.headers['If-None-Match'] = etagCache.stories; 210 + storiesOptions.headers["If-None-Match"] = etagCache.stories; 211 211 } 212 - 213 - fetch('/api/stories', storiesOptions) 212 + 213 + fetch("/api/stories", storiesOptions) 214 214 .then((response) => { 215 215 // Store the new ETag if available 216 - const etag = response.headers.get('ETag'); 216 + const etag = response.headers.get("ETag"); 217 217 if (etag) etagCache.stories = etag; 218 - 218 + 219 219 if (!response.ok) { 220 220 // Allow 304 Not Modified 221 221 if (response.status === 304) { 222 - console.log('Stories not modified, using cached data'); 222 + console.log("Stories not modified, using cached data"); 223 223 return responseCache.stories; // Use cached data 224 224 } 225 225 throw new Error("Network response was not ok"); ··· 230 230 if (data) { 231 231 // Store in cache for future 304 responses 232 232 responseCache.stories = data; 233 - 233 + 234 234 // Store all stories for filtering 235 235 allStories = data; 236 236 // Apply filters and update UI ··· 388 388 noGraph.innerHTML = '<div class="loading">Loading graph data...</div>'; 389 389 390 390 const options = { 391 - headers: {} 391 + headers: {}, 392 392 }; 393 - 393 + 394 394 // Add If-None-Match header if we have an ETag for this story 395 395 if (snapshotEtagCache[storyId]) { 396 - options.headers['If-None-Match'] = snapshotEtagCache[storyId]; 396 + options.headers["If-None-Match"] = snapshotEtagCache[storyId]; 397 397 } 398 - 398 + 399 399 // Use template literal correctly since we need string interpolation 400 400 fetch(`/api/story/${storyId}/snapshots`, options) 401 401 .then((response) => { 402 402 // Store the new ETag if available 403 - const etag = response.headers.get('ETag'); 403 + const etag = response.headers.get("ETag"); 404 404 if (etag) snapshotEtagCache[storyId] = etag; 405 - 405 + 406 406 if (!response.ok) { 407 407 // Allow 304 Not Modified 408 408 if (response.status === 304) { 409 - console.log(`Story ${storyId} snapshots not modified, using cached data`); 409 + console.log( 410 + `Story ${storyId} snapshots not modified, using cached data`, 411 + ); 410 412 // Use the cached data for this story ID 411 413 if (snapshotDataCache[storyId]) { 412 414 return snapshotDataCache[storyId]; ··· 427 429 428 430 // Cache the snapshots data for future use 429 431 snapshotDataCache[storyId] = snapshots; 430 - 432 + 431 433 displayGraph(snapshots); 432 434 }) 433 435 .catch((error) => { ··· 550 552 551 553 // Get total stories from the API 552 554 const options = { 553 - headers: {} 555 + headers: {}, 554 556 }; 555 - 557 + 556 558 // Add If-None-Match header if we have an ETag 557 559 if (etagCache.totalStories) { 558 - options.headers['If-None-Match'] = etagCache.totalStories; 560 + options.headers["If-None-Match"] = etagCache.totalStories; 559 561 } 560 - 561 - fetch('/api/stats/total-stories', options) 562 + 563 + fetch("/api/stats/total-stories", options) 562 564 .then((response) => { 563 565 // Store the new ETag if available 564 - const etag = response.headers.get('ETag'); 566 + const etag = response.headers.get("ETag"); 565 567 if (etag) etagCache.totalStories = etag; 566 - 568 + 567 569 // If 304 Not Modified, use cached data 568 570 if (response.status === 304) { 569 - console.log('Total stories not modified, using cached data for metrics'); 571 + console.log( 572 + "Total stories not modified, using cached data for metrics", 573 + ); 570 574 return responseCache.totalStories; 571 575 } 572 - 576 + 573 577 return response.json(); 574 578 }) 575 579 .then((data) => { 576 580 if (data) { 577 581 // Update cache 578 582 responseCache.totalStories = data; 579 - 583 + 580 584 if (data.count !== undefined) { 581 585 currentFrontpageCountEl.textContent = data.count; 582 586 } else { ··· 684 688 align-items: center; 685 689 gap: 5px; 686 690 transition: all 0.2s ease; 691 + margin-right: 1.1rem; 687 692 } 688 693 .duration:hover { 689 694 transform: translateY(-2px); ··· 752 757 // Update performance metrics if they exist 753 758 const topTenCountEl = document.getElementById("top-ten-count"); 754 759 const mostActiveTimeEl = document.getElementById("most-active-time"); 755 - 760 + 756 761 if (topTenCountEl) { 757 762 topTenCountEl.textContent = data.frontPageCount || "0"; 758 763 } 759 764 if (mostActiveTimeEl) { 760 765 mostActiveTimeEl.textContent = data.avgPeakPoints || "0"; 761 766 } 762 - 767 + 763 768 // Update verified user analytics metrics if they exist 764 769 const verifiedUserCountEl = document.getElementById("verified-user-count"); 765 770 const verifiedAvgPointsEl = document.getElementById("verified-avg-points"); 766 - 771 + 767 772 if (verifiedUserCountEl) { 768 773 verifiedUserCountEl.textContent = data.totalCount || "0"; 769 774 } ··· 873 878 874 879 // Start live counters 875 880 startLiveCounters(); 876 - 877 - 878 881 879 882 // We'll initialize the chart on demand rather than empty 880 883 });
+108 -2
public/index.html
··· 18 18 /> 19 19 <meta name="apple-mobile-web-app-title" content="HN Alerts" /> 20 20 <link rel="manifest" href="/public/site.webmanifest" /> 21 + <meta 22 + name="theme-color" 23 + content="#ff6600" 24 + media="(prefers-color-scheme: light)" 25 + /> 26 + <meta 27 + name="theme-color" 28 + content="#1a1a1a" 29 + media="(prefers-color-scheme: dark)" 30 + /> 21 31 <title>HN Alerts Dashboard</title> 22 32 <link 23 33 rel="stylesheet" ··· 85 95 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); 86 96 border: 1px solid rgba(59, 130, 246, 0.1); 87 97 transition: all 0.3s ease; 98 + max-width: 600px; 88 99 } 89 100 90 101 .graph-container:hover { ··· 471 482 472 483 .performance-metrics-container { 473 484 display: grid; 474 - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); 485 + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 475 486 gap: 1.5rem; 476 487 padding: 0.5rem 0; 477 488 } ··· 631 642 border-radius: 4px; 632 643 } 633 644 634 - @media (max-width: 768px) { 645 + @media (max-width: 1240px) { 646 + .graph-container { 647 + max-width: 500px; 648 + } 649 + 650 + .story-list { 651 + max-width: 100%; 652 + } 653 + } 654 + 655 + @media (max-width: 1015px) { 656 + .graph-container { 657 + max-width: 400px; 658 + } 659 + 660 + .story-list { 661 + max-width: 100%; 662 + } 663 + } 664 + 665 + @media (max-width: 765px) { 635 666 .main-container { 636 667 flex-direction: column; 637 668 flex-direction: column-reverse; 638 669 } 670 + 671 + .graph-container { 672 + max-width: 100%; 673 + } 674 + 675 + .story-list { 676 + max-width: 100%; 677 + } 639 678 } 640 679 641 680 @media (max-width: 540px) { 642 681 .graph-container { 643 682 height: 300px; 683 + } 684 + 685 + body { 686 + font-size: 0.8rem; 687 + } 688 + 689 + .story-item h3 { 690 + font-size: 1rem; 691 + } 692 + 693 + .metric-value { 694 + font-size: 1.6rem; 695 + } 696 + 697 + .stat-number { 698 + font-size: 1.9rem; 699 + } 700 + 701 + .performance-metrics-container { 702 + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); 703 + } 704 + } 705 + 706 + @media (max-width: 420px) { 707 + .graph-container { 708 + height: 240px; 709 + } 710 + 711 + .no-graph { 712 + font-size: 1rem; 713 + } 714 + 715 + body { 716 + font-size: 0.8rem; 717 + padding: 0.7rem; 718 + } 719 + 720 + .story-item { 721 + font-size: 0.9rem; 722 + } 723 + 724 + .story-item h3 { 725 + font-size: 1.1rem; 726 + } 727 + 728 + .story-meta { 729 + font-size: 0.8rem; 730 + } 731 + 732 + .metric-value { 733 + font-size: 1.2rem; 734 + } 735 + 736 + .stat-number { 737 + font-size: 1.5rem; 738 + } 739 + 740 + .header h1 { 741 + font-size: 1.3rem; 742 + } 743 + 744 + .header img { 745 + height: 50px; 746 + } 747 + 748 + .performance-metric { 749 + padding: 0.8rem 0.7rem; 644 750 } 645 751 } 646 752 </style>