theWitcher commited on
Commit
e7e1c0e
·
verified ·
1 Parent(s): 8e9e4d7

add new sort

Browse files
Files changed (1) hide show
  1. index.html +267 -242
index.html CHANGED
@@ -1,4 +1,3 @@
1
-
2
  <!--
3
  https://huggingface.co/spaces/theWitcher/sagi-ai-tools
4
  https://huggingface.co/spaces/theWitcher/sagi-ai-tools/blob/main/index.html
@@ -11,47 +10,7 @@
11
  <meta charset="UTF-8">
12
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
  <title>ארגז הכלים שלי לבינה מלאכותית</title>
14
- <script src="https://cdn.tailwindcss.com">
15
- function getYouTubeID(url) {
16
- const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
17
- return match ? match[1] : '';
18
- }
19
-
20
- function renderVideos() {
21
- const videosContainer = document.getElementById('videosContainer');
22
- if (!videosContainer) return;
23
-
24
- videosContainer.innerHTML = '';
25
-
26
- toolsData.videos.forEach(video => {
27
- const videoId = getYouTubeID(video.url);
28
- const embedUrl = `https://www.youtube.com/embed/${videoId}`;
29
-
30
- const videoCard = document.createElement('div');
31
- videoCard.className = 'bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden';
32
-
33
- videoCard.innerHTML = `
34
- <div class="relative pt-[56.25%]">
35
- <iframe class="absolute top-0 left-0 w-full h-full" src="${embedUrl}" frameborder="0"
36
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
37
- allowfullscreen></iframe>
38
- </div>
39
- <div class="p-4">
40
- <h3 class="text-lg font-semibold mb-2">${video.title}</h3>
41
- <p class="text-gray-600 text-sm mb-3">${video.description}</p>
42
- <p class="text-gray-500 text-xs">${formatDate(video.date)}</p>
43
- </div>
44
- `;
45
-
46
- videosContainer.appendChild(videoCard);
47
- });
48
- }
49
-
50
- window.addEventListener('DOMContentLoaded', () => {
51
- renderVideos();
52
- });
53
-
54
- </script>
55
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
56
  <style>
57
  @import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600;700&display=swap');
@@ -207,6 +166,11 @@ window.addEventListener('DOMContentLoaded', () => {
207
  border-radius: 50%;
208
  opacity: 0.3;
209
  }
 
 
 
 
 
210
  </style>
211
  </head>
212
  <body class="min-h-screen">
@@ -230,6 +194,7 @@ window.addEventListener('DOMContentLoaded', () => {
230
  <button id="refreshBtn" class="px-4 py-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition">
231
  <i class="fas fa-sync-alt ml-2"></i> רענן
232
  </button>
 
233
  <!-- <button id="editJsonBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition relative">
234
  <i class="fas fa-edit ml-2"></i> ערוך JSON
235
  <span class="admin-badge">A</span>
@@ -243,10 +208,11 @@ window.addEventListener('DOMContentLoaded', () => {
243
  <button id="refreshBtnMobile" class="px-4 py-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition text-right">
244
  <i class="fas fa-sync-alt ml-2"></i> רענן
245
  </button>
246
- <button id="editJsonBtnMobile" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-right relative">
 
247
  <i class="fas fa-edit ml-2"></i> ערוך JSON
248
  <span class="admin-badge">A</span>
249
- </button>
250
  </div>
251
  </div>
252
  </div>
@@ -324,6 +290,7 @@ window.addEventListener('DOMContentLoaded', () => {
324
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="design">עיצוב</button>
325
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="coding">תכנות</button>
326
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="video">וידאו</button>
 
327
  </div>
328
  </div>
329
  </div>
@@ -342,7 +309,8 @@ window.addEventListener('DOMContentLoaded', () => {
342
  </div>
343
  </div>
344
  </div>
345
- <div class="bg-white p-4 rounded-lg shadow-sm border border-gray-100">
 
346
  <div class="flex items-center">
347
  <div class="p-3 rounded-full bg-purple-50 text-purple-600 ml-3">
348
  <i class="fas fa-star text-lg"></i>
@@ -353,7 +321,8 @@ window.addEventListener('DOMContentLoaded', () => {
353
  </div>
354
  </div>
355
  </div>
356
- <div class="bg-white p-4 rounded-lg shadow-sm border border-gray-100">
 
357
  <div class="flex items-center">
358
  <div class="p-3 rounded-full bg-green-50 text-green-600 ml-3">
359
  <i class="fas fa-bolt text-lg"></i>
@@ -395,7 +364,7 @@ window.addEventListener('DOMContentLoaded', () => {
395
  <div id="jsonEditorModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
396
  <div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
397
  <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
398
- <h3 class="text-lg font-semibold">עריכת הכלים ב-JSON</h3>
399
  <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
400
  <i class="fas fa-times"></i>
401
  </button>
@@ -405,14 +374,14 @@ window.addEventListener('DOMContentLoaded', () => {
405
  <button id="showToolsBtn" class="px-4 py-2 bg-blue-600 text-white rounded-l-lg">כלים</button>
406
  <button id="showVideosBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-r-lg">סרטונים</button>
407
  </div>
408
- <textarea id="jsonEditor" class="w-full h-96 p-4 border border-gray-300 rounded-lg font-mono text-sm spellcheck="false"></textarea>
409
  </div>
410
  <div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3 space-x-reverse">
411
  <button id="cancelEditBtn" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition">
412
  ביטול
413
  </button>
414
  <button id="saveJsonBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
415
- שמור שינויים
416
  </button>
417
  </div>
418
  </div>
@@ -423,19 +392,19 @@ window.addEventListener('DOMContentLoaded', () => {
423
  <div class="container mx-auto px-4">
424
  <div class="flex flex-col md:flex-row justify-between items-center">
425
  <div class="text-gray-600 mb-4 md:mb-0">
426
- © 2023 ארגז הכלים שלי ל-AI. כל הזכויות שמורות.
427
  </div>
428
  <div class="flex space-x-4 space-x-reverse">
429
- <a href="https://www.youtube.com/@SAGIBARON" target="_blank" class="text-gray-500 hover:text-red-600 transition">
430
  <i class="fab fa-youtube text-xl"></i>
431
  </a>
432
- <a href="https://www.facebook.com/SAGI.BARON" target="_blank" class="text-gray-500 hover:text-blue-800 transition">
433
  <i class="fab fa-facebook text-xl"></i>
434
  </a>
435
- <a href="http://www.linkedin.com/in/sagi-bar-on" target="_blank" class="text-gray-500 hover:text-blue-700 transition">
436
  <i class="fab fa-linkedin text-xl"></i>
437
  </a>
438
- <a href="https://chat.whatsapp.com/GPFASYBEA9CFGUMCVZ5RXP" target="_blank" class="text-gray-500 hover:text-green-600 transition">
439
  <i class="fab fa-whatsapp text-xl"></i>
440
  </a>
441
  </div>
@@ -443,10 +412,10 @@ window.addEventListener('DOMContentLoaded', () => {
443
  </div>
444
  </footer>
445
 
446
- <script>
447
-
 
448
  // DOM Elements
449
- // DOM Elements (נשארים ללא שינוי - רק לרפרנס)
450
  const toolsContainer = document.getElementById('toolsContainer');
451
  const videosContainer = document.getElementById('videosContainer');
452
  const searchInput = document.getElementById('searchInput');
@@ -464,21 +433,27 @@ window.addEventListener('DOMContentLoaded', () => {
464
  const saveJsonBtn = document.getElementById('saveJsonBtn');
465
  const showToolsBtn = document.getElementById('showToolsBtn');
466
  const showVideosBtn = document.getElementById('showVideosBtn');
467
- // const editJsonBtn = document.getElementById('editJsonBtn'); // הלחצן מוסתר כרגע
468
- // const editJsonBtnMobile = document.getElementById('editJsonBtnMobile'); // הלחצן מוסתר כרגע
469
  const refreshBtn = document.getElementById('refreshBtn');
470
  const refreshBtnMobile = document.getElementById('refreshBtnMobile');
 
 
 
 
 
 
471
 
472
  // State
473
  let currentCategory = 'all';
474
  let currentSearchTerm = '';
475
  let toolsData = { tools: [], videos: [] }; // Initialize empty
 
476
 
477
  // Initialize - Make it async
478
  async function init() {
479
  await loadData(); // Wait for data to load
 
480
  renderTools();
481
- renderVideos(); // Call the specific renderVideos function
482
  updateStats();
483
  setupEventListeners();
484
  // Set initial active filter button
@@ -492,8 +467,8 @@ window.addEventListener('DOMContentLoaded', () => {
492
  async function fetchDefaultData() {
493
  try {
494
  const [toolsResponse, videosResponse] = await Promise.all([
495
- fetch('tools.json'),
496
- fetch('videos.json')
497
  ]);
498
 
499
  if (!toolsResponse.ok || !videosResponse.ok) {
@@ -505,10 +480,30 @@ window.addEventListener('DOMContentLoaded', () => {
505
  videosResponse.json()
506
  ]);
507
 
508
- return { tools: toolsArray, videos: videosArray };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  } catch (error) {
510
  console.error("Failed to fetch default data:", error);
511
- alert("שגיאה בטעינת נתוני ברירת המחדל. נסה לרענן את הדף.");
512
  return { tools: [], videos: [] }; // Return empty structure on error
513
  }
514
  }
@@ -524,6 +519,25 @@ window.addEventListener('DOMContentLoaded', () => {
524
  console.warn("Invalid data structure in localStorage. Fetching defaults.");
525
  toolsData = await fetchDefaultData();
526
  saveData(); // Save the fetched defaults
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  }
528
  } catch (e) {
529
  console.error("Error parsing data from localStorage:", e);
@@ -546,43 +560,78 @@ window.addEventListener('DOMContentLoaded', () => {
546
  }
547
  }
548
 
549
- // Render tools based on filters (Modified slightly for safety)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  function renderTools() {
551
  if (!toolsContainer || !toolsData || !Array.isArray(toolsData.tools)) {
552
  console.error("Cannot render tools: Missing container or invalid data.");
553
  return;
554
  }
555
- const filteredTools = filterTools();
556
 
557
  toolsContainer.innerHTML = ''; // Clear previous tools
558
 
559
  if (filteredTools.length === 0) {
560
- toolsContainer.innerHTML = '<p class="text-center text-gray-500 col-span-full">לא נמצאו כלים התואמים את החיפוש או הסינון.</p>';
561
  } else {
562
  filteredTools.forEach(tool => {
563
  const toolCard = document.createElement('div');
564
- // Add relative positioning for potential future badges (like admin edit)
565
  toolCard.className = `relative tool-card bg-white rounded-lg shadow-sm border border-gray-100 p-6 transition duration-300 ${tool.isFeatured ? 'ring-2 ring-blue-500' : ''}`;
566
 
567
  toolCard.innerHTML = `
568
  <div class="flex items-start mb-4">
569
  <div class="p-3 rounded-lg ${getCategoryColor(tool.category)} text-white mr-4 flex-shrink-0">
570
- <i class="${tool.icon || 'fas fa-question-circle'} text-xl"></i>
571
  </div>
572
  <div class="flex-grow">
573
- <h3 class="text-xl font-semibold">${tool.name || 'שם לא ידוע'}</h3>
574
- <span class="text-xs px-2 py-1 rounded-full ${getCategoryBadgeColor(tool.category)}">${getCategoryName(tool.category || 'כללי')}</span>
575
  </div>
 
576
  </div>
577
- <p class="text-gray-700 mb-4 text-sm min-h-[60px]">${tool.description || 'אין תיאור זמין.'}</p>
578
  <div class="flex justify-between items-center mb-4">
579
  <div class="flex">
580
- ${renderRatingStars(tool.rating || 0)}
581
  </div>
582
- ${tool.isNew ? '<span class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">חדש!</span>' : ''}
583
  </div>
584
- <a href="${tool.url || '#'}" target="_blank" rel="noopener noreferrer" class="inline-block w-full text-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition ${!tool.url ? 'opacity-50 cursor-not-allowed' : ''}">
585
- <i class="fas fa-external-link-alt ml-2"></i> ${tool.url ? 'גישה לכלי' : 'אין קישור'}
586
  </a>
587
  `;
588
 
@@ -591,18 +640,18 @@ window.addEventListener('DOMContentLoaded', () => {
591
  }
592
  }
593
 
594
- // Helper to get YouTube ID (Keep as is)
595
  function getYouTubeID(url) {
596
- if (!url) return '';
597
  const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
598
  return match ? match[1] : '';
599
  }
600
 
601
- // Render videos (Keep mostly as is, just ensure data source is correct)
602
  function renderVideos() {
603
  if (!videosContainer || !toolsData || !Array.isArray(toolsData.videos)) {
604
  console.error("Cannot render videos: Missing container or invalid data.");
605
- return; // Prevent errors if data isn't ready or invalid
606
  }
607
 
608
  videosContainer.innerHTML = ''; // Clear previous videos
@@ -612,8 +661,7 @@ window.addEventListener('DOMContentLoaded', () => {
612
  } else {
613
  toolsData.videos.forEach(video => {
614
  const videoId = getYouTubeID(video.url);
615
- // If we can't get an ID, maybe skip or show a placeholder? For now, generate embed URL anyway.
616
- const embedUrl = videoId ? `https://www.youtube.com/embed/${videoId}` : '#'; // Use '#' or a placeholder URL if ID is missing
617
 
618
  const videoCard = document.createElement('div');
619
  videoCard.className = 'bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden';
@@ -622,16 +670,16 @@ window.addEventListener('DOMContentLoaded', () => {
622
  <div class="relative pt-[56.25%] ${!videoId ? 'bg-gray-200 flex items-center justify-center' : ''}">
623
  ${videoId ? `
624
  <iframe class="absolute top-0 left-0 w-full h-full" src="${embedUrl}" frameborder="0"
625
- title="${video.title || 'YouTube video'}"
626
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
627
  allowfullscreen></iframe>
628
  ` : `
629
- <p class="text-gray-500 text-sm">קישור וידאו לא תקין</p>
630
  `}
631
  </div>
632
  <div class="p-4">
633
- <h3 class="text-lg font-semibold mb-2">${video.title || 'כותרת חסרה'}</h3>
634
- <p class="text-gray-600 text-sm mb-3 min-h-[40px]">${video.description || 'אין תיאור זמין.'}</p>
635
  <p class="text-gray-500 text-xs">${formatDate(video.date)}</p>
636
  </div>
637
  `;
@@ -641,16 +689,18 @@ window.addEventListener('DOMContentLoaded', () => {
641
  }
642
  }
643
 
644
- // Filter tools (Modified slightly for safety)
645
  function filterTools() {
646
  if (!toolsData || !Array.isArray(toolsData.tools)) {
647
- return []; // Return empty array if data is not ready
648
  }
649
  return toolsData.tools.filter(tool => {
650
- const nameMatch = tool.name && tool.name.toLowerCase().includes(currentSearchTerm.toLowerCase());
651
- const descMatch = tool.description && tool.description.toLowerCase().includes(currentSearchTerm.toLowerCase());
652
- const categoryMatch = tool.category && tool.category.toLowerCase().includes(currentSearchTerm.toLowerCase()); // Optional: search category name too
653
- const matchesSearch = nameMatch || descMatch || categoryMatch;
 
 
654
 
655
  const matchesCategoryFilter = currentCategory === 'all' || tool.category === currentCategory;
656
 
@@ -658,7 +708,7 @@ window.addEventListener('DOMContentLoaded', () => {
658
  });
659
  }
660
 
661
- // Update statistics (Modified slightly for safety)
662
  function updateStats() {
663
  const toolsCount = (toolsData && Array.isArray(toolsData.tools)) ? toolsData.tools.length : 0;
664
  totalToolsElement.textContent = toolsCount;
@@ -673,121 +723,84 @@ window.addEventListener('DOMContentLoaded', () => {
673
  totalCategoriesElement.textContent = categories.size;
674
  }
675
 
676
- // Render rating stars (Keep as is)
677
  function renderRatingStars(rating) {
678
  let stars = '';
679
- const filledStars = Math.max(0, Math.min(5, Math.round(rating || 0))); // Ensure rating is between 0 and 5
680
  for (let i = 1; i <= 5; i++) {
681
  if (i <= filledStars) {
682
- stars += '<i class="fas fa-star text-yellow-400"></i>';
683
  } else {
684
- stars += '<i class="far fa-star text-gray-300"></i>'; // Use gray for empty stars
685
  }
686
  }
687
  return stars;
688
  }
689
 
690
- // Get category name (Keep as is, maybe add a default)
691
  function getCategoryName(category) {
692
  const categories = {
693
- 'productivity': 'פרודוקטיביות',
694
- 'writing': 'כתיבה',
695
- 'design': 'עיצוב',
696
- 'coding': 'תכנות',
697
- 'video': 'וידאו',
698
- 'image': 'תמונה',
699
- 'education': 'חינוך',
700
- 'data': 'נתונים',
701
- 'search': 'חיפוש',
702
- 'builder': 'בנייה',
703
- 'customer-support': 'תמיכה',
704
- 'automation': 'אוטומציה',
705
- 'hosting': 'אחסון',
706
- 'agents': 'סוכנים',
707
- 'directory': 'אינדקס',
708
- 'utility': 'כלי עזר',
709
- 'platform': 'פלטפורמה',
710
- 'media': 'מדיה',
711
- 'presentation': 'מצגות',
712
- 'audio': 'שמע',
713
- 'infrastructure': 'תשתיות',
714
- 'nlp': 'עיבוד שפה',
715
- 'accessibility': 'נגישות'
716
- // Add other categories from your JSON here
717
  };
718
- return categories[category] || category || 'כללי'; // Return category key or 'כללי' if null/undefined
719
  }
720
 
721
- // Get category color (Add more colors or a default)
722
  function getCategoryColor(category) {
723
  const colors = {
724
- 'productivity': 'bg-blue-600',
725
- 'writing': 'bg-purple-600',
726
- 'design': 'bg-pink-600',
727
- 'coding': 'bg-green-600',
728
- 'video': 'bg-red-600',
729
- 'image': 'bg-yellow-600',
730
- 'education': 'bg-indigo-600',
731
- 'data': 'bg-cyan-600',
732
- 'search': 'bg-teal-600',
733
- 'builder': 'bg-orange-600',
734
- 'customer-support': 'bg-lime-600',
735
- 'automation': 'bg-sky-600',
736
- 'hosting': 'bg-amber-600',
737
- 'agents': 'bg-violet-600',
738
- 'directory': 'bg-fuchsia-600',
739
- 'utility': 'bg-rose-600',
740
- 'platform': 'bg-emerald-600',
741
- 'media': 'bg-stone-600',
742
- 'presentation': 'bg-red-500',
743
- 'audio': 'bg-blue-500',
744
- 'infrastructure': 'bg-gray-700',
745
- 'nlp': 'bg-purple-500',
746
- 'accessibility': 'bg-green-500'
747
- // Add more as needed
748
  };
749
- return colors[category] || 'bg-gray-600'; // Default color
750
  }
751
 
752
- // Get category badge color (Add more or a default)
753
  function getCategoryBadgeColor(category) {
754
  const colors = {
755
- 'productivity': 'bg-blue-100 text-blue-800',
756
- 'writing': 'bg-purple-100 text-purple-800',
757
- 'design': 'bg-pink-100 text-pink-800',
758
- 'coding': 'bg-green-100 text-green-800',
759
- 'video': 'bg-red-100 text-red-800',
760
- 'image': 'bg-yellow-100 text-yellow-800',
761
- 'education': 'bg-indigo-100 text-indigo-800',
762
- 'data': 'bg-cyan-100 text-cyan-800',
763
- 'search': 'bg-teal-100 text-teal-800',
764
- 'builder': 'bg-orange-100 text-orange-800',
765
- 'customer-support': 'bg-lime-100 text-lime-800',
766
- 'automation': 'bg-sky-100 text-sky-800',
767
- 'hosting': 'bg-amber-100 text-amber-800',
768
- 'agents': 'bg-violet-100 text-violet-800',
769
- 'directory': 'bg-fuchsia-100 text-fuchsia-800',
770
- 'utility': 'bg-rose-100 text-rose-800',
771
- 'platform': 'bg-emerald-100 text-emerald-800',
772
- 'media': 'bg-stone-100 text-stone-800',
773
- 'presentation': 'bg-red-100 text-red-800',
774
- 'audio': 'bg-blue-100 text-blue-800',
775
- 'infrastructure': 'bg-gray-200 text-gray-800',
776
- 'nlp': 'bg-purple-100 text-purple-800',
777
- 'accessibility': 'bg-green-100 text-green-800'
778
- // Add more as needed
779
  };
780
- return colors[category] || 'bg-gray-100 text-gray-800'; // Default badge color
781
  }
782
 
783
- // Format date (Keep as is, maybe add error handling)
784
  function formatDate(dateString) {
785
  if (!dateString) return 'תאריך לא זמין';
786
  try {
787
- // Handle potential non-standard date formats if necessary
788
  const date = new Date(dateString);
789
- // Check if date is valid
790
  if (isNaN(date.getTime())) {
 
 
 
 
 
 
 
 
 
 
791
  return 'תאריך לא תקין';
792
  }
793
  const options = { year: 'numeric', month: 'long', day: 'numeric' };
@@ -799,12 +812,12 @@ window.addEventListener('DOMContentLoaded', () => {
799
  }
800
 
801
 
802
- // Setup event listeners (Update refresh button logic)
803
  function setupEventListeners() {
804
  // Search input
805
  searchInput.addEventListener('input', (e) => {
806
  currentSearchTerm = e.target.value;
807
- renderTools();
808
  });
809
 
810
  // Filter buttons
@@ -813,7 +826,7 @@ window.addEventListener('DOMContentLoaded', () => {
813
  filterButtons.forEach(btn => btn.classList.remove('active'));
814
  button.classList.add('active');
815
  currentCategory = button.dataset.category;
816
- renderTools();
817
  });
818
  });
819
 
@@ -822,45 +835,52 @@ window.addEventListener('DOMContentLoaded', () => {
822
  mobileMenu.classList.toggle('open');
823
  });
824
 
825
- // JSON Editor Modal (Keep as is, but consider removing buttons if not admin)
826
  const openEditorHandler = () => {
827
- // Default to showing tools first
828
- showToolsBtn.click(); // Simulate click to set initial state
829
- jsonEditorModal.classList.remove('hidden');
830
  };
831
 
832
- // If Edit buttons exist, add listeners
833
- // const editJsonBtn = document.getElementById('editJsonBtn');
834
- // const editJsonBtnMobile = document.getElementById('editJsonBtnMobile');
835
  // if (editJsonBtn) editJsonBtn.addEventListener('click', openEditorHandler);
836
  // if (editJsonBtnMobile) editJsonBtnMobile.addEventListener('click', openEditorHandler);
837
 
838
- closeModalBtn.addEventListener('click', () => {
839
- jsonEditorModal.classList.add('hidden');
840
- });
841
-
842
- cancelEditBtn.addEventListener('click', () => {
843
- jsonEditorModal.classList.add('hidden');
844
- });
845
 
846
  saveJsonBtn.addEventListener('click', () => {
847
  try {
848
- const newData = JSON.parse(jsonEditor.value);
 
 
849
  if (showToolsBtn.classList.contains('bg-blue-600')) {
850
- if (!Array.isArray(newData)) throw new Error("Data must be an array.");
851
- toolsData.tools = newData;
 
 
 
 
 
 
 
852
  } else {
853
- if (!Array.isArray(newData)) throw new Error("Data must be an array.");
854
- toolsData.videos = newData;
 
 
 
 
 
855
  }
856
- saveData(); // Save changes to localStorage
857
  renderTools();
858
- renderVideos(); // Re-render videos as well
859
  updateStats();
860
  jsonEditorModal.classList.add('hidden');
861
  alert("הנתונים נשמרו בהצלחה (באחסון המקומי).");
862
  } catch (e) {
863
- alert('JSON לא תקין: ' + e.message);
 
864
  }
865
  });
866
 
@@ -880,26 +900,52 @@ window.addEventListener('DOMContentLoaded', () => {
880
  jsonEditor.value = JSON.stringify(toolsData.videos, null, 2);
881
  });
882
 
883
- // Refresh buttons - Load default data from JSON files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  const refreshHandler = async () => {
885
- if (confirm("פעולה זו תחליף את כל הנתונים הנוכחיים (כולל שינויים שביצעת בעורך) בנתוני ברירת המחדל. האם להמשיך?")) {
886
  console.log("Refreshing data from default JSON files...");
887
- toolsData = await fetchDefaultData(); // Fetch the defaults
888
- saveData(); // Overwrite localStorage with defaults
889
- renderTools();
890
- renderVideos();
891
- updateStats();
892
- // Reset filters and search
893
- searchInput.value = '';
894
- currentSearchTerm = '';
895
- filterButtons.forEach(btn => btn.classList.remove('active'));
896
- const allFilterBtn = document.querySelector('.filter-btn[data-category="all"]');
897
- if (allFilterBtn) {
898
- allFilterBtn.classList.add('active');
899
- }
900
- currentCategory = 'all';
901
- renderTools(); // Render again with reset filters
902
- alert("הנתונים רועננו לערכי ברירת המחדל.");
 
 
 
 
 
 
 
 
 
 
903
  }
904
  };
905
 
@@ -910,27 +956,6 @@ window.addEventListener('DOMContentLoaded', () => {
910
  // Initialize the app when the DOM is ready
911
  document.addEventListener('DOMContentLoaded', init);
912
 
913
- // --- Keep the separate getYouTubeID and renderVideos for DOMContentLoaded if needed ---
914
- // Although the main init() now calls renderVideos after data loading,
915
- // keeping this might be a fallback or part of the original structure you wanted.
916
- // It's slightly redundant now if init() works correctly.
917
- /*
918
- function getYouTubeID(url) { // Already defined above, potentially remove this duplicate
919
- const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
920
- return match ? match[1] : '';
921
- }
922
-
923
- function renderVideos() { // Already defined above, potentially remove this duplicate
924
- // ... (implementation is identical to the one inside the main script block) ...
925
- }
926
-
927
- window.addEventListener('DOMContentLoaded', () => {
928
- // This might run before init() finishes loading data if init is slow.
929
- // It's safer to rely on init() calling renderVideos.
930
- // renderVideos();
931
- });
932
- */
933
-
934
  </script>
935
  </body>
936
  </html>
 
 
1
  <!--
2
  https://huggingface.co/spaces/theWitcher/sagi-ai-tools
3
  https://huggingface.co/spaces/theWitcher/sagi-ai-tools/blob/main/index.html
 
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
  <title>ארגז הכלים שלי לבינה מלאכותית</title>
13
+ <!-- Tailwind CDN Script is loaded dynamically in the <script> tag below for clarity -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
15
  <style>
16
  @import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600;700&display=swap');
 
166
  border-radius: 50%;
167
  opacity: 0.3;
168
  }
169
+ /* Added cursor style for clickable stats */
170
+ .clickable-stat:hover {
171
+ cursor: pointer;
172
+ background-color: #f3f4f6; /* Slightly lighter gray on hover */
173
+ }
174
  </style>
175
  </head>
176
  <body class="min-h-screen">
 
194
  <button id="refreshBtn" class="px-4 py-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition">
195
  <i class="fas fa-sync-alt ml-2"></i> רענן
196
  </button>
197
+ <!-- Admin Edit Button - Uncomment if needed -->
198
  <!-- <button id="editJsonBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition relative">
199
  <i class="fas fa-edit ml-2"></i> ערוך JSON
200
  <span class="admin-badge">A</span>
 
208
  <button id="refreshBtnMobile" class="px-4 py-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition text-right">
209
  <i class="fas fa-sync-alt ml-2"></i> רענן
210
  </button>
211
+ <!-- Admin Edit Button (Mobile) - Uncomment if needed -->
212
+ <!-- <button id="editJsonBtnMobile" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-right relative">
213
  <i class="fas fa-edit ml-2"></i> ערוך JSON
214
  <span class="admin-badge">A</span>
215
+ </button> -->
216
  </div>
217
  </div>
218
  </div>
 
290
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="design">עיצוב</button>
291
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="coding">תכנות</button>
292
  <button class="filter-btn px-3 py-1 rounded-full border border-gray-300 text-sm hover:bg-gray-100 transition" data-category="video">וידאו</button>
293
+
294
  </div>
295
  </div>
296
  </div>
 
309
  </div>
310
  </div>
311
  </div>
312
+ <!-- Clickable Stat Box for Top Rated -->
313
+ <div id="topRatedStatBox" class="bg-white p-4 rounded-lg shadow-sm border border-gray-100 transition clickable-stat">
314
  <div class="flex items-center">
315
  <div class="p-3 rounded-full bg-purple-50 text-purple-600 ml-3">
316
  <i class="fas fa-star text-lg"></i>
 
321
  </div>
322
  </div>
323
  </div>
324
+ <!-- Clickable Stat Box for New Tools -->
325
+ <div id="newToolsStatBox" class="bg-white p-4 rounded-lg shadow-sm border border-gray-100 transition clickable-stat">
326
  <div class="flex items-center">
327
  <div class="p-3 rounded-full bg-green-50 text-green-600 ml-3">
328
  <i class="fas fa-bolt text-lg"></i>
 
364
  <div id="jsonEditorModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
365
  <div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
366
  <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
367
+ <h3 class="text-lg font-semibold">עריכת הנתונים ב-JSON</h3>
368
  <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
369
  <i class="fas fa-times"></i>
370
  </button>
 
374
  <button id="showToolsBtn" class="px-4 py-2 bg-blue-600 text-white rounded-l-lg">כלים</button>
375
  <button id="showVideosBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-r-lg">סרטונים</button>
376
  </div>
377
+ <textarea id="jsonEditor" class="w-full h-96 p-4 border border-gray-300 rounded-lg font-mono text-sm spellcheck="false" style="direction: ltr; text-align: left;"></textarea>
378
  </div>
379
  <div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3 space-x-reverse">
380
  <button id="cancelEditBtn" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition">
381
  ביטול
382
  </button>
383
  <button id="saveJsonBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
384
+ שמור שינויים (באחסון המקומי)
385
  </button>
386
  </div>
387
  </div>
 
392
  <div class="container mx-auto px-4">
393
  <div class="flex flex-col md:flex-row justify-between items-center">
394
  <div class="text-gray-600 mb-4 md:mb-0">
395
+ © 2024 ארגז הכלים שלי ל-AI. כל הזכויות שמורות לשגיא בר און.
396
  </div>
397
  <div class="flex space-x-4 space-x-reverse">
398
+ <a href="https://www.youtube.com/@SAGIBARON" target="_blank" rel="noopener" class="text-gray-500 hover:text-red-600 transition">
399
  <i class="fab fa-youtube text-xl"></i>
400
  </a>
401
+ <a href="https://www.facebook.com/SAGI.BARON" target="_blank" rel="noopener" class="text-gray-500 hover:text-blue-800 transition">
402
  <i class="fab fa-facebook text-xl"></i>
403
  </a>
404
+ <a href="http://www.linkedin.com/in/sagi-bar-on" target="_blank" rel="noopener" class="text-gray-500 hover:text-blue-700 transition">
405
  <i class="fab fa-linkedin text-xl"></i>
406
  </a>
407
+ <a href="https://chat.whatsapp.com/GPFASYBEA9CFGUMCVZ5RXP" target="_blank" rel="noopener" class="text-gray-500 hover:text-green-600 transition">
408
  <i class="fab fa-whatsapp text-xl"></i>
409
  </a>
410
  </div>
 
412
  </div>
413
  </footer>
414
 
415
+ <!-- Load Tailwind CSS via CDN -->
416
+ <script src="https://cdn.tailwindcss.com"></script>
417
+ <script>
418
  // DOM Elements
 
419
  const toolsContainer = document.getElementById('toolsContainer');
420
  const videosContainer = document.getElementById('videosContainer');
421
  const searchInput = document.getElementById('searchInput');
 
433
  const saveJsonBtn = document.getElementById('saveJsonBtn');
434
  const showToolsBtn = document.getElementById('showToolsBtn');
435
  const showVideosBtn = document.getElementById('showVideosBtn');
 
 
436
  const refreshBtn = document.getElementById('refreshBtn');
437
  const refreshBtnMobile = document.getElementById('refreshBtnMobile');
438
+ // Clickable stat boxes
439
+ const topRatedStatBox = document.getElementById('topRatedStatBox');
440
+ const newToolsStatBox = document.getElementById('newToolsStatBox');
441
+ // Admin Buttons (Uncomment if needed)
442
+ // const editJsonBtn = document.getElementById('editJsonBtn');
443
+ // const editJsonBtnMobile = document.getElementById('editJsonBtnMobile');
444
 
445
  // State
446
  let currentCategory = 'all';
447
  let currentSearchTerm = '';
448
  let toolsData = { tools: [], videos: [] }; // Initialize empty
449
+ let currentSort = 'newest'; // 'newest', 'rating', potentially others
450
 
451
  // Initialize - Make it async
452
  async function init() {
453
  await loadData(); // Wait for data to load
454
+ sortTools(currentSort); // Apply default sort ('newest')
455
  renderTools();
456
+ renderVideos();
457
  updateStats();
458
  setupEventListeners();
459
  // Set initial active filter button
 
467
  async function fetchDefaultData() {
468
  try {
469
  const [toolsResponse, videosResponse] = await Promise.all([
470
+ fetch('tools.json?cacheBust=' + Date.now()), // Add cache busting
471
+ fetch('videos.json?cacheBust=' + Date.now()) // Add cache busting
472
  ]);
473
 
474
  if (!toolsResponse.ok || !videosResponse.ok) {
 
480
  videosResponse.json()
481
  ]);
482
 
483
+ // Ensure basic structure and add default values if missing
484
+ const processedTools = toolsArray.map(tool => ({
485
+ ...tool,
486
+ rating: tool.rating ?? 0, // Default rating if missing
487
+ isNew: tool.isNew ?? false, // Default isNew if missing
488
+ category: tool.category ?? 'general', // Default category
489
+ icon: tool.icon || 'fas fa-tools', // Default icon
490
+ url: tool.url || '#', // Default URL
491
+ description: tool.description || 'אין תיאור זמין.',
492
+ name: tool.name || 'שם לא ידוע'
493
+ }));
494
+
495
+ const processedVideos = videosArray.map(video => ({
496
+ ...video,
497
+ url: video.url || '#',
498
+ title: video.title || 'כותרת חסרה',
499
+ description: video.description || 'אין תיאור זמין.',
500
+ date: video.date ?? new Date().toISOString() // Default date if missing
501
+ }));
502
+
503
+ return { tools: processedTools, videos: processedVideos };
504
  } catch (error) {
505
  console.error("Failed to fetch default data:", error);
506
+ alert("שגיאה בטעינת נתוני ברירת המחדל. ייתכן שהקבצים חסרים או שגויים. נסה לרענן את הדף.");
507
  return { tools: [], videos: [] }; // Return empty structure on error
508
  }
509
  }
 
519
  console.warn("Invalid data structure in localStorage. Fetching defaults.");
520
  toolsData = await fetchDefaultData();
521
  saveData(); // Save the fetched defaults
522
+ } else {
523
+ // Ensure loaded tools have default values too
524
+ toolsData.tools = toolsData.tools.map(tool => ({
525
+ ...tool,
526
+ rating: tool.rating ?? 0,
527
+ isNew: tool.isNew ?? false,
528
+ category: tool.category ?? 'general',
529
+ icon: tool.icon || 'fas fa-tools',
530
+ url: tool.url || '#',
531
+ description: tool.description || 'אין תיאור זמין.',
532
+ name: tool.name || 'שם לא ידוע'
533
+ }));
534
+ toolsData.videos = toolsData.videos.map(video => ({
535
+ ...video,
536
+ url: video.url || '#',
537
+ title: video.title || 'כותרת חסרה',
538
+ description: video.description || 'אין תיאור זמין.',
539
+ date: video.date ?? new Date().toISOString()
540
+ }));
541
  }
542
  } catch (e) {
543
  console.error("Error parsing data from localStorage:", e);
 
560
  }
561
  }
562
 
563
+ // Sorting function
564
+ function sortTools(sortBy) {
565
+ if (!toolsData || !Array.isArray(toolsData.tools)) return;
566
+ console.log("Sorting by:", sortBy);
567
+ currentSort = sortBy; // Update the current sort state
568
+
569
+ toolsData.tools.sort((a, b) => {
570
+ if (sortBy === 'newest') {
571
+ // Sort by isNew (true first)
572
+ const aIsNew = a.isNew || false;
573
+ const bIsNew = b.isNew || false;
574
+ if (aIsNew !== bIsNew) {
575
+ return bIsNew - aIsNew; // true (1) comes before false (0)
576
+ }
577
+ // Optional: secondary sort by name if newness is the same
578
+ // return (a.name || '').localeCompare(b.name || '', 'he');
579
+ return 0; // Keep original relative order if newness is the same
580
+ } else if (sortBy === 'rating') {
581
+ // Sort by rating (descending)
582
+ const aRating = a.rating || 0;
583
+ const bRating = b.rating || 0;
584
+ if (bRating !== aRating) {
585
+ return bRating - aRating; // Higher rating first
586
+ }
587
+ // Optional: secondary sort by name if rating is the same
588
+ // return (a.name || '').localeCompare(b.name || '', 'he');
589
+ return 0; // Keep original relative order if rating is the same
590
+ }
591
+ // Add other sort criteria here if needed
592
+ return 0; // Default: no change in order
593
+ });
594
+ // console.log("First few items after sort:", toolsData.tools.slice(0,3));
595
+ }
596
+
597
+ // Render tools based on filters
598
  function renderTools() {
599
  if (!toolsContainer || !toolsData || !Array.isArray(toolsData.tools)) {
600
  console.error("Cannot render tools: Missing container or invalid data.");
601
  return;
602
  }
603
+ const filteredTools = filterTools(); // Gets filtered tools based on current search/category
604
 
605
  toolsContainer.innerHTML = ''; // Clear previous tools
606
 
607
  if (filteredTools.length === 0) {
608
+ toolsContainer.innerHTML = '<p class="text-center text-gray-500 col-span-full">לא נמצאו כלים התואמים את החיפוש, הסינון והמיון.</p>';
609
  } else {
610
  filteredTools.forEach(tool => {
611
  const toolCard = document.createElement('div');
612
+ // Added relative positioning and moved badge location
613
  toolCard.className = `relative tool-card bg-white rounded-lg shadow-sm border border-gray-100 p-6 transition duration-300 ${tool.isFeatured ? 'ring-2 ring-blue-500' : ''}`;
614
 
615
  toolCard.innerHTML = `
616
  <div class="flex items-start mb-4">
617
  <div class="p-3 rounded-lg ${getCategoryColor(tool.category)} text-white mr-4 flex-shrink-0">
618
+ <i class="${tool.icon} text-xl"></i>
619
  </div>
620
  <div class="flex-grow">
621
+ <h3 class="text-xl font-semibold">${tool.name}</h3>
622
+ <span class="text-xs px-2 py-1 rounded-full ${getCategoryBadgeColor(tool.category)}">${getCategoryName(tool.category)}</span>
623
  </div>
624
+ ${tool.isNew ? '<span class="absolute top-2 left-2 bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">חדש!</span>' : ''}
625
  </div>
626
+ <p class="text-gray-700 mb-4 text-sm min-h-[60px]">${tool.description}</p>
627
  <div class="flex justify-between items-center mb-4">
628
  <div class="flex">
629
+ ${renderRatingStars(tool.rating)}
630
  </div>
631
+
632
  </div>
633
+ <a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="inline-block w-full text-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition ${tool.url === '#' ? 'opacity-50 cursor-not-allowed' : ''}">
634
+ <i class="fas fa-external-link-alt ml-2"></i> ${tool.url !== '#' ? 'גישה לכלי' : 'אין קישור'}
635
  </a>
636
  `;
637
 
 
640
  }
641
  }
642
 
643
+ // Helper to get YouTube ID
644
  function getYouTubeID(url) {
645
+ if (!url || url === '#') return '';
646
  const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
647
  return match ? match[1] : '';
648
  }
649
 
650
+ // Render videos
651
  function renderVideos() {
652
  if (!videosContainer || !toolsData || !Array.isArray(toolsData.videos)) {
653
  console.error("Cannot render videos: Missing container or invalid data.");
654
+ return;
655
  }
656
 
657
  videosContainer.innerHTML = ''; // Clear previous videos
 
661
  } else {
662
  toolsData.videos.forEach(video => {
663
  const videoId = getYouTubeID(video.url);
664
+ const embedUrl = videoId ? `https://www.youtube.com/embed/${videoId}` : '#';
 
665
 
666
  const videoCard = document.createElement('div');
667
  videoCard.className = 'bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden';
 
670
  <div class="relative pt-[56.25%] ${!videoId ? 'bg-gray-200 flex items-center justify-center' : ''}">
671
  ${videoId ? `
672
  <iframe class="absolute top-0 left-0 w-full h-full" src="${embedUrl}" frameborder="0"
673
+ title="${video.title}"
674
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
675
  allowfullscreen></iframe>
676
  ` : `
677
+ <p class="text-gray-500 text-sm p-4">${video.url === '#' ? 'אין ��ישור וידאו' : 'קישור וידאו לא תקין'}</p>
678
  `}
679
  </div>
680
  <div class="p-4">
681
+ <h3 class="text-lg font-semibold mb-2">${video.title}</h3>
682
+ <p class="text-gray-600 text-sm mb-3 min-h-[40px]">${video.description}</p>
683
  <p class="text-gray-500 text-xs">${formatDate(video.date)}</p>
684
  </div>
685
  `;
 
689
  }
690
  }
691
 
692
+ // Filter tools (operates on the current toolsData.tools which might be sorted)
693
  function filterTools() {
694
  if (!toolsData || !Array.isArray(toolsData.tools)) {
695
+ return [];
696
  }
697
  return toolsData.tools.filter(tool => {
698
+ const searchTermLower = currentSearchTerm.toLowerCase();
699
+ const nameMatch = tool.name.toLowerCase().includes(searchTermLower);
700
+ const descMatch = tool.description.toLowerCase().includes(searchTermLower);
701
+ // Optional: search category name too (using the display name)
702
+ const categoryNameMatch = getCategoryName(tool.category).toLowerCase().includes(searchTermLower);
703
+ const matchesSearch = nameMatch || descMatch || categoryNameMatch;
704
 
705
  const matchesCategoryFilter = currentCategory === 'all' || tool.category === currentCategory;
706
 
 
708
  });
709
  }
710
 
711
+ // Update statistics
712
  function updateStats() {
713
  const toolsCount = (toolsData && Array.isArray(toolsData.tools)) ? toolsData.tools.length : 0;
714
  totalToolsElement.textContent = toolsCount;
 
723
  totalCategoriesElement.textContent = categories.size;
724
  }
725
 
726
+ // Render rating stars
727
  function renderRatingStars(rating) {
728
  let stars = '';
729
+ const filledStars = Math.max(0, Math.min(5, Math.round(rating)));
730
  for (let i = 1; i <= 5; i++) {
731
  if (i <= filledStars) {
732
+ stars += '<i class="fas fa-star text-yellow-400 ml-1"></i>'; // Added margin
733
  } else {
734
+ stars += '<i class="far fa-star text-gray-300 ml-1"></i>'; // Added margin
735
  }
736
  }
737
  return stars;
738
  }
739
 
740
+ // Get category name
741
  function getCategoryName(category) {
742
  const categories = {
743
+ 'productivity': 'פרודוקטיביות', 'writing': 'כתיבה', 'design': 'עיצוב',
744
+ 'coding': 'תכנות', 'video': 'וידאו', 'image': 'תמונה',
745
+ 'education': 'חינוך', 'data': 'נתונים', 'search': 'חיפוש',
746
+ 'builder': 'בנייה', 'customer-support': 'תמיכה', 'automation': 'אוטומציה',
747
+ 'hosting': 'אחסון', 'agents': 'סוכנים', 'directory': 'אינדקס',
748
+ 'utility': 'כלי עזר', 'platform': 'פלטפורמה', 'media': 'מדיה',
749
+ 'presentation': 'מצגות', 'audio': 'שמע', 'infrastructure': 'תשתיות',
750
+ 'nlp': 'עיבוד שפה', 'accessibility': 'נגישות',
751
+ 'general': 'כללי' // Default category name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
  };
753
+ return categories[category] || category || 'כללי';
754
  }
755
 
756
+ // Get category color
757
  function getCategoryColor(category) {
758
  const colors = {
759
+ 'productivity': 'bg-blue-600', 'writing': 'bg-purple-600', 'design': 'bg-pink-600',
760
+ 'coding': 'bg-green-600', 'video': 'bg-red-600', 'image': 'bg-yellow-600',
761
+ 'education': 'bg-indigo-600', 'data': 'bg-cyan-600', 'search': 'bg-teal-600',
762
+ 'builder': 'bg-orange-600', 'customer-support': 'bg-lime-600', 'automation': 'bg-sky-600',
763
+ 'hosting': 'bg-amber-600', 'agents': 'bg-violet-600', 'directory': 'bg-fuchsia-600',
764
+ 'utility': 'bg-rose-600', 'platform': 'bg-emerald-600', 'media': 'bg-stone-600',
765
+ 'presentation': 'bg-red-500', 'audio': 'bg-blue-500', 'infrastructure': 'bg-gray-700',
766
+ 'nlp': 'bg-purple-500', 'accessibility': 'bg-green-500',
767
+ 'general': 'bg-gray-600' // Default color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  };
769
+ return colors[category] || 'bg-gray-600';
770
  }
771
 
772
+ // Get category badge color
773
  function getCategoryBadgeColor(category) {
774
  const colors = {
775
+ 'productivity': 'bg-blue-100 text-blue-800', 'writing': 'bg-purple-100 text-purple-800', 'design': 'bg-pink-100 text-pink-800',
776
+ 'coding': 'bg-green-100 text-green-800', 'video': 'bg-red-100 text-red-800', 'image': 'bg-yellow-100 text-yellow-800',
777
+ 'education': 'bg-indigo-100 text-indigo-800', 'data': 'bg-cyan-100 text-cyan-800', 'search': 'bg-teal-100 text-teal-800',
778
+ 'builder': 'bg-orange-100 text-orange-800', 'customer-support': 'bg-lime-100 text-lime-800', 'automation': 'bg-sky-100 text-sky-800',
779
+ 'hosting': 'bg-amber-100 text-amber-800', 'agents': 'bg-violet-100 text-violet-800', 'directory': 'bg-fuchsia-100 text-fuchsia-800',
780
+ 'utility': 'bg-rose-100 text-rose-800', 'platform': 'bg-emerald-100 text-emerald-800', 'media': 'bg-stone-100 text-stone-800',
781
+ 'presentation': 'bg-red-100 text-red-800', 'audio': 'bg-blue-100 text-blue-800', 'infrastructure': 'bg-gray-200 text-gray-800',
782
+ 'nlp': 'bg-purple-100 text-purple-800', 'accessibility': 'bg-green-100 text-green-800',
783
+ 'general': 'bg-gray-100 text-gray-800' // Default badge color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  };
785
+ return colors[category] || 'bg-gray-100 text-gray-800';
786
  }
787
 
788
+ // Format date
789
  function formatDate(dateString) {
790
  if (!dateString) return 'תאריך לא זמין';
791
  try {
 
792
  const date = new Date(dateString);
 
793
  if (isNaN(date.getTime())) {
794
+ console.warn("Could not parse date:", dateString);
795
+ // Attempt common formats if needed, e.g., DD/MM/YYYY
796
+ const parts = dateString.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
797
+ if(parts) {
798
+ // Assuming DD/MM/YYYY - adjust if MM/DD/YYYY is possible
799
+ const isoDate = new Date(`${parts[3]}-${parts[2].padStart(2, '0')}-${parts[1].padStart(2, '0')}T00:00:00Z`);
800
+ if (!isNaN(isoDate.getTime())) {
801
+ return isoDate.toLocaleDateString('he-IL', { year: 'numeric', month: 'long', day: 'numeric' });
802
+ }
803
+ }
804
  return 'תאריך לא תקין';
805
  }
806
  const options = { year: 'numeric', month: 'long', day: 'numeric' };
 
812
  }
813
 
814
 
815
+ // Setup event listeners
816
  function setupEventListeners() {
817
  // Search input
818
  searchInput.addEventListener('input', (e) => {
819
  currentSearchTerm = e.target.value;
820
+ renderTools(); // Re-render with current sort order applied to filtered results
821
  });
822
 
823
  // Filter buttons
 
826
  filterButtons.forEach(btn => btn.classList.remove('active'));
827
  button.classList.add('active');
828
  currentCategory = button.dataset.category;
829
+ renderTools(); // Re-render with current sort order applied to newly filtered results
830
  });
831
  });
832
 
 
835
  mobileMenu.classList.toggle('open');
836
  });
837
 
838
+ // JSON Editor Modal Logic (Admin feature, ensure buttons are uncommented if needed)
839
  const openEditorHandler = () => {
840
+ showToolsBtn.click(); // Default to showing tools
841
+ jsonEditorModal.classList.remove('hidden');
 
842
  };
843
 
844
+ // Uncomment these lines if the admin edit buttons are present in the HTML
 
 
845
  // if (editJsonBtn) editJsonBtn.addEventListener('click', openEditorHandler);
846
  // if (editJsonBtnMobile) editJsonBtnMobile.addEventListener('click', openEditorHandler);
847
 
848
+ closeModalBtn.addEventListener('click', () => jsonEditorModal.classList.add('hidden'));
849
+ cancelEditBtn.addEventListener('click', () => jsonEditorModal.classList.add('hidden'));
 
 
 
 
 
850
 
851
  saveJsonBtn.addEventListener('click', () => {
852
  try {
853
+ const rawData = JSON.parse(jsonEditor.value);
854
+ if (!Array.isArray(rawData)) throw new Error("Data must be an array.");
855
+
856
  if (showToolsBtn.classList.contains('bg-blue-600')) {
857
+ // Add default values when saving tools JSON
858
+ toolsData.tools = rawData.map(tool => ({
859
+ ...tool,
860
+ rating: tool.rating ?? 0, isNew: tool.isNew ?? false,
861
+ category: tool.category ?? 'general', icon: tool.icon || 'fas fa-tools',
862
+ url: tool.url || '#', description: tool.description || 'אין תיאור זמין.',
863
+ name: tool.name || 'שם לא ידוע'
864
+ }));
865
+ sortTools(currentSort); // Re-apply current sort after editing
866
  } else {
867
+ // Add default values when saving videos JSON
868
+ toolsData.videos = rawData.map(video => ({
869
+ ...video, url: video.url || '#', title: video.title || 'כותרת חסרה',
870
+ description: video.description || 'אין תיאור זמין.',
871
+ date: video.date ?? new Date().toISOString()
872
+ }));
873
+ // No sorting needed for videos currently
874
  }
875
+ saveData();
876
  renderTools();
877
+ renderVideos();
878
  updateStats();
879
  jsonEditorModal.classList.add('hidden');
880
  alert("הנתונים נשמרו בהצלחה (באחסון המקומי).");
881
  } catch (e) {
882
+ alert('JSON לא תקין או שגיאה בשמירה:\n' + e.message);
883
+ console.error("JSON Save Error:", e);
884
  }
885
  });
886
 
 
900
  jsonEditor.value = JSON.stringify(toolsData.videos, null, 2);
901
  });
902
 
903
+ // Event listeners for sorting stat boxes
904
+ if (topRatedStatBox) {
905
+ topRatedStatBox.addEventListener('click', () => {
906
+ console.log("Top Rated box clicked");
907
+ sortTools('rating');
908
+ renderTools(); // Re-render the tools list with the new sorting
909
+ });
910
+ }
911
+ if (newToolsStatBox) {
912
+ newToolsStatBox.addEventListener('click', () => {
913
+ console.log("New Tools box clicked");
914
+ sortTools('newest');
915
+ renderTools(); // Re-render the tools list with the new sorting
916
+ });
917
+ }
918
+
919
+ // Refresh buttons - Load default data AND reset sort/filters
920
  const refreshHandler = async () => {
921
+ if (confirm("פעולה זו תחליף את כל הנתונים הנוכחיים (כולל שינויים שביצעת בעורך אם קיימים) בנתוני ברירת המחדל מהשרת. האם להמשיך?")) {
922
  console.log("Refreshing data from default JSON files...");
923
+ try {
924
+ toolsData = await fetchDefaultData(); // Fetch the defaults
925
+ saveData(); // Overwrite localStorage with defaults
926
+
927
+ // Reset filters and search
928
+ searchInput.value = '';
929
+ currentSearchTerm = '';
930
+ filterButtons.forEach(btn => btn.classList.remove('active'));
931
+ const allFilterBtn = document.querySelector('.filter-btn[data-category="all"]');
932
+ if (allFilterBtn) {
933
+ allFilterBtn.classList.add('active');
934
+ }
935
+ currentCategory = 'all';
936
+
937
+ // Reset sort to newest and re-render
938
+ sortTools('newest'); // Ensure default sort is applied
939
+ renderTools(); // Render sorted and reset tools
940
+ renderVideos(); // Render videos
941
+ updateStats(); // Update stats based on new data
942
+
943
+ alert("הנתונים רועננו לערכי ברירת המחדל.");
944
+
945
+ } catch (error) {
946
+ alert("שגיאה ברענון הנתונים. בדוק את חיבור האינטרנט או נסה שוב מאוחר יותר.");
947
+ console.error("Refresh error:", error);
948
+ }
949
  }
950
  };
951
 
 
956
  // Initialize the app when the DOM is ready
957
  document.addEventListener('DOMContentLoaded', init);
958
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
959
  </script>
960
  </body>
961
  </html>