efecelik commited on
Commit
b5ae4f4
·
1 Parent(s): 3f89cc4

Add leaderboard with HuggingFace OAuth and Supabase integration

Browse files
Files changed (2) hide show
  1. README.md +2 -0
  2. 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;