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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +769 -254
app.py CHANGED
@@ -1,35 +1,82 @@
1
  from flask import Flask, render_template, request, jsonify
2
  import requests
3
  import os
 
4
 
5
  app = Flask(__name__)
6
 
7
- # Function to fetch trending spaces from Huggingface
8
- def fetch_trending_spaces(limit=100):
9
  try:
10
- # 가장 단순한 방식으로 API 호출 (파라미터 없이)
11
  url = "https://huggingface.co/api/spaces"
 
12
 
13
- response = requests.get(url, timeout=10)
 
14
 
15
  if response.status_code == 200:
16
- # None 값이 있는 항목 필터링
17
  spaces = response.json()
18
  filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None']
19
- return filtered_spaces[:limit]
 
 
 
 
 
 
 
 
 
 
 
 
20
  else:
21
  print(f"Error fetching spaces: {response.status_code}")
22
- return []
 
 
 
 
 
 
23
  except Exception as e:
24
  print(f"Exception when fetching spaces: {e}")
25
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # Transform Huggingface URL to direct space URL
28
  def transform_url(owner, name):
 
 
 
 
 
 
 
 
29
  return f"https://{owner}-{name}.hf.space"
30
 
31
  # Get space details
32
- def get_space_details(space_data):
33
  try:
34
  # 공통 정보 추출
35
  if '/' in space_data.get('id', ''):
@@ -62,11 +109,22 @@ def get_space_details(space_data):
62
  'owner': owner,
63
  'name': name, # Space 이름 추가 저장
64
  'likes_count': likes_count,
65
- 'tags': tags
 
66
  }
67
  except Exception as e:
68
  print(f"Error processing space data: {e}")
69
- return None
 
 
 
 
 
 
 
 
 
 
70
 
71
  # Homepage route
72
  @app.route('/')
@@ -77,15 +135,16 @@ def home():
77
  @app.route('/api/trending-spaces', methods=['GET'])
78
  def trending_spaces():
79
  search_query = request.args.get('search', '').lower()
80
- limit = int(request.args.get('limit', 100))
 
81
 
82
  # Fetch trending spaces
83
- spaces_data = fetch_trending_spaces(limit)
84
 
85
  # Process and filter spaces
86
  results = []
87
- for index, space_data in enumerate(spaces_data):
88
- space_info = get_space_details(space_data)
89
 
90
  if not space_info:
91
  continue
@@ -105,7 +164,12 @@ def trending_spaces():
105
 
106
  results.append(space_info)
107
 
108
- return jsonify(results)
 
 
 
 
 
109
 
110
  if __name__ == '__main__':
111
  # Create templates folder
@@ -115,85 +179,180 @@ if __name__ == '__main__':
115
  with open('templates/index.html', 'w', encoding='utf-8') as f:
116
  f.write('''
117
  <!DOCTYPE html>
118
- <html lang="ko">
119
  <head>
120
  <meta charset="UTF-8">
121
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
- <title>인기 허깅페이스 스페이스</title>
123
  <style>
124
- body {
125
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
126
- line-height: 1.6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  margin: 0;
128
  padding: 0;
129
- color: #333;
130
- background-color: #f5f8fa;
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
  .container {
134
- max-width: 1400px;
135
  margin: 0 auto;
136
- padding: 1rem;
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  .header {
140
- background-color: #ffffff;
141
- padding: 1rem;
142
- border-radius: 8px;
143
- margin-bottom: 1rem;
144
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
145
  text-align: center;
 
 
146
  }
147
 
148
  .header h1 {
 
 
149
  margin: 0;
150
- color: #2c3e50;
151
- font-size: 1.8rem;
152
  }
153
 
154
- .filter-controls {
155
- background-color: #ffffff;
156
- padding: 1rem;
157
- border-radius: 8px;
158
- margin-bottom: 1rem;
159
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
 
 
160
  display: flex;
161
- justify-content: center;
162
  align-items: center;
163
- gap: 10px;
 
 
 
 
 
 
 
164
  }
165
 
166
- input[type="text"] {
167
- padding: 0.7rem;
168
- border: 1px solid #ddd;
169
- border-radius: 4px;
170
  font-size: 1rem;
171
- width: 300px;
 
 
172
  }
173
 
174
- button.refresh-btn {
175
- padding: 0.7rem 1.2rem;
176
- background-color: #4CAF50;
177
- color: white;
178
  border: none;
179
- border-radius: 4px;
180
- cursor: pointer;
181
  font-size: 1rem;
182
- transition: background-color 0.2s;
 
 
183
  display: flex;
184
  align-items: center;
185
- gap: 5px;
186
  }
187
 
188
- button.refresh-btn:hover {
189
- background-color: #45a049;
 
190
  }
191
 
192
  .refresh-icon {
193
  display: inline-block;
194
  width: 16px;
195
  height: 16px;
196
- border: 2px solid white;
197
  border-top-color: transparent;
198
  border-radius: 50%;
199
  animation: none;
@@ -208,105 +367,141 @@ if __name__ == '__main__':
208
  100% { transform: rotate(360deg); }
209
  }
210
 
 
211
  .grid-container {
212
  display: grid;
213
- grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
214
  gap: 1.5rem;
 
215
  }
216
 
217
  .grid-item {
218
- background-color: #ffffff;
219
- border-radius: 8px;
220
  overflow: hidden;
221
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  display: flex;
223
  flex-direction: column;
224
- height: 600px;
225
- position: relative;
 
226
  }
227
 
228
- .grid-header {
229
- padding: 0.8rem;
230
- border-bottom: 1px solid #eee;
231
- background-color: #f9f9f9;
232
  display: flex;
233
  justify-content: space-between;
234
  align-items: center;
235
- z-index: 2;
236
  }
237
 
238
- .grid-header-left {
239
- display: flex;
240
- flex-direction: column;
241
- max-width: 70%;
 
 
 
242
  }
243
 
244
  .grid-header h3 {
245
  margin: 0;
246
- padding: 0;
247
- font-size: 1.1rem;
248
- color: #333;
249
  white-space: nowrap;
250
  overflow: hidden;
251
  text-overflow: ellipsis;
252
  }
253
 
254
- .owner-info {
255
- font-size: 0.85rem;
256
- color: #666;
257
- margin-top: 3px;
258
- }
259
-
260
- .grid-actions {
261
  display: flex;
 
262
  align-items: center;
 
263
  }
264
 
265
- .open-link {
266
- color: #4CAF50;
267
- text-decoration: none;
268
- font-size: 0.9rem;
269
- margin-left: 10px;
270
  }
271
 
272
  .likes-counter {
273
  display: flex;
274
  align-items: center;
275
- font-size: 0.9rem;
276
- color: #e91e63;
277
  }
278
 
279
  .likes-counter span {
280
  margin-left: 4px;
281
  }
282
 
283
- .rank-badge {
284
- position: absolute;
285
- top: 10px;
286
- right: 10px; /* 오른쪽으로 이동 */
287
- background: rgba(0,0,0,0.7);
288
- color: white;
289
- padding: 5px 15px;
290
- border-radius: 20px;
291
- font-weight: bold;
292
- font-size: 0.9em;
293
  backdrop-filter: blur(5px);
294
- z-index: 3;
 
 
 
 
 
 
295
  }
296
 
297
- .grid-content {
298
- flex: 1;
299
- position: relative;
300
- overflow: hidden;
 
 
 
 
301
  }
302
 
303
- .grid-content iframe {
 
 
 
 
304
  position: absolute;
305
  top: 0;
306
  left: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  width: 100%;
308
  height: 100%;
309
  border: none;
 
310
  }
311
 
312
  .error-placeholder {
@@ -319,18 +514,57 @@ if __name__ == '__main__':
319
  flex-direction: column;
320
  justify-content: center;
321
  align-items: center;
322
- background-color: #f8f9fa;
323
- color: #6c757d;
324
- text-align: center;
325
  padding: 20px;
 
 
326
  }
327
 
328
- .error-placeholder .error-icon {
329
  font-size: 3rem;
330
  margin-bottom: 1rem;
331
- color: #dc3545;
 
 
 
 
 
 
 
 
 
332
  }
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  .loading {
335
  position: fixed;
336
  top: 0;
@@ -338,62 +572,114 @@ if __name__ == '__main__':
338
  right: 0;
339
  bottom: 0;
340
  background-color: rgba(255, 255, 255, 0.8);
 
341
  display: flex;
342
  justify-content: center;
343
  align-items: center;
344
  z-index: 1000;
345
- font-size: 1.5rem;
 
 
 
346
  }
347
 
348
  .loading-spinner {
349
- border: 5px solid #f3f3f3;
350
- border-top: 5px solid #4CAF50;
 
 
351
  border-radius: 50%;
352
- width: 50px;
353
- height: 50px;
354
  animation: spin 1s linear infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
 
 
357
  @media (max-width: 768px) {
358
- .filter-controls {
 
 
 
 
 
 
 
 
359
  flex-direction: column;
360
- align-items: flex-start;
361
  }
362
 
363
- .filter-controls > * {
364
- margin-bottom: 0.5rem;
365
  width: 100%;
 
366
  }
367
 
368
- .grid-container {
369
- grid-template-columns: 1fr;
 
370
  }
371
 
372
- input[type="text"] {
373
- width: 100%;
374
  }
375
  }
376
  </style>
377
  </head>
378
  <body>
379
  <div class="container">
380
- <div class="header">
381
- <h1>인기 허깅페이스 스페이스 Top 100</h1>
382
- </div>
383
-
384
- <div class="filter-controls">
385
- <input type="text" id="searchInput" placeholder="이름, 태그 등으로 검색" />
386
- <button id="refreshButton" class="refresh-btn">
387
- <span class="refresh-icon"></span>
388
- 새로고침
389
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  </div>
391
-
392
- <div id="gridContainer" class="grid-container"></div>
393
  </div>
394
 
395
  <div id="loadingIndicator" class="loading">
396
- <div class="loading-spinner"></div>
 
 
 
 
 
 
397
  </div>
398
 
399
  <script>
@@ -401,14 +687,21 @@ if __name__ == '__main__':
401
  const elements = {
402
  gridContainer: document.getElementById('gridContainer'),
403
  loadingIndicator: document.getElementById('loadingIndicator'),
 
404
  searchInput: document.getElementById('searchInput'),
405
- refreshButton: document.getElementById('refreshButton')
 
406
  };
407
 
408
  // Application state
409
  const state = {
410
  isLoading: false,
411
- spaces: []
 
 
 
 
 
412
  };
413
 
414
  // Display loading indicator
@@ -418,8 +711,15 @@ if __name__ == '__main__':
418
 
419
  if (isLoading) {
420
  elements.refreshButton.classList.add('refreshing');
 
 
 
 
 
421
  } else {
422
  elements.refreshButton.classList.remove('refreshing');
 
 
423
  }
424
  }
425
 
@@ -427,65 +727,182 @@ if __name__ == '__main__':
427
  async function handleApiResponse(response) {
428
  if (!response.ok) {
429
  const errorText = await response.text();
430
- throw new Error(`API 오류 (${response.status}): ${errorText}`);
431
  }
432
  return response.json();
433
  }
434
 
435
- // 직접 URL 생성 함수 (클라이언트 측에서도 구현)
436
  function createDirectUrl(owner, name) {
437
- return `https://${owner}-${name}.hf.space`;
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
- // Load spaces
441
- async function loadSpaces() {
442
  setLoading(true);
443
 
444
  try {
445
  const searchText = elements.searchInput.value;
 
 
 
 
 
 
 
 
446
 
447
- const response = await fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}`);
448
- const spaces = await handleApiResponse(response);
 
449
 
450
- state.spaces = spaces;
451
- renderGrid(spaces);
 
 
 
 
 
452
  } catch (error) {
453
- console.error('스페이스 목록 로드 오류:', error);
454
- alert(`스페이스 로드 오류: ${error.message}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  } finally {
456
  setLoading(false);
457
  }
458
  }
459
 
460
- // iframe 로드 에러 처리
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  function handleIframeError(iframe, owner, name, title) {
462
  const container = iframe.parentNode;
463
 
464
- // 에러 메시지 컨테이너 생성
465
  const errorPlaceholder = document.createElement('div');
466
  errorPlaceholder.className = 'error-placeholder';
467
 
468
- // 에러 아이콘
469
  const errorIcon = document.createElement('div');
470
  errorIcon.className = 'error-icon';
471
  errorIcon.textContent = '⚠️';
472
  errorPlaceholder.appendChild(errorIcon);
473
 
474
- // 에러 메시지
475
  const errorMessage = document.createElement('p');
476
- errorMessage.textContent = `"${title}" 스페이스를 로드할 없습니다.`;
477
  errorPlaceholder.appendChild(errorMessage);
478
 
479
- // 직접 링크 제공
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  const directLink = document.createElement('a');
481
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
482
  directLink.target = '_blank';
483
- directLink.textContent = '직접 방문하기';
484
- directLink.style.color = '#007bff';
485
  directLink.style.marginTop = '10px';
 
 
 
 
 
486
  errorPlaceholder.appendChild(directLink);
487
 
488
- // iframe 숨기고 에러 메시지 표��
489
  iframe.style.display = 'none';
490
  container.appendChild(errorPlaceholder);
491
  }
@@ -496,105 +913,165 @@ if __name__ == '__main__':
496
 
497
  if (!spaces || spaces.length === 0) {
498
  const noResultsMsg = document.createElement('p');
499
- noResultsMsg.textContent = '표시할 스페이스가 없습니다.';
500
- noResultsMsg.style.padding = '1rem';
 
501
  noResultsMsg.style.fontStyle = 'italic';
 
502
  elements.gridContainer.appendChild(noResultsMsg);
503
  return;
504
  }
505
 
506
- spaces.forEach((item, index) => {
507
- const { url, embedUrl, title, likes_count, owner, name } = item;
508
-
509
- // Skip if owner is 'None'
510
- if (owner === 'None') {
511
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  }
513
-
514
- // Create grid item
515
- const gridItem = document.createElement('div');
516
- gridItem.className = 'grid-item';
517
-
518
- // Rank badge - 오른쪽 상단으로 이동
519
- const rankBadge = document.createElement('div');
520
- rankBadge.className = 'rank-badge';
521
- rankBadge.textContent = `#${index + 1}`;
522
- gridItem.appendChild(rankBadge);
523
-
524
- // Header with title and actions
525
- const header = document.createElement('div');
526
- header.className = 'grid-header';
527
-
528
- // Left side of header (title and owner)
529
- const headerLeft = document.createElement('div');
530
- headerLeft.className = 'grid-header-left';
531
-
532
- const titleEl = document.createElement('h3');
533
- titleEl.textContent = title;
534
- titleEl.title = title; // For tooltip on hover
535
- headerLeft.appendChild(titleEl);
536
-
537
- const ownerEl = document.createElement('div');
538
- ownerEl.className = 'owner-info';
539
- ownerEl.textContent = `by: ${owner}`;
540
- headerLeft.appendChild(ownerEl);
541
-
542
- header.appendChild(headerLeft);
543
-
544
- // Actions container
545
- const actionsDiv = document.createElement('div');
546
- actionsDiv.className = 'grid-actions';
547
-
548
- // Likes count
549
- const likesCounter = document.createElement('div');
550
- likesCounter.className = 'likes-counter';
551
- likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>';
552
- actionsDiv.appendChild(likesCounter);
553
-
554
- // Open link
555
- const linkEl = document.createElement('a');
556
- linkEl.href = url;
557
- linkEl.target = '_blank';
558
- linkEl.className = 'open-link';
559
- linkEl.textContent = '새 창에서 열기';
560
- actionsDiv.appendChild(linkEl);
561
-
562
- header.appendChild(actionsDiv);
563
-
564
- // Add header to grid item
565
- gridItem.appendChild(header);
566
-
567
- // Content with iframe
568
- const content = document.createElement('div');
569
- content.className = 'grid-content';
570
-
571
- // Create iframe to display the content - 직접 URL 생성
572
- const iframe = document.createElement('iframe');
573
- const directUrl = createDirectUrl(owner, name);
574
- iframe.src = directUrl;
575
- iframe.title = title;
576
- iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi';
577
- iframe.setAttribute('allowfullscreen', '');
578
- iframe.setAttribute('frameborder', '0');
579
- iframe.loading = 'lazy'; // Lazy load iframes for better performance
580
-
581
- // 로드 에러 처리
582
- iframe.onerror = function() {
583
- handleIframeError(iframe, owner, name, title);
584
- };
585
-
586
- iframe.onload = function() {
587
- // iframe이 로드되었지만 에러 페이지일 수 있음
588
- // 여기에 추가 검증 로직 구현 가능
589
- };
590
-
591
- content.appendChild(iframe);
592
-
593
- // Add content to grid item
594
- gridItem.appendChild(content);
595
-
596
- // Add grid item to container
597
- elements.gridContainer.appendChild(gridItem);
598
  });
599
  }
600
 
@@ -602,14 +1079,52 @@ if __name__ == '__main__':
602
  elements.searchInput.addEventListener('input', () => {
603
  // Debounce input to prevent API calls on every keystroke
604
  clearTimeout(state.searchTimeout);
605
- state.searchTimeout = setTimeout(loadSpaces, 300);
 
 
 
 
 
 
 
606
  });
607
 
608
  // Refresh button event listener
609
- elements.refreshButton.addEventListener('click', loadSpaces);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
- // Initialize
612
- loadSpaces();
613
  </script>
614
  </body>
615
  </html>
 
1
  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
+
26
+ print(f"Fetched {len(filtered_spaces)} spaces, returning {end-start} items from {start} to {end}")
27
+
28
+ return {
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):
57
+ spaces.append({
58
+ 'id': f'dummy/space-{i}',
59
+ 'owner': 'dummy',
60
+ 'title': f'Example Space {i+1}',
61
+ 'likes': 100 - i,
62
+ 'createdAt': '2023-01-01T00:00:00.000Z'
63
+ })
64
+ return spaces
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
+
76
  return f"https://{owner}-{name}.hf.space"
77
 
78
  # Get space details
79
+ def get_space_details(space_data, index, offset):
80
  try:
81
  # 공통 정보 추출
82
  if '/' in space_data.get('id', ''):
 
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',
121
+ 'title': 'Error Loading Space',
122
+ 'owner': 'huggingface',
123
+ 'name': 'error',
124
+ 'likes_count': 0,
125
+ 'tags': [],
126
+ 'rank': offset + index + 1
127
+ }
128
 
129
  # Homepage route
130
  @app.route('/')
 
135
  @app.route('/api/trending-spaces', methods=['GET'])
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)
143
 
144
  # Process and filter spaces
145
  results = []
146
+ for index, space_data in enumerate(spaces_data['spaces']):
147
+ space_info = get_space_details(space_data, index, offset)
148
 
149
  if not space_info:
150
  continue
 
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__':
175
  # Create templates folder
 
179
  with open('templates/index.html', 'w', encoding='utf-8') as f:
180
  f.write('''
181
  <!DOCTYPE html>
182
+ <html lang="en">
183
  <head>
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
+
190
+ :root {
191
+ --pastel-pink: #FFD6E0;
192
+ --pastel-blue: #C5E8FF;
193
+ --pastel-purple: #E0C3FC;
194
+ --pastel-yellow: #FFF2CC;
195
+ --pastel-green: #C7F5D9;
196
+ --pastel-orange: #FFE0C3;
197
+
198
+ --mac-window-bg: rgba(250, 250, 250, 0.85);
199
+ --mac-toolbar: #F5F5F7;
200
+ --mac-border: #E2E2E2;
201
+ --mac-button-red: #FF5F56;
202
+ --mac-button-yellow: #FFBD2E;
203
+ --mac-button-green: #27C93F;
204
+
205
+ --text-primary: #333;
206
+ --text-secondary: #666;
207
+ --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
208
+ }
209
+
210
+ * {
211
  margin: 0;
212
  padding: 0;
213
+ box-sizing: border-box;
214
+ }
215
+
216
+ body {
217
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
218
+ line-height: 1.6;
219
+ color: var(--text-primary);
220
+ background-color: #f8f9fa;
221
+ background-image: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
222
+ min-height: 100vh;
223
+ padding: 2rem;
224
  }
225
 
226
  .container {
227
+ max-width: 1600px;
228
  margin: 0 auto;
 
229
  }
230
 
231
+ /* Mac OS Window Styling */
232
+ .mac-window {
233
+ background-color: var(--mac-window-bg);
234
+ border-radius: 10px;
235
+ box-shadow: var(--box-shadow);
236
+ backdrop-filter: blur(10px);
237
+ overflow: hidden;
238
+ margin-bottom: 2rem;
239
+ border: 1px solid var(--mac-border);
240
+ }
241
+
242
+ .mac-toolbar {
243
+ display: flex;
244
+ align-items: center;
245
+ padding: 10px 15px;
246
+ background-color: var(--mac-toolbar);
247
+ border-bottom: 1px solid var(--mac-border);
248
+ }
249
+
250
+ .mac-buttons {
251
+ display: flex;
252
+ gap: 8px;
253
+ margin-right: 15px;
254
+ }
255
+
256
+ .mac-button {
257
+ width: 12px;
258
+ height: 12px;
259
+ border-radius: 50%;
260
+ cursor: default;
261
+ }
262
+
263
+ .mac-close {
264
+ background-color: var(--mac-button-red);
265
+ }
266
+
267
+ .mac-minimize {
268
+ background-color: var(--mac-button-yellow);
269
+ }
270
+
271
+ .mac-maximize {
272
+ background-color: var(--mac-button-green);
273
+ }
274
+
275
+ .mac-title {
276
+ flex-grow: 1;
277
+ text-align: center;
278
+ font-size: 0.9rem;
279
+ color: var(--text-secondary);
280
+ }
281
+
282
+ .mac-content {
283
+ padding: 20px;
284
+ }
285
+
286
+ /* Header Styling */
287
  .header {
 
 
 
 
 
288
  text-align: center;
289
+ margin-bottom: 1.5rem;
290
+ position: relative;
291
  }
292
 
293
  .header h1 {
294
+ font-size: 2.2rem;
295
+ font-weight: 700;
296
  margin: 0;
297
+ color: #2d3748;
298
+ letter-spacing: -0.5px;
299
  }
300
 
301
+ .header p {
302
+ color: var(--text-secondary);
303
+ margin-top: 0.5rem;
304
+ font-size: 1.1rem;
305
+ }
306
+
307
+ /* Controls Styling */
308
+ .search-bar {
309
  display: flex;
 
310
  align-items: center;
311
+ margin-bottom: 1.5rem;
312
+ background-color: white;
313
+ border-radius: 30px;
314
+ padding: 5px;
315
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
316
+ max-width: 600px;
317
+ margin-left: auto;
318
+ margin-right: auto;
319
  }
320
 
321
+ .search-bar input {
322
+ flex-grow: 1;
323
+ border: none;
324
+ padding: 12px 20px;
325
  font-size: 1rem;
326
+ outline: none;
327
+ background: transparent;
328
+ border-radius: 30px;
329
  }
330
 
331
+ .search-bar .refresh-btn {
332
+ background-color: var(--pastel-green);
333
+ color: #1a202c;
 
334
  border: none;
335
+ border-radius: 30px;
336
+ padding: 10px 20px;
337
  font-size: 1rem;
338
+ font-weight: 600;
339
+ cursor: pointer;
340
+ transition: all 0.2s;
341
  display: flex;
342
  align-items: center;
343
+ gap: 8px;
344
  }
345
 
346
+ .search-bar .refresh-btn:hover {
347
+ background-color: #9ee7c0;
348
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
349
  }
350
 
351
  .refresh-icon {
352
  display: inline-block;
353
  width: 16px;
354
  height: 16px;
355
+ border: 2px solid #1a202c;
356
  border-top-color: transparent;
357
  border-radius: 50%;
358
  animation: none;
 
367
  100% { transform: rotate(360deg); }
368
  }
369
 
370
+ /* Grid Styling */
371
  .grid-container {
372
  display: grid;
373
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
374
  gap: 1.5rem;
375
+ margin-bottom: 2rem;
376
  }
377
 
378
  .grid-item {
379
+ height: 500px;
380
+ position: relative;
381
  overflow: hidden;
382
+ transition: all 0.3s ease;
383
+ border-radius: 15px;
384
+ }
385
+
386
+ .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
387
+ .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
388
+ .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
389
+ .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
390
+ .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
391
+ .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
392
+
393
+ .grid-item:hover {
394
+ transform: translateY(-5px);
395
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
396
+ }
397
+
398
+ .grid-header {
399
+ padding: 15px;
400
  display: flex;
401
  flex-direction: column;
402
+ background-color: rgba(255, 255, 255, 0.7);
403
+ backdrop-filter: blur(5px);
404
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
405
  }
406
 
407
+ .grid-header-top {
 
 
 
408
  display: flex;
409
  justify-content: space-between;
410
  align-items: center;
411
+ margin-bottom: 8px;
412
  }
413
 
414
+ .rank-badge {
415
+ background-color: #1a202c;
416
+ color: white;
417
+ font-size: 0.8rem;
418
+ font-weight: 600;
419
+ padding: 4px 8px;
420
+ border-radius: 50px;
421
  }
422
 
423
  .grid-header h3 {
424
  margin: 0;
425
+ font-size: 1.2rem;
426
+ font-weight: 700;
 
427
  white-space: nowrap;
428
  overflow: hidden;
429
  text-overflow: ellipsis;
430
  }
431
 
432
+ .grid-meta {
 
 
 
 
 
 
433
  display: flex;
434
+ justify-content: space-between;
435
  align-items: center;
436
+ font-size: 0.9rem;
437
  }
438
 
439
+ .owner-info {
440
+ color: var(--text-secondary);
441
+ font-weight: 500;
 
 
442
  }
443
 
444
  .likes-counter {
445
  display: flex;
446
  align-items: center;
447
+ color: #e53e3e;
448
+ font-weight: 600;
449
  }
450
 
451
  .likes-counter span {
452
  margin-left: 4px;
453
  }
454
 
455
+ .grid-actions {
456
+ padding: 10px 15px;
457
+ text-align: right;
458
+ background-color: rgba(255, 255, 255, 0.7);
 
 
 
 
 
 
459
  backdrop-filter: blur(5px);
460
+ position: absolute;
461
+ bottom: 0;
462
+ left: 0;
463
+ right: 0;
464
+ z-index: 10;
465
+ display: flex;
466
+ justify-content: flex-end;
467
  }
468
 
469
+ .open-link {
470
+ text-decoration: none;
471
+ color: #2c5282;
472
+ font-weight: 600;
473
+ padding: 5px 10px;
474
+ border-radius: 5px;
475
+ transition: all 0.2s;
476
+ background-color: rgba(237, 242, 247, 0.8);
477
  }
478
 
479
+ .open-link:hover {
480
+ background-color: #e2e8f0;
481
+ }
482
+
483
+ .grid-content {
484
  position: absolute;
485
  top: 0;
486
  left: 0;
487
+ width: 100%;
488
+ height: 100%;
489
+ padding-top: 85px; /* Header height */
490
+ padding-bottom: 45px; /* Actions height */
491
+ }
492
+
493
+ .iframe-container {
494
+ width: 100%;
495
+ height: 100%;
496
+ overflow: hidden;
497
+ position: relative;
498
+ }
499
+
500
+ .grid-content iframe {
501
  width: 100%;
502
  height: 100%;
503
  border: none;
504
+ border-radius: 0;
505
  }
506
 
507
  .error-placeholder {
 
514
  flex-direction: column;
515
  justify-content: center;
516
  align-items: center;
 
 
 
517
  padding: 20px;
518
+ background-color: rgba(255, 255, 255, 0.9);
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 */
529
+ .pagination {
530
+ display: flex;
531
+ justify-content: center;
532
+ align-items: center;
533
+ gap: 10px;
534
+ margin: 2rem 0;
535
  }
536
 
537
+ .pagination-button {
538
+ background-color: white;
539
+ border: none;
540
+ padding: 10px 20px;
541
+ border-radius: 10px;
542
+ font-size: 1rem;
543
+ font-weight: 600;
544
+ cursor: pointer;
545
+ transition: all 0.2s;
546
+ color: var(--text-primary);
547
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
548
+ }
549
+
550
+ .pagination-button:hover {
551
+ background-color: #f8f9fa;
552
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
553
+ }
554
+
555
+ .pagination-button.active {
556
+ background-color: var(--pastel-purple);
557
+ color: #4a5568;
558
+ }
559
+
560
+ .pagination-button:disabled {
561
+ background-color: #edf2f7;
562
+ color: #a0aec0;
563
+ cursor: default;
564
+ box-shadow: none;
565
+ }
566
+
567
+ /* Loading Indicator */
568
  .loading {
569
  position: fixed;
570
  top: 0;
 
572
  right: 0;
573
  bottom: 0;
574
  background-color: rgba(255, 255, 255, 0.8);
575
+ backdrop-filter: blur(5px);
576
  display: flex;
577
  justify-content: center;
578
  align-items: center;
579
  z-index: 1000;
580
+ }
581
+
582
+ .loading-content {
583
+ text-align: center;
584
  }
585
 
586
  .loading-spinner {
587
+ width: 60px;
588
+ height: 60px;
589
+ border: 5px solid #e2e8f0;
590
+ border-top-color: var(--pastel-purple);
591
  border-radius: 50%;
 
 
592
  animation: spin 1s linear infinite;
593
+ margin: 0 auto 15px;
594
+ }
595
+
596
+ .loading-text {
597
+ font-size: 1.2rem;
598
+ font-weight: 600;
599
+ color: #4a5568;
600
+ }
601
+
602
+ .loading-error {
603
+ display: none;
604
+ margin-top: 10px;
605
+ color: #e53e3e;
606
+ font-size: 0.9rem;
607
  }
608
 
609
+ /* Responsive Design */
610
  @media (max-width: 768px) {
611
+ body {
612
+ padding: 1rem;
613
+ }
614
+
615
+ .grid-container {
616
+ grid-template-columns: 1fr;
617
+ }
618
+
619
+ .search-bar {
620
  flex-direction: column;
621
+ padding: 10px;
622
  }
623
 
624
+ .search-bar input {
 
625
  width: 100%;
626
+ margin-bottom: 10px;
627
  }
628
 
629
+ .search-bar .refresh-btn {
630
+ width: 100%;
631
+ justify-content: center;
632
  }
633
 
634
+ .pagination {
635
+ flex-wrap: wrap;
636
  }
637
  }
638
  </style>
639
  </head>
640
  <body>
641
  <div class="container">
642
+ <div class="mac-window">
643
+ <div class="mac-toolbar">
644
+ <div class="mac-buttons">
645
+ <div class="mac-button mac-close"></div>
646
+ <div class="mac-button mac-minimize"></div>
647
+ <div class="mac-button mac-maximize"></div>
648
+ </div>
649
+ <div class="mac-title">Huggingface Explorer</div>
650
+ </div>
651
+
652
+ <div class="mac-content">
653
+ <div class="header">
654
+ <h1>HF Space 'Top Rank' Gallery</h1>
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">
661
+ <span class="refresh-icon"></span>
662
+ Refresh
663
+ </button>
664
+ </div>
665
+
666
+ <div id="gridContainer" class="grid-container"></div>
667
+
668
+ <div id="pagination" class="pagination">
669
+ <!-- Pagination buttons will be dynamically created by JavaScript -->
670
+ </div>
671
+ </div>
672
  </div>
 
 
673
  </div>
674
 
675
  <div id="loadingIndicator" class="loading">
676
+ <div class="loading-content">
677
+ <div class="loading-spinner"></div>
678
+ <div class="loading-text">Loading amazing spaces...</div>
679
+ <div id="loadingError" class="loading-error">
680
+ If this takes too long, try refreshing the page.
681
+ </div>
682
+ </div>
683
  </div>
684
 
685
  <script>
 
687
  const elements = {
688
  gridContainer: document.getElementById('gridContainer'),
689
  loadingIndicator: document.getElementById('loadingIndicator'),
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
697
  const state = {
698
  isLoading: false,
699
+ spaces: [],
700
+ currentPage: 0,
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
 
711
 
712
  if (isLoading) {
713
  elements.refreshButton.classList.add('refreshing');
714
+ // Show error message if loading takes too long
715
+ clearTimeout(state.loadingTimeout);
716
+ state.loadingTimeout = setTimeout(() => {
717
+ elements.loadingError.style.display = 'block';
718
+ }, 10000); // Show error message after 10 seconds
719
  } else {
720
  elements.refreshButton.classList.remove('refreshing');
721
+ clearTimeout(state.loadingTimeout);
722
+ elements.loadingError.style.display = 'none';
723
  }
724
  }
725
 
 
727
  async function handleApiResponse(response) {
728
  if (!response.ok) {
729
  const errorText = await response.text();
730
+ throw new Error(`API Error (${response.status}): ${errorText}`);
731
  }
732
  return response.json();
733
  }
734
 
735
+ // Create direct URL function with fixes for static sites
736
  function createDirectUrl(owner, name) {
737
+ try {
738
+ // 1. Replace '.' characters with '-'
739
+ name = name.replace(/\./g, '-');
740
+ // 2. Replace '_' characters with '-'
741
+ name = name.replace(/_/g, '-');
742
+ // 3. Convert everything to lowercase
743
+ owner = owner.toLowerCase();
744
+ name = name.toLowerCase();
745
+
746
+ return `https://${owner}-${name}.hf.space`;
747
+ } catch (error) {
748
+ console.error('URL creation error:', error);
749
+ return 'https://huggingface.co';
750
+ }
751
  }
752
 
753
+ // Load spaces with timeout
754
+ async function loadSpaces(page = 0) {
755
  setLoading(true);
756
 
757
  try {
758
  const searchText = elements.searchInput.value;
759
+ const offset = page * state.itemsPerPage;
760
+
761
+ // Set timeout (30 seconds)
762
+ const timeoutPromise = new Promise((_, reject) =>
763
+ setTimeout(() => reject(new Error('Request timeout')), 30000)
764
+ );
765
+
766
+ const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
767
 
768
+ // Use the first Promise that completes
769
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
770
+ const data = await handleApiResponse(response);
771
 
772
+ // Update state on successful load
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
+
782
+ // Show empty grid with error message
783
+ elements.gridContainer.innerHTML = `
784
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
785
+ <div style="font-size: 3rem; margin-bottom: 20px;">⚠️</div>
786
+ <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
787
+ <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
788
+ <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
789
+ Try Again
790
+ </button>
791
+ </div>
792
+ `;
793
+
794
+ // Add event listener to retry button
795
+ document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
796
+
797
+ // Render simple pagination
798
+ renderPagination();
799
  } finally {
800
  setLoading(false);
801
  }
802
  }
803
 
804
+ // Render pagination
805
+ function renderPagination() {
806
+ elements.pagination.innerHTML = '';
807
+
808
+ const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
809
+
810
+ // Previous page button
811
+ const prevButton = document.createElement('button');
812
+ prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`;
813
+ prevButton.textContent = 'Previous';
814
+ prevButton.disabled = state.currentPage === 0;
815
+ prevButton.addEventListener('click', () => {
816
+ if (state.currentPage > 0) {
817
+ loadSpaces(state.currentPage - 1);
818
+ }
819
+ });
820
+ elements.pagination.appendChild(prevButton);
821
+
822
+ // Page buttons (maximum of 7)
823
+ const maxButtons = 7;
824
+ let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
825
+ let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
826
+
827
+ // Adjust start page if the end page is less than maximum buttons
828
+ if (endPage - startPage + 1 < maxButtons) {
829
+ startPage = Math.max(0, endPage - maxButtons + 1);
830
+ }
831
+
832
+ for (let i = startPage; i <= endPage; i++) {
833
+ const pageButton = document.createElement('button');
834
+ pageButton.className = `pagination-button ${i === state.currentPage ? 'active' : ''}`;
835
+ pageButton.textContent = i + 1;
836
+ pageButton.addEventListener('click', () => {
837
+ if (i !== state.currentPage) {
838
+ loadSpaces(i);
839
+ }
840
+ });
841
+ elements.pagination.appendChild(pageButton);
842
+ }
843
+
844
+ // Next page button
845
+ const nextButton = document.createElement('button');
846
+ nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`;
847
+ nextButton.textContent = 'Next';
848
+ nextButton.disabled = state.currentPage >= totalPages - 1;
849
+ nextButton.addEventListener('click', () => {
850
+ if (state.currentPage < totalPages - 1) {
851
+ loadSpaces(state.currentPage + 1);
852
+ }
853
+ });
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
 
861
+ // Error message container
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');
873
+ errorMessage.textContent = `"${title}" space couldn't be loaded`;
874
  errorPlaceholder.appendChild(errorMessage);
875
 
876
+ // Try static site version button
877
+ const directStaticLink = document.createElement('a');
878
+ directStaticLink.href = `https://${owner}-${name}.hf.space/index.html`;
879
+ directStaticLink.target = '_blank';
880
+ directStaticLink.textContent = 'Try Static Version';
881
+ directStaticLink.style.color = '#3182ce';
882
+ directStaticLink.style.marginTop = '10px';
883
+ directStaticLink.style.display = 'inline-block';
884
+ directStaticLink.style.padding = '8px 16px';
885
+ directStaticLink.style.background = '#ebf8ff';
886
+ directStaticLink.style.borderRadius = '5px';
887
+ directStaticLink.style.fontWeight = '600';
888
+ directStaticLink.style.marginRight = '10px';
889
+ errorPlaceholder.appendChild(directStaticLink);
890
+
891
+ // Direct HF link
892
  const directLink = document.createElement('a');
893
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
894
  directLink.target = '_blank';
895
+ directLink.textContent = 'Visit HF Space';
896
+ directLink.style.color = '#3182ce';
897
  directLink.style.marginTop = '10px';
898
+ directLink.style.display = 'inline-block';
899
+ directLink.style.padding = '8px 16px';
900
+ directLink.style.background = '#ebf8ff';
901
+ directLink.style.borderRadius = '5px';
902
+ directLink.style.fontWeight = '600';
903
  errorPlaceholder.appendChild(directLink);
904
 
905
+ // Hide iframe and show error message
906
  iframe.style.display = 'none';
907
  container.appendChild(errorPlaceholder);
908
  }
 
913
 
914
  if (!spaces || spaces.length === 0) {
915
  const noResultsMsg = document.createElement('p');
916
+ noResultsMsg.textContent = 'No spaces found matching your search.';
917
+ noResultsMsg.style.padding = '2rem';
918
+ noResultsMsg.style.textAlign = 'center';
919
  noResultsMsg.style.fontStyle = 'italic';
920
+ noResultsMsg.style.color = '#718096';
921
  elements.gridContainer.appendChild(noResultsMsg);
922
  return;
923
  }
924
 
925
+ spaces.forEach((item) => {
926
+ try {
927
+ const { url, title, likes_count, owner, name, rank } = item;
928
+
929
+ // Skip if owner is 'None'
930
+ if (owner === 'None') {
931
+ return;
932
+ }
933
+
934
+ // Create grid item - Apply rotating pastel colors
935
+ const gridItem = document.createElement('div');
936
+ gridItem.className = 'grid-item';
937
+
938
+ // Header
939
+ const header = document.createElement('div');
940
+ header.className = 'grid-header';
941
+
942
+ // Header top part with rank
943
+ const headerTop = document.createElement('div');
944
+ headerTop.className = 'grid-header-top';
945
+
946
+ // Title
947
+ const titleEl = document.createElement('h3');
948
+ titleEl.textContent = title;
949
+ titleEl.title = title; // For tooltip on hover
950
+ headerTop.appendChild(titleEl);
951
+
952
+ // Rank badge
953
+ const rankBadge = document.createElement('div');
954
+ rankBadge.className = 'rank-badge';
955
+ rankBadge.textContent = `#${rank}`;
956
+ headerTop.appendChild(rankBadge);
957
+
958
+ header.appendChild(headerTop);
959
+
960
+ // Grid meta info
961
+ const metaInfo = document.createElement('div');
962
+ metaInfo.className = 'grid-meta';
963
+
964
+ // Owner info
965
+ const ownerEl = document.createElement('div');
966
+ ownerEl.className = 'owner-info';
967
+ ownerEl.textContent = `by ${owner}`;
968
+ metaInfo.appendChild(ownerEl);
969
+
970
+ // Likes counter
971
+ const likesCounter = document.createElement('div');
972
+ likesCounter.className = 'likes-counter';
973
+ likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>';
974
+ metaInfo.appendChild(likesCounter);
975
+
976
+ header.appendChild(metaInfo);
977
+
978
+ // Add header to grid item
979
+ gridItem.appendChild(header);
980
+
981
+ // Content area
982
+ const content = document.createElement('div');
983
+ content.className = 'grid-content';
984
+
985
+ // iframe container
986
+ const iframeContainer = document.createElement('div');
987
+ iframeContainer.className = 'iframe-container';
988
+
989
+ // Create iframe to display the content
990
+ const iframe = document.createElement('iframe');
991
+ const directUrl = createDirectUrl(owner, name);
992
+ iframe.src = directUrl;
993
+ iframe.title = title;
994
+ // Remove microphone permission
995
+ iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
996
+ iframe.setAttribute('allowfullscreen', '');
997
+ iframe.setAttribute('frameborder', '0');
998
+ iframe.loading = 'lazy'; // Lazy load iframes for better performance
999
+
1000
+ // Track this space
1001
+ const spaceKey = `${owner}/${name}`;
1002
+ state.staticModeAttempted[spaceKey] = false;
1003
+
1004
+ // Handle iframe loading errors
1005
+ iframe.onerror = function() {
1006
+ if (!state.staticModeAttempted[spaceKey]) {
1007
+ // Try static mode
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
+ };
1015
+
1016
+ // Advanced error handling for iframe load
1017
+ iframe.onload = function() {
1018
+ try {
1019
+ // Try to access iframe content to check if it loaded properly
1020
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1021
+ // Check if we got a 404 page or other error by looking for certain elements
1022
+ const isErrorPage = iframeDoc.title.includes('404') ||
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
1036
+ // If we need to check for static mode, we do it based on other signals
1037
+
1038
+ // We can try detecting failed loads by using a timer and checking if the iframe content is visible
1039
+ setTimeout(() => {
1040
+ // This is a basic heuristic - if the iframe still has no visible content after 5s, try static mode
1041
+ if (!state.staticModeAttempted[spaceKey] &&
1042
+ (iframe.clientHeight < 10 || iframe.clientWidth < 10)) {
1043
+ state.staticModeAttempted[spaceKey] = true;
1044
+ iframe.src = directUrl + '/index.html';
1045
+ }
1046
+ }, 5000);
1047
+ }
1048
+ };
1049
+
1050
+ iframeContainer.appendChild(iframe);
1051
+ content.appendChild(iframeContainer);
1052
+
1053
+ // Actions section at bottom
1054
+ const actions = document.createElement('div');
1055
+ actions.className = 'grid-actions';
1056
+
1057
+ // Open link
1058
+ const linkEl = document.createElement('a');
1059
+ linkEl.href = url;
1060
+ linkEl.target = '_blank';
1061
+ linkEl.className = 'open-link';
1062
+ linkEl.textContent = 'Open in new window';
1063
+ actions.appendChild(linkEl);
1064
+
1065
+ // Add content and actions to grid item
1066
+ gridItem.appendChild(content);
1067
+ gridItem.appendChild(actions);
1068
+
1069
+ // Add grid item to container
1070
+ elements.gridContainer.appendChild(gridItem);
1071
+ } catch (error) {
1072
+ console.error('Item rendering error:', error);
1073
+ // Continue rendering other items even if one fails
1074
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  });
1076
  }
1077
 
 
1079
  elements.searchInput.addEventListener('input', () => {
1080
  // Debounce input to prevent API calls on every keystroke
1081
  clearTimeout(state.searchTimeout);
1082
+ state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1083
+ });
1084
+
1085
+ // Enter key in search box
1086
+ elements.searchInput.addEventListener('keyup', (event) => {
1087
+ if (event.key === 'Enter') {
1088
+ loadSpaces(0);
1089
+ }
1090
  });
1091
 
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) {
1098
+ e.preventDefault();
1099
+ // Mac buttons don't do anything, just for style
1100
+ });
1101
+ });
1102
+
1103
+ // Page load complete event detection
1104
+ window.addEventListener('load', function() {
1105
+ // Start loading data when page is fully loaded
1106
+ setTimeout(() => loadSpaces(0), 500);
1107
+ });
1108
+
1109
+ // Safety mechanism to prevent infinite loading
1110
+ setTimeout(() => {
1111
+ if (state.isLoading) {
1112
+ setLoading(false);
1113
+ elements.gridContainer.innerHTML = `
1114
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1115
+ <div style="font-size: 3rem; margin-bottom: 20px;">⏱️</div>
1116
+ <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
1117
+ <p style="color: #666;">Please try refreshing the page.</p>
1118
+ <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1119
+ Reload Page
1120
+ </button>
1121
+ </div>
1122
+ `;
1123
+ }
1124
+ }, 20000); // Force end loading state after 20 seconds
1125
 
1126
+ // Start loading immediately - dual call with window.load for reliability
1127
+ loadSpaces(0);
1128
  </script>
1129
  </body>
1130
  </html>