Spaces:
Running
Running
Add leaderboard with HuggingFace OAuth and Supabase integration
Browse files- README.md +2 -0
- index.html +501 -0
README.md
CHANGED
|
@@ -5,6 +5,8 @@ colorFrom: yellow
|
|
| 5 |
colorTo: red
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
# 🎯 Paper Popularity Game
|
|
|
|
| 5 |
colorTo: red
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
hf_oauth: true
|
| 9 |
+
hf_oauth_expiration_minutes: 480
|
| 10 |
---
|
| 11 |
|
| 12 |
# 🎯 Paper Popularity Game
|
index.html
CHANGED
|
@@ -410,6 +410,254 @@
|
|
| 410 |
font-weight: 700;
|
| 411 |
}
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
/* Loading State */
|
| 414 |
.loading {
|
| 415 |
display: flex;
|
|
@@ -604,10 +852,38 @@
|
|
| 604 |
</style>
|
| 605 |
</head>
|
| 606 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
<div class="high-score">
|
| 608 |
🏆 Best Streak: <span id="highScore">0</span>
|
| 609 |
</div>
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
<div class="container">
|
| 612 |
<header>
|
| 613 |
<h1>🎯 Paper Popularity Game</h1>
|
|
@@ -641,6 +917,226 @@
|
|
| 641 |
</footer>
|
| 642 |
</div>
|
| 643 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
<script>
|
| 645 |
// Game State
|
| 646 |
let papers = [];
|
|
@@ -881,6 +1377,11 @@
|
|
| 881 |
highScore = streak;
|
| 882 |
localStorage.setItem('paperGameHighScore', highScore);
|
| 883 |
highScoreEl.textContent = highScore;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
}
|
| 885 |
} else {
|
| 886 |
streak = 0;
|
|
|
|
| 410 |
font-weight: 700;
|
| 411 |
}
|
| 412 |
|
| 413 |
+
/* Top-left buttons container */
|
| 414 |
+
.top-left-buttons {
|
| 415 |
+
position: fixed;
|
| 416 |
+
top: 1.5vh;
|
| 417 |
+
left: 3vw;
|
| 418 |
+
display: flex;
|
| 419 |
+
gap: 10px;
|
| 420 |
+
z-index: 100;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
/* Leaderboard Button */
|
| 424 |
+
.leaderboard-btn {
|
| 425 |
+
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 140, 0, 0.1) 100%);
|
| 426 |
+
padding: clamp(8px, 1vh, 14px) clamp(16px, 2vw, 28px);
|
| 427 |
+
border-radius: 50px;
|
| 428 |
+
font-size: clamp(0.8rem, 1.1vw, 1rem);
|
| 429 |
+
border: 1px solid rgba(255, 215, 0, 0.3);
|
| 430 |
+
backdrop-filter: blur(10px);
|
| 431 |
+
font-weight: 600;
|
| 432 |
+
color: #ffd700;
|
| 433 |
+
cursor: pointer;
|
| 434 |
+
transition: all 0.3s ease;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.leaderboard-btn:hover {
|
| 438 |
+
transform: scale(1.05);
|
| 439 |
+
border-color: rgba(255, 215, 0, 0.6);
|
| 440 |
+
box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* Login Button */
|
| 444 |
+
.login-btn {
|
| 445 |
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
| 446 |
+
padding: clamp(8px, 1vh, 14px) clamp(16px, 2vw, 28px);
|
| 447 |
+
border-radius: 50px;
|
| 448 |
+
font-size: clamp(0.8rem, 1.1vw, 1rem);
|
| 449 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 450 |
+
backdrop-filter: blur(10px);
|
| 451 |
+
font-weight: 600;
|
| 452 |
+
color: #fff;
|
| 453 |
+
cursor: pointer;
|
| 454 |
+
transition: all 0.3s ease;
|
| 455 |
+
display: flex;
|
| 456 |
+
align-items: center;
|
| 457 |
+
gap: 8px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.login-btn:hover {
|
| 461 |
+
transform: scale(1.05);
|
| 462 |
+
border-color: rgba(255, 255, 255, 0.3);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
/* User Badge (when logged in) */
|
| 466 |
+
.user-badge {
|
| 467 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.1) 100%);
|
| 468 |
+
padding: clamp(6px, 0.8vh, 10px) clamp(12px, 1.5vw, 20px);
|
| 469 |
+
border-radius: 50px;
|
| 470 |
+
font-size: clamp(0.75rem, 1vw, 0.9rem);
|
| 471 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 472 |
+
backdrop-filter: blur(10px);
|
| 473 |
+
font-weight: 600;
|
| 474 |
+
color: #22c55e;
|
| 475 |
+
display: flex;
|
| 476 |
+
align-items: center;
|
| 477 |
+
gap: 8px;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.user-badge img {
|
| 481 |
+
width: 24px;
|
| 482 |
+
height: 24px;
|
| 483 |
+
border-radius: 50%;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
/* Leaderboard Modal */
|
| 487 |
+
.modal-overlay {
|
| 488 |
+
position: fixed;
|
| 489 |
+
top: 0;
|
| 490 |
+
left: 0;
|
| 491 |
+
right: 0;
|
| 492 |
+
bottom: 0;
|
| 493 |
+
background: rgba(0, 0, 0, 0.8);
|
| 494 |
+
backdrop-filter: blur(8px);
|
| 495 |
+
z-index: 200;
|
| 496 |
+
display: none;
|
| 497 |
+
justify-content: center;
|
| 498 |
+
align-items: center;
|
| 499 |
+
padding: 20px;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.modal-overlay.visible {
|
| 503 |
+
display: flex;
|
| 504 |
+
animation: fadeIn 0.3s ease;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
@keyframes fadeIn {
|
| 508 |
+
from { opacity: 0; }
|
| 509 |
+
to { opacity: 1; }
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.modal {
|
| 513 |
+
background: linear-gradient(145deg, rgba(26, 26, 46, 0.98) 0%, rgba(15, 52, 96, 0.98) 100%);
|
| 514 |
+
border-radius: 24px;
|
| 515 |
+
padding: clamp(24px, 4vw, 40px);
|
| 516 |
+
max-width: 500px;
|
| 517 |
+
width: 100%;
|
| 518 |
+
max-height: 80vh;
|
| 519 |
+
overflow-y: auto;
|
| 520 |
+
border: 1px solid rgba(255, 215, 0, 0.2);
|
| 521 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 40px rgba(255, 215, 0, 0.1);
|
| 522 |
+
animation: slideIn 0.3s ease;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
@keyframes slideIn {
|
| 526 |
+
from { transform: translateY(-30px); opacity: 0; }
|
| 527 |
+
to { transform: translateY(0); opacity: 1; }
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.modal-header {
|
| 531 |
+
display: flex;
|
| 532 |
+
justify-content: space-between;
|
| 533 |
+
align-items: center;
|
| 534 |
+
margin-bottom: 24px;
|
| 535 |
+
padding-bottom: 16px;
|
| 536 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.modal-title {
|
| 540 |
+
font-size: clamp(1.3rem, 2.5vw, 1.8rem);
|
| 541 |
+
font-weight: 700;
|
| 542 |
+
color: #ffd700;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.modal-close {
|
| 546 |
+
background: rgba(255, 255, 255, 0.1);
|
| 547 |
+
border: none;
|
| 548 |
+
color: #fff;
|
| 549 |
+
width: 36px;
|
| 550 |
+
height: 36px;
|
| 551 |
+
border-radius: 50%;
|
| 552 |
+
cursor: pointer;
|
| 553 |
+
font-size: 1.2rem;
|
| 554 |
+
transition: all 0.3s ease;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.modal-close:hover {
|
| 558 |
+
background: rgba(239, 68, 68, 0.3);
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
/* Leaderboard Table */
|
| 562 |
+
.leaderboard-list {
|
| 563 |
+
display: flex;
|
| 564 |
+
flex-direction: column;
|
| 565 |
+
gap: 8px;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.leaderboard-entry {
|
| 569 |
+
display: flex;
|
| 570 |
+
align-items: center;
|
| 571 |
+
gap: 12px;
|
| 572 |
+
padding: 12px 16px;
|
| 573 |
+
background: rgba(255, 255, 255, 0.05);
|
| 574 |
+
border-radius: 12px;
|
| 575 |
+
transition: all 0.3s ease;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.leaderboard-entry:hover {
|
| 579 |
+
background: rgba(255, 255, 255, 0.08);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.leaderboard-entry.current-user {
|
| 583 |
+
background: rgba(255, 215, 0, 0.1);
|
| 584 |
+
border: 1px solid rgba(255, 215, 0, 0.3);
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.leaderboard-rank {
|
| 588 |
+
font-weight: 700;
|
| 589 |
+
font-size: 1.1rem;
|
| 590 |
+
width: 32px;
|
| 591 |
+
text-align: center;
|
| 592 |
+
color: #64748b;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.leaderboard-entry:nth-child(1) .leaderboard-rank { color: #ffd700; }
|
| 596 |
+
.leaderboard-entry:nth-child(2) .leaderboard-rank { color: #c0c0c0; }
|
| 597 |
+
.leaderboard-entry:nth-child(3) .leaderboard-rank { color: #cd7f32; }
|
| 598 |
+
|
| 599 |
+
.leaderboard-avatar {
|
| 600 |
+
width: 36px;
|
| 601 |
+
height: 36px;
|
| 602 |
+
border-radius: 50%;
|
| 603 |
+
background: rgba(255, 255, 255, 0.1);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.leaderboard-name {
|
| 607 |
+
flex: 1;
|
| 608 |
+
font-weight: 500;
|
| 609 |
+
overflow: hidden;
|
| 610 |
+
text-overflow: ellipsis;
|
| 611 |
+
white-space: nowrap;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.leaderboard-score {
|
| 615 |
+
font-weight: 700;
|
| 616 |
+
color: #22c55e;
|
| 617 |
+
font-size: 1.1rem;
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.leaderboard-empty {
|
| 621 |
+
text-align: center;
|
| 622 |
+
padding: 40px 20px;
|
| 623 |
+
color: #64748b;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.leaderboard-loading {
|
| 627 |
+
text-align: center;
|
| 628 |
+
padding: 40px 20px;
|
| 629 |
+
color: #94a3b8;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.login-prompt {
|
| 633 |
+
text-align: center;
|
| 634 |
+
padding: 20px;
|
| 635 |
+
background: rgba(255, 215, 0, 0.1);
|
| 636 |
+
border-radius: 12px;
|
| 637 |
+
margin-top: 16px;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.login-prompt p {
|
| 641 |
+
color: #94a3b8;
|
| 642 |
+
margin-bottom: 12px;
|
| 643 |
+
font-size: 0.9rem;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.login-prompt button {
|
| 647 |
+
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
|
| 648 |
+
color: #1a1a2e;
|
| 649 |
+
border: none;
|
| 650 |
+
padding: 10px 24px;
|
| 651 |
+
border-radius: 50px;
|
| 652 |
+
font-weight: 600;
|
| 653 |
+
cursor: pointer;
|
| 654 |
+
transition: all 0.3s ease;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.login-prompt button:hover {
|
| 658 |
+
transform: scale(1.05);
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
/* Loading State */
|
| 662 |
.loading {
|
| 663 |
display: flex;
|
|
|
|
| 852 |
</style>
|
| 853 |
</head>
|
| 854 |
<body>
|
| 855 |
+
<!-- Top Left Buttons: Leaderboard & Login -->
|
| 856 |
+
<div class="top-left-buttons">
|
| 857 |
+
<button class="leaderboard-btn" id="leaderboardBtn">🏆 Leaderboard</button>
|
| 858 |
+
<button class="login-btn" id="loginBtn">🤗 Sign in</button>
|
| 859 |
+
<div class="user-badge" id="userBadge" style="display: none;">
|
| 860 |
+
<img id="userAvatar" src="" alt="avatar">
|
| 861 |
+
<span id="userName"></span>
|
| 862 |
+
</div>
|
| 863 |
+
</div>
|
| 864 |
+
|
| 865 |
+
<!-- High Score -->
|
| 866 |
<div class="high-score">
|
| 867 |
🏆 Best Streak: <span id="highScore">0</span>
|
| 868 |
</div>
|
| 869 |
|
| 870 |
+
<!-- Leaderboard Modal -->
|
| 871 |
+
<div class="modal-overlay" id="leaderboardModal">
|
| 872 |
+
<div class="modal">
|
| 873 |
+
<div class="modal-header">
|
| 874 |
+
<h2 class="modal-title">🏆 Global Leaderboard</h2>
|
| 875 |
+
<button class="modal-close" id="modalClose">✕</button>
|
| 876 |
+
</div>
|
| 877 |
+
<div class="leaderboard-list" id="leaderboardList">
|
| 878 |
+
<div class="leaderboard-loading">Loading leaderboard...</div>
|
| 879 |
+
</div>
|
| 880 |
+
<div class="login-prompt" id="loginPrompt" style="display: none;">
|
| 881 |
+
<p>Sign in with your Hugging Face account to compete!</p>
|
| 882 |
+
<button id="modalLoginBtn">🤗 Sign in with Hugging Face</button>
|
| 883 |
+
</div>
|
| 884 |
+
</div>
|
| 885 |
+
</div>
|
| 886 |
+
|
| 887 |
<div class="container">
|
| 888 |
<header>
|
| 889 |
<h1>🎯 Paper Popularity Game</h1>
|
|
|
|
| 917 |
</footer>
|
| 918 |
</div>
|
| 919 |
|
| 920 |
+
<!-- Hugging Face Hub for OAuth -->
|
| 921 |
+
<script type="module">
|
| 922 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from 'https://esm.sh/@huggingface/hub';
|
| 923 |
+
|
| 924 |
+
// ========================================
|
| 925 |
+
// SUPABASE CONFIGURATION
|
| 926 |
+
// ========================================
|
| 927 |
+
// To enable the leaderboard, create a free Supabase project at https://supabase.com
|
| 928 |
+
// Then replace these values with your project's URL and anon key
|
| 929 |
+
const SUPABASE_URL = 'YOUR_SUPABASE_URL'; // e.g., 'https://xxxxx.supabase.co'
|
| 930 |
+
const SUPABASE_ANON_KEY = 'YOUR_SUPABASE_ANON_KEY';
|
| 931 |
+
|
| 932 |
+
// Check if Supabase is configured
|
| 933 |
+
const isSupabaseConfigured = SUPABASE_URL !== 'YOUR_SUPABASE_URL' && SUPABASE_ANON_KEY !== 'YOUR_SUPABASE_ANON_KEY';
|
| 934 |
+
|
| 935 |
+
let supabase = null;
|
| 936 |
+
if (isSupabaseConfigured) {
|
| 937 |
+
const { createClient } = await import('https://esm.sh/@supabase/supabase-js@2');
|
| 938 |
+
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// ========================================
|
| 942 |
+
// USER STATE
|
| 943 |
+
// ========================================
|
| 944 |
+
let currentUser = null;
|
| 945 |
+
|
| 946 |
+
// ========================================
|
| 947 |
+
// DOM ELEMENTS
|
| 948 |
+
// ========================================
|
| 949 |
+
const loginBtn = document.getElementById('loginBtn');
|
| 950 |
+
const userBadge = document.getElementById('userBadge');
|
| 951 |
+
const userAvatar = document.getElementById('userAvatar');
|
| 952 |
+
const userName = document.getElementById('userName');
|
| 953 |
+
const leaderboardBtn = document.getElementById('leaderboardBtn');
|
| 954 |
+
const leaderboardModal = document.getElementById('leaderboardModal');
|
| 955 |
+
const modalClose = document.getElementById('modalClose');
|
| 956 |
+
const leaderboardList = document.getElementById('leaderboardList');
|
| 957 |
+
const loginPrompt = document.getElementById('loginPrompt');
|
| 958 |
+
const modalLoginBtn = document.getElementById('modalLoginBtn');
|
| 959 |
+
|
| 960 |
+
// ========================================
|
| 961 |
+
// HUGGINGFACE OAUTH
|
| 962 |
+
// ========================================
|
| 963 |
+
async function initAuth() {
|
| 964 |
+
try {
|
| 965 |
+
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 966 |
+
|
| 967 |
+
if (oauthResult) {
|
| 968 |
+
currentUser = {
|
| 969 |
+
id: oauthResult.userInfo.sub,
|
| 970 |
+
username: oauthResult.userInfo.preferred_username || oauthResult.userInfo.name,
|
| 971 |
+
avatar: oauthResult.userInfo.picture
|
| 972 |
+
};
|
| 973 |
+
showLoggedInUI();
|
| 974 |
+
|
| 975 |
+
// Sync local high score to leaderboard
|
| 976 |
+
const localHighScore = parseInt(localStorage.getItem('paperGameHighScore')) || 0;
|
| 977 |
+
if (localHighScore > 0) {
|
| 978 |
+
await saveScoreToLeaderboard(localHighScore);
|
| 979 |
+
}
|
| 980 |
+
} else {
|
| 981 |
+
showLoggedOutUI();
|
| 982 |
+
}
|
| 983 |
+
} catch (error) {
|
| 984 |
+
console.log('OAuth not available or error:', error);
|
| 985 |
+
showLoggedOutUI();
|
| 986 |
+
}
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
function showLoggedInUI() {
|
| 990 |
+
loginBtn.style.display = 'none';
|
| 991 |
+
userBadge.style.display = 'flex';
|
| 992 |
+
userAvatar.src = currentUser.avatar || '';
|
| 993 |
+
userAvatar.onerror = function() { this.style.display = 'none'; };
|
| 994 |
+
userName.textContent = currentUser.username;
|
| 995 |
+
loginPrompt.style.display = 'none';
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
function showLoggedOutUI() {
|
| 999 |
+
loginBtn.style.display = 'flex';
|
| 1000 |
+
userBadge.style.display = 'none';
|
| 1001 |
+
loginPrompt.style.display = 'block';
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
async function handleLogin() {
|
| 1005 |
+
try {
|
| 1006 |
+
const loginUrl = await oauthLoginUrl();
|
| 1007 |
+
window.location.href = loginUrl;
|
| 1008 |
+
} catch (error) {
|
| 1009 |
+
console.error('Login failed:', error);
|
| 1010 |
+
alert('Login is only available when running on Hugging Face Spaces');
|
| 1011 |
+
}
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
// ========================================
|
| 1015 |
+
// LEADERBOARD FUNCTIONS
|
| 1016 |
+
// ========================================
|
| 1017 |
+
async function loadLeaderboard() {
|
| 1018 |
+
if (!isSupabaseConfigured) {
|
| 1019 |
+
leaderboardList.innerHTML = `
|
| 1020 |
+
<div class="leaderboard-empty">
|
| 1021 |
+
<p style="font-size: 2rem; margin-bottom: 16px;">🔧</p>
|
| 1022 |
+
<p>Leaderboard not configured yet.</p>
|
| 1023 |
+
<p style="font-size: 0.8rem; margin-top: 8px; color: #64748b;">
|
| 1024 |
+
Set up Supabase to enable global leaderboards!
|
| 1025 |
+
</p>
|
| 1026 |
+
</div>
|
| 1027 |
+
`;
|
| 1028 |
+
return;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
leaderboardList.innerHTML = '<div class="leaderboard-loading">Loading leaderboard...</div>';
|
| 1032 |
+
|
| 1033 |
+
try {
|
| 1034 |
+
const { data: scores, error } = await supabase
|
| 1035 |
+
.from('paper_game_leaderboard')
|
| 1036 |
+
.select('user_id, username, avatar_url, score')
|
| 1037 |
+
.order('score', { ascending: false })
|
| 1038 |
+
.limit(20);
|
| 1039 |
+
|
| 1040 |
+
if (error) throw error;
|
| 1041 |
+
|
| 1042 |
+
if (!scores || scores.length === 0) {
|
| 1043 |
+
leaderboardList.innerHTML = `
|
| 1044 |
+
<div class="leaderboard-empty">
|
| 1045 |
+
<p style="font-size: 2rem; margin-bottom: 16px;">🎮</p>
|
| 1046 |
+
<p>No scores yet!</p>
|
| 1047 |
+
<p style="font-size: 0.8rem; margin-top: 8px;">Be the first to set a record!</p>
|
| 1048 |
+
</div>
|
| 1049 |
+
`;
|
| 1050 |
+
return;
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
leaderboardList.innerHTML = scores.map((entry, index) => `
|
| 1054 |
+
<div class="leaderboard-entry ${currentUser && entry.user_id === currentUser.id ? 'current-user' : ''}">
|
| 1055 |
+
<span class="leaderboard-rank">${index + 1}</span>
|
| 1056 |
+
<img class="leaderboard-avatar" src="${entry.avatar_url || ''}" alt="" onerror="this.style.display='none'">
|
| 1057 |
+
<span class="leaderboard-name">${escapeHtml(entry.username)}</span>
|
| 1058 |
+
<span class="leaderboard-score">${entry.score} 🔥</span>
|
| 1059 |
+
</div>
|
| 1060 |
+
`).join('');
|
| 1061 |
+
} catch (error) {
|
| 1062 |
+
console.error('Failed to load leaderboard:', error);
|
| 1063 |
+
leaderboardList.innerHTML = `
|
| 1064 |
+
<div class="leaderboard-empty">
|
| 1065 |
+
<p style="font-size: 2rem; margin-bottom: 16px;">😢</p>
|
| 1066 |
+
<p>Failed to load leaderboard</p>
|
| 1067 |
+
</div>
|
| 1068 |
+
`;
|
| 1069 |
+
}
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
async function saveScoreToLeaderboard(score) {
|
| 1073 |
+
if (!isSupabaseConfigured || !currentUser || score <= 0) return;
|
| 1074 |
+
|
| 1075 |
+
try {
|
| 1076 |
+
// Check if user already has a score
|
| 1077 |
+
const { data: existing } = await supabase
|
| 1078 |
+
.from('paper_game_leaderboard')
|
| 1079 |
+
.select('score')
|
| 1080 |
+
.eq('user_id', currentUser.id)
|
| 1081 |
+
.single();
|
| 1082 |
+
|
| 1083 |
+
// Only update if new score is higher
|
| 1084 |
+
if (!existing || score > existing.score) {
|
| 1085 |
+
await supabase
|
| 1086 |
+
.from('paper_game_leaderboard')
|
| 1087 |
+
.upsert({
|
| 1088 |
+
user_id: currentUser.id,
|
| 1089 |
+
username: currentUser.username,
|
| 1090 |
+
avatar_url: currentUser.avatar,
|
| 1091 |
+
score: score,
|
| 1092 |
+
updated_at: new Date().toISOString()
|
| 1093 |
+
}, { onConflict: 'user_id' });
|
| 1094 |
+
}
|
| 1095 |
+
} catch (error) {
|
| 1096 |
+
console.error('Failed to save score:', error);
|
| 1097 |
+
}
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
function escapeHtml(text) {
|
| 1101 |
+
const div = document.createElement('div');
|
| 1102 |
+
div.textContent = text;
|
| 1103 |
+
return div.innerHTML;
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
+
// ========================================
|
| 1107 |
+
// MODAL HANDLERS
|
| 1108 |
+
// ========================================
|
| 1109 |
+
function openLeaderboard() {
|
| 1110 |
+
leaderboardModal.classList.add('visible');
|
| 1111 |
+
loadLeaderboard();
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
function closeLeaderboard() {
|
| 1115 |
+
leaderboardModal.classList.remove('visible');
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
// Event Listeners
|
| 1119 |
+
leaderboardBtn.addEventListener('click', openLeaderboard);
|
| 1120 |
+
modalClose.addEventListener('click', closeLeaderboard);
|
| 1121 |
+
leaderboardModal.addEventListener('click', (e) => {
|
| 1122 |
+
if (e.target === leaderboardModal) closeLeaderboard();
|
| 1123 |
+
});
|
| 1124 |
+
loginBtn.addEventListener('click', handleLogin);
|
| 1125 |
+
modalLoginBtn.addEventListener('click', handleLogin);
|
| 1126 |
+
|
| 1127 |
+
// Escape key to close modal
|
| 1128 |
+
document.addEventListener('keydown', (e) => {
|
| 1129 |
+
if (e.key === 'Escape') closeLeaderboard();
|
| 1130 |
+
});
|
| 1131 |
+
|
| 1132 |
+
// Expose saveScoreToLeaderboard for game code
|
| 1133 |
+
window.saveScoreToLeaderboard = saveScoreToLeaderboard;
|
| 1134 |
+
window.currentUser = () => currentUser;
|
| 1135 |
+
|
| 1136 |
+
// Initialize auth
|
| 1137 |
+
initAuth();
|
| 1138 |
+
</script>
|
| 1139 |
+
|
| 1140 |
<script>
|
| 1141 |
// Game State
|
| 1142 |
let papers = [];
|
|
|
|
| 1377 |
highScore = streak;
|
| 1378 |
localStorage.setItem('paperGameHighScore', highScore);
|
| 1379 |
highScoreEl.textContent = highScore;
|
| 1380 |
+
|
| 1381 |
+
// Save to global leaderboard if logged in
|
| 1382 |
+
if (window.saveScoreToLeaderboard) {
|
| 1383 |
+
window.saveScoreToLeaderboard(highScore);
|
| 1384 |
+
}
|
| 1385 |
}
|
| 1386 |
} else {
|
| 1387 |
streak = 0;
|