seawolf2357 commited on
Commit
616e960
·
verified ·
1 Parent(s): b1a8e9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +374 -42
app.py CHANGED
@@ -2,24 +2,26 @@ from flask import Flask, render_template, request, jsonify
2
  import requests
3
  import os
4
  import time
 
 
5
 
6
  app = Flask(__name__)
7
 
8
  # Function to fetch trending spaces from Huggingface with pagination
9
  def fetch_trending_spaces(offset=0, limit=72):
10
  try:
11
- # 단순하게 데이터 가져오기
12
  url = "https://huggingface.co/api/spaces"
13
- params = {"limit": 500} # 최대 500개 가져오기
14
 
15
- # 타임아웃 늘리기
16
  response = requests.get(url, params=params, timeout=30)
17
 
18
  if response.status_code == 200:
19
  spaces = response.json()
20
  filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None']
21
 
22
- # 요청된 offset limit에 맞게 슬라이싱
23
  start = min(offset, len(filtered_spaces))
24
  end = min(offset + limit, len(filtered_spaces))
25
 
@@ -29,28 +31,31 @@ def fetch_trending_spaces(offset=0, limit=72):
29
  'spaces': filtered_spaces[start:end],
30
  'total': len(filtered_spaces),
31
  'offset': offset,
32
- 'limit': limit
 
33
  }
34
  else:
35
  print(f"Error fetching spaces: {response.status_code}")
36
- # 공간 반환하지만 200 제한의 가짜 데이터
37
  return {
38
  'spaces': generate_dummy_spaces(limit),
39
  'total': 200,
40
  'offset': offset,
41
- 'limit': limit
 
42
  }
43
  except Exception as e:
44
  print(f"Exception when fetching spaces: {e}")
45
- # 가짜 데이터 생성
46
  return {
47
  'spaces': generate_dummy_spaces(limit),
48
  'total': 200,
49
  'offset': offset,
50
- 'limit': limit
 
51
  }
52
 
53
- # 오류 가짜 데이터 생성
54
  def generate_dummy_spaces(count):
55
  spaces = []
56
  for i in range(count):
@@ -65,11 +70,11 @@ def generate_dummy_spaces(count):
65
 
66
  # Transform Huggingface URL to direct space URL
67
  def transform_url(owner, name):
68
- # 1. '.' 문자를 '-'로 변경
69
  name = name.replace('.', '-')
70
- # 2. '_' 문자를 '-'로 변경
71
  name = name.replace('_', '-')
72
- # 3. 대소문자 구분 없이 모두 소문자로 변경
73
  owner = owner.lower()
74
  name = name.lower()
75
 
@@ -78,28 +83,28 @@ def transform_url(owner, name):
78
  # Get space details
79
  def get_space_details(space_data, index, offset):
80
  try:
81
- # 공통 정보 추출
82
  if '/' in space_data.get('id', ''):
83
  owner, name = space_data.get('id', '').split('/', 1)
84
  else:
85
  owner = space_data.get('owner', '')
86
  name = space_data.get('id', '')
87
 
88
- # None이 포함된 경우 무시
89
  if owner == 'None' or name == 'None':
90
  return None
91
 
92
- # URL 구성
93
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
94
  embed_url = transform_url(owner, name)
95
 
96
- # 좋아요
97
  likes_count = space_data.get('likes', 0)
98
 
99
- # 제목 추출
100
  title = space_data.get('title', name)
101
 
102
- # 태그
103
  tags = space_data.get('tags', [])
104
 
105
  return {
@@ -107,14 +112,14 @@ def get_space_details(space_data, index, offset):
107
  'embedUrl': embed_url,
108
  'title': title,
109
  'owner': owner,
110
- 'name': name, # Space 이름 추가 저장
111
  'likes_count': likes_count,
112
  'tags': tags,
113
  'rank': offset + index + 1
114
  }
115
  except Exception as e:
116
  print(f"Error processing space data: {e}")
117
- # 오류 발생 시에도 기본 객체 반환
118
  return {
119
  'url': 'https://huggingface.co/spaces',
120
  'embedUrl': 'https://huggingface.co/spaces',
@@ -126,6 +131,26 @@ def get_space_details(space_data, index, offset):
126
  'rank': offset + index + 1
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  # Homepage route
130
  @app.route('/')
131
  def home():
@@ -136,7 +161,7 @@ def home():
136
  def trending_spaces():
137
  search_query = request.args.get('search', '').lower()
138
  offset = int(request.args.get('offset', 0))
139
- limit = int(request.args.get('limit', 72)) # 기본값 72개로 변경
140
 
141
  # Fetch trending spaces
142
  spaces_data = fetch_trending_spaces(offset, limit)
@@ -164,11 +189,15 @@ def trending_spaces():
164
 
165
  results.append(space_info)
166
 
 
 
 
167
  return jsonify({
168
  'spaces': results,
169
  'total': spaces_data['total'],
170
  'offset': offset,
171
- 'limit': limit
 
172
  })
173
 
174
  if __name__ == '__main__':
@@ -184,6 +213,7 @@ if __name__ == '__main__':
184
  <meta charset="UTF-8">
185
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
186
  <title>Huggingface Spaces Gallery</title>
 
187
  <style>
188
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
189
 
@@ -519,10 +549,19 @@ if __name__ == '__main__':
519
  text-align: center;
520
  }
521
 
522
- .error-icon {
523
- font-size: 3rem;
524
  margin-bottom: 1rem;
525
- color: #e53e3e;
 
 
 
 
 
 
 
 
 
526
  }
527
 
528
  /* Pagination Styling */
@@ -606,6 +645,58 @@ if __name__ == '__main__':
606
  font-size: 0.9rem;
607
  }
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  /* Responsive Design */
610
  @media (max-width: 768px) {
611
  body {
@@ -634,6 +725,10 @@ if __name__ == '__main__':
634
  .pagination {
635
  flex-wrap: wrap;
636
  }
 
 
 
 
637
  }
638
  </style>
639
  </head>
@@ -655,6 +750,29 @@ if __name__ == '__main__':
655
  <p>Discover the top 500 trending spaces from the Huggingface</p>
656
  </div>
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  <div class="search-bar">
659
  <input type="text" id="searchInput" placeholder="Search by name, owner, or tags..." />
660
  <button id="refreshButton" class="refresh-btn">
@@ -690,7 +808,10 @@ if __name__ == '__main__':
690
  loadingError: document.getElementById('loadingError'),
691
  searchInput: document.getElementById('searchInput'),
692
  refreshButton: document.getElementById('refreshButton'),
693
- pagination: document.getElementById('pagination')
 
 
 
694
  };
695
 
696
  // Application state
@@ -701,9 +822,122 @@ if __name__ == '__main__':
701
  itemsPerPage: 72, // 72 items per page
702
  totalItems: 0,
703
  loadingTimeout: null,
704
- staticModeAttempted: {} // Track which spaces have attempted static mode
 
 
 
705
  };
706
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  // Display loading indicator
708
  function setLoading(isLoading) {
709
  state.isLoading = isLoading;
@@ -750,6 +984,92 @@ if __name__ == '__main__':
750
  }
751
  }
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  // Load spaces with timeout
754
  async function loadSpaces(page = 0) {
755
  setLoading(true);
@@ -773,9 +1093,15 @@ if __name__ == '__main__':
773
  state.spaces = data.spaces;
774
  state.totalItems = data.total;
775
  state.currentPage = page;
 
776
 
777
  renderGrid(data.spaces);
778
  renderPagination();
 
 
 
 
 
779
  } catch (error) {
780
  console.error('Error loading spaces:', error);
781
 
@@ -854,7 +1180,7 @@ if __name__ == '__main__':
854
  elements.pagination.appendChild(nextButton);
855
  }
856
 
857
- // Handle iframe error and provide static site fallback
858
  function handleIframeError(iframe, owner, name, title) {
859
  const container = iframe.parentNode;
860
 
@@ -862,11 +1188,11 @@ if __name__ == '__main__':
862
  const errorPlaceholder = document.createElement('div');
863
  errorPlaceholder.className = 'error-placeholder';
864
 
865
- // Error icon
866
- const errorIcon = document.createElement('div');
867
- errorIcon.className = 'error-icon';
868
- errorIcon.textContent = '⚠️';
869
- errorPlaceholder.appendChild(errorIcon);
870
 
871
  // Error message
872
  const errorMessage = document.createElement('p');
@@ -1008,7 +1334,7 @@ if __name__ == '__main__':
1008
  state.staticModeAttempted[spaceKey] = true;
1009
  iframe.src = directUrl + '/index.html';
1010
  } else {
1011
- // If static mode also failed, show error
1012
  handleIframeError(iframe, owner, name, title);
1013
  }
1014
  };
@@ -1023,13 +1349,16 @@ if __name__ == '__main__':
1023
  iframeDoc.body.textContent.includes('404') ||
1024
  iframeDoc.body.textContent.includes('not found');
1025
 
1026
- if (isErrorPage && !state.staticModeAttempted[spaceKey]) {
1027
- // If it's an error page and we haven't tried static mode yet
1028
- state.staticModeAttempted[spaceKey] = true;
1029
- iframe.src = directUrl + '/index.html';
1030
- } else if (isErrorPage) {
1031
- // If static mode already attempted and still failing
1032
- handleIframeError(iframe, owner, name, title);
 
 
 
1033
  }
1034
  } catch (e) {
1035
  // Cross-origin errors are expected, this generally means the iframe loaded
@@ -1092,6 +1421,9 @@ if __name__ == '__main__':
1092
  // Refresh button event listener
1093
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1094
 
 
 
 
1095
  // Mac buttons functionality (just for show)
1096
  document.querySelectorAll('.mac-button').forEach(button => {
1097
  button.addEventListener('click', function(e) {
 
2
  import requests
3
  import os
4
  import time
5
+ import random
6
+ from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
10
  # Function to fetch trending spaces from Huggingface with pagination
11
  def fetch_trending_spaces(offset=0, limit=72):
12
  try:
13
+ # Simple data fetching
14
  url = "https://huggingface.co/api/spaces"
15
+ params = {"limit": 500} # Get max 500
16
 
17
+ # Increase timeout
18
  response = requests.get(url, params=params, timeout=30)
19
 
20
  if response.status_code == 200:
21
  spaces = response.json()
22
  filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None']
23
 
24
+ # Slice according to requested offset and limit
25
  start = min(offset, len(filtered_spaces))
26
  end = min(offset + limit, len(filtered_spaces))
27
 
 
31
  'spaces': filtered_spaces[start:end],
32
  'total': len(filtered_spaces),
33
  'offset': offset,
34
+ 'limit': limit,
35
+ 'all_spaces': filtered_spaces # Return all spaces for stats calculation
36
  }
37
  else:
38
  print(f"Error fetching spaces: {response.status_code}")
39
+ # Return empty spaces with fake 200 limit data
40
  return {
41
  'spaces': generate_dummy_spaces(limit),
42
  'total': 200,
43
  'offset': offset,
44
+ 'limit': limit,
45
+ 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats
46
  }
47
  except Exception as e:
48
  print(f"Exception when fetching spaces: {e}")
49
+ # Generate fake data
50
  return {
51
  'spaces': generate_dummy_spaces(limit),
52
  'total': 200,
53
  'offset': offset,
54
+ 'limit': limit,
55
+ 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats
56
  }
57
 
58
+ # Generate dummy spaces in case of error
59
  def generate_dummy_spaces(count):
60
  spaces = []
61
  for i in range(count):
 
70
 
71
  # Transform Huggingface URL to direct space URL
72
  def transform_url(owner, name):
73
+ # 1. Replace '.' with '-'
74
  name = name.replace('.', '-')
75
+ # 2. Replace '_' with '-'
76
  name = name.replace('_', '-')
77
+ # 3. Convert to lowercase
78
  owner = owner.lower()
79
  name = name.lower()
80
 
 
83
  # Get space details
84
  def get_space_details(space_data, index, offset):
85
  try:
86
+ # Extract common info
87
  if '/' in space_data.get('id', ''):
88
  owner, name = space_data.get('id', '').split('/', 1)
89
  else:
90
  owner = space_data.get('owner', '')
91
  name = space_data.get('id', '')
92
 
93
+ # Ignore if contains None
94
  if owner == 'None' or name == 'None':
95
  return None
96
 
97
+ # Construct URLs
98
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
99
  embed_url = transform_url(owner, name)
100
 
101
+ # Likes count
102
  likes_count = space_data.get('likes', 0)
103
 
104
+ # Extract title
105
  title = space_data.get('title', name)
106
 
107
+ # Tags
108
  tags = space_data.get('tags', [])
109
 
110
  return {
 
112
  'embedUrl': embed_url,
113
  'title': title,
114
  'owner': owner,
115
+ 'name': name, # Store Space name
116
  'likes_count': likes_count,
117
  'tags': tags,
118
  'rank': offset + index + 1
119
  }
120
  except Exception as e:
121
  print(f"Error processing space data: {e}")
122
+ # Return basic object even if error occurs
123
  return {
124
  'url': 'https://huggingface.co/spaces',
125
  'embedUrl': 'https://huggingface.co/spaces',
 
131
  'rank': offset + index + 1
132
  }
133
 
134
+ # Get owner statistics from all spaces
135
+ def get_owner_stats(all_spaces):
136
+ owners = []
137
+ for space in all_spaces:
138
+ if '/' in space.get('id', ''):
139
+ owner, _ = space.get('id', '').split('/', 1)
140
+ else:
141
+ owner = space.get('owner', '')
142
+
143
+ if owner != 'None':
144
+ owners.append(owner)
145
+
146
+ # Count occurrences of each owner
147
+ owner_counts = Counter(owners)
148
+
149
+ # Get top 30 owners by count
150
+ top_owners = owner_counts.most_common(30)
151
+
152
+ return top_owners
153
+
154
  # Homepage route
155
  @app.route('/')
156
  def home():
 
161
  def trending_spaces():
162
  search_query = request.args.get('search', '').lower()
163
  offset = int(request.args.get('offset', 0))
164
+ limit = int(request.args.get('limit', 72)) # Default 72
165
 
166
  # Fetch trending spaces
167
  spaces_data = fetch_trending_spaces(offset, limit)
 
189
 
190
  results.append(space_info)
191
 
192
+ # Get owner statistics for all spaces
193
+ top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
194
+
195
  return jsonify({
196
  'spaces': results,
197
  'total': spaces_data['total'],
198
  'offset': offset,
199
+ 'limit': limit,
200
+ 'top_owners': top_owners # Add top owners data
201
  })
202
 
203
  if __name__ == '__main__':
 
213
  <meta charset="UTF-8">
214
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
215
  <title>Huggingface Spaces Gallery</title>
216
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
217
  <style>
218
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
219
 
 
549
  text-align: center;
550
  }
551
 
552
+ .error-emoji {
553
+ font-size: 5rem;
554
  margin-bottom: 1rem;
555
+ animation: bounce 1s infinite alternate;
556
+ }
557
+
558
+ @keyframes bounce {
559
+ from {
560
+ transform: translateY(0px);
561
+ }
562
+ to {
563
+ transform: translateY(-10px);
564
+ }
565
  }
566
 
567
  /* Pagination Styling */
 
645
  font-size: 0.9rem;
646
  }
647
 
648
+ /* Stats window styling */
649
+ .stats-window {
650
+ margin-top: 2rem;
651
+ margin-bottom: 2rem;
652
+ }
653
+
654
+ .stats-header {
655
+ display: flex;
656
+ justify-content: space-between;
657
+ align-items: center;
658
+ margin-bottom: 1rem;
659
+ }
660
+
661
+ .stats-title {
662
+ font-size: 1.5rem;
663
+ font-weight: 700;
664
+ color: #2d3748;
665
+ }
666
+
667
+ .stats-toggle {
668
+ background-color: var(--pastel-blue);
669
+ border: none;
670
+ padding: 8px 16px;
671
+ border-radius: 20px;
672
+ font-weight: 600;
673
+ cursor: pointer;
674
+ transition: all 0.2s;
675
+ }
676
+
677
+ .stats-toggle:hover {
678
+ background-color: var(--pastel-purple);
679
+ }
680
+
681
+ .stats-content {
682
+ background-color: white;
683
+ border-radius: 10px;
684
+ padding: 20px;
685
+ box-shadow: var(--box-shadow);
686
+ max-height: 0;
687
+ overflow: hidden;
688
+ transition: max-height 0.5s ease-out;
689
+ }
690
+
691
+ .stats-content.open {
692
+ max-height: 600px;
693
+ }
694
+
695
+ .chart-container {
696
+ width: 100%;
697
+ height: 500px;
698
+ }
699
+
700
  /* Responsive Design */
701
  @media (max-width: 768px) {
702
  body {
 
725
  .pagination {
726
  flex-wrap: wrap;
727
  }
728
+
729
+ .chart-container {
730
+ height: 300px;
731
+ }
732
  }
733
  </style>
734
  </head>
 
750
  <p>Discover the top 500 trending spaces from the Huggingface</p>
751
  </div>
752
 
753
+ <!-- Stats Section -->
754
+ <div class="stats-window mac-window">
755
+ <div class="mac-toolbar">
756
+ <div class="mac-buttons">
757
+ <div class="mac-button mac-close"></div>
758
+ <div class="mac-button mac-minimize"></div>
759
+ <div class="mac-button mac-maximize"></div>
760
+ </div>
761
+ <div class="mac-title">Creator Statistics</div>
762
+ </div>
763
+ <div class="mac-content">
764
+ <div class="stats-header">
765
+ <div class="stats-title">Top 30 Creators by Number of Spaces</div>
766
+ <button id="statsToggle" class="stats-toggle">Show Stats</button>
767
+ </div>
768
+ <div id="statsContent" class="stats-content">
769
+ <div class="chart-container">
770
+ <canvas id="creatorStatsChart"></canvas>
771
+ </div>
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
  <div class="search-bar">
777
  <input type="text" id="searchInput" placeholder="Search by name, owner, or tags..." />
778
  <button id="refreshButton" class="refresh-btn">
 
808
  loadingError: document.getElementById('loadingError'),
809
  searchInput: document.getElementById('searchInput'),
810
  refreshButton: document.getElementById('refreshButton'),
811
+ pagination: document.getElementById('pagination'),
812
+ statsToggle: document.getElementById('statsToggle'),
813
+ statsContent: document.getElementById('statsContent'),
814
+ creatorStatsChart: document.getElementById('creatorStatsChart')
815
  };
816
 
817
  // Application state
 
822
  itemsPerPage: 72, // 72 items per page
823
  totalItems: 0,
824
  loadingTimeout: null,
825
+ staticModeAttempted: {}, // Track which spaces have attempted static mode
826
+ statsVisible: false,
827
+ chartInstance: null,
828
+ topOwners: []
829
  };
830
 
831
+ // Random emoji list for 404 errors
832
+ const randomEmojis = [
833
+ '🙈', '🙉', '🙊', '🐵', '🐒', '🦍', '🦧', '🐶', '🐕', '🦮',
834
+ '🐩', '🐺', '🦊', '🦝', '🐱', '🐈', '🦁', '🐯', '🐅', '🐆',
835
+ '🐴', '🐎', '🦄', '🦓', '🦌', '🐮', '🐂', '🐃', '🐄', '🐷',
836
+ '🐖', '🐗', '🐏', '🐑', '🐐', '🐪', '🐫', '🦙', '🦒', '🐘',
837
+ '🦏', '🦛', '🐭', '🐁', '🐀', '🐹', '🐰', '🐇', '🐿️', '🦔',
838
+ '🦇', '🐻', '🐨', '🐼', '🦥', '🦦', '🦨', '🦘', '🦡', '🐾',
839
+ '🦃', '🐔', '🐓', '🐣', '🐤', '🐥', '🐦', '🐧', '🕊️', '🦅',
840
+ '🦆', '🦢', '🦉', '🦩', '🦚', '🦜', '🐸', '🐊', '🐢', '🦎',
841
+ '🐍', '🐲', '🐉', '🦕', '🦖', '🐳', '🐋', '🐬', '🐟', '🐠',
842
+ '🐡', '🦈', '🐙', '🐚', '🐌', '🦋', '🐛', '🐜', '🐝', '🐞',
843
+ '🦗', '🕷️', '🕸️', '🦂', '🦟', '🦠', '💐', '🌸', '💮', '🏵️',
844
+ '🌹', '🥀', '🌺', '🌻', '🌼', '🌷', '🌱', '🌲', '🌳', '🌴',
845
+ '🌵', '🌾', '🌿', '☘️', '🍀', '🍁', '🍂', '🍃', '🍇', '🍈',
846
+ '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑',
847
+ '🍒', '🍓', '🥝', '🍅', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽',
848
+ '🌶️', '🥒', '🥬', '🥦', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞',
849
+ '🥐', '🥖', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩',
850
+ '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥙', '🧆',
851
+ '🥚', '🍳', '🥘', '🍲', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫',
852
+ '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣',
853
+ '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🦀', '🦞', '🦐',
854
+ '🦑', '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁',
855
+ '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🍵',
856
+ '🍶', '🍾', '🍷', '🍸', '🍹', '🍺', '🍻', '🥂', '🥃', '🥤',
857
+ '🧃', '🧉', '🧊', '🥢', '🍽️', '🍴', '🥄', '🔪', '🏺', '🌍',
858
+ '🌎', '🌏', '🌐', '🗺️', '🗾', '🧭', '🏔️', '⛰️', '🌋', '🗻',
859
+ '🏕️', '🏖️', '🏜️', '🏝️', '🏞️', '🏟️', '🏛️', '🏗️', '🧱', '🏘️',
860
+ '🏚️', '🏠', '🏡', '🏢', '🏣', '🏤', '🏥', '🏦', '🏨', '🏩',
861
+ '🏪', '🏫', '🏬', '🏭', '🏯', '🏰', '💒', '🗼', '🗽', '⛪',
862
+ '🕌', '🛕', '🕍', '⛩️', '🕋', '⛲', '⛺', '🌁', '🌃', '🏙️',
863
+ '🌄', '🌅', '🌆', '🌇', '🌉', '♨️', '🎠', '🎡', '🎢', '💈',
864
+ '🎪', '🚂', '🚃', '🚄', '🚅', '🚆', '🚇', '🚈', '🚉', '🚊',
865
+ '🚝', '🚞', '🚋', '🚌', '🚍', '🚎', '🚐', '🚑', '🚒', '🚓',
866
+ '🚔', '🚕', '🚖', '🚗', '🚘', '🚙', '🚚', '🚛', '🚜', '🏎️',
867
+ '🏍️', '🛵', '🦽', '🦼', '🛺', '🚲', '🛴', '🛹', '🚏', '🛣️',
868
+ '🛤️', '🛢️', '⛽', '🚨', '🚥', '🚦', '🛑', '🚧', '⚓', '⛵',
869
+ '🛶', '🚤', '🛳️', '⛴️', '🛥️', '🚢', '✈️', '🛩️', '🛫', '🛬',
870
+ '🪂', '💺', '🚁', '🚟', '🚠', '🚡', '🛰️', '🚀', '🛸', '🛎️',
871
+ '🧳', '⌛', '⏳', '⌚', '⏰', '⏱️', '⏲️', '🕰️', '🕛', '🕧',
872
+ '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠',
873
+ '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥',
874
+ '🕚', '🕦', '🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘',
875
+ '🌙', '🌚', '🌛', '🌜', '🌡️', '☀️', '🌝', '🌞', '🪐', '⭐',
876
+ '🌟', '🌠', '🌌', '☁️', '⛅', '⛈️', '🌤️', '🌥️', '🌦️', '🌧️',
877
+ '🌨️', '🌩️', '🌪️', '🌫️', '🌬️', '🌀', '🌈', '🌂', '☂️', '☔',
878
+ '⛱️', '⚡', '❄️', '☃️', '⛄', '☄️', '���', '💧', '🌊', '🎃',
879
+ '🎄', '🎆', '🎇', '🧨', '✨', '🎈', '🎉', '🎊', '🎋', '🎍',
880
+ '🎎', '🎏', '🎐', '🎑', '🧧', '🎀', '🎁', '🎗️', '🎟️', '🎫',
881
+ '🎖️', '🏆', '🏅', '🥇', '🥈', '🥉', '⚽', '⚾', '🥎', '🏀',
882
+ '🏐', '🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍',
883
+ '🏓', '🏸', '🥊', '🥋', '🥅', '⛳', '⛸️', '🎣', '🤿', '🎽',
884
+ '🎿', '🛷', '🥌', '🎯', '🪀', '🪁', '🎱', '🔮', '🧿', '🎮',
885
+ '🕹️', '🎰', '🎲', '🧩', '🧸', '♠️', '♥️', '♦️', '♣️', '♟️',
886
+ '🃏', '🀄', '🎴', '🎭', '🖼️', '🎨', '🧵', '🧶', '👓', '🕶️',
887
+ '🥽', '🥼', '🦺', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦',
888
+ '👗', '👘', '🥻', '🩱', '🩲', '🩳', '👙', '👚', '👛', '👜',
889
+ '👝', '🛍️', '🎒', '👞', '👟', '🥾', '🥿', '👠', '👡', '🩰',
890
+ '👢', '👑', '👒', '🎩', '🎓', '🧢', '⛑️', '📿', '💄', '💍',
891
+ '💎', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕',
892
+ '🎼', '🎵', '🎶', '🎙️', '🎚️', '🎛️', '🎤', '🎧', '📻', '🎷',
893
+ '🎸', '🎹', '🎺', '🎻', '🪕', '🥁', '📱', '📲', '☎️', '📞',
894
+ '📟', '📠', '🔋', '🔌', '💻', '🖥️', '🖨️', '⌨️', '🖱️', '🖲️',
895
+ '💽', '💾', '💿', '📀', '🧮', '🎥', '🎞️', '📽️', '🎬', '📺',
896
+ '📷', '📸', '📹', '📼', '🔍', '🔎', '🕯️', '💡', '🔦', '🏮',
897
+ '🪔', '📔', '📕', '📖', '📗', '📘', '📙', '📚', '📓', '📒',
898
+ '📃', '📜', '📄', '📰', '🗞️', '📑', '🔖', '🏷️', '💰', '💴',
899
+ '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '✉️', '📧', '📨',
900
+ '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳️',
901
+ '✏️', '✒️', '🖋️', '🖊️', '🖌️', '🖍️', '📝', '💼', '📁', '📂',
902
+ '🗂️', '📅', '📆', '🗒️', '🗓️', '📇', '📈', '📉', '📊', '📋',
903
+ '📌', '📍', '📎', '🖇️', '📏', '📐', '✂️', '🗃️', '🗄️', '🗑️',
904
+ '🔒', '🔓', '🔏', '🔐', '🔑', '🗝️', '🔨', '🪓', '⛏️', '⚒️',
905
+ '🛠️', '🗡️', '⚔️', '🔫', '🏹', '🛡️', '🔧', '🔩', '⚙️', '🗜️',
906
+ '⚖️', '🦯', '🔗', '⛓️', '🧰', '🧲', '⚗️', '🧪', '🧫', '🧬',
907
+ '🔬', '🔭', '📡', '💉', '🩸', '💊', '🩹', '🩺', '🚪', '🛏️',
908
+ '🛋️', '🪑', '🚽', '🚿', '🛁', '🪒', '🧴', '🧷', '🧹', '🧺',
909
+ '🧻', '🧼', '🧽', '🧯', '🛒', '🚬', '⚰️', '⚱️', '🗿', '🏧',
910
+ '🚮', '🚰', '♿', '🚹', '🚺', '🚻', '🚼', '🚾', '🛂', '🛃',
911
+ '🛄', '🛅', '⚠️', '🚸', '⛔', '🚫', '🚳', '🚭', '🚯', '🚱',
912
+ '🚷', '📵', '🔞', '☢️', '☣️', '⬆️', '↗️', '➡️', '↘️', '⬇️',
913
+ '↙️', '⬅️', '↖️', '↕️', '↔️', '↩️', '↪️', '⤴️', '⤵️', '🔃',
914
+ '🔄', '🔙', '🔚', '🔛', '🔜', '🔝', '🛐', '⚛️', '🕉️', '✡️',
915
+ '☸️', '☯️', '✝️', '☦️', '☪️', '☮️', '🕎', '🔯', '♈', '♉',
916
+ '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎',
917
+ '🔀', '🔁', '🔂', '▶️', '⏩', '⏭️', '⏯️', '◀️', '⏪', '⏮️',
918
+ '🔼', '⏫', '🔽', '⏬', '⏸️', '⏹️', '⏺️', '⏏️', '🎦', '🔅',
919
+ '🔆', '📶', '📳', '📴', '♀️', '♂️', '⚧️', '✖️', '➕', '➖',
920
+ '➗', '♾️', '‼️', '⁉️', '❓', '❔', '❕', '❗', '〰️', '💱',
921
+ '💲', '⚕️', '♻️', '⚜️', '🔱', '📛', '🔰', '⭕', '✅', '☑️',
922
+ '✔️', '❌', '❎', '➰', '➿', '〽️', '✳️', '✴️', '❇️', '©️',
923
+ '®️', '™️', '#️⃣', '*️⃣', '0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣',
924
+ '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔠', '🔡', '🔢', '🔣', '🔤',
925
+ '🅰️', '🆎', '🅱️', '🆑', '🆒', '🆓', 'ℹ️', '🆔', 'Ⓜ️', '🆕',
926
+ '🆖', '🅾️', '🆗', '🅿️', '🆘', '🆙', '🆚', '🈁', '🈂️', '🈷️',
927
+ '🈶', '🈯', '🉐', '🈹', '🈚', '🈲', '🉑', '🈸', '🈴', '🈳',
928
+ '㊗️', '㊙️', '🈺', '🈵', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣',
929
+ '🟤', '⚫', '⚪', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '🟫',
930
+ '⬛', '⬜', '◼️', '◻️', '◾', '◽', '▪️', '▫️', '🔶', '🔷',
931
+ '🔸', '🔹', '🔺', '🔻', '💠', '🔘', '🔳', '🔲', '🏁', '🚩',
932
+ '🎌', '🏴', '🏳️', '🏳️‍🌈', '🏴‍☠️', '🇦🇨', '🇦🇩', '🇦🇪', '🇦🇫', '🇦����',
933
+ '🇦🇮', '🇦🇱', '🇦🇲', '🇦🇴', '🇦🇶', '🇦🇷', '🇦🇸', '🇦🇹', '🇦🇺', '🇦🇼',
934
+ '🇦🇽', '🇦🇿', '🇧🇦', '🇧🇧', '🇧🇩', '🇧🇪', '🇧🇫', '🇧🇬', '🇧🇭', '🇧🇮',
935
+ '🇧🇯', '🇧🇱', '🇧🇲', '🇧🇳', '🇧🇴', '🇧🇶', '🇧🇷', '🇧🇸', '🇧🇹', '🇧🇻',
936
+ '🇧🇼', '🇧🇾', '🇧🇿', '🇨🇦', '🇨🇨', '🇨🇩', '🇨🇫', '🇨🇬', '🇨🇭', '🇨🇮',
937
+ '🇨🇰', '🇨🇱', '🇨🇲', '🇨🇳', '🇨🇴', '🇨🇵', '🇨🇷', '🇨🇺', '🇨🇻', '🇨🇼',
938
+ '🇨🇽', '🇨🇾', '🇨🇿'
939
+ ];
940
+
941
  // Display loading indicator
942
  function setLoading(isLoading) {
943
  state.isLoading = isLoading;
 
984
  }
985
  }
986
 
987
+ // Get random emoji for 404 errors
988
+ function getRandomEmoji() {
989
+ return randomEmojis[Math.floor(Math.random() * randomEmojis.length)];
990
+ }
991
+
992
+ // Toggle stats display
993
+ function toggleStats() {
994
+ state.statsVisible = !state.statsVisible;
995
+ elements.statsContent.classList.toggle('open', state.statsVisible);
996
+ elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
997
+
998
+ if (state.statsVisible && state.topOwners.length > 0) {
999
+ renderCreatorStats();
1000
+ }
1001
+ }
1002
+
1003
+ // Render creator stats chart
1004
+ function renderCreatorStats() {
1005
+ if (state.chartInstance) {
1006
+ state.chartInstance.destroy();
1007
+ }
1008
+
1009
+ const ctx = elements.creatorStatsChart.getContext('2d');
1010
+
1011
+ // Prepare data
1012
+ const labels = state.topOwners.map(item => item[0]);
1013
+ const data = state.topOwners.map(item => item[1]);
1014
+
1015
+ // Generate colors for bars
1016
+ const colors = [];
1017
+ for (let i = 0; i < labels.length; i++) {
1018
+ const hue = (i * 360 / labels.length) % 360;
1019
+ colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1020
+ }
1021
+
1022
+ // Create chart
1023
+ state.chartInstance = new Chart(ctx, {
1024
+ type: 'bar',
1025
+ data: {
1026
+ labels: labels,
1027
+ datasets: [{
1028
+ label: 'Number of Spaces',
1029
+ data: data,
1030
+ backgroundColor: colors,
1031
+ borderColor: colors.map(color => color.replace('0.7', '1')),
1032
+ borderWidth: 1
1033
+ }]
1034
+ },
1035
+ options: {
1036
+ indexAxis: 'y',
1037
+ responsive: true,
1038
+ maintainAspectRatio: false,
1039
+ plugins: {
1040
+ legend: {
1041
+ display: false
1042
+ },
1043
+ tooltip: {
1044
+ callbacks: {
1045
+ title: function(tooltipItems) {
1046
+ return tooltipItems[0].label;
1047
+ },
1048
+ label: function(context) {
1049
+ return `Spaces: ${context.raw}`;
1050
+ }
1051
+ }
1052
+ }
1053
+ },
1054
+ scales: {
1055
+ x: {
1056
+ beginAtZero: true,
1057
+ title: {
1058
+ display: true,
1059
+ text: 'Number of Spaces'
1060
+ }
1061
+ },
1062
+ y: {
1063
+ title: {
1064
+ display: true,
1065
+ text: 'Creator ID'
1066
+ }
1067
+ }
1068
+ }
1069
+ }
1070
+ });
1071
+ }
1072
+
1073
  // Load spaces with timeout
1074
  async function loadSpaces(page = 0) {
1075
  setLoading(true);
 
1093
  state.spaces = data.spaces;
1094
  state.totalItems = data.total;
1095
  state.currentPage = page;
1096
+ state.topOwners = data.top_owners || [];
1097
 
1098
  renderGrid(data.spaces);
1099
  renderPagination();
1100
+
1101
+ // If stats are visible, update chart
1102
+ if (state.statsVisible && state.topOwners.length > 0) {
1103
+ renderCreatorStats();
1104
+ }
1105
  } catch (error) {
1106
  console.error('Error loading spaces:', error);
1107
 
 
1180
  elements.pagination.appendChild(nextButton);
1181
  }
1182
 
1183
+ // Handle iframe error and provide static site fallback with random emoji
1184
  function handleIframeError(iframe, owner, name, title) {
1185
  const container = iframe.parentNode;
1186
 
 
1188
  const errorPlaceholder = document.createElement('div');
1189
  errorPlaceholder.className = 'error-placeholder';
1190
 
1191
+ // Random emoji instead of error icon
1192
+ const errorEmoji = document.createElement('div');
1193
+ errorEmoji.className = 'error-emoji';
1194
+ errorEmoji.textContent = getRandomEmoji();
1195
+ errorPlaceholder.appendChild(errorEmoji);
1196
 
1197
  // Error message
1198
  const errorMessage = document.createElement('p');
 
1334
  state.staticModeAttempted[spaceKey] = true;
1335
  iframe.src = directUrl + '/index.html';
1336
  } else {
1337
+ // If static mode also failed, show error with random emoji
1338
  handleIframeError(iframe, owner, name, title);
1339
  }
1340
  };
 
1349
  iframeDoc.body.textContent.includes('404') ||
1350
  iframeDoc.body.textContent.includes('not found');
1351
 
1352
+ if (isErrorPage) {
1353
+ // For 404 errors, show random emoji
1354
+ if (!state.staticModeAttempted[spaceKey]) {
1355
+ // Try static mode first
1356
+ state.staticModeAttempted[spaceKey] = true;
1357
+ iframe.src = directUrl + '/index.html';
1358
+ } else {
1359
+ // If static mode already attempted and still failing, show random emoji
1360
+ handleIframeError(iframe, owner, name, title);
1361
+ }
1362
  }
1363
  } catch (e) {
1364
  // Cross-origin errors are expected, this generally means the iframe loaded
 
1421
  // Refresh button event listener
1422
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1423
 
1424
+ // Stats toggle button event listener
1425
+ elements.statsToggle.addEventListener('click', toggleStats);
1426
+
1427
  // Mac buttons functionality (just for show)
1428
  document.querySelectorAll('.mac-button').forEach(button => {
1429
  button.addEventListener('click', function(e) {