openfree commited on
Commit
976a525
ยท
verified ยท
1 Parent(s): 2840034

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -323
app.py CHANGED
@@ -3,7 +3,7 @@ import requests
3
  import os
4
  from collections import Counter
5
  from datetime import datetime
6
- import dateutil.parser # โ† ์ถ”๊ฐ€
7
 
8
  ##############################################################################
9
  # 1) ์ „์—ญ ๋ณ€์ˆ˜ & ๋”๋ฏธ ๋ฐ์ดํ„ฐ
@@ -26,8 +26,7 @@ def generate_dummy_spaces(count):
26
  'title': f'Dummy Space {i+1}',
27
  'description': 'This is a fallback dummy space.',
28
  'likes': 100 - i,
29
- # createdAt๋„ ์˜ˆ์‹œ๋กœ ์ถ”๊ฐ€ (๋ชจ๋‘ ๋™์ผ ๋‚ ์งœ)
30
- 'createdAt': '2023-01-01T00:00:00.000Z',
31
  'hardware': 'cpu',
32
  'user': {
33
  'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
@@ -43,12 +42,11 @@ def generate_dummy_spaces(count):
43
  def fetch_zero_gpu_spaces_once():
44
  """
45
  Hugging Face API (hardware=cpu) ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ด
46
- limit์„ ์ž‘๊ฒŒ ์„ค์ •ํ•˜์—ฌ ์‘๋‹ต ์†๋„๋ฅผ ๊ฐœ์„ 
47
  """
48
  try:
49
  url = "https://huggingface.co/api/spaces"
50
  params = {
51
- "limit": 1000, # ๋„ˆ๋ฌด ํฌ๊ฒŒ ์žก์œผ๋ฉด ์‘๋‹ต ์ง€์—ฐ โ†’ (์›๋ž˜ 50~200 ์ถ”์ฒœ, ์—ฌ๊ธฐ์„  1000)
52
  "hardware": "cpu"
53
  }
54
  resp = requests.get(url, params=params, timeout=30)
@@ -181,7 +179,7 @@ def fetch_trending_spaces(offset=0, limit=24):
181
  }
182
 
183
  ##############################################################################
184
- # 4) Flask ๋ผ์šฐํŠธ (Trending & New Created & Picks)
185
  ##############################################################################
186
 
187
  @app.route('/')
@@ -194,7 +192,7 @@ def home():
194
  @app.route('/api/trending-spaces', methods=['GET'])
195
  def trending_spaces():
196
  """
197
- Zero-GPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก (๊ธฐ์กด ํŠธ๋ Œ๋”ฉ)
198
  - Lazy Load๋กœ ์บ์‹œ ๋กœ๋“œ
199
  - offset, limit ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜
200
  - search ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฒ€์ƒ‰
@@ -236,54 +234,13 @@ def trending_spaces():
236
  'top_owners': top_owners
237
  })
238
 
239
- @app.route('/api/new-created-spaces', methods=['GET'])
240
- def new_created_spaces():
241
- """
242
- NEW CREATED ํƒญ์šฉ:
243
- - ์ „์—ญ ์บ์‹œ์—์„œ createdAt ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ
244
- - ์ตœ๋Œ€ 100๊ฐœ๋งŒ ๋ฐ˜ํ™˜
245
- """
246
- ensure_cache_loaded()
247
-
248
- # createdAt์ด ISO8601 ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ ํ›„ ํŒŒ์‹ฑ. ์‹คํŒจ ์‹œ datetime.min ์‚ฌ์šฉ
249
- def parse_created_at(space):
250
- date_str = space.get('createdAt', '')
251
- try:
252
- return dateutil.parser.isoparse(date_str)
253
- except:
254
- return datetime.min
255
-
256
- # createdAt ๊ธฐ์ค€์œผ๋กœ ํŒŒ์‹ฑํ•˜์—ฌ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
257
- sorted_by_new = sorted(
258
- SPACE_CACHE,
259
- key=lambda s: parse_created_at(s),
260
- reverse=True
261
- )
262
-
263
- # ์ตœ๋Œ€ 100๊ฐœ๊นŒ์ง€๋งŒ
264
- newest_100 = sorted_by_new[:100]
265
-
266
- final = []
267
- for i, sp in enumerate(newest_100):
268
- # rank๋Š” ๋‹จ์ˆœ i+1 ๋กœ
269
- detail = get_space_details(sp, i, 0)
270
- if detail:
271
- final.append(detail)
272
-
273
- return jsonify({
274
- 'spaces': final
275
- })
276
-
277
  ##############################################################################
278
- # 5) ์„œ๋ฒ„ ์‹คํ–‰ (templates/index.html ์ž‘์„ฑ)
279
  ##############################################################################
280
 
281
  if __name__ == '__main__':
282
  os.makedirs('templates', exist_ok=True)
283
 
284
- # -------------------
285
- # index.html ์ „์ฒด ์ƒ์„ฑ
286
- # -------------------
287
  with open('templates/index.html', 'w', encoding='utf-8') as f:
288
  f.write('''<!DOCTYPE html>
289
  <html lang="en">
@@ -292,9 +249,9 @@ if __name__ == '__main__':
292
  <title>Huggingface Zero-GPU Spaces</title>
293
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
294
  <style>
295
- /* ==================== (๊ธฐ์กด CSS ๊ทธ๋Œ€๋กœ ์œ ์ง€) ==================== */
296
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
297
-
298
  :root {
299
  --pastel-pink: #FFD6E0;
300
  --pastel-blue: #C5E8FF;
@@ -302,25 +259,25 @@ if __name__ == '__main__':
302
  --pastel-yellow: #FFF2CC;
303
  --pastel-green: #C7F5D9;
304
  --pastel-orange: #FFE0C3;
305
-
306
  --mac-window-bg: rgba(250, 250, 250, 0.85);
307
  --mac-toolbar: #F5F5F7;
308
  --mac-border: #E2E2E2;
309
  --mac-button-red: #FF5F56;
310
  --mac-button-yellow: #FFBD2E;
311
  --mac-button-green: #27C93F;
312
-
313
  --text-primary: #333;
314
  --text-secondary: #666;
315
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
316
  }
317
-
318
  * {
319
  margin: 0;
320
  padding: 0;
321
  box-sizing: border-box;
322
  }
323
-
324
  body {
325
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
326
  line-height: 1.6;
@@ -330,12 +287,12 @@ if __name__ == '__main__':
330
  min-height: 100vh;
331
  padding: 2rem;
332
  }
333
-
334
  .container {
335
  max-width: 1600px;
336
  margin: 0 auto;
337
  }
338
-
339
  .mac-window {
340
  background-color: var(--mac-window-bg);
341
  border-radius: 10px;
@@ -345,7 +302,7 @@ if __name__ == '__main__':
345
  margin-bottom: 2rem;
346
  border: 1px solid var(--mac-border);
347
  }
348
-
349
  .mac-toolbar {
350
  display: flex;
351
  align-items: center;
@@ -353,49 +310,49 @@ if __name__ == '__main__':
353
  background-color: var(--mac-toolbar);
354
  border-bottom: 1px solid var(--mac-border);
355
  }
356
-
357
  .mac-buttons {
358
  display: flex;
359
  gap: 8px;
360
  margin-right: 15px;
361
  }
362
-
363
  .mac-button {
364
  width: 12px;
365
  height: 12px;
366
  border-radius: 50%;
367
  cursor: default;
368
  }
369
-
370
  .mac-close {
371
  background-color: var(--mac-button-red);
372
  }
373
-
374
  .mac-minimize {
375
  background-color: var(--mac-button-yellow);
376
  }
377
-
378
  .mac-maximize {
379
  background-color: var(--mac-button-green);
380
  }
381
-
382
  .mac-title {
383
  flex-grow: 1;
384
  text-align: center;
385
  font-size: 0.9rem;
386
  color: var(--text-secondary);
387
  }
388
-
389
  .mac-content {
390
  padding: 20px;
391
  }
392
-
393
  .header {
394
  text-align: center;
395
  margin-bottom: 1.5rem;
396
  position: relative;
397
  }
398
-
399
  .header h1 {
400
  font-size: 2.2rem;
401
  font-weight: 700;
@@ -403,19 +360,19 @@ if __name__ == '__main__':
403
  color: #2d3748;
404
  letter-spacing: -0.5px;
405
  }
406
-
407
  .header p {
408
  color: var(--text-secondary);
409
  margin-top: 0.5rem;
410
  font-size: 1.1rem;
411
  }
412
-
413
  .tab-nav {
414
  display: flex;
415
  justify-content: center;
416
  margin-bottom: 1.5rem;
417
  }
418
-
419
  .tab-button {
420
  border: none;
421
  background-color: #edf2f7;
@@ -427,20 +384,20 @@ if __name__ == '__main__':
427
  font-size: 1rem;
428
  font-weight: 600;
429
  }
430
-
431
  .tab-button.active {
432
  background-color: var(--pastel-purple);
433
  color: #fff;
434
  }
435
-
436
  .tab-content {
437
  display: none;
438
  }
439
-
440
  .tab-content.active {
441
  display: block;
442
  }
443
-
444
  .search-bar {
445
  display: flex;
446
  align-items: center;
@@ -453,7 +410,7 @@ if __name__ == '__main__':
453
  margin-left: auto;
454
  margin-right: auto;
455
  }
456
-
457
  .search-bar input {
458
  flex-grow: 1;
459
  border: none;
@@ -463,7 +420,7 @@ if __name__ == '__main__':
463
  background: transparent;
464
  border-radius: 30px;
465
  }
466
-
467
  .search-bar .refresh-btn {
468
  background-color: var(--pastel-green);
469
  color: #1a202c;
@@ -478,12 +435,12 @@ if __name__ == '__main__':
478
  align-items: center;
479
  gap: 8px;
480
  }
481
-
482
  .search-bar .refresh-btn:hover {
483
  background-color: #9ee7c0;
484
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
485
  }
486
-
487
  .refresh-icon {
488
  display: inline-block;
489
  width: 16px;
@@ -493,23 +450,23 @@ if __name__ == '__main__':
493
  border-radius: 50%;
494
  animation: none;
495
  }
496
-
497
  .refreshing .refresh-icon {
498
  animation: spin 1s linear infinite;
499
  }
500
-
501
  @keyframes spin {
502
  0% { transform: rotate(0deg); }
503
  100% { transform: rotate(360deg); }
504
  }
505
-
506
  .grid-container {
507
  display: grid;
508
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
509
  gap: 1.5rem;
510
  margin-bottom: 2rem;
511
  }
512
-
513
  .grid-item {
514
  height: 500px;
515
  position: relative;
@@ -517,19 +474,19 @@ if __name__ == '__main__':
517
  transition: all 0.3s ease;
518
  border-radius: 15px;
519
  }
520
-
521
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
522
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
523
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
524
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
525
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
526
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
527
-
528
  .grid-item:hover {
529
  transform: translateY(-5px);
530
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
531
  }
532
-
533
  .grid-header {
534
  padding: 15px;
535
  display: flex;
@@ -538,14 +495,14 @@ if __name__ == '__main__':
538
  backdrop-filter: blur(5px);
539
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
540
  }
541
-
542
  .grid-header-top {
543
  display: flex;
544
  justify-content: space-between;
545
  align-items: center;
546
  margin-bottom: 8px;
547
  }
548
-
549
  .rank-badge {
550
  background-color: #1a202c;
551
  color: white;
@@ -555,7 +512,7 @@ if __name__ == '__main__':
555
  border-radius: 50px;
556
  display: inline-block;
557
  }
558
-
559
  .grid-header h3 {
560
  margin: 0;
561
  font-size: 1.2rem;
@@ -564,30 +521,30 @@ if __name__ == '__main__':
564
  overflow: hidden;
565
  text-overflow: ellipsis;
566
  }
567
-
568
  .grid-meta {
569
  display: flex;
570
  justify-content: space-between;
571
  align-items: center;
572
  font-size: 0.9rem;
573
  }
574
-
575
  .owner-info {
576
  color: var(--text-secondary);
577
  font-weight: 500;
578
  }
579
-
580
  .likes-counter {
581
  display: flex;
582
  align-items: center;
583
  color: #e53e3e;
584
  font-weight: 600;
585
  }
586
-
587
  .likes-counter span {
588
  margin-left: 4px;
589
  }
590
-
591
  .grid-actions {
592
  padding: 10px 15px;
593
  text-align: right;
@@ -601,7 +558,7 @@ if __name__ == '__main__':
601
  display: flex;
602
  justify-content: flex-end;
603
  }
604
-
605
  .open-link {
606
  text-decoration: none;
607
  color: #2c5282;
@@ -611,11 +568,11 @@ if __name__ == '__main__':
611
  transition: all 0.2s;
612
  background-color: rgba(237, 242, 247, 0.8);
613
  }
614
-
615
  .open-link:hover {
616
  background-color: #e2e8f0;
617
  }
618
-
619
  .grid-content {
620
  position: absolute;
621
  top: 0;
@@ -625,14 +582,14 @@ if __name__ == '__main__':
625
  padding-top: 85px; /* Header height */
626
  padding-bottom: 45px; /* Actions height */
627
  }
628
-
629
  .iframe-container {
630
  width: 100%;
631
  height: 100%;
632
  overflow: hidden;
633
  position: relative;
634
  }
635
-
636
  /* Apply 70% scaling to iframes */
637
  .grid-content iframe {
638
  transform: scale(0.7);
@@ -642,7 +599,7 @@ if __name__ == '__main__':
642
  border: none;
643
  border-radius: 0;
644
  }
645
-
646
  .error-placeholder {
647
  position: absolute;
648
  top: 0;
@@ -657,14 +614,14 @@ if __name__ == '__main__':
657
  background-color: rgba(255, 255, 255, 0.9);
658
  text-align: center;
659
  }
660
-
661
  .error-emoji {
662
  font-size: 6rem;
663
  margin-bottom: 1.5rem;
664
  animation: bounce 1s infinite alternate;
665
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
666
  }
667
-
668
  @keyframes bounce {
669
  from {
670
  transform: translateY(0px) scale(1);
@@ -673,7 +630,7 @@ if __name__ == '__main__':
673
  transform: translateY(-15px) scale(1.1);
674
  }
675
  }
676
-
677
  /* Pagination Styling */
678
  .pagination {
679
  display: flex;
@@ -682,7 +639,7 @@ if __name__ == '__main__':
682
  gap: 10px;
683
  margin: 2rem 0;
684
  }
685
-
686
  .pagination-button {
687
  background-color: white;
688
  border: none;
@@ -695,24 +652,24 @@ if __name__ == '__main__':
695
  color: var(--text-primary);
696
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
697
  }
698
-
699
  .pagination-button:hover {
700
  background-color: #f8f9fa;
701
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
702
  }
703
-
704
  .pagination-button.active {
705
  background-color: var(--pastel-purple);
706
  color: #4a5568;
707
  }
708
-
709
  .pagination-button:disabled {
710
  background-color: #edf2f7;
711
  color: #a0aec0;
712
  cursor: default;
713
  box-shadow: none;
714
  }
715
-
716
  /* Loading Indicator */
717
  .loading {
718
  position: fixed;
@@ -727,11 +684,11 @@ if __name__ == '__main__':
727
  align-items: center;
728
  z-index: 1000;
729
  }
730
-
731
  .loading-content {
732
  text-align: center;
733
  }
734
-
735
  .loading-spinner {
736
  width: 60px;
737
  height: 60px;
@@ -741,39 +698,39 @@ if __name__ == '__main__':
741
  animation: spin 1s linear infinite;
742
  margin: 0 auto 15px;
743
  }
744
-
745
  .loading-text {
746
  font-size: 1.2rem;
747
  font-weight: 600;
748
  color: #4a5568;
749
  }
750
-
751
  .loading-error {
752
  display: none;
753
  margin-top: 10px;
754
  color: #e53e3e;
755
  font-size: 0.9rem;
756
  }
757
-
758
  /* Stats window styling */
759
  .stats-window {
760
  margin-top: 2rem;
761
  margin-bottom: 2rem;
762
  }
763
-
764
  .stats-header {
765
  display: flex;
766
  justify-content: space-between;
767
  align-items: center;
768
  margin-bottom: 1rem;
769
  }
770
-
771
  .stats-title {
772
  font-size: 1.5rem;
773
  font-weight: 700;
774
  color: #2d3748;
775
  }
776
-
777
  .stats-toggle {
778
  background-color: var(--pastel-blue);
779
  border: none;
@@ -783,11 +740,11 @@ if __name__ == '__main__':
783
  cursor: pointer;
784
  transition: all 0.2s;
785
  }
786
-
787
  .stats-toggle:hover {
788
  background-color: var(--pastel-purple);
789
  }
790
-
791
  .stats-content {
792
  background-color: white;
793
  border-radius: 10px;
@@ -797,50 +754,50 @@ if __name__ == '__main__':
797
  overflow: hidden;
798
  transition: max-height 0.5s ease-out;
799
  }
800
-
801
  .stats-content.open {
802
  max-height: 600px;
803
  }
804
-
805
  .chart-container {
806
  width: 100%;
807
  height: 500px;
808
  }
809
-
810
  /* Responsive Design */
811
  @media (max-width: 768px) {
812
  body {
813
  padding: 1rem;
814
  }
815
-
816
  .grid-container {
817
  grid-template-columns: 1fr;
818
  }
819
-
820
  .search-bar {
821
  flex-direction: column;
822
  padding: 10px;
823
  }
824
-
825
  .search-bar input {
826
  width: 100%;
827
  margin-bottom: 10px;
828
  }
829
-
830
  .search-bar .refresh-btn {
831
  width: 100%;
832
  justify-content: center;
833
  }
834
-
835
  .pagination {
836
  flex-wrap: wrap;
837
  }
838
-
839
  .chart-container {
840
  height: 300px;
841
  }
842
  }
843
-
844
  .error-emoji-detector {
845
  position: fixed;
846
  top: -9999px;
@@ -917,7 +874,6 @@ if __name__ == '__main__':
917
  align-items: center;
918
  justify-content: center;
919
  }
920
- /* ==================== CSS ๋ ==================== */
921
  </style>
922
  </head>
923
  <body>
@@ -936,10 +892,9 @@ if __name__ == '__main__':
936
  <h1>ZeroGPU Spaces Leaderboard</h1>
937
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
938
  </div>
939
- <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜: (1) Trending, (2) New Created, (3) Picks -->
940
  <div class="tab-nav">
941
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
942
- <button id="tabNewCreatedButton" class="tab-button">New Created</button>
943
  <button id="tabFixedButton" class="tab-button">Picks</button>
944
  </div>
945
 
@@ -977,12 +932,6 @@ if __name__ == '__main__':
977
  <div id="pagination" class="pagination"></div>
978
  </div>
979
 
980
- <!-- New Created Tab Content -->
981
- <div id="newCreatedTab" class="tab-content">
982
- <!-- ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ์ŠคํŽ˜์ด์Šค๋ฅผ ํ‘œ์‹œํ•  ๊ทธ๋ฆฌ๋“œ -->
983
- <div id="newCreatedGrid" class="grid-container"></div>
984
- </div>
985
-
986
  <!-- Picks Tab Content (๊ธฐ์กด) -->
987
  <div id="fixedTab" class="tab-content">
988
  <div id="fixedGrid" class="grid-container"></div>
@@ -1001,9 +950,7 @@ if __name__ == '__main__':
1001
  </div>
1002
 
1003
  <script>
1004
- /* ==================== JS ๋กœ์ง (์งˆ๋ฌธ ๋ณธ๋ฌธ์˜ ๊ธฐ์กด ๋กœ์ง + New Created ํƒญ ์ถ”๊ฐ€) ==================== */
1005
-
1006
- // DOM Elements
1007
  const elements = {
1008
  gridContainer: document.getElementById('gridContainer'),
1009
  loadingIndicator: document.getElementById('loadingIndicator'),
@@ -1014,20 +961,18 @@ if __name__ == '__main__':
1014
  statsToggle: document.getElementById('statsToggle'),
1015
  statsContent: document.getElementById('statsContent'),
1016
  creatorStatsChart: document.getElementById('creatorStatsChart'),
1017
-
1018
- newCreatedGrid: document.getElementById('newCreatedGrid')
1019
  };
1020
 
1021
- // ํƒญ
1022
  const tabTrendingButton = document.getElementById('tabTrendingButton');
1023
- const tabNewCreatedButton = document.getElementById('tabNewCreatedButton');
1024
  const tabFixedButton = document.getElementById('tabFixedButton');
1025
 
1026
  // ํƒญ ๋‚ด์šฉ
1027
  const trendingTab = document.getElementById('trendingTab');
1028
- const newCreatedTab = document.getElementById('newCreatedTab');
1029
  const fixedTab = document.getElementById('fixedTab');
1030
 
 
1031
  const state = {
1032
  isLoading: false,
1033
  spaces: [],
@@ -1042,7 +987,7 @@ if __name__ == '__main__':
1042
  iframeStatuses: {}
1043
  };
1044
 
1045
- // iframe ์—๋Ÿฌ ๊ฐ์ง€ ๋กœ๋”
1046
  const iframeLoader = {
1047
  checkQueue: {},
1048
  maxAttempts: 5,
@@ -1120,6 +1065,7 @@ if __name__ == '__main__':
1120
  }
1121
  };
1122
 
 
1123
  function toggleStats() {
1124
  state.statsVisible = !state.statsVisible;
1125
  elements.statsContent.classList.toggle('open', state.statsVisible);
@@ -1216,6 +1162,7 @@ if __name__ == '__main__':
1216
  renderGrid(state.spaces);
1217
  renderPagination();
1218
 
 
1219
  if (state.statsVisible && state.topOwners.length > 0) {
1220
  renderCreatorStats();
1221
  }
@@ -1454,163 +1401,7 @@ if __name__ == '__main__':
1454
  });
1455
  }
1456
 
1457
- // --------------------- NEW CREATED ---------------------
1458
- // ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ์ŠคํŽ˜์ด์Šค(์ตœ์‹  ์ˆœ 100๊ฐœ) ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
1459
- function loadNewCreatedSpaces() {
1460
- setLoading(true);
1461
- fetch('/api/new-created-spaces')
1462
- .then(res => res.json())
1463
- .then(data => {
1464
- setLoading(false);
1465
- renderNewCreatedGrid(data.spaces);
1466
- })
1467
- .catch(err => {
1468
- setLoading(false);
1469
- console.error('Error loading new-created spaces:', err);
1470
- elements.newCreatedGrid.innerHTML = '<p style="padding:2rem;text-align:center;color:#666;">Unable to load new created spaces</p>';
1471
- });
1472
- }
1473
-
1474
- function renderNewCreatedGrid(spaces) {
1475
- elements.newCreatedGrid.innerHTML = '';
1476
- if (!spaces || spaces.length === 0) {
1477
- const noResults = document.createElement('p');
1478
- noResults.textContent = 'No newly created zero-gpu spaces found.';
1479
- noResults.style.padding = '2rem';
1480
- noResults.style.textAlign = 'center';
1481
- noResults.style.fontStyle = 'italic';
1482
- noResults.style.color = '#718096';
1483
- elements.newCreatedGrid.appendChild(noResults);
1484
- return;
1485
- }
1486
-
1487
- // ์ตœ์‹  ์ƒ์„ฑ๋œ ์ŠคํŽ˜์ด์Šค 100๊ฐœ๋ฅผ ํ‘œ์‹œํ•˜๋ฏ€๋กœ, rank๋Š” i+1 ๋กœ ๋‹จ์ˆœ ๋ถ€์—ฌ
1488
- spaces.forEach((item) => {
1489
- try {
1490
- const {
1491
- url, title, likes_count, owner, name, rank,
1492
- description, avatar_url, author_name, embedUrl
1493
- } = item;
1494
-
1495
- const gridItem = document.createElement('div');
1496
- gridItem.className = 'grid-item';
1497
-
1498
- // Header
1499
- const headerDiv = document.createElement('div');
1500
- headerDiv.className = 'grid-header';
1501
-
1502
- const spaceHeader = document.createElement('div');
1503
- spaceHeader.className = 'space-header';
1504
-
1505
- const rankBadge = document.createElement('div');
1506
- rankBadge.className = 'rank-badge';
1507
- rankBadge.textContent = `#${rank}`;
1508
- spaceHeader.appendChild(rankBadge);
1509
-
1510
- const titleWrapper = document.createElement('div');
1511
- titleWrapper.style.display = 'flex';
1512
- titleWrapper.style.alignItems = 'center';
1513
- titleWrapper.style.marginLeft = '8px';
1514
-
1515
- const titleEl = document.createElement('h3');
1516
- titleEl.className = 'space-title';
1517
- titleEl.textContent = title;
1518
- titleEl.title = title;
1519
- titleWrapper.appendChild(titleEl);
1520
-
1521
- const zeroGpuBadge = document.createElement('span');
1522
- zeroGpuBadge.className = 'zero-gpu-badge';
1523
- zeroGpuBadge.textContent = 'ZERO GPU';
1524
- titleWrapper.appendChild(zeroGpuBadge);
1525
-
1526
- spaceHeader.appendChild(titleWrapper);
1527
- headerDiv.appendChild(spaceHeader);
1528
-
1529
- const metaInfo = document.createElement('div');
1530
- metaInfo.className = 'grid-meta';
1531
- metaInfo.style.display = 'flex';
1532
- metaInfo.style.justifyContent = 'space-between';
1533
- metaInfo.style.alignItems = 'center';
1534
- metaInfo.style.marginTop = '6px';
1535
-
1536
- const leftMeta = document.createElement('div');
1537
- const authorSpan = document.createElement('span');
1538
- authorSpan.className = 'author-name';
1539
- authorSpan.style.marginLeft = '8px';
1540
- authorSpan.textContent = `by ${author_name}`;
1541
- leftMeta.appendChild(authorSpan);
1542
- metaInfo.appendChild(leftMeta);
1543
-
1544
- const likesDiv = document.createElement('div');
1545
- likesDiv.className = 'likes-wrapper';
1546
- likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
1547
- metaInfo.appendChild(likesDiv);
1548
-
1549
- headerDiv.appendChild(metaInfo);
1550
- gridItem.appendChild(headerDiv);
1551
-
1552
- if (description) {
1553
- const descP = document.createElement('p');
1554
- descP.className = 'desc-text';
1555
- descP.textContent = description;
1556
- gridItem.appendChild(descP);
1557
- }
1558
-
1559
- const content = document.createElement('div');
1560
- content.className = 'grid-content';
1561
-
1562
- const iframeContainer = document.createElement('div');
1563
- iframeContainer.className = 'iframe-container';
1564
-
1565
- const iframe = document.createElement('iframe');
1566
- iframe.src = embedUrl;
1567
- iframe.title = title;
1568
- iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1569
- iframe.setAttribute('allowfullscreen', '');
1570
- iframe.setAttribute('frameborder', '0');
1571
- iframe.loading = 'lazy';
1572
-
1573
- const spaceKey = `${owner}/${name}`;
1574
- state.iframeStatuses[spaceKey] = 'loading';
1575
-
1576
- iframe.onload = function() {
1577
- iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1578
- };
1579
- iframe.onerror = function() {
1580
- handleIframeError(iframe, owner, name, title);
1581
- state.iframeStatuses[spaceKey] = 'error';
1582
- };
1583
- setTimeout(() => {
1584
- if (state.iframeStatuses[spaceKey] === 'loading') {
1585
- handleIframeError(iframe, owner, name, title);
1586
- state.iframeStatuses[spaceKey] = 'error';
1587
- }
1588
- }, 30000);
1589
-
1590
- iframeContainer.appendChild(iframe);
1591
- content.appendChild(iframeContainer);
1592
-
1593
- const actions = document.createElement('div');
1594
- actions.className = 'grid-actions';
1595
-
1596
- const linkEl = document.createElement('a');
1597
- linkEl.href = url;
1598
- linkEl.target = '_blank';
1599
- linkEl.className = 'open-link';
1600
- linkEl.textContent = 'Open in new window';
1601
-
1602
- actions.appendChild(linkEl);
1603
- gridItem.appendChild(content);
1604
- gridItem.appendChild(actions);
1605
-
1606
- elements.newCreatedGrid.appendChild(gridItem);
1607
- } catch (err) {
1608
- console.error('Item rendering error:', err);
1609
- }
1610
- });
1611
- }
1612
-
1613
- // --------------------- PICKS (๊ธฐ์กด) ---------------------
1614
  function renderFixedGrid() {
1615
  // ์˜ˆ์‹œ์šฉ ์ •์  ๋ชฉ๋ก
1616
  const fixedGridContainer = document.getElementById('fixedGrid');
@@ -1750,43 +1541,25 @@ if __name__ == '__main__':
1750
  // --------------------- Tab Switching ---------------------
1751
  tabTrendingButton.addEventListener('click', () => {
1752
  tabTrendingButton.classList.add('active');
1753
- tabNewCreatedButton.classList.remove('active');
1754
  tabFixedButton.classList.remove('active');
1755
 
1756
  trendingTab.classList.add('active');
1757
- newCreatedTab.classList.remove('active');
1758
  fixedTab.classList.remove('active');
1759
 
1760
- // ๊ธฐ์กด Trending ๋ชฉ๋ก ๋กœ๋”ฉ
1761
  loadSpaces(state.currentPage);
1762
  });
1763
 
1764
- tabNewCreatedButton.addEventListener('click', () => {
1765
- tabTrendingButton.classList.remove('active');
1766
- tabNewCreatedButton.classList.add('active');
1767
- tabFixedButton.classList.remove('active');
1768
-
1769
- trendingTab.classList.remove('active');
1770
- newCreatedTab.classList.add('active');
1771
- fixedTab.classList.remove('active');
1772
-
1773
- // ์ƒˆ๋กœ ๋งŒ๋“  New Created ๋ชฉ๋ก ๋กœ๋”ฉ
1774
- loadNewCreatedSpaces();
1775
- });
1776
-
1777
  tabFixedButton.addEventListener('click', () => {
1778
  tabTrendingButton.classList.remove('active');
1779
- tabNewCreatedButton.classList.remove('active');
1780
  tabFixedButton.classList.add('active');
1781
 
1782
  trendingTab.classList.remove('active');
1783
- newCreatedTab.classList.remove('active');
1784
  fixedTab.classList.add('active');
1785
 
1786
  renderFixedGrid();
1787
  });
1788
 
1789
- // ๊ฒ€์ƒ‰ ์ž…๋ ฅ ๊ด€๋ จ
1790
  elements.searchInput.addEventListener('input', () => {
1791
  clearTimeout(state.searchTimeout);
1792
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
@@ -1799,7 +1572,7 @@ if __name__ == '__main__':
1799
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1800
  elements.statsToggle.addEventListener('click', toggleStats);
1801
 
1802
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐ ํƒญ(Trending) ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
1803
  window.addEventListener('load', function() {
1804
  setTimeout(() => loadSpaces(0), 500);
1805
  });
@@ -1820,7 +1593,6 @@ if __name__ == '__main__':
1820
  }
1821
  }, 20000);
1822
 
1823
- // ๋กœ๋”ฉ ํ‘œ์‹œ๋Š” ๊ณตํ†ต
1824
  function setLoading(isLoading) {
1825
  state.isLoading = isLoading;
1826
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
 
3
  import os
4
  from collections import Counter
5
  from datetime import datetime
6
+ import dateutil.parser # ํ˜„์žฌ ์ฝ”๋“œ์—์„œ dateutil.parser๋Š” ์“ฐ์ง€ ์•Š์ง€๋งŒ, ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ํ•„์š”ํ•  ์ˆ˜๋„ ์žˆ์–ด ๋‚จ๊น€
7
 
8
  ##############################################################################
9
  # 1) ์ „์—ญ ๋ณ€์ˆ˜ & ๋”๋ฏธ ๋ฐ์ดํ„ฐ
 
26
  'title': f'Dummy Space {i+1}',
27
  'description': 'This is a fallback dummy space.',
28
  'likes': 100 - i,
29
+ 'createdAt': '2023-01-01T00:00:00.000Z', # ์˜ˆ์‹œ
 
30
  'hardware': 'cpu',
31
  'user': {
32
  'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
 
42
  def fetch_zero_gpu_spaces_once():
43
  """
44
  Hugging Face API (hardware=cpu) ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ด
 
45
  """
46
  try:
47
  url = "https://huggingface.co/api/spaces"
48
  params = {
49
+ "limit": 1000,
50
  "hardware": "cpu"
51
  }
52
  resp = requests.get(url, params=params, timeout=30)
 
179
  }
180
 
181
  ##############################################################################
182
+ # 4) Flask ๋ผ์šฐํŠธ
183
  ##############################################################################
184
 
185
  @app.route('/')
 
192
  @app.route('/api/trending-spaces', methods=['GET'])
193
  def trending_spaces():
194
  """
195
+ Zero-GPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก (ํŠธ๋ Œ๋”ฉ)
196
  - Lazy Load๋กœ ์บ์‹œ ๋กœ๋“œ
197
  - offset, limit ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜
198
  - search ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฒ€์ƒ‰
 
234
  'top_owners': top_owners
235
  })
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  ##############################################################################
238
+ # 5) ์„œ๋ฒ„ ์‹คํ–‰ (templates/index.html)
239
  ##############################################################################
240
 
241
  if __name__ == '__main__':
242
  os.makedirs('templates', exist_ok=True)
243
 
 
 
 
244
  with open('templates/index.html', 'w', encoding='utf-8') as f:
245
  f.write('''<!DOCTYPE html>
246
  <html lang="en">
 
249
  <title>Huggingface Zero-GPU Spaces</title>
250
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
251
  <style>
252
+ /* ==================== (CSS ๊ทธ๋Œ€๋กœ ์œ ์ง€) ==================== */
253
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
254
+
255
  :root {
256
  --pastel-pink: #FFD6E0;
257
  --pastel-blue: #C5E8FF;
 
259
  --pastel-yellow: #FFF2CC;
260
  --pastel-green: #C7F5D9;
261
  --pastel-orange: #FFE0C3;
262
+
263
  --mac-window-bg: rgba(250, 250, 250, 0.85);
264
  --mac-toolbar: #F5F5F7;
265
  --mac-border: #E2E2E2;
266
  --mac-button-red: #FF5F56;
267
  --mac-button-yellow: #FFBD2E;
268
  --mac-button-green: #27C93F;
269
+
270
  --text-primary: #333;
271
  --text-secondary: #666;
272
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
273
  }
274
+
275
  * {
276
  margin: 0;
277
  padding: 0;
278
  box-sizing: border-box;
279
  }
280
+
281
  body {
282
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
283
  line-height: 1.6;
 
287
  min-height: 100vh;
288
  padding: 2rem;
289
  }
290
+
291
  .container {
292
  max-width: 1600px;
293
  margin: 0 auto;
294
  }
295
+
296
  .mac-window {
297
  background-color: var(--mac-window-bg);
298
  border-radius: 10px;
 
302
  margin-bottom: 2rem;
303
  border: 1px solid var(--mac-border);
304
  }
305
+
306
  .mac-toolbar {
307
  display: flex;
308
  align-items: center;
 
310
  background-color: var(--mac-toolbar);
311
  border-bottom: 1px solid var(--mac-border);
312
  }
313
+
314
  .mac-buttons {
315
  display: flex;
316
  gap: 8px;
317
  margin-right: 15px;
318
  }
319
+
320
  .mac-button {
321
  width: 12px;
322
  height: 12px;
323
  border-radius: 50%;
324
  cursor: default;
325
  }
326
+
327
  .mac-close {
328
  background-color: var(--mac-button-red);
329
  }
330
+
331
  .mac-minimize {
332
  background-color: var(--mac-button-yellow);
333
  }
334
+
335
  .mac-maximize {
336
  background-color: var(--mac-button-green);
337
  }
338
+
339
  .mac-title {
340
  flex-grow: 1;
341
  text-align: center;
342
  font-size: 0.9rem;
343
  color: var(--text-secondary);
344
  }
345
+
346
  .mac-content {
347
  padding: 20px;
348
  }
349
+
350
  .header {
351
  text-align: center;
352
  margin-bottom: 1.5rem;
353
  position: relative;
354
  }
355
+
356
  .header h1 {
357
  font-size: 2.2rem;
358
  font-weight: 700;
 
360
  color: #2d3748;
361
  letter-spacing: -0.5px;
362
  }
363
+
364
  .header p {
365
  color: var(--text-secondary);
366
  margin-top: 0.5rem;
367
  font-size: 1.1rem;
368
  }
369
+
370
  .tab-nav {
371
  display: flex;
372
  justify-content: center;
373
  margin-bottom: 1.5rem;
374
  }
375
+
376
  .tab-button {
377
  border: none;
378
  background-color: #edf2f7;
 
384
  font-size: 1rem;
385
  font-weight: 600;
386
  }
387
+
388
  .tab-button.active {
389
  background-color: var(--pastel-purple);
390
  color: #fff;
391
  }
392
+
393
  .tab-content {
394
  display: none;
395
  }
396
+
397
  .tab-content.active {
398
  display: block;
399
  }
400
+
401
  .search-bar {
402
  display: flex;
403
  align-items: center;
 
410
  margin-left: auto;
411
  margin-right: auto;
412
  }
413
+
414
  .search-bar input {
415
  flex-grow: 1;
416
  border: none;
 
420
  background: transparent;
421
  border-radius: 30px;
422
  }
423
+
424
  .search-bar .refresh-btn {
425
  background-color: var(--pastel-green);
426
  color: #1a202c;
 
435
  align-items: center;
436
  gap: 8px;
437
  }
438
+
439
  .search-bar .refresh-btn:hover {
440
  background-color: #9ee7c0;
441
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
442
  }
443
+
444
  .refresh-icon {
445
  display: inline-block;
446
  width: 16px;
 
450
  border-radius: 50%;
451
  animation: none;
452
  }
453
+
454
  .refreshing .refresh-icon {
455
  animation: spin 1s linear infinite;
456
  }
457
+
458
  @keyframes spin {
459
  0% { transform: rotate(0deg); }
460
  100% { transform: rotate(360deg); }
461
  }
462
+
463
  .grid-container {
464
  display: grid;
465
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
466
  gap: 1.5rem;
467
  margin-bottom: 2rem;
468
  }
469
+
470
  .grid-item {
471
  height: 500px;
472
  position: relative;
 
474
  transition: all 0.3s ease;
475
  border-radius: 15px;
476
  }
477
+
478
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
479
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
480
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
481
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
482
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
483
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
484
+
485
  .grid-item:hover {
486
  transform: translateY(-5px);
487
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
488
  }
489
+
490
  .grid-header {
491
  padding: 15px;
492
  display: flex;
 
495
  backdrop-filter: blur(5px);
496
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
497
  }
498
+
499
  .grid-header-top {
500
  display: flex;
501
  justify-content: space-between;
502
  align-items: center;
503
  margin-bottom: 8px;
504
  }
505
+
506
  .rank-badge {
507
  background-color: #1a202c;
508
  color: white;
 
512
  border-radius: 50px;
513
  display: inline-block;
514
  }
515
+
516
  .grid-header h3 {
517
  margin: 0;
518
  font-size: 1.2rem;
 
521
  overflow: hidden;
522
  text-overflow: ellipsis;
523
  }
524
+
525
  .grid-meta {
526
  display: flex;
527
  justify-content: space-between;
528
  align-items: center;
529
  font-size: 0.9rem;
530
  }
531
+
532
  .owner-info {
533
  color: var(--text-secondary);
534
  font-weight: 500;
535
  }
536
+
537
  .likes-counter {
538
  display: flex;
539
  align-items: center;
540
  color: #e53e3e;
541
  font-weight: 600;
542
  }
543
+
544
  .likes-counter span {
545
  margin-left: 4px;
546
  }
547
+
548
  .grid-actions {
549
  padding: 10px 15px;
550
  text-align: right;
 
558
  display: flex;
559
  justify-content: flex-end;
560
  }
561
+
562
  .open-link {
563
  text-decoration: none;
564
  color: #2c5282;
 
568
  transition: all 0.2s;
569
  background-color: rgba(237, 242, 247, 0.8);
570
  }
571
+
572
  .open-link:hover {
573
  background-color: #e2e8f0;
574
  }
575
+
576
  .grid-content {
577
  position: absolute;
578
  top: 0;
 
582
  padding-top: 85px; /* Header height */
583
  padding-bottom: 45px; /* Actions height */
584
  }
585
+
586
  .iframe-container {
587
  width: 100%;
588
  height: 100%;
589
  overflow: hidden;
590
  position: relative;
591
  }
592
+
593
  /* Apply 70% scaling to iframes */
594
  .grid-content iframe {
595
  transform: scale(0.7);
 
599
  border: none;
600
  border-radius: 0;
601
  }
602
+
603
  .error-placeholder {
604
  position: absolute;
605
  top: 0;
 
614
  background-color: rgba(255, 255, 255, 0.9);
615
  text-align: center;
616
  }
617
+
618
  .error-emoji {
619
  font-size: 6rem;
620
  margin-bottom: 1.5rem;
621
  animation: bounce 1s infinite alternate;
622
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
623
  }
624
+
625
  @keyframes bounce {
626
  from {
627
  transform: translateY(0px) scale(1);
 
630
  transform: translateY(-15px) scale(1.1);
631
  }
632
  }
633
+
634
  /* Pagination Styling */
635
  .pagination {
636
  display: flex;
 
639
  gap: 10px;
640
  margin: 2rem 0;
641
  }
642
+
643
  .pagination-button {
644
  background-color: white;
645
  border: none;
 
652
  color: var(--text-primary);
653
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
654
  }
655
+
656
  .pagination-button:hover {
657
  background-color: #f8f9fa;
658
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
659
  }
660
+
661
  .pagination-button.active {
662
  background-color: var(--pastel-purple);
663
  color: #4a5568;
664
  }
665
+
666
  .pagination-button:disabled {
667
  background-color: #edf2f7;
668
  color: #a0aec0;
669
  cursor: default;
670
  box-shadow: none;
671
  }
672
+
673
  /* Loading Indicator */
674
  .loading {
675
  position: fixed;
 
684
  align-items: center;
685
  z-index: 1000;
686
  }
687
+
688
  .loading-content {
689
  text-align: center;
690
  }
691
+
692
  .loading-spinner {
693
  width: 60px;
694
  height: 60px;
 
698
  animation: spin 1s linear infinite;
699
  margin: 0 auto 15px;
700
  }
701
+
702
  .loading-text {
703
  font-size: 1.2rem;
704
  font-weight: 600;
705
  color: #4a5568;
706
  }
707
+
708
  .loading-error {
709
  display: none;
710
  margin-top: 10px;
711
  color: #e53e3e;
712
  font-size: 0.9rem;
713
  }
714
+
715
  /* Stats window styling */
716
  .stats-window {
717
  margin-top: 2rem;
718
  margin-bottom: 2rem;
719
  }
720
+
721
  .stats-header {
722
  display: flex;
723
  justify-content: space-between;
724
  align-items: center;
725
  margin-bottom: 1rem;
726
  }
727
+
728
  .stats-title {
729
  font-size: 1.5rem;
730
  font-weight: 700;
731
  color: #2d3748;
732
  }
733
+
734
  .stats-toggle {
735
  background-color: var(--pastel-blue);
736
  border: none;
 
740
  cursor: pointer;
741
  transition: all 0.2s;
742
  }
743
+
744
  .stats-toggle:hover {
745
  background-color: var(--pastel-purple);
746
  }
747
+
748
  .stats-content {
749
  background-color: white;
750
  border-radius: 10px;
 
754
  overflow: hidden;
755
  transition: max-height 0.5s ease-out;
756
  }
757
+
758
  .stats-content.open {
759
  max-height: 600px;
760
  }
761
+
762
  .chart-container {
763
  width: 100%;
764
  height: 500px;
765
  }
766
+
767
  /* Responsive Design */
768
  @media (max-width: 768px) {
769
  body {
770
  padding: 1rem;
771
  }
772
+
773
  .grid-container {
774
  grid-template-columns: 1fr;
775
  }
776
+
777
  .search-bar {
778
  flex-direction: column;
779
  padding: 10px;
780
  }
781
+
782
  .search-bar input {
783
  width: 100%;
784
  margin-bottom: 10px;
785
  }
786
+
787
  .search-bar .refresh-btn {
788
  width: 100%;
789
  justify-content: center;
790
  }
791
+
792
  .pagination {
793
  flex-wrap: wrap;
794
  }
795
+
796
  .chart-container {
797
  height: 300px;
798
  }
799
  }
800
+
801
  .error-emoji-detector {
802
  position: fixed;
803
  top: -9999px;
 
874
  align-items: center;
875
  justify-content: center;
876
  }
 
877
  </style>
878
  </head>
879
  <body>
 
892
  <h1>ZeroGPU Spaces Leaderboard</h1>
893
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
894
  </div>
895
+ <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜: (1) Trending, (2) Picks) -->
896
  <div class="tab-nav">
897
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
 
898
  <button id="tabFixedButton" class="tab-button">Picks</button>
899
  </div>
900
 
 
932
  <div id="pagination" class="pagination"></div>
933
  </div>
934
 
 
 
 
 
 
 
935
  <!-- Picks Tab Content (๊ธฐ์กด) -->
936
  <div id="fixedTab" class="tab-content">
937
  <div id="fixedGrid" class="grid-container"></div>
 
950
  </div>
951
 
952
  <script>
953
+ // --------------------- ์ „์—ญ DOM Elements ---------------------
 
 
954
  const elements = {
955
  gridContainer: document.getElementById('gridContainer'),
956
  loadingIndicator: document.getElementById('loadingIndicator'),
 
961
  statsToggle: document.getElementById('statsToggle'),
962
  statsContent: document.getElementById('statsContent'),
963
  creatorStatsChart: document.getElementById('creatorStatsChart'),
964
+ // second tab ์ œ๊ฑฐ๋กœ newCreatedGrid๋Š” ์ œ๊ฑฐ
 
965
  };
966
 
967
+ // --------------------- ํƒญ ๋ฒ„ํŠผ ---------------------
968
  const tabTrendingButton = document.getElementById('tabTrendingButton');
 
969
  const tabFixedButton = document.getElementById('tabFixedButton');
970
 
971
  // ํƒญ ๋‚ด์šฉ
972
  const trendingTab = document.getElementById('trendingTab');
 
973
  const fixedTab = document.getElementById('fixedTab');
974
 
975
+ // --------------------- ์ƒํƒœ ---------------------
976
  const state = {
977
  isLoading: false,
978
  spaces: [],
 
987
  iframeStatuses: {}
988
  };
989
 
990
+ // --------------------- iframe ์—๋Ÿฌ ๊ฐ์ง€ ๋กœ์ง ---------------------
991
  const iframeLoader = {
992
  checkQueue: {},
993
  maxAttempts: 5,
 
1065
  }
1066
  };
1067
 
1068
+ // --------------------- ํ†ต๊ณ„ ์ฐฝ toggle ---------------------
1069
  function toggleStats() {
1070
  state.statsVisible = !state.statsVisible;
1071
  elements.statsContent.classList.toggle('open', state.statsVisible);
 
1162
  renderGrid(state.spaces);
1163
  renderPagination();
1164
 
1165
+ // ํ†ต๊ณ„ Visible ์ƒํƒœ๋ผ๋ฉด ์—…๋ฐ์ดํŠธ
1166
  if (state.statsVisible && state.topOwners.length > 0) {
1167
  renderCreatorStats();
1168
  }
 
1401
  });
1402
  }
1403
 
1404
+ // --------------------- PICKS (์ •์ ) ---------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405
  function renderFixedGrid() {
1406
  // ์˜ˆ์‹œ์šฉ ์ •์  ๋ชฉ๋ก
1407
  const fixedGridContainer = document.getElementById('fixedGrid');
 
1541
  // --------------------- Tab Switching ---------------------
1542
  tabTrendingButton.addEventListener('click', () => {
1543
  tabTrendingButton.classList.add('active');
 
1544
  tabFixedButton.classList.remove('active');
1545
 
1546
  trendingTab.classList.add('active');
 
1547
  fixedTab.classList.remove('active');
1548
 
 
1549
  loadSpaces(state.currentPage);
1550
  });
1551
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1552
  tabFixedButton.addEventListener('click', () => {
1553
  tabTrendingButton.classList.remove('active');
 
1554
  tabFixedButton.classList.add('active');
1555
 
1556
  trendingTab.classList.remove('active');
 
1557
  fixedTab.classList.add('active');
1558
 
1559
  renderFixedGrid();
1560
  });
1561
 
1562
+ // ๊ฒ€์ƒ‰ ์ž…๋ ฅ
1563
  elements.searchInput.addEventListener('input', () => {
1564
  clearTimeout(state.searchTimeout);
1565
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
 
1572
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1573
  elements.statsToggle.addEventListener('click', toggleStats);
1574
 
1575
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ฒซ ํƒญ(Trending) ์‹คํ–‰
1576
  window.addEventListener('load', function() {
1577
  setTimeout(() => loadSpaces(0), 500);
1578
  });
 
1593
  }
1594
  }, 20000);
1595
 
 
1596
  function setLoading(isLoading) {
1597
  state.isLoading = isLoading;
1598
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';