openfree commited on
Commit
3e83b63
·
verified ·
1 Parent(s): 24f5139

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1514
app.py DELETED
@@ -1,1514 +0,0 @@
1
- from flask import Flask, render_template, request, jsonify
2
- import requests
3
- import os
4
- import time
5
- import random
6
- from collections import Counter
7
-
8
- app = Flask(__name__)
9
-
10
- # Generate dummy spaces in case of error
11
- def generate_dummy_spaces(count):
12
- """
13
- API 호출 실패 시 예시용 더미 스페이스 생성
14
- """
15
- spaces = []
16
- for i in range(count):
17
- spaces.append({
18
- 'id': f'dummy/space-{i}',
19
- 'owner': 'dummy',
20
- 'title': f'Example Space {i+1}',
21
- 'description': 'Dummy space for fallback',
22
- 'likes': 100 - i,
23
- 'createdAt': '2023-01-01T00:00:00.000Z',
24
- 'hardware': 'cpu',
25
- 'user': {
26
- 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
27
- 'name': 'dummyUser'
28
- }
29
- })
30
- return spaces
31
-
32
- # Function to fetch Zero-GPU (CPU-based) Spaces, TRENDING order, top 500
33
- def fetch_trending_spaces(offset=0, limit=72):
34
- """
35
- 1) hardware=cpu
36
- 2) sort=trending
37
- 3) 상위 500개만 남김 (그 이후 offset/limit 페이징)
38
- """
39
- try:
40
- url = "https://huggingface.co/api/spaces"
41
- params = {
42
- "limit": 10000, # 일단 많이 가져오기
43
- "hardware": "cpu",
44
- "sort": "trending" # 트렌딩 정렬
45
- }
46
- response = requests.get(url, params=params, timeout=30)
47
-
48
- if response.status_code == 200:
49
- spaces = response.json()
50
-
51
- # owner나 id가 'None'인 경우 제외
52
- filtered_spaces = [
53
- space for space in spaces
54
- if space.get('owner') != 'None'
55
- and space.get('id', '').split('/', 1)[0] != 'None'
56
- ]
57
-
58
- # 트렌딩 중 상위 500개만 남김
59
- if len(filtered_spaces) > 500:
60
- filtered_spaces = filtered_spaces[:500]
61
-
62
- total = len(filtered_spaces)
63
-
64
- # 페이징
65
- start = min(offset, total)
66
- end = min(offset + limit, total)
67
-
68
- print(f"[fetch_trending_spaces] Zero-GPU TRENDING Spaces: {total}개 중, "
69
- f"{start}~{end-1} 반환")
70
-
71
- return {
72
- 'spaces': filtered_spaces[start:end],
73
- 'total': total,
74
- 'offset': offset,
75
- 'limit': limit,
76
- 'all_spaces': filtered_spaces # 상위 500 전체
77
- }
78
- else:
79
- print(f"Error fetching spaces: {response.status_code}")
80
- # 실패 시 더미 데이터 생성
81
- return {
82
- 'spaces': generate_dummy_spaces(limit),
83
- 'total': 200,
84
- 'offset': offset,
85
- 'limit': limit,
86
- 'all_spaces': generate_dummy_spaces(500)
87
- }
88
-
89
- except Exception as e:
90
- print(f"Exception when fetching spaces: {e}")
91
- return {
92
- 'spaces': generate_dummy_spaces(limit),
93
- 'total': 200,
94
- 'offset': offset,
95
- 'limit': limit,
96
- 'all_spaces': generate_dummy_spaces(500)
97
- }
98
-
99
- # Transform Huggingface URL to direct space URL
100
- def transform_url(owner, name):
101
- """
102
- Hugging Face Space -> 서브도메인 접근 URL
103
- 예) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
104
- """
105
- name = name.replace('.', '-').replace('_', '-')
106
- owner = owner.lower()
107
- name = name.lower()
108
- return f"https://{owner}-{name}.hf.space"
109
-
110
- # Get space details
111
- def get_space_details(space_data, index, offset):
112
- """
113
- space의 주요 정보(타이틀, likes, owner, url 등) 추출
114
- """
115
- try:
116
- if '/' in space_data.get('id', ''):
117
- owner, name = space_data.get('id', '').split('/', 1)
118
- else:
119
- owner = space_data.get('owner', '')
120
- name = space_data.get('id', '')
121
-
122
- if owner == 'None' or name == 'None':
123
- return None
124
-
125
- original_url = f"https://huggingface.co/spaces/{owner}/{name}"
126
- embed_url = transform_url(owner, name)
127
-
128
- likes_count = space_data.get('likes', 0)
129
- title = space_data.get('title') or name
130
- short_desc = space_data.get('description', '')
131
-
132
- user_info = space_data.get('user', {})
133
- avatar_url = user_info.get('avatar_url', '')
134
- author_name = user_info.get('name') or owner
135
-
136
- return {
137
- 'url': original_url,
138
- 'embedUrl': embed_url,
139
- 'title': title,
140
- 'owner': owner,
141
- 'name': name,
142
- 'likes_count': likes_count,
143
- 'description': short_desc,
144
- 'avatar_url': avatar_url,
145
- 'author_name': author_name,
146
- # 'rank'는 (offset + index + 1)로 표시
147
- 'rank': offset + index + 1
148
- }
149
- except Exception as e:
150
- print(f"Error processing space data: {e}")
151
- return {
152
- 'url': 'https://huggingface.co/spaces',
153
- 'embedUrl': 'https://huggingface.co/spaces',
154
- 'title': 'Error Loading Space',
155
- 'owner': 'huggingface',
156
- 'name': 'error',
157
- 'likes_count': 0,
158
- 'description': '',
159
- 'avatar_url': '',
160
- 'author_name': 'huggingface',
161
- 'rank': offset + index + 1
162
- }
163
-
164
- # Get owner statistics from all spaces
165
- def get_owner_stats(all_spaces):
166
- """
167
- all_spaces: 상위 500개 트렌딩 CPU 스페이스
168
- -> owner별로 몇 개인지 세서 상위 30명 반환
169
- """
170
- owners = []
171
- for space in all_spaces:
172
- if '/' in space.get('id', ''):
173
- owner, _ = space.get('id', '').split('/', 1)
174
- else:
175
- owner = space.get('owner', '')
176
-
177
- if owner != 'None':
178
- owners.append(owner)
179
-
180
- owner_counts = Counter(owners)
181
- top_owners = owner_counts.most_common(30)
182
- return top_owners
183
-
184
- from flask import render_template
185
-
186
- @app.route('/')
187
- def home():
188
- return render_template('index.html')
189
-
190
- @app.route('/api/trending-spaces', methods=['GET'])
191
- def trending_spaces():
192
- """
193
- 1) 상위 500 트렌딩 CPU 스페이스
194
- 2) offset, limit으로 페이지 나눠서 반환
195
- 3) top 30 creator stats
196
- """
197
- search_query = request.args.get('search', '').lower()
198
- offset = int(request.args.get('offset', 0))
199
- limit = int(request.args.get('limit', 72))
200
-
201
- # 500개 (CPU+trending) 받아오기
202
- spaces_data = fetch_trending_spaces(offset, limit)
203
-
204
- results = []
205
- for index, space_data in enumerate(spaces_data['spaces']):
206
- space_info = get_space_details(space_data, index, offset)
207
- if not space_info:
208
- continue
209
-
210
- # 검색 필터
211
- if search_query:
212
- if (search_query not in space_info['title'].lower()
213
- and search_query not in space_info['owner'].lower()
214
- and search_query not in space_info['url'].lower()
215
- and search_query not in space_info['description'].lower()):
216
- continue
217
-
218
- results.append(space_info)
219
-
220
- top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
221
-
222
- return jsonify({
223
- 'spaces': results,
224
- 'total': spaces_data['total'],
225
- 'offset': offset,
226
- 'limit': limit,
227
- 'top_owners': top_owners
228
- })
229
-
230
- if __name__ == '__main__':
231
- """
232
- Flask 실행 시, templates/index.html 생성
233
- """
234
- os.makedirs('templates', exist_ok=True)
235
-
236
- with open('templates/index.html', 'w', encoding='utf-8') as f:
237
- f.write('''<!DOCTYPE html>
238
- <html lang="en">
239
- <head>
240
- <meta charset="UTF-8">
241
- <title>Zero-GPU Trending (Top 500)</title>
242
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
243
- <style>
244
- /* Google Fonts & Base Styling */
245
- @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
246
-
247
- :root {
248
- --pastel-pink: #FFD6E0;
249
- --pastel-blue: #C5E8FF;
250
- --pastel-purple: #E0C3FC;
251
- --pastel-yellow: #FFF2CC;
252
- --pastel-green: #C7F5D9;
253
- --pastel-orange: #FFE0C3;
254
-
255
- --mac-window-bg: rgba(250, 250, 250, 0.85);
256
- --mac-toolbar: #F5F5F7;
257
- --mac-border: #E2E2E2;
258
- --mac-button-red: #FF5F56;
259
- --mac-button-yellow: #FFBD2E;
260
- --mac-button-green: #27C93F;
261
-
262
- --text-primary: #333;
263
- --text-secondary: #666;
264
- --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
265
- }
266
-
267
- * {
268
- margin: 0;
269
- padding: 0;
270
- box-sizing: border-box;
271
- }
272
-
273
- body {
274
- font-family: 'Nunito', sans-serif;
275
- line-height: 1.6;
276
- color: var(--text-primary);
277
- background-color: #f8f9fa;
278
- background-image: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
279
- min-height: 100vh;
280
- padding: 2rem;
281
- }
282
-
283
- .container {
284
- max-width: 1600px;
285
- margin: 0 auto;
286
- }
287
-
288
- /* Mac OS Window Styling */
289
- .mac-window {
290
- background-color: var(--mac-window-bg);
291
- border-radius: 10px;
292
- box-shadow: var(--box-shadow);
293
- backdrop-filter: blur(10px);
294
- overflow: hidden;
295
- margin-bottom: 2rem;
296
- border: 1px solid var(--mac-border);
297
- }
298
-
299
- .mac-toolbar {
300
- display: flex;
301
- align-items: center;
302
- padding: 10px 15px;
303
- background-color: var(--mac-toolbar);
304
- border-bottom: 1px solid var(--mac-border);
305
- }
306
-
307
- .mac-buttons {
308
- display: flex;
309
- gap: 8px;
310
- margin-right: 15px;
311
- }
312
-
313
- .mac-button {
314
- width: 12px;
315
- height: 12px;
316
- border-radius: 50%;
317
- cursor: default;
318
- }
319
-
320
- .mac-close {
321
- background-color: var(--mac-button-red);
322
- }
323
-
324
- .mac-minimize {
325
- background-color: var(--mac-button-yellow);
326
- }
327
-
328
- .mac-maximize {
329
- background-color: var(--mac-button-green);
330
- }
331
-
332
- .mac-title {
333
- flex-grow: 1;
334
- text-align: center;
335
- font-size: 0.9rem;
336
- color: var(--text-secondary);
337
- }
338
-
339
- .mac-content {
340
- padding: 20px;
341
- }
342
-
343
- /* Header Styling */
344
- .header {
345
- text-align: center;
346
- margin-bottom: 1.5rem;
347
- position: relative;
348
- }
349
-
350
- .header h1 {
351
- font-size: 2.2rem;
352
- font-weight: 700;
353
- margin: 0;
354
- color: #2d3748;
355
- letter-spacing: -0.5px;
356
- }
357
-
358
- .header p {
359
- color: var(--text-secondary);
360
- margin-top: 0.5rem;
361
- font-size: 1.1rem;
362
- }
363
-
364
- /* Tabs Styling */
365
- .tab-nav {
366
- display: flex;
367
- justify-content: center;
368
- margin-bottom: 1.5rem;
369
- }
370
-
371
- .tab-button {
372
- border: none;
373
- background-color: #edf2f7;
374
- color: var(--text-primary);
375
- padding: 10px 20px;
376
- margin: 0 5px;
377
- cursor: pointer;
378
- border-radius: 5px;
379
- font-size: 1rem;
380
- font-weight: 600;
381
- }
382
-
383
- .tab-button.active {
384
- background-color: var(--pastel-purple);
385
- color: #fff;
386
- }
387
-
388
- .tab-content {
389
- display: none;
390
- }
391
-
392
- .tab-content.active {
393
- display: block;
394
- }
395
-
396
- /* Controls Styling */
397
- .search-bar {
398
- display: flex;
399
- align-items: center;
400
- margin-bottom: 1.5rem;
401
- background-color: white;
402
- border-radius: 30px;
403
- padding: 5px;
404
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
405
- max-width: 600px;
406
- margin-left: auto;
407
- margin-right: auto;
408
- }
409
-
410
- .search-bar input {
411
- flex-grow: 1;
412
- border: none;
413
- padding: 12px 20px;
414
- font-size: 1rem;
415
- outline: none;
416
- background: transparent;
417
- border-radius: 30px;
418
- }
419
-
420
- .search-bar .refresh-btn {
421
- background-color: var(--pastel-green);
422
- color: #1a202c;
423
- border: none;
424
- border-radius: 30px;
425
- padding: 10px 20px;
426
- font-size: 1rem;
427
- font-weight: 600;
428
- cursor: pointer;
429
- transition: all 0.2s;
430
- display: flex;
431
- align-items: center;
432
- gap: 8px;
433
- }
434
-
435
- .search-bar .refresh-btn:hover {
436
- background-color: #9ee7c0;
437
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
438
- }
439
-
440
- .refresh-icon {
441
- display: inline-block;
442
- width: 16px;
443
- height: 16px;
444
- border: 2px solid #1a202c;
445
- border-top-color: transparent;
446
- border-radius: 50%;
447
- animation: none;
448
- }
449
-
450
- .refreshing .refresh-icon {
451
- animation: spin 1s linear infinite;
452
- }
453
-
454
- @keyframes spin {
455
- 0% { transform: rotate(0deg); }
456
- 100% { transform: rotate(360deg); }
457
- }
458
-
459
- /* Grid Styling */
460
- .grid-container {
461
- display: grid;
462
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
463
- gap: 1.5rem;
464
- margin-bottom: 2rem;
465
- }
466
-
467
- .grid-item {
468
- height: 500px;
469
- position: relative;
470
- overflow: hidden;
471
- transition: all 0.3s ease;
472
- border-radius: 15px;
473
- }
474
-
475
- .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
476
- .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
477
- .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
478
- .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
479
- .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
480
- .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
481
-
482
- .grid-item:hover {
483
- transform: translateY(-5px);
484
- box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
485
- }
486
-
487
- .grid-header {
488
- padding: 15px;
489
- display: flex;
490
- flex-direction: column;
491
- background-color: rgba(255, 255, 255, 0.7);
492
- backdrop-filter: blur(5px);
493
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
494
- }
495
-
496
- .grid-header-top {
497
- display: flex;
498
- justify-content: space-between;
499
- align-items: center;
500
- margin-bottom: 8px;
501
- }
502
-
503
- .rank-badge {
504
- background-color: #1a202c;
505
- color: white;
506
- font-size: 0.8rem;
507
- font-weight: 600;
508
- padding: 4px 8px;
509
- border-radius: 50px;
510
- display: inline-block;
511
- }
512
-
513
- .grid-header h3 {
514
- margin: 0;
515
- font-size: 1.2rem;
516
- font-weight: 700;
517
- white-space: nowrap;
518
- overflow: hidden;
519
- text-overflow: ellipsis;
520
- }
521
-
522
- .grid-meta {
523
- display: flex;
524
- justify-content: space-between;
525
- align-items: center;
526
- font-size: 0.9rem;
527
- }
528
-
529
- .owner-info {
530
- color: var(--text-secondary);
531
- font-weight: 500;
532
- }
533
-
534
- .likes-counter {
535
- display: flex;
536
- align-items: center;
537
- color: #e53e3e;
538
- font-weight: 600;
539
- }
540
-
541
- .likes-counter span {
542
- margin-left: 4px;
543
- }
544
-
545
- .grid-actions {
546
- padding: 10px 15px;
547
- text-align: right;
548
- background-color: rgba(255, 255, 255, 0.7);
549
- backdrop-filter: blur(5px);
550
- position: absolute;
551
- bottom: 0;
552
- left: 0;
553
- right: 0;
554
- z-index: 10;
555
- display: flex;
556
- justify-content: flex-end;
557
- }
558
-
559
- .open-link {
560
- text-decoration: none;
561
- color: #2c5282;
562
- font-weight: 600;
563
- padding: 5px 10px;
564
- border-radius: 5px;
565
- transition: all 0.2s;
566
- background-color: rgba(237, 242, 247, 0.8);
567
- }
568
-
569
- .open-link:hover {
570
- background-color: #e2e8f0;
571
- }
572
-
573
- .grid-content {
574
- position: absolute;
575
- top: 0;
576
- left: 0;
577
- width: 100%;
578
- height: 100%;
579
- padding-top: 85px; /* Header height */
580
- padding-bottom: 45px; /* Actions height */
581
- }
582
-
583
- .iframe-container {
584
- width: 100%;
585
- height: 100%;
586
- overflow: hidden;
587
- position: relative;
588
- }
589
-
590
- /* Apply 70% scaling to iframes */
591
- .grid-content iframe {
592
- transform: scale(0.7);
593
- transform-origin: top left;
594
- width: 142.857%;
595
- height: 142.857%;
596
- border: none;
597
- border-radius: 0;
598
- }
599
-
600
- .error-placeholder {
601
- position: absolute;
602
- top: 0;
603
- left: 0;
604
- width: 100%;
605
- height: 100%;
606
- display: flex;
607
- flex-direction: column;
608
- justify-content: center;
609
- align-items: center;
610
- padding: 20px;
611
- background-color: rgba(255, 255, 255, 0.9);
612
- text-align: center;
613
- }
614
-
615
- .error-emoji {
616
- font-size: 6rem;
617
- margin-bottom: 1.5rem;
618
- animation: bounce 1s infinite alternate;
619
- text-shadow: 0 10px 20px rgba(0,0,0,0.1);
620
- }
621
-
622
- @keyframes bounce {
623
- from {
624
- transform: translateY(0px) scale(1);
625
- }
626
- to {
627
- transform: translateY(-15px) scale(1.1);
628
- }
629
- }
630
-
631
- /* Pagination Styling */
632
- .pagination {
633
- display: flex;
634
- justify-content: center;
635
- align-items: center;
636
- gap: 10px;
637
- margin: 2rem 0;
638
- }
639
-
640
- .pagination-button {
641
- background-color: white;
642
- border: none;
643
- padding: 10px 20px;
644
- border-radius: 10px;
645
- font-size: 1rem;
646
- font-weight: 600;
647
- cursor: pointer;
648
- transition: all 0.2s;
649
- color: var(--text-primary);
650
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
651
- }
652
-
653
- .pagination-button:hover {
654
- background-color: #f8f9fa;
655
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
656
- }
657
-
658
- .pagination-button.active {
659
- background-color: var(--pastel-purple);
660
- color: #4a5568;
661
- }
662
-
663
- .pagination-button:disabled {
664
- background-color: #edf2f7;
665
- color: #a0aec0;
666
- cursor: default;
667
- box-shadow: none;
668
- }
669
-
670
- /* Loading Indicator */
671
- .loading {
672
- position: fixed;
673
- top: 0;
674
- left: 0;
675
- right: 0;
676
- bottom: 0;
677
- background-color: rgba(255, 255, 255, 0.8);
678
- backdrop-filter: blur(5px);
679
- display: flex;
680
- justify-content: center;
681
- align-items: center;
682
- z-index: 1000;
683
- }
684
-
685
- .loading-content {
686
- text-align: center;
687
- }
688
-
689
- .loading-spinner {
690
- width: 60px;
691
- height: 60px;
692
- border: 5px solid #e2e8f0;
693
- border-top-color: var(--pastel-purple);
694
- border-radius: 50%;
695
- animation: spin 1s linear infinite;
696
- margin: 0 auto 15px;
697
- }
698
-
699
- .loading-text {
700
- font-size: 1.2rem;
701
- font-weight: 600;
702
- color: #4a5568;
703
- }
704
-
705
- .loading-error {
706
- display: none;
707
- margin-top: 10px;
708
- color: #e53e3e;
709
- font-size: 0.9rem;
710
- }
711
-
712
- /* Stats window styling */
713
- .stats-window {
714
- margin-top: 2rem;
715
- margin-bottom: 2rem;
716
- }
717
-
718
- .stats-header {
719
- display: flex;
720
- justify-content: space-between;
721
- align-items: center;
722
- margin-bottom: 1rem;
723
- }
724
-
725
- .stats-title {
726
- font-size: 1.5rem;
727
- font-weight: 700;
728
- color: #2d3748;
729
- }
730
-
731
- .stats-toggle {
732
- background-color: var(--pastel-blue);
733
- border: none;
734
- padding: 8px 16px;
735
- border-radius: 20px;
736
- font-weight: 600;
737
- cursor: pointer;
738
- transition: all 0.2s;
739
- }
740
-
741
- .stats-toggle:hover {
742
- background-color: var(--pastel-purple);
743
- }
744
-
745
- .stats-content {
746
- background-color: white;
747
- border-radius: 10px;
748
- padding: 20px;
749
- box-shadow: var(--box-shadow);
750
- max-height: 0;
751
- overflow: hidden;
752
- transition: max-height 0.5s ease-out;
753
- }
754
-
755
- .stats-content.open {
756
- max-height: 600px;
757
- }
758
-
759
- .chart-container {
760
- width: 100%;
761
- height: 500px;
762
- }
763
- </style>
764
- </head>
765
- <body>
766
- <div class="container">
767
- <div class="mac-window">
768
- <div class="mac-toolbar">
769
- <div class="mac-buttons">
770
- <div class="mac-button mac-close"></div>
771
- <div class="mac-button mac-minimize"></div>
772
- <div class="mac-button mac-maximize"></div>
773
- </div>
774
- <div class="mac-title">Huggingface Explorer</div>
775
- </div>
776
-
777
- <div class="mac-content">
778
- <div class="header">
779
- <h1>Zero GPU Spaces (Top 500 Trending)</h1>
780
- <p>Discover trending CPU-based spaces from Hugging Face. Only top 500 are considered!</p>
781
- </div>
782
-
783
- <!-- Tab Navigation -->
784
- <div class="tab-nav">
785
- <button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
786
- <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
787
- </div>
788
-
789
- <!-- Trending Tab Content -->
790
- <div id="trendingTab" class="tab-content active">
791
- <div class="stats-window mac-window">
792
- <div class="mac-toolbar">
793
- <div class="mac-buttons">
794
- <div class="mac-button mac-close"></div>
795
- <div class="mac-button mac-minimize"></div>
796
- <div class="mac-button mac-maximize"></div>
797
- </div>
798
- <div class="mac-title">Creator Statistics</div>
799
- </div>
800
- <div class="mac-content">
801
- <div class="stats-header">
802
- <div class="stats-title">Top 30 Creators by Space Count (of Top 500)</div>
803
- <button id="statsToggle" class="stats-toggle">Show Stats</button>
804
- </div>
805
- <div id="statsContent" class="stats-content">
806
- <div class="chart-container">
807
- <canvas id="creatorStatsChart"></canvas>
808
- </div>
809
- </div>
810
- </div>
811
- </div>
812
-
813
- <div class="search-bar">
814
- <input type="text" id="searchInput" placeholder="Search by name, owner, or description..." />
815
- <button id="refreshButton" class="refresh-btn">
816
- <span class="refresh-icon"></span>
817
- Refresh
818
- </button>
819
- </div>
820
-
821
- <div id="gridContainer" class="grid-container"></div>
822
-
823
- <div id="pagination" class="pagination"></div>
824
- </div>
825
-
826
- <!-- Fixed Tab Content -->
827
- <div id="fixedTab" class="tab-content">
828
- <div id="fixedGrid" class="grid-container"></div>
829
- </div>
830
- </div>
831
- </div>
832
- </div>
833
-
834
- <div id="loadingIndicator" class="loading">
835
- <div class="loading-content">
836
- <div class="loading-spinner"></div>
837
- <div class="loading-text">Loading Zero-GPU spaces...</div>
838
- <div id="loadingError" class="loading-error">
839
- If this takes too long, try refreshing the page.
840
- </div>
841
- </div>
842
- </div>
843
-
844
- <script>
845
- // DOM References
846
- const elements = {
847
- gridContainer: document.getElementById('gridContainer'),
848
- loadingIndicator: document.getElementById('loadingIndicator'),
849
- loadingError: document.getElementById('loadingError'),
850
- searchInput: document.getElementById('searchInput'),
851
- refreshButton: document.getElementById('refreshButton'),
852
- pagination: document.getElementById('pagination'),
853
- statsToggle: document.getElementById('statsToggle'),
854
- statsContent: document.getElementById('statsContent'),
855
- creatorStatsChart: document.getElementById('creatorStatsChart')
856
- };
857
-
858
- const tabTrendingButton = document.getElementById('tabTrendingButton');
859
- const tabFixedButton = document.getElementById('tabFixedButton');
860
- const trendingTab = document.getElementById('trendingTab');
861
- const fixedTab = document.getElementById('fixedTab');
862
- const fixedGridContainer = document.getElementById('fixedGrid');
863
-
864
- const state = {
865
- isLoading: false,
866
- spaces: [],
867
- currentPage: 0,
868
- itemsPerPage: 72,
869
- totalItems: 0,
870
- loadingTimeout: null,
871
- staticModeAttempted: {},
872
- statsVisible: false,
873
- chartInstance: null,
874
- topOwners: [],
875
- iframeStatuses: {}
876
- };
877
-
878
- // Advanced iframe loader
879
- const iframeLoader = {
880
- checkQueue: {},
881
- maxAttempts: 5,
882
- checkInterval: 5000,
883
-
884
- startChecking: function(iframe, owner, name, title, spaceKey) {
885
- this.checkQueue[spaceKey] = {
886
- iframe, owner, name, title,
887
- attempts: 0,
888
- status: 'loading'
889
- };
890
- this.checkIframeStatus(spaceKey);
891
- },
892
-
893
- checkIframeStatus: function(spaceKey) {
894
- if (!this.checkQueue[spaceKey]) return;
895
- const item = this.checkQueue[spaceKey];
896
- if (item.status !== 'loading') {
897
- delete this.checkQueue[spaceKey];
898
- return;
899
- }
900
- item.attempts++;
901
-
902
- try {
903
- if (!item.iframe || !item.iframe.parentNode) {
904
- delete this.checkQueue[spaceKey];
905
- return;
906
- }
907
-
908
- try {
909
- const hasContent = item.iframe.contentWindow &&
910
- item.iframe.contentWindow.document &&
911
- item.iframe.contentWindow.document.body;
912
- if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
913
- const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
914
- if (bodyText.includes('forbidden') || bodyText.includes('404') ||
915
- bodyText.includes('not found') || bodyText.includes('error')) {
916
- item.status = 'error';
917
- handleIframeError(item.iframe, item.owner, item.name, item.title);
918
- } else {
919
- item.status = 'success';
920
- }
921
- delete this.checkQueue[spaceKey];
922
- return;
923
- }
924
- } catch(e) {
925
- // cross-origin => not necessarily an error
926
- }
927
-
928
- const rect = item.iframe.getBoundingClientRect();
929
- if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
930
- item.status = 'success';
931
- delete this.checkQueue[spaceKey];
932
- return;
933
- }
934
-
935
- if (item.attempts >= this.maxAttempts) {
936
- if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
937
- item.status = 'success';
938
- } else {
939
- item.status = 'error';
940
- handleIframeError(item.iframe, item.owner, item.name, item.title);
941
- }
942
- delete this.checkQueue[spaceKey];
943
- return;
944
- }
945
-
946
- const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
947
- setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
948
-
949
- } catch (e) {
950
- console.error('Error checking iframe status:', e);
951
- if (item.attempts >= this.maxAttempts) {
952
- item.status = 'error';
953
- handleIframeError(item.iframe, item.owner, item.name, item.title);
954
- delete this.checkQueue[spaceKey];
955
- } else {
956
- setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval);
957
- }
958
- }
959
- }
960
- };
961
-
962
- function toggleStats() {
963
- state.statsVisible = !state.statsVisible;
964
- elements.statsContent.classList.toggle('open', state.statsVisible);
965
- elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
966
-
967
- if (state.statsVisible && state.topOwners.length > 0) {
968
- renderCreatorStats();
969
- }
970
- }
971
-
972
- function renderCreatorStats() {
973
- if (state.chartInstance) {
974
- state.chartInstance.destroy();
975
- }
976
- const ctx = elements.creatorStatsChart.getContext('2d');
977
- const labels = state.topOwners.map(item => item[0]);
978
- const data = state.topOwners.map(item => item[1]);
979
-
980
- const colors = [];
981
- for (let i = 0; i < labels.length; i++) {
982
- const hue = (i * 360 / labels.length) % 360;
983
- colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
984
- }
985
-
986
- state.chartInstance = new Chart(ctx, {
987
- type: 'bar',
988
- data: {
989
- labels: labels,
990
- datasets: [{
991
- label: 'Number of Spaces',
992
- data: data,
993
- backgroundColor: colors,
994
- borderColor: colors.map(color => color.replace('0.7','1')),
995
- borderWidth: 1
996
- }]
997
- },
998
- options: {
999
- indexAxis: 'y',
1000
- responsive: true,
1001
- maintainAspectRatio: false,
1002
- plugins: {
1003
- legend: { display: false },
1004
- tooltip: {
1005
- callbacks: {
1006
- title: function(tooltipItems) {
1007
- return tooltipItems[0].label;
1008
- },
1009
- label: function(context) {
1010
- return `Spaces: ${context.raw}`;
1011
- }
1012
- }
1013
- }
1014
- },
1015
- scales: {
1016
- x: {
1017
- beginAtZero: true,
1018
- title: {
1019
- display: true,
1020
- text: 'Number of Spaces'
1021
- }
1022
- },
1023
- y: {
1024
- title: {
1025
- display: true,
1026
- text: 'Creator ID'
1027
- },
1028
- ticks: {
1029
- autoSkip: false,
1030
- font: function(context) {
1031
- const defaultSize = 11;
1032
- return {
1033
- size: labels.length > 20 ? defaultSize - 1 : defaultSize
1034
- };
1035
- }
1036
- }
1037
- }
1038
- }
1039
- }
1040
- });
1041
- }
1042
-
1043
- async function loadSpaces(page = 0) {
1044
- setLoading(true);
1045
- try {
1046
- const searchText = elements.searchInput.value;
1047
- const offset = page * state.itemsPerPage;
1048
-
1049
- const timeoutPromise = new Promise((_, reject) =>
1050
- setTimeout(() => reject(new Error('Request timeout')), 30000)
1051
- );
1052
- const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
1053
- const response = await Promise.race([fetchPromise, timeoutPromise]);
1054
- const data = await response.json();
1055
-
1056
- state.spaces = data.spaces;
1057
- state.totalItems = data.total;
1058
- state.currentPage = page;
1059
- state.topOwners = data.top_owners || [];
1060
-
1061
- renderGrid(state.spaces);
1062
- renderPagination();
1063
-
1064
- if (state.statsVisible && state.topOwners.length > 0) {
1065
- renderCreatorStats();
1066
- }
1067
- } catch (error) {
1068
- console.error('Error loading spaces:', error);
1069
- elements.gridContainer.innerHTML = `
1070
- <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1071
- <div style="font-size: 3rem; margin-bottom: 20px;">⚠️</div>
1072
- <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
1073
- <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1074
- <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1075
- Try Again
1076
- </button>
1077
- </div>
1078
- `;
1079
- document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
1080
- renderPagination();
1081
- } finally {
1082
- setLoading(false);
1083
- }
1084
- }
1085
-
1086
- function renderPagination() {
1087
- elements.pagination.innerHTML = '';
1088
- const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1089
-
1090
- // Prev
1091
- const prevButton = document.createElement('button');
1092
- prevButton.className = 'pagination-button';
1093
- prevButton.textContent = 'Previous';
1094
- prevButton.disabled = (state.currentPage === 0);
1095
- prevButton.addEventListener('click', () => {
1096
- if (state.currentPage > 0) {
1097
- loadSpaces(state.currentPage - 1);
1098
- }
1099
- });
1100
- elements.pagination.appendChild(prevButton);
1101
-
1102
- // Middle pages
1103
- const maxButtons = 7;
1104
- let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1105
- let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1106
-
1107
- if (endPage - startPage + 1 < maxButtons) {
1108
- startPage = Math.max(0, endPage - maxButtons + 1);
1109
- }
1110
-
1111
- for (let i = startPage; i <= endPage; i++) {
1112
- const pageButton = document.createElement('button');
1113
- pageButton.className = 'pagination-button' + (i === state.currentPage ? ' active' : '');
1114
- pageButton.textContent = (i + 1);
1115
- pageButton.addEventListener('click', () => {
1116
- if (i !== state.currentPage) {
1117
- loadSpaces(i);
1118
- }
1119
- });
1120
- elements.pagination.appendChild(pageButton);
1121
- }
1122
-
1123
- // Next
1124
- const nextButton = document.createElement('button');
1125
- nextButton.className = 'pagination-button';
1126
- nextButton.textContent = 'Next';
1127
- nextButton.disabled = (state.currentPage >= totalPages - 1);
1128
- nextButton.addEventListener('click', () => {
1129
- if (state.currentPage < totalPages - 1) {
1130
- loadSpaces(state.currentPage + 1);
1131
- }
1132
- });
1133
- elements.pagination.appendChild(nextButton);
1134
- }
1135
-
1136
- function handleIframeError(iframe, owner, name, title) {
1137
- const container = iframe.parentNode;
1138
- const errorPlaceholder = document.createElement('div');
1139
- errorPlaceholder.className = 'error-placeholder';
1140
-
1141
- const errorMessage = document.createElement('p');
1142
- errorMessage.textContent = `"${title}" space couldn't be loaded`;
1143
- errorPlaceholder.appendChild(errorMessage);
1144
-
1145
- const directLink = document.createElement('a');
1146
- directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1147
- directLink.target = '_blank';
1148
- directLink.textContent = 'Visit HF Space';
1149
- directLink.style.color = '#3182ce';
1150
- directLink.style.marginTop = '10px';
1151
- directLink.style.display = 'inline-block';
1152
- directLink.style.padding = '8px 16px';
1153
- directLink.style.background = '#ebf8ff';
1154
- directLink.style.borderRadius = '5px';
1155
- directLink.style.fontWeight = '600';
1156
- errorPlaceholder.appendChild(directLink);
1157
-
1158
- iframe.style.display = 'none';
1159
- container.appendChild(errorPlaceholder);
1160
- }
1161
-
1162
- // renderGrid: 로봇 이모지 제거 -> rankBadge를 그 자리에 배치
1163
- function renderGrid(spaces) {
1164
- elements.gridContainer.innerHTML = '';
1165
-
1166
- if (!spaces || spaces.length === 0) {
1167
- const noResultsMsg = document.createElement('p');
1168
- noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
1169
- noResultsMsg.style.padding = '2rem';
1170
- noResultsMsg.style.textAlign = 'center';
1171
- noResultsMsg.style.fontStyle = 'italic';
1172
- noResultsMsg.style.color = '#718096';
1173
- elements.gridContainer.appendChild(noResultsMsg);
1174
- return;
1175
- }
1176
-
1177
- spaces.forEach((item) => {
1178
- try {
1179
- const {
1180
- url, title, likes_count, owner, name, rank,
1181
- description, avatar_url, author_name, embedUrl
1182
- } = item;
1183
-
1184
- const gridItem = document.createElement('div');
1185
- gridItem.className = 'grid-item';
1186
-
1187
- // 상단 header
1188
- const headerDiv = document.createElement('div');
1189
- headerDiv.className = 'grid-header';
1190
-
1191
- // grid-header-top (rank + title + zero gpu)
1192
- const headerTop = document.createElement('div');
1193
- headerTop.className = 'grid-header-top';
1194
-
1195
- // 왼쪽: rank badge
1196
- const rankBadge = document.createElement('div');
1197
- rankBadge.className = 'rank-badge';
1198
- rankBadge.textContent = `#${rank}`;
1199
- headerTop.appendChild(rankBadge);
1200
-
1201
- // 중간: title
1202
- const titleEl = document.createElement('h3');
1203
- titleEl.textContent = title;
1204
- titleEl.title = title;
1205
- headerTop.appendChild(titleEl);
1206
-
1207
- // 오른쪽(Zero GPU)
1208
- const zeroGpuBadge = document.createElement('span');
1209
- zeroGpuBadge.className = 'rank-badge';
1210
- zeroGpuBadge.style.backgroundColor = '#319795';
1211
- zeroGpuBadge.style.marginLeft = '10px';
1212
- zeroGpuBadge.textContent = 'ZERO GPU';
1213
- headerTop.appendChild(zeroGpuBadge);
1214
-
1215
- headerDiv.appendChild(headerTop);
1216
-
1217
- // 둘째줄: meta (owner / likes)
1218
- const metaInfo = document.createElement('div');
1219
- metaInfo.className = 'grid-meta';
1220
-
1221
- const ownerEl = document.createElement('div');
1222
- ownerEl.className = 'owner-info';
1223
- ownerEl.textContent = `by ${author_name}`;
1224
- metaInfo.appendChild(ownerEl);
1225
-
1226
- const likesDiv = document.createElement('div');
1227
- likesDiv.className = 'likes-counter';
1228
- likesDiv.innerHTML = '♥ <span>' + likes_count + '</span>';
1229
- metaInfo.appendChild(likesDiv);
1230
-
1231
- headerDiv.appendChild(metaInfo);
1232
-
1233
- if (description) {
1234
- const descP = document.createElement('p');
1235
- descP.style.fontSize = '0.9rem';
1236
- descP.style.marginTop = '6px';
1237
- descP.style.color = '#444';
1238
- descP.textContent = description;
1239
- headerDiv.appendChild(descP);
1240
- }
1241
-
1242
- gridItem.appendChild(headerDiv);
1243
-
1244
- // iframe container
1245
- const content = document.createElement('div');
1246
- content.className = 'grid-content';
1247
-
1248
- const iframeContainer = document.createElement('div');
1249
- iframeContainer.className = 'iframe-container';
1250
-
1251
- const iframe = document.createElement('iframe');
1252
- iframe.src = embedUrl;
1253
- iframe.title = title;
1254
- iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1255
- iframe.setAttribute('allowfullscreen','');
1256
- iframe.setAttribute('frameborder','0');
1257
- iframe.loading = 'lazy';
1258
-
1259
- const spaceKey = `${owner}/${name}`;
1260
- state.iframeStatuses[spaceKey] = 'loading';
1261
-
1262
- iframe.onload = function() {
1263
- iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1264
- };
1265
- iframe.onerror = function() {
1266
- handleIframeError(iframe, owner, name, title);
1267
- state.iframeStatuses[spaceKey] = 'error';
1268
- };
1269
- setTimeout(() => {
1270
- if (state.iframeStatuses[spaceKey] === 'loading') {
1271
- handleIframeError(iframe, owner, name, title);
1272
- state.iframeStatuses[spaceKey] = 'error';
1273
- }
1274
- }, 30000);
1275
-
1276
- iframeContainer.appendChild(iframe);
1277
- content.appendChild(iframeContainer);
1278
-
1279
- // actions
1280
- const actions = document.createElement('div');
1281
- actions.className = 'grid-actions';
1282
- const linkEl = document.createElement('a');
1283
- linkEl.href = url;
1284
- linkEl.target = '_blank';
1285
- linkEl.className = 'open-link';
1286
- linkEl.textContent = 'Open in new window';
1287
- actions.appendChild(linkEl);
1288
-
1289
- gridItem.appendChild(content);
1290
- gridItem.appendChild(actions);
1291
-
1292
- elements.gridContainer.appendChild(gridItem);
1293
-
1294
- } catch (err) {
1295
- console.error('Item rendering error:', err);
1296
- }
1297
- });
1298
- }
1299
-
1300
- // Fixed Tab (예시)
1301
- function renderFixedGrid() {
1302
- fixedGridContainer.innerHTML = '';
1303
-
1304
- const staticSpaces = [
1305
- {
1306
- url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1307
- title: "SanaSprint",
1308
- likes_count: 0,
1309
- owner: "VIDraft",
1310
- name: "SanaSprint",
1311
- rank: 1
1312
- },
1313
- {
1314
- url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1315
- title: "SanaSprint",
1316
- likes_count: 0,
1317
- owner: "VIDraft",
1318
- name: "SanaSprint",
1319
- rank: 2
1320
- },
1321
- {
1322
- url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1323
- title: "SanaSprint",
1324
- likes_count: 0,
1325
- owner: "VIDraft",
1326
- name: "SanaSprint",
1327
- rank: 3
1328
- }
1329
- ];
1330
-
1331
- if (!staticSpaces || staticSpaces.length === 0) {
1332
- const noResultsMsg = document.createElement('p');
1333
- noResultsMsg.textContent = 'No spaces to display.';
1334
- noResultsMsg.style.padding = '2rem';
1335
- noResultsMsg.style.textAlign = 'center';
1336
- noResultsMsg.style.fontStyle = 'italic';
1337
- noResultsMsg.style.color = '#718096';
1338
- fixedGridContainer.appendChild(noResultsMsg);
1339
- return;
1340
- }
1341
-
1342
- staticSpaces.forEach((item) => {
1343
- try {
1344
- const { url, title, likes_count, owner, name, rank } = item;
1345
- const gridItem = document.createElement('div');
1346
- gridItem.className = 'grid-item';
1347
-
1348
- const header = document.createElement('div');
1349
- header.className = 'grid-header';
1350
-
1351
- const headerTop = document.createElement('div');
1352
- headerTop.className = 'grid-header-top';
1353
-
1354
- // rank
1355
- const rankBadge = document.createElement('div');
1356
- rankBadge.className = 'rank-badge';
1357
- rankBadge.textContent = `#${rank}`;
1358
- headerTop.appendChild(rankBadge);
1359
-
1360
- // title
1361
- const titleEl = document.createElement('h3');
1362
- titleEl.textContent = title;
1363
- titleEl.title = title;
1364
- headerTop.appendChild(titleEl);
1365
-
1366
- // zero gpu (임의)
1367
- const zeroBadge = document.createElement('div');
1368
- zeroBadge.className = 'rank-badge';
1369
- zeroBadge.style.backgroundColor = '#319795';
1370
- zeroBadge.style.marginLeft = '10px';
1371
- zeroBadge.textContent = 'ZERO GPU';
1372
- headerTop.appendChild(zeroBadge);
1373
-
1374
- header.appendChild(headerTop);
1375
-
1376
- const metaInfo = document.createElement('div');
1377
- metaInfo.className = 'grid-meta';
1378
-
1379
- const ownerEl = document.createElement('div');
1380
- ownerEl.className = 'owner-info';
1381
- ownerEl.textContent = `by ${owner}`;
1382
- metaInfo.appendChild(ownerEl);
1383
-
1384
- const likesCounter = document.createElement('div');
1385
- likesCounter.className = 'likes-counter';
1386
- likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>';
1387
- metaInfo.appendChild(likesCounter);
1388
-
1389
- header.appendChild(metaInfo);
1390
- gridItem.appendChild(header);
1391
-
1392
- const content = document.createElement('div');
1393
- content.className = 'grid-content';
1394
-
1395
- const iframeContainer = document.createElement('div');
1396
- iframeContainer.className = 'iframe-container';
1397
-
1398
- const iframe = document.createElement('iframe');
1399
- iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1400
- iframe.title = title;
1401
- iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1402
- iframe.setAttribute('allowfullscreen','');
1403
- iframe.setAttribute('frameborder','0');
1404
- iframe.loading = 'lazy';
1405
-
1406
- const spaceKey = `${owner}/${name}`;
1407
- iframe.onload = function() {
1408
- iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1409
- };
1410
- iframe.onerror = function() {
1411
- handleIframeError(iframe, owner, name, title);
1412
- };
1413
- setTimeout(() => {
1414
- if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) {
1415
- handleIframeError(iframe, owner, name, title);
1416
- }
1417
- }, 30000);
1418
-
1419
- iframeContainer.appendChild(iframe);
1420
- content.appendChild(iframeContainer);
1421
-
1422
- const actions = document.createElement('div');
1423
- actions.className = 'grid-actions';
1424
-
1425
- const linkEl = document.createElement('a');
1426
- linkEl.href = url;
1427
- linkEl.target = '_blank';
1428
- linkEl.className = 'open-link';
1429
- linkEl.textContent = 'Open in new window';
1430
- actions.appendChild(linkEl);
1431
-
1432
- gridItem.appendChild(content);
1433
- gridItem.appendChild(actions);
1434
-
1435
- fixedGridContainer.appendChild(gridItem);
1436
-
1437
- } catch (error) {
1438
- console.error('Fixed tab rendering error:', error);
1439
- }
1440
- });
1441
- }
1442
-
1443
- tabTrendingButton.addEventListener('click', () => {
1444
- tabTrendingButton.classList.add('active');
1445
- tabFixedButton.classList.remove('active');
1446
- trendingTab.classList.add('active');
1447
- fixedTab.classList.remove('active');
1448
- loadSpaces(state.currentPage);
1449
- });
1450
- tabFixedButton.addEventListener('click', () => {
1451
- tabFixedButton.classList.add('active');
1452
- tabTrendingButton.classList.remove('active');
1453
- fixedTab.classList.add('active');
1454
- trendingTab.classList.remove('active');
1455
- renderFixedGrid();
1456
- });
1457
-
1458
- elements.searchInput.addEventListener('input', () => {
1459
- clearTimeout(state.searchTimeout);
1460
- state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1461
- });
1462
- elements.searchInput.addEventListener('keyup', (event) => {
1463
- if (event.key === 'Enter') {
1464
- loadSpaces(0);
1465
- }
1466
- });
1467
- elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1468
- elements.statsToggle.addEventListener('click', toggleStats);
1469
-
1470
- window.addEventListener('load', function() {
1471
- setTimeout(() => loadSpaces(0), 500);
1472
- });
1473
-
1474
- setTimeout(() => {
1475
- if (state.isLoading) {
1476
- setLoading(false);
1477
- elements.gridContainer.innerHTML = `
1478
- <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1479
- <div style="font-size: 3rem; margin-bottom: 20px;">⏱️</div>
1480
- <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
1481
- <p style="color: #666;">Please try refreshing the page.</p>
1482
- <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1483
- Reload Page
1484
- </button>
1485
- </div>
1486
- `;
1487
- }
1488
- }, 20000);
1489
-
1490
- loadSpaces(0);
1491
-
1492
- function setLoading(isLoading) {
1493
- state.isLoading = isLoading;
1494
- elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1495
-
1496
- if (isLoading) {
1497
- elements.refreshButton.classList.add('refreshing');
1498
- clearTimeout(state.loadingTimeout);
1499
- state.loadingTimeout = setTimeout(() => {
1500
- elements.loadingError.style.display = 'block';
1501
- }, 10000);
1502
- } else {
1503
- elements.refreshButton.classList.remove('refreshing');
1504
- clearTimeout(state.loadingTimeout);
1505
- elements.loadingError.style.display = 'none';
1506
- }
1507
- }
1508
- </script>
1509
- </body>
1510
- </html>
1511
- ''')
1512
-
1513
- # Flask Run
1514
- app.run(host='0.0.0.0', port=7860)