this repo has no description
3
fork

Configure Feed

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

feat: fix most of jmyeets suggestions / improvements

+311 -48
+192 -15
public/app.js
··· 419 419 <span>Points: ${story.points}</span> 420 420 <span>Peak Points: ${story.peakPoints || story.points}</span> 421 421 <span>Comments: ${story.comments}</span> 422 - <span>By: ${story.by}${story.isFromMonitoredUser ? " 💖" : ""}</span> 422 + <span>By: <a href="https://news.ycombinator.com/user?id=${story.by}" target="_blank" class="profile-link">${story.by}</a>${story.isFromMonitoredUser ? " 💖" : ""}</span> 423 423 </div> 424 424 <div class="story-meta"> 425 425 <span>Detected: ${date}</span> ··· 453 453 e.target.classList.contains("external-link") || 454 454 e.target.closest(".external-link") || 455 455 e.target.classList.contains("item-link") || 456 - e.target.closest(".item-link") 456 + e.target.closest(".item-link") || 457 + e.target.classList.contains("profile-link") || 458 + e.target.closest(".profile-link") 457 459 ) { 458 460 return; 459 461 } ··· 467 469 i.classList.remove("active"); 468 470 } 469 471 item.classList.add("active"); 472 + 473 + // Scroll graph into view with smooth animation 474 + const graphContainer = document.getElementById("graph-container"); 475 + if (graphContainer) { 476 + setTimeout(() => { 477 + graphContainer.scrollIntoView({ 478 + behavior: 'smooth', 479 + block: 'center', 480 + inline: 'nearest' 481 + }); 482 + }, 100); 483 + } 470 484 }); 471 485 } 472 486 } ··· 480 494 if (activeStoryId === storyId) return; 481 495 activeStoryId = storyId; 482 496 497 + const graphHeader = document.getElementById("graph-header"); 498 + const graphTitle = document.getElementById("graph-title"); 499 + const graphMeta = document.getElementById("graph-meta"); 500 + 501 + // Find the story details for the header 502 + const story = allStories.find(s => s.id.toString() === storyId); 503 + if (story) { 504 + graphTitle.textContent = story.title; 505 + graphMeta.innerHTML = ` 506 + <span>By: <a href="https://news.ycombinator.com/user?id=${story.by}" target="_blank" class="profile-link">${story.by}</a></span> 507 + <span>Current Rank: #${story.rank}</span> 508 + <span>Points: ${story.points}</span> 509 + <span>Comments: ${story.comments}</span> 510 + `; 511 + graphHeader.style.display = "block"; 512 + } 513 + 483 514 noGraph.style.display = "flex"; 484 515 rankChart.style.display = "none"; 485 516 noGraph.innerHTML = '<div class="loading">Loading graph data...</div>'; ··· 570 601 { 571 602 label: "Rank (Position)", 572 603 data: positionDataPoints, 573 - borderColor: "#ff6600", 574 - backgroundColor: "rgba(255, 102, 0, 0.1)", 604 + borderColor: "#e05d00", 605 + backgroundColor: "rgba(224, 93, 0, 0.1)", 575 606 tension: 0.1, 576 607 yAxisID: "y", 608 + borderWidth: 3, 577 609 }, 578 610 { 579 611 label: "Score (Points)", 580 612 data: scoreDataPoints, 581 - borderColor: "#3b82f6", 582 - backgroundColor: "rgba(59, 130, 246, 0.1)", 613 + borderColor: "#0277BD", 614 + backgroundColor: "rgba(2, 119, 189, 0.1)", 583 615 tension: 0.1, 584 616 yAxisID: "y1", 617 + borderWidth: 3, 585 618 }, 586 619 ], 587 620 }, ··· 597 630 text: "Time", 598 631 }, 599 632 ticks: { 600 - callback: (value) => new Date(value).toLocaleString(), 633 + maxTicksLimit: 6, 634 + callback: (value) => { 635 + const date = new Date(value); 636 + const now = new Date(); 637 + const diffMs = now - date; 638 + const diffHours = diffMs / (1000 * 60 * 60); 639 + const diffDays = diffMs / (1000 * 60 * 60 * 24); 640 + 641 + // If less than 24 hours ago, show time only 642 + if (diffHours < 24) { 643 + return date.toLocaleTimeString([], { 644 + hour: '2-digit', 645 + minute: '2-digit', 646 + hour12: false 647 + }); 648 + } 649 + // 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 + }); 657 + } 658 + // 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 + } 668 + }, 601 669 }, 602 670 }, 603 671 y: { ··· 622 690 }, 623 691 }, 624 692 }, 693 + interaction: { 694 + intersect: false, 695 + mode: 'index', 696 + }, 625 697 plugins: { 626 698 tooltip: { 699 + backgroundColor: 'rgba(255, 255, 255, 0.98)', 700 + titleColor: '#1a1a1a', 701 + bodyColor: '#333', 702 + borderColor: 'rgba(255, 102, 0, 0.3)', 703 + borderWidth: 2, 704 + cornerRadius: 12, 705 + displayColors: true, 706 + padding: 12, 707 + titleAlign: 'center', 708 + titleFont: { 709 + size: 14, 710 + weight: 600, 711 + }, 712 + bodyFont: { 713 + size: 13, 714 + weight: 500, 715 + }, 716 + boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)', 717 + caretPadding: 10, 627 718 callbacks: { 628 719 title: (context) => 629 720 new Date(context[0].parsed.x).toLocaleString(), 630 721 }, 631 722 }, 723 + legend: { 724 + position: 'top', 725 + labels: { 726 + usePointStyle: true, 727 + boxWidth: 10, 728 + padding: 20, 729 + font: { 730 + size: 13, 731 + weight: 500, 732 + }, 733 + }, 734 + }, 735 + }, 736 + onHover: (event, elements) => { 737 + if (!event.native) return; 738 + 739 + const canvas = chart.canvas; 740 + const rect = canvas.getBoundingClientRect(); 741 + const x = event.native.clientX - rect.left; 742 + const y = event.native.clientY - rect.top; 743 + 744 + // Clear previous crosshair lines 745 + chart.update('none'); 746 + 747 + // Draw crosshair lines 748 + const ctx = chart.ctx; 749 + const chartArea = chart.chartArea; 750 + 751 + if (x >= chartArea.left && x <= chartArea.right && 752 + y >= chartArea.top && y <= chartArea.bottom) { 753 + 754 + ctx.save(); 755 + ctx.strokeStyle = 'rgba(153, 153, 153, 0.8)'; 756 + ctx.lineWidth = 1; 757 + ctx.setLineDash([3, 3]); 758 + 759 + // Vertical line 760 + ctx.beginPath(); 761 + ctx.moveTo(x, chartArea.top); 762 + ctx.lineTo(x, chartArea.bottom); 763 + ctx.stroke(); 764 + 765 + // Horizontal line 766 + ctx.beginPath(); 767 + ctx.moveTo(chartArea.left, y); 768 + ctx.lineTo(chartArea.right, y); 769 + ctx.stroke(); 770 + 771 + ctx.restore(); 772 + } 632 773 }, 633 774 }, 775 + }); 776 + 777 + // Add mouse leave event listener to clean up crosshair 778 + chart.canvas.addEventListener('mouseleave', () => { 779 + chart.update('none'); 634 780 }); 635 781 } 636 782 ··· 807 953 transform: translateY(-2px); 808 954 } 809 955 .duration-short { 810 - background-color: rgba(76, 175, 80, 0.2); 811 - color: #4CAF50; /* Green for new stories (<3h) */ 956 + background-color: rgba(76, 175, 80, 0.3); 957 + color: #2E7D32; /* Higher contrast green for new stories (<3h) */ 812 958 box-shadow: 0 2px 6px rgba(76, 175, 80, 0.2); 813 959 } 814 960 .duration-normal { 815 - background-color: rgba(3, 169, 244, 0.2); 816 - color: #03A9F4; /* Blue for normal-age stories (3-12h) */ 961 + background-color: rgba(3, 169, 244, 0.3); 962 + color: #0277BD; /* Higher contrast blue for normal-age stories (3-12h) */ 817 963 box-shadow: 0 2px 6px rgba(3, 169, 244, 0.2); 818 964 } 819 965 .duration-medium { 820 - background-color: rgba(255, 152, 0, 0.2); 821 - color: #FF9800; /* Orange for medium-age stories (12-24h) */ 966 + background-color: rgba(255, 152, 0, 0.3); 967 + color: #E65100; /* Higher contrast orange for medium-age stories (12-24h) */ 822 968 box-shadow: 0 2px 6px rgba(255, 152, 0, 0.2); 823 969 } 824 970 .duration-long { 825 - background-color: rgba(156, 39, 176, 0.2); 826 - color: #9C27B0; /* Purple for long-lasting stories (24h+) */ 971 + background-color: rgba(156, 39, 176, 0.3); 972 + color: #6A1B99; /* Higher contrast purple for long-lasting stories (24h+) */ 827 973 box-shadow: 0 2px 6px rgba(156, 39, 176, 0.2); 828 974 } 829 975 976 + @media (prefers-color-scheme: dark) { 977 + .duration-short { 978 + background-color: rgba(76, 175, 80, 0.4); 979 + color: #66BB6A; 980 + } 981 + .duration-normal { 982 + background-color: rgba(3, 169, 244, 0.4); 983 + color: #42A5F5; 984 + } 985 + .duration-medium { 986 + background-color: rgba(255, 152, 0, 0.4); 987 + color: #FFAB40; 988 + } 989 + .duration-long { 990 + background-color: rgba(156, 39, 176, 0.4); 991 + color: #AB47BC; 992 + } 993 + } 994 + 830 995 /* Tooltip styles for duration */ 831 996 .story-meta .duration { 832 997 cursor: help; 998 + } 999 + 1000 + /* Profile link styles */ 1001 + .profile-link { 1002 + color: var(--hn-orange); 1003 + text-decoration: none; 1004 + font-weight: 600; 1005 + transition: color 0.2s ease; 1006 + } 1007 + .profile-link:hover { 1008 + color: var(--hn-orange-hover); 1009 + text-decoration: underline; 833 1010 } 834 1011 `; 835 1012 document.head.appendChild(style);
+68 -15
public/index.html
··· 84 84 --bg-color: #ffffff; 85 85 --bg-secondary: #f5f5f5; 86 86 --text-color: #212121; 87 - --text-secondary: #555; 88 - --hn-orange: #ff6600; 89 - --hn-orange-hover: #e05d00; 90 - --border-color: #ddd; 87 + --text-secondary: #333; 88 + --hn-orange: #e05d00; 89 + --hn-orange-hover: #cc5500; 90 + --border-color: #ccc; 91 91 } 92 92 93 93 @media (prefers-color-scheme: dark) { ··· 95 95 --bg-color: #1a1a1a; 96 96 --bg-secondary: #2a2a2a; 97 97 --text-color: #f0f0f0; 98 - --text-secondary: #aaa; 99 - --border-color: #444; 98 + --text-secondary: #cccccc; 99 + --border-color: #555; 100 100 } 101 101 } 102 102 ··· 112 112 display: flex; 113 113 align-items: center; 114 114 justify-content: space-between; 115 + flex-wrap: wrap; 116 + gap: 1rem; 117 + } 118 + 119 + .header-text { 120 + display: flex; 121 + align-items: center; 122 + gap: 1rem; 123 + flex-wrap: wrap; 124 + } 125 + 126 + .header h1 { 127 + margin: 0; 128 + margin-right: 1rem; 129 + } 130 + 131 + .header p { 132 + margin: 0; 133 + color: var(--text-secondary); 134 + font-style: italic; 115 135 } 116 136 117 137 .header img { ··· 136 156 background-color: var(--bg-secondary); 137 157 border-radius: 16px; 138 158 padding: 1.5rem; 139 - height: 500px; 159 + height: 580px; 140 160 top: 1rem; 141 161 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); 142 162 border: 1px solid rgba(59, 130, 246, 0.1); ··· 144 164 max-width: 600px; 145 165 position: sticky; 146 166 top: 0.5rem; 167 + display: flex; 168 + flex-direction: column; 147 169 } 148 170 149 171 .graph-container:hover { ··· 154 176 .story-item { 155 177 background-color: var(--bg-secondary); 156 178 border-radius: 12px; 157 - padding: 1.2rem; 158 - margin-bottom: 1.2rem; 179 + padding: 1rem; 180 + margin-bottom: 1rem; 159 181 border-left: 4px solid var(--hn-orange); 160 182 cursor: pointer; 161 183 transition: all 0.3s ease; ··· 181 203 182 204 .story-item h3 { 183 205 margin-top: 0; 184 - margin-bottom: 0.8rem; 185 - font-size: 1.3rem; 206 + margin-bottom: 0.6rem; 207 + font-size: 1.2rem; 186 208 line-height: 1.3; 187 209 color: var(--text-color); 188 210 max-width: 95%; 189 211 } 190 212 191 213 .story-meta { 192 - font-size: 0.9rem; 214 + font-size: 0.85rem; 193 215 color: var(--text-secondary); 194 216 display: flex; 195 217 justify-content: space-between; 196 - margin-top: 0.8rem; 197 - padding-top: 0.8rem; 218 + margin-top: 0.6rem; 219 + padding-top: 0.6rem; 198 220 border-top: 1px solid rgba(255, 102, 0, 0.1); 199 221 max-width: 27rem; 200 222 } ··· 478 500 font-size: 0.9rem; 479 501 } 480 502 503 + .graph-header { 504 + margin-bottom: 1rem; 505 + padding-bottom: 0.75rem; 506 + border-bottom: 2px solid rgba(255, 102, 0, 0.1); 507 + } 508 + 509 + .graph-header h3 { 510 + margin: 0 0 0.5rem 0; 511 + font-size: 1.1rem; 512 + line-height: 1.3; 513 + color: var(--text-color); 514 + max-width: 100%; 515 + } 516 + 517 + .graph-meta { 518 + font-size: 0.85rem; 519 + color: var(--text-secondary); 520 + display: flex; 521 + flex-wrap: wrap; 522 + gap: 1rem; 523 + } 524 + 525 + #rank-chart { 526 + flex: 1; 527 + min-height: 0; 528 + } 529 + 481 530 .no-graph { 482 531 display: flex; 483 532 height: 100%; ··· 802 851 </head> 803 852 <body> 804 853 <div class="header"> 805 - <div> 854 + <div class="header-text"> 806 855 <h1>Hacker News Alerts Dashboard</h1> 807 856 <p>Monitor your HN front page appearances</p> 808 857 </div> ··· 866 915 </div> 867 916 868 917 <div class="graph-container" id="graph-container"> 918 + <div class="graph-header" id="graph-header" style="display: none;"> 919 + <h3 id="graph-title"></h3> 920 + <div class="graph-meta" id="graph-meta"></div> 921 + </div> 869 922 <div class="no-graph" id="no-graph"> 870 923 <p> 871 924 <span class="section-icon">📈</span><br />
+51 -18
public/item.html
··· 1073 1073 labels: timeLabels, 1074 1074 datasets: [ 1075 1075 { 1076 - label: "Position", 1076 + label: "Rank (Position)", 1077 1077 data: positions, 1078 - borderColor: "rgb(255, 102, 0)", 1079 - backgroundColor: "rgba(255, 102, 0, 0.1)", 1078 + borderColor: "#e05d00", 1079 + backgroundColor: "rgba(224, 93, 0, 0.1)", 1080 1080 fill: false, 1081 1081 tension: 0.1, 1082 1082 pointRadius: 3, 1083 1083 pointHoverRadius: 5, 1084 + borderWidth: 3, 1084 1085 yAxisID: "y-position", 1085 1086 }, 1086 1087 { 1087 - label: "Score", 1088 + label: "Score (Points)", 1088 1089 data: scores, 1089 - borderColor: "rgb(102, 187, 106)", 1090 - backgroundColor: "rgba(102, 187, 106, 0.1)", 1090 + borderColor: "#0277BD", 1091 + backgroundColor: "rgba(2, 119, 189, 0.1)", 1091 1092 fill: false, 1092 1093 tension: 0.1, 1093 1094 pointRadius: 3, 1094 1095 pointHoverRadius: 5, 1096 + borderWidth: 3, 1095 1097 yAxisID: "y-score", 1096 1098 }, 1097 1099 ], ··· 1109 1111 display: true, 1110 1112 text: "Time", 1111 1113 }, 1114 + ticks: { 1115 + maxTicksLimit: 6, 1116 + callback: function(value, index) { 1117 + const timestamps = this.chart.data.labels; 1118 + const timeLabel = timestamps[index]; 1119 + 1120 + // Parse the time string (HH:MM format) 1121 + if (timeLabel && timeLabel.includes(':')) { 1122 + return timeLabel; 1123 + } 1124 + 1125 + // Fallback to showing every other label 1126 + return index % 2 === 0 ? timeLabel : ''; 1127 + }, 1128 + }, 1112 1129 }, 1113 1130 "y-position": { 1114 1131 type: "linear", ··· 1116 1133 position: "left", 1117 1134 title: { 1118 1135 display: true, 1119 - text: "Position (#1 is Best)", 1120 - color: "rgb(255, 102, 0)", 1136 + text: "Rank", 1121 1137 }, 1122 1138 reverse: true, // Lower values (#1) at top 1123 1139 min: 1, ··· 1126 1142 callback: function (value) { 1127 1143 return "#" + value; 1128 1144 }, 1129 - color: "rgb(255, 102, 0)", 1130 1145 }, 1131 1146 grid: { 1132 - color: "rgba(255, 102, 0, 0.1)", 1147 + display: true, 1133 1148 }, 1134 1149 }, 1135 1150 "y-score": { ··· 1139 1154 title: { 1140 1155 display: true, 1141 1156 text: "Points", 1142 - color: "rgb(102, 187, 106)", 1143 1157 }, 1144 1158 min: 0, 1145 1159 suggestedMax: Math.max(...scores) * 1.1, 1146 - ticks: { 1147 - color: "rgb(102, 187, 106)", 1148 - }, 1149 1160 grid: { 1150 - drawOnChartArea: false, 1151 - color: "rgba(102, 187, 106, 0.1)", 1161 + display: false, 1152 1162 }, 1153 1163 }, 1154 1164 }, 1155 1165 plugins: { 1156 1166 tooltip: { 1167 + backgroundColor: 'rgba(255, 255, 255, 0.98)', 1168 + titleColor: '#1a1a1a', 1169 + bodyColor: '#333', 1170 + borderColor: 'rgba(255, 102, 0, 0.3)', 1171 + borderWidth: 2, 1172 + cornerRadius: 12, 1173 + displayColors: true, 1174 + padding: 12, 1175 + titleAlign: 'center', 1176 + titleFont: { 1177 + size: 14, 1178 + weight: 600, 1179 + }, 1180 + bodyFont: { 1181 + size: 13, 1182 + weight: 500, 1183 + }, 1184 + boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)', 1185 + caretPadding: 10, 1157 1186 callbacks: { 1158 1187 title: function (tooltipItems) { 1159 1188 const index = tooltipItems[0].dataIndex; ··· 1166 1195 if (label) { 1167 1196 label += ": "; 1168 1197 } 1169 - if (label === "Position: ") { 1198 + if (label === "Rank (Position): ") { 1170 1199 return label + "#" + context.raw; 1171 1200 } 1172 - if (label === "Score: ") { 1201 + if (label === "Score (Points): ") { 1173 1202 return ( 1174 1203 label + context.raw + " points" 1175 1204 ); ··· 1184 1213 usePointStyle: true, 1185 1214 boxWidth: 10, 1186 1215 padding: 20, 1216 + font: { 1217 + size: 13, 1218 + weight: 500, 1219 + }, 1187 1220 }, 1188 1221 }, 1189 1222 },