this repo has no description
3
fork

Configure Feed

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

feat: switch up colors to be more accessible

+198 -145
+151 -102
public/app.js
··· 100 100 let showVerifiedOnly = false; // Default to showing all stories 101 101 102 102 // Cache for ETags and data to support conditional requests with sessionStorage persistence 103 - const etagCache = JSON.parse(sessionStorage.getItem('etagCache') || JSON.stringify({ 104 - stories: null, 105 - totalStories: null, 106 - verifiedUsers: null, 107 - })); 103 + const etagCache = JSON.parse( 104 + sessionStorage.getItem("etagCache") || 105 + JSON.stringify({ 106 + stories: null, 107 + totalStories: null, 108 + verifiedUsers: null, 109 + }), 110 + ); 108 111 109 112 // Cache for actual response data 110 - const responseCache = JSON.parse(sessionStorage.getItem('responseCache') || JSON.stringify({ 111 - stories: null, 112 - totalStories: null, 113 - verifiedUsers: null, 114 - })); 113 + const responseCache = JSON.parse( 114 + sessionStorage.getItem("responseCache") || 115 + JSON.stringify({ 116 + stories: null, 117 + totalStories: null, 118 + verifiedUsers: null, 119 + }), 120 + ); 115 121 116 122 // Helper function to persist cache state 117 123 function persistCaches() { 118 - sessionStorage.setItem('etagCache', JSON.stringify(etagCache)); 119 - sessionStorage.setItem('responseCache', JSON.stringify(responseCache)); 124 + sessionStorage.setItem("etagCache", JSON.stringify(etagCache)); 125 + sessionStorage.setItem("responseCache", JSON.stringify(responseCache)); 120 126 } 121 127 122 128 // Fetch stories data ··· 189 195 } 190 196 191 197 // Add Accept-Encoding header if browser supports it 192 - if ('Accept-Encoding' in navigator) { 198 + if ("Accept-Encoding" in navigator) { 193 199 verifiedUsersOptions.headers["Accept-Encoding"] = "gzip, deflate, br"; 194 200 } 195 201 ··· 223 229 .catch((error) => { 224 230 console.error("Error fetching verified user stats:", error); 225 231 }); 226 - 232 + 227 233 // Fetch header stats for performance metrics 228 234 const statsHeaderOptions = { 229 235 headers: {}, 230 236 }; 231 - 237 + 232 238 // Add If-None-Match header if we have an ETag 233 239 if (etagCache.statsHeader) { 234 240 statsHeaderOptions.headers["If-None-Match"] = etagCache.statsHeader; 235 241 } 236 - 242 + 237 243 // Add Accept-Encoding header if browser supports it 238 - if ('Accept-Encoding' in navigator) { 244 + if ("Accept-Encoding" in navigator) { 239 245 statsHeaderOptions.headers["Accept-Encoding"] = "gzip, deflate, br"; 240 246 } 241 - 247 + 242 248 fetch("/api/stats/header", statsHeaderOptions) 243 249 .then((response) => { 244 250 // Store the new ETag if available ··· 247 253 etagCache.statsHeader = etag; 248 254 persistCaches(); 249 255 } 250 - 256 + 251 257 // If 304 Not Modified, use cached data 252 258 if (response.status === 304) { 253 259 console.log("Stats header not modified, using cached data"); 254 260 return Promise.resolve(responseCache.statsHeader); // Use cached data 255 261 } 256 - 262 + 257 263 return response.json(); 258 264 }) 259 265 .then((data) => { ··· 261 267 // Store in cache for future 304 responses 262 268 responseCache.statsHeader = data; 263 269 persistCaches(); 264 - 270 + 265 271 // Update UI with the stats header data 266 272 updateHeaderStats(data); 267 273 } ··· 281 287 } 282 288 283 289 // Add Accept-Encoding header if browser supports it 284 - if ('Accept-Encoding' in navigator) { 290 + if ("Accept-Encoding" in navigator) { 285 291 storiesOptions.headers["Accept-Encoding"] = "gzip, deflate, br"; 286 292 } 287 293 ··· 413 419 // Add verified badge if story is from a monitored user 414 420 html += ` 415 421 <div class="story-item${isCurrentTop ? " top-story" : ""}${isCurrentRankOne ? " top-ranked" : ""}${isBestRankOne && !isCurrentRankOne ? " previously-top-ranked" : ""}" data-id="${story.id}" data-url="${story.url}" data-timestamp="${timestampMs}"> 416 - <h3>${story.title}</h3> 422 + <h2>${story.title}</h2> 417 423 ${rankDisplay} 418 424 <div class="story-meta"> 419 425 <span>Points: ${story.points}</span> ··· 474 480 const graphContainer = document.getElementById("graph-container"); 475 481 if (graphContainer) { 476 482 setTimeout(() => { 477 - graphContainer.scrollIntoView({ 478 - behavior: 'smooth', 479 - block: 'center', 480 - inline: 'nearest' 483 + graphContainer.scrollIntoView({ 484 + behavior: "smooth", 485 + block: "center", 486 + inline: "nearest", 481 487 }); 482 488 }, 100); 483 489 } ··· 497 503 const graphHeader = document.getElementById("graph-header"); 498 504 const graphTitle = document.getElementById("graph-title"); 499 505 const graphMeta = document.getElementById("graph-meta"); 500 - 506 + 501 507 // Find the story details for the header 502 - const story = allStories.find(s => s.id.toString() === storyId); 508 + const story = allStories.find((s) => s.id.toString() === storyId); 503 509 if (story) { 504 510 graphTitle.textContent = story.title; 505 511 graphMeta.innerHTML = ` ··· 640 646 641 647 // If less than 24 hours ago, show time only 642 648 if (diffHours < 24) { 643 - return date.toLocaleTimeString([], { 644 - hour: '2-digit', 645 - minute: '2-digit', 646 - hour12: false 649 + return date.toLocaleTimeString([], { 650 + hour: "2-digit", 651 + minute: "2-digit", 652 + hour12: false, 647 653 }); 648 654 } 649 655 // If less than 7 days ago, show day and time 650 - else if (diffDays < 7) { 651 - return date.toLocaleDateString([], { 652 - weekday: 'short', 653 - hour: '2-digit', 654 - minute: '2-digit', 655 - hour12: false 656 + if (diffDays < 7) { 657 + return date.toLocaleDateString([], { 658 + weekday: "short", 659 + hour: "2-digit", 660 + minute: "2-digit", 661 + hour12: false, 656 662 }); 657 663 } 658 664 // Otherwise show date and time 659 - else { 660 - return date.toLocaleDateString([], { 661 - month: 'short', 662 - day: 'numeric', 663 - hour: '2-digit', 664 - minute: '2-digit', 665 - hour12: false 666 - }); 667 - } 665 + return date.toLocaleDateString([], { 666 + month: "short", 667 + day: "numeric", 668 + hour: "2-digit", 669 + minute: "2-digit", 670 + hour12: false, 671 + }); 668 672 }, 669 673 }, 670 674 }, ··· 692 696 }, 693 697 interaction: { 694 698 intersect: false, 695 - mode: 'index', 699 + mode: "index", 696 700 }, 697 701 plugins: { 698 702 tooltip: { 699 - backgroundColor: 'rgba(255, 255, 255, 0.98)', 700 - titleColor: '#1a1a1a', 701 - bodyColor: '#333', 702 - borderColor: 'rgba(255, 102, 0, 0.3)', 703 + backgroundColor: "rgba(255, 255, 255, 0.98)", 704 + titleColor: "#1a1a1a", 705 + bodyColor: "#333", 706 + borderColor: "rgba(255, 102, 0, 0.3)", 703 707 borderWidth: 2, 704 708 cornerRadius: 12, 705 709 displayColors: true, 706 710 padding: 12, 707 - titleAlign: 'center', 711 + titleAlign: "center", 708 712 titleFont: { 709 713 size: 14, 710 714 weight: 600, ··· 713 717 size: 13, 714 718 weight: 500, 715 719 }, 716 - boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)', 720 + boxShadow: "0 8px 24px rgba(0, 0, 0, 0.15)", 717 721 caretPadding: 10, 718 722 callbacks: { 719 723 title: (context) => ··· 721 725 }, 722 726 }, 723 727 legend: { 724 - position: 'top', 728 + position: "top", 725 729 labels: { 726 730 usePointStyle: true, 727 731 boxWidth: 10, ··· 735 739 }, 736 740 onHover: (event, elements) => { 737 741 if (!event.native) return; 738 - 742 + 739 743 const canvas = chart.canvas; 740 744 const rect = canvas.getBoundingClientRect(); 741 745 const x = event.native.clientX - rect.left; 742 746 const y = event.native.clientY - rect.top; 743 - 747 + 744 748 // Clear previous crosshair lines 745 - chart.update('none'); 746 - 749 + chart.update("none"); 750 + 747 751 // Draw crosshair lines 748 752 const ctx = chart.ctx; 749 753 const chartArea = chart.chartArea; 750 - 751 - if (x >= chartArea.left && x <= chartArea.right && 752 - y >= chartArea.top && y <= chartArea.bottom) { 753 - 754 + 755 + if ( 756 + x >= chartArea.left && 757 + x <= chartArea.right && 758 + y >= chartArea.top && 759 + y <= chartArea.bottom 760 + ) { 754 761 ctx.save(); 755 - ctx.strokeStyle = 'rgba(153, 153, 153, 0.8)'; 762 + ctx.strokeStyle = "rgba(153, 153, 153, 0.8)"; 756 763 ctx.lineWidth = 1; 757 764 ctx.setLineDash([3, 3]); 758 - 765 + 759 766 // Vertical line 760 767 ctx.beginPath(); 761 768 ctx.moveTo(x, chartArea.top); 762 769 ctx.lineTo(x, chartArea.bottom); 763 770 ctx.stroke(); 764 - 771 + 765 772 // Horizontal line 766 773 ctx.beginPath(); 767 774 ctx.moveTo(chartArea.left, y); 768 775 ctx.lineTo(chartArea.right, y); 769 776 ctx.stroke(); 770 - 777 + 771 778 ctx.restore(); 772 779 } 773 780 }, 774 781 }, 775 782 }); 776 - 783 + 777 784 // Add mouse leave event listener to clean up crosshair 778 - chart.canvas.addEventListener('mouseleave', () => { 779 - chart.update('none'); 785 + chart.canvas.addEventListener("mouseleave", () => { 786 + chart.update("none"); 780 787 }); 781 788 } 782 789 ··· 953 960 transform: translateY(-2px); 954 961 } 955 962 .duration-short { 956 - background-color: rgba(76, 175, 80, 0.3); 957 - color: #2E7D32; /* Higher contrast green for new stories (<3h) */ 958 - box-shadow: 0 2px 6px rgba(76, 175, 80, 0.2); 963 + background: linear-gradient(135deg, #A5D6A7 0%, #C8E6C9 100%); 964 + color: #1B5E20; 965 + border: 2px solid #4CAF50; 966 + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); 959 967 } 960 968 .duration-normal { 961 - background-color: rgba(3, 169, 244, 0.3); 962 - color: #0277BD; /* Higher contrast blue for normal-age stories (3-12h) */ 963 - box-shadow: 0 2px 6px rgba(3, 169, 244, 0.2); 969 + background: linear-gradient(135deg, #90CAF9 0%, #BBDEFB 100%); 970 + color: #0D47A1; 971 + border: 2px solid #2196F3; 972 + box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3); 964 973 } 965 974 .duration-medium { 966 - background-color: rgba(255, 152, 0, 0.3); 967 - color: #E65100; /* Higher contrast orange for medium-age stories (12-24h) */ 968 - box-shadow: 0 2px 6px rgba(255, 152, 0, 0.2); 975 + background: linear-gradient(135deg, #FFCC80 0%, #FFE0B2 100%); 976 + color: #E65100; 977 + border: 2px solid #FF9800; 978 + box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3); 969 979 } 970 980 .duration-long { 971 - background-color: rgba(156, 39, 176, 0.3); 972 - color: #6A1B99; /* Higher contrast purple for long-lasting stories (24h+) */ 973 - box-shadow: 0 2px 6px rgba(156, 39, 176, 0.2); 981 + background: linear-gradient(135deg, #CE93D8 0%, #E1BEE7 100%); 982 + color: #4A148C; 983 + border: 2px solid #9C27B0; 984 + box-shadow: 0 2px 8px rgba(156, 39, 176, 0.3); 974 985 } 975 986 976 987 @media (prefers-color-scheme: dark) { 977 988 .duration-short { 978 - background-color: rgba(76, 175, 80, 0.4); 979 - color: #66BB6A; 989 + background: linear-gradient(135deg, #2E7D32 0%, #388E3C 100%); 990 + color: #E8F5E8; 991 + border: 2px solid #66BB6A; 980 992 } 981 993 .duration-normal { 982 - background-color: rgba(3, 169, 244, 0.4); 983 - color: #42A5F5; 994 + background: linear-gradient(135deg, #1976D2 0%, #1E88E5 100%); 995 + color: #E3F2FD; 996 + border: 2px solid #42A5F5; 984 997 } 985 998 .duration-medium { 986 - background-color: rgba(255, 152, 0, 0.4); 987 - color: #FFAB40; 999 + background: linear-gradient(135deg, #F57C00 0%, #FB8C00 100%); 1000 + color: #FFF3E0; 1001 + border: 2px solid #FFAB40; 988 1002 } 989 1003 .duration-long { 990 - background-color: rgba(156, 39, 176, 0.4); 991 - color: #AB47BC; 1004 + background: linear-gradient(135deg, #7B1FA2 0%, #8E24AA 100%); 1005 + color: #F3E5F5; 1006 + border: 2px solid #AB47BC; 992 1007 } 993 1008 } 994 1009 ··· 999 1014 1000 1015 /* Profile link styles */ 1001 1016 .profile-link { 1002 - color: var(--hn-orange); 1017 + color: #9E3110; 1003 1018 text-decoration: none; 1004 1019 font-weight: 600; 1005 1020 transition: color 0.2s ease; 1006 1021 } 1007 1022 .profile-link:hover { 1008 - color: var(--hn-orange-hover); 1023 + color: #BF360C; 1009 1024 text-decoration: underline; 1010 1025 } 1026 + 1027 + /* External and item link styles */ 1028 + .external-link, .item-link { 1029 + color: #1565C0; 1030 + text-decoration: none; 1031 + font-weight: 500; 1032 + transition: color 0.2s ease; 1033 + } 1034 + .external-link:hover, .item-link:hover { 1035 + color: #0D47A1; 1036 + text-decoration: underline; 1037 + } 1038 + 1039 + @media (prefers-color-scheme: dark) { 1040 + .profile-link { 1041 + color: #FFAB91; 1042 + } 1043 + .profile-link:hover { 1044 + color: #FFCCBC; 1045 + } 1046 + 1047 + .external-link, .item-link { 1048 + color: #90CAF9; 1049 + } 1050 + .external-link:hover, .item-link:hover { 1051 + color: #BBDEFB; 1052 + } 1053 + } 1011 1054 `; 1012 1055 document.head.appendChild(style); 1013 1056 ··· 1045 1088 // Update header stats based on API data 1046 1089 function updateHeaderStats(data) { 1047 1090 // Update the stats from the header API endpoint 1048 - const currentFrontpageCountEl = document.getElementById("current-frontpage-count"); 1091 + const currentFrontpageCountEl = document.getElementById( 1092 + "current-frontpage-count", 1093 + ); 1049 1094 const topTenCountEl = document.getElementById("top-ten-count"); 1050 1095 const avgFrontpageTimeEl = document.getElementById("avg-frontpage-time"); 1051 1096 const mostActiveTimeEl = document.getElementById("most-active-time"); 1052 - 1097 + 1053 1098 if (currentFrontpageCountEl) { 1054 1099 currentFrontpageCountEl.textContent = data.totalStories || "0"; 1055 1100 } 1056 - 1101 + 1057 1102 if (topTenCountEl) { 1058 1103 topTenCountEl.textContent = data.topPoints || "0"; 1059 1104 } 1060 - 1105 + 1061 1106 if (avgFrontpageTimeEl && data.avgTimeOnFrontPageMinutes) { 1062 1107 const minutes = data.avgTimeOnFrontPageMinutes; 1063 1108 const hours = Math.floor(minutes / 60); 1064 1109 const remainingMinutes = minutes % 60; 1065 - avgFrontpageTimeEl.textContent = `${hours}:${remainingMinutes.toString().padStart(2, '0')}`; 1110 + avgFrontpageTimeEl.textContent = `${hours}:${remainingMinutes.toString().padStart(2, "0")}`; 1066 1111 } 1067 - 1112 + 1068 1113 // Mark stats as loaded 1069 1114 headerStatsLoaded = true; 1070 1115 } ··· 1072 1117 function updateTopStats(data) { 1073 1118 // This function is now primarily used for verified user data updates 1074 1119 // Main stats are updated directly from the /api/stats/header endpoint 1075 - 1120 + 1076 1121 // Update verified user analytics metrics if they exist 1077 1122 const verifiedUserCountEl = document.getElementById("verified-user-count"); 1078 1123 const verifiedAvgPointsEl = document.getElementById("verified-avg-points"); ··· 1084 1129 if (verifiedAvgPointsEl) { 1085 1130 verifiedAvgPointsEl.textContent = data.avgPeakPoints || "0"; 1086 1131 } 1087 - 1132 + 1088 1133 // Update the most active time element if it exists 1089 - if (mostActiveTimeEl && !mostActiveTimeEl.textContent.trim() && !headerStatsLoaded) { 1134 + if ( 1135 + mostActiveTimeEl && 1136 + !mostActiveTimeEl.textContent.trim() && 1137 + !headerStatsLoaded 1138 + ) { 1090 1139 mostActiveTimeEl.textContent = data.avgPeakPoints || "0"; 1091 1140 } 1092 1141 }
+17 -11
public/index.html
··· 201 201 ); 202 202 } 203 203 204 - .story-item h3 { 204 + .story-item h2 { 205 205 margin-top: 0; 206 206 margin-bottom: 0.6rem; 207 207 font-size: 1.2rem; ··· 424 424 } 425 425 426 426 .refresh-button { 427 - background: linear-gradient( 428 - 135deg, 429 - var(--hn-orange), 430 - var(--hn-orange-hover) 431 - ); 432 - color: white; 427 + background: #fa6800; 428 + color: #000000; 433 429 border: none; 434 430 padding: 0.6rem 1.2rem; 435 431 border-radius: 8px; ··· 642 638 .metric-label { 643 639 font-size: 1.1rem; 644 640 font-weight: 600; 645 - color: #3b82f6; 641 + color: #014f9d; 646 642 margin-bottom: 0.8rem; 647 643 letter-spacing: 0.03em; 648 644 } 649 645 646 + @media (prefers-color-scheme: dark) { 647 + .metric-label { 648 + color: #bbdefb !important; 649 + } 650 + } 651 + 650 652 .metric-value { 651 653 font-size: 2rem; 652 654 font-weight: bold; ··· 784 786 font-size: 0.8rem; 785 787 } 786 788 787 - .story-item h3 { 789 + .story-item h2 { 788 790 font-size: 1rem; 789 791 } 790 792 ··· 819 821 font-size: 0.9rem; 820 822 } 821 823 822 - .story-item h3 { 824 + .story-item h2 { 823 825 font-size: 1.1rem; 824 826 } 825 827 ··· 915 917 </div> 916 918 917 919 <div class="graph-container" id="graph-container"> 918 - <div class="graph-header" id="graph-header" style="display: none;"> 920 + <div 921 + class="graph-header" 922 + id="graph-header" 923 + style="display: none" 924 + > 919 925 <h3 id="graph-title"></h3> 920 926 <div class="graph-meta" id="graph-meta"></div> 921 927 </div>
+30 -32
public/item.html
··· 181 181 box-shadow: 0 6px 15px rgba(255, 102, 0, 0.15); 182 182 } 183 183 184 - .story-item h3 { 184 + .story-item h2 { 185 185 margin-top: 0; 186 186 margin-bottom: 0.8rem; 187 187 font-size: 1.3rem; ··· 190 190 max-width: 95%; 191 191 } 192 192 193 - .story-item h3 a { 193 + .story-item h2 a { 194 194 color: var(--hn-orange); 195 195 text-decoration: none; 196 196 transition: all 0.3s ease; 197 197 position: relative; 198 198 } 199 199 200 - .story-item h3 a:hover { 200 + .story-item h2 a:hover { 201 201 text-decoration: underline; 202 202 text-shadow: 0 2px 4px rgba(255, 102, 0, 0.3); 203 203 filter: drop-shadow(0 0 8px rgba(255, 102, 0, 0.6)); ··· 407 407 .metric-label { 408 408 font-size: 1.1rem; 409 409 font-weight: 600; 410 - color: #3b82f6; 410 + color: #014f9d; 411 411 margin-bottom: 0.8rem; 412 412 letter-spacing: 0.03em; 413 413 } 414 414 415 + @media (prefers-color-scheme: dark) { 416 + .metric-label { 417 + color: #bbdefb !important; 418 + } 419 + } 420 + 415 421 .metric-value { 416 422 font-size: 2rem; 417 423 font-weight: bold; ··· 420 426 -webkit-background-clip: text; 421 427 background-clip: text; 422 428 color: transparent; 423 - animation: numberCountUp 1.5s ease-out forwards; 424 - } 425 - 426 - @keyframes numberCountUp { 427 - from { 428 - opacity: 0; 429 - transform: scale(0.8); 430 - } 431 - to { 432 - opacity: 1; 433 - transform: scale(1); 434 - } 435 429 } 436 430 437 431 .metric-description { ··· 618 612 font-size: 0.8rem; 619 613 } 620 614 621 - .story-item h3 { 615 + .story-item h2 { 622 616 font-size: 1rem; 623 617 } 624 618 ··· 649 643 font-size: 0.9rem; 650 644 } 651 645 652 - .story-item h3 { 646 + .story-item h2 { 653 647 font-size: 1.1rem; 654 648 } 655 649 ··· 850 844 : ""; 851 845 852 846 const html = ` 853 - <h3> 847 + <h2> 854 848 <a href="${story.url || `https://news.ycombinator.com/item?id=${story.id}`}" target="_blank" rel="noopener"> 855 849 ${escapeHTML(story.title)} 856 850 </a> 857 - </h3> 851 + </h2> 858 852 859 853 <div class="meta-bar"> 860 854 <div class="meta-item"> ··· 1113 1107 }, 1114 1108 ticks: { 1115 1109 maxTicksLimit: 6, 1116 - callback: function(value, index) { 1117 - const timestamps = this.chart.data.labels; 1110 + callback: function (value, index) { 1111 + const timestamps = 1112 + this.chart.data.labels; 1118 1113 const timeLabel = timestamps[index]; 1119 - 1114 + 1120 1115 // Parse the time string (HH:MM format) 1121 - if (timeLabel && timeLabel.includes(':')) { 1116 + if ( 1117 + timeLabel && 1118 + timeLabel.includes(":") 1119 + ) { 1122 1120 return timeLabel; 1123 1121 } 1124 - 1122 + 1125 1123 // Fallback to showing every other label 1126 - return index % 2 === 0 ? timeLabel : ''; 1124 + return index % 2 === 0 ? timeLabel : ""; 1127 1125 }, 1128 1126 }, 1129 1127 }, ··· 1164 1162 }, 1165 1163 plugins: { 1166 1164 tooltip: { 1167 - backgroundColor: 'rgba(255, 255, 255, 0.98)', 1168 - titleColor: '#1a1a1a', 1169 - bodyColor: '#333', 1170 - borderColor: 'rgba(255, 102, 0, 0.3)', 1165 + backgroundColor: "rgba(255, 255, 255, 0.98)", 1166 + titleColor: "#1a1a1a", 1167 + bodyColor: "#333", 1168 + borderColor: "rgba(255, 102, 0, 0.3)", 1171 1169 borderWidth: 2, 1172 1170 cornerRadius: 12, 1173 1171 displayColors: true, 1174 1172 padding: 12, 1175 - titleAlign: 'center', 1173 + titleAlign: "center", 1176 1174 titleFont: { 1177 1175 size: 14, 1178 1176 weight: 600, ··· 1181 1179 size: 13, 1182 1180 weight: 500, 1183 1181 }, 1184 - boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)', 1182 + boxShadow: "0 8px 24px rgba(0, 0, 0, 0.15)", 1185 1183 caretPadding: 10, 1186 1184 callbacks: { 1187 1185 title: function (tooltipItems) {