paper-popularity-game / index.html
efecelik's picture
Configure Supabase leaderboard credentials
29876a7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Paper Popularity Game</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #0d0d1a 0%, #1a1a2e 30%, #16213e 60%, #0f3460 100%);
height: 100vh;
height: 100dvh;
color: #fff;
overflow: hidden;
}
/* Animated background particles effect */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 140, 0, 0.03) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(96, 165, 250, 0.02) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
padding: 1.5vh 3vw;
position: relative;
z-index: 1;
}
/* Header - More compact */
header {
text-align: center;
padding: 0.5vh 0 1vh;
flex-shrink: 0;
}
h1 {
font-size: clamp(1.5rem, 3.5vw, 2.5rem);
font-weight: 800;
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 50%, #ff8c00 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 60px rgba(255, 215, 0, 0.3);
letter-spacing: -0.02em;
}
.subtitle {
color: #94a3b8;
font-size: clamp(0.8rem, 1.5vw, 1rem);
margin-top: 0.5vh;
font-weight: 500;
}
/* Stats Bar - Horizontal pill design */
.stats-bar {
display: flex;
justify-content: center;
gap: clamp(20px, 5vw, 60px);
padding: 1.5vh 0;
flex-shrink: 0;
}
.stat {
text-align: center;
background: rgba(255, 255, 255, 0.03);
padding: clamp(8px, 1vh, 12px) clamp(20px, 3vw, 40px);
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
}
.stat-value {
font-size: clamp(1.2rem, 2.8vw, 1.8rem);
font-weight: 700;
color: #ffd700;
}
.stat-label {
font-size: clamp(0.65rem, 1vw, 0.8rem);
color: #64748b;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-top: 2px;
}
.streak-fire {
animation: pulse 0.5s ease-in-out infinite alternate;
}
@keyframes pulse {
from { transform: scale(1); }
to { transform: scale(1.1); }
}
/* Result Message */
.result-message {
text-align: center;
font-size: clamp(1.1rem, 2.5vw, 1.6rem);
font-weight: 700;
min-height: 3vh;
flex-shrink: 0;
text-shadow: 0 2px 20px currentColor;
transition: all 0.3s ease;
}
.result-message.correct { color: #22c55e; }
.result-message.wrong { color: #ef4444; }
/* Game Area - Full width, prominent cards */
.game-area {
display: flex;
gap: clamp(15px, 3vw, 50px);
justify-content: center;
align-items: stretch;
flex: 1;
min-height: 0;
padding: 1vh 2vw;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 800;
color: #ffd700;
text-shadow:
0 0 30px rgba(255, 215, 0, 0.6),
0 0 60px rgba(255, 215, 0, 0.3);
flex-shrink: 0;
padding: 0 2vw;
position: relative;
}
.vs-divider::before,
.vs-divider::after {
content: '';
position: absolute;
width: 3px;
height: 60%;
background: linear-gradient(180deg, transparent, rgba(255, 215, 0, 0.3), transparent);
}
.vs-divider::before { left: 0; }
.vs-divider::after { right: 0; }
/* Paper Card - Larger, more prominent */
.paper-card {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%);
border-radius: clamp(16px, 2vw, 28px);
padding: clamp(16px, 2.5vw, 32px);
flex: 1;
max-width: 700px;
min-width: 280px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
backdrop-filter: blur(20px);
box-shadow:
0 4px 30px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.paper-card:hover {
transform: translateY(-8px) scale(1.02);
border-color: rgba(255, 215, 0, 0.5);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.4),
0 0 40px rgba(255, 215, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.paper-card.correct {
border-color: #22c55e;
background: linear-gradient(145deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.05) 100%);
box-shadow:
0 0 50px rgba(34, 197, 94, 0.3),
inset 0 0 30px rgba(34, 197, 94, 0.1);
}
.paper-card.wrong {
border-color: #ef4444;
background: linear-gradient(145deg, rgba(239, 68, 68, 0.2) 0%, rgba(239, 68, 68, 0.05) 100%);
box-shadow:
0 0 50px rgba(239, 68, 68, 0.3),
inset 0 0 30px rgba(239, 68, 68, 0.1);
}
.paper-card.disabled {
pointer-events: none;
}
/* Thumbnail - Smaller to give more room for content */
.paper-thumbnail {
width: 100%;
height: clamp(80px, 12vh, 140px);
object-fit: cover;
border-radius: clamp(8px, 1vw, 12px);
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
flex-shrink: 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.paper-title {
font-size: clamp(1rem, 1.6vw, 1.25rem);
font-weight: 700;
line-height: 1.4;
margin: clamp(10px, 1.5vh, 16px) 0 clamp(6px, 1vh, 10px);
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
flex-shrink: 0;
color: #f8fafc;
}
.paper-authors {
font-size: clamp(0.7rem, 1vw, 0.85rem);
color: #94a3b8;
margin-bottom: clamp(6px, 1vh, 10px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.paper-summary {
font-size: clamp(0.8rem, 1.1vw, 0.95rem);
color: #cbd5e1;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 8;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
min-height: 0;
}
.paper-meta {
display: flex;
align-items: center;
gap: clamp(12px, 1.5vw, 20px);
padding-top: clamp(12px, 1.5vh, 18px);
margin-top: auto;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.upvote-display {
font-weight: 700;
color: #ffd700;
font-size: clamp(1rem, 1.5vw, 1.3rem);
opacity: 0;
transition: all 0.4s ease;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
.upvote-display.revealed {
opacity: 1;
animation: revealBounce 0.5s ease;
}
@keyframes revealBounce {
0% { transform: scale(0.5); opacity: 0; }
50% { transform: scale(1.2); }
100% { transform: scale(1); opacity: 1; }
}
.vote-hidden {
font-size: clamp(1.1rem, 1.5vw, 1.4rem);
color: #64748b;
}
.click-hint {
font-size: clamp(0.7rem, 1vw, 0.85rem);
color: #64748b;
text-align: center;
margin-top: clamp(8px, 1vh, 12px);
flex-shrink: 0;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: opacity 0.3s ease;
}
/* Paper Link */
.paper-link {
color: #60a5fa;
text-decoration: none;
font-size: clamp(0.75rem, 1.1vw, 0.9rem);
pointer-events: auto;
position: relative;
z-index: 10;
margin-left: auto;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
background: rgba(96, 165, 250, 0.1);
transition: all 0.3s ease;
}
.paper-link:hover {
color: #fff;
background: rgba(96, 165, 250, 0.3);
}
/* Next Round Button */
.next-btn {
display: none;
margin: 2vh auto;
padding: clamp(12px, 1.8vh, 18px) clamp(30px, 5vw, 50px);
font-size: clamp(1rem, 1.4vw, 1.2rem);
font-weight: 700;
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
color: #1a1a2e;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 20px rgba(255, 215, 0, 0.3);
}
.next-btn:hover {
transform: scale(1.08);
box-shadow: 0 8px 40px rgba(255, 215, 0, 0.5);
}
.next-btn.visible {
display: block;
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Footer */
footer {
text-align: center;
padding: 1vh 0;
color: #64748b;
font-size: clamp(0.7rem, 1vw, 0.85rem);
flex-shrink: 0;
}
footer a {
color: #ffd700;
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
}
footer a:hover {
color: #ffaa00;
}
/* High Score - More prominent */
.high-score {
position: fixed;
top: 1.5vh;
right: 3vw;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
padding: clamp(8px, 1vh, 14px) clamp(16px, 2vw, 28px);
border-radius: 50px;
font-size: clamp(0.8rem, 1.1vw, 1rem);
z-index: 100;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
font-weight: 600;
}
.high-score span {
color: #ffd700;
font-weight: 700;
}
/* Top-left buttons container */
.top-left-buttons {
position: fixed;
top: 1.5vh;
left: 3vw;
display: flex;
gap: 10px;
z-index: 100;
}
/* Leaderboard Button */
.leaderboard-btn {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 140, 0, 0.1) 100%);
padding: clamp(8px, 1vh, 14px) clamp(16px, 2vw, 28px);
border-radius: 50px;
font-size: clamp(0.8rem, 1.1vw, 1rem);
border: 1px solid rgba(255, 215, 0, 0.3);
backdrop-filter: blur(10px);
font-weight: 600;
color: #ffd700;
cursor: pointer;
transition: all 0.3s ease;
}
.leaderboard-btn:hover {
transform: scale(1.05);
border-color: rgba(255, 215, 0, 0.6);
box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
}
/* Login Button */
.login-btn {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
padding: clamp(8px, 1vh, 14px) clamp(16px, 2vw, 28px);
border-radius: 50px;
font-size: clamp(0.8rem, 1.1vw, 1rem);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
font-weight: 600;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.login-btn:hover {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.3);
}
/* User Badge (when logged in) */
.user-badge {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.1) 100%);
padding: clamp(6px, 0.8vh, 10px) clamp(12px, 1.5vw, 20px);
border-radius: 50px;
font-size: clamp(0.75rem, 1vw, 0.9rem);
border: 1px solid rgba(34, 197, 94, 0.3);
backdrop-filter: blur(10px);
font-weight: 600;
color: #22c55e;
display: flex;
align-items: center;
gap: 8px;
}
.user-badge img {
width: 24px;
height: 24px;
border-radius: 50%;
}
/* Leaderboard Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 200;
display: none;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-overlay.visible {
display: flex;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
background: linear-gradient(145deg, rgba(26, 26, 46, 0.98) 0%, rgba(15, 52, 96, 0.98) 100%);
border-radius: 24px;
padding: clamp(24px, 4vw, 40px);
max-width: 500px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
border: 1px solid rgba(255, 215, 0, 0.2);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 40px rgba(255, 215, 0, 0.1);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateY(-30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-title {
font-size: clamp(1.3rem, 2.5vw, 1.8rem);
font-weight: 700;
color: #ffd700;
}
.modal-close {
background: rgba(255, 255, 255, 0.1);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.modal-close:hover {
background: rgba(239, 68, 68, 0.3);
}
/* Leaderboard Table */
.leaderboard-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.leaderboard-entry {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
}
.leaderboard-entry:hover {
background: rgba(255, 255, 255, 0.08);
}
.leaderboard-entry.current-user {
background: rgba(255, 215, 0, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
}
.leaderboard-rank {
font-weight: 700;
font-size: 1.1rem;
width: 32px;
text-align: center;
color: #64748b;
}
.leaderboard-entry:nth-child(1) .leaderboard-rank { color: #ffd700; }
.leaderboard-entry:nth-child(2) .leaderboard-rank { color: #c0c0c0; }
.leaderboard-entry:nth-child(3) .leaderboard-rank { color: #cd7f32; }
.leaderboard-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.leaderboard-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaderboard-score {
font-weight: 700;
color: #22c55e;
font-size: 1.1rem;
}
.leaderboard-empty {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.leaderboard-loading {
text-align: center;
padding: 40px 20px;
color: #94a3b8;
}
.login-prompt {
text-align: center;
padding: 20px;
background: rgba(255, 215, 0, 0.1);
border-radius: 12px;
margin-top: 16px;
}
.login-prompt p {
color: #94a3b8;
margin-bottom: 12px;
font-size: 0.9rem;
}
.login-prompt button {
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
color: #1a1a2e;
border: none;
padding: 10px 24px;
border-radius: 50px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.login-prompt button:hover {
transform: scale(1.05);
}
/* Loading State */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 20px;
}
.spinner {
width: clamp(40px, 6vw, 60px);
height: clamp(40px, 6vw, 60px);
border: 4px solid rgba(255, 215, 0, 0.2);
border-top-color: #ffd700;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading p {
font-size: clamp(1rem, 1.4vw, 1.2rem);
color: #94a3b8;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Error state */
.error-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
color: #ef4444;
}
.error-message button {
margin-top: 20px;
padding: 12px 35px;
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
color: #1a1a2e;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: 700;
font-size: 1rem;
transition: all 0.3s ease;
}
.error-message button:hover {
transform: scale(1.05);
box-shadow: 0 5px 20px rgba(255, 215, 0, 0.3);
}
/* Tablet: Slightly smaller cards */
@media (max-width: 1200px) {
.paper-card {
max-width: 550px;
}
.paper-thumbnail {
height: clamp(100px, 20vh, 200px);
}
}
/* Mobile ONLY - width based, NOT height */
@media (max-width: 700px) {
.container {
padding: 1vh 4vw;
}
.game-area {
flex-direction: column;
align-items: center;
padding: 0.5vh 0;
}
.vs-divider {
padding: 1vh 0;
font-size: clamp(1.5rem, 3vw, 2rem);
}
.vs-divider::before,
.vs-divider::after {
display: none;
}
.paper-card {
max-width: 100%;
width: 100%;
flex: none;
height: auto;
max-height: 38vh;
padding: clamp(12px, 2vw, 20px);
}
.paper-thumbnail {
height: clamp(50px, 8vh, 80px);
}
.paper-title {
font-size: clamp(0.9rem, 4vw, 1.1rem);
margin: 8px 0 6px;
-webkit-line-clamp: 3;
}
.paper-summary {
-webkit-line-clamp: 5;
font-size: clamp(0.75rem, 3.5vw, 0.9rem);
}
.stats-bar {
gap: clamp(10px, 3vw, 25px);
}
.stat {
padding: 6px 16px;
}
}
/* Short screens - keep side by side, prioritize content */
@media (max-height: 600px) and (min-width: 700px) {
.paper-thumbnail {
height: clamp(50px, 8vh, 80px);
}
.paper-summary {
-webkit-line-clamp: 5;
}
.paper-title {
-webkit-line-clamp: 3;
}
header {
padding: 0.3vh 0 0.5vh;
}
.stats-bar {
padding: 0.5vh 0;
}
}
/* Very short screens - hide thumbnail, show more text */
@media (max-height: 500px) and (min-width: 700px) {
.paper-thumbnail {
display: none;
}
.subtitle {
display: none;
}
.paper-summary {
-webkit-line-clamp: 6;
}
.paper-title {
-webkit-line-clamp: 3;
}
}
/* Actual mobile phones only */
@media (max-height: 600px) and (max-width: 700px) {
.paper-thumbnail {
display: none;
}
.paper-card {
max-height: 42vh;
}
.paper-title {
-webkit-line-clamp: 3;
}
.paper-summary {
-webkit-line-clamp: 5;
}
header {
padding: 0.3vh 0 0.5vh;
}
.stats-bar {
padding: 0.5vh 0;
}
}
</style>
</head>
<body>
<!-- Top Left Buttons: Leaderboard & Login -->
<div class="top-left-buttons">
<button class="leaderboard-btn" id="leaderboardBtn">🏆 Leaderboard</button>
<button class="login-btn" id="loginBtn">🤗 Sign in</button>
<div class="user-badge" id="userBadge" style="display: none;">
<img id="userAvatar" src="" alt="avatar">
<span id="userName"></span>
</div>
</div>
<!-- High Score -->
<div class="high-score">
🏆 Best Streak: <span id="highScore">0</span>
</div>
<!-- Leaderboard Modal -->
<div class="modal-overlay" id="leaderboardModal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">🏆 Global Leaderboard</h2>
<button class="modal-close" id="modalClose"></button>
</div>
<div class="leaderboard-list" id="leaderboardList">
<div class="leaderboard-loading">Loading leaderboard...</div>
</div>
<div class="login-prompt" id="loginPrompt" style="display: none;">
<p>Sign in with your Hugging Face account to compete!</p>
<button id="modalLoginBtn">🤗 Sign in with Hugging Face</button>
</div>
</div>
</div>
<div class="container">
<header>
<h1>🎯 Paper Popularity Game</h1>
<p class="subtitle">Which Hugging Face paper got more upvotes?</p>
</header>
<div class="stats-bar">
<div class="stat">
<div class="stat-value" id="streak">0 🔥</div>
<div class="stat-label">Streak</div>
</div>
<div class="stat">
<div class="stat-value" id="round">1</div>
<div class="stat-label">Round</div>
</div>
</div>
<div class="result-message" id="resultMessage"></div>
<div class="game-area" id="gameArea">
<div class="loading">
<div class="spinner"></div>
<p>Loading papers from Hugging Face...</p>
</div>
</div>
<button class="next-btn" id="nextBtn">Next Round →</button>
<footer>
Data from <a href="https://huggingface.co/papers" target="_blank">Hugging Face Daily Papers</a>
</footer>
</div>
<!-- Hugging Face Hub for OAuth -->
<script type="module">
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from 'https://esm.sh/@huggingface/hub';
// ========================================
// SUPABASE CONFIGURATION
// ========================================
// To enable the leaderboard, create a free Supabase project at https://supabase.com
// Then replace these values with your project's URL and anon key
const SUPABASE_URL = 'https://hovfstwfulrtfmsudxum.supabase.co';
const SUPABASE_ANON_KEY = 'sb_publishable_2AYgdEK7so_jsq75lr9mfw_f820xUxz';
// Check if Supabase is configured
const isSupabaseConfigured = SUPABASE_URL !== 'YOUR_SUPABASE_URL' && SUPABASE_ANON_KEY !== 'YOUR_SUPABASE_ANON_KEY';
let supabase = null;
if (isSupabaseConfigured) {
const { createClient } = await import('https://esm.sh/@supabase/supabase-js@2');
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
}
// ========================================
// USER STATE
// ========================================
let currentUser = null;
// ========================================
// DOM ELEMENTS
// ========================================
const loginBtn = document.getElementById('loginBtn');
const userBadge = document.getElementById('userBadge');
const userAvatar = document.getElementById('userAvatar');
const userName = document.getElementById('userName');
const leaderboardBtn = document.getElementById('leaderboardBtn');
const leaderboardModal = document.getElementById('leaderboardModal');
const modalClose = document.getElementById('modalClose');
const leaderboardList = document.getElementById('leaderboardList');
const loginPrompt = document.getElementById('loginPrompt');
const modalLoginBtn = document.getElementById('modalLoginBtn');
// ========================================
// HUGGINGFACE OAUTH
// ========================================
async function initAuth() {
try {
const oauthResult = await oauthHandleRedirectIfPresent();
if (oauthResult) {
currentUser = {
id: oauthResult.userInfo.sub,
username: oauthResult.userInfo.preferred_username || oauthResult.userInfo.name,
avatar: oauthResult.userInfo.picture
};
showLoggedInUI();
// Sync local high score to leaderboard
const localHighScore = parseInt(localStorage.getItem('paperGameHighScore')) || 0;
if (localHighScore > 0) {
await saveScoreToLeaderboard(localHighScore);
}
} else {
showLoggedOutUI();
}
} catch (error) {
console.log('OAuth not available or error:', error);
showLoggedOutUI();
}
}
function showLoggedInUI() {
loginBtn.style.display = 'none';
userBadge.style.display = 'flex';
userAvatar.src = currentUser.avatar || '';
userAvatar.onerror = function() { this.style.display = 'none'; };
userName.textContent = currentUser.username;
loginPrompt.style.display = 'none';
}
function showLoggedOutUI() {
loginBtn.style.display = 'flex';
userBadge.style.display = 'none';
loginPrompt.style.display = 'block';
}
async function handleLogin() {
try {
const loginUrl = await oauthLoginUrl();
window.location.href = loginUrl;
} catch (error) {
console.error('Login failed:', error);
alert('Login is only available when running on Hugging Face Spaces');
}
}
// ========================================
// LEADERBOARD FUNCTIONS
// ========================================
async function loadLeaderboard() {
if (!isSupabaseConfigured) {
leaderboardList.innerHTML = `
<div class="leaderboard-empty">
<p style="font-size: 2rem; margin-bottom: 16px;">🔧</p>
<p>Leaderboard not configured yet.</p>
<p style="font-size: 0.8rem; margin-top: 8px; color: #64748b;">
Set up Supabase to enable global leaderboards!
</p>
</div>
`;
return;
}
leaderboardList.innerHTML = '<div class="leaderboard-loading">Loading leaderboard...</div>';
try {
const { data: scores, error } = await supabase
.from('paper_game_leaderboard')
.select('user_id, username, avatar_url, score')
.order('score', { ascending: false })
.limit(20);
if (error) throw error;
if (!scores || scores.length === 0) {
leaderboardList.innerHTML = `
<div class="leaderboard-empty">
<p style="font-size: 2rem; margin-bottom: 16px;">🎮</p>
<p>No scores yet!</p>
<p style="font-size: 0.8rem; margin-top: 8px;">Be the first to set a record!</p>
</div>
`;
return;
}
leaderboardList.innerHTML = scores.map((entry, index) => `
<div class="leaderboard-entry ${currentUser && entry.user_id === currentUser.id ? 'current-user' : ''}">
<span class="leaderboard-rank">${index + 1}</span>
<img class="leaderboard-avatar" src="${entry.avatar_url || ''}" alt="" onerror="this.style.display='none'">
<span class="leaderboard-name">${escapeHtml(entry.username)}</span>
<span class="leaderboard-score">${entry.score} 🔥</span>
</div>
`).join('');
} catch (error) {
console.error('Failed to load leaderboard:', error);
leaderboardList.innerHTML = `
<div class="leaderboard-empty">
<p style="font-size: 2rem; margin-bottom: 16px;">😢</p>
<p>Failed to load leaderboard</p>
</div>
`;
}
}
async function saveScoreToLeaderboard(score) {
if (!isSupabaseConfigured || !currentUser || score <= 0) return;
try {
// Check if user already has a score
const { data: existing } = await supabase
.from('paper_game_leaderboard')
.select('score')
.eq('user_id', currentUser.id)
.single();
// Only update if new score is higher
if (!existing || score > existing.score) {
await supabase
.from('paper_game_leaderboard')
.upsert({
user_id: currentUser.id,
username: currentUser.username,
avatar_url: currentUser.avatar,
score: score,
updated_at: new Date().toISOString()
}, { onConflict: 'user_id' });
}
} catch (error) {
console.error('Failed to save score:', error);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========================================
// MODAL HANDLERS
// ========================================
function openLeaderboard() {
leaderboardModal.classList.add('visible');
loadLeaderboard();
}
function closeLeaderboard() {
leaderboardModal.classList.remove('visible');
}
// Event Listeners
leaderboardBtn.addEventListener('click', openLeaderboard);
modalClose.addEventListener('click', closeLeaderboard);
leaderboardModal.addEventListener('click', (e) => {
if (e.target === leaderboardModal) closeLeaderboard();
});
loginBtn.addEventListener('click', handleLogin);
modalLoginBtn.addEventListener('click', handleLogin);
// Escape key to close modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLeaderboard();
});
// Expose saveScoreToLeaderboard for game code
window.saveScoreToLeaderboard = saveScoreToLeaderboard;
window.currentUser = () => currentUser;
// Initialize auth
initAuth();
</script>
<script>
// Game State
let papers = [];
let currentPair = [];
let score = 0;
let streak = 0;
let round = 1;
let highScore = parseInt(localStorage.getItem('paperGameHighScore')) || 0;
let hasAnswered = false;
// DOM Elements
const gameArea = document.getElementById('gameArea');
const streakEl = document.getElementById('streak');
const roundEl = document.getElementById('round');
const highScoreEl = document.getElementById('highScore');
const resultMessage = document.getElementById('resultMessage');
const nextBtn = document.getElementById('nextBtn');
// Initialize
highScoreEl.textContent = highScore;
// Fetch papers from Hugging Face API
async function fetchPapers() {
try {
const dates = [];
const today = new Date();
for (let i = 0; i < 30; i++) {
const d = new Date(today);
d.setDate(d.getDate() - i);
dates.push(d.toISOString().split('T')[0]);
}
const selectedDates = dates.sort(() => Math.random() - 0.5).slice(0, 5);
const requests = selectedDates.map(date =>
fetch(`https://huggingface.co/api/daily_papers?date=${date}`)
.then(r => r.ok ? r.json() : [])
.catch(() => [])
);
const results = await Promise.all(requests);
const allPapers = results.flat();
const seenIds = new Set();
papers = allPapers
.filter(item => {
if (!item.paper || item.paper.upvotes === undefined) return false;
if (seenIds.has(item.paper.id)) return false;
seenIds.add(item.paper.id);
return true;
})
.map(item => ({
id: item.paper.id,
title: item.paper.title || 'Untitled Paper',
authors: item.paper.authors?.map(a => a.name).slice(0, 3).join(', ') || 'Unknown',
summary: item.paper.ai_summary || item.paper.summary || 'No summary available',
upvotes: parseInt(item.paper.upvotes, 10) || 0,
thumbnail: item.paper.thumbnail,
url: `https://huggingface.co/papers/${encodeURIComponent(item.paper.id)}`
}))
.filter(p => p.upvotes >= 1);
if (papers.length < 2) {
throw new Error('Not enough papers loaded');
}
startNewRound();
} catch (error) {
console.error('Error fetching papers:', error);
showError(error.message);
}
}
function showError(message) {
while (gameArea.firstChild) {
gameArea.removeChild(gameArea.firstChild);
}
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
const emoji = document.createElement('p');
emoji.textContent = '😢 Failed to load papers';
errorDiv.appendChild(emoji);
const errorText = document.createElement('p');
errorText.style.cssText = 'font-size: 0.9rem; color: #94a3b8; margin-top: 10px;';
errorText.textContent = message;
errorDiv.appendChild(errorText);
const retryBtn = document.createElement('button');
retryBtn.textContent = 'Try Again';
retryBtn.addEventListener('click', () => location.reload());
errorDiv.appendChild(retryBtn);
gameArea.appendChild(errorDiv);
}
function getRandomPair() {
const shuffled = [...papers].sort(() => Math.random() - 0.5);
return [shuffled[0], shuffled[1]];
}
function createPaperCard(paper, index) {
const card = document.createElement('div');
card.className = 'paper-card';
card.id = `card${index}`;
card.addEventListener('click', () => selectPaper(index));
const thumbnail = document.createElement('img');
thumbnail.className = 'paper-thumbnail';
thumbnail.alt = paper.title;
thumbnail.src = paper.thumbnail || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200"><rect fill="%232d3748" width="400" height="200"/><text x="200" y="100" text-anchor="middle" fill="%2364748b" font-size="20">📄</text></svg>';
thumbnail.onerror = function() {
this.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200"><rect fill="%232d3748" width="400" height="200"/><text x="200" y="100" text-anchor="middle" fill="%2364748b" font-size="20">📄</text></svg>';
};
card.appendChild(thumbnail);
const title = document.createElement('h3');
title.className = 'paper-title';
title.textContent = paper.title;
card.appendChild(title);
const authors = document.createElement('p');
authors.className = 'paper-authors';
authors.textContent = '👤 ' + paper.authors;
card.appendChild(authors);
const summary = document.createElement('p');
summary.className = 'paper-summary';
summary.textContent = paper.summary;
card.appendChild(summary);
const meta = document.createElement('div');
meta.className = 'paper-meta';
const upvoteDisplay = document.createElement('div');
upvoteDisplay.className = 'upvote-display';
upvoteDisplay.id = `upvotes${index}`;
upvoteDisplay.textContent = `👍 ${paper.upvotes}`;
meta.appendChild(upvoteDisplay);
const voteHidden = document.createElement('span');
voteHidden.className = 'vote-hidden';
voteHidden.id = `hidden${index}`;
voteHidden.textContent = '❓';
meta.appendChild(voteHidden);
const link = document.createElement('a');
link.className = 'paper-link';
link.href = paper.url;
link.target = '_blank';
link.textContent = 'View →';
link.addEventListener('click', (e) => e.stopPropagation());
meta.appendChild(link);
card.appendChild(meta);
const hint = document.createElement('p');
hint.className = 'click-hint';
hint.id = `hint${index}`;
hint.textContent = 'Click if MORE upvotes';
card.appendChild(hint);
return card;
}
function startNewRound() {
hasAnswered = false;
currentPair = getRandomPair();
resultMessage.textContent = '';
resultMessage.className = 'result-message';
nextBtn.classList.remove('visible');
while (gameArea.firstChild) {
gameArea.removeChild(gameArea.firstChild);
}
gameArea.appendChild(createPaperCard(currentPair[0], 0));
const vsDiv = document.createElement('div');
vsDiv.className = 'vs-divider';
vsDiv.textContent = 'VS';
gameArea.appendChild(vsDiv);
gameArea.appendChild(createPaperCard(currentPair[1], 1));
}
function selectPaper(selectedIndex) {
if (hasAnswered) return;
hasAnswered = true;
const card0 = document.getElementById('card0');
const card1 = document.getElementById('card1');
const upvotes0 = document.getElementById('upvotes0');
const upvotes1 = document.getElementById('upvotes1');
const hidden0 = document.getElementById('hidden0');
const hidden1 = document.getElementById('hidden1');
const hint0 = document.getElementById('hint0');
const hint1 = document.getElementById('hint1');
card0.classList.add('disabled');
card1.classList.add('disabled');
hint0.style.opacity = '0';
hint1.style.opacity = '0';
upvotes0.classList.add('revealed');
upvotes1.classList.add('revealed');
hidden0.style.display = 'none';
hidden1.style.display = 'none';
const votes0 = currentPair[0].upvotes;
const votes1 = currentPair[1].upvotes;
let winnerIndex;
if (votes0 > votes1) {
winnerIndex = 0;
} else if (votes1 > votes0) {
winnerIndex = 1;
} else {
winnerIndex = selectedIndex;
}
const isCorrect = selectedIndex === winnerIndex || votes0 === votes1;
if (isCorrect) {
streak++;
score = streak; // Score = streak
document.getElementById(`card${selectedIndex}`).classList.add('correct');
resultMessage.textContent = votes0 === votes1
? `🎉 Tie! Streak: ${streak}`
: `🎉 Correct! Streak: ${streak}`;
resultMessage.className = 'result-message correct';
// Update high score (best streak)
if (streak > highScore) {
highScore = streak;
localStorage.setItem('paperGameHighScore', highScore);
highScoreEl.textContent = highScore;
// Save to global leaderboard if logged in
if (window.saveScoreToLeaderboard) {
window.saveScoreToLeaderboard(highScore);
}
}
} else {
streak = 0;
score = 0; // Reset score when streak breaks
document.getElementById(`card${selectedIndex}`).classList.add('wrong');
document.getElementById(`card${winnerIndex}`).classList.add('correct');
resultMessage.textContent = "❌ Wrong! Streak reset!";
resultMessage.className = 'result-message wrong';
}
streakEl.textContent = streak > 0 ? `${streak} 🔥` : '0 🔥';
if (streak >= 3) {
streakEl.classList.add('streak-fire');
} else {
streakEl.classList.remove('streak-fire');
}
setTimeout(() => {
nextBtn.classList.add('visible');
}, 300);
}
nextBtn.addEventListener('click', () => {
round++;
roundEl.textContent = round;
startNewRound();
});
fetchPapers();
</script>
</body>
</html>