this repo has no description
3
fork

Configure Feed

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

feat: improve item page styling again

+301 -117
+301 -117
public/item.html
··· 84 84 } 85 85 86 86 .graph-container { 87 + flex: 1; 88 + flex-basis: inherit; 87 89 background-color: var(--bg-secondary); 88 90 border-radius: 16px; 89 91 padding: 1.5rem; 90 92 height: 500px; 91 - margin: 1.5rem 0; 93 + top: 1rem; 92 94 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); 93 95 border: 1px solid rgba(59, 130, 246, 0.1); 94 96 transition: all 0.3s ease; 97 + max-width: 600px; 95 98 } 96 99 97 100 .graph-container:hover { ··· 107 110 border-left: 4px solid var(--hn-orange); 108 111 position: relative; 109 112 box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); 113 + transition: all 0.3s ease; 114 + } 115 + 116 + .story-item:hover { 117 + transform: translateX(6px); 118 + box-shadow: 0 6px 15px rgba(255, 102, 0, 0.15); 110 119 } 111 120 112 121 .story-item h3 { ··· 116 125 line-height: 1.3; 117 126 color: var(--text-color); 118 127 max-width: 95%; 128 + } 129 + 130 + .story-item h3 a { 131 + color: var(--hn-orange); 132 + text-decoration: none; 133 + transition: all 0.3s ease; 134 + position: relative; 135 + } 136 + 137 + .story-item h3 a:hover { 138 + text-decoration: underline; 139 + text-shadow: 0 2px 4px rgba(255, 102, 0, 0.3); 140 + filter: drop-shadow(0 0 8px rgba(255, 102, 0, 0.6)); 119 141 } 120 142 121 143 .story-meta { ··· 176 198 margin-right: auto; 177 199 } 178 200 201 + @media (prefers-color-scheme: dark) { 202 + .stats { 203 + background: linear-gradient( 204 + 135deg, 205 + rgba(255, 102, 0, 0.12) 0%, 206 + rgba(255, 102, 0, 0.05) 50%, 207 + rgba(255, 102, 0, 0.12) 100% 208 + ); 209 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); 210 + } 211 + } 212 + 179 213 .back-link { 180 214 display: inline-flex; 181 215 align-items: center; 182 216 gap: 8px; 183 217 margin-bottom: 16px; 184 - color: var(--hn-orange); 218 + background: linear-gradient( 219 + 135deg, 220 + var(--hn-orange), 221 + var(--hn-orange-hover) 222 + ); 223 + color: white; 185 224 text-decoration: none; 186 - font-weight: bold; 187 - transition: all 0.2s ease; 225 + font-weight: 600; 226 + padding: 0.8rem 1.5rem; 227 + border-radius: 12px; 228 + box-shadow: 0 4px 12px rgba(255, 102, 0, 0.25); 229 + transition: all 0.3s ease; 230 + letter-spacing: 0.02em; 188 231 } 189 232 190 233 .back-link:hover { 191 - color: var(--hn-orange-hover); 234 + background: linear-gradient( 235 + 135deg, 236 + var(--hn-orange-hover), 237 + var(--hn-orange) 238 + ); 239 + transform: translateY(-2px); 240 + box-shadow: 0 8px 20px rgba(255, 102, 0, 0.35); 192 241 } 193 242 194 243 .performance-summary { 195 - margin-top: 2rem; 244 + margin-bottom: 2rem; 196 245 } 197 246 198 - .section-icon { 247 + .stat-icon { 199 248 font-size: 2rem; 200 - color: var(--hn-orange); 249 + margin-bottom: 0.5rem; 250 + background: linear-gradient( 251 + 135deg, 252 + rgba(255, 102, 0, 0.2), 253 + rgba(255, 102, 0, 0.1) 254 + ); 255 + width: 50px; 256 + height: 50px; 257 + line-height: 50px; 258 + border-radius: 50%; 201 259 display: inline-block; 202 - margin-bottom: 1rem; 260 + position: relative; 261 + z-index: 1; 262 + box-shadow: 0 4px 10px rgba(255, 102, 0, 0.1); 263 + } 264 + 265 + .section-icon { 266 + display: inline-block; 267 + margin-right: 0.5rem; 268 + animation: float 3s ease-in-out infinite; 269 + } 270 + 271 + @keyframes float { 272 + 0% { 273 + transform: translateY(0); 274 + } 275 + 50% { 276 + transform: translateY(-5px); 277 + } 278 + 100% { 279 + transform: translateY(0); 280 + } 203 281 } 204 282 205 283 .performance-metrics-container { 206 284 display: grid; 207 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 208 - gap: 1rem; 209 - margin-bottom: 2rem; 285 + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 286 + gap: 1.5rem; 287 + padding: 0.5rem 0; 210 288 } 211 289 212 290 .performance-metric { 213 291 background-color: var(--bg-secondary); 214 - border-radius: 12px; 215 - padding: 1rem; 216 - display: flex; 217 - flex-direction: column; 218 - align-items: center; 292 + border-radius: 16px; 293 + padding: 1.8rem 1.5rem; 219 294 text-align: center; 220 - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); 295 + transition: all 0.3s ease; 296 + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05); 221 297 position: relative; 222 298 overflow: hidden; 223 - transition: all 0.3s ease; 299 + border: 1px solid rgba(59, 130, 246, 0.1); 300 + animation: fadeIn 0.8s ease-out forwards; 301 + animation-delay: calc(var(--i, 0) * 0.1s); 302 + opacity: 0; 303 + } 304 + 305 + @keyframes fadeIn { 306 + from { 307 + opacity: 0; 308 + transform: translateY(15px); 309 + } 310 + to { 311 + opacity: 1; 312 + transform: translateY(0); 313 + } 224 314 } 225 315 226 316 .performance-metric:hover { 227 - transform: translateY(-5px); 228 - box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); 317 + transform: translateY(-8px); 318 + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); 319 + border-color: rgba(59, 130, 246, 0.3); 229 320 } 230 321 231 322 .performance-metric::after { ··· 234 325 bottom: 0; 235 326 left: 0; 236 327 width: 100%; 237 - height: 3px; 238 - background: linear-gradient( 239 - to right, 240 - var(--hn-orange), 241 - var(--hn-orange-hover) 242 - ); 243 - transform: scaleX(0); 244 - transform-origin: bottom right; 245 - transition: transform 0.3s ease-out; 328 + height: 5px; 329 + background: linear-gradient(90deg, #3b82f6, #60a5fa); 246 330 } 247 331 248 332 @media (prefers-color-scheme: dark) { 249 333 .performance-metric { 250 - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); 334 + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); 335 + border: 1px solid rgba(59, 130, 246, 0.15); 251 336 } 252 337 253 338 .performance-metric:hover { 254 - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); 339 + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); 340 + border-color: rgba(59, 130, 246, 0.4); 255 341 } 256 342 } 257 343 258 344 .metric-label { 259 - color: var(--text-secondary); 260 - font-size: 0.9rem; 261 - margin-bottom: 0.5rem; 345 + font-size: 1.1rem; 346 + font-weight: 600; 347 + color: #3b82f6; 348 + margin-bottom: 0.8rem; 349 + letter-spacing: 0.03em; 262 350 } 263 351 264 352 .metric-value { 265 - font-size: 1.8rem; 353 + font-size: 2rem; 266 354 font-weight: bold; 267 - color: var(--hn-orange); 268 - margin-bottom: 0.5rem; 355 + margin: 0.8rem 0; 356 + background: linear-gradient(135deg, #3b82f6, #60a5fa); 357 + -webkit-background-clip: text; 358 + background-clip: text; 359 + color: transparent; 360 + animation: numberCountUp 1.5s ease-out forwards; 361 + } 362 + 363 + @keyframes numberCountUp { 364 + from { 365 + opacity: 0; 366 + transform: scale(0.8); 367 + } 368 + to { 369 + opacity: 1; 370 + transform: scale(1); 371 + } 269 372 } 270 373 271 374 .metric-description { 272 - font-size: 0.8rem; 375 + font-size: 0.9rem; 273 376 color: var(--text-secondary); 377 + margin-top: 0.5rem; 378 + font-style: italic; 274 379 } 275 380 276 381 .verified-badge { 277 - background-color: var(--hn-orange); 382 + position: absolute; 383 + top: 0.8rem; 384 + right: 0.8rem; 385 + background: linear-gradient(135deg, #4caf50, #2e7d32); 278 386 color: white; 279 - padding: 0.2em 0.5em; 280 - border-radius: 4px; 281 - font-size: 0.8rem; 387 + font-size: 0.75rem; 388 + padding: 0.25rem 0.5rem; 389 + border-radius: 8px; 282 390 font-weight: bold; 283 - display: inline-flex; 284 - align-items: center; 285 - gap: 4px; 286 - margin-left: 0.5rem; 287 - vertical-align: middle; 391 + box-shadow: 0 2px 5px rgba(76, 175, 80, 0.3); 392 + animation: badgePulse 2s infinite alternate; 393 + z-index: 2; 394 + } 395 + 396 + @keyframes badgePulse { 397 + from { 398 + box-shadow: 0 2px 5px rgba(76, 175, 80, 0.3); 399 + } 400 + to { 401 + box-shadow: 0 4px 8px rgba(76, 175, 80, 0.5); 402 + } 288 403 } 289 404 290 405 .no-graph { 291 406 display: flex; 292 - flex-direction: column; 407 + height: 100%; 408 + align-items: center; 293 409 justify-content: center; 294 - align-items: center; 295 - height: 100%; 410 + color: var(--text-secondary); 296 411 text-align: center; 297 - color: var(--text-secondary); 412 + font-size: 1.1rem; 413 + line-height: 1.6; 414 + background: linear-gradient( 415 + 135deg, 416 + rgba(59, 130, 246, 0.03), 417 + rgba(59, 130, 246, 0.08) 418 + ); 419 + border-radius: 16px; 420 + flex-direction: column; 421 + margin: 2rem; 422 + } 423 + 424 + .no-graph .error-message { 425 + color: #e74c3c; 426 + margin-top: 1rem; 427 + padding: 0.75rem 1rem; 428 + background-color: rgba(231, 76, 60, 0.1); 429 + border-radius: 8px; 430 + max-width: 90%; 431 + font-size: 0.9rem; 298 432 } 299 433 300 434 .error-message { 301 - color: var(--hn-orange); 435 + color: #e74c3c; 302 436 font-weight: bold; 303 437 margin-top: 1rem; 438 + padding: 0.75rem 1rem; 439 + background-color: rgba(231, 76, 60, 0.1); 440 + border-radius: 8px; 441 + border-left: 4px solid #e74c3c; 304 442 } 305 443 306 444 .link-group { ··· 314 452 display: inline-flex; 315 453 align-items: center; 316 454 gap: 6px; 317 - background-color: var(--bg-secondary); 318 - color: var(--hn-orange); 319 - border: 1px solid var(--hn-orange); 320 - border-radius: 8px; 321 - padding: 0.5rem 1rem; 455 + background: linear-gradient( 456 + 135deg, 457 + rgba(59, 130, 246, 0.1), 458 + rgba(59, 130, 246, 0.05) 459 + ); 460 + color: #3b82f6; 461 + border: 2px solid rgba(59, 130, 246, 0.2); 462 + border-radius: 12px; 463 + padding: 0.8rem 1.2rem; 322 464 text-decoration: none; 323 - font-weight: bold; 324 - transition: all 0.2s ease; 465 + font-weight: 600; 466 + transition: all 0.3s ease; 467 + box-shadow: 0 4px 10px rgba(59, 130, 246, 0.1); 468 + letter-spacing: 0.02em; 325 469 } 326 470 327 471 .external-link:hover { 328 - background-color: var(--hn-orange); 472 + background: linear-gradient(135deg, #3b82f6, #60a5fa); 329 473 color: white; 474 + transform: translateY(-3px); 475 + box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25); 476 + border-color: #3b82f6; 330 477 } 331 478 332 479 .meta-bar { 333 480 display: flex; 334 481 flex-wrap: wrap; 335 - gap: 1rem; 336 - margin-bottom: 1rem; 482 + gap: 1.5rem; 483 + margin-bottom: 1.5rem; 484 + padding: 1rem; 485 + background: linear-gradient( 486 + 135deg, 487 + rgba(255, 102, 0, 0.05), 488 + rgba(255, 102, 0, 0.02) 489 + ); 490 + border-radius: 12px; 491 + border: 1px solid rgba(255, 102, 0, 0.1); 337 492 } 338 493 339 494 .meta-item { ··· 342 497 gap: 0.5rem; 343 498 color: var(--text-secondary); 344 499 font-size: 0.9rem; 500 + font-weight: 500; 501 + padding: 0.5rem 0.8rem; 502 + border-radius: 8px; 503 + transition: all 0.3s ease; 504 + } 505 + 506 + .meta-item svg { 507 + flex-shrink: 0; 508 + } 509 + 510 + .meta-item strong { 511 + color: var(--hn-orange); 512 + font-weight: 700; 345 513 } 346 514 347 515 @media (max-width: 1240px) { 348 516 .graph-container { 349 - height: 400px; 517 + max-width: 500px; 518 + } 519 + 520 + .story-details { 521 + max-width: 100%; 350 522 } 351 523 } 352 524 353 525 @media (max-width: 1015px) { 354 526 .graph-container { 355 - height: 350px; 527 + max-width: 400px; 528 + } 529 + 530 + .story-details { 531 + max-width: 100%; 356 532 } 357 533 } 358 534 ··· 362 538 } 363 539 364 540 .graph-container { 365 - max-width: none; 541 + max-width: 100%; 366 542 } 367 543 368 544 .story-details { 369 - max-width: none; 545 + max-width: 100%; 370 546 } 371 547 } 372 548 ··· 376 552 } 377 553 378 554 body { 379 - padding: 1rem; 555 + font-size: 0.8rem; 380 556 } 381 557 382 558 .story-item h3 { 383 - font-size: 1.1rem; 559 + font-size: 1rem; 384 560 } 385 561 386 562 .metric-value { 387 - font-size: 1.5rem; 563 + font-size: 1.6rem; 388 564 } 389 565 390 566 .performance-metrics-container { 391 - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); 567 + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); 392 568 } 393 569 } 394 570 ··· 469 645 <div class="performance-summary"> 470 646 <div id="performance-metrics" class="performance-metrics-container"> 471 647 <!-- Performance metrics will be populated here --> 472 - <div class="performance-metric"> 648 + <div class="performance-metric" style="--i: 1"> 473 649 <div class="metric-label">Loading...</div> 474 650 <div class="metric-value">-</div> 475 651 </div> 476 - <div class="performance-metric"> 652 + <div class="performance-metric" style="--i: 2"> 477 653 <div class="metric-label">Loading...</div> 478 654 <div class="metric-value">-</div> 479 655 </div> 480 - <div class="performance-metric"> 656 + <div class="performance-metric" style="--i: 3"> 481 657 <div class="metric-label">Loading...</div> 482 658 <div class="metric-value">-</div> 483 659 </div> ··· 506 682 const storyId = urlParams.get("id"); 507 683 508 684 // Cache for snapshot data and ETags with localStorage persistence 509 - const snapshotDataCache = JSON.parse(localStorage.getItem('snapshotDataCache') || '{}'); 510 - const snapshotEtagCache = JSON.parse(localStorage.getItem('snapshotEtagCache') || '{}'); 511 - 685 + const snapshotDataCache = JSON.parse( 686 + localStorage.getItem("snapshotDataCache") || "{}", 687 + ); 688 + const snapshotEtagCache = JSON.parse( 689 + localStorage.getItem("snapshotEtagCache") || "{}", 690 + ); 691 + 512 692 // Helper functions for ETag management 513 693 const etagManager = { 514 694 save: () => { 515 - localStorage.setItem('snapshotEtagCache', JSON.stringify(snapshotEtagCache)); 516 - localStorage.setItem('snapshotDataCache', JSON.stringify(snapshotDataCache)); 695 + localStorage.setItem( 696 + "snapshotEtagCache", 697 + JSON.stringify(snapshotEtagCache), 698 + ); 699 + localStorage.setItem( 700 + "snapshotDataCache", 701 + JSON.stringify(snapshotDataCache), 702 + ); 517 703 }, 518 704 clear: () => { 519 - localStorage.removeItem('snapshotEtagCache'); 520 - localStorage.removeItem('snapshotDataCache'); 521 - Object.keys(snapshotDataCache).forEach(key => delete snapshotDataCache[key]); 522 - Object.keys(snapshotEtagCache).forEach(key => delete snapshotEtagCache[key]); 523 - } 705 + localStorage.removeItem("snapshotEtagCache"); 706 + localStorage.removeItem("snapshotDataCache"); 707 + Object.keys(snapshotDataCache).forEach( 708 + (key) => delete snapshotDataCache[key], 709 + ); 710 + Object.keys(snapshotEtagCache).forEach( 711 + (key) => delete snapshotEtagCache[key], 712 + ); 713 + }, 524 714 }; 525 715 526 716 // Check if we have a valid story ID ··· 539 729 540 730 function fetchStoryData(storyId) { 541 731 const options = { 542 - headers: {} 732 + headers: {}, 543 733 }; 544 - 734 + 545 735 // Add If-None-Match header if we have an ETag for this story 546 736 const storyKey = `story_${storyId}`; 547 737 if (snapshotEtagCache[storyKey]) { 548 - options.headers["If-None-Match"] = snapshotEtagCache[storyKey]; 738 + options.headers["If-None-Match"] = 739 + snapshotEtagCache[storyKey]; 549 740 } 550 - 741 + 551 742 fetch(`/api/story/${storyId}`, options) 552 743 .then((response) => { 553 744 // Store the new ETag if available ··· 556 747 snapshotEtagCache[storyKey] = etag; 557 748 etagManager.save(); 558 749 } 559 - 750 + 560 751 // If 304 Not Modified, use cached data 561 752 if (response.status === 304) { 562 - console.log("Story not modified, using cached data"); 753 + console.log( 754 + "Story not modified, using cached data", 755 + ); 563 756 return snapshotDataCache[storyKey]; 564 757 } 565 - 758 + 566 759 if (!response.ok) { 567 760 throw new Error(`HTTP error ${response.status}`); 568 761 } 569 - 570 - return response.json().then(data => { 762 + 763 + return response.json().then((data) => { 571 764 // Cache the data 572 765 snapshotDataCache[storyKey] = data; 573 766 etagManager.save(); ··· 629 822 <span>Current Score: <strong>${story.points}</strong> points</span> 630 823 </div> 631 824 632 - 633 825 </div> 634 826 635 827 <div class="story-meta"> ··· 671 863 label: "Peak Position", 672 864 value: story.peakRank ? `#${story.peakRank}` : "N/A", 673 865 description: "Best position achieved", 674 - icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--hn-orange)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 675 - <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> 676 - </svg>`, 866 + i: 1, 677 867 }, 678 868 { 679 869 label: "Peak Points", 680 870 value: story.peakPoints || story.points || "N/A", 681 871 description: "Maximum points received", 682 - icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--hn-orange)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 683 - <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon> 684 - </svg>`, 872 + i: 2, 685 873 }, 686 874 { 687 875 label: "Time on Front Page", ··· 689 877 ? formatDuration(story.timeOnFrontPage) 690 878 : "N/A", 691 879 description: "Total time tracked", 692 - icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--hn-orange)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 693 - <circle cx="12" cy="12" r="10"></circle> 694 - <polyline points="12 6 12 12 16 14"></polyline> 695 - </svg>`, 880 + i: 3, 696 881 }, 697 882 { 698 883 label: "Comments", 699 884 value: story.comments || "N/A", 700 885 description: "Discussion count", 701 - icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--hn-orange)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 702 - <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> 703 - </svg>`, 886 + i: 4, 704 887 }, 705 888 ]; 706 889 707 890 let html = ""; 708 891 metrics.forEach((metric) => { 709 892 html += ` 710 - <div class="performance-metric"> 711 - ${metric.icon} 893 + <div class="performance-metric" style="--i: ${metric.i}"> 712 894 <div class="metric-label">${metric.label}</div> 713 895 <div class="metric-value">${metric.value}</div> 714 896 <div class="metric-description">${metric.description}</div> ··· 718 900 719 901 container.innerHTML = html; 720 902 } 721 - 722 - // Comment link function removed as we now show comments in metrics 723 903 724 904 function loadStoryGraph(storyId) { 725 905 const noGraph = document.getElementById("no-graph"); ··· 734 914 735 915 // Create snapshot-specific cache key 736 916 const snapshotKey = `snapshots_${storyId}`; 737 - 917 + 738 918 // Add If-None-Match header if we have an ETag for this story's snapshots 739 919 if (snapshotEtagCache[snapshotKey]) { 740 - options.headers["If-None-Match"] = snapshotEtagCache[snapshotKey]; 920 + options.headers["If-None-Match"] = 921 + snapshotEtagCache[snapshotKey]; 741 922 } 742 923 743 924 fetch(`/api/story/${storyId}/snapshots`, options) ··· 751 932 752 933 if (!response.ok) { 753 934 // Handle 304 Not Modified - use cached data 754 - if (response.status === 304 && snapshotDataCache[snapshotKey]) { 935 + if ( 936 + response.status === 304 && 937 + snapshotDataCache[snapshotKey] 938 + ) { 755 939 console.log( 756 940 `Story ${storyId} snapshots not modified, using cached data`, 757 941 ); 758 942 // Use the cached data for this story ID 759 - if (snapshotDataCache[snapshotKey]) { 760 - return snapshotDataCache[snapshotKey]; 761 - } 943 + if (snapshotDataCache[snapshotKey]) { 944 + return snapshotDataCache[snapshotKey]; 945 + } 762 946 // If we don't have cached data, re-fetch 763 947 throw new Error( 764 948 "Cache miss on 304, re-fetching",