Spaces:
Running
Running
| <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> | |